Замена медленного Bitmap.GetPixel при получении HSB-характеристик изображения

в 20:11, , рубрики: .net, HSB, метки: ,

Так уж получилось, что мне надо было написать маленькую программку для получения HSB-характеристик изображения. Самое тривиальное решение пришло в голову сразу:

public struct HSB
{
      public float H, S, B;
}

public static HSB GetHSB(Bitmap img)
{
      HSB imgHSB = new HSB();
      int width = img.Width, height = img.Height;
      int pixelsCount = height * width;

      for (int i = 0; i < pixelsCount; i++)
      {
            int y = i / width, x = i % height;
            imgHSB.H += img.GetPixel(x, y).GetHue();
            imgHSB.S += img.GetPixel(x, y).GetSaturation();
            imgHSB.B += img.GetPixel(x, y).GetBrightness();
      }

      imgHSB.H /=  pixelsCount;
      imgHSB.S /= pixelsCount;
      imgHSB.B /=  pixelsCount;
      return imgHSB;
}

Но оно не удовлетворило меня своей медлительностью: для изображения с размерами 2100х1500 пикселей метод выполнялся долгих 14209мс. Оказалось, что во всем виноват метод Bitmap.GetPixel.
Следовало искать другие, более быстрые способы.

Первое, что пришло на ум — распараллелить цикл суммирования как-то так:

Parallel.For(0, pixelsCount, i =>
{
      int y = i / width, x = i % height;
      imgHSB.B += img.GetPixel(x, y).GetBrightness();
      imgHSB.S += img.GetPixel(x, y).GetSaturation();
      imgHSB.H += img.GetPixel(x, y).GetHue();
});

Но компилятор был против, ибо нельзя использовать System.Drawing.Image из нескольких потоков одновременно, он доступен только в том потоке, которые его создал.
Пришлось искать новое решение. Я порылся в справке и на глаза попались методы Bitmap.LockBits и Bitmap.UnlockBits, с помощью которых можно было преобразовать Bitmap в byte[]:

public static byte[] ConvertBitmapToArray(Bitmap img)
{
      Rectangle rect = new Rectangle(0, 0, img.Width, img.Height);
      System.Drawing.Imaging.BitmapData tempData =
            img.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite,
            img.PixelFormat);
      IntPtr ptr = tempData.Scan0;
      int bytes = img.Width * img.Height * 3;
      byte[] rgbValues = new byte[bytes];
      System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes);
      img.UnlockBits(tempData);
      return rgbValues;
}

Осталось лишь преобразовать RGB в HSB. Но это уже не так сложно:

public static HSB GetHSB(Bitmap img)
{
      byte[] inData = ConvertBitmapToArray(img);
      HSB imgHSB = new HSB();
      int pixelsCount = inData.Count();
      float hue = 0, saturation = 0, brightness = 0, tempHue = 0, tempSaturation = 0, tempBrightness = 0;

      for (int i = 0; i < pixelsCount; i += 3)
      {
            float MinRGB, MaxRGB, Delta;
            float R = inData[i];
            float G = inData[i + 1];
            float B = inData[i + 2];
            hue = 0;
            MinRGB = Math.Min(Math.Min(R, G), B);
            MaxRGB = Math.Max(Math.Max(R, G), B);
            Delta = MaxRGB - MinRGB;
            brightness = MaxRGB;

            if (MaxRGB != 0.0)
            {
                  saturation = 255 * Delta / MaxRGB;
            }

            else
            {
                  saturation = 0;
            }

            if (saturation != 0.0)
            {
                  if (R == MaxRGB)
                  {
                        hue = (G - B) / Delta;
                  }

                  else if (G == MaxRGB)
                  {
                        hue = 2 + (B - R) / Delta;
                  }

                  else if (B == MaxRGB)
                  {
                        hue = 4 + (R - G) / Delta;
                  }
            }

            else
            {
                  hue = -1;
                  hue = hue * 60;
            }

            if (hue < 0)
            {
                  hue = hue + 360;
            }

            tempHue += hue;
            tempSaturation += saturation * 100 / 255;
            tempBrightness += brightness * 100 / 255;
      }

      imgHSB.H = tempHue / pixelsCount;
      imgHSB.S = tempSaturation / pixelsCount;
      imgHSB.B = tempBrightness / pixelsCount;
      return imgHSB;
}

Вот и все. Метод выполняется на том же изображении всего 289мс.

Хотелось еще увеличить скорость, распараллелив цикл из вышеприведенного метода с помощью того же Parallel.For, но метод стал выполняться медленнее (311мс), да и полученные значения HSB для одного изображения все время были разные.

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

Автор: 3StYleR

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js