Подтвердить что ты не робот

Поиск конкретных цветов пикселей BitmapImage

У меня есть BitmapImage WPF, который я загрузил из файла .JPG следующим образом:

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

Я хочу задать вопрос о том, какой цвет находится в определенных точках. Например, каково значение RGB в пикселе (65,32)?

Как мне это сделать? Я придерживался такого подхода:

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?

4b9b3361

Ответ 1

Интерпретация результирующего массива байтов зависит от формата пикселя исходного растрового изображения, но в простейшем случае 32-битного изображения ARGB каждый пиксель будет состоять из четырех байтов в массиве байтов. Первый пиксель будет интерпретироваться так:

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

Чтобы обрабатывать каждый пиксель в изображении, вы, вероятно, захотите создать вложенные циклы, чтобы ходить по строкам и столбцам, увеличивая индексную переменную на количество байтов в каждом пикселе.

Некоторые типы растровых изображений объединяют несколько пикселей в один байт. Например, монохромное изображение объединяет восемь пикселей в каждый байт. Если вам нужно иметь дело с изображениями, отличными от 24/32 бит на пиксель (простые), тогда я бы предложил найти хорошую книгу, которая покрывает базовую двоичную структуру растровых изображений.

Ответ 2

Вот как я мог бы манипулировать пикселями в С# с помощью многомерных массивов:

[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)
  ...

Если вы хотите обновлять пиксели, работает очень похожий код, за исключением того, что вы создадите WritableBitmap и используйте это:

public void PutPixels(WritableBitmap 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];
pixel[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 принимает двумерные массивы, поэтому не требуется метод расширения.

Ответ 3

Я хотел бы добавить к ответу 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 (для быстрого доступа к пикселям или копии), помимо отдельных компонентов байта.

Ответ 4

Я бы хотел улучшить ответ Рэя - недостаточно комментариев для комментариев. > :( Эта версия имеет лучшее из безопасного и управляемого, а также эффективность небезопасной версии. Кроме того, я покончил с тем, чтобы перейти к шагу, поскольку документация .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;
    }

Ответ 5

Я взял все примеры и создал немного лучше - протестировал его тоже
(единственным недостатком было то, что магия 96 была DPI, которая действительно меня била).

Я также сравнивал эту тактику WPF против:

  • GDI с помощью Graphics (system.drawing)
  • Взаимодействие путем прямого вызова GetPixel из GDI32.Dll

К моему supprise,
Это работает на x10 быстрее, чем GDI, и примерно в 15 раз быстрее, чем в Interop.
Поэтому, если вы используете 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;
    }
}

Ответ 6

Если вам нужен только один цвет пикселя:

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;
    }

Ответ 7

Небольшое замечание:

Если вы пытаетесь использовать этот код (Edit: предоставлено 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.

Надеюсь, это поможет вам, если у вас возникнут те же проблемы.

Ответ 8

Вы можете получить компоненты цвета в байтовом массиве. Сначала скопируйте пиксели в 32 бит в массив и преобразуйте их в 8-разрядный массив с размером в 4 раза больше

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...]