[JavaScript]使用opencv.js实现基于傅里叶变换的频域水印(隐水印)

news2024/11/23 15:34:45

PS:查了多方资料,都没有提到用 JavaScript 来实现频域水印的教程,故经过笔者的实践,遂写一篇教程来简单介绍。

通过了解频域水印的相关知识,我理解了频域水印就是先将图片进行傅里叶变换,得到频域图,然后将水印文字加到频域图中,在将频域图转换回去得到加了频域水印的图片。

数学原理可以参考如下回答:

阿里巴巴公司根据截图查到泄露信息的具体员工的技术是什么?

因此在自己实现频域水印时,我也是按照这样的方式来逐步实现:

1. 傅里叶变换

我基于 opencv.jsopencv.jsopencv编译到 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);

因此就得到了一张频域图,从上面的过程中可以看出得到的频域图是单通道的灰度图,展现出来的结果也确实是如此。

image-20221213161216004

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);

image-20221213161716059

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);

展示到屏幕上的效果如下:

image-20221213162913265

问题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.js8 位无符号数的类型。并且一定要写 alpha 系数,即 255.0,如果不写那么会将 0~1 的浮点数都转成 8 位无符号数 1,而不是映射到 0~255 的范围

【/(ㄒoㄒ)/,问就是在这里被坑了好久才发现】

效果如下:

image-20221213171236669

再查看加了频域水印的图片的频域图,检查是否成功添加了水印,结果如下,确实出现了水印

image-20221213171335411

总结

仔细观察加了频域水印的图片,可以发现颜色和原图有些许偏差,特别是对于白色的图片很明显,其他颜色的图片则不会很明显:

image-20221213173537292

并且在我的实践过程中仅仅使用了蓝色通道,而其他的两个通道以及透明通道都没有修改,想必肯定会有更合适的方法来均匀地用到多个通道,使得颜色偏差尽可能的小。

欢迎大佬评论区指出更有效的处理办法!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/91188.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

全国制造业新产品开发流程管理(甄选班)招生简章

近来,越来越多的TO C企业已经将业务从C端转型到了B端,而传统的市场品牌部门除了品牌相关的工作,也将会越来越多承担企业“增长”的业务;“营”与“销”脱节的是市场部门也越来越受到了Diss。 那么,To B产品经理能力提…

package xxx does not exist

1、开发环境如下: gradle 7.3.3: gradle插件:7.2 AS: 2、报错日志 /Users/andrew/Documents/React/Code/TestRetrofit/app/src/main/java/com/lwd/testretrofit/MainActivity.java:8: error: package com.lwd.testretrofit.lwd does not e…

如何用好图表?

如何选择合适的图表? 在可视化图表中,基于不同的目的,我们要选择不同的图表 一般情况下,我们需要借助图表达成4个目的:展示比较,展示分布,展示联系,展示构成 一.作用 1.展示比较 …

数据结构-克鲁斯卡尔算法,普利姆算法(求最小生成树)

当然了昨天晚上写了求两源点之间最少权值和,就不得不再写一下另外两个求最小生成树的算法分别是克鲁斯卡尔和普利姆了,话不多说直接进入主题 目录 并查集: 克鲁斯卡尔算法(与并查集结合起来): 普利姆算法: 并查集&a…

[附源码]Python计算机毕业设计高校教学过程管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等…

产品诚可贵,质量价更高

产品诚可贵,质量价更高缘起所属行业研发人员规模所在团队规模团队开发模式产品类别软件主体层次软件交付周期软件需求质量/感受/问题设计质量保证及好与不好开源组件代码质量千行缺陷数单元测试代码覆盖率和测试质量质量文化和QA人员测试团队人员配置质量工程活动功…

第4篇:嵌入式Linux应用开发基础知识

嵌入式Linux应用开发基础知识一、GCC编译过程二、MakefileMakefile的引入及规则Makefile的语法a. 通配符b. 假想目标: .PHONYC. 变量Makefile函数函数foreach函数filter/filter-outWildcardpatsubst函数Makefile实例通用MakefikeMakefikeMakefile.build说明.txt三、TCPserver.c…

springMVC+mysql实现的Java web图书管理系统源码+运行教程+参考论文

今天给大家演示的是一款由srpingMVC实现的Java web图书管理系统,本项目功能非常丰富,且附带配套论文及视频指导配置运行教程,系统实现的功能主要有:图书馆里、图书分类管理、出版社管理、图表图书统计展示、用户管理、角色管理、权…

小村庄迸发大能量,桃浦村“藏”着花样经,“烘”出财富来

