图像隐写术
本文为图像的隐写提供了一种思路。还有更多的思路,这里不做讲述。
项目源代码在:jeefies/jimg-ivs 中。
原理
利用了像素近似用肉眼难以察觉的前提(这就是为什么 jpeg
会存在……
以及 png
图片会原滋原味的保留像素数据,而不是像 jpeg
一样神秘的变化。
而这里并没有采用利用 8bit
像素最后几位来保存数据的方式,而是采用字体渲染,只在某一个颜色通道中展现文本(只读)。
所在通道中的每一个 8bit
位中,有 64
个取值表示被渲染的像素。
也就是说,要做一个近似处理。
至于如何渲染……利用了 SDL_ttf
来完成这件事。
实现
这次我没有继续使用 dlang 写这个小小的项目。而是纯纯只用了 C++,主要是比较方便,因为不仅使用了 SDL
,还使用了 opencv
,实在是不想封装一层。
Canvas
这是用来渲染文本的东西。由于一行能显示的文本有限,所以需要适当的裁剪。
一一裁剪太慢了,所以考虑倍增求解。
学算法的好处……仅此一点
int len = 0, lg = (int)log2(s.size()) + 1;
for (int step = 1 << lg; step; step >>= 1) {
if (len + step <= s.size()) {
int clen = len + step;
while (clen > len && !checkCompleteUTF8(s.substr(0, clen)))
--clen;
TTF_SizeUTF8(font, s.substr(0, clen).c_str(), &sw, &sh);
if (x + sw < w) len = clen;
}
}
但是,你可能关注到了 checkCompleteUTF8
,这是用于解决中文被截断无法完整渲染的问题(这个参考 utf8
编码方式)
// Important when showing Chinese!
bool checkCompleteUTF8(const std::string &s) {
char nBytes = 0;
for (unsigned char c : s) {
if (nBytes == 0 && c >= 0x80) {
if (0xFC <= c && c <= 0xFD) nBytes = 6;
else if (0xF8 <= c) nBytes = 5;
else if (0xF0 <= c) nBytes = 4;
else if (0xE0 <= c) nBytes = 3;
else if (0xC0 <= c) nBytes = 2;
else return false;
--nBytes;
} else if (nBytes) {
if ((c & 0xC0) != 0x80) return false;
--nBytes;
}
}
if (nBytes > 0) return false;
return true;
}
注意,必须要用
unsigned char
!
近似处理
核心部分。
void invisibleWrite(SDL_Surface *sur, cv::Mat &mat) {
SDL_LockSurface(sur);
int w = sur->w, h = sur->h;
Uint32 * pixels = (Uint32 *)sur->pixels;
auto threeSumPixel = [](Uint32 cl) {
return ((cl & 0xFF) + ((cl >> 8) & 0xFF) + ((cl >> 16) & 0xFF)) / 3;
};
auto discrete = [](unsigned char x) -> unsigned char { return (unsigned char)((round((double)x / 8)) * 8 - 1); };
for (int i = 0; i < h; ++i) {
int base = i * w;
unsigned char* data = mat.ptr<unsigned char>(i);
for (int j = 0; j < w; ++j) {
if (threeSumPixel(pixels[base + j]) > 128)
data[j] = discrete(data[j]);
else if (data[j] == discrete(data[j]))
data[j] = data[j] == 0 ? 1 : data[j] - 1;
}
}
SDL_UnlockSurface(sur);
}
其中呢,mat
表示图像分离后的一个通道。使用的 cv::split
函数完成。
相信我写的代码还是不难读明白的……
读入处理
cv::Mat encode(int channel_id = GREEN) {
cv::Mat newImage;
if (!_open) return newImage;
std::vector<cv::Mat> channels;
cv::split(image, channels);
showRendered(cvs.getSurface(), 10);
invisibleWrite(cvs.getSurface(), channels[channel_id]);
cv::merge(channels, newImage);
return newImage;
}
这是从 JIVS_Encoder
中截出来的一段代码。
其他部分的代码与上文代码类似……只要理解的上文中的代码,其他部分也就很好写了。
反正我写了这么多封装也就写了 294 行。不多,1 天就可以再写一遍 QwQ
其实吧
这种隐写术很难看破,因为没有对图像二进制文件做出任何的变动,并且像素的近似也是肉眼难以察觉,所以完全可以成为和神秘的人说神秘的话的一种途径吧(就是只能传输文本信息)。
如果要二进制,可能还是利用掩码或者神秘的异或来完成更好。