近来,一神奇友人与我提起,欲藏信息于图像,以作拷贝追踪之用也。大致为加一层肉眼不可见,然可以检测出来之标识,亦称为数字水印技术。之前虽未涉猎,然觉得有趣,遂决定尝试一番。同月,广电总局居然也和NexGuard签署了水印保护协议,本人不才,目前也是影视技术研发一员,顿觉此技术约莫也是有用的,何须耗费财力于外邦也?摸索三日,得一粗陋之结果,虽觉毫无创新之处,然也有所得,遂记之~
逻辑流程
数字水印的意义在于,一般都是通过在图像的频域上进行操作,隐藏性特别好。相当于把水印的空间域信息加到了原图的频域信号上。而且在低频上加载几乎不影响原始图像的空间域信息,肉眼识别度非常低。查了一些网上的资料,大多都是通过matlab实现的,我觉得matlab实用性不高,因此想用openCV,但是C++又太啰嗦,只是玩玩而已,不要太认真。于是最后决定用Python下的openCV在图像的频域实现的。因此需要先安装好OpenCV和numpy。其大致流程很简单:
- 将原始图像变换到频域,比如通过离散余弦变换,傅里叶变换等。
- 将水印图案做混淆处理之后,加入到原图像的频域系数中。
- 逆变换行程带有数字水印的图像
相应的提取流程:
- 将含有水印的待检测图像和原始图像同时变换到频域。
- 根据原来的嵌入方式,逆向求出混淆以后的水印。
- 还原混淆的水印图案。
水印嵌入
DCT变换
DCT变换,多的就不说了,具体可以参详此处。由于是彩色图像,我采取的一个简单的方法就是提取RGB分量中的B分量,作为欲加载水印的地方,这是由于人眼在RGB中,对B分量敏感度较低,也是为了做起来比较简单。$$I_{b}(i,j)=I(i,j,b)$$
之后通过DCT变换,将其变换到频域,即可待用。
oriImg=cv2.imread("A.bmp")
H=oriImg.shape[0]
W=oriImg.shape[1]
imgB=oriImg[0:H,0:W,2]
dctImg=cv2.dct(double(imgB))
水印图案处理
其实水印图案直接二值化嵌入也是可以的。但是做混淆的目的,是为了防止被别人提取,也为了增加隐蔽性。简单来说,就是把一副图案的像素按照一定规律打乱,但是这个规律必须是可复原的。我采用的是Arnold置乱变换,其要求被置乱的图案一定要是N×N的方图。
$$
\begin{pmatrix}
i^{‘} \\ j^{‘}
\end{pmatrix}
=\begin{bmatrix}
1 & 1\\
1 & 2
\end{bmatrix}\begin{pmatrix}
i \\ j
\end{pmatrix}~Mod~N
$$
Arnold变换是有周期性的,也就是说通过上述公式移动像素,重复一定次数以后就可以恢复成原图,而这个周期也是和图像大小有关的。因此我们可以把移动次数作为密钥来加密置乱之后的图像。在不知道到底置乱了多少次的情况下,及时提取到了置乱的水印,也很难复原水印。另外,Arnold变换是从(1,1)位置开始的,因为(0,0)的话,相当于没有移动。而Python中的图像初始是(0,0)位置,为了避免这个问题,我现在原水印图中加了一圈白边,把N×N的图变成(N+2)×(N+2),但实际处理中还是把它当作N×N来处理。
def Arnold(w0,row,colum,times):
for k in range(0,times):
w1=zeros([row+2,colum+2],dtype=uint8)
for i in range(1,row+1):
for j in range(1,colum+1):
i1=i+j
j1=i+2*j
if i1>row:
i1=divmod(i1,row)[1]
if j1>colum:
j1=divmod(j1,colum)[1]
if i1==0:
i1=row
if j1==0:
j1=colum
w1[i1,j1]=w0[i,j]
w0=w1
return w0
其中w0是加了白边以后(N+2)×(N+2)图,row, colum是原图的尺寸N,times表示置乱次数。
水印原图
置乱90次以后
那么如何求出变换的周期呢?对于N×N的图来说,其周期为:
def period(N):
x=1
y=1
T=1
t=x
x=x+y
y=t+2*y
while x!=1 and y!=1:
T=T+1
if x>N:
x=divmod(x,N)[1]
if y>N:
y=divmod(y,N)[1]
t=x
x=x+y
y=t+2*y
a=T
return a
也就是说,对于196×196的的水印图,其周期为168次,置乱用了90次,复原还需要再变换78次。
嵌入水印
接下来就是把水印信息,加载到之前待用的原图频域中。一般来说,应该尽量把水印信息分散加到低频信号中,这样隐藏度较高,而且对原图视觉上影响较小。数值上解释就是对于置乱的水印图\(W(i,j)\),需要找到一个对应关系把其中的每一个像素对应到频域\(I^{dct}(u,v)\)中。比较好一点的办法,是将\(I^{dct}(u,v)\)进行8×8分块,然后\(W(i,j)\)分散加到\(I^{dct}(u,v)\)中。我这里主要为了测试,因此为了简便,我直接把\(W(i,j)\)加载到了一个相对低频的固定位置。 另外,对于水印的加载,最好不要\(I^{dct}(u,v)=I^{dct}(u,v)+W(i,j)\)这样处理,因为很有可能会溢出。我采取的方法是设置一个系数\(a\),当\(W(i,j)\leq120\),\(a=-1\),否则为1。然后用这个系数来调整\(I^{dct}(u,v)\)的值。最后,再把载有水印信息的图,反DCT变换,赋值到原图B通道,就得到了带有水印的彩色图。
for i in range(0,N):
for j in range(0,N):
if W[i,j]<=120:
a=-1
else:
a=1
dctImg[500+i,500+j]=(1+a*0.01)*dctImg[500+i,500+j]
idct=cv2.idct(dctImg)
final=oriImg
final[0:H,0:W,2]=idct[0:H,0:W]
原图
嵌入水印后的图
以上两图,其实还是有点变化的,亮度方面提升了。一来,是因为水印都在B分量,二来,我也没有分散加到频率信号中,以后实际使用细化即可。
水印提取
水印的提取就是上述过程的逆过程。直接看代码好了,先把原图和带水印的图的B分量都进行DCT变换:
finalB=final[0:H,0:W,2]
dctFinal=cv2.dct(double(finalB))
newOriImg=cv2.imread("A.bmp")
newImgB=newOriImg[0:H,0:W,2]
newDctImgB=cv2.dct(double(newImgB))
之后,根据嵌入时候的加载方式,逆向提取出置乱后的水印。
extract=zeros([N,N],dtype=uint8)
for i in range(0,N):
for j in range(0,N):
a=(dctFinal[500+i,500+j]/newDctImgB[500+i,500+j]-1)/0.01
if a<0:
extract[i,j]=0
else:
extract[i,j]=255
最后,由于我们自己之前的置乱是90次,因此,再调用Anorld变换78次即可恢复水印图。
提取出的水印
这里水印提取出现了一些噪点,我一时也不确定是什么原因,但是大致应该是添加水印的那个方程设计过于随意,导致了一些精度上的丢失,来回变换以后\(a\)的值出现了不准确的情况。反正是个小小的尝试,以后再优化就好了。
OK~就这样简单完成一下数字水印了,如果觉得有用,赶快扫描一下水印二维码,打赏在下一下好了。