788 字
4 分钟
关于图片隐写的研究

前言#

起因是前几天在搜图那看到了一张比较“特别”的图: 图片 1

这张图本身带有一些透明度,在白色/黑色(或者说浅色/深色)背景下能显示两种图案,也就是把两张图以某种方式合并在了一起。

关于这种方式我想了几天,最后得出了一种方案,可能和他的不太一样,但也算能实现。

原理#

首先得知道半透明的像素点,放在背景上,最终会渲染成什么颜色:

渲染的颜色 = 点的颜色 * 透明度 + 背景的颜色 * (1 - 透明度)

我的方法主要是将两张图片的像素点间隔排列,因为看上面的“渲染方式”,在一个像素点内,控制显示成两种颜色非常困难,所以干脆分离成两个点,让其中一个在黑/白背景下不可见,以凸显另一个的颜色。

这种做法不会出现明显的间隔,因为“缺失”的点在渲染的时候会被背景色补全。

图片 2 假设每个格子都是半透明的,在黑色背景下,本来纯黑的部分就会和背景融为一体,白色的部分会叠加成灰色,反之同理,在白色背景下,白色格子不可见,原本的黑色格子的“黑度”会被稀释成灰色。

所以解题的关键就成了“灰度”,在颜色叠加之后,利用灰度来区分像素点的颜色变化

流程上就变成了,先拿到图片像素点的灰度值,然后根据灰度值算出对应的透明度,在白色背景下让图 A 的每个点变成“不同透明度的黑色”,反之在黑色背景下让图 B 的每个点变成“不同透明度的白色”,再把两张图片间隔叠加实现合并。

至于灰度的计算,我查了一下,在不同的色彩空间下,计算方式不一样,我使用的是这个回答提到的:

Y' = 0.299 R' + 0.587 G' + 0.114 B'

关键代码#

if (isImageB) {
	// 处理黑色背景的图像
	const pixelData = blackCtx.getImageData(coordX, coordY, 1, 1).data;
	// 计算灰度值
	const gray = Math.round(pixelData[0] * 0.299 + pixelData[1] * 0.587 + pixelData[2] * 0.114);
	// 设置像素值(黑色,透明度基于灰度值)
	resultData.data[idx] = 0;     // R
	resultData.data[idx+1] = 0;   // G
	resultData.data[idx+2] = 0;   // B
	resultData.data[idx+3] = 255 - gray; // A
} else {
	// 处理白色背景的图像
	const pixelData = whiteCtx.getImageData(coordX, coordY, 1, 1).data;
	// 计算灰度值
	const gray = Math.round(pixelData[0] * 0.299 + pixelData[1] * 0.587 + pixelData[2] * 0.114);
	// 设置像素值(白色,透明度基于灰度值)
	resultData.data[idx] = 255;   // R
	resultData.data[idx+1] = 255; // G
	resultData.data[idx+2] = 255; // B
	resultData.data[idx+3] = gray; // A
}

补充#

由于实现方法不太一样,所以我这里的只能生成灰度图,原图还是带了点其他颜色的,不过他的效果不是很好,在白色背景下也会显示出部分图 B。

试玩可以到这里

关于图片隐写的研究
https://blog.erio.work/posts/关于图片隐写的研究/
作者
Dupfioire
发布于
2025-04-04
许可协议
CC BY-NC-SA 4.0