Использовать будем классический 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/
Добавить комментарий