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/
Добавить комментарий