Прямой доступ к пикселям Bitmap

Тема прямого доступа к пикселям как-то сама-собой получилась из темы быстрого доступа к пикселям. Потому что не важно, в какой технологии или какой библиотеке сейчас работаем, как только возникает необходимость быстрой обработки изображения, в конечном счете, приходим к необходимости иметь указатель на массив пикселей.

Имея такой указатель, мы максимально быстро может осуществить и навигацию по массиву, и изменения в нем. Главное, чтобы эти изменения отобразились потом в результирующем изображении.

Нет секретных технологий скоростного доступа к пикселям. Есть указатель на начало массива пикселей. Получением которого сейчас и займемся.

TBitmap. ScanLine

Старый добрый TBitmap, активно руганный на всевозможных форумах. Люди до сих пор пишут свои реализации работы с DIB, в надежде получить выигрыш в скорости и секретные знания, недоступные в стандартном TBitmap.

Пример подобного подхода, когда отказываемся от готовых решений и пишем все рукми.

Пример загрузки образа DIB из файла BMP с помощью DIB секции

[свернуть]

Хотя все давным-давно уже сделано непосредственно в TBitmap. Класс TBitmap, кто бы что ни говорил, это очень удачная инкапсуляция всего того геморроя, который пришлось бы пережить, реализуя работу с DIB руками.

Доступ к массиву пикселей можно легко получить через свойство ScanLine.

Доступ к массиву пикселей через ScanLine

[свернуть]

Можно было бы конечно просто идти в цикле по Y и брать указатель на начало каждой новой строки, как ScanLine[Y] . Но метод GetScanLine, обслуживающий свойство ScanLine, помимо вычисления указателя на просимую строку bitmap, выполняет еще ряд действий и проверок. Этих действий хотелось бы избежать, потому что 1) уверены в своей непогрешимости, и 2) нам нужен просто указатель на нужный пиксель. Поэтому перед заходом в цикл, мы просто встаем на начала массивов источника и приемника и далее просто смещаем указатели.

Как-то странно выглядит фраза:

Как будто мы встаем не в начало, а в самый конец. Дело в том, что изображение в стандартном bitmap хранится в перевернутом виде. Когда мы требуем дать нам 0-ую строку (Row=0), метод GetScanLine вычисляет ее как:

Т.е. на самом деле нам вернут самую «нижнюю» и двигаться в этом случае надо снизу вверх. Если бы мы запрашивали ABitmap.ScanLine[0], нам пришлось бы декрементировать указатели на значение 2*Width всякий раз после цикла по X. Потому что один Width – только что прошли по строке, и надо вернуться снова на начало строки, и другой Width – чтобы выйти на начало строки, которая «выше». А это лишние действия, которые можно и нужно избежать.

Или можно заменить получение ссылки на начало строки чем-то более легковесным, нежели GetScanLine. Для этого нам нужно знать: стартовый указатель и ширину строки в байтах. Указатель получим как ScanLine[0] . В статье все исходники написаны для 32-битного формата. При использовании других форматов для подсчета ширины строки удобно использовать функцию BytesPerScanline, которая находится в Vcl.Graphics. Берет параметрами 3 значения:

  • PixelsPerScanline — ширина, bitmap.Width;
  • BitsPerPixel — сколько бит на цвет;
  • Alignment — значение выравнивания, считаем, что 32.

В том случае, когда нужны пиксели строго по координатам, можно сделать следующим образом:

«Живой» пример перевода изображения формата OpenCV в TBitmap. Функция, отвечающая за это в библиотеке для OpenCV 2.4.13 для Delphi (об этой библиотеке в следующей статье), а именно ocv.utils.cvImage2Bitmap, написана не самым лучшим образом. Поэтому пришлось переписать в свое время. Работает раза в 3 быстрее оригинала. Ограничений на размер изображения не имеет.

Конвертация PIplImage в TBitmap. OpenCV

[свернуть]

GDI+. TGPBitmap. LockBits

Для получения указателя на массив пикселей в GDI+ для класса TGPBitmap предусмотрена пара методов:

Первый метод блокирует битовый образ (весь или часть его) в системной памяти. Область блокировки задается прямоугольником rect. Режим чтения и/или записи задается параметром flags. Глубина цвета определяется параметром format. Просимая глубина может отличаться от текущей глубины изображения.

Заблокированный таким образом массив будет доступен в структуре lockedBitmapData. Указатель на массив находится в поле Scan0. Ширина строки в байтах содержится в поле Stride. Если Stride имеет отрицательное значение, это означает, что двигаться надо снизу вверх. Для экономии места и времени будем считать, что знак положительный. По окончании работы с массивом образ надо разблокировать методом UnlockBits.

Доступ к массиву пикселей через блокировку GDI+

[свернуть]

Как видно по коду, отказались от вызова LoadGPBitmapFromGraphic. Функция безусловно полезная и универсальная. Но у нас на входе TBitmap. Поэтому давайте сильно упростим себе жизнь и запишем просто:

Чтобы обратиться к любому пикселю по координатам (X,Y) надо посчитать указатель на него. Будем считать, как индекс в массиве 32-разрядных целых.

Direct2D. TWicImage. IWICBitmapLock

Добрались до DirectX. Это все по-прежнему есть в стандартной Delphi. Для работы с битовыми матрицами используется интерфейс IWICBitmap. Именно на его основе формируется ID2D1Bitmap для дальнейших манипуляций и рисований.

Строго говоря, массив пикселей в DirectX сокрыт в недрах CPU, и «в терем тот прекрасный нет входа никому». Это официальная парадигма. Однако, достучаться до массива пикселей все таки возможно.

Речь пойдет об интерфейсах IWICBitmap,  IWICBitmapLock. Описаны в модуле Winapi.Wincodec. Отправной точкой будем считать эту статью. Но, будучи программистами, людьми прогрессивными, т.е. ленивыми, воспроизводить полностью код не будем, ибо многое уже сделано в классе TWicImage.

Класс TWICImage инкапсулирует Microsoft Windows Imaging Component (WIC), позволяющий загружать форматы графических файлов, зарегистрированные в WIC. Поддерживает форматы: BMP, GIF, ICO, JPEG, PNG, TIFF, Windows Media Photo и прочие.

Итак. Если верить статье, а оснований ей не верить нет, перед тем как начинать что-то делать необходимо создать экземпляр IWICImagingFactory, с помощью него создать IWICBitmapDecoder, получить первый (и единственный) фрейм  IWICBitmapFrameDecode, и только потом создать IWICBitmap. Этого всего делать не будем, потому что все есть в начинке TWicImage, и как это делается всегда можно подсмотреть в исходниках. Итак, начнем с пункта 4 статьи. Свойство Handle класса TWicImage имеет тип IWICBitmap. Это как раз то, что нам нужно. Далее вызываем метод Lock (см.п.5. статьи), получаем интерфейс IWICBitmapLock.

Далее  получаем указатель на начало битового массива с помощью метода GetDataPointer только что полученного экземпляра IWICBitmapLock.

У нас есть все, что нужно. Т.к. интересует не только чтение пикселей, но и запись, создаем два экземпляра TWicImage. Один для чтения – исходный bitmap, второй для записи — результат.

Сам процесс вполне уже привычный. Это та часть, которая во фрагменте выше звучит как «Pixel manipulation using the image data pointer pv».

Вот как это звучит в Delphi. Проверок на успех операций делать не стал, код и так объемный.

Доступ к массиву пикселей через блокировку IWICBitmapLock

[свернуть]

Нельзя не заметить, что подход ровно такой же, как в GDI+. Заблокировать участок, получить массив, что-то сделать, разблокировать.

Конечно, много времени тратится на создание всех промежуточных классов и эту функцию в реальности использовать вряд ли стоит. Поэтому листинг для подсмотреть – как получить и модифицировать напрямую массив пикселей для IWICBitmap, как выгрузить в обычный TBitmap или как загрузиться из TBitmap.

Но даже при всех интерфейсных тормозах и потоковых выгрузках картинка 800×800 обрабатывается за 13-17 миллисекунд. Неплохо, правда?

FastDIB

Посмотрим, что есть хорошего вне Delphi. Хорошего, но без дополнительных dll. Есть ряд интересных библиотек, допустим, FreeImage или ImageEn, но они с собой тащат ряд дополнительных модулей. Последняя еще и платная. Для серьезного проекта, возможно, это и не зазорно. Но давайте исходить из концепции «маленького проекта». Быстро, легко, портабельно.

С древних времен люди пытаются найти свой особенный путь наибыстрейшей работы с битовой матрицей. Вот пример такого подхода. И, что ни говори, работа с bitmap с помощью этой библиотеки оправдывает свое название. Это действительно быстро. Однако, если заглянуть под капот, увидим код, очень похожий на тот, который реализован в TBitmap. И массив пикселей, в конечном счете, хранится все там же. Скачать можно тут.

