Так уж получилось, что мне надо было написать маленькую программку для получения 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