2015-11-06 5 views
2

Прежде чем ничего, я отмечу, что принимаю решение на C# или VB.Net.Замените цвет изображения, используя Lockbits

У меня есть этот старый код, который я пытаюсь реорганизовать, чтобы избежать вредных привычек и производительности неэффективности использования GetPixel/SetPixel методы:

<Extension> 
Public Function ChangeColor(ByVal sender As Image, 
          ByVal oldColor As Color, 
          ByVal newColor As Color) As Image 

    Dim bmp As New Bitmap(sender.Width, sender.Height, sender.PixelFormat) 

    Dim x As Integer = 0 
    Dim y As Integer = 0 

    While (x < bmp.Width) 

     y = 0 
     While y < bmp.Height 
      If DirectCast(sender, Bitmap).GetPixel(x, y) = oldColor Then 
       bmp.SetPixel(x, y, newColor) 
      End If 
      Math.Max(Threading.Interlocked.Increment(y), y - 1) 
     End While 
     Math.Max(Threading.Interlocked.Increment(x), x - 1) 

    End While 

    Return bmp 

End Function 

Итак, после прочтения наибольшего количества голосов решения here с использованием LockBits подход, я пытаюсь адаптировать код к моим потребностям, использовать параметр Color в качестве параметра, а не последовательность байтов (потому что по существу то же самое):

<Extension> 
Public Function ChangeColor(ByVal sender As Image, 
          ByVal oldColor As Color, 
          ByVal newColor As Color) As Image 

    Dim bmp As Bitmap = DirectCast(sender.Clone, Bitmap) 

    ' Lock the bitmap's bits. 
    Dim rect As New Rectangle(0, 0, bmp.Width, bmp.Height) 
    Dim bmpData As BitmapData = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat) 

    ' Get the address of the first line. 
    Dim ptr As IntPtr = bmpData.Scan0 

    ' Declare an array to hold the bytes of the bitmap. 
    Dim numBytes As Integer = (bmpData.Stride * bmp.Height) 
    Dim rgbValues As Byte() = New Byte(numBytes - 1) {} 

    ' Copy the RGB values into the array. 
    Marshal.Copy(ptr, rgbValues, 0, numBytes) 

    ' Manipulate the bitmap. 
    For i As Integer = 0 To rgbValues.Length - 1 Step 3 

     If (Color.FromArgb(rgbValues(i), rgbValues(i + 1), rgbValues(i + 2)) = oldColor) Then 
      rgbValues(i) = newColor.R 
      rgbValues(i + 1) = newColor.G 
      rgbValues(i + 2) = newColor.B 
     End If 

    Next i 

    ' Copy the RGB values back to the bitmap. 
    Marshal.Copy(rgbValues, 0, ptr, numBytes) 

    ' Unlock the bits. 
    bmp.UnlockBits(bmpData) 

    Return bmp 

End Function 

У меня есть две проблемы с методом расширения, первое, что если pixelformat не является Format24bppRgb в качестве исходного примера, тогда все идет не так, исключение IndexOutOfRange выбрано в цикле, я полагаю, это связано с тем, что я читаю 3 байта (RGB) вместо 4 (ARGB), но я не уверен, как его адаптировать для любого исходного пиксельного формата, который я могу передать функции.

Второе, что если я использую Format24bppRgb в качестве исходного примера C#, цвет меняется на черный.

Обратите внимание, что я не уверен, что исходное решение, заданное в вопросе C#, которое я связал, неверно, потому что, по мнению их комментариев, в некотором роде это неправильно.

Это способ, которым я пытаюсь использовать:

' This function creates a bitmap of a solid color. 
    Dim srcImg As Bitmap = ImageUtil.CreateSolidcolorBitmap(New Size(256, 256), Color.Red) 
    Dim modImg As Image = srcImg.ChangeColor(Color.Red, Color.Blue) 

    PictureBox1.BackgroundImage = srcImg 
    PictureBox2.BackgroundImage = modImg 

ответ

2

Я полагаю, это потому, что я читаю 3 байта (RGB) вместо 4 (ARGB)

Да, в этом дело. Если вы хотите манипулировать сырым изображением, вы должны полагаться на PixelFormat. И вы должны различать индексированные форматы (8bpp или меньше), где пиксели в BitmapData - это не цвета, а индексы цветовой палитры.

