Манипулируем System.Drawing.Bitmap
Класс System.Drawing.Bitmap очень полезен в инфраструктуре .NET, т.к. позволяет считывать и сохранять файлы различных графических форматов. Единственная проблема – это то, что он не очень полезен для попиксельной обработки – например если нужно перевести битмап в ч/б. Под катом – небольшой этюд на эту тему.Допустим у нас есть два битмапа, один из которых считан из файла, а другой должен содержать ч/б конверсию:
// загружаем картинку
sourceBitmap = (Bitmap) Image.FromFile("Zap.png");
// делаем пустую картинку того же размера
targetBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height, sourceBitmap.PixelFormat);
Мы хотим чтобы targetBitmap был как sourceBitmap, только черно-белый. На самом деле, в C# делается это просто:void NaïveBlackAndWhite()
{
for (int y = 0; y < sourceBitmap.Height; ++y)
for (int x = 0; x < sourceBitmap.Width; ++x)
{
Color c = sourceBitmap.GetPixel(x, y);
byte rgb = (byte)(0.3 * c.R + 0.59 * c.G + 0.11 * c.B);
targetBitmap.SetPixel(x, y, Color.FromArgb(c.A, rgb, rgb, rgb));
}
}
Это решение понятное и простое, но к сожалению жуть как неэффективное. Чтобы получить более «резвый» код, можно попробовать написать все это дело на С++. Для начала создадим структурку для хранения цветовых значений пикселя// структура отражает один пиксель в 32bpp RGBA
struct Pixel {
BYTE Blue;
BYTE Green;
BYTE Red;
BYTE Alpha;
};
Теперь можно написать функцию которая будет делать пиксель черно-белым:Pixel MakeGrayscale(Pixel& pixel)
{
const BYTE scale = static_cast<BYTE>(0.3 * pixel.Red + 0.59 * pixel.Green + 0.11 * pixel.Blue);
Pixel p;
p.Red = p.Green = p.Blue = scale;
p.Alpha = pixel.Alpha;
return p;
}
Теперь собственно пишем саму функцию обхода:CPPSIMDLIBRARY_API void AlterBitmap(BYTE* src, BYTE* dst, int width, int height, int stride)
{
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x)
{
int offset = x * sizeof(Pixel) + y * stride;
Pixel& s = *reinterpret_cast<Pixel*>(src + offset);
Pixel& d = *reinterpret_cast<Pixel*>(dst + offset);
// изменяем d
d = MakeGrayscale(s);
}
}
}
А дальше остается только использовать ее из C#.void UnmanagedBlackAndWhite()
{
// "зажимаем" байты обеих картинок
Rectangle rect = new Rectangle(0, 0, sourceBitmap.Width, sourceBitmap.Height);
BitmapData srcData = sourceBitmap.LockBits(rect, ImageLockMode.ReadWrite, sourceBitmap.PixelFormat);
BitmapData dstData = targetBitmap.LockBits(rect, ImageLockMode.ReadWrite, sourceBitmap.PixelFormat);
// отсылаем в unmanaged код для изменений
AlterBitmap(srcData.Scan0, dstData.Scan0, srcData.Width, srcData.Height, srcData.Stride);
// отпускаем картинки
sourceBitmap.UnlockBits(srcData);
targetBitmap.UnlockBits(dstData);
}
Это улучшило быстродействие, но мне захотелось еще большего. Я добавил директиву OpenMP перед циклом по y и получил предсказуемое ускорение в 2 раза. Дальше захотелось поэкспериментировать и попробовать применить еще и SIMD. Для этого я написал вот этот, не очень читабельный, код:CPPSIMDLIBRARY_API void AlterBitmap(BYTE* src, BYTE* dst, int width, int height, int stride)
{
// факторы для конверсии в ч/б
static __m128 factor = _mm_set_ps(1.0f, 0.3f, 0.59f, 0.11f);
#pragma omp parallel for
for (int y = 0; y < height; ++y)
{
const int offset = y * stride;
__m128i* s = (__m128i*)(src + offset);
__m128i* d = (__m128i*)(dst + offset);
for (int x = 0; x < (width >> 2); ++x) {
// у нас 4 пикселя за раз
for (int p = 0; p < 4; ++p)
{
// конвертируем пиксель
__m128 pixel;
pixel.m128_f32[0] = s->m128i_u8[(p<<2)];
pixel.m128_f32[1] = s->m128i_u8[(p<<2)+1];
pixel.m128_f32[2] = s->m128i_u8[(p<<2)+2];
pixel.m128_f32[3] = s->m128i_u8[(p<<2)+3];
// четыре операции умножения - одной командой!
pixel = _mm_mul_ps(pixel, factor);
// считаем сумму
const BYTE sum = (BYTE)(pixel.m128_f32[0] + pixel.m128_f32[1] + pixel.m128_f32[2]);
// пишем назад в битмап
d->m128i_u8[p<<2] = d->m128i_u8[(p<<2)+1] = d->m128i_u8[(p<<2)+2] = sum;
d->m128i_u8[(p<<2)+3] = (BYTE)pixel.m128_f32[3];
}
s++;
d++;
}
}
}
Несмотря на то, что этот код делает 4 операции умножения за раз (инструкция _mm_mul_ps), все эти конверсии не дали никакого выигрыша по сравнению с обычными операциями – скорее наоборот, алгоритм начал работать медленнее. Вот результаты выполнения функций на картинке 360×480. Использовался 2х-ядерный MacBook с 4Гб RAM, результаты усредненные.
А вот и конечный результат:

Выводы:
SetPixel/GetPixel– это зло, их трогать не стоит.
- OpenMP продолжает радовать, давая линейный scalability.
- Использование SIMD не гарантирует повышение производительности. Зато доставляет много хлопот.
Если кто-нибудь из читателей готов написать еще более эффективный алгоритм – милости просим! Протестирую и опубликую его прямо здесь.



комментарии (54)