Diese Seite erklärt, wie binäre Daten in einem Paletten-Bild (8 Bits/Pixel) versteckt werden können. Die Methode ist eine ganz andere als die, die vorher für RBG (24 Bits/Pixel) Bitmaps verwendet wurde. Dennoch kann es ganz nützlich sein, den ersten Teil gelesen zu haben, falls Dir die Unterschiede zwischen den beiden Arten von Bitmaps noch nicht bekannt sind.
Auf den vorhergehenden Seiten haben wir die Bits der geheimen Nachrichten in den unteren Bits der Farbwerte versteckt. In einer indizierten Bitmap stehen die Farbwerte nicht in den Pixeln, sondern in der Palette:
24 Bits pro Pixel, keine Palette
8 Bits pro Pixel, Palette von beliebiger Größe (1 - 256)
Deine erste Idee könnte sein, die Nachricht in der Palette statt in den Pixeln zu verstecken. Aber die Palette enthält 256 oder weniger Farben, sie könnte nur ein paar Bytes verstecken. Dieses Problem führt zur zweiten Idee: Vergrößern wir die Palette doch, kopieren wird die Farbwerte!
Wenn wir die Palette verdoppeln, bekommen wir zwei alternative Indizes für jede Farbe. Hey, das könnte eine bessere Möglichkeit sein! Eine Farbe aus den ersten zwei Zeilen zu referenzieren könnte “Nachrichten-Bit ist 0” heissen, und die gleiche Farbe aus den unteren zwei Zeilen zu referenzieren könnte “Nachrichten-Bit ist 1” bedeuten. Aber wieder gibt es ein Problem: Eine Palette mit doppelten Farben ist Quatsch. Niemand würde jemals so eine Palette verwenden. Eine verdoppelte Palette ist viel zu offensichtlich.
Also können wir die Palette nicht einfach kopieren, aber wir können Farben einfügen, die ein wenig von den vorhandenen Farben abweichen. Angenommen, wir fügen für jede Farbe der Palette zwei ähnliche (aber nicht gleiche) Farbwerte ein:
Jetzt haben wir eine gestreckte Palette, aber das Vorhandensein einer versteckten Nachricht ist genauso offensichtlich, weil nur jede dritte Farbe tatsächlich verwendet wird. Das ist überhaupt kein Problem, wir müssen nur die Pixel ändern, so dass ein paar der Pixel, die eine Original-Farbe referenziert haben, jetzt eine ihrer Kopien referenzieren.
Jetzt können wir etwas in den Pixeln verstecken. Für ein Nachrichten-Bit von “1” lassen wir ein Pixel auf die Original-Farbe verweisen, und für ein Nachrichten-Bit von “0” lassen wir es auf eine der hinzugefügten Farben verweisen. Aber wie sollen wir die versteckte Nachricht wieder auslesen? Wenn wir nur die gestreckte Palette kennen, wissen wir nicht, welche Farben von der Original-Palette kopiert und welche hinzugefügt wurden.
Gibt es eine Möglichkeit, einen Farbwert als “aus der Original-Palette” zu markieren? Tja… da wären die niedrigsten Bits in jeder Farbe. Dieser Name ist nicht ganz richtig. Dank üblicher Bildschirme und schwacher menschlicher Augen verdient das unterste Bits einer Farbkomponente den Namen irrelevantes Bit. Indem wir das erste Bit einer Farbkomponente setzen oder nicht, können wir die Palette strecken und jede Farbe als kopiert oder neu markieren:
Während wir die neue Palette erstellen, müssen wir die kopierten und eingefügten Farben mitschreiben,
weil wir später die Pixel anpassen werden und dafür in der Lage sein müssen, eine Original-Farbe (mit gerader
Blaukomponente) für eine “0”, oder eine neue Farbe für eine “1” zu referenzieren.
Die Methode StretchPalette
verwendet einen Hashtable
, um jedem Index der alten Palette
die entsprechenden Indizes in der neuen Palette zuzuordnen.
/// <summary>
/// Erstellt eine größere Palette durch Duplizieren
/// und Verändern der Farben einer anderen Palette
/// </summary>
/// <param name="oldPalette">Die Palette, die getreckt werden soll</param>
/// <param name="maxPaletteSize">Anzahl der Farben in der neuen Palette</param>
/// <param name="newPalette">Erhält die neuen Paletten-Einträge</param>
/// <param name="colorIndexToNewIndices">
/// Erhält einen Hashtable mit den Original-Farben als Schlüssel,
/// und den entsprechenden neuen Indizes als Werte
/// </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
}
}
}
}
Falls Du den Code-Ausschnitt gelesen hast (andernfalls kannst du es nachholen, wenn Du den vollständigen
Quellcode heruntergeladen hast), hast Du wahrscheinlich die Methode GetSimilarColor
bemerkt.
Diese Methode gibt eine Variation einer Farbe zurück:
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;
}
Jetzt haben wir eine neue Palette, und eine Schlüssel/Wert-Tabelle, um alte Indizes
ihren neuen Indizes zuzuordnen.
Der nächste Schritt ist endlich, die Bits der Nachricht zu verstecken, während das Bild kopiert wird.
System.Drawing.Image
hat eine Eigenschaft Palette
vom Typ ColorPalette
. Das ist eine der restriktivsten Klassen, ide ich je gesehen habe.
Sie hat zwei Eigenschaften, Flags
und Entries
-
beide sind schreibgeschützt. ColorPalette
erlaubt es, die Farben der orhandenen Palette zu
ändern, aber wir können keine Farben einfügen.
Ich wollte nicht stundenlang nach eine sauberen .NET Lösung suchen, eine neue Bitmap zu schreiben ist einfacher:
/// <summary>
/// Erstellt ein Bild mit gestreckter Palette,
/// konvertiert die Pixel des Original-Bildes für die neue Palette,
/// und versteckt eine Nachricht in den konvertierten Pixeln
/// </summary>
/// <param name="bmp">Original-Bilde</param>
/// <param name="palette">Neue Palette</param>
/// <param name="colorIndexToNewIndices">
/// Hashtable der jedem Index der Original-Palette
/// eine Liste von Indizes in der neuen Palette zuordnet
/// </param>
/// <param name="messageStream">Geheime Nachricht</param>
/// <param name="keyStream">
/// Schlüssel der die Entfernungen zwischen zwei zum
/// Verstecken verwendeten Pixeln angibt
/// </param>
/// <returns>Das neue Bild</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);
}
}
//Jetzt haben wir die Palette und die neuen Pixel.
//Genug Informationen um die Bitmap zu schreiben !
</b>
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;
}
Eine versteckte Nachricht auszulesen ist viel einfacher, als sie zu verstecken. Es gibt nur eine Palette, und wir müssen uns nicht mit alten und neuen Indizes herumschlagen. Wir verwenden einfach den Verteilungsschlüssel um ein Träger-Pixel zu finden, prüfen die referenzierte Farbe nach gerader oder ungerade Blaukomponente, speichern das gefundene Bit (welches color.B % 2 > 0 ist) und machen mit dem nächsten Pixel weiter, bis dsie Nachricht vollständig ist:
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();
}
Die Bilder dürfen nur 128 oder weniger Farben enthalten, sonst könnten wir nicht für jede Farbe einen alternativen Paletteneintrag hinzufügen. Für eine ernsthafte Einschränkung halte ich das jedoch nicht, denn die meisten indizierten GIF oder PNG Bilder haben weniger Farben. Wenn alle 256 Farben für ein Bild gebraucht werden, ist ein Format mit 24 Bits/Pixel besser geeignet. Aber jetzt schauen wir endlich zu, wie sich die Palette verändert…
Das ist Sternchen Kanari, unser Modell:
Hier sind das gleiche Foto als indiziertes PNG mit 64 Farben und seine Palette:
Starte die Demo-Anwendung, wähle das Bild als Träger und irgendeine andere Datei als Schlüssel…
…und klick Hide. Das generierte Bild enthält eine Palette mit 192 Farben, und sieht nicht viel anders aus, obwohl alle Farben verwendet werden und keine sich wiederholt.
Das generierte Bild das Du hier siehst enthält eine versteckte Nachricht von 42 Wörtern, die mit der Demo-Anwendung ausgelesen werden kann. Der Schlüssel zur Nachricht steckt irgendwo im fünften Bild dieser Seite. Du brauchst einen Hexadezimal-Editor, um seine 11 Bytes in eine Schlüssel-Datei zu tippen…