public void ChangeColor(Bitmap bitmap, Color from, Color to) 
{ 
    if (Image.GetPixelFormatSize(bitmap.PixelFormat) > 8) 
    { 
     ChangeColorHiColoredBitmap(bitmap, from, to); 
     return; 
    } 

    int indexFrom = Array.IndexOf(bitmap.Palette.Entries, from); 
    if (indexFrom < 0) 
     return; // nothing to change 

    // we could replace the color in the palette but we want to see an example for manipulating the pixels 
    int indexTo = Array.IndexOf(bitmap.Palette.Entries, to); 
    if (indexTo < 0) 
     return; // destination color not found - you can search for the nearest color if you want 

    ChangeColorIndexedBitmap(bitmap, indexFrom, indexTo); 
} 

private unsafe void ChangeColorHiColoredBitmap(Bitmap bitmap, Color from, Color to) 
{ 
    int rawFrom = from.ToArgb(); 
    int rawTo = to.ToArgb(); 

    BitmapData data = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), ImageLockMode.ReadWrite, bitmap.PixelFormat); 
    byte* line = (byte*)data.Scan0; 
    for (int y = 0; y < data.Height; y++) 
    { 
     for (int x = 0; x < data.Width; x++) 
     { 
      switch (data.PixelFormat) 
      { 
       case PixelFormat.Format24bppRgb: 
        byte* pos = line + x * 3; 
        int c24 = Color.FromArgb(pos[0], pos[1], pos[2]).ToArgb(); 
        if (c24 == rawFrom) 
        { 
         pos[0] = (byte)(rawTo & 0xFF); 
         pos[1] = (byte)((rawTo >> 8) & 0xFF); 
         pos[2] = (byte)((rawTo >> 16) & 0xFF); 
        } 
        break; 
       case PixelFormat.Format32bppRgb: 
       case PixelFormat.Format32bppArgb: 
        int c32 = *((int*)line + x); 
        if (c32 == rawFrom) 
         *((int*)line + x) = rawTo; 
        break; 
       default: 
        throw new NotSupportedException(); // of course, you can do the same for other pixelformats, too 
      } 
     } 

     line += data.Stride; 
    } 

    bitmap.UnlockBits(data); 
} 

private unsafe void ChangeColorIndexedBitmap(Bitmap bitmap, int from, int to) 
{ 
    int bpp = Image.GetPixelFormatSize(bitmap.PixelFormat); 
    if (from < 0 || to < 0 || from >= (1 << bpp) || to >= (1 << bpp)) 
     throw new ArgumentOutOfRangeException(); 

    if (from == to) 
     return; 

    BitmapData data = bitmap.LockBits(
     new Rectangle(Point.Empty, bitmap.Size), 
     ImageLockMode.ReadWrite, 
     bitmap.PixelFormat); 

    byte* line = (byte*)data.Scan0; 

    // scanning through the lines 
    for (int y = 0; y < data.Height; y++) 
    { 
     // scanning through the pixels within the line 
     for (int x = 0; x < data.Width; x++) 
     { 
      switch (bpp) 
      { 
       case 8: 
        if (line[x] == from) 
         line[x] = (byte)to; 
        break; 
       case 4: 
        // First pixel is the high nibble. From and To indices are 0..16 
        byte nibbles = line[x/2]; 
        if ((x & 1) == 0 ? nibbles >> 4 == from : (nibbles & 0x0F) == from) 
        { 
         if ((x & 1) == 0) 
         { 
          nibbles &= 0x0F; 
          nibbles |= (byte)(to << 4); 
         } 
         else 
         { 
          nibbles &= 0xF0; 
          nibbles |= (byte)to; 
         } 

         line[x/2] = nibbles; 
        } 
        break; 
       case 1: 
        // First pixel is MSB. From and To are 0 or 1. 
        int pos = x/8; 
        byte mask = (byte)(128 >> (x & 7)); 
        if (to == 0) 
         line[pos] &= (byte)~mask; 
        else 
         line[pos] |= mask; 
        break; 
      } 
     } 

     line += data.Stride; 
    } 

    bitmap.UnlockBits(data); 
} 
+0

