Замена медленного Bitmap.GetPixel при получении 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, totalPixels, 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 для одного изображения все время были разные.

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

ссылка на оригинал статьи http://habrahabr.ru/post/213203/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *