感觉自己像个秘密特工:用隐写术在图像中隐藏信息🖼️🕵️‍♀️

2025-05-28

感觉自己像个秘密特工:用隐写术在图像中隐藏信息🖼️🕵️‍♀️

詹姆斯·邦德、伊森·亨特、拿破仑·索罗——这些伪装成秘密特​​工的人物,向他们的雇主和其他特工传递秘密信息。说实话,秘密特工很酷,至少在电影和书籍里是这样。他们拥有超酷的装备,追捕恶棍,穿着华丽的衣服出入高档俱乐部。最终,他们拯救了世界。我小时候就梦想成为一名秘密特工。

在这篇文章中,我将向您展示一种可能被秘密特工用来将图像隐藏在其他图像中的技术:隐写术。

但首先要问的是:隐写术到底是什么?

隐写术可能是《007》电影中英国军情六处著名工程师Q发明的,但它实际上历史悠久!隐藏信息或图像,不让不该看到的人看到,这早已是古已有之的技术。

据维基百科记载,公元前440年,古希腊作家希罗多德曾剃掉他最忠诚的仆人之一的头发,让他在光头上写下信息,等到头发长回来后,就让仆人去送信。

今天我们不会剃光任何人的头发,更不会在彼此头上藏信息。我们只是把一张图片藏在另一张图片里。

为此,我们去除一幅图像中不重要的颜色部分,并用另一幅图像中重要的颜色部分替换它。

等等,什么?重要,还是不重要?

要理解这是什么意思,我们首先需要了解颜色是如何工作的,例如在 PNG 中。Web 开发者可能熟悉颜色的十六进制表示法,例如#f60053、 或#16ee8a。十六进制颜色由四个不同的部分组成:

  • A#作为前缀
  • 两个十六进制数字表示红色
  • 两位十六进制数字表示绿色
  • 两位十六进制数字代表蓝色

由于每种颜色的值可以从 到00FF这意味着它在十进制中是从0255。在二进制中,它将从0000000011111111

二进制的工作原理与十进制非常相似:一位数字越靠左,其值就越高。因此,位越靠左,“重要性”就越高。

例如:11111111几乎是 的两倍0111111111111110而 仅略小。人眼很可能不会注意到#FFFFFF和之间的差异。但它会注意到#FEFEFE之间的差异#FFFFFF#7F7F7F

让我们用 JS 隐藏一张图片

让我们隐藏这张图片:

带有一些 CLI 输出的计算机库存图像

在这张猫图片中:

一只毛茸茸的橘猫

我要写一个小 Node 脚本来将一张图片隐藏在另一张图片中。这意味着我的脚本需要接受三个参数:

  • 主图
  • 隐藏的图像
  • 目的地

我们先来编写一下代码:

const args = process.argv.slice(2)

const mainImagePath = args[0]
const hiddenImagePath = args[1]
const targetImagePath = args[2]

// Usage:
// node hide-image.js ./cat.png ./hidden.png ./target.png
Enter fullscreen mode Exit fullscreen mode

到目前为止一切顺利。现在我将安装image-size来获取主图像和画布的大小,以便 Node检查图像并生成新图像。

首先,我们先确定主图像和秘密图像的尺寸,并为它们分别创建画布。我还将为输出图像创建一个画布:

const imageSize = require('image-size')
const { createCanvas, loadImage } = require('canvas')

const args = process.argv.slice(2)

const mainImagePath = args[0]
const hiddenImagePath = args[1]
const targetImagePath = args[2]

const sizeMain = imageSize(mainImagePath)
const sizeHidden = imageSize(hiddenImagePath)

const canvasMain = createCanvas(sizeMain.width, sizeMain.height)
const canvasHidden = createCanvas(sizeHidden.width, sizeHidden.height)
const canvasTarget = createCanvas(sizeMain.width, sizeMain.height)

const contextMain = canvasMain.getContext('2d')
const contextHidden = canvasHidden.getContext('2d')
const contextTarget = canvasTarget.getContext('2d')
Enter fullscreen mode Exit fullscreen mode

接下来,我需要将两张图片加载到各自的画布中。由于这些方法返回的是 Promise,我将剩余的代码放在一个允许 async/await 的立即调用函数表达式中:

;(async () => {
  const mainImage = await loadImage(mainImagePath)
  contextMain.drawImage(mainImage, 0, 0, sizeMain.width, sizeMain.height)

  const hiddenImage = await loadImage(hiddenImagePath)
  contextHidden.drawImage(hiddenImage, 0, 0, sizeHidden.width, sizeHidden.height)
})()
Enter fullscreen mode Exit fullscreen mode

