LSB隐写算法的实现与性能分析
Presented by R.G.
- 强烈建议点击:本README更好的排版和阅读体验 【Github可能不能正常显示 目录、图片、数学公式】
注:
-
本README有少量数学公式需要LaTeX支持,github貌似没有原生支持LaTeX,若您在阅读本README时无法正确显示数学公式,请安装支持LaTeX显示的浏览器插件,我推荐 MathJax Plugin for Github 插件
-
注:如果你的github无法看到图片的话,请参考我的这篇文章
[TOC]
LSB全称为 Least Significant Bit(最低有效位),是一种简单而有效的数据隐藏技术。LSB隐写的基本方法是用欲嵌入的秘密信息取代载体图像的最低比特位,原来的图像的高位平面与代表秘密信息的最低平面组成含隐蔽信息的新图像。
灰度化的图像为单通道格式存储像素,每个像素值在0~255内,而像素的位平面则是对应二进制的像素的各个位。以上图为例,某个像素的值为78,其二进制01001110
,从左到右位权依次降低,最左边为最高有效位(MSB,其位权为
需要注意的一点是,LSB嵌入的时候,载体图像格式应该为灰度图格式
以著名的Lena图为例,一下是灰度图Lena原图:
下面是其各个位平面图,从左到右、从上到下位平面依次降低:
可以看到,位平面越高包含的原图像信息越多,对图像的灰度值贡献越大,并且相邻比特的相关性也越强,反之则相反。LSB最低位平面基本上不包含图像信息了,类似随机的噪点/噪声,因此,可以在此处填入水印/秘密信息。
嵌入示意图如下:
选取不同位平面嵌入时,LSB算法的保真度:
LSB算法的基本特点:
- LSB是一种大容量的数据隐藏算法
- LSB的鲁棒性相对较差(当stego图像遇到信号处理,比如:加噪声,有损压缩等,在提取嵌入信息时会丢失)
常见LSB算法的嵌入方法:
- 秘密信息在最低位平面连续嵌入至结束,余下部分不作任何处理(典型软件MandelSteg)
- 秘密信息在最低位平面连续嵌入至结束,余下部分随机化处理(也称沙化处理,典型软件PGMStealth)
- 秘密信息在最低位平面和次低位平面连续嵌入,并且是同时嵌入最低位平面和次低位平面
- 秘密信息在最低位平面嵌入,等最低位平面嵌入完全嵌入之后,再嵌入次低位平面
- 秘密信息在最低位平面随机嵌入
以上五种方式,当嵌入容量不同时,鲁棒性不同
与标准的LSB算法不同,我在设计我的LSB算法的时候,对嵌入的信息进行了随机嵌入,就是水印图像的比特流在嵌入载体图像时,并不是依次嵌入,而是随机选择位置嵌入。
此外,在嵌入时,对水印图像的比特流还进行了01规范化处理(使得嵌入的比特流0和1的个数一样多)。通过 规范化比特流 + 比特流随机嵌入 的方法,能够使得我的LSB算法具有抗击位平面图分析攻击,这里会在之后写一篇LSB隐写分析的文章中具体解释。
由于我增加了 规范化比特流 + 比特流随机嵌入,因此,为了能够提取水印,在嵌入过程中,我的算法会生成2个密钥文件:比特流规范密钥、嵌入位置密钥。所以,我的LSB算法不仅具有隐写术部分,还增加了密码术部分。
这部分可以结合我的代码来具体理解我的LSB算法流程
编写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算法的时候,对嵌入的信息进行了随机嵌入,就是水印图像的比特流在嵌入载体图像时,并不是依次嵌入,而是随机选择位置嵌入。此外,在嵌入时,对水印图像的比特流还进行了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('b must be "0" or "1" !')
return
x = bin(x)[2:]
if len(x) != 8: # 不足八位前面补0
x= '0'*(8 - len(x)) + x
# xbinList = [ x[i] for i in range(len(x))]
xbinList = list(x)
# print(xbinList)
xbinList[len(x) - pos] = b
# print(xbinList)
return int(''.join(xbinList),2)
def embeding(imgCover,binStreamList,embedZone,bitPlane=1):
'''
具体嵌入操作
'''
for coordinate, embedBin in zip(embedZone,binStreamList):
tmp = imgCover.item(coordinate[0],coordinate[1])
replace = binReplace(tmp,str(embedBin),bitPlane)
# 一定要注意embedBin是int的01但是传入的要是str的'0''1'
# imgCover.itemset(coordinate,replace) # 指定位平面替换要嵌入的比特流
# a = imgCover[coordinate[0]][coordinate[1]]
imgCover[coordinate[0]][coordinate[1]]=replace
return imgCover
# embedZone生成器(完全随机乱序)
def genRandEmbedZone(imgCoverPath,imgEmbedPath): # 做lsb分析时弃用
'''生成随机的嵌入位置序列,整张imgcover随机选取位置序号'''
imgCover = cv.imread(imgCoverPath,cv.IMREAD_GRAYSCALE) # 以灰度图方式读取载体图像
imgEmbed = cv.imread(imgEmbedPath,cv.IMREAD_GRAYSCALE) #以灰度图方式读取嵌入图像
binStreamScale = len(genEmbedBinStream(imgEmbed))
rowScale = imgCover.shape[0]
columnScale = imgCover.shape[1]
zone = []
for i in range(rowScale):
for j in range(columnScale):
zone.append(tuple([i,j]))
return random.sample(zone,binStreamScale)
# embedZone生成器(指定区域随机乱序)
def genNormalZone(imgCoverPath,imgEmbedPath): # lsb分析的时候用这个,
'''
从imgcover的第0个像素依次取imgEmbed的比特流长度的位置作为嵌入位置
但是,对其进行位置打乱嵌入,具体看代码
'''
imgCover = cv.imread(imgCoverPath,cv.IMREAD_GRAYSCALE) # 以灰度图方式读取载体图像
imgEmbed = cv.imread(imgEmbedPath,cv.IMREAD_GRAYSCALE) #以灰度图方式读取嵌入图像
binStreamScale = len(genEmbedBinStream(imgEmbed))
rowScale = imgCover.shape[0]
columnScale = imgCover.shape[1]
zone = [] # zone是获得整个imgcover的所有像素坐标,可以不这么写,这么写效率比较低,但是懒得优化了
for i in range(rowScale):
for j in range(columnScale):
zone.append(tuple([i,j]))
return random.sample(zone[:binStreamScale],binStreamScale) # 返回打乱位置的位置序列列表
# return zone[:binStreamScale] # 直接依次嵌入,不做随机处理
def LSBembedding(imgCoverPath:str,imgEmbedPath:str,embedZone:list,bitPlane=1):
'''
LSB嵌入主函数,imgCoverPath为载体图像路径,imgEmbedPath为水印路径,
embedZone为嵌入位置(由我写的算法自动生成),bitPlane用于指定嵌入的位平面,默认为1(LSB位)
'''
imgCover = cv.imread(imgCoverPath,cv.IMREAD_GRAYSCALE) # 以灰度图方式读取载体图像
imgEmbed = cv.imread(imgEmbedPath,cv.IMREAD_GRAYSCALE) #以灰度图方式读取嵌入图像
# 将嵌入水印抽取成比特流并做规范化,将比特流01比例调整为1:1,并做随机话处理,可以理解为对水印加密了
binStreamList = streamNormalize(genEmbedBinStream(imgEmbed))
# 判断嵌入信息是否过大
if len(binStreamList) > imgCover.shape[0]*imgCover.shape[1]:
print('嵌入的信息过大')
return
imgStego = embeding(imgCover,binStreamList,embedZone,bitPlane)
imgCoverName = imgCoverPath[9:].split('.')[0]
size = int(imgEmbed.shape[0]*imgEmbed.shape[1]/(512*512)*100)
cv.imwrite('./img/{}_stego{}.bmp'.format(imgCoverName,size),imgStego)
print('LSB Embeding done!')
return imgStego
测试lsbEmbed.py,并生成嵌入率为25%、50%和100%的stego图像:
可以看到生成了对应的stego图像和密钥文件(用于提取时解密水印):
以下从左到右依次为,嵌入率25%、50%和100%的stego图像:
从肉眼看,基本看不出图像被嵌入了水印。
编写LSB提取代码lsbExtract.py: