图片隐写实验

信息隐藏是该实验主要要探究的内容。

实验目的

本次实验目的为运用信息隐藏技术,开发 hide 和 show 算法,能将一段超过1000个字符的内容通过 hide 算法隐藏在24位的 BMP 图片中,并且可以通过 show 算法将这段信息提取出来。

程序要能在其他文本文件和24位BMP图片上执行隐藏和恢复操作。

实验原理

该信息隐藏技术利用图像最低有效位(LSB)替换的原理,具体如下:

  1. 位平面分解:将24位BMP图像的每个像素点的RGB分量(各8位)分解为8个位平面,从最高位(MSB)到最低位(LSB)。
  2. LSB替换:选择最低位平面(对视觉影响最小)来隐藏信息,因为修改最低位对人眼几乎不可察觉。
  3. 嵌入算法
    • 对于待隐藏信息的每一位(0或1):
      • 如果是0:将像素值的最低位置0 (AND 11111110)
      • 如果是1:将像素值的最低位置1 (OR 00000001)
  4. 提取算法
    • 读取每个像素值的最低位(AND 00000001)即可恢复隐藏的信息

实验过程

1.算法实现

hide 算法实现(旧)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from PIL import Image

def hide_text_in_image():
# 1. 让用户输入文本文件和图片文件路径
text_file = input("请输入要隐藏的文本文件路径: ").strip('"')
image_file = input("请输入BMP图片路径: ").strip('"')
output_image_file = input("请输入保存后的图片路径: ").strip('"')

# 2. 读取文本内容
try:
with open(text_file, 'rb') as f:
text_data = f.read()
except FileNotFoundError:
print(f"错误:找不到文本文件 {text_file}!")
return
except Exception as e:
print(f"读取文本文件时出错: {e}")
return

# 3. 读取并检查图片
try:
img = Image.open(image_file)
if img.mode != 'RGB':
img = img.convert('RGB')
except FileNotFoundError:
print(f"错误:找不到图片文件 {image_file}!")
return
except Exception as e:
print(f"读取图片时出错: {e}")
return

pixels = img.load()
width, height = img.size

# 4. 检查图片容量是否足够
required_pixels = len(text_data) * 8 # 每个字节需要8个像素
if width * height < required_pixels:
print(f"错误:图片太小!需要至少 {required_pixels} 像素,但图片只有 {width * height} 像素。")
return

# 5. 将文本转换为二进制
binary_str = ''.join(format(byte, '08b') for byte in text_data)

# 6. 修改图片像素(LSB隐写)
index = 0
for y in range(height):
for x in range(width):
if index >= len(binary_str):
break

r, g, b = pixels[x, y]

# 修改RGB最低位
if index < len(binary_str):
r = (r & 0xFE) | int(binary_str[index])
index += 1
if index < len(binary_str):
g = (g & 0xFE) | int(binary_str[index])
index += 1
if index < len(binary_str):
b = (b & 0xFE) | int(binary_str[index])
index += 1

pixels[x, y] = (r, g, b)

# 7. 保存图片
try:
img.save(output_image_file)
print(f"✅ 隐藏成功!图片已保存到: {output_image_file}")
except Exception as e:
print(f"保存图片时出错: {e}")

# 运行主函数
if __name__ == "__main__":
print("=== BMP图片隐写工具===")
hide_text_in_image()
input("按 Enter 键退出...")

show 算法实现(旧)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
from PIL import Image

def show():
"""从BMP图片中提取隐藏的文本"""
# 输入图片路径和输出文本路径
image_file = input("请输入包含隐藏文本的BMP图片路径(如:C:\\隐藏的图片.bmp): ").strip('"')
output_text_file = input("请输入提取后的文本保存路径(如:C:\\提取结果.txt): ").strip('"')

# 读取图片
try:
img = Image.open(image_file)
if img.mode != 'RGB':
img = img.convert('RGB') # 确保是RGB模式
except Exception as e:
print(f"❌ 读取图片失败: {e}")
return

pixels = img.load()
width, height = img.size

# 提取所有像素的LSB(最低有效位)
binary_data = []
for y in range(height):
for x in range(width):
r, g, b = pixels[x, y]
binary_data.append(str(r & 1)) # 红色通道最低位
binary_data.append(str(g & 1)) # 绿色通道最低位
binary_data.append(str(b & 1)) # 蓝色通道最低位