从几人的小公司到产税千万级的高新技术和数集团企业总部;从拿着10万元注册资金开公司处处碰壁的小公司到家喻户晓的国牌护肤品林清轩……在普陀区桃浦镇的桃浦村,默默“耕耘”出了一家家优秀的民营企业。 筑巢引凤聚人才,注入乡村振兴新动能。在上海市委…

【数据结构】二叉树的前序遍历、中序遍历、后序遍历、层序遍历

文章目录 1.二叉树的概念 1.1概念 1.2存储方式 1.3特殊的二叉树 1.4规律 2.二叉树的实现 2.1表现方式 2.2遍历 2.2.1前序遍历 思想 代码 详细分析 2.2.2中序遍历 2.2.3后序遍历 2.2.4层序遍历 思想 代码 详细过程 1.二叉树的概念 1.1概念 一棵二叉树是结点的一个有限…

第25届京港会开幕 元宇宙产业委与香港国际元宇宙协会启动全面合作

央链直播讯,以“融入新格局 合作谱新篇”为主题的第25届北京香港经济合作研讨洽谈会(简称“京港洽谈会”)14日在北京和香港开幕。据悉,自1997年香港回归以来,京港洽谈会已成功举办24届,两地在金融、专业服务…

基于 KubeSphere 的运管系统落地实践

作者:任建伟,某知名互联网公司云原生工程师,容器技术信徒,云原生领域的实践者。 背景介绍 在接触容器化之前,我们团队内部的应用一直都是基于虚拟机运管,由开发人员自行维护。 由于面向多开发部门服务&am…

ThingsBoard 3.1.1版本在window本地运行之TB-Gateway ODBC数据上传(四)

目录 1、前言 2、Thingsboard Gateway 1.tb-gateway的概念 2.tb-gateway的配置 3.odbc连接器配置 3、ODBC的配置 1.安装window的ODBC驱动程序 2.配置ODBC的驱动程序信息 4、效果展示 1、前言 项目中会出现这样的情况,有个平台搭建在本地,而数据也存…

Jenkins构建并部署一个go语言项目

Jenkins安装 1、下载 安装java [rootlocalhost ~]# yum install java-1.8.0-openjdk* -y 方式一: #下载安装包 [rootlocalhost ~]# wget https://mirrors.tuna.tsinghua.edu.cn/jenkins/redhat-stable/jenkins-2.249.1-1.1.noarch.rpm #安装Jenkins [rootlocalhost…

【excel导入、导出】pom、实体类、工具类、例子

目录 一、环境搭建: pom: 实体类(ExcelClassField ): 工具类: 二、【示例】导入 controller: service 实体类: 注意: 三、【示例】导出 controller: …

搜索与图论 - bellman-ford 算法

文章目录一、为什么 Dijkstra 算法不适用于含负权的图1. 理论推导2. 实例演示2.1 详细步骤2.2 结果二、bellman-ford 算法1. 简介2. 基本思路3. 简单举例4. bellman-ford 算法具体实现过程详见例题有边数限制的最短路。三、bellman-ford 算法例题——有边数限制的最短路具体实现…

仓库24代 “ CK_Label_v24

产品型号 CK_Label_v24 尺寸 124x90x12mm(不含安装支架) 屏幕尺寸 4.2 inch 显示技术 电子墨水屏显示 显示区域面积 (mm) 84.8(H) x 63.6(V) 分辨率 400*300 像素密度 120dpi 显示颜色 黑/白 外观颜色 白色&灰外圏 按键 3 指示灯…

【C++】STL标准模板库

目录 一、概念 STL的四种基本组件 容器vector 迭代器iterator 函数对象function object 算法algorithm 二、使用 容器vector的使用 泛型程序设计: 所谓泛型程序设计就是编写不依赖于具体数据类型的程序。C中,模板就是泛型程序设计的主要…

一次疑似 JVM native 内存泄漏的排查实录

最近开发同学反馈,某定时任务服务疑似有内存泄漏,整个进程的内存占用比 Xmx 内存大不少,而且看起来是缓慢上升的,做了下面这次分析,包括下面的内容: 分析 JVM native 内存的一些常见思路内存增长了&#x…

关于Arduino连接L298N供电问题

关于Arduino连接L298N供电问题 查看原文 该L298N板声称有一个5V稳压器为Arduino供电,在这种情况下,您可以使用单个电源,并让电机板为Arduino供电。 关于为Arduino和电机提供动力有两种思想流派: 使用两个独立的电源&#xff0…