利用取模在图片中隐藏信息

思路为所有像素对指定的数取模,然后利用进制转化存储信息。例如原来的局部像素为 [0, 2, 4, 6, 8, 10, 12, 14, 16, 18],指定的模为 16,存储的信息为 iydon

  1. 令局部像素对 16 取模为 0,即 [0, 0, 0, 0, 0, 0, 0, 0, 16, 16]
  2. iydon 编码为 bytes,即 [105, 121, 100, 111, 110]
  3. 将编码后的数据按位进制转化,即 [6, 9, 7, 9, 6, 4, 6, 15, 6, 14]
  4. 局部像素与进制转化后的数据相加,即 [6, 9, 7, 9, 6, 4, 6, 15, 22, 30]

程序如下,但是程序目前仅支持 PNG,三通道图片上出现的问题可能是由读取与写入的 RGB 顺序导致的。

 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
78
79
80
81
82
83
84
85
86
87
88
89
90
import math
import pathlib as p
import typing as t

import cv2
import numpy as np


Path = t.Union[str, p.Path]


class Bytes:
    '''Vec<u8>'''

    def __init__(self, size: int = 2) -> None:
        # assert 1 <= size <= 256
        self._size = size
        self._part = math.ceil(8/math.log2(size))

    def __lt__(self, other: t.List[int]) -> t.List[int]:
        '''<: from'''
        # assert len(other) % self._part == 0
        # assert all(o//self._size==0 for o in other)
        length = len(other) // self._part
        ans = [0] * length
        for ith in range(length):
            factor = 1
            for digit in reversed(other[ith*self._part: (ith+1)*self._part]):
                ans[ith] += factor * digit
                factor *= self._size
        return ans

    def __gt__(self, other: t.List[int]) -> t.List[int]:
        '''>: to'''
        # assert all(o//256==0 for o in other)
        ans = [0] * (len(other)*self._part)
        for ith, byte in enumerate(other):
            start = (ith+1) * self._part
            for jth in range(self._part):
                ans[start-jth-1] = byte % self._size
                byte //= self._size
        return ans


class Image:
    def __init__(self, data: np.ndarray) -> None:
        self._data = data

    @property
    def capacity(self) -> int:
        return self._data.size

    @classmethod
    def from_file(cls, path: Path) -> 'Image':
        if p.Path(path).suffix != '.png':
            raise NotImplementedError('The bug with non-png images is not yet fixed')
        return cls(cv2.imread(str(path)))

    def read(self, size: int) -> bytes:
        # TODO: 使用第一位存储 size 信息
        _bytes = Bytes(size)
        ith = np.nonzero(self._data.flat % np.uint8(size))[0][0]
        info = self._data.flat[ith+1:] % size
        return bytes(_bytes < info)

    def burn(self, content: bytes, size: int) -> 'Image':
        _bytes = Bytes(size)
        info = np.array(_bytes>content, dtype=np.uint8)
        capacity, length = self._data.size, info.size
        assert capacity > length
        self._data.flat[:capacity] -= self._data.flat[:capacity] % size
        self._data.flat[capacity-length-1] += 1
        self._data.flat[-length:] += info
        return self

    def write(self, path: Path) -> None:
        cv2.imwrite(path, self._data)

    def capacity(self, size: int) -> float:
        # Kilo-Bytes (KB)
        return self._data.size / Bytes(size)._part / 1024.


if __name__ == '__main__':
    with open('airFoil2D.yaml', 'rb') as f:  # https://github.com/iydon/of.yaml/blob/main/tutorials/incompressible/simpleFoam/airFoil2D.yaml
        Image \
            .from_file('example.png') \
            .burn(f.read(), 2) \
            .write('output.png')
    # print(Image.from_file('output.png').read(2))

Dummy image

隐藏信息的图片:output.png