#import "../template.typ": *

#show: project.with(
  title: "5",
  authors: ("absolutex",),
)

= 机器视觉实践 五

== 实验目的

图像水印添加与提取

+ 要求考虑 DCT 变换实现图像水印算法
+ 要求在对图像添加扰动（如噪声旋转、缩放）时仍能提取水印

== 实验代码

本次实验我用了两种方法进行添加水印：频域直接添加和自定义的 LSB 最低有效位。

=== 频域直接添加

频域直接添加的原理是先将图像做 fft 得到频域，然后直接对频域图像绘制信息，最后 ifft 得到隐写图像。提取信息时，只需要将携带信息的图像进行 fft 即可在频域图中看到信息。

对于彩色图像，我选择分离 RGB 通道后，使用蓝色通道进行频域信息的承载，因为人眼对蓝色通道的变化更不敏感。

#include_code("../src/watermark/myfft.py")

=== LSB 最低有效位

LSB 最低有效位是广泛使用的一种隐写技术，LSB隐写就是修改RGB颜色分量的最低二进制位，也就是最低有效位（LSB），而人类的眼睛很难注意到像素的 1 亮度变化。

这里利用 LSB 原理设计了一个比较复杂的算法：
1. 计算隐写内容长度，并将每一个 bit 较为平均地分散到图像（的蓝色通道）中。这里分散的值使用的是 jump，也就是同一行中有效 bit 的距离。
2. 对于每一张图像，随机选取 $31 times 8$ 个点（共计 31 bytes）存储 31 次 jump 数字本身，恢复时取多数。
3. 隐写内容以 64 个 0 作为结束，而恢复时只要遇到连续 8 个 0 就认为结束。

算法的目的是为了减少噪声干扰对隐写信息造成的影响。下面是代码：

#include_code("../src/watermark/LSB.py")

== 实验结果与心得

=== 频域直接添加

#figure(
  image("DCT1.jpg", width: 100%),
  caption: [频域直接添加 隐写前后对比],
)

在空间域，隐写前后的图像没有任何区别。第三图是频域直接添加信息后的频谱，将第三图进行 ifft 再 fft 后得到第四图。可以看到第四图中心有隐约的中心对称的 “1234” 字样，但并不是特别明显。

#figure(
  image("DCT1_big.jpg", width: 20%),
  caption: [加密后频域放大图],
)

放大后可以明显看到隐写信息。

我也尝试过在频域不同位置添加隐写信息，结果恢复的频谱图上根本无法看到隐写的信息，因为频域能量集中在图片中心；即使现在将字写得靠近图片中心，也很难看到隐写的信息，更不用说将字写在远离中心的地方了。

#figure(
  image("DCT2.jpg", width: 70%),
  caption: [椒盐噪声干扰下的信息恢复],
)

在椒盐噪声干扰下，频谱噪声也加大，信息痕迹更淡，但是依然可以观察到隐写的信息。这种情况下，可能需要对频域图像进行二次处理，提高对比度以更好地恢复信息。

#figure(
  image("DCT3.jpg", width: 70%),
  caption: [高斯噪声干扰下的信息恢复],
)

高斯噪声干扰下，频谱的隐写信息反而看得更加清楚。这可能是因为频谱的中心能量更加集中所导致的。

#figure(
  image("DCT4.jpg", width: 70%),
  caption: [色块切割干扰下的信息恢复],
)

色块切割干扰下，对频谱的信息提取没有明显影响。

=== LSB 最低有效位

#figure(
  image("LSB.jpg", width: 50%),
  caption: [LSB 法原图与隐写图对比],
)

LSB 法并不会对图像造成肉眼可见的影响。因此后续不再放出对比图像。

无干扰条件下：

```
请输入水印内容：1234
INFO:root:jump: 255
INFO:root:got image shape : (1440, 1080)
INFO:root:most common jump: (255, 31)
获取水印： 1234
```

可以看到所有 31 个 jump 值均未改变，成功提取出水印。

椒盐噪声：

```
请输入水印内容：1234
INFO:root:got image shape : (1440, 1080)
INFO:root:most common jump: 255, 个数为 25
获取水印： 1234    !
```

可以看到 jump 成功读取，但是在获取水印时没有正确读取结束符，导致读取长度大于实际信息长度。该算法对椒盐噪声干扰抗性一般。因为椒盐噪声本身是全图随机的，这降低了将 LSB 的信息位分散到全图的抗干扰意义。

高斯噪声：

```
请输入水印内容：1234
INFO:root:jump: 255
INFO:root:got image shape : (1440, 1080)
WARNING:root:相同的 jump 不够多，可能有干扰，jump 为 239， 个数为 2
zB{Ìe,mkRµ)@!Â¦áiâ¬+¯=J5¼ÚçwÊv§{Îó¥9LcÆRç9ÜÆ³ç{Þ÷­û½-^ÿü­kÞsÞs×¹¥¦5½!)@+\B¹¥PÄCÊs3µé¾ Á.y¡[ÌB^öR&!aµ³¯g8qLd :´w¥¯
....
```

可以看到 jump 未能成功读取，信息获取错误。该算法无法抗高斯噪声的干扰：高斯噪声是全图影响，并且对于像素最后一位影响很大，几乎等于完全随机错位。

色块切割：

```
请输入水印内容：1234
INFO:root:jump: 255
INFO:root:got image shape : (1440, 1080)
INFO:root:most common jump: 255, 个数为 30
获取水印： 1234
```

对于色块切割干扰，该算法拥有非常高的抗干扰能力。不如说，算法的核心思想是将色块分散到全图，这一开始就是为了色块切割设计的。局部的色块切割只会丢失很小一部分信息，均匀分散能保证大多数信息的完整性。

=== 其他心得体会

本次实验多次将图像的 RGB 三通道分离，在处理后再将三通道合并回图片。而在 LSB 的测试中，一直出现图片 jump 和信息提取失败的情况，无论何种干扰（特别是色块切割干扰），jump 正确的个数都小于 3。将写入信息时和解码时对整个 G 通道矩阵打印并 diff，发现比信息量多得多的像素发生了漂移，也就是灰度值出现变化，幅度在 1-3 左右。这对于 LSB 是致命性的灾难，因为这意味着最低位发生了改变。

最后经过实验我发现这个现象是由于 JPG 格式图像导致的精度损失，因为 JPG 图像是有损压缩格式。将图像换为 PNG 后，观测到了期望结果。