JPEG隐写术
如果您想将图像保存为JPEG格式,必须按照JPEG编码过程进行操作。不幸的是,我阅读的大多数文献都没有很好地解释这一过程。完整的过程如下(摘自维基百科摘要和182页规格书):
- RGB转换为YCbCr(可选),
- 色度通道的子采样(可选),
- 8x8块分割,
- 像素值重新调整中心,
- DCT变换,
- 根据压缩比率/质量进行量化,
- 以Zigzag模式排序系数,以及
- 熵编码;最常用的是霍夫曼编码和游程编码(RLE)。
实际上还涉及到更多的细节,比如标题、部分标记、如何存储DC和AC系数的具体细节等。此外,标准只定义了一些方面,其实现在编解码器之间可能会有所不同,例如子采样算法、量化表和熵编码。尽管如此,大多数软件都遵守通用的JFIF标准,并且可以被各种软件读取。如果你想让你的JPEG文件也能这样做,就需要准备好写数百(甚至一千)行代码来编写编码器。最好借鉴已经在互联网上发布的编码器,而不是自己编写。你可以从libjpeg开始,它是用C语言编写的,并成为许多其他JPEG编解码器的基础,或者使用其C#实现,甚至是受其启发的Java版本。
在一些伪代码中,编码/解码过程可以描述如下。
function saveToJpeg(pixels, fileout) {
// pixels is a 2D or 3D array containing your raw pixel values
// blocks is a list of 2D arrays of size 8x8 each, containing pixel values
blocks = splitBlocks(pixels);
// a list similar to blocks, but for the DCT coefficients
coeffs = dct(blocks);
saveCoefficients(coeffs, fileout);
}
function loadJpeg(filein) {
coeffs = readCoefficients(filein);
blocks = idct(coeffs);
pixels = combineBlocks(blocks);
return pixels;
}
对于隐写术,您需要将其修改如下。
function embedSecretToJpeg(pixels, secret, fileout) {
blocks = splitBlocks(pixels);
coeffs = dct(blocks);
modified_coeffs = embedSecret(coeffs, secret);
saveCoefficients(modified_coeffs, fileout);
}
function extractSecretFromJpeg(filein) {
coeffs = readCoefficients(filein);
secret = extractSecret(coeffs);
return secret;
}
如果您的封面图片已经是jpeg格式,就不需要使用解码器将其转换为像素,然后再传递给编码器嵌入消息。您可以采用以下方法。
function embedSecretToJpeg(pixels, secret, filein, fileout) {
coeffs = readCoefficients(filein);
modified_coeffs = embedSecret(coeffs, secret);
saveCoefficients(modified_coeffs, fileout);
}
就您提出的问题而言,编码器/解码器应该负责处理1、2、3和5,除非您自己写一个。
问题1:通常,您需要使用必要数量的行/列填充图像,以使宽度和高度都可被8整除。在内部,编码器将跟踪填充的行/列,以便解码器在重建后将其丢弃。这些虚拟行/列的像素值选择取决于您,但不建议使用恒定值,因为会导致振铃现象,这与正弦函数是方波傅里叶变换的事实有关。
问题2:虽然您只修改了几个块,但编码过程需要将它们全部转换,以便存储到文件中。
问题3:必须对浮点DCT系数进行量化,因为这是无损存储在文件中的内容。在量化步骤之后,您可以随心所欲地修改它们。
问题4: 没有人会阻止你修改任何系数,但你必须记住每个系数都会影响块中的所有64个像素。直流系数和
低频AC系数引入了最大的失真,因此你可能要远离它们。更具体地说,由于直流系数的存储方式,修改一个系数会将失真传播到所有后续块。
由于大多数高频系数为0,它们可以有效地通过RLE压缩。修改一个0系数可能会将其翻转为1(如果你正在进行基本LSB替换),这会破坏这种有效的压缩。
最后,一些算法将它们的秘密存储在任何非零系数中,并跳过任何0。然而,如果你试图修改一个1,它可能会翻转为0,在提取过程中,你会盲目地跳过读取它。因此,这种算法不会接近任何值为1或0的系数。
问题5:在解码中,您只需将系数与相应的量化表值相乘。例如,DC系数为309.443,量化值为 round(309.443 / 16) = 19
。舍入位是损失的部分,在这里不允许您重建309.433。因此,反向操作就是 19 * 16 = 304
。
DCT在隐写术中的其他用途
频率变换,如DCT和DWT,可用于在频域中嵌入秘密消息,但不一定要将隐写图像存储为jpeg格式。这个过程是像素 -> DCT -> 系数 -> 修改系数 -> IDCT -> 像素,这就是您发送给接收者的内容。因此,格式的选择在这里很重要。如果您决定将像素保存为jpeg格式,则DCT系数中的秘密消息可能会被jpeg编码的另一层量化所干扰。