Работа с растром на низком уровне для начинающих

от автора

Поводом для данной статьи стал следующий пост: «Конвертация bmp изображения в матрицу и обратно для дальнейшей обработки». В свое время, мне немало пришлось написать исследовательского кода на C#, который реализовывал различные алгоритмы сжатия, обработки. То, что код исследовательский, я упомянул не случайно. У этого кода своеобразные требования. С одной стороны, оптимизация не очень важна – ведь важно проверить идею. Хотя и хочется, чтобы эта проверка не растягивалась на часы и дни (когда идет запуск с различными параметрами, либо обрабатывается большой корпус тестовых изображений). Примененный в вышеупомянутом посте способ обращения к яркостям пикселов bmp.GetPixel(x, y) – это то, с чего начинался мой первый проект. Это самый медленный, хотя и простой способ. Стоит ли тут заморачиваться? Давайте, замерим.

Использовать будем классический Bitmap (System.Drawing.Bitmap). Данный класс удобен тем, что скрывает от нас детали кодирования растровых форматов – как правило, они нас и не интересуют. При этом поддерживаются все распространенные форматы, типа BMP, GIF, JPEG, PNG.

Кстати, предложу для начинающих первую пользу. У класса Bitmap есть конструктор, который позволяет открыть файл с картинкой. Но у него есть неприятная особенность – он оставляет открытым доступ к этому файлу, поэтому повторные обращения к нему приводят к эксепшену. Чтобы исправить это поведение, можно использовать такой метод, заставляющий битмап сразу «отпустить» файл:

public static Bitmap LoadBitmap(string fileName) {     using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))         return new Bitmap(fs); }

Методика замеров

Замерять будем, перегоняя в массив и обратно в Bitmap классику обработки изображений – Лену (http://en.wikipedia.org/wiki/Lenna). Это свободное изображение, его можно встретить в большом количестве работ по обработке изображений (и в заголовке данного поста тоже). Размер – 512*512 пикселов.

Немного о методике – в таких случаях я предпочитаю не гоняться за сверхточными таймерами, а просто много раз выполнять одно и то же действие. Конечно, с одной стороны, в этом случае данные и код уже будут в кэше процессора. Но, зато мы вычленяем затраты на первый запуск кода, связанный с переводом MSIL-кода в код процессора и другие накладные расходы. Чтобы гарантировать это, предварительно запускаем каждый кусок кода один раз – выполняем так называемый «прогрев».

Компилируем код в Release. Запускаем его обязательно не из-под студии. Более того, студию также желательно закрыть – сталкивался со случаями, когда сам факт её «запущенности» иногда сказывается на полученных результатах. Также, желательно закрыть и другие приложения.

Запускаем код несколько раз, добиваясь типичных результатов – необходимо убедиться, что на нем не сказывается какой-то неожиданный процесс. Скажем, проснулся антивирус или еще что-то. Все эти меры позволяют нам получить стабильные, повторяемые результаты.

«Наивный» метод

Именно этот метод был применен в оригинальной статье. Он состоит в том, что используется метод Bitmap.GetPixel(x, y). Приведем полностью код подобного метода, который конвертирует содержимое битмапа в трехмерный байтовый массив. При этом первая размерность – это цветовая компонента (от 0 до 2), вторая – позиция y, третья – позиция x. Так сложилось в моих проектах, если вам захочется расположить данные иначе – думаю, проблем не возникнет.

public static byte[, ,] BitmapToByteRgbNaive(Bitmap bmp) {     int width = bmp.Width,         height = bmp.Height;     byte[, ,] res = new byte[3, height, width];     for (int y = 0; y < height; ++y)     {         for (int x = 0; x < width; ++x)         {             Color color = bmp.GetPixel(x, y);             res[0, y, x] = color.R;             res[1, y, x] = color.G;             res[2, y, x] = color.B;         }     }     return res; }

Обратное преобразование аналогично, только перенос данных идет в другом направлении. Я не буду приводить его код здесь – желающие могут посмотреть в исходных кодах проекта по ссылке в конце статьи.

100 преобразований в изображение и обратно на моем ноутбуке с процессором I5-2520M 2.5GHz, требуют 43.90 сек. Получается, что при изображении 512*512, только на перенос данных, тратится порядка полусекунды!

Прямая работа с данными Bitmap

К счастью, класс Bitmap предоставляет более быстрый способ обратиться к своим данным. Для этого нам необходимо воспользоваться ссылками, предоставляемыми классом BitmapData и адресной арифметикой:

public unsafe static byte[, ,] BitmapToByteRgb(Bitmap bmp) {     int width = bmp.Width,         height = bmp.Height;     byte[, ,] res = new byte[3, height, width];     BitmapData bd = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly,         PixelFormat.Format24bppRgb);     try     {         byte* curpos;         for (int h = 0; h < height; h++)         {             curpos = ((byte*)bd.Scan0) + h * bd.Stride;             for (int w = 0; w < width; w++)             {                 res[2, h, w] = *(curpos++);                 res[1, h, w] = *(curpos++);                 res[0, h, w] = *(curpos++);             }         }     }     finally     {         bmp.UnlockBits(bd);     }     return res; }

