一种从输入数组创建位图的更快方法,而不是使用 for 循环和 SetPixel()

Posted

技术标签:

【中文标题】一种从输入数组创建位图的更快方法,而不是使用 for 循环和 SetPixel()【英文标题】:A faster way to create Bitmap from input array instead of using for loops and SetPixel( ) 【发布时间】:2020-12-05 08:39:16 【问题描述】:

我有以下 工作 代码从源 pixels 数组创建Bitmap。 源数组的类型是 byte[ ],它是库函数的输出,它是一个 flatten 数组 3D => 1D。

我创建位图的代码:

var bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb);
for (int j = 0; j < height; j++) 
    for (int i = 0; i < width; i++) 
        // unflattening 1D array using 3D coordinates
        //      index = z *   yMax *  xMax + y *  xMax + x
        var rv = pixels[0 * height * width + j * width + i];
        var gv = pixels[1 * height * width + j * width + i];
        var bv = pixels[2 * height * width + j * width + i];
        bmp.SetPixel(i, j, Color.FromArgb(rv, gv, bv));
    

上面的代码可以工作,但是。对于 2048 x 1364 位图,创建大约需要 2 秒。 我检查了 *** 是否有类似情况,有使用 BitmapData 和 Marshal.Copy 的解决方案,但这些情况是源数组不是 3D=>1D 扁平数组的情况。当然,我尝试使用 BitmapData 并从像素复制到BitmapData.Scan0,但得到了错误的位图。 有没有什么技巧,如何加快位图的创建速度?

更新: 根据下面的 cmets,我最终得到了这个代码,它的运行速度要快得多:

var bytes = new byte[pixels.Length];
var idx = 0L;
var area = height * width;
var area2 = 2 * area;
var bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb);
var bmd = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, bmp.PixelFormat);
for (int j = 0; j < height; j++) 
    for (int i = 0; i < width; i++) 
        var Unflatten1D = j * width + i;
        bytes[idx]      = pixels[Unflatten1D];
        bytes[idx + 1]  = pixels[Unflatten1D + area];
        bytes[idx + 2]  = pixels[Unflatten1D + area2];
        idx += 3;
    

Marshal.Copy(bytes, 0, bmd.Scan0, bytes.Length);
bmp.UnlockBits(bmd);

【问题讨论】:

如果你先“解压”数组,但不为每个像素调用SetPixel() - 将其存储在临时数组中;然后使用BitmapData / Marshal.Copy 方法?基本上,我认为是SetPixel() 调用很慢,而不是计算。临时数组将采用正确的格式,因此您最终会得到正确的位图。 Of course I tried to use BitmapData and copy from pixels to BitmapData.Scan0 but got a wrong bitmap. 你应该问一个关于如何解决这个问题的问题。 LockBits 是正确的方法。向我们展示您尝试使用它的代码,我们可以帮助您解决此问题。 使用不安全代码(如下)或 LockBits 谢谢@CoolBots,你的想法很完美! SetPixel() 方法在代码中很慢——很明显。打算用一个有效且快速的解决方案更新我的帖子。 几年前我在一个相关问题上发布了一个完整的解决方案,用于将字节数组转储到图像中:***.com/a/43967594/395685 扁平化当然可以简单地提前完成,在字节数组中。 【参考方案1】:

您可以使用 /unsafe 标志编译您的应用程序(在 Visual Studio 中右键单击项目,然后属性然后允许不安全代码)

接下来让方法代码不安全。例如:

public unsafe void CreateImage()

然后改代码如下。

var bmp = new Bitmap(width, height, PixelFormat.Format24bppRgb);
var bitmapData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);


byte* bits = (byte*)bitmapData.Scan0;
fixed (byte* p = pixels)

    // Avoid having those 2 multiplications in the loop
    int wtimesh1 = height * width * 1;
    int wtimesh2 = height * width * 2;
    for (int j = 0; j < height; j++)
    
        // Avoid multiplication in the loop
        int jtimesw = j * width;
        for (int i = 0; i < width; i++)
        
            // unflattening 1D array using 3D coordinates
            //      index = z *   yMax *  xMax + y *  xMax + x
            int pixel = j * bitmapData.Stride + i * 3;
            // Avoid recalculation
            int jtimeswplusi = jtimesw + i;
            bits[pixel + 2] = p[jtimeswplusi];
            bits[pixel + 1] = p[wtimesh1 + jtimeswplusi];
            bits[pixel] = p[wtimesh2 + jtimeswplusi];
        
    


在我的电脑上,这个优化导致创建时间从 2.1s 下降到 0.05s

当然,这里需要注意的是代码是不安全的(正如编译器标志所暗示的那样),因此必须特别注意代码的正确性以避免崩溃、未定义的行为和/或安全问题。

【讨论】:

我会尽可能避免不安全的上下文,反正现在我有解决方案,它既快速又安全。 太棒了!但还要注意 Marshal.Copy (根据定义)也不安全,因为非托管内存指针不包含大小信息,因此无法验证。但当然,您的解决方案“更安全”,因为与我的解决方案中的多行代码相比,只需要检查一行代码是否存在错误。但是 Marshal.Copy 解决方案有点慢,因为您将字节写入数组(使用边界检查),然后将它们全部复制到 Scan0 ,在我的解决方案中,我直接写入 Scan0 而不进行边界检查。所以这是安全和性能之间的一个非常小的权衡。

以上是关于一种从输入数组创建位图的更快方法,而不是使用 for 循环和 SetPixel()的主要内容,如果未能解决你的问题,请参考以下文章

从向量而不是数组调用类函数

需要一种从现有数据框创建熊猫数据框的方法

需要一种从文件中获取信息并跳过信息是不是在另一个文件中的方法

一种从外部调用 React 组件方法的方法(使用它的状态和道具)

将大矢量图像转换为二进制矩阵的快速方法

比 O(n) 更快地获取数组元素的索引