Взял на себя смелость включить в состав демо проекта. Исходник очень небольшой. Ключевой класс – TFastDIB. Указателем на начало массива пикселей в случае TFastDIB является свойство Pixels32[0].

Доступ к массиву пикселей FastDIB

[свернуть]

Стоит отметить, что тут, в отличие от TBitmap, при обращении к ScanLine (Pixels32), не происходит подсчета «настоящего» индекса. Хочешь 0-ю строку, пожалуйста, возвращает именно 0-ю строку.

Изображение, помним, перевернуто. Но для текущей задачи, как и для большинства прочих, это не имеет никакого значения.

В каждом методе мы получаем результирующий TBitmap разными способами. Здесь мы его просто рисуем. Для демонстрации возможностей TFastDIB.

Graphics32

Что тут скажешь… Это легенда. В отличие от многих аналогов, библиотека живет, дышит и активно развивается. Кто в теме, давно имеет ее в своем багаже. Кто еще не обзавелся, активно приглашаю сюда. Ее надо поставить. Изучать исходники. Наслаждаться результатом.

Однако, по прежнему ратую за то, что если есть возможность обойтись без использования дополнительных компонент, значит надо обходиться без них. Имейте ввиду, что исходный ход распространяется под лицензией MPL 1.1 / LGPL 2.1. Это означает, что ваш продукт, или часть его, использующая graphics32, должна идти под той же лицензией и с открытым кодом.

Не все мои заказчики испытывали радость от такой новости.  

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

Основа библиотеки — класс TBitmap32. И указателем на начало массива пикселей будет являться свойство Bits. Свойство ради этого и существует. Все просто.

Для работы с компонентами R, G, B используем тип PColor32Entry, который, по сути, повторяет PRGBQuad. Можем вообще использовать его вместо PColor32Entry. Просто хочется продемонстрировать типы Graphics32. Остальное все как обычно.

Доступ к массиву пикселей TBitmap32

[свернуть]

ПолучениеTBitmap (Result) из TBitmap32 (dst) также проще простого:

Дополнение 1. Скорость обработки

Вернемся к теме скоростной обработки битовой матрицы. В такой могучей библиотеке, как graphics32, наверняка есть что-то готовое для нашей операции инвертирования. И действительно есть. В модуле GR32_Filters находим прямо таки конкретную функцию для нашей операции. Пишем, пробуем.

Использование штатных средств Graphics32

[свернуть]

Интересуемся начинкой функции Invert и обнаруживаем, что внутри происходит точно такой же пробег по массиву пикселей, но для инвертирования используется операция XOR. Ну что же, напишем свой вариант.

Использование XOR для инвертирования изображения

[свернуть]

И как тебе такое, Илон Маск?

Кстати, обычный пробег по TBitmap, описанный в самом начале, дает результат ничуть не хуже.

Дополнение 2. Попытка быстрой отрисовки

Может рисованием получиться добиться сногсшибательных скоростей? GDI+ не имеет продвинутых инструментов обработки массива пикселей, зато имеет просто уникальный набор эффектов при рисовании. Воспользуемся TColorMatrix при рисовании инвертированного изображения.

Структура цветовой матрицы TColorMatrix

Инициализируем цветовую матрицу следующим образом:

Latex formula

Таким образом при выводе изображения, цвет будет считаться по каждой компоненте R, G, B, как:

R = –1.0*R + 1.0*255 = 255-R

Рисуем инвертированное изображение силами GDI+

[свернуть]

Скорость рисования оставляет желать лучшего. Намного быстрее обработать Bitmap до вывода.

Выводы

Посмотрим, что у нас по скорости при использовании разных методик.

Результаты работы разных методик

Честно говоря, все, за исключением последней «рисовашки», показывают примерно одно и то же время. Одно и то же очень малое время. Обработка картинки 800 x 800 занимает примерно 3-4 миллисекунды.

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

Не надо гнаться за какими-то мифическими супер-быстрыми технологиями. Обычный TBitmap + ScanLine показывает просто сногсшибательный результат.

P.S.

В структуре TRGBQuad есть поле rgbReserved, которое в теории должно быть равно 0 и не использоваться. Да как бы не так:

Установка альфа канала для TBitmap

[свернуть]

Перед установкой в TImage (imgRes) сделаем так:

Использование альфа-канала TBitmap при отображении в TImage

Друзья, спасибо за внимание!

Информация о новых статьях смотрим в телеграм-канале.

Не забываем комментировать и подписываться )))


Скачать (5.22 Мб): Исходники (Delphi XE 7-10)

Скачать (5.10 Мб): Исполняемый файл

5 6 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest
0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x
()
x