没有 GetPixel 的索引 8bpp 图像和像素数组
Indexed 8bpp image and array of pixels without GetPixel
我需要在调色板中获取位图每个像素的索引(我有一个索引为 8bpp 的图像)。现在我使用以下方式:
List<byte> data = new List<byte>(); // indexes
List<Color> pixels = bitmap.Palette.Entries.ToList(); // palette
for (int i = 0; i <bitmap.Height; i++)
for (int j = 0; j < bitmap.Width; j++)
data.Add((byte)pixels[k].IndexOf(bitmap.GetPixel(j, i)));
但是这种方式工作起来很慢,因为我使用了几个高分辨率的图像。
我的问题是:
1)有没有办法加快循环获取每个像素RGBA值的过程?
2) 也许有更好的方法来获取调色板中图像的颜色索引?
也许这样的事情会更快。 Getbits
和 Setbits
的推理非常慢,每个都在内部调用锁定内部存储器的锁位。最好一次全部完成
使用 LockBits
、unsafe
、fixed
和 Dictionary
为了测试结果,我使用了这张图片
来自
我用你原来的版本测试结果是一样的
基准测试
----------------------------------------------------------------------------
Mode : Release (64Bit)
Test Framework : .NET Framework 4.7.1 (CLR 4.0.30319.42000)
----------------------------------------------------------------------------
Operating System : Microsoft Windows 10 Pro
Version : 10.0.17134
----------------------------------------------------------------------------
CPU Name : Intel(R) Core(TM) i7-2600 CPU @ 3.40GHz
Description : Intel64 Family 6 Model 42 Stepping 7
Cores (Threads) : 4 (8) : Architecture : x64
Clock Speed : 3401 MHz : Bus Speed : 100 MHz
L2Cache : 1 MB : L3Cache : 8 MB
----------------------------------------------------------------------------
测试 1
--- Random Set ------------------------------------------------------------
| Value | Average | Fastest | Cycles | Garbage | Test | Gain |
--- Scale 1 ------------------------------------------------ Time 8.894 ---
| Mine1 | 5.211 ms | 4.913 ms | 17.713 M | 0.000 B | Pass | 93.50 % |
| Original | 80.107 ms | 75.131 ms | 272.423 M | 0.000 B | Base | 0.00 % |
---------------------------------------------------------------------------
完整代码
public unsafe byte[] Convert(string input)
{
using (var bmp = new Bitmap(input))
{
var pixels = bmp.Palette.Entries.Select((color, i) => new {x = color,i})
.ToDictionary(arg => arg.x.ToArgb(), x => x.i);
// lock the image data for direct access
var bits = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppPArgb);
// create array as we know the size
var data = new byte[bmp.Height * bmp.Width];
// pin the data array
fixed (byte* pData = data)
{
// just getting a pointer we can increment
var d = pData;
// store the max length so we don't have to recalculate it
var length = (int*)bits.Scan0 + bmp.Height * bmp.Width;
// Iterate through the scanlines of the image as contiguous memory by pointer
for (var p = (int*)bits.Scan0; p < length; p++, d++)
//the magic, get the pixel, lookup the Dict, assign the values
*d = (byte)pixels[*p];
}
// unlock the bitmap
bmp.UnlockBits(bits);
return data;
}
}
总结
无论如何我都不是图像专家,如果这不起作用,那么您的索引图像可能有一些我不理解的不同之处
更新
要检查像素格式,如果它有调色板,您可以使用以下内容
bmp.PixelFormat
bmp.Palette.Entries.Any()
更新2
来自Vlad i Slav的工作解决方案如下
I needed to replace PixelFormat.Format32bppPArgb
to Format32bppArgb
and to
add this checking
if (pixels.ContainsKey(*p))
*d = (byte)pixels[*p];
else
*d = 0;.
Also need to take a distinct values from palette, because I have been
give some errors there.
如果事先检查过pixelformat,确定是PixelFormat.Format8bppIndexed
,就可以直接在LockBits
调用中使用bmp.PixelFormat
,得到real 直接像素值。这也避免了在调色板包含重复颜色的情况下得到错误的索引。
不过,你必须小心步幅; .Net 中图像中每行的字节长度为 rounded up to the next multiple of 4 bytes. So on an image with, for example, a width of 386 (like your penguin),实际数据将为每行 388 字节。这就是他们所说的 'stride'。为了避免出现问题,而是完全压缩数据,复制每行的输入数据,以实际使用的行数据的长度为块,同时移动读取 pointer 按 BitmapData
对象给出的全步前进。
最小步幅通常计算为(bpp * width + 7) / 8
。对于 8 位图像,当然,这将简单地等于宽度,因为它是每个像素 1 个字节,但我为它编写的函数支持任何位深度,甚至低于 8bpp,这就是为什么要进行计算。
完整功能:
/// <summary>
/// Gets the raw bytes from an image.
/// </summary>
/// <param name="sourceImage">The image to get the bytes from.</param>
/// <param name="stride">Stride of the retrieved image data.</param>
/// <param name="collapseStride">Collapse the stride to the minimum required for the image data.</param>
/// <returns>The raw bytes of the image.</returns>
public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride, Boolean collapseStride)
{
if (sourceImage == null)
throw new ArgumentNullException("sourceImage", "Source image is null!");
Int32 width = sourceImage.Width;
Int32 height = sourceImage.Height;
BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
stride = sourceData.Stride;
Byte[] data;
if (collapseStride)
{
Int32 actualDataWidth = ((Image.GetPixelFormatSize(sourceImage.PixelFormat) * width) + 7) / 8;
Int64 sourcePos = sourceData.Scan0.ToInt64();
Int32 destPos = 0;
data = new Byte[actualDataWidth * height];
for (Int32 y = 0; y < height; ++y)
{
Marshal.Copy(new IntPtr(sourcePos), data, destPos, actualDataWidth);
sourcePos += stride;
destPos += actualDataWidth;
}
stride = actualDataWidth;
}
else
{
data = new Byte[stride * height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
}
sourceImage.UnlockBits(sourceData);
return data;
}
在您的情况下,可以这样简单地调用:
if (image.PixelFormat == PixelFormat.Format8bppIndexed)
{
Int32 stride;
Byte[] rawData = GetImageData(image, out stride, true);
// ... do whatever you want with that image data.
}
请注意,正如我在对其他答案的评论中所说,.Net 框架有一个错误,该错误会阻止包含调色板透明度信息的 8 位 PNG 图像被正确加载为 8 位。要解决该问题,请查看此答案:
A: How to read 8-bit PNG image as 8-bit PNG image only?
我需要在调色板中获取位图每个像素的索引(我有一个索引为 8bpp 的图像)。现在我使用以下方式:
List<byte> data = new List<byte>(); // indexes
List<Color> pixels = bitmap.Palette.Entries.ToList(); // palette
for (int i = 0; i <bitmap.Height; i++)
for (int j = 0; j < bitmap.Width; j++)
data.Add((byte)pixels[k].IndexOf(bitmap.GetPixel(j, i)));
但是这种方式工作起来很慢,因为我使用了几个高分辨率的图像。
我的问题是:
1)有没有办法加快循环获取每个像素RGBA值的过程?
2) 也许有更好的方法来获取调色板中图像的颜色索引?
也许这样的事情会更快。 Getbits
和 Setbits
的推理非常慢,每个都在内部调用锁定内部存储器的锁位。最好一次全部完成
使用 LockBits
、unsafe
、fixed
和 Dictionary
为了测试结果,我使用了这张图片
来自
我用你原来的版本测试结果是一样的
基准测试
----------------------------------------------------------------------------
Mode : Release (64Bit)
Test Framework : .NET Framework 4.7.1 (CLR 4.0.30319.42000)
----------------------------------------------------------------------------
Operating System : Microsoft Windows 10 Pro
Version : 10.0.17134
----------------------------------------------------------------------------
CPU Name : Intel(R) Core(TM) i7-2600 CPU @ 3.40GHz
Description : Intel64 Family 6 Model 42 Stepping 7
Cores (Threads) : 4 (8) : Architecture : x64
Clock Speed : 3401 MHz : Bus Speed : 100 MHz
L2Cache : 1 MB : L3Cache : 8 MB
----------------------------------------------------------------------------
测试 1
--- Random Set ------------------------------------------------------------
| Value | Average | Fastest | Cycles | Garbage | Test | Gain |
--- Scale 1 ------------------------------------------------ Time 8.894 ---
| Mine1 | 5.211 ms | 4.913 ms | 17.713 M | 0.000 B | Pass | 93.50 % |
| Original | 80.107 ms | 75.131 ms | 272.423 M | 0.000 B | Base | 0.00 % |
---------------------------------------------------------------------------
完整代码
public unsafe byte[] Convert(string input)
{
using (var bmp = new Bitmap(input))
{
var pixels = bmp.Palette.Entries.Select((color, i) => new {x = color,i})
.ToDictionary(arg => arg.x.ToArgb(), x => x.i);
// lock the image data for direct access
var bits = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format32bppPArgb);
// create array as we know the size
var data = new byte[bmp.Height * bmp.Width];
// pin the data array
fixed (byte* pData = data)
{
// just getting a pointer we can increment
var d = pData;
// store the max length so we don't have to recalculate it
var length = (int*)bits.Scan0 + bmp.Height * bmp.Width;
// Iterate through the scanlines of the image as contiguous memory by pointer
for (var p = (int*)bits.Scan0; p < length; p++, d++)
//the magic, get the pixel, lookup the Dict, assign the values
*d = (byte)pixels[*p];
}
// unlock the bitmap
bmp.UnlockBits(bits);
return data;
}
}
总结
无论如何我都不是图像专家,如果这不起作用,那么您的索引图像可能有一些我不理解的不同之处
更新
要检查像素格式,如果它有调色板,您可以使用以下内容
bmp.PixelFormat
bmp.Palette.Entries.Any()
更新2
来自Vlad i Slav的工作解决方案如下
I needed to replace
PixelFormat.Format32bppPArgb
toFormat32bppArgb
and to add this checking
if (pixels.ContainsKey(*p))
*d = (byte)pixels[*p];
else
*d = 0;.
Also need to take a distinct values from palette, because I have been give some errors there.
如果事先检查过pixelformat,确定是PixelFormat.Format8bppIndexed
,就可以直接在LockBits
调用中使用bmp.PixelFormat
,得到real 直接像素值。这也避免了在调色板包含重复颜色的情况下得到错误的索引。
不过,你必须小心步幅; .Net 中图像中每行的字节长度为 rounded up to the next multiple of 4 bytes. So on an image with, for example, a width of 386 (like your penguin),实际数据将为每行 388 字节。这就是他们所说的 'stride'。为了避免出现问题,而是完全压缩数据,复制每行的输入数据,以实际使用的行数据的长度为块,同时移动读取 pointer 按 BitmapData
对象给出的全步前进。
最小步幅通常计算为(bpp * width + 7) / 8
。对于 8 位图像,当然,这将简单地等于宽度,因为它是每个像素 1 个字节,但我为它编写的函数支持任何位深度,甚至低于 8bpp,这就是为什么要进行计算。
完整功能:
/// <summary>
/// Gets the raw bytes from an image.
/// </summary>
/// <param name="sourceImage">The image to get the bytes from.</param>
/// <param name="stride">Stride of the retrieved image data.</param>
/// <param name="collapseStride">Collapse the stride to the minimum required for the image data.</param>
/// <returns>The raw bytes of the image.</returns>
public static Byte[] GetImageData(Bitmap sourceImage, out Int32 stride, Boolean collapseStride)
{
if (sourceImage == null)
throw new ArgumentNullException("sourceImage", "Source image is null!");
Int32 width = sourceImage.Width;
Int32 height = sourceImage.Height;
BitmapData sourceData = sourceImage.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, sourceImage.PixelFormat);
stride = sourceData.Stride;
Byte[] data;
if (collapseStride)
{
Int32 actualDataWidth = ((Image.GetPixelFormatSize(sourceImage.PixelFormat) * width) + 7) / 8;
Int64 sourcePos = sourceData.Scan0.ToInt64();
Int32 destPos = 0;
data = new Byte[actualDataWidth * height];
for (Int32 y = 0; y < height; ++y)
{
Marshal.Copy(new IntPtr(sourcePos), data, destPos, actualDataWidth);
sourcePos += stride;
destPos += actualDataWidth;
}
stride = actualDataWidth;
}
else
{
data = new Byte[stride * height];
Marshal.Copy(sourceData.Scan0, data, 0, data.Length);
}
sourceImage.UnlockBits(sourceData);
return data;
}
在您的情况下,可以这样简单地调用:
if (image.PixelFormat == PixelFormat.Format8bppIndexed)
{
Int32 stride;
Byte[] rawData = GetImageData(image, out stride, true);
// ... do whatever you want with that image data.
}
请注意,正如我在对其他答案的评论中所说,.Net 框架有一个错误,该错误会阻止包含调色板透明度信息的 8 位 PNG 图像被正确加载为 8 位。要解决该问题,请查看此答案:
A: How to read 8-bit PNG image as 8-bit PNG image only?