...

пятница, 21 февраля 2014 г.

[Из песочницы] Замена медленного 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 для одного изображения все время были разные.


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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий