【问题标题】:Fast image conversion to 8 bit palette format in memory在内存中快速将图像转换为 8 位调色板格式
【发布时间】:2019-04-08 23:57:30
【问题描述】:

我正在编写一个对快速图像处理有关键要求的应用程序。会有大量的图像。我需要以可快速处理的格式保存它们。所以,我决定保存 colorful 具有 8 位颜色深度的 mipmap(使用调色板),然后将其保存为 byte 数组。所以,我需要一种在内存中快速转换格式的方法。此外,它必须尽可能跨平台(我在 Windows 上编写代码并将部署到 AWS)。

我已经尝试过ImageSharp 和.NET Core System.Drawing.Common 包。在 ImageSharp 的情况下,我得到了几乎可以接受的结果:调整大小的图像的质量是完美的,但它的工作速度真的很慢......我得到了 12 秒 4096x4096 24-bit full palette image。生成的调色板中有 256 种不同的颜色,这对我来说是完美的。 在 System.Drawing.Common 库的情况下,我得到了更快的转换速度:

  • tiff 格式为 900 毫秒
  • gif 格式为 2500 毫秒。

但是,结果调色板中不同颜色的数量:

  • 对于 tiff:224(对我的解决方案不太好)
  • 对于 gif:252(好) 此外,生成的 tiff 格式的图像质量非常很差,因此可能无法使用(也许我错了)。

System.Drawing.Common 实现:

var parameters = new EncoderParameters(1);
parameters.Param[0] = new EncoderParameter(Encoder.ColorDepth, 8L);

ImageCodecInfo bmpEncoder = ImageCodecInfo.GetImageEncoders()
    .FirstOrDefault(x => x.FormatID == ImageFormat.Gif.Guid);

Stopwatch sw = Stopwatch.StartNew();

var allColors = new Bitmap(@"16777216colors.png");

Console.WriteLine("Loaded: " + sw.ElapsedMilliseconds); // 462

using (MemoryStream ms = new MemoryStream())
{
    allColors.Save(ms, bmpEncoder, parameters);

    Console.WriteLine("Saved: " + sw.ElapsedMilliseconds); // 2104

    ms.Position = 0;
    Bitmap bitmap = new Bitmap(ms);

    Console.WriteLine("Loaded: " + sw.ElapsedMilliseconds); // 2383

    // set to determine palette size
    HashSet<int> set = new HashSet<int>();

    // Temporary solution. It is needed to replace GetPixel to faster analog
    for (int i = 0; i < bitmap.Width; i++)
    {
        for (int j = 0; j < bitmap.Height; j++)
        {
            var val = bitmap.GetPixel(i, j).ToArgb();

            set.Add(val);
        }
    }

    Console.WriteLine("Palette size: " + set.Count); // 252
    Console.WriteLine("All done: " + sw.ElapsedMilliseconds); // 26345
}

/*
    Output:

    Loaded: 462
    Saved: 2104
    Loaded: 2383
    Palette size: 252
    All done: 26345
*/

ImageSharp 实现:

PngEncoder encoder = new PngEncoder()
{
    ColorType = PngColorType.Palette,
    BitDepth = PngBitDepth.Bit8,
    CompressionLevel = 1
};

Image<Rgba32> img;

Stopwatch sw = Stopwatch.StartNew();

using (var file = File.OpenRead(@"16777216colors.png"))
{
    img = Image.Load(file);
}

Console.WriteLine("Loaded: " + sw.ElapsedMilliseconds); // 1302

using (MemoryStream ms = new MemoryStream())
{
    img.Save(ms, encoder);
    Console.WriteLine("Saved: " + sw.ElapsedMilliseconds); // 12608

    ms.Position = 0;
    img = Image.Load(ms);
    Console.WriteLine("Loaded: " + sw.ElapsedMilliseconds); // 12813
}


// set to determine palette size
HashSet<uint> set = new HashSet<uint>();

for (int i = 0; i < img.Width; i++)
{
    for (int j = 0; j < img.Height; j++)
    {
        var val = img[i, j].PackedValue;

        set.Add(val);
    }
}

Console.WriteLine("Palette size: " + set.Count); // 256
Console.WriteLine("All done: " + sw.ElapsedMilliseconds); // 15807

/*
    Output:

    Loaded: 1302
    Saved: 12608
    Loaded: 12813
    Palette size: 256
    All done: 15807
*/

