跳到主要内容

隐写

2024年06月19日
柏拉文
越努力,越幸运

一、认识


通过 RGB 分量值的小量变动 来实现图片隐写术。

下面是一张看似普通的图片,但其中却藏有另一个肉眼无法识别的图像哦。

Preview

这是如果把上图每个色彩空间和数字 3 进行逻辑与运算,再把亮度增强 85 倍,可以得到下图。

Preview

简单的说,上述的处理过程可以理解为对图片像素的处理,也就是说,加密的信息散布在每个像素点上。

我们知道图片的像素信息里存储着 RGB 的色值,RGB 分别为该像素的红、绿、蓝通道,每个通道的分量值范围在 0~25516 进制则是 00~FF。在 CSS 中经常使用其 16 进制形式,比如指定博客头部背景色为 #A9D5F4。其中 R(红色)的 16 进制值为 A9,换算成十进制为 169。这时候,对 R 分量的值+1,即为 170,整个像素 RGB 值为 #AAD5F4,别说你看不出差别,就连火眼金金的 像素眼设计师都察觉不出来呢。于此同时,修改 GB 的分量值,也是我们无法察觉的。因此可以得出重要结论:RGB 分量值的小量变动,是肉眼无法分辨的,不影响对图片的识别

基于 RGB 分量值最小变动思想, 我们通过 R 通道的奇偶加密规则实现像素合并与分离

二、思路


通过 R 通道的奇偶加密规则实现像素合并与分离思路如下:

  • imageData1imageData2 中有像素点: 将 R 偶数的通道 +1 变为奇数

  • imageData1imageData2 中没有像素点: 将 R 奇数的通道 +1 变为偶数

基于 R 通道的像素分离: 基于合并规则, imageData1R 通道像素点在 imageData2 中为奇数, 因此:

  • imageData2R 通道像素点为奇数: 说明在 imageData1 中有像素点, 将 R 通道填充为 255

  • imageData2R 通道像素点为偶数: 说明在 imageData1 中没有像素点, 将 R 通道关闭,更改为 0

  • imageData2 中的 G 通道和 B 通道为了达到分离效果,可以将其关闭,更改为 0

三、加密


3.1 JS

function mergeData(params) {
const { ctx, imageData1, imageData2 } = params || {};

const data1 = imageData1.data;
const data2 = imageData2.data;

for (let i = 0; i < data2.length; i += 4) {
if (data1[i + 3] === 0 && data2[i] % 2 === 1) {
// data1 alpha 通道值为 0,data2 通道值为奇数
if (data2[i] === 255) {
data2[i]--;
} else {
data2[i]++;
}
} else if (data1[i + 3] !== 0 && data2[i] % 2 === 0) {
// data1 alpha 通道值不为 0,data2 通道值为偶数
if (data2[i] === 255) {
data2[i]--;
} else {
data2[i]++;
}
}
}

ctx.putImageData(imageData2, 0, 0);
}

function encodeImage(params) {
return new Promise((resolve) => {
const { src = "", text = "" } = params || {};

let textData;
let originalData;
const img = new Image();

img.onload = function () {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;

ctx.font = "30px Microsoft Yahei";
ctx.fillText(text, 60, 130);
textData = ctx.getImageData(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
originalData = ctx.getImageData(0, 0, canvas.width, canvas.height);
mergeData({
ctx,
imageData1: textData,
imageData2: originalData,
});

const encodeImg = canvas.toDataURL("image/png");
resolve(encodeImg);
};

img.src = src;
});
}

async function run() {
const encodeImg = await encodeImage({
src: "../../images/origin.png",
text: "柏拉文的秘密",
});

const img = new Image();
img.src = encodeImg;
document.body.appendChild(img);
}

run();

3.2 HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>隐写术-加密</title>
</head>
<body>
<script src="./index.js"></script>
</body>
</html>

四、解密


4.1 JS

function processData(params) {
const { ctx, imageData } = params || {};
const data = imageData.data;

for (let i = 0; i < data.length; i += 4) {
if (data[i] % 2 == 0) {
data[i] = 0;
} else {
data[i] = 255;
}

data[i + 1] = 0; // 关闭其他分量, 不处理的话也可以, 会显示原图
data[i + 2] = 0; // 关闭其他分量, 不处理的话也可以, 会显示原图
}

ctx.putImageData(imageData, 0, 0);
}

function decodeImage(params) {
return new Promise((resolve) => {
const { src = "" } = params || {};

let originalData;
const img = new Image();

img.onload = function () {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;

ctx.drawImage(img, 0, 0);
originalData = ctx.getImageData(
0,
0,
ctx.canvas.width,
ctx.canvas.height
);
processData({
ctx,
imageData: originalData,
});

const decodeImg = canvas.toDataURL("image/png");
resolve(decodeImg);
};

img.src = src;
});
}

async function run() {
const decodeImg = await decodeImage({
src: "../../images/encode.png",
});

const img = new Image();
img.src = decodeImg;
document.body.appendChild(img);
}

run();

4.2 HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>隐写术-解密</title>
</head>
<body>
<script src="./index.js"></script>
</body>
</html>