查找 BitmapImage 中特定像素颜色

44

我有一个 WPF 的 BitmapImage,它是从 .JPG 文件中加载的,代码如下:

this.m_image1.Source = new BitmapImage(new Uri(path));

我想查询特定点的颜色。例如,像素点(65,32)的RGB值是多少?

我该如何做?我采取了以下方法:

ImageSource ims = m_image1.Source;
BitmapImage bitmapImage = (BitmapImage)ims;
int height = bitmapImage.PixelHeight;
int width = bitmapImage.PixelWidth;
int nStride = (bitmapImage.PixelWidth * bitmapImage.Format.BitsPerPixel + 7) / 8;
byte[] pixelByteArray = new byte[bitmapImage.PixelHeight * nStride];
bitmapImage.CopyPixels(pixelByteArray, nStride, 0);

虽然我必须承认这段代码有点“学猴子模仿”,但是,有没有一种简单明了的方法来处理这个字节数组并转换为RGB值呢?


nStride 的作用是什么?为什么在 nStride 计算中要加 7 并除以 8? - Jviaches
3
将7加上并除以8,以便正确地舍入到足够的字节数(例如,10个比特将需要2个字节)。 - erikH
9个回答

60

以下是我如何使用多维数组在C#中操作像素:

[StructLayout(LayoutKind.Sequential)]
public struct PixelColor
{
  public byte Blue;
  public byte Green;
  public byte Red;
  public byte Alpha;
}

public PixelColor[,] GetPixels(BitmapSource source)
{
  if(source.Format!=PixelFormats.Bgra32)
    source = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);

  int width = source.PixelWidth;
  int height = source.PixelHeight;
  PixelColor[,] result = new PixelColor[width, height];

  source.CopyPixels(result, width * 4, 0);
  return result;
}

用法:

var pixels = GetPixels(image);
if(pixels[7, 3].Red > 4)
{
  ...
}

如果您想更新像素,非常相似的代码可以工作,但您需要创建一个WriteableBitmap并使用它:

public void PutPixels(WriteableBitmap bitmap, PixelColor[,] pixels, int x, int y)
{
  int width = pixels.GetLength(0);
  int height = pixels.GetLength(1);
  bitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, width*4, x, y);
}

因此:

var pixels = new PixelColor[4, 3];
pixels[2,2] = new PixelColor { Red=128, Blue=0, Green=255, Alpha=255 };

PutPixels(bitmap, pixels, 7, 7);

请注意,该代码将位图转换为Bgra32格式,如果它们以不同的格式到达。这通常很快,但在某些情况下可能会成为性能瓶颈,在这种情况下,应修改此技术以更精确地匹配底层输入格式。

更新

由于BitmapSource.CopyPixels不接受二维数组,因此需要在一维和二维数组之间进行转换。以下扩展方法应该可以解决问题:

public static class BitmapSourceHelper
{
#if UNSAFE
  public unsafe static void CopyPixels(this BitmapSource source, PixelColor[,] pixels, int stride, int offset)
  {
    fixed(PixelColor* buffer = &pixels[0, 0])
      source.CopyPixels(
        new Int32Rect(0, 0, source.PixelWidth, source.PixelHeight),
        (IntPtr)(buffer + offset),
        pixels.GetLength(0) * pixels.GetLength(1) * sizeof(PixelColor),
        stride);
  }
#else
  public static void CopyPixels(this BitmapSource source, PixelColor[,] pixels, int stride, int offset)
  {
    var height = source.PixelHeight;
    var width = source.PixelWidth;
    var pixelBytes = new byte[height * width * 4];
    source.CopyPixels(pixelBytes, stride, 0);
    int y0 = offset / width;
    int x0 = offset - width * y0;
    for(int y=0; y<height; y++)
      for(int x=0; x<width; x++)
        pixels[x+x0, y+y0] = new PixelColor
        {
          Blue  = pixelBytes[(y*width + x) * 4 + 0],
          Green = pixelBytes[(y*width + x) * 4 + 1],
          Red   = pixelBytes[(y*width + x) * 4 + 2],
          Alpha = pixelBytes[(y*width + x) * 4 + 3],
        };
  }
#endif
}

这里有两种实现方式: 第一种使用不安全的代码获取指向数组的IntPtr以提高速度(必须用/unsafe选项编译)。第二种较慢但不需要不安全的代码。我在我的代码中使用不安全版本。

WritePixels接受二维数组,因此不需要扩展方法。

编辑: 正如Jerry在评论中指出的那样,由于内存布局,二维数组的垂直坐标在前,也就是说它必须被定义为Pixels[Height,Width]而不是Pixels[Width,Height],并且要将其表示为Pixels[y,x]。


