引入

最近去四季花海和社团的同好一起约拍了,收获了不少满意的照片。在一如既往地调色,上传以后,突然想到是时候为自己的照片添加水印了。然而,市面上很多实现类似的简单功能的在线工具或者软件,往往存在安全风险,比如照片被盗用,或者捆绑销售垃圾软件。总之,会给使用者带来许多隐患。再加上上半学期刚刚学习了高级图像处理,因此我打算自己写一个小程序来解决这个需求。

考虑到暑假有一些使用C++ Wxwidges、OpenCV进行图像处理的编程经验,以及C++在编译时可以直接生成可执行文件的特点,我决定使用C++进行开发。

当然,由于没有进行过系统的学习或者文档阅读,我事实上并不具备,在没有资料参考的情况下直接开发出这个程序的能力。好在时代变了,Chat GPT可以作为一个随时对我耐心指导的老师。

源码链接

程序已经上传到了Github上,链接如下:

https://github.com/517pacifikal/ImageWatermarkAdding

核心源码分析

代码的大部分内容是对GUI的处理,但是业务的真正核心:对像素点的合并,所占的代码量并不大。

我们将需求直接聚焦到下面的问题上:

如何将一个水印图片,在一定透明度的条件下合并到一张图片的右下角呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 如果图像不是4通道(含有alpha通道)的图像,将其转换为4通道图像
if (image.channels() == 3) {
cv::cvtColor(image, image, cv::COLOR_BGR2BGRA);
}

int posX = image.cols - watermarkImage.cols - 10;
int posY = image.rows - watermarkImage.rows - 10;

float watermarkAlpha = 0.65; // 设置65%的不透明度
// 合并水印和图像像素
for (int y = 0; y < watermarkImage.rows; ++y) {
for (int x = 0; x < watermarkImage.cols; ++x) {

cv::Vec4b& pixel = watermarkImage.at<cv::Vec4b>(y, x); // 水印图像像素值
cv::Vec4b& imagePixel = image.at<cv::Vec4b>(posY + y, posX + x); // 目标图像像素值

// 计算水印的透明度(根据图像顺序设置固定的水印透明度)
float alpha = pixel[3] / 255.0 * watermarkAlpha;

// 合并水印和图像的每个通道像素值(RGB通道)
for (int c = 0; c < 3; ++c) {
// 根据透明度调整水印和图像像素值的混合比例
imagePixel[c] = static_cast<uchar>(
imagePixel[c] * (1.0 - alpha) + pixel[c] * alpha
);
}
}
}

上面的代码要点如下:

  1. 为了让水印具有一定的透明度,我们需要将原图片从RGB空间转换到RGBA空间。
1
cv::cvtColor(image, image, cv::COLOR_BGR2BGRA);

其中,A指的是alpha通道,用来修改图片的透明度。图像的 Alpha 通道范围通常是从 0 到 255。这个值表示每个像素的不透明度或透明度,其中 0 表示完全透明,255 表示完全不透明。
2. 使用双层循环为图片添加水印。

双层循环是循环水印图片的行与列,在每次循环中,将水印与图片的像素值的RGB三个通道进行合并。同时也要考虑到水印透明度的问题。
3. 对透明度alpha进行适当的处理。
1. 首先根据定义的透明度(一个值域在[0,1]区间的值),对水印的alpha通道进行调整。

1
float alpha = pixel[3] / 255.0 * watermarkAlpha;
  1. 之后,将图片的alpha调整为水印alpha的补,以保证图片与水印像素值混合后,其比例是正常的。
1
2
3
imagePixel[c] = static_cast<uchar>(
imagePixel[c] * (1.0 - alpha) + pixel[c] * alpha
);

static_cast<uchar> 用于将混合后的像素值从浮点数类型转换为 uchar 类型,以便存储到图像的像素通道中。这样做是为了确保最终生成的图像像素值处于正确的范围(0 到 255),以便与 OpenCV 中的图像处理函数和数据类型兼容。

效果

1.程序的使用流程

2.处理之前的图片

3.处理之后的图片

总结

实现了上面的程序后,我迫不及待地为图片都添加了水印。当然,作为自用程序,它并不算是健壮,友好的程序,仍然存在一些问题,比如:

  • 对用户不友好。

    在对大量图片进行处理时,会执行较长时间,而在此期间用户无法与程序进行交互,也难以获得程序的进度反馈。

  • 水印是以初始大小添加到图片上的,并没有进行二次缩放。

    这意味着,假如我处理的图片分辨率极其高,那么水印的相对大小可能会很小。反之,假如我处理的图片分辨率较低,那么水印的相对大小可能会很大,甚至喧宾夺主。最好是添加一些判定来保证图片与水印的大小比例在一个合理的区间中。

  • 对于各种我预期到的与我预期不到的异常情况没有进行充分测试,健壮性不足。

当然,这只是我基于自己的需求进行的一次敏捷开发,不对上面的问题展开讨论,只是因为性价比较低。