Итак, я добавил примеры для форматов пикселей 1, 4, 8, 24 и 32 бит. Конечно, вы можете рассчитать цвета для 15/16/64 и других форматов. Я использовал здесь небезопасные методы, но вы можете использовать 'Marshal.Copy' для копирования данных там и обратно с помощью управляемого массива. – taffer

1

Есть три различных проблем в коде вы публикуемую:

  1. У вас есть порядок компонентов цвета неправильно. Класс Bitmap сохраняет значения пикселей в виде целых чисел в формате little-endian. Это означает, что порядок байтов компонентов - это BGR (или BGRA для 32bpp).
  2. В VB.NET вы не можете напрямую сравнить значения Color. Я не знаю достаточно о VB.NET, чтобы знать, почему это так, но я предполагаю, что это нормальное поведение на языке, связанное с тем, как VB.NET рассматривает типы значений. Чтобы правильно сравнить значения Color, вам необходимо позвонить ToArgb(), который возвращает значение Integer, которое можно сравнить напрямую.
  3. Ваш цикл For использует неправильное конечное значение. Если вы только вычитаете 1 из длины массива, тогда цикл может быть запущен в дополнение к концу строки, но найти слишком мало байтов, чтобы успешно добавить 2 в индекс цикла и все еще оставаться в массиве ,

Вот вариант вашего метода расширения, который отлично работает для меня:

<Extension> 
Public Function ChangeColor(ByVal image As Image, ByVal oldColor As Color, ByVal newColor As Color) 
    Dim newImage As Bitmap = New Bitmap(image.Width, image.Height, image.PixelFormat) 

    Using g As Graphics = Graphics.FromImage(newImage) 
     g.DrawImage(image, Point.Empty) 
    End Using 

    ' Lock the bitmap's bits. 
    Dim rect As New Rectangle(0, 0, newImage.Width, newImage.Height) 
    Dim bmpData As BitmapData = newImage.LockBits(rect, ImageLockMode.ReadWrite, newImage.PixelFormat) 

    ' Get the address of the first line. 
    Dim ptr As IntPtr = bmpData.Scan0 

    ' Declare an array to hold the bytes of the bitmap. 
    Dim numBytes As Integer = (bmpData.Stride * newImage.Height) 
    Dim rgbValues As Byte() = New Byte(numBytes - 1) {} 

    ' Copy the RGB values into the array. 
    Marshal.Copy(ptr, rgbValues, 0, numBytes) 

    ' Manipulate the bitmap. 
    For i As Integer = 0 To rgbValues.Length - 3 Step 3 

     Dim testColor As Color = Color.FromArgb(rgbValues(i + 2), rgbValues(i + 1), rgbValues(i)) 

     If (testColor.ToArgb() = oldColor.ToArgb()) Then 
      rgbValues(i) = newColor.B 
      rgbValues(i + 1) = newColor.G 
      rgbValues(i + 2) = newColor.R 
     End If 

    Next i 

    ' Copy the RGB values back to the bitmap. 
    Marshal.Copy(rgbValues, 0, ptr, numBytes) 

    ' Unlock the bits. 
    newImage.UnlockBits(bmpData) 

    Return newImage 

End Function 

Насколько это идет:

Я не знаю, как адаптировать его для любого источника pixelformat, что я могу перейти к функции.

К сожалению, API напрямую не возвращает бит-за-пиксель или байты на пиксель для растрового изображения. Вы можете обобщить свой код, чтобы учесть количество байтов на пиксель, но вам все равно придется по крайней мере сопоставить значение PixelFormat с этими байтами на пиксель.

+1

Спасибо за ответ! В этом случае я буду оценивать некоторые пиксельные формы, а затем выбросить исключение NotImplemented для неизвестного пиксельного формата. Не могли бы вы упомянуть или указать URL-адрес (из wikipedia? MSDN или другого источника), где узнать каждый байт на пиксель общих пиксельных форматов? к моменту, когда у меня есть: «PixelFormat.Format24bppRgb = 3, PixelFormat.Format32bppArgb = 4, PixelFormat.Format32bppRgb = 4' – ElektroStudios

+0

Кажется, что в WPF мы можем получить бит за пиксель: https://msdn.microsoft.com/ ан-нас/библиотека/system.windows.media.pixelformat.bitsperpixel% 2 = vs.110% 29.aspx – ElektroStudios