Такой подход дает нам получить 0.533 секунды на 100 преобразований (ускорились в 82 раза)! Думаю, это уже отвечает на вопрос – а стоит ли писать более сложный код преобразования? Но можем ли мы еще ускорить процесс, оставаясь в рамках managed-кода?

Массивы vs указатели

Многомерные массивы являются не самыми быстрыми структурами данных. Здесь производятся проверки на выход за пределы индекса, сам элемент вычисляется, используя операции умножения и сложения. Поскольку адресная арифметика уже дала нам один раз существенное ускорение при работе с данными Bitmap, то может быть, попробуем её применить и для многомерных массивов? Вот код прямого преобразования:

public unsafe static byte[, ,] BitmapToByteRgbQ(Bitmap bmp) {     int width = bmp.Width,         height = bmp.Height;     byte[, ,] res = new byte[3, height, width];     BitmapData bd = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly,         PixelFormat.Format24bppRgb);     try     {         byte* curpos;         fixed (byte* _res = res)         {             byte* _r = _res, _g = _res + 1, _b = _res + 2;             for (int h = 0; h < height; h++)             {                 curpos = ((byte*)bd.Scan0) + h * bd.Stride;                 for (int w = 0; w < width; w++)                 {                     *_b = *(curpos++); _b += 3;                     *_g = *(curpos++); _g += 3;                     *_r = *(curpos++); _r += 3;                 }             }         }     }     finally     {         bmp.UnlockBits(bd);     }     return res; }

Результат? 0.162 сек на 100 преобразований. Так что ускорились еще в 3.3 раза (270 раз по сравнению с «наивной» версией). Именно подобный код и использовался мною при исследованиях алгоритмов.

Зачем вообще переносить?

Не совсем очевидно, а зачем вообще переносить данные из Bitmap. Может вообще, все преобразования осуществлять именно там? Соглашусь, что это один из возможных вариантов. Но, дело в том, что многие алгоритмы удобнее проверять на данных с плавающей запятой. Тогда нет проблем с переполнениями, потерей точности на промежуточных этапах. Преобразовать в double/float-массив можно аналогичным способом. Обратное преобразование требует проверки при конвертации в byte. Вот простой код такой проверки:

private static byte Limit(double x) {     if (x < 0)         return 0;     if (x > 255)         return 255;     return (byte)x; }

Добавление таких проверок и преобразование типов замедляет наш код. Версия с адресной арифметикой на double-массивах исполняется уже 0.713 сек (на 100 преобразований). Но на фоне «наивного» варианта – она просто молния.

А если нужно быстрее?

Если нужно быстрее, то пишем перенос, обработку на C, Asm, используем SIMD-команды. Загружаем растровый формат напрямую, без обертки Bitmap. Конечно, в этом случае мы выходим за пределы Managed-кода, со всеми вытекающими плюсами и минусами. И делать это имеет смысл для уже отлаженного алгоритма.

Полный код к статье можно найти здесь: rasterconversion.codeplex.com/SourceControl/latest

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


Комментарии

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

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