3
当我尝试这样做时,会出现“输入数组的等级无效”的错误提示。有什么想法吗? - whitehawk
9
致微软开发人员:我们不应该为了在WPF中读取位图文件中像素值而费这么多力气。 - dodgy_coder
我不明白PutPixels函数中的x和y是什么意思。我有一个PixelColor数组,想将其转换为BitmapImage。我该怎么做? - takayoshi
我知道这个问题已经很老了,但是我想指出当使用不安全的代码时,您需要将PixelColor声明为PixelColor[Height, Width],因为内存布局是[0,0] [0,1] [0,2] [1,0] [1,1]等。 - Jerry
谢谢Jerry。这对人们来说很有用。我把你的信息添加到了答案中。 - Ray Burns
显示剩余6条评论

17

我想补充Ray的答案,你还可以将PixelColor结构声明为联合体:

[StructLayout(LayoutKind.Explicit)]
public struct PixelColor
{
    // 32 bit BGRA 
    [FieldOffset(0)] public UInt32 ColorBGRA;
    // 8 bit components
    [FieldOffset(0)] public byte Blue;
    [FieldOffset(1)] public byte Green;
    [FieldOffset(2)] public byte Red;
    [FieldOffset(3)] public byte Alpha;
}

这样您还可以访问UInit32 BGRA(用于快速像素访问或复制),除了单个字节组件。


16

生成的字节数组的解释取决于源位图的像素格式,但在最简单的情况下,即32位ARGB图像中,每个像素将由字节数组中的四个字节组成。第一个像素将如下解释:

alpha = pixelByteArray[0];
red   = pixelByteArray[1];
green = pixelByteArray[2];
blue  = pixelByteArray[3];

要处理图像中的每个像素,您可能需要创建嵌套循环来遍历行和列,并通过每个像素中的字节数递增一个索引变量。

某些位图类型将多个像素合并为单个字节。例如,单色图像将八个像素打包到每个字节中。如果您需要处理除了24/32位每像素(简单的)之外的图像,则建议查找一本涵盖位图底层二进制结构的好书。


6
谢谢,但是......我感觉.NET框架应该有一些接口来抽象掉所有的编码复杂性。显然System.Windows.Media库知道如何解码位图,因为它将其呈现到屏幕上。为什么我需要知道所有的实现细节?我不能以某种方式利用已经实现的呈现逻辑吗? - Andrew Shepherd
2
我理解你的痛苦。在Windows Forms中,Bitmap类有GetPixel和SetPixel方法来检索和设置单个像素的值,但这些方法因极其缓慢而几乎无用,除非是非常简单的情况。您可以使用RenderTargetBitmap和其他更高级别的对象来创建和合成位图,但在我看来,图像处理最好留在C++领域,因为直接访问位更加适合。当然,您可以在C#/.NET中进行图像处理,但根据我的经验,这总感觉像是试图将方形钉子塞进圆孔。 - Michael A. McCloskey
11
我不同意。在我看来,对于90%的图像处理需求,C#比C++是更好的选择。C#语言具有许多优点,而其“unsafe”和“fixed”关键字在需要时可直接访问位,就像C++一样。在大多数情况下,使用C#实现图像处理算法的速度与C++非常相似,但偶尔会慢得多。只有在C#由于JIT编译器错过了优化而表现不佳的少数情况下,我才会使用C++。 - Ray Burns

6
我采用了所有示例,并创建了一个稍微更好的示例-我也进行了测试(唯一的缺陷是96作为DPI,这真的很烦人)。
我还将此WPF策略与以下内容进行了比较:
  • 通过使用Graphics (system.drawing)的GDI
  • 通过直接调用GDI32.Dll中的GetPixel进行Interop
令我惊讶的是,这比GDI快10倍左右,比Interop快约15倍。因此,如果您正在使用WPF,则最好使用此方法来获取像素颜色。
public static class GraphicsHelpers
{
    public static readonly float DpiX;
    public static readonly float DpiY;

    static GraphicsHelpers()
    {
        using (var g = Graphics.FromHwnd(IntPtr.Zero))
        {
            DpiX = g.DpiX;
            DpiY = g.DpiY;
        }
    }

    public static Color WpfGetPixel(double x, double y, FrameworkElement AssociatedObject)
    {
        var renderTargetBitmap = new RenderTargetBitmap(
            (int)AssociatedObject.ActualWidth,
            (int)AssociatedObject.ActualHeight,
            DpiX, DpiY, PixelFormats.Default);
        renderTargetBitmap.Render(AssociatedObject);

        if (x <= renderTargetBitmap.PixelWidth && y <= renderTargetBitmap.PixelHeight)
        {
            var croppedBitmap = new CroppedBitmap(
                renderTargetBitmap, new Int32Rect((int)x, (int)y, 1, 1));
            var pixels = new byte[4];
            croppedBitmap.CopyPixels(pixels, 4, 0);
            return Color.FromArgb(pixels[3], pixels[2], pixels[1], pixels[0]);
        }
        return Colors.Transparent;
    }
}