# 将二进制数据转换为字节
text_bytes = []
for i in range(0, len(binary_data), 8):
byte_bits = binary_data[i:i+8]
if len(byte_bits) < 8:
break # 忽略不完整的字节
byte = int(''.join(byte_bits), 2)
text_bytes.append(byte)

# 写入提取的文本
try:
with open(output_text_file, 'wb') as f:
f.write(bytes(text_bytes))
print(f"✅ 文本提取成功!已保存到: {output_text_file}")
except Exception as e:
print(f"❌ 保存提取结果失败: {e}")

# 使用示例
if __name__ == "__main__":
print("=== BMP隐藏文本提取工具 ===")
show()
input("按 Enter 键退出...")

问题与反思

经过实验后,这两段代码都能准确的将文本隐藏进去和提取出来,但是出现了一个新的问题,就是在提取出来的文本中,除了原本存放进去的文本,后面还跟着一大串的二进制数据。这样的结果不是我想要的,于是开始思考有什么方法可以解决。

思考过后,我想出的方法为添加文件头并且确定文本字符长度,让 show 算法可以准确提取相应的文本。

hide 算法实现(新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
from PIL import Image

def hide_text_in_image():
# 输入文件路径
text_file = input("请输入要隐藏的文本文件路径: ").strip('"')
image_file = input("请输入BMP图片路径: ").strip('"')
output_image_file = input("请输入保存后的图片路径: ").strip('"')

# 读取文本并添加长度头
try:
with open(text_file, 'rb') as f:
text_data = f.read()
text_length = len(text_data).to_bytes(4, byteorder='big')
text_data_with_length = text_length + text_data # 4字节长度 + 真实数据
except Exception as e:
print(f"❌ 读取文本失败: {e}")
return

# 转换为二进制字符串(确保只有一次转换)
binary_str = ''.join(format(byte, '08b') for byte in text_data_with_length)

# 读取图片
try:
img = Image.open(image_file)
if img.mode != 'RGB':
img = img.convert('RGB')
pixels = img.load()
width, height = img.size
except Exception as e:
print(f"❌ 读取图片失败: {e}")
return

# 检查容量是否足够(长度头+文本)
required_pixels = len(text_data_with_length) * 8
if width * height < required_pixels:
print(f"❌ 图片太小!需要 {required_pixels} 像素,实际 {width * height} 像素")
return

# 嵌入数据
index = 0
for y in range(height):
for x in range(width):
if index >= len(binary_str):
break
r, g, b = pixels[x, y]
if index < len(binary_str):
r = (r & 0xFE) | int(binary_str[index])
index += 1
if index < len(binary_str):
g = (g & 0xFE) | int(binary_str[index])
index += 1
if index < len(binary_str):
b = (b & 0xFE) | int(binary_str[index])
index += 1
pixels[x, y] = (r, g, b)

# 保存图片
try:
img.save(output_image_file)
print(f"✅ 隐藏成功!图片已保存到: {output_image_file}")
except Exception as e:
print(f"❌ 保存图片失败: {e}")

if __name__ == "__main__":
print("=== BMP图片隐写工具 ===")
hide_text_in_image()
input("按 Enter 键退出...")

show 算法实现(新)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from PIL import Image
import traceback

def show():
"""从BMP图片中提取隐藏的文本"""
try:
# 输入路径
image_file = input("请输入BMP图片路径: ").strip('"')
output_text_file = input("请输入输出文本路径: ").strip('"')

# 读取图片
img = Image.open(image_file)
if img.mode != 'RGB':
img = img.convert('RGB')
pixels = img.load()
width, height = img.size

# 提取前32位(长度头)
length_bits = []
bits_collected = 0
for y in range(height):
for x in range(width):
if bits_collected >= 32:
break
r, g, b = pixels[x, y]
if bits_collected < 32:
length_bits.append(str(r & 1))
bits_collected += 1
if bits_collected < 32:
length_bits.append(str(g & 1))
bits_collected += 1
if bits_collected < 32:
length_bits.append(str(b & 1))
bits_collected += 1

# 解析长度头
text_length = int(''.join(length_bits), 2)
print(f"[调试] 隐藏文本长度: {text_length} 字节")

# 提取真实数据
data_bytes = bytearray()
byte_buffer = []
bits_needed = text_length * 8
bits_collected = 0

for y in range(height):
for x in range(width):
if bits_collected >= bits_needed:
break
r, g, b = pixels[x, y]
if bits_collected < bits_needed:
byte_buffer.append(str(r & 1))
bits_collected += 1
if bits_collected < bits_needed:
byte_buffer.append(str(g & 1))
bits_collected += 1
if bits_collected < bits_needed:
byte_buffer.append(str(b & 1))
bits_collected += 1
# 每8位转换为一个字节
while len(byte_buffer) >= 8:
byte = int(''.join(byte_buffer[:8]), 2)
data_bytes.append(byte)
byte_buffer = byte_buffer[8:]

# 保存结果(跳过前4字节的长度头)
with open(output_text_file, 'wb') as f:
f.write(data_bytes[4:4 + text_length]) # 精确截取有效数据
print(f"✅ 提取完成!结果已保存到: {output_text_file}")

except Exception as e:
print(f"❌ 错误:\n{traceback.format_exc()}")

if __name__ == "__main__":
print("=== BMP隐藏文本提取工具 ===")
show()
input("按 Enter 键退出...")

2.运行结果展示

原文本

image-20250416154907938

原图片

QQ20250416-103939

hide 函数运行结果

1
2
3
4
5
6
7
PS D:\cprogram> & C:/Users/13483/AppData/Local/Microsoft/WindowsApps/python3.11.exe d:/cprogram/two/view_hide.py
=== BMP图片隐写工具 ===
请输入要隐藏的文本文件路径: "C:\Users\13483\Desktop\纪念\致我最爱的你.txt"
请输入BMP图片路径: "C:\Users\13483\OneDrive\图片\QQ20250416-103939.bmp"
请输入保存后的图片路径: "C:\Users\13483\OneDrive\图片\QQ20250416-103939(1).bmp"
✅ 隐藏成功!图片已保存到: C:\Users\13483\OneDrive\图片\QQ20250416-103939(1).bmp
按 Enter 键退出...

隐藏后图片展示

QQ20250416-103939(1)

show 函数运行结果

1
2
3
4
5
6
7
PS D:\cprogram> & C:/Users/13483/AppData/Local/Microsoft/WindowsApps/python3.11.exe d:/cprogram/two/view_show.py
=== BMP隐藏文本提取工具 ===
请输入BMP图片路径: "C:\Users\13483\OneDrive\图片\QQ20250416-103939(1).bmp"
请输入输出文本路径: 4.txt
[调试] 隐藏文本长度: 1282 字节
✅ 提取完成!结果已保存到: 4.txt
按 Enter 键退出...

提取文本展示

image-20250416155451418

可以看到,实验很成功。

思考和总结

如何储藏隐藏的内容长度

1. 长度头设计原理

在隐藏数据前,先在文件头部预留固定4字节空间,以大端序(big-endian)存储原始文本的字节长度值。这种设计具有:

  • 固定开销:仅增加4字节(32bit)的存储成本
  • 超大容量:最大可表示4GB内容(2^32-1字节)
  • 快速解析:提取时优先读取这4字节即可获知数据量

2. 具体实现方法

隐藏阶段(hide):
1
2
3
# 计算并插入长度头
text_length = len(text_data).to_bytes(4, byteorder='big') # 生成4字节长度头
text_data_with_length = text_length + text_data # [长度头][真实数据]
提取阶段(show):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 提取前32位(长度头)
length_bits = []
bits_collected = 0
for y in range(height):
for x in range(width):
if bits_collected >= 32:
break
r, g, b = pixels[x, y]
if bits_collected < 32:
length_bits.append(str(r & 1))
bits_collected += 1
if bits_collected < 32:
length_bits.append(str(g & 1))
bits_collected += 1
if bits_collected < 32:
length_bits.append(str(b & 1))
bits_collected += 1

# 解析长度头
text_length = int(''.join(length_bits), 2)

3. 技术优势

  1. 精准控制:完全避免读取多余像素数据
  2. 格式兼容:与任何文件类型(txt/jpg/exe等)兼容
  3. 抗干扰性:长度头损坏会立即发现,避免错误解析

关键问题与解决

  1. 数据污染:通过添加4字节长度头标记,精确控制提取范围
  2. 字节对齐:严格按比特数控制提取,实时转换字节流
  3. 格式限制:选用24位BMP保证数据完整性

改进方向

  1. 安全性:增加AES加密
  2. 容错性:添加纠错编码
  3. 易用性:开发GUI界面

核心收获

  • 掌握LSB隐写基本原理
  • 提升二进制数据处理能力
  • 理解信息隐藏的不可感知性特性

实验验证了LSB技术的可行性,后续可结合加密算法构建更安全的隐写系统。


图片隐写实验
http://example.com/2025/04/16/图片隐写实验/
作者
yuhua
发布于
2025年4月16日
许可协议