作为xlang的项目示例之一, 这篇博文主要讲制作一个简单的隐形水印工具。
隐形水印就是把一些信息秘密地嵌入到音频、视频、图片等载体中,用户角度上看不到,但可以使用特定算法检测出来,用户传播媒体,该水印也不会消失,这样可以实现追踪。
本文主要讲通过fftw在图片频域嵌入水印。
FFTW是用做快速傅里叶变换的库,关于使用傅里叶变换进行时频域转换这里做大概介绍,时域和频域是信号的基本性质,图像也是一种信号,用户角度观察到的是时域图像,而使用fftw则可得到频域图像,我们的水印就是在图像信号的频域进行处理,想了解时频域相关详细资料自行查询, 这里不多做解释(不了解详细也不影响,知道这个大概原理即可)。
开发原理: 使用fftw库对图像的数据进行变换,然后对变换后的数据进行处理,最后还原图像。
需要支持文字图像或者自定义的图案,并且需要手动调节频域信号的能量,使之具有一定的抗攻击性。
接下来使用XStudio新建功能创建一个form应用.
并编辑ui界面文件(xlang使用了qt作为基本界面库, 所以编辑界面是直接使用qtdesigner的,还是比较方便的)。
编辑成最终想要的样子:
然后在SecretWatermark.xcs中定义一系列控件和事件关联,详细过程略去,需要的在文章末尾下载源代码自行查看。
紧接着我们给项目引入fftw包,点击菜单【工具】-》【包管理】,找到并选中 xfftw 然后点击添加到项目,如下图:
我们需要得到图像的像素数据,在这里使用QXImage 类加载图片, 然后使用QXImage的getdata方法即可获得像素数据:
QXImage tmp = nilptr;
try{
// 从文件加载图片
tmp = new QXImage(file, nilptr);
}catch(Exception e){
// 加载出错了
return tmp;
}
//获取像素数据
byte [] data = tmp.getdata();
//图片宽
int w = tmp.width();
//图片高
int h = tmp.height();
然后对所有像素按字节进行归一化, 处理255.f 转为 0~1的浮点数。
// 像素数据是ARGB8888的 所以一个像素有四个字节,则 length / 4是像素总数
int l = data.length / 4;
//读取数据并归一化
double []r = new double[l];
double []g = new double[l];
double []b = new double[l];
for (int i = 0; i < l; i ++){
r[i] = data[(i * 4) + 2] / 255.f;
g[i] = data[(i * 4) + 1] / 255.f;
b[i] = data[(i * 4) + 0] / 255.f;
}
把所有数据归一化放到一个double的数组中去,进行傅里叶变换:
// 傅里叶正变换
double[][] rf = fftw.fft2(h, w, r, nilptr);
double[][] gf = fftw.fft2(h, w, g, nilptr);
double[][] bf = fftw.fft2(h, w, b, nilptr);
同样,把水印的图像也使用QXImage加载并取得其像素数据, 得到一个byte数组。
QXImage wmpic = nilptr;
try{
// 从文件加载水印图片
wmpic = new QXImage(markfile, nilptr);
}catch(Exception e){
// 加载出错了
QXMessageBox.Critical("注意","无效的图像文件:" + markfile,QXMessageBox.Ok, QXMessageBox.Ok);
return ;
}
//获取像素数据
byte [] wm = water.getdata();
//图片宽
int mw = water.width();
//图片高
int mh = water.height();
然后将水印的byte数组中的像素数据直接与频域double数组进行混合.
// pf 是水印能量系数
for (int x = 0; x < mw; x++){
for (int y = 0; y < mh; y++){
int ry = h - (y + sh + 1);
int rx = w - (x + st + 1);
int mi = y * mw + x;
int oi = ((y + sh)* w) + x + st;
int ni = (ry * w) + rx;
rf[0][oi] += ( wm[mi * 4 + 0]) * pf;
gf[0][oi] += ( wm[mi * 4 + 1]) * pf;
bf[0][oi] += ( wm[mi * 4 + 2]) * pf;
rf[0][ni] += ( wm[mi * 4 + 0]) * pf;
gf[0][ni] += ( wm[mi * 4 + 1]) * pf;
bf[0][ni] += ( wm[mi * 4 + 2]) * pf;
}
}
最后使用傅里叶逆变换还原图像:
// 傅里叶逆变换
double [][]rt = fftw.ifft2(h,w,rf[0],rf[1]);
double [][]gt = fftw.ifft2(h,w,gf[0],gf[1]);
double [][]bt = fftw.ifft2(h,w,bf[0],bf[1]);
double pv = 255.f / l;
// 转换回byte数组
byte []rgb = new byte [w * h * 4];
for (int i = 0; i < l; i ++){
rgb[2+ (i * 4)] = Math.max(0, Math.min(255,(int)(rt[0][i] * pv)));
rgb[1+ (i * 4)] = Math.max(0, Math.min(255,(int)(gt[0][i] * pv)));
rgb[0+ (i * 4)] = Math.max(0, Math.min(255,(int)(bt[0][i] * pv)));
rgb[3+ (i * 4)] = 0xff;
}
runOnUi(new Runnable(){
void run()override{
// 通过byte数组构造QXImage还原图像
doneimg = new QXImage(rgb, w, h, QXImage.Format_RGB32);
//刷新界面显示图像
widgetdon.update();
}
});
然后我们来测试一下
这是原图:
我们载入原图看一下:
原图的频域是一些无意义的噪点,可以认为是空白。
这是水印图片:
选择水印图片再看看效果:
调整水印的位置和能量,更新一下
中间的图像是最终生成的预览图,可以发现从原图上几乎看不出来,然后点保存,存为bmp文件。
然后重新运行程序,加载上面保存的bmp文件看看。
频域的水印清晰可见, 然后我们把这个bmp转换为png和jpg等其他格式试试遭遇攻击的情况下水印的效果如何。
在使用ps打开bmp文件,然后另存为png 和 jpg文件。
先看png压缩.
再看jpg压缩:
jpg压缩变形比较严重,但是水印识辨度仍比较高。
源码下载地址: 点此去下载