隐写术 12 - 不同数据密度的区域






4.81/5 (24投票s)
定义图像中的区域,以使平滑色彩区域不包含隐藏数据。
引言
一种广为人知的隐写分析方法是搜索图像的一个颜色区域的变异。在每个像素都与邻近像素颜色不同的扩散区域中,隐藏位的变异很难检测到,但大多数图片也包含颜色平滑的区域。看看这个
- 中间上方的蓝色天空不应该包含隐藏数据,因为颜色几乎没有自然变异。
- 云层有更多的蓝色色调,但任何非蓝色都会很容易被发现。如果必须隐藏任何数据在云层中,每个像素最多只能改变一位。
- 右侧的树木也是如此:改变高位会产生浅色,但这里只允许深色像素,因此每个像素的容量减至一位或两位。
- 左侧的船只和海滩则更好。它们包含红色、黄色、白色、蓝色、绿色…… 我们可以在这个区域改变多达 7 位,没人会注意到任何异常。
为了逃避简单的变异分析,我们将只在这些区域隐藏秘密消息,并以“调整的比特率”进行。
编辑和存储区域
与前面的示例一样,我们需要一个载体位图、一个秘密消息和一个密钥。
新功能是 *区域编辑器*,它允许用户定义区域及其容量。
绘制区域的一种简单方法是单击多边形的顶点。因此,我们让用户单击图像,并将每个单击的点添加到多边形中。双击可以关闭多边形,然后下一次单击将开始一个新的多边形。
private void picImage_MouseUp(object sender, MouseEventArgs e){
if (e.Button == MouseButtons.Left){
if (isDoubleClicked){
//ignore the MouseUp event following a double click
isDoubleClicked = false;
}
else{
if (!isDrawing){
//start a new polygon
isDrawing = true;
drawingPoints = new ArrayList();
cleanImage = picImage.Image;
bufferImage = new Bitmap(cleanImage.Width, cleanImage.Height);
}
AddPoint(e.X, e.Y);
}
}
}
当通过双击关闭一个多边形时,我们必须确保它不与已存在的任何多边形重叠。如果新多边形与其他多边形相交,我们就合并这两个区域。如果多边形独立存在,我们创建一个新区域并将其添加到列表中。ctlRegions
是一个 RegionInfoList
,该控件显示每个区域的统计信息和输入字段。
private void picImage_DoubleClick(object sender, EventArgs e){
if (drawingPoints.Count > 2){
isDrawing = false;
isDoubleClicked = true;
Point[] points = (Point[])drawingPoints.ToArray(typeof(Point));
GraphicsPath path = new GraphicsPath();
path.AddPolygon(points);
if (!UniteWithIntersectedRegions(path, points)){
RegionInfo info = new RegionInfo(path, points, picImage.Image.Size);
drawnRegions.Add(info); //add to regions
ctlRegions.Add(new RegionInfoListItem(info)); //add to list
}
ReDrawImages(true);
}
}
如果绘制了另一个区域,我们就必须更新地图和统计信息块。ReDrawImages
将区域绘制到源图像和地图概览上。(地图概览和渐变笔刷不是必需的,它们只是一个很好的视觉效果。)
/// <summary>Display the source image with the regions on it in [picImage],
/// and only the regions in [picMap]</summary>
/// <param name="updateSummary">true: call UpdateSummary() when finished</param>
private void ReDrawImages(bool updateSummary){
//create empty images
Image bufferImageNoBackground =
new Bitmap(baseImage.Width, baseImage.Height);
Image bufferImageWithBackground = new
Bitmap(baseImage.Width, baseImage.Height);
//get graphics
Graphics graphicsWithBackground =
Graphics.FromImage(bufferImageWithBackground);
Graphics graphicsNoBackground =
Graphics.FromImage(bufferImageNoBackground);
//draw/clear backgrounds
graphicsNoBackground.Clear(Color.White);
graphicsWithBackground.DrawImage(baseImage, 0, 0,
baseImage.Width, baseImage.Height);
//draw regions
foreach (RegionInfo info in drawnRegions){
PathGradientBrush brush =
new PathGradientBrush(info.Points, WrapMode.Clamp);
brush.CenterColor = Color.Transparent;
if (info == selectedRegionInfo){
//mark the region that's selected in the list
brush.SurroundColors = new Color[1] { Color.Green };
}else{
brush.SurroundColors = new Color[1] { Color.Red };
}
//draw the region
graphicsWithBackground.DrawPolygon(new
Pen(Color.Black, 4), info.Points);
graphicsNoBackground.DrawPolygon(new
Pen(Color.Black, 4), info.Points);
graphicsWithBackground.FillRegion(brush, info.Region);
graphicsNoBackground.FillRegion(brush, info.Region);
}
//clean up
graphicsWithBackground.Dispose();
graphicsNoBackground.Dispose();
//show images
picImage.Image = bufferImageWithBackground;
picMap.Image = bufferImageNoBackground;
picImage.Invalidate();
picMap.Invalidate();
//update numbers and errors
if (updateSummary) { UpdateSummary(); }
}
地图必须存储在图像的第一个像素中,以便在其余消息之前可以提取它。这意味着,我们必须将标题嵌入图像中。提取隐藏消息时,我们必须先提取标题,从中读取区域信息,然后我们就可以从这些区域提取实际消息。标题可以分布在从 0/0 到顶部区域第一个像素的所有像素上。在提取地图之前,我们不会知道第一个区域从哪里开始,因此我们必须在标题本身中存储属于任何区域的第一个像素的索引。此像素的坐标不重要,因为我们将像素视为一个长流,而不是行和列。一个完整的标题包含这些信息
- (
Int32
) 顶部区域的顶部像素的索引(不是坐标!)。 - (
Int32
) 后续区域数据的长度。 - 对于每个区域
- (
Int32
) 长度 (Region.GetRegionData().Data.Length
) - (
Int32
) 容量(在此区域隐藏的字节数) - (
byte
) 每像素使用的位数 - (
byte[]
) 区域 (Region.GetRegionData().Data
)
- (
标题的长度取决于区域的数量和复杂性。当在 *区域编辑器* 中添加新区域时,我们必须检查新标题的长度和顶部区域的位置。如果图像的第一个像素和第一个区域之间的像素不足,则无法隐藏标题。在这种情况下,我们将显示警告并禁用“下一步”按钮。将承载实际消息的区域必须足够大,如果消息不适合区域,我们将显示另一个警告。
private void UpdateSummary(){
bool isOkay = true; //no errors yet
long countPixels = 0; //count of selected pixels
int capacity = 0; //capacity of all regions
RegionInfo firstRegion = null; //topmost region - not found yet
//first pixel inside a region - not found yet
int firstPixelInRegions = baseImage.Width * baseImage.Height;
//Int32 beginning of first region +
//Int32 regions length + Byte bits per pixel
long mapStreamLength = 65;
foreach (RegionInfo info in drawnRegions) {
countPixels += info.CountPixels;
capacity += info.Capacity;
mapStreamLength += 64; //Int32 RegionData Length + Int32 Capacity
mapStreamLength += info.Region.GetRegionData().Data.Length * 8;
//is this region the first one?
if ((int)info.PixelIndices[0] < firstPixelInRegions) {
firstPixelInRegions = (int)info.PixelIndices[0];
firstRegion = info;
}
}
//pixels in the region
lblSelectedPixels.Text = countPixels.ToString();
//percent of the image covered by the region
lblPercent.Text = (100 * countPixels /
(baseImage.Width*baseImage.Height)).ToString();
//capacity
lblCapacity.Text = capacity.ToString();
if (capacity == messageLength) {
SetControlColor(lblCapacity, false);
errors.SetError(lblCapacity, String.Empty);
} else {
SetControlColor(lblCapacity, true);
errors.SetError(lblCapacity,
"Overall capacity must be equal to the message's length.");
isOkay = false;
}
//header size
lblHeaderSize.Text = mapStreamLength.ToString() + " Bits";
//are there enough pixels left for the header?
if (firstRegion != null) {
if (firstPixelInRegions > mapStreamLength) {
lblHeaderSpace.Text = firstPixelInRegions.ToString() + " Pixels";
SetControlColor(lblHeaderSpace, false);
} else {
isOkay = false;
lblHeaderSpace.Text = String.Format(
"{0} Pixels - Please remove the topmost region.",
firstPixelInRegions);
SetControlColor(lblHeaderSpace, true);
selectedRegionInfo = firstRegion;
ctlRegions.SelectItem(firstRegion);
ReDrawImages(false);
}
} else {
lblHeaderSpace.Text = "0 - Please define one or more regions";
SetControlColor(lblHeaderSpace, true);
}
btnNext.Enabled = isOkay;
}
private void SetControlColor(Control control, bool isError) {
if (isError) {
control.BackColor = Color.DarkRed;
control.ForeColor = Color.White;
} else {
control.BackColor = SystemColors.Control;
control.ForeColor = SystemColors.ControlText;
}
}
如果区域足够大,配置了足够的容量,并且为标题留下了足够的空间,UpdateSummary
将会启用“下一步”按钮。现在,地图和消息就可以被隐藏了。
嵌入数据
到目前为止,除了接收关于载体图像的输入数据外,我们什么也没做。现在,有趣的部分开始了!在之前的文章中,我们使用了密钥来定位像素,并简单地嵌入了消息。这现在行不通了。我们有特定的区域来分配数据,并且标题应该均匀分布在图像开头可用的像素上。这意味着,来自密钥流的字节不能直接用作下一个偏移量,但我们可以用它们来初始化一个伪随机数生成器。这个数字生成器可以决定下一个偏移量。
稍后提取消息时,我们只需使用相同的种子(来自密钥流的值)初始化 System.Ramdom
对象,就能再次获得相同的偏移量。但是,我们需要两个值来计算间隔:我们要隐藏/提取的数据长度,以及剩余像素的数量。让我们将这两个 Int32
值隐藏在前 64 个像素中,以便我们轻松读取它们。
public unsafe void Hide(Stream messageStream, Stream keyStream){
//make sure that the image has RGB format
Bitmap image = (Bitmap)carrierFile.Image;
image = PaletteToRGB(image);
int pixelOffset = 0, maxOffset = 0, messageValue = 0;
byte key, messageByte, colorComponent;
Random random;
BitmapData bitmapData = image.LockBits(
new Rectangle(0, 0, image.Width, image.Height),
ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
//go to the first pixel
PixelData* pPixel = (PixelData*)bitmapData.Scan0.ToPointer();
PixelData* pFirstPixel;
//get the first pixel that belongs to a region
int firstPixelInRegions = image.Width * image.Height;
foreach (RegionInfo info in carrierFile.RegionInfo){
info.PixelIndices.Sort();
if ((int)info.PixelIndices[0] < firstPixelInRegions){
firstPixelInRegions = (int)info.PixelIndices[0];
}
}
//hide [firstPixelInRegions]
HideInt32(firstPixelInRegions, ref pPixel);
//get map stream
MemoryStream regionData = new MemoryStream();
BinaryWriter regionDataWriter = new BinaryWriter(regionData);
foreach (RegionInfo regionInfo in carrierFile.RegionInfo)
{
byte[] regionBytes = PointsToBytes(regionInfo.Points);
regionDataWriter.Write((Int32)regionBytes.Length);
regionDataWriter.Write((Int32)regionInfo.Capacity);
regionDataWriter.Write(regionInfo.CountUsedBitsPerPixel);
regionDataWriter.Write(regionBytes);
}
//go to the beginning of the stream
regionDataWriter.Flush();
regionData.Seek(0, SeekOrigin.Begin);
//hide length of map stream
HideInt32((Int32)regionData.Length, ref pPixel);
初始值已存储后,我们可以将区域地图分布在像素 65 和第一个区域之间的所有可用像素上。
pFirstPixel = pPixel; //don't overwrite already written header
int regionByte;
while ((regionByte = regionData.ReadByte()) >= 0){
key = GetKey(keyStream);
random = new Random(key);
for (int regionBitIndex = 0; regionBitIndex < 8; ){
pixelOffset += random.Next(1,
(int)((firstPixelInRegions-1 - pixelOffset) /
((regionData.Length - regionData.Position + 1)*8))
);
pPixel = pFirstPixel + pixelOffset;
//place [regionBit] in one bit of the colour component
//rotate color components
currentColorComponent = (currentColorComponent == 2) ? 0 :
(currentColorComponent + 1);
//get value of Red, Green or Blue
colorComponent = GetColorComponent(pPixel, currentColorComponent);
//put the bits into the color component
//and write it back into the bitmap
CopyBitsToColor(1, (byte)regionByte,
ref regionBitIndex, ref colorComponent);
SetColorComponent(pPixel, currentColorComponent, colorComponent);
}
}
现在,我们已经隐藏了区域以及提取它们所需的一切。是时候进入正题,隐藏秘密消息了。
//begin with the first pixel of the image
pPixel = (PixelData*)bitmapData.Scan0.ToPointer();
pFirstPixel = pPixel;
foreach (RegionInfo regionInfo in carrierFile.RegionInfo){
//go to first pixel of this region
pPixel = (PixelData*)bitmapData.Scan0.ToPointer();
pPixel += (int)regionInfo.PixelIndices[0];
pixelOffset = 0;
for (int n = 0; n < regionInfo.Capacity; n++){
messageValue = messageStream.ReadByte();
if (messageValue < 0) { break; } //end of message
messageByte = (byte)messageValue;
key = GetKey(keyStream);
random = new Random(key);
for (int messageBitIndex = 0; messageBitIndex < 8; ){
maxOffset = (int)Math.Floor(
((decimal)(regionInfo.CountPixels - pixelOffset - 1) *
regionInfo.CountUsedBitsPerPixel)/
(decimal)((regionInfo.Capacity - n) * 8)
);
pixelOffset += random.Next(1, maxOffset);
pPixel = pFirstPixel + (int)regionInfo.PixelIndices[pixelOffset];
//place [messageBit] in one bit of the colour component
//rotate color components
currentColorComponent = (currentColorComponent == 2) ? 0 :
(currentColorComponent + 1);
//get value of Red, Green or Blue
colorComponent = GetColorComponent(pPixel, currentColorComponent);
//put the bits into the color component
//and write it back into the bitmap
CopyBitsToColor(
regionInfo.CountUsedBitsPerPixel,
messageByte, ref messageBitIndex,
ref colorComponent);
SetColorComponent(pPixel, currentColorComponent, colorComponent);
}
}
}
image.UnlockBits(bitmapData);
SaveBitmap(image, carrierFile.DestinationFileName);
}
提取数据
因此,我们有一个图像,并想从中读取隐藏的消息。我们必须以隐藏消息的相同顺序读取消息的字节。
- 获取地图数据的长度
- 获取顶部区域中第一个像素的索引
- 使用这些值提取区域
- 使用区域提取消息
让我们去获取区域!
/// <summary>Extract the header from an image</summary>
/// <remarks>The header contains information about
/// the regions which carry the message</remarks>
/// <param name="keyStream">Key stream</param>
/// <returns>The extracted regions with all meta data that
/// is needed to extract the message</returns>
public unsafe RegionInfo[] ExtractRegionData(Stream keyStream) {
byte key, colorComponent;
PixelData* pPixel;
PixelData* pFirstPixel;
int pixelOffset = 0;
Random random;
Bitmap image = (Bitmap)carrierFile.Image;
BitmapData bitmapData = image.LockBits(
new Rectangle(0, 0, image.Width, image.Height),
ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb);
//go to the first pixel
pPixel = (PixelData*)bitmapData.Scan0.ToPointer();
//get firstPixelInRegions
int firstPixelInRegions = ExtractInt32(ref pPixel);
//get length of region information
int regionDataLength = ExtractInt32(ref pPixel);
//get region information
pFirstPixel = pPixel;
MemoryStream regionData = new MemoryStream();
byte regionByte;
while (regionDataLength > regionData.Length) {
regionByte = 0;
key = GetKey(keyStream);
random = new Random(key);
for (int regionBitIndex = 0; regionBitIndex < 8; regionBitIndex++) {
//move to the next pixel
pixelOffset += random.Next(1,
(int)(
(firstPixelInRegions - 1 - pixelOffset) /
((regionDataLength - regionData.Length) * 8))
);
pPixel = pFirstPixel + pixelOffset;
//rotate color components
currentColorComponent = (currentColorComponent == 2) ? 0 :
(currentColorComponent + 1);
//get value of Red, Green or Blue
colorComponent = GetColorComponent(pPixel, currentColorComponent);
//extract one bit and add it to [regionByte]
AddBit(regionBitIndex, ref regionByte, 0, colorComponent);
}
//write the extracted byte
regionData.WriteByte(regionByte);
}
image.UnlockBits(bitmapData);
现在,我们已经重建了地图流。要对其执行任何有用的操作,我们必须重建区域。
//read regions from [regionData]
ArrayList regions = new ArrayList();
BinaryReader regionReader = new BinaryReader(regionData);
Region anyRegion = new Region(); //dummy region
RegionData anyRegionData = anyRegion.GetRegionData(); //dummy region data
Region region; //extracted region
byte[] regionContent; //extracted region data
//extracted region header
int regionLength, regionCapacity;
byte regionBitsPerPixel;
regionReader.BaseStream.Seek(0, SeekOrigin.Begin);
do {
//If the program crashes here,
//the image is damaged,
//it contains no hidden data,
//or you tried to use a wrong key.
int regionLength = regionReader.ReadInt32();
int regionCapacity = regionReader.ReadInt32();
byte regionBitsPerPixel = regionReader.ReadByte();
byte[] regionContent = regionReader.ReadBytes(regionLength);
Point[] regionPoints = BytesToPoints(regionContent);
GraphicsPath regionPath = new GraphicsPath();
regionPath.AddPolygon(regionPoints);
Region region = new Region(regionPath);
regions.Add(new RegionInfo(region, regionCapacity,
regionBitsPerPixel, image.Size));
} while (regionData.Position < regionData.Length);
return (RegionInfo[])regions.ToArray(typeof(RegionInfo));
}
我们快完成了。现在,我们知道消息嵌入在哪些区域,每个区域隐藏了多少字节,以及必须从像素中提取多少比特/像素。
/// <summary>Extract a message</summary>
/// <param name="messageStream">Empty stream to receive
/// the extracted message</param>
/// <param name="keyStream">Key stream</param>
public unsafe void Extract(Stream messageStream, Stream keyStream) {
//lock the bitmap, go to the first pixel, and so on
//...
//...
foreach (RegionInfo regionInfo in carrierFile.RegionInfo) {
//go to first pixel of this region
pFirstPixel = (PixelData*)bitmapData.Scan0.ToPointer();
pPixel = pFirstPixel + (int)regionInfo.PixelIndices[0];
pixelOffset = 0;
for (int n = 0; n < regionInfo.Capacity; n++) {
messageByte = 0;
key = GetKey(keyStream);
random = new Random(key);
for (int messageBitIndex = 0; messageBitIndex < 8; ) {
//move to the next pixel
maxOffset = (int)Math.Floor(
((decimal)(regionInfo.CountPixels - pixelOffset - 1) *
regionInfo.CountUsedBitsPerPixel)/
(decimal)((regionInfo.Capacity - n) * 8)
);
pixelOffset += random.Next(1, maxOffset);
pPixel = pFirstPixel +
(int)regionInfo.PixelIndices[pixelOffset];
//rotate color components
currentColorComponent = (currentColorComponent == 2) ? 0 :
(currentColorComponent + 1);
//get value of Red, Green or Blue
colorComponent = GetColorComponent(pPixel,
currentColorComponent);
for(int carrierBitIndex=0; carrierBitIndex <
regionInfo.CountUsedBitsPerPixel; carrierBitIndex++)
{
AddBit(messageBitIndex, ref messageByte,
carrierBitIndex, colorComponent);
messageBitIndex++;
}
}
//add the re-constructed byte to the message
messageStream.WriteByte(messageByte);
}
}
//clean up
//...
//...
}
完成!现在,让我们显示消息以及从中读取它的区域。
这就是我们摆脱那些试图通过寻找载体图像均匀部分中意想不到的变异来发现我们隐藏消息的人所需要的一切。
顺便说一句,还有一个额外的秘密消息通道:如果您确定收件人有足够的创造力能够看到它,您可以使用区域编辑器绘制轮廓和字母。