隐写术 11 - 索引图像及其调色板






4.80/5 (25投票s)
将任何类型的数据隐藏在索引位图(如 PNG 和 GIF)中。
引言
本文解释了如何将二进制数据隐藏在索引(每像素 8 位)位图中。这里描述的方法与我们用于 RGB(每像素 24 位)位图的方法完全不同,但阅读本系列的第一部分隐写术 - 在图片噪声中隐藏消息,可能有助于理解这两种位图之间的区别。
思考索引图像...
在之前的文章中,我们将秘密消息的位隐藏在颜色值的最低位中。在索引位图中,颜色值不在像素中,而在调色板中
每像素 24 位,无调色板
每像素 8 位,调色板大小任意(1 - 256)
因此,你的第一个想法可能是将消息隐藏在调色板中而不是像素中。但调色板最多包含 256 种颜色,只能携带几个字节。这个问题导致了第二个想法:扩大调色板,复制颜色值!
如果我们复制调色板,我们就为每种颜色提供了两个可选索引。嘿,这可能是一个更好的方法!引用前两行的颜色可能意味着“消息位是 0”,引用下两行的相同颜色可能意味着“消息位是 1”。但再次出现问题:包含重复颜色的调色板很愚蠢。没有人会使用这样的调色板。双调色板太明显了。
所以我们不能简单地复制调色板,但我们可以添加与现有颜色略有不同的颜色。让我们说,对于调色板中的每种颜色,我们添加两种相似(但不相同)的颜色值
现在我们有一个扩展的调色板,但隐藏消息的存在同样明显,因为只有三种颜色中的一种实际上被使用了。这完全不是问题,我们只需要更改像素,以便一些引用原始颜色的像素引用其副本之一。
现在我们可以在像素中隐藏东西。对于消息位“1”,让像素引用原始颜色;对于消息位“0”,让它引用添加的颜色之一。但如何提取隐藏的消息?如果我们只知道扩展的调色板,我们就无法知道哪些颜色是复制自原始调色板,哪些是新创建的。
有没有办法将颜色值标记为“取自原始调色板”?嗯,每种颜色都有三个最低有效位。这个名字不正确。由于普通显示器和人眼的敏感度,颜色分量的最低位应该称为完全不重要。设置或重置颜色分量的第一位是如何扩展调色板并标记每种颜色为复制或新
如何“优化”调色板
在创建新调色板时,我们必须跟踪复制和添加的颜色,因为之后,当我们更改像素以引用新调色板中的每种颜色时,我们必须能够引用原始颜色(甚至蓝色分量)来隐藏“0”,或者更改颜色(奇数蓝色分量)来隐藏“1”。StretchPalette
方法使用 HashTable
将旧调色板中的每个索引映射到新调色板中的相应索引。
/// <summary>
/// Creates a larger palette by duplicating and changing
/// the colors of another palette
/// </summary>
/// <param name="oldPalette">The palette to stretch</param>
/// <param name="maxPaletteSize">Count of colors in the new palette</param>
/// <param name="newPalette">Receives the new palette entries</param>
/// <param name="colorIndexToNewIndices">
/// Receives a Hashtable with the original indices as the keys,
/// and the corresponding new indices as the values
/// </param>
public void StretchPalette(ColorPalette oldPalette, int maxPaletteSize,
ref ArrayList newPalette, ref Hashtable colorIndexToNewIndices) {
//collects the new palette entries
newPalette = new ArrayList(maxPaletteSize);
//maps each old index to the new indices
colorIndexToNewIndices = new Hashtable( oldPalette.Entries.Length );
Random random = new Random();
byte indexInNewPalette;
Color color, newColor;
ColorIndexList colorIndexList;
//repeat the loop if necessary
while(newPalette.Count < maxPaletteSize){
//loop over old palette entries
for(byte n=0; n<oldPalette.Entries.Length; n++){
color = oldPalette.Entries[n]; //original color
if(colorIndexToNewIndices.ContainsKey(n)){
//this color from the original palette already has
//one or more copies in the new palette
colorIndexList = (ColorIndexList)colorIndexToNewIndices[n];
}else{
if(color.B%2 > 0){ //make even
color = Color.FromArgb(color.R, color.G, color.B-1); }
//add color
indexInNewPalette = (byte)newPalette.Add(color);
colorIndexList = new ColorIndexList(random);
colorIndexList.Add(indexInNewPalette);
colorIndexToNewIndices.Add(n, colorIndexList);
}
if(newPalette.Count < maxPaletteSize){
//create a non-exact copy of the color
newColor = GetSimilarColor(random, newPalette, color);
if(newColor.B%2 == 0){ //make odd
newColor = Color.FromArgb(
newColor.R, newColor.G, newColor.B+1);
}
//add the changed color to the new palette
indexInNewPalette = (byte)newPalette.Add(newColor);
//add the new index to the list of alternative indices
colorIndexList.Add(indexInNewPalette);
}
//update the Hashtable
colorIndexToNewIndices[n] = colorIndexList;
if(newPalette.Count == maxPaletteSize){
break; //the new palette is full - cancel
}
}
}
}
如果您阅读了代码(如果没有,可以在下载完整源代码后阅读),您可能已经看到一个名为 GetSimilarColor
的方法。此方法会创建颜色值的变体
private Color GetSimilarColor(Random random,
ArrayList excludeColors,
Color color) {
Color newColor = color;
int countLoops = 0, red, green, blue;
do{
red = GetSimilarColorComponent(random, newColor.R);
green = GetSimilarColorComponent(random, newColor.G);
blue = GetSimilarColorComponent(random, newColor.B);
newColor = Color.FromArgb(red, green, blue);
countLoops++;
//make sure that there are no duplicate colors
}while(excludeColors.Contains(newColor)&&(countLoops<10));
return newColor;
}
private byte GetSimilarColorComponent(Random random, byte colorValue){
if(colorValue < 128){
colorValue = (byte)(colorValue *
(1 + random.Next(1,8)/(float)100) );
}else{
colorValue = (byte)(colorValue /
(1 + random.Next(1,8)/(float)100) );
}
return colorValue;
}
现在,我们有了一个新调色板,以及一个用于将旧索引映射到新索引的键/值表。下一步是在复制图像时隐藏消息的位。System.Drawing.Image
有一个名为 Palette
的属性,类型为 ColorPalette
。这是我见过的最受限制的类之一。它有两个属性,Flags
和 Entries
- 两者都是只读的。ColorPalette
允许更改现有调色板的颜色,但我们无法添加任何颜色。我不想花几个小时寻找一个干净的 .NET 解决方案,编写一个新的位图更容易
/// <summary>
/// Creates an image with a stretched palette,
/// converts the pixels of the original image for
/// that new palette, and hides a message in the converted pixels
/// </summary>
/// <param name="bmp">The original image</param>
/// <param name="palette">The new palette</param>
/// <param name="colorIndexToNewIndices">
/// Hashtable which maps every index in the original palette
/// to a list of indices in the new palette.
/// </param>
/// <param name="messageStream">The secret message</param>
/// <param name="keyStream">
/// A key that specifies the distances between two
/// pixels used to hide a bit
/// </param>
/// <returns>The new bitmap</returns>
private Bitmap CreateBitmap(
Bitmap bmp, ArrayList palette,
Hashtable colorIndexToNewIndices,
Stream messageStream, Stream keyStream) {
//lock the original bitmap
BitmapData bmpData = bmp.LockBits(
new Rectangle(0,0,bmp.Width, bmp.Height),
ImageLockMode.ReadWrite,
PixelFormat.Format8bppIndexed);
//size of the image data in bytes
int imageSize = (bmpData.Height * bmpData.Stride)+(palette.Count * 4);
//copy all pixels
byte[] pixels = new byte[imageSize];
Marshal.Copy(bmpData.Scan0, pixels, 0, (bmpData.Height*bmpData.Stride));
int messageByte=0, messageBitIndex=7;
bool messageBit;
ColorIndexList newColorIndices;
Random random = new Random();
//index of the next pixel that's going to hide one bit
int nextUseablePixelIndex = GetKey(keyStream);
//loop over the pixels
for(int pixelIndex=0; pixelIndex<pixels.Length; pixelIndex++){
//get the list of new color indices for the current pixel
newColorIndices=(ColorIndexList)colorIndexToNewIndices[pixels[pixelIndex]];
if((pixelIndex < nextUseablePixelIndex) || messageByte < 0){
//message complete or this pixel has to be skipped - use a random color
pixels[pixelIndex] = newColorIndices.GetIndex();
}else{
//message not complete yet
if(messageBitIndex == 7){
//one byte has been hidden - proceed to the next one
messageBitIndex = 0;
messageByte = messageStream.ReadByte();
}else{
messageBitIndex++; //next bit
}
//get a bit out of the current byte
messageBit = (messageByte & (1 << messageBitIndex)) > 0;
//get the index of a similar color in the new palette
pixels[pixelIndex] = newColorIndices.GetIndex(messageBit);
nextUseablePixelIndex += GetKey(keyStream);
}
}
//Now we have the palette and the new pixels.
//Enough data to write the bitmap !
BinaryWriter bw = new BinaryWriter( new MemoryStream() );
//write bitmap file header
//...
//...
//write bitmap info header
//...
//...
//write palette
foreach(Color color in palette){
bw.Write((UInt32)color.ToArgb());
}
//write pixels
bw.Write(pixels);
bmp.UnlockBits(bmpData);
Bitmap newImage = (Bitmap)Image.FromStream(bw.BaseStream);
newImage.RotateFlip(RotateFlipType.RotateNoneFlipY);
bw.Close();
return newImage;
}
提取隐藏的消息
从图像中提取消息比隐藏它容易得多。只有一个调色板,我们不必关心新旧索引。我们只需使用分布键来定位载体像素,检查引用的颜色是否具有奇数或偶数蓝色分量,保存找到的位(它是color.B % 2 > 0
),然后继续处理下一个像素,直到消息完成
public void Extract(Stream messageStream, Stream keyStream){
//load the carrier image
Bitmap bmp = new Bitmap(sourceFileName);
BitmapData bmpData = bmp.LockBits(
new Rectangle(0,0,bmp.Width, bmp.Height),
ImageLockMode.ReadWrite,
PixelFormat.Format8bppIndexed);
//copy all pixels
byte[] pixels = new byte[bmpData.Stride*bmpData.Height];
Marshal.Copy(bmpData.Scan0, pixels, 0, pixels.Length);
Color[] palette = bmp.Palette.Entries;
byte messageByte=0, messageBitIndex=0, pixel=0;
int messageLength=0, pixelIndex=0;
//read pixels until the message is complete
while((messageLength==0) || (messageStream.Length < messageLength)){
//locate the next pixel that carries a hidden bit
pixelIndex += GetKey(keyStream);
pixel = pixels[pixelIndex];
if( (palette[pixel].B % 2) == 1 ){
//odd blue-component: message-bit was "1"
messageByte += (byte)(1 << messageBitIndex);
} //else: messageBit was "0", nothing to do
if(messageBitIndex == 7){ //a byte is complete
//save and reset messageByte, reset messageBitIndex
messageStream.WriteByte(messageByte);
messageBitIndex = 0;
messageByte = 0;
if((messageLength == 0)&&(messageStream.Length==4)){
//message's length has been read
messageStream.Seek(0, SeekOrigin.Begin);
messageLength = new BinaryReader(messageStream).ReadInt32();
messageStream.SetLength(0);
}
}else{
messageBitIndex++; //next bit
}
}
//release the carrier bitmap
bmp.UnlockBits(bmpData);
bmp.Dispose();
}
示例
图像必须有 128 种或更少的颜色,否则我们无法为每种颜色添加备用调色板条目。我认为这实际上不是限制,因为大多数索引 GIF 或 PNG 图像的颜色都少于此。如果您需要图像使用所有 256 种颜色,您应该考虑使用每像素 24 位的格式。现在,让我们看看调色板如何变化...
这是 Sternchen Canary,本次成像会话的模型
这是同一图像作为具有 64 种颜色的索引 PNG 及其调色板
启动演示应用程序,选择图像作为载体,并选择另一个文件作为密钥...
...然后单击隐藏。生成的图像包含一个 192 种颜色的调色板,看起来并没有太大区别,尽管所有颜色都被使用且没有重复。
上面看到的生成的图像携带一条包含 42 个单词的隐藏消息,可以使用演示应用程序(而不是跨格式应用程序)进行提取。此消息的密钥位于本文的第五张图片中,您需要一个十六进制编辑器将其 11 个字节键入一个密钥文件...