在 C# 中的图像处理中使用 Marshal.Copy 和 LockBits

using Marshal.Copy and LockBits in image processing in C#

我正在尝试写一个灰度 code.I 基本上写了但是我不明白我怎么能在这段代码中使用 Marshal 或 lockbits 来提高速度。(我选择这种方法是因为使用指针更复杂对我来说)我写了一些东西,但我知道有太多或遗漏了 codes.I 知道我应该使用 lockbits 而不是 getpixel 来提高速度,但我 confused.It 对我来说很复杂,可能我正在使用它们在错误的地方。

编辑:我正在尝试将照片变成灰色 color.It 正在编译,但它没有变灰,我不明白代码有什么问题。

编辑:我知道下面有答案,但我想学习用 Lockbites 和 Marshal.Copy 函数编写这个。

代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;

namespace dnm2709normalimg
{
    class GrayFilter
    {
        
        public static Bitmap DoGray(Bitmap bmp)
        {
            for (int i = 0; i < bmp.Height; i++)
            {
                for (int j = 0; j < bmp.Width; j++)
                {
                    int value = (bmp.GetPixel(j, i).R + bmp.GetPixel(j, i).G + bmp.GetPixel(j, i).B) / 3;
                    Color clr;
                    clr = Color.FromArgb(value, value, value);
                    BitmapData bmpData =bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height),ImageLockMode.ReadOnly, bmp.PixelFormat);
                    // Get the address of the first line.
                    IntPtr ptr = bmpData.Scan0;

                    // Declare an array to hold the bytes of the bitmap. 
                    int bytes = Math.Abs(bmpData.Stride) * bmp.Height;
                    byte[] rgbValues = new byte[bytes];

                    // Copy the RGB values into the array.
                    Marshal.Copy(ptr, rgbValues, 0, bytes);

                    // Set every third value to 255. A 24bpp bitmap will look red.   
                    for (int counter = 2; counter < rgbValues.Length; counter += 3)
                        rgbValues[counter] = 255;

                    // Copy the RGB values back to the bitmap
                    Marshal.Copy(rgbValues, 0, ptr, bytes);
                    bmp.UnlockBits(bmpData);
                }
            }
            return bmp;
        }
    }
}

首先,您遍历每个像素,每次都处理整个图像。所以去掉外层循环。

// Set every third value to 255. A 24bpp bitmap will look red.   
for (int counter = 2; counter < rgbValues.Length; counter += 3)

不正确。它假定每一行都包含精确数量的像素,没有剩余的字节。由于对齐,图像的每一行可能包含一些额外的字节,这就是为什么我们需要一个“步幅”而不仅仅是每个像素的宽度 + 字节数。

另外,在你的例子中,没有计算灰度值。这应该是每个颜色通道的某种平均,通常使用一些加权来更加强调绿色通道。

因此转换为灰度的代码应如下所示:

for(var y = 0; y < bmp.Height; y++){
   var row = y * bmpData.Stride;
   for(var x = 0; x < bmp.Width; x++){
        var i = row + x * bytesPerPixel;
        var grayscaleValue = rgbValues[i] * 0.11 + rgbValues[i + 1]*0.59 + rgbValues[i + 2] * 0.3;
        rgbValues[i] = grayscaleValue ;
        rgbValues[i + 1] = grayscaleValue ;
        rgbValues[i + 2] = grayscaleValue ;
    }
}

另请注意,这并不真正关心 rgbValues 是指针还是复制的数组。困难的部分不是使用 pointer/unsafe 代码,而是让索引正确。

如果您像@JonasH 在他的回答中那样访问原始数据,那么您必须知道输入 Bitmap 的实际 PixelFormat 是什么。他的回答适用于 24bpp RGB 图像。

适用于所有位图的更通用的答案:

private const float RLum = 0.299f;
private const float GLum = 0.587f;
private const float BLum = 0.114f;


public static Bitmap DoGray(Bitmap bmp)
{
    using var result = new Bitmap(bmp.Width, bmp.Height);
    using (Graphics g = Graphics.FromImage(result))
    {
        // Grayscale color matrix
        var colorMatrix = new ColorMatrix(new float[][]
        {
            new float[] { RLum, RLum, RLum, 0, 0 },
            new float[] { GLum, GLum, GLum, 0, 0 },
            new float[] { BLum, BLum, BLum, 0, 0 },
            new float[] { 0, 0, 0, 1, 0 },
            new float[] { 0, 0, 0, 0, 1 }
        });

        using (var attrs = new ImageAttributes())
        {
            attrs.SetColorMatrix(colorMatrix);
            g.DrawImage(bmp, new Rectangle(0, 0, result.Width, result.Height), 0, 0, result.Width, result.Height, GraphicsUnit.Pixel, attrs);
        }
    }
    return result;
}

但是如果您不介意使用隐藏了解释实际像素格式的复杂性的 library (shameless self promotion alert) it offers you fast bitmap data manipulation,那么您可以像这样使用它:

public static Bitmap DoGray(Bitmap bmp)
{
    using var result = new Bitmap(bmp.Width, bmp.Height);
    using IReadableBitmapData src = bmp.GetReadableBitmapData();
    using IWritableBitmapData dst = result.GetWritableBitmapData();
    IReadableBitmapDataRow rowSrc = src.FirstRow;
    IWritableBitmapDataRow rowDst = dst.FirstRow;
    do
    {
        for (int x = 0; x < src.Width; x++)
            rowDst[x] = rowSrc[x].ToGray();
    } while (rowSrc.MoveNextRow() && rowDst.MoveNextRow());
    return result;
}

但其实图书馆有一个ToGrayscale extension method that is even much faster because it uses parallel processing (the test cases are taken from here):

==[Performance Test Results]================================================
Test Time: 2,000 ms
Warming up: Yes
Test cases: 3
Calling GC.Collect: Yes
Forced CPU Affinity: No
Cases are sorted by fulfilled iterations (the most first)
--------------------------------------------------
1. ImageExtensions.ToGrayscale: 4,549 iterations in 2,000.20 ms. Adjusted for 2,000 ms: 4,548.55
2. Sequential processing: 1,651 iterations in 2,001.54 ms. Adjusted for 2,000 ms: 1,649.73 (-2,898.82 / 36.27 %)
3. Graphics.DrawImage(..., ImageAttributes): 964 iterations in 2,000.17 ms. Adjusted for 2,000 ms: 963.92 (-3,584.63 / 21.19 %)

更新: 以上所有三个选项 return 一个新的 Bitmap 32bpp ARGB 像素格式。要就地制作位图灰度,您可以使用上述库中的 MakeGrayscale 扩展方法。

第二个例子可以很容易地重写以修改原来的Bitmap,只需通过GetReadWriteBitmapData扩展方法获得一个IReadWriteBitmapData。 link 实际上包含一个关于如何就地制作 Bitmap 灰度的示例。