5
我想改进Ray的答案 - 没有足够的声望来评论。:( 这个版本既拥有安全/受管制的优点,又具有不安全版本的效率。此外,我取消了将步幅传递为参数的做法,因为.Net文档中CopyPixels的说明是位图的步幅,而不是缓冲区的步幅。这很容易引起误解,而且可以在函数内部计算。由于PixelColor数组必须与位图具有相同的步幅(以便能够作为单个复制调用进行操作),因此在函数中创建一个新数组也很合理。像吃蛋糕一样简单。
    public static PixelColor[,] CopyPixels(this BitmapSource source)
    {
        if (source.Format != PixelFormats.Bgra32)
            source = new FormatConvertedBitmap(source, PixelFormats.Bgra32, null, 0);
        PixelColor[,] pixels = new PixelColor[source.PixelWidth, source.PixelHeight];
        int stride = source.PixelWidth * ((source.Format.BitsPerPixel + 7) / 8);
        GCHandle pinnedPixels = GCHandle.Alloc(pixels, GCHandleType.Pinned);
        source.CopyPixels(
          new Int32Rect(0, 0, source.PixelWidth, source.PixelHeight),
          pinnedPixels.AddrOfPinnedObject(),
          pixels.GetLength(0) * pixels.GetLength(1) * 4,
              stride);
        pinnedPixels.Free();
        return pixels;
    }

注意:最佳管理并不意味着代码是安全的。这段代码将不能安全运行(或者至少我无法让它安全运行)。 - Peter
1
为了让这个方法起作用,数组必须是[高度,宽度]。 - 0xDEAD BEEF

3

如果你只想要一个像素的颜色:

using System.Windows.Media;
using System.Windows.Media.Imaging;
...
    public static Color GetPixelColor(BitmapSource source, int x, int y)
    {
        Color c = Colors.White;
        if (source != null)
        {
            try
            {
                CroppedBitmap cb = new CroppedBitmap(source, new Int32Rect(x, y, 1, 1));
                var pixels = new byte[4];
                cb.CopyPixels(pixels, 4, 0);
                c = Color.FromRgb(pixels[2], pixels[1], pixels[0]);
            }
            catch (Exception) { }
        }
        return c;
    }

谢谢,这对于在WPF中完成此操作非常有用:http://www.functionx.com/vcsharp/gdi/colorselector.htm - ToastyMallows

2

一点小备注:

如果您尝试使用此代码(由Ray Burns提供),但遇到有关数组秩的错误,请尝试按以下方式编辑扩展方法:

public static void CopyPixels(this BitmapSource source, PixelColor[,] pixels, int stride, int offset, bool dummy)

然后像这样调用 CopyPixels 方法:

source.CopyPixels(result, width * 4, 0, false);

问题在于,当扩展方法与原始方法没有区别时,会调用原始方法。我猜想这是因为PixelColor[,]也与Array匹配。希望这能帮助您解决同样的问题。

2
更简单了。无需复制数据,可以直接获取。但这是有代价的:指针和 unsafe。在特定情况下,需要权衡速度和易用性是否值得使用(但您可以将图像处理放入其自己的独立的不安全类中,而程序的其他部分则不受影响)。
var bitmap = new WriteableBitmap(image);
data = (Pixel*)bitmap.BackBuffer;
stride = bitmap.BackBufferStride / 4;
bitmap.Lock();

// getting a pixel value
Pixel pixel = (*(data + y * stride + x));

bitmap.Unlock();

where

[StructLayout(LayoutKind.Explicit)]
protected struct Pixel {
  [FieldOffset(0)]
  public byte B;
  [FieldOffset(1)]
  public byte G;
  [FieldOffset(2)]
  public byte R;
  [FieldOffset(3)]
  public byte A;
}

错误检查(是否确实为BGRA格式以及处理不是的情况)将留给读者。

0

您可以在字节数组中获取颜色分量。首先将32位像素复制到数组中,然后将其转换为大小4倍的8位数组。

int[] pixelArray = new int[stride * source.PixelHeight];

source.CopyPixels(pixelArray, stride, 0);

// byte[] colorArray = new byte[pixelArray.Length];
// EDIT:
byte[] colorArray = new byte[pixelArray.Length * 4];

for (int i = 0; i < colorArray.Length; i += 4)
{
    int pixel = pixelArray[i / 4];
    colorArray[i] = (byte)(pixel >> 24); // alpha
    colorArray[i + 1] = (byte)(pixel >> 16); // red
    colorArray[i + 2] = (byte)(pixel >> 8); // green
    colorArray[i + 3] = (byte)(pixel); // blue
}

// colorArray is an array of length 4 times more than the actual number of pixels
// in the order of [(ALPHA, RED, GREEN, BLUE), (ALPHA, RED...]

我认为正确的代码应该是:byte[] colorArray = new byte[pixelArray.Length * 4]; - Der_Meister

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接