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

使用CN2/CN2GIA顶级线路,支持Shadowsocks/V2ray科学上网,支持支付宝付款,每月仅需 5 美元
## 加入品葱精选 Telegram Channel ##

知乎用户 fuqiang liu 发表

本文通过一个的实验,简要介绍频域手段添加**数字盲水印的方法,并进一步验证其抗攻击性**。在上述实验的基础上,总结躲避数字盲水印的方法。(多图预警

本文分为五个部分,第一部分综述;第二部分频域数字盲水印制作原理介绍;第三部分盲水印攻击性实验;第四部分总结;第五部分附录(源代码)。

一、综述

本文提供的一种实现 “阿里通过肉眼无法识别的标识码追踪员工” 的技术手段。通过看其他答主的分析,阿里可能还没用到频域加水印的技术。

相对于空域方法,频域加盲水印的方法隐匿性更强,抵抗攻击能力更强。这类算法解水印困难,你不知道水印加在那个频段,而且受到攻击往往会破坏图像原本内容。本文简要科普通过频域手段添加数字盲水印。对于 web,可以添加一个背景图片,来追踪截图者。

所谓盲水印,是指人感知不到的水印,包括看不到听不见(没错,数字盲水印也能够用于音频)。其主要应用于音像作品、数字图书等,目的是,在不破坏原始作品的情况下,实现版权的防护与追踪。

添加数字盲水印的方法简单可分为空域方法和频域方法,这两种方法添加了冗余信息,但在编码和压缩情况不变的情况下,不会使原始图像大小产生变化(原来是 10MB 添加盲水印之后还是 10MB)。

空域是指空间域,我们日常所见的图像就是空域。空域添加数字水印的方法是在空间域直接对图像操作(之所以说的这么绕,是因为不仅仅原图是空域,原图的差分等等也是空域),比如将水印直接叠加在图像上。

我们常说一个音有多高,这个音高是指频率;同样,图像灰度变化强烈的情况,也可以视为图像的频率。频域添加数字水印的方法,是指通过某种变换手段(傅里叶变换,离散余弦变换,小波变换等)将图像变换到频域(小波域),在频域对图像添加水印,再通过逆变换,将图像转换为空间域。相对于空域手段,频域手段隐匿性更强,抗攻击性更高

所谓对水印的攻击,是指破坏水印,包括**涂抹,剪切,放缩,旋转,压缩,加噪,滤波等。**数字盲水印不仅仅要敏捷性高(不被人抓到),也要防御性强(抗打)。就像 Dota 的敏捷英雄往往是脆皮,**数字盲水印的隐匿性和鲁棒性**是互斥的。(鲁棒性是抗攻击性的学术名字)

二、频域制作数字盲水印的方法

信号是有频率的,一个信号可以看做是无数个不同阶的正弦信号的的叠加。

上式为傅里叶变换公式

是指时域信号(对于信号我们说时域,因为是与时间有关的,而图像我们往往说空域,与空间有关),

是指频率。想要对傅里叶变换有深入了解的同学,建议看一下《信号与系统》或者《数字信号处理》的教材,里面系统介绍了傅里叶变换、快速傅里叶变换、拉普拉斯变换、z 变换等。

简而言之,我们有方法将时域信号转换成为频域,同样,**我们也能将二维信号(图像)转换为频域。**在上文中提到,图像的频率是指图像灰度变换的强烈情况。关于此方面更系统的知识,参见冈萨雷斯的《图像处理》。

下面以傅里叶变换为例,介绍通过频域给图像添加数字盲水印的方法。注意,因为图像是离散信号,我们实际用的是离散傅里叶变换,在本文采用的都是二维快速傅里叶变换,快速傅里叶变换与离散时间傅里叶变换等价,通过蝶型归并的手段,速度更快。下文中傅里叶变换均为二维快速傅里叶变换。

上图为叠加数字盲水印的基本流程。编码的目的有二,一是对水印加密,二控制水印能量的分布。以下是叠加数字盲水印的实验。

这是原图像,尺寸 300*240 (不要问我为什么不用 Lena,那是我前女友),

之后进行傅里叶变换,下图变换后的频域图像,

这是我想加的水印,尺寸 200*100,

这是我编码后的水印,编码方式采用随机序列编码,通过编码,水印分布到随机分布到各个频率,并且对水印进行了加密,

将上图与原图的频谱叠加,可见图像的频谱已经发生了巨大的变化,

之后,将叠加水印的频谱进行傅里叶逆变换,得到叠加数字水印后的图像,

肉眼几乎看不出叠加水印后的图像与原图的差异,这样,数字盲水印已经叠加到图像中去。

实际上,我们是把水印以噪声的形式添加到原图像中。

下图是在空域上的加水印图与原图的残差(调整了对比度,不然残差调小看不见),

可以看出,实际上上述方法是通过频域添加冗余信息(像噪声一样)。这些噪声遍布全图,在空域上并不容易破坏。

最终,均方误差(MSE)为 0.0244

信噪比(PSNR)为 64.2dB

那么,为什么频谱发生了巨大的变化,而在空域却变化如此小呢?这是因为我们避开了图像的主要频率。下图是原图频谱竖过来的样子,其能量主要集中在低频。

水印提取是水印叠加的逆过程

经提取后,我们得到如下水印,**问:为什么水印要对称呢?**嘿嘿,大家想想看。

三、攻击性实验

本部分进行攻击性实验,来验证通过频域手段叠加数字盲水印的鲁棒性。

1. 进行涂抹攻击,这是攻击后的图片:

再进行水印提取:

2. 进行剪切攻击,就是网上经常用的截图截取一部分的情况:

进行循环补全:

提取水印:

3. 伸缩攻击(这个实验明码做的,水印能量较高,隐匿性不强):

提取水印(水印加的不好,混频挺严重的):

4. 旋转攻击(明码):

提取水印:

5.JPEG 压缩后(这个实验我好像是拿明码做的,能量主要加在了高频):

提取结果:

6.PS 4 像素马赛克 / 均值滤波等,攻击后图像 (这是我女朋友吗?丑死了):

提取水印后图像:

7. 截屏,

截屏后我手动抠出要测试的图像区域,并且抽样或者插值到原图尺寸:

测试结果:

8. 亮度调节(明码):

水印提取:

9. 色相调节(明码):

水印提取:

10. 饱和度调节(明码):

水印:

11. 对比度(明码):

水印:

12. 评论区用 waifu2x 去噪后图片:

解水印:

13. 美图秀秀,我对我女票一键美颜,美白,磨皮,加腮红,加唇彩(有一种很羞耻的感觉,捂脸):

提取水印:

14. 对于背景纯色的图其实也是无所谓的

能量系数为 10 时加水印图片:觉得太显噪就把能量系数调低,不过水印的隐秘性和鲁棒性是互斥的

最终提取出的水印:

15. 我用将 RGB>600 的像素设置成为 (0,255,0) 来模拟 PS 魔术手,

提取水印为:

16. 屏摄,好吧,这个实验我做哭了

屏摄图:

实验结果:

我把水印能量系数调整到 2000 都没有用。

屏摄之后与原图信噪比为 4dB 左右,我用多抽样滤波的方式试过,滤不掉屏摄引入的噪声。屏摄不仅引入了椒盐噪声,乘性噪声,还有有规律的雪花纹理(摩尔纹)。

四、总结

基于频域的盲水印方法隐藏性强,鲁棒性高,能够抵御大部分攻击。但是,对于盲水印算法,鲁棒性和隐匿性是互斥的。

本文方法针对屏摄不行,我多次实验没有成功,哪位大神可以做一下或者讨论讨论。还有二值化不行,这是我想当然的,觉得肯定不行所以没做实验。其他的我试了试,用给出的方法调整一下能量系数都可以。

我想大家最关心的是什么最安全,不会被追踪。

不涉及图像的都安全,比如拿笔记下来。

涉及图像的屏摄最安全

截屏十分不安全。

===== 彩蛋 ====

我在上图明码写入了信息。为了抵抗 jpg 压缩,我水印能量较高,并且因为没有编码,能量分布不均。图中规律性纹路,就是你懂的。嘿嘿,你懂的,解开看看吧。

@杨一丁

在答案中给出了上图隐写的内容,(雾)。

五、附录

%%傅里叶变换加水印源代码
%% 运行环境Matlab2010a 
clc;clear;close all;
alpha = 1;

%% read data
im = double(imread('gl1.jpg'))/255;
mark = double(imread('watermark.jpg'))/255;
figure, imshow(im),title('original image');
figure, imshow(mark),title('watermark');

%% encode mark
imsize = size(im);
%random
TH=zeros(imsize(1)\*0.5,imsize(2),imsize(3));
TH1 = TH;
TH1(1:size(mark,1),1:size(mark,2),:) = mark;
M=randperm(0.5\*imsize(1));
N=randperm(imsize(2));
save('encode.mat','M','N');
for i=1:imsize(1)\*0.5
    for j=1:imsize(2)
        TH(i,j,:)=TH1(M(i),N(j),:);
    end
end
% symmetric
mark\_ = zeros(imsize(1),imsize(2),imsize(3));
mark\_(1:imsize(1)\*0.5,1:imsize(2),:)=TH;
for i=1:imsize(1)\*0.5
    for j=1:imsize(2)
        mark\_(imsize(1)+1-i,imsize(2)+1-j,:)=TH(i,j,:);
    end
end
figure,imshow(mark\_),title('encoded watermark');
%imwrite(mark\_,'encoded watermark.jpg');

%% add watermark
FA=fft2(im);
figure,imshow(FA);title('spectrum of original image');
FB=FA+alpha\*double(mark\_);
figure,imshow(FB); title('spectrum of watermarked image');
FAO=ifft2(FB);
figure,imshow(FAO); title('watermarked image');
%imwrite(uint8(FAO),'watermarked image.jpg');
RI = FAO-double(im);
figure,imshow(uint8(RI)); title('residual');
%imwrite(uint8(RI),'residual.jpg');
xl = 1:imsize(2);
yl = 1:imsize(1);
\[xx,yy\] = meshgrid(xl,yl);
figure, plot3(xx,yy,FA(:,:,1).^2+FA(:,:,2).^2+FA(:,:,3).^2),title('spectrum of original image');
figure, plot3(xx,yy,FB(:,:,1).^2+FB(:,:,2).^2+FB(:,:,3).^2),title('spectrum of watermarked image');
figure, plot3(xx,yy,FB(:,:,1).^2+FB(:,:,2).^2+FB(:,:,3).^2-FA(:,:,1).^2+FA(:,:,2).^2+FA(:,:,3).^2),title('spectrum of watermark');

%% extract watermark
FA2=fft2(FAO);
G=(FA2-FA)/alpha;
GG=G;
for i=1:imsize(1)\*0.5
    for j=1:imsize(2)
        GG(M(i),N(j),:)=G(i,j,:);
    end
end
for i=1:imsize(1)\*0.5
    for j=1:imsize(2)
        GG(imsize(1)+1-i,imsize(2)+1-j,:)=GG(i,j,:);
    end
end
figure,imshow(GG);title('extracted watermark');
%imwrite(uint8(GG),'extracted watermark.jpg');

%% MSE and PSNR
C=double(im);
RC=double(FAO);
MSE=0; PSNR=0;
for i=1:imsize(1)
    for j=1:imsize(2)
        MSE=MSE+(C(i,j)-RC(i,j)).^2;
    end
end
MSE=MSE/360.^2;
PSNR=20\*log10(255/sqrt(MSE));
MSE
PSNR

%% attack test
%% attack by smearing
%A = double(imread('gl1.jpg'));
%B = double(imread('attacked image.jpg'));
attack = 1-double(imread('attack.jpg'))/255;
figure,imshow(attack);
FAO\_ = FAO;
for i=1:imsize(1)
    for j=1:imsize(2)
        if attack(i,j,1)+attack(i,j,2)+attack(i,j,3)>0.5
            FAO\_(i,j,:) = attack(i,j,:);
        end
    end
end
figure,imshow(FAO\_);
%extract watermark
FA2=fft2(FAO\_);
G=(FA2-FA)\*2;
GG=G;
for i=1:imsize(1)\*0.5
    for j=1:imsize(2)
        GG(M(i),N(j),:)=G(i,j,:);
    end
end
for i=1:imsize(1)\*0.5
    for j=1:imsize(2)
        GG(imsize(1)+1-i,imsize(2)+1-j,:)=GG(i,j,:);
    end
end
figure,imshow(GG);title('extracted watermark');

%% attack by cutting
s2 = 0.8;
FAO\_ = FAO;
FAO\_(:,s2\*imsize(2)+1:imsize(2),:) = FAO\_(:,1:int32((1-s2)\*imsize(2)),:);
figure,imshow(FAO\_);
%extract watermark
FA2=fft2(FAO\_);
G=(FA2-FA)\*2;
GG=G;
for i=1:imsize(1)\*0.5
    for j=1:imsize(2)
        GG(M(i),N(j),:)=G(i,j,:);
    end
end
for i=1:imsize(1)\*0.5
    for j=1:imsize(2)
        GG(imsize(1)+1-i,imsize(2)+1-j,:)=GG(i,j,:);
    end
end
figure,imshow(GG);title('extracted watermark');


%%小波变换加水印,解水印大家按照加的思路逆过来就好
clc;clear;close all;
%% read data
im = double(imread('gl1.jpg'))/255;
mark = double(imread('watermark.jpg'))/255;
figure, imshow(im),title('original image');
figure, imshow(mark),title('watermark');
%% RGB division
im=double(im); 
mark=double(mark); 
imr=im(:,:,1); 
markr=mark(:,:,1); 
img=im(:,:,2); 
markg=mark(:,:,2); 
imb=im(:,:,3); 
markb=mark(:,:,3); 
%% parameter
r=0.04; 
g = 0.04; 
b = 0.04;
%% wavelet tranform and add watermark
% for red
\[Cwr,Swr\]=wavedec2(markr,1,'haar'); 
\[Cr,Sr\]=wavedec2(imr,2,'haar'); 
% add watermark
Cr(1:size(Cwr,2)/16)=... 
Cr(1:size(Cwr,2)/16)+r\*Cwr(1:size(Cwr,2)/16); 
k=0; 
while k<=size(Cr,2)/size(Cwr,2)-1 
Cr(1+size(Cr,2)/4+k\*size(Cwr,2)/4:size(Cr,2)/4+... 
(k+1)\*size(Cwr,2)/4)=Cr(1+size(Cr,2)/4+... 
k\*size(Cwr,2)/4:size(Cr,2)/4+(k+1)\*size(Cwr,2)/4)+... 
r\*Cwr(1+size(Cwr,2)/4:size(Cwr,2)/2); 
Cr(1+size(Cr,2)/2+k\*size(Cwr,2)/4:size(Cr,2)/2+... 
(k+1)\*size(Cwr,2)/4)=Cr(1+size(Cr,2)/2+... 
k\*size(Cwr,2)/4:size(Cr,2)/2+(k+1)\*size(Cwr,2)/4)+... 
r\*Cwr(1+size(Cwr,2)/2:3\*size(Cwr,2)/4); 
Cr(1+3\*size(Cwr,2)/4+k\*size(Cwr,2)/4:3\*size(Cwr,2)/4+... 
(k+1)\*size(Cwr,2)/4)=Cr(1+3\*size(Cr,2)/4+... 
k\*size(Cwr,2)/4:3\*size(Cr,2)/4+(k+1)\*size(Cwr,2)/4)+... 
r\*Cwr(1+3\*size(Cwr,2)/4:size(Cwr,2)); 
k=k+1; 
end; 
Cr(1:size(Cwr,2)/4)=Cr(1:size(Cwr,2)/4)+r\*Cwr(1:size(Cwr,2)/4); 

% for green
\[Cwg,Swg\]=WAVEDEC2(markg,1,'haar'); 
\[Cg,Sg\]=WAVEDEC2(img,2,'haar'); 
Cg(1:size(Cwg,2)/16)=... 
Cg(1:size(Cwg,2)/16)+g\*Cwg(1:size(Cwg,2)/16); 
k=0; 
while k<=size(Cg,2)/size(Cwg,2)-1 
Cg(1+size(Cg,2)/4+k\*size(Cwg,2)/4:size(Cg,2)/4+... 
(k+1)\*size(Cwg,2)/4)=Cg(1+size(Cg,2)/4+... 
k\*size(Cwg,2)/4:size(Cg,2)/4+(k+1)\*size(Cwg,2)/4)+... 
g\*Cwg(1+size(Cwg,2)/4:size(Cwg,2)/2); 
Cg(1+size(Cg,2)/2+k\*size(Cwg,2)/4:size(Cg,2)/2+... 
(k+1)\*size(Cwg,2)/4)=Cg(1+size(Cg,2)/2+... 
k\*size(Cwg,2)/4:size(Cg,2)/2+(k+1)\*size(Cwg,2)/4)+... 
g\*Cwg(1+size(Cwg,2)/2:3\*size(Cwg,2)/4); 
Cg(1+3\*size(Cg,2)/4+k\*size(Cwg,2)/4:3\*size(Cg,2)/4+... 
(k+1)\*size(Cwg,2)/4)=Cg(1+3\*size(Cg,2)/4+... 
k\*size(Cwg,2)/4:3\*size(Cg,2)/4+(k+1)\*size(Cwg,2)/4)+... 
g\*Cwg(1+3\*size(Cwg,2)/4:size(Cwg,2)); 
k=k+1; 
end; 
Cg(1:size(Cwg,2)/4)=Cg(1:size(Cwg,2)/4)+g\*Cwg(1:size(Cwg,2)/4); 

% for blue
\[Cwb,Swb\]=WAVEDEC2(markb,1,'haar'); 
\[Cb,Sb\]=WAVEDEC2(imb,2,'haar'); 
Cb(1:size(Cwb,2)/16)+b\*Cwb(1:size(Cwb,2)/16); 
k=0; 
while k<=size(Cb,2)/size(Cwb,2)-1 
Cb(1+size(Cb,2)/4+k\*size(Cwb,2)/4:size(Cb,2)/4+... 
(k+1)\*size(Cwb,2)/4)=Cb(1+size(Cb,2)/4+... 
k\*size(Cwb,2)/4:size(Cb,2)/4+(k+1)\*size(Cwb,2)/4)+... 
g\*Cwb(1+size(Cwb,2)/4:size(Cwb,2)/2); 
Cb(1+size(Cb,2)/2+k\*size(Cwb,2)/4:size(Cb,2)/2+... 
(k+1)\*size(Cwb,2)/4)=Cb(1+size(Cb,2)/2+... 
k\*size(Cwb,2)/4:size(Cb,2)/2+(k+1)\*size(Cwb,2)/4)+... 
b\*Cwb(1+size(Cwb,2)/2:3\*size(Cwb,2)/4); 
Cb(1+3\*size(Cb,2)/4+k\*size(Cwb,2)/4:3\*size(Cb,2)/4+... 
(k+1)\*size(Cwb,2)/4)=Cb(1+3\*size(Cb,2)/4+... 
k\*size(Cwb,2)/4:3\*size(Cb,2)/4+(k+1)\*size(Cwb,2)/4)+... 
b\*Cwb(1+3\*size(Cwb,2)/4:size(Cwb,2)); 
k=k+1; 
end; 
Cb(1:size(Cwb,2)/4)=Cb(1:size(Cwb,2)/4)+b\*Cwb(1:size(Cwb,2)/4); 
%% image reconstruction
imr=WAVEREC2(Cr,Sr,'haar'); 
img=WAVEREC2(Cg,Sg,'haar'); 
imb=WAVEREC2(Cb,Sb,'haar'); 
imsize=size(imr); 
FAO=zeros(imsize(1),imsize(2),3); 
for i=1:imsize(1); 
for j=1:imsize(2); 
FAO(i,j,1)=imr(i,j); 
FAO(i,j,2)=img(i,j); 
FAO(i,j,3)=imb(i,j); 
end 
end 
figure, imshow(FAO); title('watermarked image');

知乎用户 坏蛋​ 发表

序言

每增加一个数学公式都会使读者减半,原话出自霍金的时间简史。所以我知道这篇可能不会有多少人看,但是仍然执着的想分享出来。(长文多图,流量慎点)

佛语中有箴言:坐亦禅, 行亦禅, 一花一世界, 一叶一如来

世间万物是复杂的,但是又是纯粹的简单的,从宏观的花花世界到微观的原子电子,万物都在按照它的规律运行,而我们的先辈前人,一直都在用自己的方式与经验,总结着万物运行的规律。

不要误会,本文并不是哲学论题的讨论,一个偶然的机会拜读了知乎上的一篇关于频域水印的问答。(阿里巴巴公司根据截图查到泄露信息的具体员工的技术是什么? - 知乎)当中有对频域数字水印的实现与讨论,身边有不少的朋友对此颇感兴趣,于是我就想以后机会写一个 “从零开始的频域变换到水印的完整解答”,

当然,你也可以直接跳转到本文的最后一章节 “鲁棒盲水印” 来查看频域水印是如何在版权保护中起作用并对抗各类版权偷盗者的攻击的。

本人并非全才,本文中多有疏漏错误还望各位读者多多指正,文章将从最基础的三角函数开始,一步一步地推到并演化到傅里叶变换直到频域签名。

我相信学习如流水,希望读者能在本文的循序渐进的推导过程中满足对相关技术的求知欲,并对文中的不足与错误不吝赐教。

最后再次引用箴言的后半句作为导言的结束语:

春来花自青, 秋至叶飘零, 无穷般若心自在, 语默动静体自然。

为了避免一开始就引入那些过于严肃的话题,笔者希望在开篇的部分,做个简短的说明来告诉大家,这篇文章做的是什么的。

首先,笔者画了一张图名叫:《虎虎生威》,好吧,就是下面这张

因为笔者太叼了,所以画的图应该署名一下,你可以看到右上角的 DBinary,没错,那就是笔者的大名,但是,盗图狗很快就把我这张大作给偷走了,最后他居然把自己的名字写了上去

好气啊,明明是笔者画的图,怎么变成张三了,现在死无对证了,到底是谁画的,笔者暗暗不爽,于是做了一台时光机,回到笔者画图后还没发布之前,不行,这回不能光靠签名,要加点靠谱的水印,于是,笔者开发了一款软件,对这个图片进行了隐签名:

(这是签名后的图片,好吧,因为颜色过于单调还是能看出明显干扰的,实际对照片签名几乎看不出来)

盗图狗果然还是出手了,它篡改了我的图片

我一看跳了起来,*** 张三你怎么盗我图,显然,张三不服,凭什么说我盗你图,证据呢???

我不慌不忙打开频域程序,加载了张三盗窃后的图

那么张三同志,麻烦你告诉我图像频域里为什么写的是我的名字?

张三哑口无言…..

这就是本文将要讨论的技术细节,如果阅读到这里你还不明白本文的主旨与目标是什么,你可以直接跳转到本文的最后一章节 “鲁棒盲水印” 来查看频域水印是如何在版权保护中起作用并对抗各类版权偷盗者的攻击的。

从三角形开始

有人说,上帝使用三角形创造了这个世界,这点我完全同意,三角形拥有如此之多的特性,足够让每一个探究者为之所着迷,它仿佛维系着几何与世界的基石,诞生出数学中众多的定律并在今天成就了我们的学术与技术大厦。

当然,本文并不是抒情散文,我并不打算也没有这个能力去探究那些更深层次的数学理论关系,但理解三角形并不是制作变形金刚,你可以在一张纸上画三个点然后用直线把他们连起来,那就是一个三角形了如图(a.1)

三角形有非常多的特性,首先,确定它是一个稳定的结构。另外确定一个平面也仅仅只需要一个三角形就足够了,三角形的所有内角角度之和是 180°(a.2)。

但最为有意思的是被称之为直角三角形的东西,在直角三角形中有一个角的角度是 90°(a.3),例如图(a.3)就是一个直角三角形。

实际上在这个直角三角形中,三角形的边长比例,将随着角度θ的变化而变化,一个角度θ决定了 abc 三边长度的比例关系,于是在这里,我们引入了一个叫三角函数的东西,其中

,当θ小于 90° 的时候,我们可以在直角三角形中非常直观地看到三角函数的变化关系,但在直角三角形中,θ的取值范围,也被限制到了 0 到 90°,为了能让θ表示更大的范围,我们就需要引入新的表达方式了。

我们首先先建立一个直角坐标系(a.4),然后以原点为圆心,画一个圆。同时,我们在圆上任意取一点,并将该点的坐标设为(x,y)

通过勾股定理我们知道,圆上的点到原点的距离,r=

那么,对三角函数我们重新定义为

,这样一来,我们就可以表示θ为任意实数时三角函数对应的值了

通过这个图我们同时可以知道,当时

,实际上相当于在一个圆上绕了若干个个圈,你可以想象看着你家里的时钟,秒针每过 60 秒就转了一个圈,你现在看到秒针的位置,在 60 秒后,120 秒后,180 秒后… 它仍然会指向同样的位置,这个特性在三角函数中同样有效,我们管它叫做三角函数的周期性。

现在,让我们引入一个新的符号π,π在角度上代表 180°,除了角度外,我们还引入弧度,它的定义是弧长等于半径的弧,其所对的圆心角为 1 弧度。实际上半圆弧长和半径的比例恰好是一个定值,因此,π除了在角度上表示 180°,在弧度上则是一个定值,这个值是一个无限不循环小数,我们常常将它约等于为 3.14。

所以

,同时因为周期性,其中 n 是一个整数。

那么,在第一章节,三角函数的几何意义,就显而易见了。

让三角函数动起来

假如我们把时间引入进来,那么,三角函数就开始变得生动了,最直观的比喻就是现在挂在大厅墙上的时钟,秒针每分钟都会转动 360°,现在,让我们假设秒针指向 “12”,也就是垂直于水平面时,它的角度是 0°,那么经过 15 秒后,秒针指向“3”,也就是转动了 90°,30 秒后经过了半分钟,指向“6” 也就是转动了 180°,那么,秒针每秒转动的角度是

, 我们用 t 来表示时间,那么时间与秒针转动角度我们可以用θ=6t 来表示,在物理上,我们常常使用ω来表示角速度,那么,时间内转过的角度就是

θ=ωt

现在,让我们画一个二维坐标系,并设横坐标为时间,角速度ω=2π,那么

,的坐标如下图(b.1)所示

可以看到,在一秒 0-1 秒,已经是一个完整的周期了,因为它在 1 秒内拥有一个完整的周期,我们就称他的频率为 1hz,显然的当ω=4π时 1 秒内有两个完整的周期,因此,它的频率就为 2hz(图 b.2)。

可以看到,频率实际上和角速度是相关联的,我们使用字母 f 来表示频率,函数公式如下

角速度和频率是一个正比关系,角速度越大,频率也就越大

对正余弦函数而言,我们也常常写作

三角函数的一些常用公式

我们可以非常直观的从几何图像中推导出三角函数的一些性质,画一个坐标系,同时以原点为圆心绘制一个半径为 1 的圆(图 c.1)

那么,

可知

由周期性可知

同时,三角函数间是可以互相转换的,我们很容易得出这样的公式:

因为正余弦函数存在这种互相转换的关系,因此之后我们统称它们为正弦函数

我们还可以进一步证明二角的更多公式(证明过程略),这些公式我们都可以直接拿来使用

二角和差:

和差化积

通过联立二角和差公式,我们还可以得到积化和差公式

和二角和差推导出的二倍角公式

三角函数的正交性

在讨论正交性之前,让我们用最简单的方式了解下微积分,我们取一个最简单的一元函数做比方

现在,让我们画一个坐标轴,那么,这个函数的图像是这样的 (d.1):

现在,做一条经过(2,0)并垂直于 x 轴的直线,交于点 A,(2,0)为 B,原点为 C 如图(d.2)

那么,我们很容易求出三角形 ABC 的面积

实际上这个求面积的过程,就是微积分于该函数上的几何意义,这个求面积的过程,我们用微积分来表示,就是

同样类比的,

这个积分方程实际上是求四边形 ABCD 的面积(d.3)

在二维平面上我们很容易将微积分理解为函数在一定范围内和 x 轴围成的面积,在但实际上面积和微积分稍有不同,因为面积肯定是一个非负数,但积分却可以是负数,

例如

他在坐标系中是三角形 HCI 的面积,但是它的 x 轴下方,我们可以理解为三角形的高度是一个 “负数”,因此,这个区域的面积也是一个 “负的面积”,所以,这段的积分为 - 2(d.3)

那么,假如我们对这个函数的 - 2 到 2 积分,那么就会变成一个正的面积加上一个负的面积,结果它们相互抵消了,结果变成了 0,如下推导

实际上连续的中心对称函数图形 h(x),在

的积分都是 0

显然的,正弦函数图像满足这个性质(d.4),不仅如此,我们可以非常直观的看出来,正余弦函数在函数的一个周期内的积分都是 0(d.4 d.5)

现在,我们假设有两个自然数 m,n 且

,并假设两个函数

然后我们将这两个正余弦函数任意进行组合,并对他们在 -π到π进行积分

通过上述的常用公式,我们可以推导出下面的三个式子

可以看到,m 不等于 n 时,积分结果都是 0

从正交性得到的启发

还记得之前我们使用的将时间作为变量的三角函数么

现在,因为三角函数的特性,我们管三角函数在时域上的表示叫波,当然

也就是正弦波了,我们用更加通用的公式来表示一个正弦波 \

或者,余弦波也同样是这个道理,毕竟它与正弦波的不同仅仅只是 “移动了一下”

我们很容易证明这点。

现在我们可以想象一个正弦波在二维坐标上的样子,或者是我们在科幻电影或科研实验室中,看到仪器仪表上那一段段的波形。或者更加直观的,我们将一块石投进水池里,荡起的波浪也像极了正弦波。

当然,在自然界一般不会出现标准的正弦波,各种波的叠加,你可以想象在一个房间里一群朋友聊天,或者是在 KTV 中歌声和谈话声混杂的场景,它就是声波的各种叠加

尽管聊天中大家都在说话,但是我们仍然不会把朋友们说的话混淆起来,因为不同的人发出的声音频率也不一样,这也就是我们常说的未见其人先闻其声,我们天生具备有分别不同频率声波的能力,所以尽管环境嘈杂,我们仍然能够取得我们想要的信息。

但是现在我们如何使用数学的手段,查看波形函数 f(x) 中是否有我们想要频率的波形呢。

显而易见的,答案当然就是本章节的标题,应用波的相关性,例如我们使用一个正弦波
去乘以一个由多个正余弦波叠加而成的函数 f(t),然后对它们进行积分

从相关性可以知道,当角速度(上一章节的 m,n)不同时,也就是频率不同时,积分结果是 0,只有频率相同时,积分结果才不为 0,因此,如果 f(t) 中包含 1HZ 的正弦波,那么积分的结果就不为 0,否者,结果就是 0

那么,一个检波手段也就诞生了。

欧拉公式复变函数

正弦函数和复数表面上并没有什么关联,但正所谓千里姻缘一线牵,在我们考虑着那根看不见的红线另一端系的是谁的时候,欧拉早在几个世纪前就将正弦函数与复数牵了一条红线,从而撮合了数学上这一段流传千古的因缘,不过如果再在红线上扯下去,这篇文章就要变爱情小说了,但是我们仍然需要回到这个渣作的现实三次元,现在让我们来介绍一下大名鼎鼎的一个复变函数,它的知名程度基本上在是数学上的安徒生童话,人人皆知

它的公式如下

其中,i 表示复数的虚部,它是

,也就是说

,当然我们并不能深究他在现实生活中的意义,但它在数学多个方面,起着举足轻重的意义。

那么,复数如何理解呢

我们来看看复数的标准形式

其中,a,b 为任意实数,a 在复数中表示实部,b 在复数中表示虚部

假设我们现在画一条二维坐标系,x 轴为实部,y 表示虚部,那么,1+2i 实际上表示的是坐标上的(1,2)

那么

在坐标系上,实际上是一个半径为 1 的圆(d.7)

复数在信号的表示

现在让我们来考虑下面这个复数

它的实部为

,如果我们希望用

的形式来表示它,那么,它就变成了

画在坐标系中,如图 d.8

按照极坐标的角度而言,即一个长度为 2 的变绕 X 轴逆时针旋转

现在让我们更进一步放大我们的脑洞,以原点为圆心,2 为半径画一个圆(图 d.9)

那么, 假如我们将

看作是与原点距离为 1,绕 x 轴逆时针旋转的点,将

与之相乘

就变成了,初始位置为 (以角速度以原点为圆心逆时针旋转的点

那么,假如我们将复数用在信号中,就从复数坐标系上的点拓展到了在线信号的幅度与初相了。

还记得之前我们写的通用的正弦 / 余弦函数么

这就是上面的通用公式,因为这个公式太重要了(或者说避免你翻回去找这个公式),我将它再写了一遍

可以看到,对于一个正弦函数,我们仅需要知道它的幅度,角频率,初相,就可以确定这个正弦函数是什么样子的了

但正弦函数比善变的女人还有能耐,通过一些数学变形,你还会发现它有时比哄妹纸有意思多了,通过三角函数的公式,我们可以得到

那么,这个函数就变成了正余弦函数的组合,同时我们也得出了

那么就有

傅里叶的故事

这里我们抛开那些繁琐的数学公式,然后把时间拉回到 17 世纪,当时有一位法国的男爵名叫巴普蒂斯 · 约瑟夫 · 傅里叶(Baron Jean Baptiste Joseph Fourier)当然,并不是因为它当了一官半职我们才介绍它,但他的成就,却从 17 世纪一直影响到我们今天,毫不夸张的说他奠定了信号系统的基础 (或者说给出了指导方向?)。

具体时间还是要回到 18 世纪,伯努利(D.Bernoulli)就曾经提出:一个弦的实际运动都可以用标准张模的线性组合来表示,但是当时另一个数学大神拉格朗日是不兹磁这个说法的,拉格朗日认为,不可能用三角级数来表示一个具有间断点的函数,就在这个环境下,傅里叶仍然坚信:“任何” 周期信号都能够成谐波关系的正弦级数来表示,他还专门写了一篇论文,但迫于拉格朗日当时在学术界的威望,傅里叶理论的论文一直没能被发表,直到傅里叶的晚年,他才得到他应有的承认。

当然,以我们现在的角度来说,傅里叶当时的理论是有缺陷的,在后人不懈努力下,傅里叶变换才趋于完善,实际上傅里叶变换并不是因为傅里叶对这个理论在数学上做了多大的贡献,但傅里叶当时对问题的前瞻性与指导性,却奠定了这个数字信号举足轻重的公式基础。

检波与傅里叶变换

现在我们讨论的就是傅里叶大神 “任何连续周期信号都可以由一组适当的正弦曲线组合而成” 这一命题,在信号系统或者是工学,傅里叶变换绝对是入门的加减乘除,实际上傅里叶变换并不是一个公式,而是多个分别应对不同类型信号的多个公式,但万变不离其宗,不管变体如何变换,其核心的思想是不会改变的。

那么傅里叶变换的意义是什么呢,回想一下我们之前说的正弦函数的相关性,通过这个相关性,我们可以检测某个波里是否包含某个频率的正弦波,进一步的,假设我们用无限多个不同频率的正弦波与它正交组合,我们就能知道,这个波是由哪些频率的正弦波组合而成的了,我们管波在时间轴上的表达,叫做时域,图 b.1 其实就是的时域图,通过傅里叶变换,我们能够将是由哪些频率组成的图像表示出来(当然只有一个 1HZ 频率),也就是横坐标由时间变成了频率,也就是我们说的频域了。

简单来说,在时间信号处理中傅里叶变换就是将时域信号转换为频域信号的一个变换过程

这里,我们先来看看连续傅里叶变换,因为傅里叶英文的开头是 F,所以我们使用大写的 F 来表示傅里叶变换,f(t) 用来表示某连续非周期性的时域信号函数

看看我们的欧拉公式

然后把欧拉公式代入傅里叶变换

角速度乘以时间,不就是了么,我们将这个变换函数写成这种形式

再将带入公式得到

结果变得显而易见了,简单来看,傅里叶变换与检波的手段多多少少的相似性 — 对 sin 与 cos 分别检波(为什么要使用同一频率 sin,cos 进行检波呢,因为对于一个频率的正弦波我们不仅仅要知道其存不存在,更需要知道其波幅及相位,用两个不同相位的正弦波进行检波,就可以取得其相位,在之后章节会进一步讨论)

傅里叶级数的三角函数表达

但仅从上面的检波手法来推断傅里叶变换,是不严谨的。

现在设一个函数由一个直流分量(简单来说就是一个常数)和多个正弦函数组成,那么它可以写成这种形式

int sum;

for(n=0;n<N-1;n++)

{

Sum+=n;

}

由上式可知,表示这个三角函数的角速度或者叫角频率,当 n=1 时,我们管叫基频,管叫傅里叶级数(余弦信号形式)

利用三角函数的变换公式,上式可变形为

那么,上式变为

现在,让我们正式的引入正交性的性质,还记得检波手段么,这里,我们假设对 f(x) 用 sin(nwt) 进行检波,那么就有

假设 f(x) 中含有角频率的正弦波系数为,那么根据三角函数的正交性,上式就有

进一步计算,可得

周期连续时间傅里叶级数

现在,让我们来想象一个函数 f(x)它是一个周期函数,那么根据傅里叶的理论,它能够表示成若干个(无穷个)一组 “适当” 的正弦曲线组合而成,在前面几个章节,我们通过欧拉公式,得到

显然的,这个复指数信号的频率是,现在我们假设有另一组 “适当” 的正弦函数,它们的频率刚好是的整数倍并且幅度也不同,那么,这组信号我们可以使用

![](data:image/svg+xml;utf8,)

来表示(k 为自然数)。那么从上面的根据欧拉公式,我们也很容易得出下面的推导

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

现在将这两个公式带入

得到

化简后得

因此,上式最终变为了

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

上式就写成了, n=k

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

并对两边同时在一个周期内积分,那么我们就得到公式

\ 进一步变形为

于是,得到

也就是

实际上 an 就是我们所说的傅里叶级数,或者说是频域系数。通过这个系数的值,我们可以知道这个频率的波对原始波的能量贡献值,在之前的《信号的复数表示》章节中,我们可以了解到这个系数确定了该频率波的幅度,初相,从而完成信号时域到频域的分解,并且我们还知道了

也就是说,通过 ak 的模

我们知道对应角频率波的波幅(等于该频率幅值的一半)

通过

可以得出对应角频率正弦波的相角。

周期离散时间傅里叶级数

在上一个章节中,讨论了周期连续时间信号的傅里叶级数求解方式,那么,连续信号可以求其级数,离散的是否也有这样一个公式呢

但在介绍离散变换变换前,我们先来了解连续和离散是什么,。其实顾名思义。比如给你一段长度 100 米的绳子,当然,这个 100 米的绳子是连续的,如果你在 50 米的地方剪短这根绳子,那么它就是不连续的了,假设我们把这根绳子切成若干个段。直到每个段都变成一个 “点”,我们可以直接用数字编号每一个点,那么他就变成离散的了。

用图像来继续说明,如图 e.1,这是一个 sinx 的函数图像,当然,它是周期无限长且连续的

现在,我们每隔二分之π就取一个点,那么这些点在坐标系上就是周期离散的(e.2)

说到这里,我们现在要介绍的变换,就是处理这些点的变换方式

首先,因为是等间距对周期信号采样,所以采样出的点也是周期性的,假设采样的点用数组 x[n]来表示,也就是说假设周期为 N,x[a]与 x[a+Nk]是完全相等的值,也就是说,整个离散样本中取任意周期 N 内他们的累加和是一样的,同样的,x[n]仍然可以用 “恰当” 的正弦函数组合而成(或者说可以由基波频率为的一系列波形组合而成)基于这点,我们就可以将 x[n]写成这种形式(k= 表示取任意连续 N 点即一个周期内点,不管如何取,结果都是一样的)

![](data:image/svg+xml;utf8,)

我们取

![](data:image/svg+xml;utf8,)

可以知道,当 K 不等于 N 时的结果为 0,仅当 K 等于 N 时的结果为 N。同样的,我们再次将两边同时乘以得到

然后再同时对两边进行 N 项上求和

显然的,仅当 k=r 或 n 为 N 的整数倍时,右式才不为 0,那么,上式就变为

那么

![](data:image/svg+xml;utf8,)

频谱系数求得!。

非周期离散时间傅里叶变换

如果严格来说,自然界大多是没有理想的周期信号的,那么,是否有办法处理非周期离散时间的信号么。

我们来看看这样一个离散信号(图 e.3),它只有一个脉冲取样

这个脉冲信号仅在 - 3,-2,-1 上是有值的,其余的值都是 0

那么我们是否可以把它当成一个周期无穷大的周期信号呢。

我们先来看看上一节中周期离散时间傅里叶级数的分析公式

![](data:image/svg+xml;utf8,)

假如把这个分析公式套用在上述的信号中,那么因为仅在三点有值(其它都是 0)并且周期是无穷大那么我们就得到了公式

![](data:image/svg+xml;utf8,)

当然,它和下面这个式子是等价的

![](data:image/svg+xml;utf8,)

这样我们就将公式推广到了更加通用的公式类型

现在定义函数

![](data:image/svg+xml;utf8,)

那么

![](data:image/svg+xml;utf8,)

我们现在再将 ar 代入原式

![](data:image/svg+xml;utf8,)

中,得到

![](data:image/svg+xml;utf8,)

从式中看出,随着 N 趋近于无穷大,趋近于无穷小,那么,上式就从累加变成了积分,且因为的周期为 2π,且其仅在周期内有值,于是,上式也随之变为了

![](data:image/svg+xml;utf8,)

非周期离散有限长度傅里叶变换

最后,我们将上述的变换公式进行进一步的推广,就是非周期离散有限长度傅里叶变换了,实际上它与周期离散傅里叶变换已经非常的接近了

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

如果我们将有限长的信号推广到无限长的信号,那么我们先假设信号的样本点数为 N 个,那么,信号的 n 取值范围就可以定义在 [0,N-1]

我们假设将这个有限长的区间补到无限长,除了 [0,N-1] 区间,我们仅仅需要在其他区间再补上 N 个同样的离散信号就行了,这并不影响其结果,那么我们就可以将有限长非周期离散信号变为周期离散信号了,这样我们就可以直接套用周期离散时间傅里叶变换的分析公式:

![](data:image/svg+xml;utf8,)

为了方便计算,我们周期取 [0,N-1],那么,公式就变成了

![](data:image/svg+xml;utf8,)

当然在实际应用中,我们常常设:

![](data:image/svg+xml;utf8,)

那么就有了有限长非周期傅里叶变换的的分析公式:

![](data:image/svg+xml;utf8,)

非周期离散时间傅里叶变换的应用

在上面几个章节中,我们从周期连续时间的傅里叶级数逐步的推广到了有限长离散时间傅里叶变换。虽然内容不少,但是真正实际在日常用的多的,是有限长非周期离散时间傅里叶变换,为何?当然我们幸运的不是在一个老牛拉破车的年代计算只能靠人脑算盘,现在的信号处理除非一些数学推导应用,大多数实际生活应用都是靠计算机来完成的,而这也决定了我们的公式必须与计算机的硬件相关,我们的计算机目前存储空间是有限的,而连续的信号(不管是频域还是时域),就相当于由无穷多个点组成,很遗憾,现在仍然没有存储容量无穷大的内存,就算有,也没有能够处理无穷大数据的 CPU,因此,我们无法直接处理连续的信号,观察上面几个变换,也就只有时域和频域都是有限长度且离散的有限长离散时域傅里叶变换能够满足我们的需求了。

下面为了说明方便,我们将有限长度且离散的有限长离散时域傅里叶变换的分析公式统一简称为离散傅里叶变换(Discrete Fourier Transform)或者其英文缩写 DFT,将它的逆变换(Inverse Discrete Fourier Transform)也就是综合公式简称为 IDFT。

现在让我们来看看离散傅里叶变换对

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

其中,N 表示信号的采样点数,n 的范围是 0 到 N-1,

![](data:image/svg+xml;utf8,)

的第一个点,

的第二个点,以此类推,k 的取值范围是 0 到 N-1,由 f=

可知,其分辨率等于

为了更加方便浅显地了解其中的计算,我们继续观察公式中的

仍然是我们熟悉的味道,现在我们使用欧拉公式替换它于是离散傅里叶变换的公式变成了

当 k=0 时

可以看到,它实际上是所有样本点的累加和,那么它意味着什么呢,我想象一个波形函数

它表示基频成分,因为我们从公式中可以看到,频率都是其整数倍,至于多少倍,就是由 k 值决定的。

现在,让我们用一个实际的范例来验证离散傅里叶变换

假设正弦函数

我们假设对这个函数进行采样,采样频率是 4HZ,那么,实际上我们将在 0.25s,0.5s,0.75s,1s 处取得其样本点,那么,对应的值应该是

现在对其进行离散傅里叶变换那么

因为有四个样本点,所以,N 的值是 4,k 的取值范围是 0-N-1,也就是 0 到 3 了

我们先来计算的值

这点没错,因为没有直流分量,所以它理所当然是 0

继续是的值

![](data:image/svg+xml;utf8,)

也确实是 1HZ。

现在是的值

这是 2HZ 的值,显然,它为 0

最后 x3 值是

的值是 2,但是我们知道,并不包含 3HZ 的波,实际上根据香农采样定律,4HZ 的采样只能表达 2HZ 的波,因此这个点实际上是不准确的。但是,出现 2 的结果并不是偶然,我们接着往下看。

巧妙的对称性,共轭

如果说对称是世界上共有的一种美的表达,那么,在几何平面上,无穷多的函数就拥有这种对称性,在对称美这一个方面,出色的数学家也许并不逊色于毕加索。

当然深究的话就是后话了,在这里我们来简单看看几个拥有对称性的简单函数:

![](data:image/svg+xml;utf8,)

这是一个非常简单的二元一次方程,可以从图 f.2 中看到,他是关于 y 轴对称的,这句话如果用数学的语言来将,非常简单且直观的,我们可以用下面的公式来表达这个 “对称” 的思想

![](data:image/svg+xml;utf8,)

对于这类关于满足上述公式的函数,我们管它叫偶函数。

现在再让我们看另外一个函数

![](data:image/svg+xml;utf8,)

它的函数图像如图 f.3

可以看到,函数的图像是原点对称的,相等,我们用如下公式表示这种 “原点对称” 关系

![](data:image/svg+xml;utf8,)

我们管它叫奇对称

当然,对称未必一定要是 y 轴或者是原点,现在我们将

![](data:image/svg+xml;utf8,)

的函数图像向右平移两个单位,变成

![](data:image/svg+xml;utf8,)

这样,它就关于 x=2 这条竖线对称了(f.4)

这回我们在数学上用这个方程式来表示它关于 x=2 对称

![](data:image/svg+xml;utf8,)

通过变形,它可以写成

![](data:image/svg+xml;utf8,)

最后,我们用更加通用的方程式来表示某个函数 f(x) 关于 x=N/2 这条垂线对称

![](data:image/svg+xml;utf8,)

现在,让我们来看看共轭是什么:

你看过几米的《向左走,向右走》么,共轭是数学中一个文艺而浪漫的代名词,一句相当文艺诗来总结这个关系,就是:

向左走,向右走,纵使背道而驰,相隔万里,但只要心连接在一起,也终会在大洋的彼岸,迎来相会的交点。

文艺的诗歌艺术生看到后也许就开始感叹,多么美的诗句,世间万物芸芸众生,千里有缘一线牵,感谢我能遇见你,但对于地理大神也许不削一顾:这不就是说地球是圆的么,数学系的牛人毅然站出来,别做梦了少年,你们只会越离越远,就算到了世界末日,你们也碰不到一块儿。

结果,文艺青年赢的了女神的芳心。理工大神则斩获了 “屌丝” 的称号

实际上,共轭我们可以理解成相关联,也就是所谓的 “缘分”,就是这一对数存在某种联系的意思,这里我们不深究更深层次的含义,我们主要说说共轭复数,首先,复数的形式是:

![](data:image/svg+xml;utf8,)

它的共轭是

![](data:image/svg+xml;utf8,)

非常简单的一句概括:实部相等,虚部取反,这两个复数互为共轭。

那么,和我们之前说的对称性相结合的话什么是共轭对称性呢

我们这样给出定义:

当一个函数 f 其实部为偶函数,虚部为奇函数时,此函数就为共轭对称函数,即 f(x) 的共轭等于 f(-x),举一个非常简单的例子:

![](data:image/svg+xml;utf8,)

显然的,实部是个偶函数,虚部 xi 是个奇函数,因此它是一个共轭对称函数,现在,我们来看看离散的范例,假设有以下复数序列

![](data:image/svg+xml;utf8,)

我们分别提取实部与虚部那么分别是

实部:1,2,3,3,2,1

虚部:1,2,3,-3,-2,-1

我们很清晰的看到了序列的对称性,实部偶对称,虚部奇对称。在这里,因为他是离散的序列,所以我们管它叫共轭对称序列。

离散傅里叶变换的共轭对称性

对称之美存在世间万物中的每一个角落,作为信号与系统中最美的变换函数之一,她也存在这种对称性。

我们回到离散傅里叶变换的公式:

![](data:image/svg+xml;utf8,)

我们假设:

![](data:image/svg+xml;utf8,)

依据欧拉公式,可以得出

![](data:image/svg+xml;utf8,)

那么,上式可以写成

![](data:image/svg+xml;utf8,)

我们设 k=N-k,并将它带入

![](data:image/svg+xml;utf8,)

当中,那么就有

![](data:image/svg+xml;utf8,)

进行对比,发现,实部相同虚部相反,假如输入的信号为实信号时,刚好呈共轭对称性,将它代入离散傅里叶变换方程后

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

其中 * 的意思就是共轭。也就是说离散傅里叶变换具有这种共轭对称性(输入为实信号时)。

但共轭对称的范围是 1~N-1,因此当 k=0 时,它的共轭对称并不存在(序列范围是 0~N-1),所以我们需要额外讨论 k=0 时的情况:

也就是之前所说的直流分量了。

现在回到之前我们所求函数

![](data:image/svg+xml;utf8,)

的序列离散傅里叶变换的结果

![](data:image/svg+xml;utf8,)

根据共轭对称性得

![](data:image/svg+xml;utf8,)

快速傅里叶变换

在上文的范例中,我们仅仅是计算了序列长度只有 4 的离散傅里叶变换,也可以看到非常麻烦,倘若序列再长点的话,工作量就会呈指数级增长,我们使用矩阵运算来表达离散傅里叶变换的运算过程

![](data:image/svg+xml;utf8,)

可以看到需要 16 次乘法运算,按照时间复杂度来算的话,它的复杂度是 O(),当然,这还没算上 sin cos 与复数加减法带来的性能开销。所以,如果你想将一段 2000 长度的离散信号进行 DFT 运算,意味着你至少需要做 400 万次的运算,巨大的性能花销,导致了 DFT 在以前并不被看好,毕竟除非一些非常重要的信号,谁会花大量的人力物力去做这几百万次的运算呢,即使是在今天,几百万次的计算开销在有了计算机的帮助之后,也并不算一个小数目,倘若对于那些实时的频域分析,这些计算开销都是非常昂贵的,不过幸运的是,一个 DFT 的优化算法很快被开发出来,我们称之为快速傅里叶变换(Fast Fourier Transformation),当然,其本质上仍然是 DFT 只是算法上进行了优化使它更加适合于计算机处理,不过话说归说,不给出理论证明的广告都是瞎扯淡,那么,接下来就继续看看,DFT 是如何被优化的:

首先第一点,我们仍然先把离散傅里叶变换对贴上来

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

现在,我们假设对离散序列 x[n] 的离散傅里叶变换写作

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

就可以写成

进一步变形,得到

后面两个式子是不是非常眼熟,没错,一个 DFT 变换变成了 2 个 DFT 变换,不同的是,后面序列的长度只有前面的一半,为了保证后面两个 DFT 变换成立,k 的范围也应随之变为了【0,二分之 n】

既然,前半部分可以变为两个 DFT 变换,那么后半部分呢,我们使用 k + 来表示离散序列后半部分,那么,DFT 就变为了

进一步变形得到

根据欧拉公式,因为

所以

可以看到,仅仅是多了一个,其它都与之前的推导相同,那么这也就是为什么我们要将离散序列分为奇数列与偶数列的原因了,偶数列不变,奇数列多个负号,于是,公式变为了

那么,公式总结为

那么,计算的复杂度就从

![](data:image/svg+xml;utf8,)

, 只要 n 的值大于 2,计算的时间复杂度无疑是降低了,我们进一步对多项式进行分解,直到最后仅剩下 2 个离散点的傅里叶变换,那么,它的复杂度将会降低至

![](data:image/svg+xml;utf8,)

,同时,这也就要求我们的离散序列项的个数必须是 2 的整数次幂,因此,这种快速傅里叶变换又叫做基 2 快速傅里叶变换,当然同样的,也有其它基底的快速傅里叶变换,但这里就不做更多的讨论了。

快速傅里叶逆变换

快速傅里叶逆变换完全可按照正变换的过程进行推导,实际上就是换汤不换药,根据 IDFT 公式

![](data:image/svg+xml;utf8,)

同样的,我们将离散序列 X[n]分成两组,例如 X[0],X[2],X[4],x[6]……x[2i]为偶数序列,记为 x[2i],,而将 X[1],X[3],X[5]….X[2i+1]记为 X[2i+1], 意思是奇 (odd) 序列

那么,公式可以写成

进一步变形,得到

后式与 DFT 正变换,仅仅只是 W 指数正负的不同,我们只需要修改下符号就可以了。

按照上面步骤同样推导出(因推导步骤一样,推导过程略):

使用 C 语言编写 DFT/IDFT 代码

回到离散傅里叶变换对

我们需要先将它们变成容易编码的格式,首先是正变换

利用欧拉公式,变为

然后是逆变换

利用欧拉公式,变形为

首先因为频域涉及到复数运算(输入信号一般为实信号)因此我们需要复数结构体 complex, 现定义结构体

![](data:image/svg+xml;utf8,)

及复数的加乘运算函数

![](data:image/svg+xml;utf8,)

之后定义 DFT 运算函数

void DFT(complex x[],complex X[],int N);

其中,x[] 为输入时域信号,X[] 为输出频域信号,N 为时域信号的样本点数

同时定义 DFT 逆变换函数

void IDFT(complex x[],complex X[],int N);

其中,X[] 为输入频域信号,x[] 为输出时域信号,N 为频域信号的样本点数

实际上通过离散傅里叶变换后频域的共轭对称性,我们仅仅需要计算前半部分就可以了,这个优化操作若读者有兴趣可以自己完成。

使用 C 语言编写 FFT/IFFT 代码

根据 FFT 的公式有

那么,我们就可以采用迭代的方式,将输入序列不断地拆分重组,直到它变为 2 点的 DFT,代码如下:

void FFT\_Base2(\_IN \_OUT complex x\[\],int N)
{
	int exbase,exrang,i,j,k;
	complex excomplex,Wnk,cx0,cx1;
	if (N>>2)
	{
		// x\[\] 4 base odd/even Sort
		exbase=1;
		exrang=0;
		while (exrang<N)
		{
			exrang=exbase<<2;

			for (i=0;i<N/exrang;i++)//for each token
			{
				for (j=0;j<exbase;j++)//for each atom in token
				{
					excomplex=x\[exrang\*i+exbase+j\];
					x\[exrang\*i+exbase+j\]=x\[exrang\*i+exbase\*2+j\];
					x\[exrang\*i+exbase\*2+j\]=excomplex;
				}
			}
			exbase<<=1;
		}
		FFT\_Base2(x,N>>1);
		FFT\_Base2(x+(N>>1),N>>1);

		for(k=0;k<N>>1;k++)
		{
			Wnk.re=(float)cos(-2\*\_\_PI\*k/N);
			Wnk.im=(float)sin(-2\*\_\_PI\*k/N);
			cx0=x\[k\];
			cx1=x\[k+(N>>1)\];
			x\[k\]=complexAdd(cx0,complexMult(Wnk,cx1));
			Wnk.re=-Wnk.re;
			Wnk.im=-Wnk.im;
			x\[k+(N>>1)\]=complexAdd(cx0,complexMult(Wnk,cx1));
		}
	}
	else
	{
		//2 dot DFT
		cx0=x\[0\];
		cx1=x\[1\];
		x\[0\]=complexAdd(cx0,cx1);
		cx1.im=-cx1.im;
		cx1.re=-cx1.re;
		x\[1\]=complexAdd(cx0,cx1);
	}


}
void FFT(\_IN complex x\[\],\_OUT complex X\[\],int N)
{
 	int size=1;
	complex \*i\_px;

 	while((size<<=1)<N);

	i\_px=(complex \*)malloc(sizeof(complex)\*size);

	memset(i\_px,0,sizeof(complex)\*N);
	memcpy(i\_px,x,sizeof(complex)\*N);

	FFT\_Base2(i\_px,size);
	memcpy(X,i\_px,sizeof(complex)\*N);

	free(i\_px);
}

FFT 的理论则依据下面两个公式,就不再复述了:

C 语言代码如下:

void IFFT\_Base2(\_IN \_OUT complex X\[\],int N)
{
	int exbase,exrang,i,j,n;
	complex excomplex,Wnnk,cx0,cx1;
	if (N>>2)
	{
		// x\[\] 4 base odd/even Sort
		exbase=1;
		exrang=0;
		while (exrang<N)
		{
			exrang=exbase<<2;

			for (i=0;i<N/exrang;i++)//for each token
			{
				for (j=0;j<exbase;j++)//for each atom in token
				{
					excomplex=X\[exrang\*i+exbase+j\];
					X\[exrang\*i+exbase+j\]=X\[exrang\*i+exbase\*2+j\];
					X\[exrang\*i+exbase\*2+j\]=excomplex;
				}
			}
			exbase<<=1;
		}
		IFFT\_Base2(X,N>>1);
		IFFT\_Base2(X+(N>>1),N>>1);

		for(n=0;n<N>>1;n++)
		{
			Wnnk.re=(float)cos(2\*\_\_PI\*n/N);
			Wnnk.im=(float)sin(2\*\_\_PI\*n/N);
			cx0=X\[n\];
			cx1=X\[n+(N>>1)\];
			X\[n\]=complexAdd(cx0,complexMult(Wnnk,cx1));

			Wnnk.re=-Wnnk.re;
			Wnnk.im=-Wnnk.im;
			X\[n+(N>>1)\]=complexAdd(cx0,complexMult(Wnnk,cx1));

		}
	}
	else
	{
		//2 dot IDFT
		cx0=X\[0\];
		cx1=X\[1\];
		X\[0\]=complexAdd(cx0,cx1);

		cx1.im=-cx1.im;
		cx1.re=-cx1.re;
		X\[1\]=complexAdd(cx0,cx1);

	}
}
void IFFT(\_IN complex X\[\],\_OUT complex x\[\],int N)
{
	int size=1,i;
	complex \*i\_px;

	while((size<<=1)<N);

	i\_px=(complex \*)malloc(sizeof(complex)\*size);

	memset(i\_px,0,sizeof(complex)\*N);
	memcpy(i\_px,X,sizeof(complex)\*N);

	IFFT\_Base2(i\_px,size);

	// 1/N operate
	for (i=0;i<N;i++)
	{
		i\_px\[i\].re/=N;
		i\_px\[i\].im/=N;
	}

	memcpy(x,i\_px,sizeof(complex)\*N);

	free(i\_px);
}

信号与采样

《淮南子 · 说山训》中有 “见一叶落而知岁之将暮。”, 宋朝《文录》则引曰:“山僧不解数甲子,一叶落知天下秋。” 直到今天的 “一沙一世界, 一花一天堂. 双手握无限, 刹那是永恒” 都在传达着局部概括全局,一刻即是永恒的思想。

如果是一个充满文艺气息的理科青年,应该很容易就能发出这样的感慨:

我们都是宇宙的一份子,我们是宇宙的缩影,即便微不足道,但冥冥之中我们也是世界不可或缺的一环。

但如果是一个理科大神,这会儿可要费点脑子了,大神抱着心爱的四路泰坦 32G 内存 1T 的 PCIE SSD 和 ryzen 18x 的电脑,琢磨着如何把

sin(x)

的波形图像完整地存入电脑,很快的大神发现即使它再把电脑容量升级一倍,他也无法存储 sinx 的完整波形,哪怕是一段他也办不到:

首先 sin(x) 是周期函数,它的区间是, 很遗憾,就算把整个地球的沙子都做成内存颗粒,也没有办法存储一个无穷大的量。

那么存储一个周期如何,很遗憾,sin(x) 是连续的函数,就算是一个周期也包含着无穷多个幅度信息,就算把整个银河系的沙子都做成内存颗粒,也无法存储无穷多的幅度信息。

不过很快的,大神找到了门道,既然无法存储完整的波形,那么存储当中一些关键的点总能办得到

很快,大神就找到了一个周期内的波峰和波谷, 如图 g.1

很快,波峰和波谷的水平间距刚好是周期的一半,而波峰与 x 轴的垂直距离刚好就是波幅。那么最终大神用 2 个离散的点表示了 sin(x),并且我们也知道了,假设 sin(x) 的频率是 n,那么我们至少要以 2n 的频率进行采样才能够还原出原始的波形。

那么,波形函数如何以数学的形式对一个时域点进行采样呢,实际上前辈们早就定义了一个理想的函数

![](data:image/svg+xml;utf8,)

这个函数的特点是,它仅在

![](data:image/svg+xml;utf8,)

而 x 在其他的值

![](data:image/svg+xml;utf8,)

它的定义是,

![](data:image/svg+xml;utf8,)

的面积始终为 1,因此在它的宽度无限小时,高度就变得无穷大,因此在 x 为 0 处它的值为无穷大,实际上在信号处理中,我们常常规定

![](data:image/svg+xml;utf8,)

而非无穷大以便于我们的处理

因此我们就可以得到一个采样的方程,例如对 sin(x) 的 x=0 点进行采样那么我们就可以用

![](data:image/svg+xml;utf8,)

来表示,如果对 x=2 进行采样,我们只需要对

![](data:image/svg+xml;utf8,)

进行右移位处理就可以了

![](data:image/svg+xml;utf8,)

当然假设我们需要对多个点进行采样,那么我们完全可以写成这种形式 (对 sinx 整数值采样)

![](data:image/svg+xml;utf8,)

在奥本海姆的《信号与系统》7.1.1 中有如下的推论

设对一信号以 T 间隔取样,那么就有如下数学式

![](data:image/svg+xml;utf8,)

根据卷积性质时域内的相乘对应于频域内的卷积,那么就有 (P(j) 为冲击串频域函数)

根据公式(《信号与系统》例 4.8 推论, 为基波频率或叫频域间距)

及(* 为卷积符号)

于是有

上式说明是频率为的周期函数,由一组移位的 X(jw) 叠加而成,在幅度标以的变化,当大于频带宽度的一半时()频带不会发生堆叠,反之会发生堆叠(引用信号与系统 7.1.1 冲击串采样,详细推论请查阅书籍)。

实际上上面的推论可以用一个简单的话来说:

表示一个持续期为 T 且最高频率为 W 的时间函数,有 2TW 的样本个数就足够了。

实际上这句话说的内容就是著名的香农采样定律,看!那些物理学家,数学家和诗人都是虽然表达方式不一样,但他们表达的东西常常都是一个意思,不管怎么说,他们都是会玩的。

钢琴与按键识别

录音频率与采样

“未见其人先闻其声” 说的就是靠声音识别人的道理,从声学的角度来说,各个人发出的嗓音也是各有不同的,我们大脑进化出了自动筛分频率的功能,因此尽管我们见不到真人,仍然可以从他说话的声音分辨出他,在一个嘈杂的环境中,我们也可以在分辨出那个人在说什么,看来,人脑也是一个实时的滤波系统。

当然,人并不是能听到所有的声音,声音归根结底仍然是波在介质中传播,人只能听到 20~20000HZ 的声波,因此,低于 20HZ 的波我们叫次声波,高于 20000HZ 的波叫超声波,根据上一章节所属的香农采样定律,如果我们要记录一段声波例如演唱会,那么我们的采样频率至少就应该是 40000HZ

实际上大部分的 MP3,WAV,OGG 等音乐媒体文件,使用的采样率是 44100HZ,如果一个样本点用 16bits 也就是 2 字节来计算的话,每分钟大约就需要 5M 字节的数据量。

在多媒体的音乐文件中,我们一般需要以下几个参数,来确定多媒体文件的声音是如何采样并如何播放的:

1. SampleRate 采样速率

2. Channel 声道数量

3. BitPerSample 每个样本的位数

依靠这几个参数,我们就可以播放音乐文件了

PCM 码流与 WAV 文件格式

PCM 脉冲编码调制是 Pulse Code Modulation 的缩写,简单来说就是抽样、量化和编码,也就是上章节分别对应的采样速率、每个样本的位数(量化)的概括了,它相当于信号的 RAW(原始数据格式)。

WAV 文件(波形文件)应该是最接近这种原始格式的文件了,它没有对数据额外的压缩,由一个文件头构成后,剩下的都是原始的 PCM 数据,对播放参数进行配置后将它写入声卡就可以直接播放出声音来了。

WAV 的文件头如下(C++ 引用 PainterEngine Audio 代码)

struct WaveHeader  
{  
	pt\_uchar riff\[4\];				//data exchange flag
	pt\_dword size;					//filesize-header 
	pt\_uchar wave\_flag\[4\];			//wave 
	pt\_uchar fmt\[4\];				//fmt
	pt\_dword fmt\_len;				//
	pt\_word tag;					//format
	pt\_word channels;				//
	pt\_dword samp\_freq;				//sample rate
	pt\_dword byte\_rate;				//samplerate\*bytes of per sample
	pt\_word block\_align;			//channles \* bit\_samp / 8  
	pt\_word bit\_samp;				//bits per sample
};  

其中,最关键的几个参数是 channels,samp_freq 和 bit_samp。读取信息头之后,往后搜索 data 字符串,之后的数据都是 PCM 码流了。

WAV 文件与频谱分析

WavFreq 是由 C++ 编写,UI 框架使用 Qt 4.8.6,播放接口使用 DirectSound 的一款简易的声波频谱分析器(因此你需要安装 DirectX SDK)。

每隔 0.1 秒,它会对声波取样 4096 个点并做 FFT 后将频谱图显示。

作为本文的附录程序,WavFreq 的源代码你可以在附件中找到,同时源码遵循 GPLv3 开源协议。

软件界面如图 g.1

然后打开一个 WAV 波形文件,如图 g.2

![](data:image/svg+xml;utf8,)

打开后将显示这个波形文件的一些基本信息,如图 g.3

图 g.3

点击菜单上的 Start Analyse,观察波形频谱,如图 g.4

琴键分析与声学建模

为了更好的讲解频域在声学分析上举足轻重的作用,笔者以一段钢琴按键音作为示范,范例文件你同样可以在附录的文件当中找到。

钢琴中分别标注了七个不同的音,使用 1,2,3,4,5,6,7,8(dou rui mi fa so la xi do)进行标注,它们的频谱分别如下。

从上面的频谱图中我们可以看出,钢琴音的频率有着明显的差别,随着音调的升高,主要的频率也随之右移动。

在这里,我们使用一些非常简单的检波滤波手段来区分这个时候按下的音是哪个。

首先我们先过滤掉那些并不主要的频域信号,例如在上图中,我们主要频域在 150~350 的区间内,同时,我们取 2500000 的度量值作为其阈值 以避免一些噪声的干扰。

这样,钢琴按键的识别过程就简单变为了:

当前频幅度量大于 2500000 时,在 150~350HZ 区间内找到幅值能量最高的点,依据该点所在频率,滤出一些关键的特征频率,并依此来判断按键的类别。

具体的代码与实现你仍然可以在附件中找到,运行结果如下图所示(图 h.1)

被泄漏的密码

如果你有看过那些间谍大片也许你对下面这个镜头场景并不陌生, 例如《谍影重重》中就有这样一个情节,特工想要进入目标的一个保险库,于是他录制了目标人物的声音然后伪造了他的语音密码,成功入侵了目标的保险库。

另一个比较经典的镜头是语音的识别,特工录制了目标的语音,很快就有设备将语音识别成了文字和数字,假如目标在谈话中或者是电话的按键音中泄露了密码,那么他就要倒大霉了。

不管情节如何或者这个到底具不具备可行性,上面两个场景都和声波的频域分析有关,那么从声音还原出密码具备多少的可行性呢。

在很多的直播录制节目中,很多的主播在众多的观众面前输入自己的账号与密码,当然因为密码是 * 字遮盖的,观众无法直接看到密码,但是不少的主播的键盘敲起来是非常的响的(例如青轴的机械键盘),我们可以很清楚的听到敲击键盘噼噼啪啪的声音,有没有可能通过对键盘音的分析建模来还原出密码呢。

笔者认为这绝对是有可能的,首先,回车键 上档键 空格键之类的键因为其模具的不同,其发出的敲击声音的频率肯定是不同的,另外键盘经过长时间的敲打,因为磨损与杂质的不同其击键音也有可能改变,最后一点是一个人长期的使用键盘,其对各个键的击键的力度也是不一样的,并且录音设备与键盘各键的距离不一样,很可能从声音的能量再做进一步的筛选。

当然,也许完全识别出密码也许有困难,但是对主播击键音长期的采样分析建立模型,极大可能的缩小筛选范围是绝对可行的。

当然,更好的声音采样器(采样精度,采样频率)对分析的成功率也有更多的帮助,以此看来劣质的麦克风反而还保护了主播们的密码隐私了。

笔者还未做过相关的实验,读者有兴趣的话,不妨试试.

FFT2

二维冲击采样

如果我们把冲击函数拓展到二维的上,显而易见的,它应该满足这样的公式:

![](data:image/svg+xml;utf8,)

还记得之前提到一维的采样函数么,我们对原函数进行了采样操作,那么拓展到二维方向,其冲击采样应该写成了这种格式

![](data:image/svg+xml;utf8,)

如果对离散的函数进行采样,那么公式就由积分变为了累加

![](data:image/svg+xml;utf8,)

如果对特定样本点进行采样例如只需要对二维冲击函数进行移位就行了

二维傅里叶变换对

那么我们很容易也将傅里叶变换推到到二维的方面上来

其中,

![](data:image/svg+xml;utf8,)

是一个大小为 M*N 的矩阵,当然,逆变换同样有

因此,二维的傅里叶变换过程的算法就能简单的概括为:

首先依次对一个二维矩阵的每一行做傅里叶变换,得出变换的结果后,再对行变换结果的每一列做傅里叶变换。

当然,为了方便计算机处理,其正变换伪代码如下

1. 设二维数组包含待变换的数据

2. 对二维数组的每一行做傅里叶变换,并将变换结果替换原数组。

3. 将二维数组的行与列元素互换。

4. 再对该二维数组的每一行做傅里叶变换,并将变换结果放回原数组。

5. 再将二维数组的行与列元素互换,其结果即为二维傅里叶变换结果。

用 C 语言实现二维傅里叶变换对

二维傅里叶变换的 C 语言代码如下,当中引用了之前编写的一维傅里叶变换函数,当然有了之前的基础,二维傅里叶变换的代码简单的多,您可以在本文的附录中找到其完整的代码:

void FFT\_2(\_IN complex x\[\],\_OUT complex X\[\],int N)
{

	for (int i=0;i<N;i++)
	{
		FFT(&x\[i\*N\],&X\[i\*N\],N);
	}
	//Matrix transpose
	for (int cy=0;cy<N;cy++)
	{
		for (int cx=cy+1;cx<N;cx++)
		{
			complex \_t=X\[cy\*N+cx\];
			X\[cy\*N+cx\]=X\[cx\*N+cy\];
			X\[cx\*N+cy\]=\_t;
		}
	}

	for(int i=0;i<N;i++)
	{
		FFT(&X\[i\*N\],&X\[i\*N\],N);
	}

	//Matrix transpose again

	for (int cy=0;cy<N;cy++)
	{
		for (int cx=cy+1;cx<N;cx++)
		{
			complex \_t=X\[cy\*N+cx\];
			X\[cy\*N+cx\]=X\[cx\*N+cy\];
			X\[cx\*N+cy\]=\_t;
		}
	}
}

void IFFT\_2(\_IN complex X\[\],\_OUT complex x\[\],int N)
{
	//Matrix transpose
	for (int cy=0;cy<N;cy++)
	{
		for (int cx=cy+1;cx<N;cx++)
		{
			complex \_t=X\[cy\*N+cx\];
			X\[cy\*N+cx\]=X\[cx\*N+cy\];
			X\[cx\*N+cy\]=\_t;
		}
	}

	for(int i=0;i<N;i++)
	{
		IFFT(&x\[i\*N\],&X\[i\*N\],N);
	}

	//Matrix transpose again
	for (int cy=0;cy<N;cy++)
	{
		for (int cx=cy+1;cx<N;cx++)
		{
			complex \_t=X\[cy\*N+cx\];
			X\[cy\*N+cx\]=X\[cx\*N+cy\];
			X\[cx\*N+cy\]=\_t;
		}
	}

	for (int i=0;i<N;i++)
	{
		IFFT(&X\[i\*N\],&X\[i\*N\],N);
	}
}

数字图像的相位与频谱

频谱能量与相位

相信读者从高中物理中经常看的到的能量公式,常常带有平方关系,例如,能量是质量与速度的平方关系,能量是电压的平方与电阻的关系。

在图像中,我们常常也使用平方关系来表示 “能量” 这一关系,但这并不是说这张图确实带有物理上的多少能量,它更像一种比喻,就像古时群朝大臣跪地大喊吾皇万岁万岁万万岁一样,或者避讳称皇帝叫“万岁”,但至少到今天,也没有哪个皇帝有那能耐活得到一万岁的,这顶多只是口头上的一个溜屁拍马,或者说叫起来方便。

图像上的能量也是类似这种关系,它更像是某种量化,并不是指它具体带有的能量,不然得话,如果你想炸掉某样东西,你只要往他们那寄一张白纸就可以了(毕竟白色所带的能量最高),更恐怖的是,要是你敢撕掉你的作业本,它甚至会引起一场爆炸。

回到正题,在图像的频谱中,我们应该如何表示这种能量的关系呢,显然的,傅里叶变换的结果是一个复信号,如果求其能量,仅需要对其实部与虚部求平方相加,当然,在频谱图中我们更多的是取复数的模。也就是对其能量进行一次开平方。

当然,仅仅有频谱是无法表示一个复信号的,因此我们还需要引入相位的概念

其值是虚部除以实部的 arctan 值,当然,这也就表明它的范围是 []。

最后仍然值得提到的一个细节是,二维傅里叶变换的结果是呈现中心共轭对称的。(你仍然可以套用之前一维傅里叶变换的证明方法来证明二维的共轭对称性),证明过程在本文就不再复述了,但由共轭对称性我们就很容易推到出频域图的中心对称性(直流分量部分除外部分)。

图像频域与相位有什么物理意义么?

实际上《数字图像处理》中有章节专门讨论这个问题,总结为图像频域表现的是灰度信息,而相位则是位置信息,频域的低频表示图像基本的灰度变化,例如一个图像灰度变化平缓也就意味着其频率较低,而频域的高频则体现其灰度剧烈变化的部分例如物体的边缘,星空的繁星(或者叫噪声?)。

因为本文讨论的并不是如何对图像进行哪些模糊锐化… 之类的操作,因此更多的信息我们这里不再进行更多的讨论,我们仅仅需要知道,图像的能量主要分布在了低频当中,对频域不过分的操作并不会特别影响图像的视觉效果就可以了。

FFT2 DEMO 程序演示

有了上述的理论基础,对图像进行二维离散傅里叶变换的过程也就水到渠成了,笔者编写了 DEMO 程序,你可以在本文的附录中找到这个 DEMO 程序和它的代码,在该程序中,首先打开的界面如图所示(i.1)

选择 File-àOpen 来打开一个图片文件,如图 i.2

![](data:image/svg+xml;utf8,)

在这里,就以笔者参照动漫《龙与虎》逢坂大河所绘制的同人图为例(非专业画师,不喜勿喷),因为是彩色图片,因此,我们分别对其的 RGB(Red Green blue)分量进行分离,分别对它们做二维傅里叶变换如图 i.3

其中,左边第一张图是原图,第二列是原图的 RBG 分量,第三列是其各分量的频谱图,第四列是其相位谱。

FFTShift

当然,在我们观察到的频谱图中,我们更希望将频谱显示的更易于观察,在上述的频谱中,其低频分量位于左上角,且因为傅里叶变换的共轭对称性,我们同样很容易推导出二维傅里叶变换是中心对称的,假设我们将低频分量移动到原点上向外为高频,那么图像就会更加的易于观察。

FFTShift 的算法并不复杂,它实际上仅仅是对矩阵进行分割后(分割为 4 部分),对对角线的两部分进行两两交换。如图 i.4

例如如下矩阵

![](data:image/svg+xml;utf8,)

经过 FFTShift 后就变为了

![](data:image/svg+xml;utf8,)

例如如下 4x4 矩阵

![](data:image/svg+xml;utf8,)

经过 fftshift 后变为

![](data:image/svg+xml;utf8,)

经过 FFTShift 后,DEMO 的频谱与相位图亦发生相应的改变

可以看到,原本分散于四个角的低频信号经过移位到中心后,变得更加的易于观察了。

对数变换

有句话叫真理往往掌握在少数人的手中,在图像的二维傅里叶变换的频谱能量图中,从上个章节我们很容易观察到,频谱的能量往往在中心才较为的明显(低频域),显然的,图像的能量往往集中在低频当中。因为低频与高频的能量差距过大,为了便于频域图像的观察,我们对频域进行对数操作,并对操作的结果缩放到 0-255 的灰度级别中,假设

![](data:image/svg+xml;utf8,)

表示图像频域复信号的模值(就是能量开根号),那么对数变换就是

![](data:image/svg+xml;utf8,)

下图是经过了对数变换的频域图像,可以看到,其频域灰度变化变得更加的平缓了(图 i.6)

鲁棒盲水印

盲水印与版权

图像水印被广泛运用在了防伪,签名,标识等版权保护方面,简单来说就是防止盗图狗窃取自己的劳动成果或者将他抓个现行。毕竟网络大了什么样的人都有,被人窃取成果反而署名上其他人的名字是一件极其令人愤慨的事情。

目前的水印主要是明水印(可见水印)居多,例如下图(j.1)使用的就是明水印

![](data:image/svg+xml;utf8,)

明水印加起来简单方便并且快捷,使用 Photoshop 来处理应该是一件非常简单的事情,不需要太多的技术含量就可以给这张图 “冠名” 了

当然,简单的事情往往攻击起来也很简单,稍微懂点的盗图者很容易就能把明水印删除并还原原图,实在嫌麻烦可以直接裁剪图片,很多时候裁去水印并不会对图像整体的视觉上造成多大影响(如图 j.2)。

当然,最烦人的是,水印常常破坏对原图的视觉享受,这在影像原画作品中,常常是不能被观众容忍的,一个作品突然冒冒失失的加上一个水印,常常导致观众看起来就像心理有个疙瘩。

那么有没有水印技术,直接看不见,经过处理后就变得可见了呢。

这实际就是本文另一个要讨论的技术细节,实际上这种盲水印技术在小学的动手实验上就有,最知名的应该是用米汤写字,写完后纸上是看不见的,如果要显示信息,那么就用点碘液一洗,字就变蓝色显示出来了。

使用流程图来表示这一过程(如图 j.3 j.4)

在数字图像中,目前非常流行的一种盲水印隐写技术,是对图像像素操作的,其具体流程是,因为一个颜色具有 RGB 分量,一般来说,RGB 三个颜色通道每个各占 8 位,也就是三字节,对 8 位的最低位做修改,对原图的影响是微乎其微的,那么,每个像素就能带有 3 位的信息,一张 100*100 的 24 位位图,就能够带有 30000 位 3750 字节的信息可插入,如果将水印数据插入到这里,就可以实现一个简单的盲水印了。

这种盲水印技术具有一定的可行性,但是,它的缺点仍然也是非常致命的,对原图像的旋转,平移,缩放,改变色相都能非常轻易地摧毁水印。

于是,具备抗攻击的盲水印技术,也就孕育而生了。

基于傅里叶变换的鲁棒频域水印

从之前的章节我们已经了解了如何利用二维傅里叶变换将图像变换为频域,并且我们也了解了图像的能量主要集中在低频当中,那么如果我们将水印叠加在频域的高频中,理论上对原图的视觉上并不会有过多的影响,同时,在频域中的水印散列分部在空域(也就是逆变换后的图像)的各个部分,对频域水印的破坏将会变得更加的困难,同时,频域水印对图像的裁切,旋转,平移,加噪都有一定的抗攻击能力(详细的推到可翻阅《数字图像处理》一书),因此,频域水印作为一种盲水印手段是拥有其理论依据的,对于这种具有一定抗攻击能力的水印技术,我们又称之为鲁棒水印。

最后,作为讨论的细节,如何将水印叠加到频域也是应该讨论的范畴,在这里,长话短说地总结以下几点:

1. 对彩色图像加水印,首先对图像的 RGB 颜色通道分别分离,对 R,G,B 三个通道的颜色分别计算频域,就和前几个章节处理的那样。

2. 叠加混合的方式分为两种,称之为缩放叠加,一种为放大一种为缩小,因为二维傅里叶变换后的结果是一个复信号,因此,如果我们仅仅修改频谱能量而不影响其相位的话,应该将复信号的实部与虚部同比例放大或缩小就可以了。因此,水印也就是安装水印轮廓对对应复信号进行放大或缩小

3. 峰值信噪比(PSNR),归一化相关系数(NC 值)可用于判定水印对原图的影响及水印的抗干扰能力,两者都是越高越好,当然,这常常是一个相互矛盾的度量,往往抗干扰能力强也意味着对原图的干扰大。

水印程序 DEMO

ImageSigner 是一款由笔者开发的图像水印程序,作为一个演示的 DEMO 程序,它仅仅支持 256*256 大小的图片,你可以在本文的附录中找到它的完整源代码,您可以通过查阅源代码,来了解频域水印的具体算法及过程,它使用 C++ 与 Qt Framework 4.8.6 编写完成,遵循 GPLv3 开源协议。下面介绍其水印签名及各种攻击效果。

1. 打开程序,界面如图(k.1)

2. 点击 Open Source Image 和 Open Sign Image 分别加载源图与签名图片(图 k.2 k.3)

载入后界面如图所示(k.4)

在签名控制面板,设置签名的参数(图 k.5)

![](data:image/svg+xml;utf8,)

其中,第一栏表示签名的通道,第二栏为签名水印的混合模式,一般来说,enlarge 对签名图像的抗干扰能力更强,Reduce 模式抗干扰模式较弱,但 Enlarge 模式对原图的影响较大。

Power 一栏表示对签名的混合能量(值越大表示水印越明显),一般来说,能量越大对原图的干扰也较大。

选择完成后,点击 Do Sign 进行签名(图 k.6)

在右侧你可以看到签名后的图片,点击 Save to file 可以将签名后的文件保存。 最后,我们选择签名的图像,并分别使用裁剪,平移,旋转,噪声,涂抹,改变色相,缩放攻击并查看攻击后水印的保留效果。

下面图 k.7 为原图,k.8 k.9 分别为缩小及放大混合签名后的图像,可以看到视觉区别不大。

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

同时,签名后图像的频谱如 k.10 K.11(放大水印只加在蓝色通道)所示

使用 PhotoShop 对图像进行攻击

1. 裁剪攻击与其频谱 k.12(蓝色通道)

![](data:image/svg+xml;utf8,)

1. 平移攻击及其频谱

1. 旋转攻击及其频谱

1. 高斯噪声与频谱

1. 涂抹攻击与频谱

![](data:image/svg+xml;utf8,)

1. 改变色相攻击与其频谱

![](data:image/svg+xml;utf8,)

1. 缩放攻击(裁剪后放大至原尺寸)与频谱

![](data:image/svg+xml;utf8,)

后记

文章到了这里也许你对当中的技术意犹未尽,也许你看的一脸云里雾里不知所云,但不管如何,《从三角函数到语音识别再到数字水印》到了这里也应该告一段落了。

我一直有个愿望,把一个看上去复杂的东西,从它最原始的最简单的根基开始,抽丝剥茧,循序渐进,一步一步地深入直到它开花结果,这个过程,就犹如见证了一场破茧化蝶。因为我相信,那些深入宇宙奥妙与规律的知识,是足以让人震撼的,它并不输于任何不可估价的艺术品。

遗憾的是,如今国内不论是学术还是技术都有一股浮躁之风,有人将其当做宣扬炫耀的筹码,有人甚至剽窃他人的成果与劳动,作为自己谋利的肮脏台阶,在我看来,他们都不是真正的 “为学” 者,我希望有更多的人,能够沉下耐心,将前辈的知识打碎发扬,将那些看起来遥不可及的智慧,分解成一步一阶的台阶,让更多的人更快更简单地得到智慧的福泽。而不是光顾着告诉大家我掌握了这个技术有多么“了不起”。

我并不知道读者们阅读本文时,是否能收到我所传达的意思与愿望,本文行文时间将近三个月,当中每一个文字每一行标点每一句代码包括每一张例示图片都出自本人之手都凝聚着我的心血,但我并非圣贤,文章并不能做到句句严谨,行行精辟,相信有不少的缺漏错误还待读者们斧正,但知识理当能够有所发扬,如果您在我的字里行间最终有所感悟并了解了 “她” 是怎么一回事,那么,您的一句 “写的不错” 将是我最大的欣慰与鼓舞。

最后

我还想再说些什么,却有些词穷了,我将 ImageSigner 做更进一步的改善,让它能够支持到 2 次幂高宽的图像数字签名。当然,你仍然可以在附件中找到它的完整源代码与 Release 版本并将它运用在实际的需求中。附件下载地址:https://pan.baidu.com/s/1pL6tgjp

如果您有什么相关的疑问或讨论,可以来群:689194365 找我,如果想进入黑客的世界,我推荐你们观看这个专题:你想了解的炫酷白帽黑客技能都在这!

行文至此,那么,就这样吧。

知乎用户 shotgun​ 发表

其实如果防御做得好,想绕过还真的没那么容易。按照 DLP,也就是数据防泄密的思路:

1. 重要的图片或者文档内加水印,文件里面再加上全息数字水印(这个大家都说得很全面了)

然而这就足够了吗?或者说只有这些防护?乃义务!

2. 所有终端监控敏感操作:另存、截屏、剪贴板、对敏感文档的操作等等,上述行为一律上传到审计服务器上备份待查。

3. 禁止未授权(未安装终端防护程序)的终端接入内网和业务系统

4. 有敏感操作或者跨终端登录的时候通过摄像头拍摄人脸(知道为什么扎克伯格要贴摄像头了吧)

5. 定期对硬盘进行文件扫描,关键字、相似度、OCR 等方法识别出可能是敏感或者涉密的文件,并提取特征,进行标定。

6. 同样采用散列值、关键字、相似度匹配等方法检查所有网络出口:邮件、聊天、网站、网盘等,对传输内容全部解码进行审计并且记录。

7. 检查或者关闭所有物理出口,USB、Wi-Fi、蓝牙、音频口(防止通过耳机口盗窃数据)。

8. 所有文件落地加密(文件加密或者全盘加密),偷硬盘也没用。

9. 所有电子设备进出办公区域全部审核检查。

10. 核心机密文档不落地,存放于核心服务器,采用远程终端或者虚拟窗口方式查阅 / 编辑。

别笑,富士康和华为基本上就是这么干的。

知乎用户 bosskwei 发表

欢迎收看别人的女朋友系列

我找到一个对付最高赞童鞋的方法→_→

@fuqiang liu

[9 月 22 日晚第四次更新:根据最高赞童鞋的建议增大了能量系数,更新内容见下文第 4 条]

[9 月 21 日晚第三次更新:之前一天课忙死我了没有仔细做,现在做了对比参照,整理如下]

[9 月 21 日早上第二次更新:特别说明下,上面的 matlab 代码在 2013b 下运行结束得到结果后会导致 matlab 闪退,这就说明代码可能有什么问题,因此这里的实验结果可能也存在问题。稍后有空我会尝试全部用 OpenCV 重写一遍再次测试是否可以复现,目前仅当提供一种思路]

1. 效果(第一张图是加过水印再 SVD 的效果,第二张图是提取效果)

奇异值 ==10

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

奇异值 ==20

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

奇异值 ==40

奇异值 ==50

奇异值 ==70

奇异值 ==90

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

(注:图像大小为 300×240,总计有 240 奇异值)

2. 方法

× 把加过水印的图片 watermarked image.jpg 做通道分离处理,分成 RGB 三通道

× 对三个通道分别求 SVD,每个通道分解出 240 个奇异值

× 对这 240 个特征值由大到小排序,只保留每个通道的前 x 个奇异值,将 x 以后设置为 0

× 把 SVD 分解得到的 sigma,u,vt 相乘,得到三个通道,再把三个通道合成 RGB 图

× 处理完成,奇异值少的图像会丢失了一些细节

× 尝试提取水印信息,在只保留前 40 个奇异值的时候效果最好

×SVD 的物理意义与代码:

奇异值的物理意义是什么? - Boss Kwei 的回

3. 数学原理(猜想)

无论如何,信息量是守恒的,对于一张图片来说,其展示的内容始终占据最大信息量,无论加什么水印进去,只要加进去的信息量比原图小,都可以用这种方法把隐写信息当成噪声过滤掉;当然如果加进去的隐写信息数据量比原图还要大,这种方法就无效了,当然这样也会导致肉眼就可以看出原图的噪声,这样的隐写也就没有意义了

4. 对水印加强,此时图片出现明显噪点,但可以突破上文去水印的办法(方法:按照最高赞所说,增大 alpha 能量系数

(第一张是加过水印后的图,第二章是水印提取结果)

能量系数 ==4 && 奇异值 ==40

![](data:image/svg+xml;utf8,)

![](data:image/svg+xml;utf8,)

能量系数 ==8 && 奇异值 ==40

![](data:image/svg+xml;utf8,)

知乎用户 沈万马​ 发表

这个玩法太多了啊。

牛逼一点的,每台电脑部署一个内部软件负责在屏幕上写一些信息就好了。一个像素可以表达 24 位信息,考虑隐蔽性只表达 3 位信息,考虑冗余度 4-16 个像素表达 3 位信息。一个员工,甚至一台设备,也最多只需要 18 位的信息就可以单独定位,也就是给 96 个像素就绰绰有余了,而你的屏幕往往有百万到千万级别的像素…

更不提另一个技术答案中提到的散频方法,无视具体像素,直接将信息插到频域中,连你放大缩小图片都可以随意抵抗。

按照阿里在客户端上一贯的玩法,都不怕这个程序被杀,和你的公司鉴权系统整合起来就好了,你杀了这个功能你就不能访问公司内部数据。

不那么牛逼的话,这一套也至少可以玩到浏览器里。客户端管不了,还管不了服务器端么?每个人渲染的网页都不同就行了。当然可以用像素嵌入信息,但也可以不用屏幕信息嵌入,玩法还可以很多——

每个人看到的文字中都会插入伪随机空格,空格的位置可以定位到个人身份。

每个人看到的页面布局都有差异,内容位置横轴、纵轴浮动几个像素,几个元素之间的相对位置关系,都可以传达信息。

每个人看到的画面都水印了二维码、条码之类的东西。

甚至,最差还可以每个人看到的页面用不同的元素、背景、字体组合。

再弱菜一点,我们还可以在审计上动脑筋呀。什么时候谁看到什么内容都是可以追踪的,什么时候进行了截图操作也是可以追踪的。组合一下,大部分嫌疑人就筛出来了。

就算这么做都很麻烦吧,还可以折腾画面以外的东西呀。又没人能保证阿里的人说的都是是实话。说不定就是有人举报了,看到谁截图了呢?

其实问出来具体阿里会用什么方法也没用,因为方法太多,阿里可以每天换个玩法… 就像密码本一样每天轮换两三次,让你就算知道所有可能的做法,你要防御追踪也要折腾很久,导致等你敢安心放出截屏的时候消息已经没有价值了,就行了。这才是信息攻防最后的战线。

要防御所有这些方法,你唯一的希望在于降低信息噪音。原理上,这些手段在信息角度看,都是在噪音中嵌入信息的方法。因为你几乎不可能防范所有这些噪音中的特定信息,你只能彻底丢掉噪音。那么最直接的方法就是用 OCR、画面处理、二值化等等。但是,处理之后虽然没有噪音了,不幸的是你也失去了证明信息来自内网的能力,因为内网环境对信息来说也是噪音的一部分。

——————————

写这么多就是想告诉同行,技术上想坑你是躲无可躲的,所以不要相信 HR(读作组织部)大于技术的互联网公司。不要自投罗网,是躲开控制的唯一手段。

知乎用户 田文泽 发表

问了下阿里的朋友

其实就是页面上有个 8 开头的五位工号

原图没抹去

知乎用户 iCoA 首席特工​ 发表

我有个办法可以破坏掉水印,就是截屏之后打印出来,然后再拍照或者扫描到电脑里,水印就没了。


以上为原回答,一个偶然的机会,我发现其实阿里的水印非常简单,我做了一个复盘:

“月饼事件” 里阿里巴巴内网肉眼不可见的水印的原理原来是这样的 - 逍遥峡谷

知乎用户 单金伟 发表

在讨论区周围找 5 个位置,每个位置用 css 分别左右上下,各两个 px, 表示 0-9 的数字,然后这样最多可以表示 99999 的工号,然后拿截图,进行像素级比对,就能知道工号了。

“按 HR 提的需求” ,在阿里,技术人员的地位,看来是除了技术人员的其他部分的共识啊,以前阿里的产品也说过类似的话:不要把技术当人看

知乎用户 YD Yang 发表

在知乎玩儿 wargame 也是有创意,不过师兄

@fuqiang liu

你放种图就不应该了啊(大雾)。

解码思路大概是这样的:先找到原图,然后和原图在频域处理一下,就可以得到下面的 ** 链接了

才怪嘞!师兄可是个守法的知乎青年。送给大家一个励志良言:

知乎用户 李小春 发表

你们答的好专业啊,我大学也学过图像处理,信息隐藏,但是基本忘了。

这个客户端我没去研究过,但我猜没那么高深,因为网页版内网我稍微看过一眼,水印就是 css background,还搞了 oncontextmenu、onselect 等事件让你不能复制。

HR 不要来找我,我只是 F12 看了几眼代码。

知乎用户 大湿 发表

关键字:数字盲水印

具体到实现可以有多种手法

我之前用 DCT 变换给图片加水印, 可以抵抗一定程度的攻击(涂抹)

如果算法高级点,还可以抵抗旋转,放缩等形式的攻击

这个水印肯定对图像质量有所影响,比方说在文字的边缘会有毛刺,或者背景中有些颜色很浅的色块

但是考虑到只需要含有 20bit(还不到 3byte) 的信息就够了,所以这个影响是很小的,肉眼难以察觉

如何规避?

1. 如果这个水印是以引入背景图片的形式插入的,直接 f12 禁用水印图层

2. 楼上说的图片另存为二色图,应该也是有效的,一般的水印很难抵挡住这种程度的攻击

我又想到另外一种形式的水印

对原文进行微调,比方说修改标点符号或者 “的地得” 的替换,总之让每个员工看到的文字都与原文有细微的差别,发现泄密后与原文做对比,就能得到工号

当然实际中受限比较多,难以应用,纯属脑洞罢了

知乎用户 Eidosper 发表

首先,假如阿里有 4294967296 个人,也不过需要一个 int32 的信息量就可以判断。

然后,根据一些其他信息,可以精确到部门,这样甚至也不过是 256 人,那就变成了一个 byte。

在一张动辄上 mb 的图片里面,隐藏一个 int32 的信息量,不要太简单。最简单的一些做法,比如文字换行规则,比如要求一行满 20 字就强行换行表示 0,不是 20 字表示 1。这样只需要 32 行就可以记录一个 int32 的数据量。

甚至,我们可以以汉语词汇来表示。比如用 “规则” 和“规矩”作为 0 和 1。我们总能找到高频词汇,然后找一组同义词。甚至可以用 “的得地” 来表示编码。这样的措施甚至连拍摄屏幕都无法去掉。

毕竟 1MB 是 4byte 的几十万倍,在百万乱军中藏身一个小卒不被发现,真的是太容易。

知乎用户 董浩亮 发表

插一句,排名第一的答案已经对原理解释得很清楚了。很多人难以理解阿里花这么大力气去开发这套技术仅仅是为了防止员工泄露内网信息?

其实这套技术最早是应用在淘宝防盗图的功能上的。卖家常抱怨淘宝上有人盗图,当时淘宝就想开发一个服务解决这个问题。具体原理是:

1. 卖家购买这项服务后,上传的图片会自动打上不可视水印,水印中包含卖家 ID。

2. 如果有其他卖家盗用了这张图,在上传后会检测是否含有水印,还可以进一步根据水印 ID 得知是盗用了谁的图,然后处罚盗图者。

这项服务最终有没有上线我不知道,请淘宝卖家补充。

知乎用户 bird​ 发表

上面的回答中,识别码都是页面叠加 “隐形” 图片。

我有一个新的思路,毕竟他们的内网是以文字为主的而不是图片。所以如果在网页文字显示上面做点手脚,比如行距、字间距、或者标点符号前后间距搞点错位,以此记录工号信息,就算是屏摄也可以抗。

知乎用户 JHack 发表

我来提供一个不需要数学就能实现的信息隐藏到图片的方法:LSB(最低有效位替换)算法。当然这个方法有很多局限性,但是非常「巧妙」。

文件字节流

首先我们要来认识一下文件的字节流这个概念,这也是这个算法最关键的部分。

如果已经掌握了这个概念,可以跳过。

在我们学习 C 语言时,肯定接触过文件处理,比如我们可以用二进制的方式打开一个文件

FILE \* fp = fopen("/dest/to/a/file.p","rb");

什么叫做 “以二进制的方式”?

事实上任何文件的 “内容” 都是一串二进制,你可以理解为这个文件的“基因”。如果两个文件,它们的基因相同,那么它们就是同样的文件(携带相同信息的文件)。

这串二进制就是文件的字节流。

而这个文件将被如何解析或者说如何识别,则取决于它的后缀名,当然本质上后缀名只是告诉操作系统应该用哪个应用程序打开这个文件,然后那个应用程序会用某种方式去解析这个文件。

举个例子,你从某个网站下载了一首歌,假如就叫它 baba.mp3 吧,当你双击打开它时,某个播放器就会尝试去解析它,播放器首先会拿到这个文件的字节流,然后按照所有 mp3 文件的「字节流编排规则」把其中包含的信息读取出来,比如你能发现有的 mp3 你能看到它的封面、专辑信息、作曲家等等,有的没有。实际上这些信息都存放到了一个 mp3 文件的「ID3V2」中,这是个什么东西大家不用管,简单来说就是 mp3 文件的前面几个字节里面包含了这些信息,比如说 baba.mp3 的作曲家叫做 jhack,那么你就会在它的字节流的前几个字节中找到这么一串二进制:

0110 1010 0110 1000 0110 0001 0110 0011 0110 1011

这就是 “jhack” 的 ascii 码的值。

播放器按照 mp3 的规则把这些字节流解析完成后,就可以进行播放了,你就能听到 abaaba 的美妙声音。

但是如果你手贱,把这个文件的后缀改成了 mp4,现在就变成了 baba.mp4,当你打开这个文件的时候,播放器就不会再用 mp3 的解析规则去从它的字节流获取信息了,它会用 mp4 的规则去读。那么刚才那一串表示作曲家的 0110 1010 0110 1000 0110 0001 0110 0011 0110 1011,可能就会被 mp4 播放器认做其他信息了(大概率会报 “文件损坏” 之类的内容,因为播放器无法正确解析 mp4 的 header 信息,认为这是一个不合法的 mp4)。

总之,文件的字节流包含了它所有的信息,而这个文件将如何「表现」,就取决于你如何去使用这一串二进制内容。

比如我们见过有些图片,你把它的后缀改成. zip,你就能得到很多很好的学习资料的种子。那就是这个图片的字节流「恰好」和这些种子按照 zip 算法压缩后的字节流一模一样,因此你修改了图片的后缀,但是字节流是没有变化的,zip 解压缩工具就能成功解压里面的内容(如果有这样的图片,请借给我研究一下)。

位图的字节流

好了,上面介绍了一大堆文件字节流的概念,如果还有点懵懵懂懂的,我们来看一个更直观的例子:位图(bmp)是如何被解析出来的。

图片实际上就是像素点的集合。比如一张

的图片(忽略显示器分辨率的影响),我们能看到的就是

那么显然对于一个图片浏览器,它要显示一张图片,它就需要每一个像素点的信息。

像素点能有什么信息呢?也就是颜色和位置。只要我们知道了这张图片包含的所有像素点的颜色和位置信息,那么我们就能将这张图片「画」出来了,就像拼图一样(每一个像素点就是一个图块,这个图块有且只有一种颜色)。

那么一张位图是如何存放这些像素信息的呢?这里我们考虑最简单的位图:24 位位图

我们知道,在计算机中,颜色可以用 RGB 来表示,也就是红绿蓝通道的值。如果你用过 PS 或者任何可以在里面定义颜色的软件,你都会发现 R、G、B 的取值范围都是 0-255。

比如某个像素点,它的 R=255, G=0, B=255,那它就是紫色,因为红 + 蓝 = 紫

又比如 R=255, G=0, B=128,它就会变得比紫色更红一些,因为红色占的比重更高。

而对 2 的幂敏感的同学(通常是程序猿)不难发现,RGB 用二进制来表示的话,每个颜色恰好可以用 8 位二进制表示,因为 8 位二进制能表示的范围恰好是

,那么 RGB 就能用 8*3=24 位二进制进行表示了。

也就是说我们能用 24 位二进制表示一个像素点的颜色。颜色的问题解决了,位置呢?位置非常简单,因为尺寸是已知的,以 200 x 300 的位图为例,每 200 个像素排一行,共排了 300 行。

我们把读取到的像素点,按「从左至右,从上到下」的顺序依次排列出来就行了。严格定义为:以左上角为坐标系原点,我们读到的第 k 个像素点,它的坐标就是

举个例子,如果一张 3 x 2 的位图(为了例子的简单,假如这位图就这么离谱),它的颜色是这样的:

那它的字节流就长这样(括号里表示的是这 8 位代表的颜色值,每 3 个 8 位代表一个像素点的 RGB):

00000000(R) 00000000(G) 11111111(B) 11111111(R) 00000000(G) 00000000(B) 00000000(R) 11111111(G) 00000000(B) 11111111(R) 00000000(G) 11111111(B) 00000000(R) 11111111(G) 00000000(B) 00000000(R) 00000000(G) 11111111(B)

是不是很形象?

当然位图文件的字节流肯定不是只有像素点信息,它还有一些头部信息,比如必须的:尺寸信息,得存到字节流里面吧?

位图的头部信息通常是 58 个字节,也就是说你用二进制的方式打开位图,从第 59 个字节开始读取到的就是像素点信息了。

哦对了,1byte = 8bit,1 字节 = 8 位二进制。

LSB 算法

我们终于到了重头戏。

我们之所以要花那么大的篇幅介绍文件字节流和位图,就是因为 LSB 算法是依赖于这两个概念的。LSB 算法通常用来将文件隐藏到位图中(这篇回答也将以此为例),其实它的核心是信息隐藏,事实上我们可以用它隐藏任何信息。

在计算机世界中,几乎所有信息都可以用二进制来表示,常见的,一个字符串,我们可以用 ascii 码或者 unicode 的二进制来表示。

因此,我们要隐藏信息,实际上就是想办法把一串二进制隐藏起来即可。

LSB 算法利用一种巧妙的方式,将这些二进制信息隐藏到了位图中,并且骗过了我们的眼睛(人眼无法察觉隐藏了信息的位图和原图之间的区别)。

LSB 算法非常简单:将位图的每个像素点的 RGB 值的最低位替换为要隐藏的信息。

例如我们要隐藏 010010 这个信息到上面那个蓝红绿紫绿蓝的 3 x 2 位图中,那么位图字节流的前 6 个字节:

00000000 00000000 11111111 11111111 00000000 00000000

的最低位被替换为了(我用红色标出了被替换的内容,也即我们要隐藏的 010010)

也就是说对于原位图中的每一个 RGB 值,其值的改变最大为 1(如果其最低位和要隐藏的信息一样,就不会改变,要改变的话只可能从 0 变为 1 或者从 1 变为 0),这样的改变对颜色的影响是微乎其微的(比如 R 从 212 变为 211),人眼几乎看不出其中的变化。

而越高位的改变,对值的影响就越大,颜色的改变就越大,因此我们只考虑对最低位进行操作,这也是这个算法被叫做 最低有效位替换 算法的原因。

亿些细节

  1. 在实际隐藏(编码)之前,我们同时要考虑到时候如何解码。一个最直接的问题是:我们解码时需要获取多少长度的内容?考虑上面的例子,我们只隐藏了 6 位二进制,但是我们解码时并不知道我们的 for 循环应该循环多少次,总不能写死一个 6 在那吧?因此在对要隐藏的具体内容(data)进行编码之前,我们首先要将长度信息进行编码。也就是说我们要隐藏 010010 之前,要先将 6 这个数(二进制为 110)编码到位图字节流中。也就是实际上我们要编码的整个内容为 110010010,前面 3 位 110 表示我们编码的内容长度为 6,这样在解码时,我们首先获取到前 3 位,将其转换为十进制得到 6,这样我们的 for 循环就循环 6 次,就能成功还原 010010。

  2. 这里肯定有小伙伴注意到了,在获取长度信息时,我怎么知道是前 3 位还是前 4 位还是前 n 位表示长度信息?是的,如果我们再用一些二进制来表示「长度的长度」,就会无限套娃下去,最后变成:要编码「长度的长度的长度的长度的长度…」。但是我们注意到,这个长度是有范围的。我们在编码时,位图的字节流每 8 位二进制可隐藏 1 位信息,也就是说我们的位图可以隐藏的最大信息量为

    (这里 size 为位图的总字节数,58 为位图的 header 的字节数,它不能用来隐藏信息),我们可以用这个数来计算我们最多需要多少位二进制来表示长度。而在实际实现中,往往为了方便,我们会直接约定一个值来表示长度,例如我们约定前 32 个字节的最低位表示长度信息,那么我们在解码时,首先获取前 32 个字节的最低位,拼成一个二进制数,这就是这个位图中实际隐藏了多少信息量。

  3. 我们还可以把一个 magic number 隐藏到开头。比如我们在实际隐藏之前,先把一个奇怪的数字(随便你喜欢哪个数字都可以,越复杂越好)例如 10100101 这个数隐藏进去,这样我们在解码时,先解析前面 8 位内容,如果恰好等于 10100101,那么说明这个位图中确实隐藏了信息,我们再执行接下来的解析步骤,这是因为任意一张位图,我们用解析算法拿到的前 8 位恰好是 10100101 的概率是很小的(这个 magic number 越复杂,概率越低)。如果解析出来的前 8 位不等于这个数,就说明这个位图中根本没有隐藏内容,用户不知道从哪里找来一张图片丢给我们解析了。

算法实现

我们只需要使用 C 语言的一些标准库就能实现这个算法,当然你可能注意到了,这个算法的实现会涉及很多位运算,只要你对位运算熟悉,这个算法就可以很容易的实现出来。

这里我们来实现把一个文件隐藏到位图中。之前我们说了,文件内容实际上就是这个文件的字节流,我们要隐藏文件,只需要隐藏它的字节流即可,到时候只要把这些字节流还原成文件就相当于把原来的文件解析出来了。有点像星际迷航中的远程传送:把人的粒子信息传输到目的地,然后把原来的人「击碎」,再在目的地用粒子信息「重组」这个人,就相当于这个人完成了传送。

首先我们来实现一些有用的工具函数。

我们实现这个函数用来读取一个文件的二进制内容并返回这个文件的大小(in byte)

typedef unsigned char Byte; // 「字节」通常表现为无符号类型,长度为8位,因此我们用unsigned char来表示Byte
typedef Byte \*           BytePtr;
long file\_buffer(const char \* file\_path, BytePtr \* file\_ptr) {
    FILE \* fp = fopen(file\_path, "rb");
    if (fp == NULL) {
        return 0;
    }
    
    // 当我们用fopen打开一个文件时,会有一个类似游标的东西用来指向当前我们正在访问这个文件的哪一个字节
    // 如果把文件字节流理解成一个一维数组的话,游标就像数组下标一样
    // fseek函数可以设置这个游标的位置,这里我们设置到文件末尾,因为我们想要获取这个文件的大小
    fseek(fp, 0, SEEK\_END);
    // ftell函数可以告诉我们当前游标处于哪个位置,结合上面设置到了文件末尾,那么返回值就是文件的大小了(byte)。
    long file\_size = ftell(fp);
    // 当然我们在使用fread函数时也是从当前游标位置开始读数据的,所以记得要把游标设置回文件开头
    fseek(fp, 0, SEEK\_SET);
    
    // 下面三行代码使用过fread的同学肯定再熟悉不过了
    BytePtr file\_buffer = (BytePtr)malloc(sizeof(Byte) \* file\_size);
    fread(file\_buffer, file\_size, 1, fp);
    fclose(fp);

    // 我们让外部传入的指针(是一个指向指针的指针)的内容(是一个指针)指向存放我们读取到的文件的内存
    // 这样调用方就能获取到在函数内部分配的堆内存
    // 记得在外部free掉这个指针(这里的设计不是很好,按理说应该谁分配谁治理,
    // 不过这个只是我们内部使用的一个私有函数而不是一个公共函数,先将就这样吧)
    \*file\_ptr = file\_buffer;
    return file\_size;
}

接下来我们肯定会频繁地修改某个字节的二进制形式的某一位,我们用这个函数来修改一个数 byte 的二进制形式第 index(从最低位算起)位,将其设置为 val,其中 val 为 0 或 1。

例如 byte = 23,其二进制形式为 00010111,val = 0, index = 0,则这个函数返回 00010110。熟悉位运算的同学不难看出这个函数为什么能正确得到结果。

Byte set\_bit\_val(Byte byte, unsigned char val, unsigned char index) {
    if (val) {
        return byte | (1 >> index);
    } else {
        return byte & ~(1 >> index);
    }
}

接下来我们就可以分别实现解码和编码的主要逻辑了。完整代码如下:

#include <stdlib.h>
#include <stdio.h>

typedef unsigned char Byte; // 「字节」通常表现为无符号类型,长度为8位,因此我们用unsigned char来表示Byte
typedef Byte \*           BytePtr;

// 如果要定义一个全局常量的话尽量使用静态常量代替宏定义,这是一个优秀的习惯
const static unsigned int \_magic\_num = 0b11011011;
// 位图的前58个字节(header)我们看都懒得看
const static int \_bmp\_offset = 58;
// 我们用32位二进制来表示隐藏的文件大小
const static int \_file\_size\_block = 32;

// 这个函数用来读取一个文件的二进制内容并返回这个文件的大小(in byte)
long file\_buffer(const char \* file\_path, BytePtr \* file\_ptr) {
    FILE \* fp = fopen(file\_path, "rb");
    if (fp == NULL) {
        return 0;
    }
    
    // 获取文件大小
    fseek(fp, 0, SEEK\_END);
    long file\_size = ftell(fp);
    fseek(fp, 0, SEEK\_SET);
    
    BytePtr file\_buffer = (BytePtr)malloc(sizeof(Byte) \* file\_size);
    
    fread(file\_buffer, file\_size, 1, fp);
    fclose(fp);
    \*file\_ptr = file\_buffer;
    return file\_size;
}

long bmp\_buffer(BytePtr \* bmp\_ptr) {
    char \* file\_path = "/Users/DreamHack/Desktop/IMG\_4917.bmp";
    return file\_buffer(file\_path, bmp\_ptr);
}

long target\_file\_buffer(BytePtr \* file\_ptr) {
    
    char \* file\_path = "/Users/DreamHack/Desktop/input.pdf";
    return file\_buffer(file\_path, file\_ptr);
}

Byte set\_bit\_val(Byte byte, unsigned char val, unsigned char index) {
    if (val) {
        return byte | (1 >> index);
    } else {
        return byte & ~(1 >> index);
    }
}

void encode() {
    BytePtr bmp\_ptr = NULL;
    
    long bmp\_size = bmp\_buffer(&bmp\_ptr);
    if (bmp\_ptr == NULL) {
        printf("读取位图失败");
        return;
    }
    // 插入magic number
    int mn = \_magic\_num;
    for (int i = 0; i < 8; ++i) {
        // 获取mn的最后一位
        unsigned char k = mn&1;
        
        bmp\_ptr\[\_bmp\_offset + i\] = set\_bit\_val(bmp\_ptr\[\_bmp\_offset+i\], k, 0);
        mn = mn >> 1;
    }
    
    int offset = \_bmp\_offset + 8;
    
    // 读取要隐藏的文件二进制流
    BytePtr file\_ptr = NULL;
    long file\_size = target\_file\_buffer(&file\_ptr);
    
    if (file\_ptr == NULL || file\_size == 0) {
        printf("读取文件失败");
        return;
    }
    
    // 插入文件大小信息
    long tmp\_file\_size = file\_size;
    for (int i = 0; i < \_file\_size\_block; ++i) {
        // 获取大小的最后一位
        unsigned char k = tmp\_file\_size&1;
        
        bmp\_ptr\[offset + i\] = set\_bit\_val(bmp\_ptr\[offset+i\], k, 0);
        tmp\_file\_size = tmp\_file\_size >> 1;
    }
    
    offset += \_file\_size\_block;
    // 插入文件信息
    
    for (int i = 0; i < file\_size; ++i) {
        Byte file\_byte = file\_ptr\[i\];
        for (int j = 0; j < 8; j++) {
            Byte digit = file\_byte & 1;
            bmp\_ptr\[offset + i \* 8 + j\] = set\_bit\_val(bmp\_ptr\[offset + i\*8+j\], digit, 0);
            file\_byte = file\_byte >> 1;
        }
    }
    
    // 现在数据已经全部读到内存了,将bmp\_buffer里面的内容写入新的bmp图片中
    
    FILE \* fp = fopen("/Users/DreamHack/Desktop/secret.bmp", "wb+");
    fwrite(bmp\_ptr, bmp\_size, 1, fp);
    
    fclose(fp);
    free(bmp\_ptr);
    free(file\_ptr);
}

void decode() {
    // 获取magic number,看这个文件里面是否有我们隐藏的文件
    BytePtr secret\_ptr = NULL;
    long secret\_size = file\_buffer("/Users/DreamHack/Desktop/secret.bmp", &secret\_ptr);
    
    Byte magic\_number = 0;
    int offset = \_bmp\_offset;
    
    for (int i = 0; i < 8; ++i) {
        // 读取每个字节的最低位
        Byte digit = \*(secret\_ptr + offset + i) & 1;
        magic\_number += pow(2, i) \* digit;
    }
    
    offset += 8;
    
    if (magic\_number != \_magic\_num) {
        printf("该位图中没有隐藏的文件");
        free(secret\_ptr);
        return;
    }
    
    // 获取文件大小
    long file\_size = 0;
    for (int i = 0; i < \_file\_size\_block; ++i) {
        // 读取每个字节的最低位
        Byte digit = \*(secret\_ptr + offset + i) & 1;
        file\_size += pow(2, i) \* digit;
    }
    
    offset += \_file\_size\_block;
    
    // 还原文件
    BytePtr file\_buffer = (BytePtr)malloc(sizeof(Byte) \* file\_size);
    
    for (int i = 0; i < file\_size; ++i) {
        
        Byte byte = 0;
        
        for (int j = 0; j < 8; ++j) {
            Byte val = \*(secret\_ptr + offset + i\*8+j) & 1;
            byte += pow(2, j) \* val;
        }
        
        // 10100111
        memset(file\_buffer + i, byte, 1);
    }
    
    
    FILE \* fp = fopen("/Users/DreamHack/Desktop/output.pdf", "wb+");
    fwrite(file\_buffer, file\_size, 1, fp);
    
    fclose(fp);
    free(secret\_ptr);
}


int main(int argc, const char \* argv\[\]) {
//        encode();
//        decode();
    return 0;
}

main 函数中,先执行encode(),我们就将 input.pdf 的内容隐藏进了 IMG_4917.bmp 这个位图中,并且输出了一张新位图 secret.bmp,它和IMG_4917.bmp长的一模一样(至少我们人眼看不出变化),再运行decode(),我们就从 secret.bmp 中解析出了 output.pdf,然后你就会发现,input.pdf 和 output.pdf 是一模一样的。

试试看,真是挺神奇的!

这个 demo 还有很多可以完善的地方,比如可以任意选择文件路径、比如把文件名信息也隐藏到位图中,这样我们解码时就可以根据文件名来写文件等等。

Pros & Cons

LSB 算法的优势在于:

  1. 实现简单。看看我们只用了用 100 多行原生的 C 语言代码就实现了这个算法的核心部分,剩下的也就改改用户体验了。
  2. 信息隐藏不会改变文件大小。我们将信息隐藏进位图的方式是「替换」原来的位图信息,因此隐藏前后位图的大小不会发生改变,这就更难看出里面隐藏了一些东西了。

LSB 算法既然这么简单,它当然存在一些缺陷。

  1. 对攻击和压缩毫无抵抗力。一旦有噪声攻击或者压缩发生,其中隐藏的内容就极有可能(几乎一定)会丢失。我们可以使用 DCT(离散余弦变换)来对抗,这里就不做介绍了,感兴趣的小伙伴可以去了解一下 LSB+DCT 实现的文件隐藏。
  2. 隐藏的信息内容较少。前面我们看到,一张位图每 8 位只能隐藏 1 位信息,相比于其他数字水印算法,其隐藏的内容实在太少了。我们可以考虑用视频来代替位图,我们将视频拆为一帧一帧的位图,抽取视频的关键帧,利用 DCT 抗压缩,隐藏信息后,将这些位图还原为「无损视频」,再对其进行压缩。

知乎用户 徐平政 发表

高票答案把细节都说的差不多了,但是还有两点。

第一,根据高票答案中,水印提取中需要原始图片来看,这不是盲数字水印,而是非盲数字水印。如果是盲数字水印,提取水印不能够使用没有加载过水印的原图。

盲水印的检测方法大概是这样,先把待检测图片变换到频域,水印图片也需要变换到频域,两者在频域进行相关运算,然后把运算结果和一个阈值进行比较确定图片中是否存在水印。

第二,如何通过水印检测不同员工。我们不可能给每一个员工分配同样的水印,不同员工图片中嵌入的水印应该尽可能差异大,这样才能保证我们在检测水印时不会造成误判。学术圈里面还把这个东西叫做数字指纹,digital fingerprinting,这东西十年前被马里兰大学一个实验室的老板和学生灌了好多篇论文,大老板还搞了个 IEEE Transactions on Information Forensics and Security,╮(╯_╰)╭ 基础论文在此

Anti-collusion Fingerprinting for Multimedia, IEEE TRANSACTIONS ON SIGNAL PROCESSING, VOL. 51, NO. 4, APRIL 2003

http://sig.umd.edu/publications/trappe_anticollusion_200304.pdf

知乎用户 鄢波 发表

内部员工来回答下,其实就是页面上有工号和名字,那个月饼泄露的家伙去掉了工号和名字,但是字和字之间的间距还有一些残留,放大然后调整下图片锐度之类的,就能看到,一个技术同学找出来的

知乎用户 Wildcity 发表

似乎已经出现可以对抗屏摄的加水印方法了,似乎用于电影屏幕的居多

知乎用户 张抗抗​​ 发表

这让我想起来了七年前的数个深夜为女神熬夜做的那份得了 100 分的基于小波分析的数字水印的大作业。

知乎用户 猫咪他大爷 发表

百万级浏览量的问题,我也凑个热闹啊。

数字盲水印也叫数字隐水印,是一种将原始保护的确权信息添加进数据文件中,数据文件在流转过程中即使遭遇各种鲁棒攻击,依然能提取出原始信息的一种技术。

@fuqiang liu

列举了各类详细鲁棒性攻击,大佬级答复,有理有据,令我等技术宅男佩服。但我发现最大的问题是:信息还原必须同时具有原始图片及隐水印图片,俩对照才能提取原始信息。但是在很多场景下,原始图片获取几乎不太可能。另外我们也使用了傅里叶变换频域变换,结果发现抗攻击能力有限。其他还有厂商打广告的答复,扫一眼就知道做得东西太皮毛,我们就直接忽略了。

基于我们兴趣组研发的隐形水印,也可以做几组抗鲁棒性实现,验证在只有单单的水印图片情况下,如何将原始保护的确权信息做到隐匿性及鲁棒性达到最优平衡。

我们从百度图片随机找了一张美女照片,不要问我为什么。志玲姐是我等技术宅男心目中永远的女神(科学测试,无意冒犯志玲姐肖像权,敬请谅解!)

这是原始照片

利用我们的 DEMO 平台 DWCTSP 算法,把下述原始确权信息写入,这只是示意,理论上收集那些原始确权信息都可以自定义,而不限于用户名、IP 地址、浏览器环境,甚至还可以包括经纬度信息、IMEI 号、MAC 地址等隐私信息。请注意,随时保持对用户隐私信息的尊重。在用户同意并之情情况下才能测试。

原始图片叠加原始确权信息合成的水印图片如下:

我们可以来一个原始图片及处理后的图片对比,肉眼几乎看不出任何差别,皮肤还是那么水嫩和细腻,志玲姐还是那么美:

再来一组放大 300% 后的照片对照

下一步,我们来开展攻击实验(还是那句话,实验目的,无意冒犯志玲姐姐肖像权),为区别于大佬的攻击实验,我这边就不一步一步测试,直接叠加复核式攻击

1. 进行涂抹攻击,这是攻击后的图片:

得到原始确权数字序列图片如下,将数字序列解析便可得到完整的原始确权信息。请注意,区别于其他答友的实验室做法,我们是将原始确权信息加密保存至数据库,图片内仅记录数字序列,目的是增加原始确权信息的安全性,同时增加抗攻击能力。

实验证明,轻松应对涂抹攻击。

2. 进行剪切攻击,外带第一步的涂抹攻击,裁剪 + 进一步涂抹复合型攻击的图片:

得到原始确权数字序列图片如下,

从数字图片我们可以看到,确实有部分信息丢失。但是不耽误原始信息还原。那能否来个极端挑战,裁剪 90% 图片呢?从图片看,我们仅保留了 248*64 面积,相对原始图片 500*751,裁剪了 96% 的面积

得到确权数字序列图片如下,从图片来看,数字序列仍完整保留了,可完整还原原始确权信息。

3. 基于第二步极端裁剪 96% 的面积,然后再增加饱和度攻击、透明度攻击、色相攻击,这些数值都是随机调的,如下图:

得到处理后的残缺图片如下:

我们再在 Demo 演示系统内还原的原始确权数字序列如下:

依然还是可以轻松完整还原原始确权信息。

未完待续……

知乎用户 thinkant​ 发表

应该除了隐藏到图片还有加密吧。

将你的个人信息比如工号先加密然后将加密后的信息隐藏到图片里。这样你从图片里即使提出出隐藏的信息,你也不知道是什么工号。

知乎用户 js xs 发表

阈下信息通道,本身检出就不是容易的事情,何况是在不知道编码和加密方式的前提下,对检出信息做臆想的唯密文攻击?

这个事情理论上是无解的,只有非技术手段,诸如拷打阿里的涉及这段编码的前端工程师;或者这个前端工程师手抖把代码放 github sourceforge 上了。

在不知道加密方式的前提下做唯密文分析就是耍流氓!

在不知道加密方式的前提下做唯密文分析就是耍流氓!

在不知道加密方式的前提下做唯密文分析就是耍流氓!

知乎用户 知乎用户 2sJ43F 发表

每个人看到的 [的得地] 不一样就足够用来编码了。

知乎用户 Belleve​​ 发表

文字网页也可以干啊,可以往字体里面藏,比如某些字的笔画位置会随着工号变化而变化

知乎用户 匿名用户 发表

傅立叶做梦也不会想到两百年后自己的研究成果会被人用来对付员工吧?

知乎用户 匿名用户 发表

说下我们单位的做法:

1. 禁止未授权(未安装终端防护程序)的终端接入内网和业务系统。(内网准入,防止员工自带设备接入内网)

2. 所有文档图纸设计文件文件落地加密,当然我们为了员工电脑性能和实际泄密后果综合考虑用了 AES 加密,加密强度不是特别高。(不然电脑会卡死啊,即使电脑配置挺高的,做透明加密还是很费性能的)

3. 所有终端监控对加密文件的敏感操作:另存、截屏、剪贴板、对敏感文档的保存操作等等,上述行为一律上传到服务器上备份。

4. 定期对硬盘进行文件扫描,关键字、相似度、识别出可能是敏感或者涉密的文件,或者你搞了什么奇怪的软件,提取特征,进行标定。

5、所有终端检测加密文件被生成压缩包,压缩包自动加密(因为要是给压缩包设置了密码,谁也打不开检查了是不,总不能全天监控键盘输入)

6. 邮件、聊天、网站、网盘等,对传输内容全部解码记录

7. 核心机密文档不落地,存放于服务器,只能用远程终端查阅,下载编辑需另行授权。(有些项目只能在云桌面里设计)

8. 封杀各种试图直接穿越检测连外网的方式,比如 PPTP 协议,L2TP 协议。(防止用 VPN 穿透防火墙)

9. 系统内置一个单位自己的 CA 证书,强制所有终端 HTTPS 流量都用单位内置的证书加密。(这样你 https 流量单位就可以了解密看,而且区分是哪台终端的流量)

不过,对手机之类拍照用 3G/4G 流量出去倒是没管。

知乎用户 匿名用户 发表

是网页版的就是一个 css background 水印,图片内容就是你的工号。

1、仅仅去掉水印 so easy,用 chrome 把 css 干掉就行。

2、想坑别人(比如某 hr),你可以通过 Charles 把这个图片代理掉,改成其他人的工号。这样一来直接有人背锅,以 hr 们的智商,根本不会想到这招。

3、被开的那个人根本就是水印工号没抹干净,你仔细看,还是能看出来工号是 8420*,并不需要什么高深的技术。

4、高票的回答们简直给 hr 们打开了新的世界:原来除了水印还能这么玩!以后估计会按这个思路升级一版。

知乎用户 江城雨 发表

最狠的办法是直接写进 GPU 显卡驱动,根本不靠什么 css 所有显示内容全带水印。

知乎用户 匿名用户 发表

其实知乎网站早就做了牛逼的肉眼不可见的全局水印,大家都不知道吗?

就比如这个问题好了

魔术看起来神奇,但解密后就非常无聊了:

在网页颜色为 rgb(255,255,255)(纯白) 的全局区域平铺颜色 rgb(254,254,254)(几乎纯白) 的图片 / 文字水印

这是一步解题过程:

此方案集诸多优点一身:隐匿效果拔群,开发实现轻松,解密还原步骤简单。

毕竟,再复杂点的解密方式 HR 就不会弄了,还得反过来找开发者解增加额外工作量。

所以还是那句话,在做任何功能的时候,要考虑到需求方的实际水平。

知乎用户 王絮飞 发表

阿里巴巴的操作其实简单多了,没那么复杂,就强制装监控工具。

知乎用户 Hao Qiao 发表

我觉得,多找几个人的截图,取平均,估计会效果很好。

知乎用户 拉动内需小能手 发表

所有页面显示工号姓名水印,就是这么简单粗暴。这不是分析,这是实践经验,无数公司就是这么做的。不懂那个工作量大且防范能力极低的方案是怎么成为高票的。那个明明是版权保护技术。

知乎用户 胡继续 发表

想到一个对付截图的办法,在 windows 图形驱动层做,屏幕上显示的一切内容均叠加一层不可见数据。

知乎用户 hoodlum1980 发表

(1)因为可以从截屏中提取,所以,可见信息是嵌入在页面的图像中的。也就是说,可能是你看上去是白色的背景,但实际上这个背景就是一个图片。这是我能想到的最可能加进去东西的地方。这个图片可能是背景,也可能放在哪个你截图无法避开的位置。总之,是个图片。

(2)因为截图上传之后,大多是 jpg 格式,意味着这个信息必须能抵抗得住 jpg 的压缩。

(3)这个图像里的信息能够和已有的人眼看到的图像信息进行分离。比如说,图像是 a,信息是 b,截图后的结果是 a + b = c; 你必须能仅从 c 分离出 b。

a 最好是常量就是最佳。此外,b 还应该放在不管你截取什么地方,都有位于截图之内,所以文字背景是我能想到的最稳妥的地方。

举个例子吧,二维码,用小方块是黑或者白来表示 0 , 1。那么你可以做一个白色的背景,然后把表示 0 和 1 的东西用亮度相对接近的灰度来表示。

(4)每个员工登录后有自己的信息(水印),因为员工数量很多,所以这个图像可能是服务器动态生成,或者生成一次后可能就保存在服务器端。然后用动态页面返回给客户端的。

-—

但是不管怎么说,这个新闻,把阿里公司的这层手段给出卖了!这对阿里来说是个非常严重的损失,属于严重泄密。今后,阿里的员工要截图内网信息发到外网,这种手段应该会失效了!因为大家都知道,屏幕图像里有员工水印,这个东西因为要隐藏自己,必然是脆弱的,要干掉它就是很容易的。

再说下水印信息,比如说是员工工号,假设是 8 位数字(0~9)组成的字符串,那么每个 byte 可以表示两个数字。意味着 4 byte (32 bits )可以表达 8 位数字组成的字符串。

有其他答案提供的思路是变换到频域。

我这里举个简单例子,就是在时域进行简单的叠加,

要把 32 bits 嵌入到一个图像中,可以每个小方块里放一个 bit。小方块可以取 8 * 8 像素或者 16 * 16 像素这样大的小块。在这样的小方块里嵌入 0 和 1 就可以。也就是说,只要在压缩后,我依然能识别出这个小方块表示是 0 和 1 就可以了。所以这里也可以进行一些设计,比如背景看起来是个花纹,通过花纹模式的不引人注目的区别等等。

知乎用户 白乔 发表

估计从华为引进的技术吧!

知乎用户 cpp 肥兔 发表

看到了

@fuqiang liu

高强度的回答,我甚是佩服(一脸懵逼),学院派从原理角度已经完全诠释清楚了(我猜的,看不懂,感觉好高深)

我来给提供一个工程实现版(低配)

正好我公司做文档安全(DLP 的一个子领域)有需求给 Windows OS 中窗口贴水印,

废话不表,直接看 demo,保客官一眼看懂

PS: 工资好久没涨了,没钱换装备,没钱买软件

知乎用户 令狐葱 发表

虽然 “技术本身并不可耻”,但是我还是建议此贴应对阿里 HR 屏蔽。

@vczh

知乎用户 周默 发表

只需要在白底 RGB255,255,255 上放一个水印颜色为 RGB254,255,255 即可,显示屏根本看不出来,放到 ps 反向,再曝光度一拉到顶就出来红色字

知乎用户 liao149 发表

屏幕矢量水印技术

深圳联软科技开发的相关防泄密解决方案,具体相关发明专利见下面图所示

这才是题主想要的答案。

另外对着屏幕拍照防泄密的,还请专业人士来解答,不过在华为里面确实有人因为对着屏幕拍照后,通过屏幕水印上的信息被找到警告和被抓的。

知乎用户 MrZhang 发表

参考最高赞的回答,自己写了个 c++ 版本的 (包括 fft 与 ifft),给个 star 不过分吧

https://github.com/17343149/Fast-Fourier-Transform

知乎用户 匿名用户 发表

张勇最初的【震惊】内网贴,是被谁转发出来的?建议严查,一并开除。

知乎用户 匿名用户 发表

老王表面上是一个 J 国大报跑时政的摄影记者,真实身份其实是被 R 国重金收买的间谍。J 国对网络的监控无处不在,他很小心地没有使用加密电子邮件或社交网络 密语传送机密情报。而是把情报通过技术手段隐写在自己的摄影作品图像文件里。每次他的摄影作品进入商业图库,被各大网媒展示在门户首页时,他的情报就神不知鬼不觉地传送出去了。

知乎用户 天下第二 keng 发表

简单来说就是图片里藏了人眼分辨不出的水印。大概率不是在文本部分,而是在图片部分。下次截图别截到头像等图片就行。另外注意看看网页背景是不是其实是一张空白图片,那样背景也危险。还是想截图,又不想被抓,最简单的就是把把图片拆成 rgb 图,然后只设定几个颜色,所有接近该 rgb 值的点全部变为该值。那么水印就没了

知乎用户 谢药 发表

已经有其他帖子说过了爆料人是忘了自己抹掉水印。但我想说根本不需要向高赞说的那么复杂,也能找到人

知乎用户 疯輕雲淡 发表

阿里巴巴云壳系统了解一下,具体就不多说了,强制安装在工作电脑的监控程序,无法卸载,无法阻止其运行,估计是云壳有数据痕迹

知乎用户 庆元侬​ 发表

有水印的,比如钉钉截图你看下就知道了,会有你的名字工号水印。

钉钉就是秉承这种思路诞生的产物。

其它内网什么的类似

知乎用户 赛虎信息科技 发表

可能是使用了加密软件。据我了解,现在市面上的加密软件都是有监控功能的,可以详细记录员工电脑从开机到关机的全部过程,如果员工以截图的方式泄露了公司机密,那么加密软件管理端后台都是可以看到他是以什么形式外泄的,是什么时间外泄的,所有记录可以一目了然。

知乎用户 乳酸君 发表

图片上模糊的工号结合服务器访问日志,基本就可以定位了

知乎用户 bg2vx 发表

有商业化软件么,如 StegSpy,Stegdetect 这样的。有没有分析图像隐藏信息的大型软件,或 matlab 成型代码。

知乎用户 w2014​ 发表

大概就是差一两个色块吧

专门应对程序员大佬们

像我这种连截图都不会,拿上个世纪的手机拍屏幕的当然是无所畏惧啦

像我这种家里用着 CRT,一拍照屏幕上全是线条的当然是无所畏惧啦

好吧我也不是阿里的员工就是了……(๑> <๑)

知乎用户 flyingchaos 发表

无语,一堆人拿图片加密扯淡装逼,能搞清楚场景再说嘛。

阿里内网只是提供一个网页,这截图程序是手机或三方提供的,阿里再牛逼也不能修改员工手机里的截图软件去做频域变换,就是网页里的 css 做了一些手脚,肉眼不可见的颜色或者字体渲染都可行。

用 ocr 软件识别可破,屏摄不行。

知乎用户 18 号 发表

热门回答就是瞎回答,我只看着笑笑不说话

知乎用户 Jim Liu​​ 发表

可以搞一组………… 欧姆龙环???(逃

知乎用户 FDrag0n 发表

盲水印吧,之前有幸学习过抗干扰的盲水印算法。

知乎用户 木已初夏 发表

我只知道是一种隐藏式的水印,就好像爱奇艺你看电影之前会提示:此影片加了隐藏水印,请勿传播或复制,否则他们能根据水印还原找到传播者。更专业的话,这不是我的领域不清楚。

知乎用户 周周 发表

普通 qq 截图里面有 qq 号码吗?有硬盘 id?

知乎用户 Vkki​ 发表

目测屏摄即可破解。

知乎用户 牧天​ 发表

看到这么多回答,还有排名第一的 2.5k 个赞,觉得答非所问。

从事件中曝光的截图是来自于阿里内网 APP 的。

每个帖子都加了以工号为背景的样式。

而曝光的截图通过图像处理手段把工号抹去了,但是,没有处理完全,留下了残迹。

通过图片反相,查看到了残留的工号。

知乎用户 匿名用户 发表

@fuqiang liu

fuqiang liu 的答案。

给他变暗了一下。

以下图片

能看出来空白部分是不一样的

他的技术原谅我看不懂。我只会把图片这样改一下。如果看到,原答主能否测试一下这些图片?

这是一个被修改了的图片

你的图片我改了最右面的高亮 1 和次亮 2

知乎用户 公孙鹏 发表

翻了翻下面的评论,貌似没有人回答

@fuqiang liu

水印为何对称的问题

我来试着答一下

因为在 encoding watermark 的时候使用了对称加密算法 (从 Matlab code 里看出来的)

知乎用户 匿名用户 发表

各位别瞎猜了,小二部门的没那么复杂,就是和我下图一样,所有的信息会打上类似这样的水印就是了. 水印是每个人的花名没有工号,另外图片里也没藏神马神秘代码之类的. 另外截屏也不会有其他答案里的什么上传服务器备份. 蚂蚁我就不知道了. 那哥们就是自己没删干净.

知乎用户 知乎用户 2WUFyh 发表

要不特工怎么都用胶卷相机呢 XD

知乎用户 匿名用户 发表

卧槽细思极恐。那万一我先滤了波照着隐写别人的水印再发呢?

知乎用户 勃呆茓​ 发表

第一次感觉信息隐藏这门课上学到的东西被应用到了实际中…

知乎用户 知乎用户 Y5XGfm 发表

后台页面记得用手机摄屏,专治各种编码水印

最简单好用的 VPS,没有之一,注册立得 100 美金
comments powered by Disqus

See Also

关于丰县的朋友圈

2月8日 早在90年代中期,我在成都商报时,基本每年都会派记者跟随四川公安的“打拐办”,跟踪采访解救被拐卖妇女。拐卖妇女的故事很复杂,有的是被人贩子在车站码头哄骗或强迫,有的是从家乡被人贩子从家里“买”走,有的是以招工为名骗走,有的是精神失 …

丰县事件第四次通报引发了什么? | 舆论手札

今天写这个东西,主要是总结一下丰县事件第四次通报之后的舆论局面。虽然第四份通报延续了简陋的行文风格,但它所节略的部分——也就是缺乏基本事实的描述——激活了两股舆论,它们在提供小花梅的故事版本,形成公众印象上,开始了公开的激烈竞争。 第四份通 …

刘学州事件中的媒体和网络暴力

海边的刘学州(来源:刘学州的微博) 01 — 前不久写了《鹿道森之死》,今天又看到被亲生父母遗弃、被霸凌的刘学州在三亚自杀去世的新闻,心情久久不能平静,不禁想起这么大的中国,这么大的世界,还有多少这样的孩子经历着类似的痛苦。 而在这流着鲜血 …

为什么大数据不能帮他找儿子?因为法律不支持

北京一名阳性病例的流调轨迹牵出一个令人泪湿眼眶的故事: 中国新闻周刊报道截图 为了寻找失联的大儿子,抚养上六年级的小儿子,赡养瘫痪的父亲、多病的母亲,撑起这个风雨飘摇的家庭,这位44岁的河南汉子在北京扛沙袋、搬水泥,运建筑垃圾…… 深夜出 …