[JavaScript]使用opencv.js实现基于傅里叶变换的频域水印(隐水印)
PS:查了多方资料,都没有提到用
JavaScript
来实现频域水印的教程,故经过笔者的实践,遂写一篇教程来简单介绍。
通过了解频域水印的相关知识,我理解了频域水印就是先将图片进行傅里叶变换,得到频域图,然后将水印文字加到频域图中,在将频域图转换回去得到加了频域水印的图片。
数学原理可以参考如下回答:
阿里巴巴公司根据截图查到泄露信息的具体员工的技术是什么?
因此在自己实现频域水印时,我也是按照这样的方式来逐步实现:
1. 傅里叶变换
我基于 opencv.js
(opencv.js
是opencv
编译到 js
的版本)在前端使用 js
来实现傅里叶变换:
关于 opencv
的离散傅立叶变换各个步骤解释可以在下面的官方文档中找到,但不是 js
版本,因此不能用于 js
:
离散傅立叶变换 — OpenCV 2.3.2 documentation
而关于 opencv.js
的官方文档中有对图像进行傅里叶变换的示例代码,可以在 js
中使用,如下:
OpenCV: Fourier Transform
为了更清楚的介绍,这里展示傅里叶变换代码,在注释中进行每部分的介绍:
【其中具体的方法调用请参考官方教程OpenCV: OpenCV.js Tutorials】
// 读入图片
let src = cv.imread(imgElement);
// 利用cvtColor将图片转成单通道的灰度图,傅里叶变换只能处理单通道,否则会报错
cv.cvtColor(src, src, cv.COLOR_RGBA2GRAY, 0);
// 获取傅里叶变换时最优的尺寸(调整尺寸可以使傅里叶变换的速度更快,即当图像的尺寸是2, 3,5的整数倍时,计算速度最快)
let optimalRows = cv.getOptimalDFTSize(src.rows);
let optimalCols = cv.getOptimalDFTSize(src.cols);
let s0 = cv.Scalar.all(0);
let padded = new cv.Mat();
// 填充图片,即上面的得到尺寸比原图大,需要用0填充增加的边缘部分的像素
cv.copyMakeBorder(src, padded, 0, optimalRows - src.rows, 0, optimalCols - src.cols, cv.BORDER_CONSTANT, s0);
// 傅立叶变换的结果是复数,对原图像进行傅里叶变换后得到的是复数,因此为傅立叶变换的结果(实部和虚部)分配存储空间。并且由于频域值范围远远超过空间值范围(0~255),因此要用float来存储,下面的cv.CV_32F就是opencv.js中的float宏
let plane0 = new cv.Mat();
padded.convertTo(plane0, cv.CV_32F);
let planes = new cv.MatVector();
let complexI = new cv.Mat();
let plane1 = new cv.Mat.zeros(padded.rows, padded.cols, cv.CV_32F);
planes.push_back(plane0);
planes.push_back(plane1);
cv.merge(planes, complexI);
// 原地傅里叶变换,即变换后的结果仍存储在当前变量
cv.dft(complexI, complexI);
// 将复数转换为幅度。并且由于幅度值范围很大,不适合在屏幕上显示,因此进行对数处理
cv.split(complexI, planes);
cv.magnitude(planes.get(0), planes.get(1), planes.get(0));
let mag = planes.get(0);
let m1 = new cv.Mat.ones(mag.rows, mag.cols, mag.type());
cv.add(mag, m1, mag);
cv.log(mag, mag);
// 将最开始添加的像素剔除
let rect = new cv.Rect(0, 0, mag.cols & -2, mag.rows & -2);
mag = mag.roi(rect);
// 重新按照象限来排列图片,使得亮部集中在中心
let cx = mag.cols / 2;
let cy = mag.rows / 2;
let tmp = new cv.Mat();
let rect0 = new cv.Rect(0, 0, cx, cy);
let rect1 = new cv.Rect(cx, 0, cx, cy);
let rect2 = new cv.Rect(0, cy, cx, cy);
let rect3 = new cv.Rect(cx, cy, cx, cy);
let q0 = mag.roi(rect0);
let q1 = mag.roi(rect1);
let q2 = mag.roi(rect2);
let q3 = mag.roi(rect3);
// exchange 1 and 4 quadrants
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
// exchange 2 and 3 quadrants
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
// 归一化,把幅度值归一化到float 0~1的范围
cv.normalize(mag, mag, 0, 1, cv.NORM_MINMAX);
// 展示傅里叶转换的频域结果图片(注:对于float类型的图片,opencv会将float0~1的范围映射到0~255,也即浮点数乘以255,从而展示出来)
cv.imshow("dstImg", mag);
因此就得到了一张频域图,从上面的过程中可以看出得到的频域图是单通道的灰度图,展现出来的结果也确实是如此。
2. 添加水印
给频域图添加水印很简单,相当于就是在图片上面加字即可。
let waterMask = "wbl_z";
let scalar = new cv.Scalar(0,0,0);
let point = new cv.Point(40,40);
// 在图中插入文字
cv.putText(complexI,waterMask,point,cv.FONT_HERSHEY_DUPLEX,1.0,scalar);// 1.0是插入的字体的大小
cv.flip(complexI,complexI,-1);// 翻转操作,然后再加一次文字
cv.putText(complexI,waterMask,point,cv.FONT_HERSHEY_DUPLEX,1.0,scalar);
cv.flip(complexI,complexI,-1);
3. 逆傅里叶变换
已经成功实现了傅里叶变换并且加上了水印,要得到加了频域水印的图片,那么只需要把频域图通过逆傅里叶变换转换回去即可。但关键就在这一步,会遇到很多的问题:
问题1
但是会发现查了很多资料,都没有关于 opencv.js
的逆傅里叶变换的介绍,在官方文档中也只有傅里叶变换的介绍,而没有逆傅里叶变换。
查到在其他语言版本的 opencv
中是通过调用 idft()
这个函数来实现的,但在 js
中却会报错 idft()
未定义,也即 opencv.js
中不存在这个函数。
解决办法1
在经过长时间的搜索后,终于发现在 opencv
的源码中对于 idft()
的定义仅仅是调用了已有的 dft()
,并且传入了一个参数来实现的(参考:OpenCV中的DFT和iDFT的详细代码及注释)。
在进一步阅读官方文档后,我最终通过手动传入 opencv.js
定义的两个宏 。cv.DFT_REAL_OUTPUT | cv.DFT_INVERSE
来了 idft()
的功能,并对结果进行了归一化。
如下:
cv.dft(complexI, ifft, (cv.DFT_REAL_OUTPUT | cv.DFT_INVERSE));
cv.normalize(ifft, ifft, 0, 1, cv.NORM_MINMAX);
cv.imshow("dstToOriginImg", ifft);
展示到屏幕上的效果如下:
问题2
在上面的图像中可以看到得到的加了水印后的图片是黑白的,而原图是彩色的,如果只能生成黑白的图片,那么这个水印加的也没有意义。
因此如何让图片加频域水印后生成的图片是彩色很重要。
所以得先了解上面傅里叶变换过程的原理和图片的显示原理:
彩色的图片是
RGB
3 通道,每个像素由 3 个值来表示,三种颜色混合才展现成彩色(这里不考虑透明通道),在傅里叶变换中,首先将图片变成了灰度图,也即图片由原来三通道变成了单通道的图片,每个像素只由一个值0~255
来表示,因为只有一个值,所以只能在亮度上有区别,值大的就亮,值小的就暗。
在傅里叶变换最开始,我们就将图片变成灰度图了,逆傅里叶变换回来的图片同样是单通道的灰度图,显示出来自然就是黑白的。那么要让图片变成彩色,就要保留原图的色彩信息,而不能直接转成灰度图
let src = cv.imread(imgElement);
// 设置存储3种颜色的矩阵vector
let colors = new cv.MatVector();
// 分离图片的3个通道到vector中
cv.split(src,colors);
// 注意opencv是顺序是BGR,不是RGB,因此colors.get(0)是蓝色通道
得到三个颜色通道后,选择其中一个,这里选择蓝色通道B来进行后续傅里叶变换、添加水印,最后将 3 个通道合并在一起即可展示添加水印后的彩色图片了。
// 逆傅里叶变换
let ifft = new cv.Mat();
cv.dft(complexI, ifft, (cv.DFT_REAL_OUTPUT | cv.DFT_INVERSE));
cv.normalize(ifft, ifft, 0, 1, cv.NORM_MINMAX);
// 下面把前面分离出来的颜色通道合并回去,否则图片会是灰色的
ifft.convertTo(B, cv.CV_8U, 255.0);let toMerge = new cv.MatVector();
// 删除傅里叶变换最开始增加的padding
let res = B.roi(new cv.Rect(0, 0, imgCols, imgRows));
toMerge.push_back(res);
toMerge.push_back(G);
toMerge.push_back(R);
cv.merge(toMerge, res);
cv.imshow("dstToOriginImg", res);
特别注意:ifft.convertTo(B, cv.CV_8U, 255.0);
这条语句非常重要,在傅里叶变换中提到了需要将颜色的 0~255
转换成 float
再进行傅里叶变换,因此逆傅里叶变换后得到的也是浮点数,需要使用 convertTo
转换回到 8
位无符号数,其中的 cv.CV_8U
就是 opencv.js
的 8
位无符号数的类型。并且一定要写 alpha
系数,即 255.0
,如果不写那么会将 0~1
的浮点数都转成 8
位无符号数 1
,而不是映射到 0~255
的范围
【/(ㄒoㄒ)/,问就是在这里被坑了好久才发现】
效果如下:
再查看加了频域水印的图片的频域图,检查是否成功添加了水印,结果如下,确实出现了水印
总结
仔细观察加了频域水印的图片,可以发现颜色和原图有些许偏差,特别是对于白色的图片很明显,其他颜色的图片则不会很明显:
并且在我的实践过程中仅仅使用了蓝色通道,而其他的两个通道以及透明通道都没有修改,想必肯定会有更合适的方法来均匀地用到多个通道,使得颜色偏差尽可能的小。
欢迎大佬评论区指出更有效的处理办法!
[JavaScript]使用opencv.js实现基于傅里叶变换的频域水印(隐水印)相关推荐
- OpenCV实现基于傅里叶变换(FFT)的旋转文本校正(文字方向检测)
OpenCV实现基于傅里叶变换的旋转文本校正 from: http://johnhany.net/2013/11/dft-based-text-rotation-correction/ 发布于 201 ...
- vue.js是基于javascript的吗?
vue.js是基于javascript的,用于构建用户界面的渐进式框架,采用MIT开源协议.Vue的核心库只关注视图层,采用自底向上增量开发的设计,并且非常容易学习,非常容易与其它库或已有项目整合. ...
- OpenCV—python 图像矫正(基于傅里叶变换—基于透视变换)
文章目录 一.基于傅里叶变换的图像矫正 1.1 傅里叶变换原理 1.2 傅里叶变换过程一系列函数 1.3 图像矫正处理流程 二.旋转图像矫正 三.基于透视的图像矫正 3.1 直接变换 3.2 自动获取 ...
- [译] JavaScript 中的 CSS:基于组件的样式的未来
本文讲的是[译] JavaScript 中的 CSS:基于组件的样式的未来, 原文地址:CSS in JavaScript: The future of component-based styling ...
- 使用opencv.js分类器和hbuilderx开发一个分类器app
一.开发工具简介 1.opencv分类器 基于图像HAAR与LBP特征训练的级联分类器,只需要简单的正负样本数据集图片,就可以训练一个检测正样本的级联分类器.最重要的是,经过编译好的的exe类文件训练 ...
- 学习opencv.js(1)图像入门
目标:了解如何阅读图像以及如何在网络中显示它. 读取图像:OpenCV.js 将图像保存为cv.Mat类型.我们使用 HTML 画布元素将cv.Mat传输到网络或反向传输.ImageData 接口可以 ...
- Python cv2(Opencv) Canny边缘检测 和 傅里叶变换
简介: OpenCV是一个基于Apache2.0许可(开源)发行的跨平台计算机视觉和机器学习软件库,可以运行在Linux.Windows.Android和Mac OS操作系统上.它轻量级而且高效,由一 ...
- 现代浏览器的web音频javascript类库 - Howler.js
日期:2013-2-6 来源:GBin1.com 在线演示 浏览器这个玩意儿并不是个新鲜事,但是随着HTML5的发展激起了浏览器技术的巨大发展.但是浏览器上对于音频控制技术来说仍旧非常的落伍.庆幸的 ...
- JavaScript和Node.js的关系
JavaScript和Node.js的关系 JavaScript是一门编程语言(脚本语言),JavaScript以前是在浏览器里执行的,需要浏览器里的JavaScript引擎,Firefox有叫做Sp ...
最新文章
- 用了这么久的 Python,居然没注意到这个操作
- poj3934Queue(dp)
- springboot+mybatis多数据源最简解决方案
- 成都Uber优步司机奖励政策(4月24日)
- intellij自动生成构造函数
- 浅议C#客户端和服务端通信的几种方法:Rest和GRPC和其他
- 【CF1199 D,E, F】Welfare State // Matching vs Independent Set // Rectangle Painting 1
- Jco服务配置以及程序编写
- Android倒计时案例展示
- XML解析的四种方式
- java基础大概_Java基础知识(一)
- 图片md5值不一样_夏天宅家,拍点不一样的宝骏530车模图片!
- Tableau可视化学习笔记:day11-12
- echarts 系列一
- android5.1去掉开机锁屏
- 《图谋职场——最经济的图形沟通》 一种能提高职场竞争力的沟通能力
- js实现web贪吃蛇小游戏
- 南京大学俞扬博士:强化学习前沿(下)
- 学习使用大数据数据采集工具(python)
- 解决linux下syslog文件过大
热门文章
- 计算机工程制图标注,工程制图与计算机辅助设计:第3章 组合体视图即尺寸标注...
- 优秀课程案例:使用Scratch制作一个射击类游戏-360度旋转射击!
- win 10 显示未识别网络的 一种解决办法
- spring框架多数据源切换问题的解决
- Json数据的对比工具,对比库(含js在线对比工具,.net 的jsondiffer包)
- 如果《使命召唤》登陆Facebook…
- 以rpm为后端及以yum为前端工具的程序包管理器在Linux发行版系统centos中的使用...
- ddr3ip核心_XILINX DDR3 IP核使用教程
- 骗子网站--正规网赚系统--www.j9m2.com--诈骗网站
- (转载)如何跟踪一个报错消息