因此,最好的方法似乎是使用 System.Drawing.Common 实现,但可能会有与缓慢像素读取相关的开销。 那么,问题:

  • 我希望 ImageSharp 具有高性能,所以我想我在做 出了点问题...知道如何提高性能会很酷!如果有人知道我做错了什么,请告诉我。
  • 是否保证调色板颜色将是 任何图像都一样,所以,我将能够缓存这 256 种颜色之间的任意色差?
  • 有没有人知道以其他方式解决我的问题的最佳方法?我很高兴看到建议。解决方案必须是跨平台且速度尽可能快的字节数组数据表示

【问题讨论】:

  • 听起来这属于代码审查
  • 为什么不使用 .bmp 文件格式?它处理您的颜色域,并且可能在 Windows 中内置了更快的转换器(不确定)。
  • superuser.com/questions/1101160/… - 还分享了一个可能的库来尝试。
  • 诚实的问题:如果您需要所有这些速度和跨平台,您为什么使用 C#?再给你一个:social.msdn.microsoft.com/Forums/vstudio/en-US/…
  • 如果您想使用 .Net GDI 类处理位图,那么您应该考虑使用 Bitmap.LockBits Method 及其示例代码来检索像素数据。

标签: c# image-processing .net-core


【解决方案1】:

尝试使用Accord。快不快很难说。你需要在你的场景中尝试一下。

您想要 8bpp 图像吗?所以你希望它是灰度?然后,从using Accord.Imaging.Filters; 使用这个函数。

private Bitmap Create8bppGreyscaleImage(Bitmap bitmap) 
                                  => Grayscale.CommonAlgorithms.BT709.Apply(bitmap);

This 上的此转换 jpg 图像采用 500ms。生成的图像如下所示:

如您所见,Grayscale.CommonAlghoritms.BT709 将Bitmap 作为参数。如果要字节数组,只需使用Marshall.Copy:

BitmapData bitmapData = this.Bitmap.LockBits(
            new Rectangle(0, 0, this.Bitmap.Width, this.Bitmap.Height),
            ImageLockMode.ReadWrite,
            this.Bitmap.PixelFormat);

        int pixels = bitmapData.Stride * this.Bitmap.Height;
        byte[] bytes = new byte[pixels];

        Marshal.Copy(bitmapData.Scan0, bytes, 0, pixels);

        this.Bitmap.UnlockBits(bitmapData);
        this.Bytes = bytes;

通过此方法将图像加载为 8bpp 字节数组(上图)采用18ms。

它将字节数组保存到内存中。然后,您可以解锁位图并将其保存为例如.png:

BitmapData bitmapData = this.Bitmap.LockBits(
            new Rectangle(0, 0, this.Bitmap.Width, this.Bitmap.Height),
            ImageLockMode.ReadWrite,
            this.Bitmap.PixelFormat);

        int stride = bitmapData.Stride * this.Height;

        Marshal.Copy(bytes, 0, bitmapData.Scan0, stride);
        this.Bitmap.UnlockBits(bitmapData);
        this.Bitmap.Save("test.png");

如果我的回答对你有帮助,请告诉我。您在 .NET Core 中没有开箱即用的 System.Drawing.dll 引用,您需要自己添加它

【讨论】:

  • 非常感谢您如此详细的回复!但恐怕我需要一个带有静态调色板的彩色图像。无论如何,这是 Marshal.Copy 的一个很好的方法,如果我决定使用 System.Drawing.Common 或 Accord,这对我会有帮助。
  • 我不明白,你想剪掉2个频道,仍然有彩色图像?它不是那样工作的。当您切割 2 个通道并让其始终是灰度图像时。 Marshal.Copy 是从图像计算字节流的好策略,所以我认为我值得支持;p
  • 您提到,System.Drawing 很慢。是的,通过 System.Drawing 获取/设置像素非常慢,因为每次调用 Get/SetPixel() 方法时,所有位图都加载到内存中,该方法获取一个特定像素,然后从内存中卸载整个位图。通过 Marshal.Copy,您通过 lockbits 将图像加载为字节数组(1d 数组)一次,然后您可以通过解锁我在回答中提到的位来卸载该数组。要将图像加载和卸载到内存,您需要使用 Marshal.Copy。没有更快的方法,即使是 OpenCV 和 Python 也会慢一点
  • 8 位彩色像素由一组颜色(理想情况下为 256)表示。当需要从 24 bpp 颜色中获得 8-bpp 颜色时,您需要从该集合中找到最接近的颜色。因此,这个 8 位值将表示为集合的索引。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-07-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-07
  • 1970-01-01
  • 2015-08-23
相关资源
最近更新 更多