- Манипулируем System.Drawing.Bitmap
- Работа с растром на низком уровне для начинающих
- Методика замеров
- «Наивный» метод
- Прямая работа с данными Bitmap
- Массивы vs указатели
- Зачем вообще переносить?
- А если нужно быстрее?
- Bitmap Конструкторы
- Определение
- Перегрузки
- Bitmap(Image)
- Параметры
- Применяется к
- Bitmap(Stream)
- Параметры
- Исключения
- Примеры
- Комментарии
- См. также раздел
- Применяется к
- Bitmap(String)
- Параметры
- Исключения
- Комментарии
- См. также раздел
- Применяется к
- Bitmap(Image, Size)
- Параметры
- Исключения
- Применяется к
- Bitmap(Int32, Int32)
- Параметры
- Исключения
- Комментарии
- Применяется к
- Bitmap(Stream, Boolean)
- Параметры
- Исключения
- Комментарии
- См. также раздел
- Применяется к
- Bitmap(String, Boolean)
- Параметры
- Примеры
- Комментарии
Манипулируем System.Drawing.Bitmap
// загружаем картинку
sourceBitmap = (Bitmap) Image.FromFile( «Zap.png» );
// делаем пустую картинку того же размера
targetBitmap = new Bitmap(sourceBitmap.Width, sourceBitmap.Height, sourceBitmap.PixelFormat);
Мы хотим чтобы targetBitmap был как sourceBitmap , только черно-белый. На самом деле, в C# делается это просто:
for ( int y = 0; y for ( int x = 0; x 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 <
Теперь можно написать функцию которая будет делать пиксель черно-белым:
const BYTE scale = static_cast BYTE >(0.3 * pixel.Red + 0.59 * pixel.Green + 0.11 * pixel.Blue);
p.Red = p.Green = p.Blue = scale;
Теперь собственно пишем саму функцию обхода:
CPPSIMDLIBRARY_API void AlterBitmap( BYTE * src, BYTE * dst, int width, int height, int stride)
for ( int y = 0; y for ( int x = 0; x int offset = x * sizeof (Pixel) + y * stride;
Pixel& s = * reinterpret_cast
Pixel& d = * reinterpret_cast
// изменяем d
d = MakeGrayscale(s);
А дальше остается только использовать ее из C#.
// «зажимаем» байты обеих картинок
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);
Это улучшило быстродействие, но мне захотелось еще большего. Я добавил директиву 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 const int offset = y * stride;
__m128i * s = ( __m128i *)(src + offset);
__m128i * d = ( __m128i *)(dst + offset);
for ( int x = 0; x > 2); ++x) <
// у нас 4 пикселя за раз
for ( int p = 0; p // конвертируем пиксель
__m128 pixel;
pixel.m128_f32[0] = s->m128i_u8[(p m128i_u8[(p m128i_u8[(p m128i_u8[(p // четыре операции умножения — одной командой!
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 m128i_u8[(p m128i_u8[(p m128i_u8[(p BYTE )pixel.m128_f32[3];
Несмотря на то, что этот код делает 4 операции умножения за раз (инструкция _mm_mul_ps ), все эти конверсии не дали никакого выигрыша по сравнению с обычными операциями – скорее наоборот, алгоритм начал работать медленнее. Вот результаты выполнения функций на картинке 360×480. Использовался 2х-ядерный MacBook с 4Гб RAM, результаты усредненные.
А вот и конечный результат:
- SetPixel/GetPixel – это зло, их трогать не стоит.
- OpenMP продолжает радовать, давая линейный scalability.
- Использование SIMD не гарантирует повышение производительности. Зато доставляет много хлопот.
Если кто-нибудь из читателей готов написать еще более эффективный алгоритм – милости просим! Протестирую и опубликую его прямо здесь.
Источник
Работа с растром на низком уровне для начинающих
Поводом для данной статьи стал следующий пост: «Конвертация bmp изображения в матрицу и обратно для дальнейшей обработки». В свое время, мне немало пришлось написать исследовательского кода на C#, который реализовывал различные алгоритмы сжатия, обработки. То, что код исследовательский, я упомянул не случайно. У этого кода своеобразные требования. С одной стороны, оптимизация не очень важна – ведь важно проверить идею. Хотя и хочется, чтобы эта проверка не растягивалась на часы и дни (когда идет запуск с различными параметрами, либо обрабатывается большой корпус тестовых изображений). Примененный в вышеупомянутом посте способ обращения к яркостям пикселов bmp.GetPixel(x, y) – это то, с чего начинался мой первый проект. Это самый медленный, хотя и простой способ. Стоит ли тут заморачиваться? Давайте, замерим.
Использовать будем классический Bitmap (System.Drawing.Bitmap). Данный класс удобен тем, что скрывает от нас детали кодирования растровых форматов – как правило, они нас и не интересуют. При этом поддерживаются все распространенные форматы, типа BMP, GIF, JPEG, PNG.
Кстати, предложу для начинающих первую пользу. У класса Bitmap есть конструктор, который позволяет открыть файл с картинкой. Но у него есть неприятная особенность – он оставляет открытым доступ к этому файлу, поэтому повторные обращения к нему приводят к эксепшену. Чтобы исправить это поведение, можно использовать такой метод, заставляющий битмап сразу «отпустить» файл:
Методика замеров
Замерять будем, перегоняя в массив и обратно в Bitmap классику обработки изображений – Лену (http://en.wikipedia.org/wiki/Lenna). Это свободное изображение, его можно встретить в большом количестве работ по обработке изображений (и в заголовке данного поста тоже). Размер – 512*512 пикселов.
Немного о методике – в таких случаях я предпочитаю не гоняться за сверхточными таймерами, а просто много раз выполнять одно и то же действие. Конечно, с одной стороны, в этом случае данные и код уже будут в кэше процессора. Но, зато мы вычленяем затраты на первый запуск кода, связанный с переводом MSIL-кода в код процессора и другие накладные расходы. Чтобы гарантировать это, предварительно запускаем каждый кусок кода один раз – выполняем так называемый «прогрев».
Компилируем код в Release. Запускаем его обязательно не из-под студии. Более того, студию также желательно закрыть – сталкивался со случаями, когда сам факт её «запущенности» иногда сказывается на полученных результатах. Также, желательно закрыть и другие приложения.
Запускаем код несколько раз, добиваясь типичных результатов – необходимо убедиться, что на нем не сказывается какой-то неожиданный процесс. Скажем, проснулся антивирус или еще что-то. Все эти меры позволяют нам получить стабильные, повторяемые результаты.
«Наивный» метод
Именно этот метод был применен в оригинальной статье. Он состоит в том, что используется метод Bitmap.GetPixel(x, y). Приведем полностью код подобного метода, который конвертирует содержимое битмапа в трехмерный байтовый массив. При этом первая размерность – это цветовая компонента (от 0 до 2), вторая – позиция y, третья – позиция x. Так сложилось в моих проектах, если вам захочется расположить данные иначе – думаю, проблем не возникнет.
Обратное преобразование аналогично, только перенос данных идет в другом направлении. Я не буду приводить его код здесь – желающие могут посмотреть в исходных кодах проекта по ссылке в конце статьи.
100 преобразований в изображение и обратно на моем ноутбуке с процессором I5-2520M 2.5GHz, требуют 43.90 сек. Получается, что при изображении 512*512, только на перенос данных, тратится порядка полусекунды!
Прямая работа с данными Bitmap
К счастью, класс Bitmap предоставляет более быстрый способ обратиться к своим данным. Для этого нам необходимо воспользоваться ссылками, предоставляемыми классом BitmapData и адресной арифметикой:
Такой подход дает нам получить 0.533 секунды на 100 преобразований (ускорились в 82 раза)! Думаю, это уже отвечает на вопрос – а стоит ли писать более сложный код преобразования? Но можем ли мы еще ускорить процесс, оставаясь в рамках managed-кода?
Массивы vs указатели
Многомерные массивы являются не самыми быстрыми структурами данных. Здесь производятся проверки на выход за пределы индекса, сам элемент вычисляется, используя операции умножения и сложения. Поскольку адресная арифметика уже дала нам один раз существенное ускорение при работе с данными Bitmap, то может быть, попробуем её применить и для многомерных массивов? Вот код прямого преобразования:
Результат? 0.162 сек на 100 преобразований. Так что ускорились еще в 3.3 раза (270 раз по сравнению с «наивной» версией). Именно подобный код и использовался мною при исследованиях алгоритмов.
Зачем вообще переносить?
Не совсем очевидно, а зачем вообще переносить данные из Bitmap. Может вообще, все преобразования осуществлять именно там? Соглашусь, что это один из возможных вариантов. Но, дело в том, что многие алгоритмы удобнее проверять на данных с плавающей запятой. Тогда нет проблем с переполнениями, потерей точности на промежуточных этапах. Преобразовать в double/float-массив можно аналогичным способом. Обратное преобразование требует проверки при конвертации в byte. Вот простой код такой проверки:
Добавление таких проверок и преобразование типов замедляет наш код. Версия с адресной арифметикой на double-массивах исполняется уже 0.713 сек (на 100 преобразований). Но на фоне «наивного» варианта – она просто молния.
А если нужно быстрее?
Если нужно быстрее, то пишем перенос, обработку на C, Asm, используем SIMD-команды. Загружаем растровый формат напрямую, без обертки Bitmap. Конечно, в этом случае мы выходим за пределы Managed-кода, со всеми вытекающими плюсами и минусами. И делать это имеет смысл для уже отлаженного алгоритма.
Update 2013-10-08:
По предложению комментаторов, добавил в код вариант переноса данных в массив с помощью Marshal.Copy(). Это сделано чисто с тестовыми целями — у такого способа работы есть свои ограничения:
- Порядок данных точно такой же, как и в оригинальном Bitmap. Т.е., компоненты перемешаны. Если мы хотим их отделить друг от друга — нужно будет все-равно проходиться циклом по массиву, копируя данные.
- Тип у яркости остается byte, в то же время, часто бывает удобно промежуточные вычисления выполнять с плавающей запятой.
- Marshal.Copy() работает с одномерными массивами. Да, они конечно самые быстрые и не очень сложно везде писать rgb[x+width*y], но все-таки.
Итак, копирование в две стороны происходит за 0.158 сек (на 100 преобразований). По сравнению с более гибким вариантом на указателях, ускорение очень небольшое, в пределах статистической погрешности результатов разных запусков.
Update 2016-04-25:
Пользователь Ant00 указал на ошибку в коде метода BitmapToByteRgbQ. Она не сказывалась на времени, но перекладывание осуществлялось неправильно. Была ошибка при копипасте фрагментов из работающего кода. Поправил. Спасибо за настойчивость (не с первого раза я внимательно рассмотрел код в статье, которой уже 2.5 года).
Источник
Bitmap Конструкторы
Определение
Некоторые сведения относятся к предварительной версии продукта, в которую до выпуска могут быть внесены существенные изменения. Майкрософт не предоставляет никаких гарантий, явных или подразумеваемых, относительно приведенных здесь сведений.
Инициализирует новый экземпляр класса Bitmap.
Перегрузки
Инициализирует новый экземпляр класса Bitmap из указанного существующего изображения.
Инициализирует новый экземпляр класса Bitmap из указанного потока данных.
Инициализирует новый экземпляр класса Bitmap из указанного файла.
Инициализирует новый экземпляр класса Bitmap из указанного существующего изображения, масштабированного до заданного размера.
Инициализирует новый экземпляр класса Bitmap с заданным размером.
Инициализирует новый экземпляр класса Bitmap из указанного потока данных.
Инициализирует новый экземпляр класса Bitmap из указанного файла.
Инициализирует новый экземпляр класса Bitmap из указанного ресурса.
Инициализирует новый экземпляр класса Bitmap из указанного существующего изображения, масштабированного до заданного размера.
Инициализирует новый экземпляр класса Bitmap с заданным размером и с разрешением указанного объекта Graphics.
Инициализирует новый экземпляр класса Bitmap с заданными значениями размера и формата.
Инициализирует новый экземпляр класса Bitmap с заданным размером, форматом и данными пикселей.
Bitmap(Image)
Инициализирует новый экземпляр класса Bitmap из указанного существующего изображения.
Параметры
Объект Image, из которого будет создан новый объект Bitmap.
Применяется к
Bitmap(Stream)
Инициализирует новый экземпляр класса Bitmap из указанного потока данных.
Параметры
Поток данных, используемый для загрузки изображения.
Исключения
Параметр stream не содержит данные изображения или равен null .
-или- Поток stream содержит файл изображения PNG, один из размеров которого превышает 65535 пикселей.
Примеры
В следующем примере кода показано, как загрузить растровое изображение из потока.
Этот пример предназначен для использования с Windows Forms. Создайте форму, содержащую PictureBox имя PictureBox1 . Вставьте код в форму и вызовите InitializeStreamBitmap метод из конструктора формы или Load метода обработки событий.
Комментарии
Поток должен оставаться открытым в течение времени существования Bitmap .
Из-за ограничения декодера GDI+ System.ArgumentException создается исключение при создании точечного рисунка из PNG-файла с одним измерением, превышающим 65 535 пикселей.
См. также раздел
Применяется к
Bitmap(String)
Инициализирует новый экземпляр класса Bitmap из указанного файла.
Параметры
Имя файла растрового изображения и путь к нему.
Исключения
Указанный файл не найден.
Комментарии
Имя файла и путь могут относиться к приложению или абсолютному пути. Используйте этот конструктор для открытия изображений со следующими форматами файлов: BMP, GIF, EXIF, JPG, PNG и TIFF. Дополнительные сведения о поддерживаемых форматах см. в разделе Типы точечных рисунков. Файл остается заблокированным до тех пор, пока Bitmap не будет удален.
См. также раздел
Применяется к
Bitmap(Image, Size)
Инициализирует новый экземпляр класса Bitmap из указанного существующего изображения, масштабированного до заданного размера.
Параметры
Объект Image, из которого будет создан новый объект Bitmap.
Структура Size, представляющая размер нового изображения Bitmap.
Исключения
Операция не удалась.
Применяется к
Bitmap(Int32, Int32)
Инициализирует новый экземпляр класса Bitmap с заданным размером.
Параметры
Ширина в пикселях нового изображения Bitmap.
Высота в пикселях нового изображения Bitmap.
Исключения
Операция не удалась.
Комментарии
Этот конструктор создает объект Bitmap со PixelFormat значением перечисления Format32bppArgb .
Применяется к
Bitmap(Stream, Boolean)
Инициализирует новый экземпляр класса Bitmap из указанного потока данных.
Параметры
Поток данных, используемый для загрузки изображения.
Значение true , чтобы воспользоваться цветокоррекцией для этого изображения Bitmap; в противном случае — значение false .
Исключения
Параметр stream не содержит данные изображения или равен null .
-или- Поток stream содержит файл изображения PNG, один из размеров которого превышает 65535 пикселей.
Комментарии
Поток должен оставаться открытым в течение времени существования Bitmap .
Из-за ограничения декодера GDI+ System.ArgumentException создается исключение при создании точечного рисунка из PNG-файла с одним измерением, превышающим 65 535 пикселей.
См. также раздел
Применяется к
Bitmap(String, Boolean)
Инициализирует новый экземпляр класса Bitmap из указанного файла.
Параметры
Имя файла точечного рисунка.
Значение true , чтобы воспользоваться цветокоррекцией для этого изображения Bitmap; в противном случае — значение false .
Примеры
В следующем примере кода показано, как создать новое растровое изображение из файла. В примере используются GetPixel методы и SetPixel для перекрашивания изображения. Он также использует PixelFormat свойство.
Этот пример предназначен для использования с формой Windows Forms, которая содержит Label , PictureBox и Button с именем Label1 PictureBox1 и Button1 , соответственно. Вставьте код в форму и свяжите Button1_Click метод с Click событием кнопки.
Комментарии
Используйте этот конструктор для открытия изображений со следующими форматами файлов: BMP, GIF, EXIF, JPG, PNG и TIFF. Дополнительные сведения о поддерживаемых форматах см. в разделе Типы точечных рисунков. Файл остается заблокированным до тех пор, пока Bitmap не будет удален.
Источник