LSB隐写算法的实现与性能分析
Presented by R.G.
本项目所有的代码文件均可以在我的Github上找到,建议运行我git仓库里的代码文件,不要直接复制本文展示的代码跑
项目地址:https://github.com/RGNil/RG_LSB
欢迎关注、fork~
注:
-
Github貌似没有原生支持LaTeX,若您在阅读本README时无法正确显示数学公式,请安装支持LaTeX显示的浏览器插件,我推荐 MathJax Plugin for Github 插件
-
如果你的github无法看到图片的话,请参考我的这篇文章
目录大纲
文章目录
LSB算法简介
LSB全称为 Least Significant Bit(最低有效位),是一种简单而有效的数据隐藏技术。LSB隐写的基本方法是用欲嵌入的秘密信息取代载体图像的最低比特位,原来的图像的高位平面与代表秘密信息的最低平面组成含隐蔽信息的新图像。
灰度化的图像为单通道格式存储像素,每个像素值在0~255内,而像素的位平面则是对应二进制的像素的各个位。以上图为例,某个像素的值为78,其二进制01001110
,从左到右位权依次降低,最左边为最高有效位(MSB,其位权为 2 7 2^7 27 ),最右边位最低有效位(LSB,位权为 2 0 2^0 20)。把每个像素的相同位抽取出来组成一个新的平面,就是所谓的图的位平面。而LSB隐写算法,如其名字,是在LSB也就是最低位平面进行信息嵌入/隐藏。
需要注意的一点是,LSB嵌入的时候,载体图像格式应该为灰度图格式
以著名的Lena图为例,一下是灰度图Lena原图:
下面是其各个位平面图,从左到右、从上到下位平面依次降低:
可以看到,位平面越高包含的原图像信息越多,对图像的灰度值贡献越大,并且相邻比特的相关性也越强,反之则相反。LSB最低位平面基本上不包含图像信息了,类似随机的噪点/噪声,因此,可以在此处填入水印/秘密信息。
嵌入示意图如下:
选取不同位平面嵌入时,LSB算法的保真度:
LSB算法的基本特点:
- LSB是一种大容量的数据隐藏算法
- LSB的鲁棒性相对较差(当stego图像遇到信号处理,比如:加噪声,有损压缩等,在提取嵌入信息时会丢失)
常见LSB算法的嵌入方法:
- 秘密信息在最低位平面连续嵌入至结束,余下部分不作任何处理(典型软件MandelSteg)
- 秘密信息在最低位平面连续嵌入至结束,余下部分随机化处理(也称沙化处理,典型软件PGMStealth)
- 秘密信息在最低位平面和次低位平面连续嵌入,并且是同时嵌入最低位平面和次低位平面
- 秘密信息在最低位平面嵌入,等最低位平面嵌入完全嵌入之后,再嵌入次低位平面
- 秘密信息在最低位平面随机嵌入
以上五种方式,当嵌入容量不同时,鲁棒性不同
我改进的LSB算法(RG_LSB)
与标准的LSB算法不同,我在设计我的LSB算法的时候,对嵌入的信息进行了随机嵌入,就是水印图像的比特流在嵌入载体图像时,并不是依次嵌入,而是随机选择位置嵌入。
此外,在嵌入时,对水印图像的比特流还进行了01规范化处理(使得嵌入的比特流0和1的个数一样多)。通过 规范化比特流 + 比特流随机嵌入 的方法,能够使得我的LSB算法具有抗击位平面图分析攻击,这里会在之后写一篇LSB隐写分析的文章中具体解释。
由于我增加了 规范化比特流 + 比特流随机嵌入,因此,为了能够提取水印,在嵌入过程中,我的算法会生成2个密钥文件:比特流规范密钥、嵌入位置密钥。所以,我的LSB算法不仅具有隐写术部分,还增加了密码术部分。
这部分可以结合我的代码来具体理解我的LSB算法流程
前期准备(利用genNeedImg.py生成测试所用图像)
编写genNeedImg.py用于生成做本次实验所需的灰度图/二值图,详细介绍看里面的注释:
def genNeedImg(imgPath,size=None,flag='binary'):
'''
用于生成指定大小的灰度图或二值图, imgPath为图像路径
size为tuple类型,用于指定生成图像的尺寸, 如:(512,512),默认为None表示输出原图像尺寸
flag为标志转换类型,默认为binary,可选的值为binary或gray
'''
imgRow = cv.imread(imgPath)
if size != None: # 调整图像尺寸
imgRow= cv.resize(imgRow,size)
imgGray = cv.cvtColor(imgRow,cv.COLOR_RGB2GRAY) # 转换颜色空间为灰度
imgName = imgPath[9:].split('.')[0] # 获取图像原始名称
if flag == 'gray': # 生成灰度图
cv.imwrite('./images/{}_gray.bmp'.format(imgName),imgGray)
print('Gray image generated!')
else: # 生成二值图
ret, imgBinary = cv.threshold(imgGray,127,255,cv.THRESH_BINARY)
prop = int(size[0]*size[1]/(512*512)*100) # 以载体图像为512x512,算生成的水印大小占载体图的百分比
cv.imwrite('./images/{}_binary{}.bmp'.format(imgName,prop),imgBinary)
print('Binary image generated!')
print('threshold:{}'.format(ret)) # 输出转换阈值
测试genNeedImg.py,并生成所需要的灰度图作为载体图像,生成二值图作为嵌入的水印。在网上随意找了2张图片,左边的hn.png用于生成载体灰度图,右边的xn.jpg用于生成嵌入的二值水印图:
使用genNeedImg.py生成hn的灰度图用作载体图像,设置载体图像尺寸为512x512:
使用genNeedImg.py生成对上述载体图嵌入量分别为25%、50%和100%的二值图用作待嵌入水印:
从左到右,第一张是用作载体的灰度图,接着依次是对载体图嵌入量分别为25%、50%和100%的二值图用作水印:
改进的LSB嵌入算法实现
与标准的LSB算法不同,我在设计我的LSB算法的时候,对嵌入的信息进行了随机嵌入,就是水印图像的比特流在嵌入载体图像时,并不是依次嵌入,而是随机选择位置嵌入。此外,在嵌入时,对水印图像的比特流还进行了01规范化处理(使得嵌入的比特流0和1的个数一样多)。通过 规范化比特流 + 比特流随机嵌入 的方法,能够使得我的LSB算法具有抗击位平面图分析攻击,这里会在之后写一篇LSB隐写分析的文章中具体解释。由于我增加了 规范化比特流 + 比特流随机嵌入,因此,为了能够提取水印,在嵌入过程中,我的算法会生成2个密钥文件:比特流规范密钥、嵌入位置密钥。所以,我的LSB算法不仅具有隐写术部分,还增加了密码术部分
编写LSB嵌入代码lsbEmbed.py:
def genEmbedBinStream(imgEmbed):
'''将嵌入图像抽取成比特流'''
rowScale = imgEmbed.shape[0]
columnScale = imgEmbed.shape[1]
binStreamList = []
for i in range(rowScale):
for j in range(columnScale):
if imgEmbed.item(i,j) != 0:
imgEmbed.itemset((i,j),1)
binStreamList.append(imgEmbed.item(i,j))
return binStreamList
def streamNormalize(binStream):
'''
规范化嵌入流,将嵌入流01比调整至1:1
'''
# binstarm长度为偶数最佳,若非偶数调整出的01比将与1:1有所偏差(但应该对lsb分析来看影响不大)
# zeroPos、onePos分别存所有0、1在binStream中的位置序号
zeroPos = [ pos for pos in range(len(binStream)) if binStream[pos] == 0]
# zeeoPos = [pos for pos,value in enumerate(binStream) if value == 0]
# 这种方式遍历更佳
onePos = [ pos for pos in range(len(binStream)) if binStream[pos] == 1]
zeroScale = len(zeroPos)
oneScale = len(onePos)
# flag 记录 0多还是1多
flag = 1 if oneScale > zeroScale else 0
appendScale = abs(oneScale - zeroScale)//2
key = [flag,] # key保存的时候首先先存一个flag用于标识规范化之前0多还是1多
if flag: # 1多,则把多出来的1置0
key+= [i for i in random.sample(onePos,appendScale)]
for pos in key[1:]: # key剔除首位的flag
binStream[pos]=0
else: # 0多,则把多出来的0置1
key+= [i for i in random.sample(zeroPos,appendScale)]
for pos in key[1:]:
binStream[pos]=1
with open('./keyfile/keyPos.json','w') as fp:
json.dump(key,fp)
return binStream
def binReplace(x:int,b:str,pos:int)->int:
''' int数值指定二进制位替换0 or 1,pos从右(二进制低位)从1开始计数 '''
if b not in ['0','1']:
print(