接下来,我迭代图像的每一个像素并获取它们的颜色值:

  for (let x = 0; x < sizeHidden.width; x++) {
    for (let y = 0; y < sizeHidden.height; y++) {
      const colorMain = Array.from(contextMain.getImageData(x, y, 1, 1).data)
      const colorHidden = Array.from(contextHidden.getImageData(x, y, 1, 1).data)
    }
  }
Enter fullscreen mode Exit fullscreen mode

有了这些值,我现在可以计算出要绘制到目标图像中的每个像素的“组合”颜色。

计算新颜色

我之前提到过有效位。为了实际计算颜色,我进一步说明一下。

假设我想将颜色 A 和 B 的红色部分组合起来。我将按如下方式表示它们的位(8 位):

A7 A6 A5 A4 A3 A2 A1 A0 (color A)
B7 B6 B5 B4 B3 B2 B1 B0 (color B)
Enter fullscreen mode Exit fullscreen mode

为了将颜色 B 隐藏在颜色 A 中,我将 A 的前 3 位(最右边)替换为 B 的最后 3 位(最左边)。 得到的位模式如下所示:

A7 A6 A5 A4 A3 B7 B6 B5
Enter fullscreen mode Exit fullscreen mode

这意味着,我会丢失两种颜色的一些信息,但组合后的颜色看起来与颜色 B 本身不会有太大区别。

让我们编写如下代码:

const combineColors = (a, b) => {
  const aBinary = a.toString(2).padStart(8, '0')
  const bBinary = b.toString(2).padStart(8, '0')

  return parseInt('' +
    aBinary[0] +
    aBinary[1] +
    aBinary[2] +
    aBinary[3] +
    aBinary[4] +
    bBinary[0] +
    bBinary[1] +
    bBinary[2], 
  2)
}
Enter fullscreen mode Exit fullscreen mode

我现在可以在像素循环中使用该功能:

const colorMain = Array.from(contextMain.getImageData(x, y, 1, 1).data)
const colorHidden = Array.from(contextHidden.getImageData(x, y, 1, 1).data)

const combinedColor = [
  combineColors(colorMain[0], colorHidden[0]),
  combineColors(colorMain[1], colorHidden[1]),
  combineColors(colorMain[2], colorHidden[2]),
]

contextTarget.fillStyle = `rgb(${combinedColor[0]}, ${combinedColor[1]}, ${combinedColor[2]})`
contextTarget.fillRect(x, y, 1, 1)
Enter fullscreen mode Exit fullscreen mode

差不多了,现在我只需要保存生成的图像:

const buffer = canvasTarget.toBuffer('image/png')
fs.writeFileSync(targetImagePath, buffer)
Enter fullscreen mode Exit fullscreen mode

结果如下:

隐藏在上方猫咪图像中的图像

根据您的屏幕设置,您可能会在图像的上半部分看到隐藏图像的图案。通常,您应该使用能够更清晰地隐藏图像的图像。

我该如何恢复隐藏的图像?

要提取隐藏的图像,只需读出每个像素的最后 3 位并再次使其成为最高有效位:

const extractColor = c => {
  const cBinary = c.toString(2).padStart(8, '0')

  return parseInt('' +
    cBinary[5] + 
    cBinary[6] + 
    cBinary[7] + 
    '00000',
  2)
}
Enter fullscreen mode Exit fullscreen mode

如果我对每个像素都执行此操作,我会再次得到原始图像(加上一些伪影):

原始图像质量较低

现在,您可以通过隐藏图像并向其他秘密特工发送隐藏消息,感觉自己就像一个真正的秘密特工!


希望你喜欢阅读这篇文章,就像我喜欢写它一样!如果喜欢,请留下❤️🦄 !我空闲时间会写科技文章,偶尔也喜欢喝咖啡。

如果你想支持我的努力, 请给我买杯咖啡 或者 在推特上关注我🐦 你也可以直接通过Paypal支持我!

给我买个咖啡按钮

文章来源:https://dev.to/thormeier/feel-like-a-secret-agent-hidden-messages-in-images-with-steganography-37kh
PREV
在 dev.to 上写文章的一年半让我成为了一名更好的开发者✍️↔️🧑‍💻🚀
NEXT
⚠️ 不要在家尝试这个:CSS 作为后端 - 引入级联服务器表!