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

Быстрый доступ к пикселям

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

Для подобных задач есть целая обойма технологий — DircetX, OpenGL, OpenCV и тому подобное. Позднее доберемся и до них. Но что делать, если в рамках локальной задачи необходимо поменять цветность картинки. Допустим, привлечь внимание к подозрительному «зависанию» процесса путем плавного наращивания красного.

Рис.1. Круговой прогресс бар с индикацией «зависания».

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

Рис.2. Определение устоявших кегель и подсчет голов сыра

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

Также, попытаемся обойтись без ассемблера. В свое время увлечение скоростью обработки (но правда, строк) вылилось в муки перевода ассемблерного кода на 64-битные рельсы. Главная задача — найти простое, понятное и легко переносимое решение, которое всегда под рукой.

Подготовка экспериментальной площадки

Работа с битовой матрицей почти всегда строится по принципу — идем по всей матрице W x H и что-то делаем с каждым пикселем. В 99.9% случаев упрощенно это выглядит так:

Пока сделаем простую операцию с пикселем — инвертирование:

Все методы обработки имеют одинаковую «сигнатуру»:

  • ABitmap — искомая битовая матрица, которую надо инвертировать.
  • AEvent — событие TNotifyEvent, в котором Sender на самом деле целочисленный процент выполнения.

Генерация события происходит следующей процедурой:

Вызов из каждого метода таков:

Сделано так с целью минимизировать время на вызов события и «рисование» прогресс бара в главном окне. Потому что ну как же без прогресса.

Обработчик события в форме очень прост:

Вызовы методов доступа к пикселям реализованы как кнопки в интерфейсе (фрагмент метода btnStandartClick):

Из кода можно сделать вывод, что есть некий режим оптимизации (в самом конце статьи) и есть возможность не генерировать событие выполнения (для получения «чистых» оценок времени).

Также видим, что на все обработки вызывается одна функция. Она выполняет роль диспетчера:

Описывать весь проект смысла не вижу, легче скачать и посмотреть.

Рис.3. Интерфейс

Интерфейс прост. Имеем 4 кнопки под рисунком, это выбор предопределенного изображения. Можем вставить из буфера обмена кнопкой «Paste» или загрузить из файла кнопкой «Load». Остальные кнопки — выбор метода. В строке статуса видим размерность изображения и примерное время выполнения. Сейчас на рисунке очень долгое время выполнения самого первого метода — Standart. Этот метод использует самое простое — свойство Pixels Canvas’а битовой матрицы.

Свойство Canvas.Pixels

Рано или поздно всем приходилось сталкиваться со стандартным свойством TCanvas.Pixels[x,y]. Ничего кроме расстройства это свойство не принесло. Останавливаться долго не будем и возвращаться к нему тоже. Но для полноты картины должен был о нем упомянуть.

Использование Canvas.Pixels

[свернуть]

На рис.3 видно, что время выполнения 3578 миллисекунд для картинки размером 900 x 900 пикселей. Если мы хотим обрабатывать видео «на лету» со скоростью 25 кадров/сек, можно навсегда забыть об этом методе.

А что скажет GDI+ по этому поводу?

Пиксель GDI+

В GDI+ тоже есть GetPixel(x,y) и SetPixel(x,y). Чтобы не тратить много времени на описание принципов работы с ним, перейдем сразу к листингу.

Пиксели в GDI+

[свернуть]

Казалось бы, все очень просто. Действительно просто, если не глянуть в PixelsGDIPBitmap, который был написан специально для этой функции.

Модуль PixelsGDIPBitmap. Работа с bitmap GDI+

[свернуть]

К сожалению, GDIP не поддерживает стандартные для Delphi типы TGraphic. Поэтому работа с GDI+ осложняется необходимостью писать много кода. Этот модуль написан всего лишь для того, чтобы получить TGPBitmap из TBitmap, и наоборот, из получившегося TGPBitmap забрать TBitmap.

Для себя эту проблему решил в свое время. Написал наследника TCanvas, который реализует в себе как стандартные вызовы, так и вызовы GDI+. А также ряд классов-оберток над TGPPen, TGPBrush и TGPImage. Все рутинные вещи спрятаны «под капот». Ниже иллюстрация как тоже самое выглядело бы с использованием TxGDIPBitmap.

Кнопка «IPBitmap»

Код, в принципе, не сильно отличается от предыдущего. Отличие в том, что вся рутина, частично представленная в PixelsGDIPBitmap убрана.

пример использования TxGDIPBitmap

[свернуть]

Итак, что имеем. Тот же рисунок, но время 265 мсек. Ура! О нет, еще не ура, это слишком долго.

Пожалуй, не все возможности стандартного TBitmap исчерпаны. Знающие люди понимают, что переходим к замечательному свойству ScanLine.

ScanLine

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

Небольшое отступление. Бытует мнение, что для ScanLine оптимальным является формат pf24bit. Чтобы проверить это утверждение, добавим checkbox «32 bits». Если на нем будет галка, работаем с 32-битной матрицей, иначе — с 24-битной.

Инициализация матриц перед использованием такова:

И сам метод:

Использование TBitmap.ScanLine

[свернуть]

Время 62 мсек. Скорость просто сказочная. Также выяснили, что от формата матрицы, 24 или 32 бита, скорость никак не зависит. Но, к сожалению, есть нюанс.

Сейчас алгоритм таков. Мы знаем, что scan-линии идут горизонтально. Поэтому первый цикл у нас по Y. Мы получаем в цикле указатель на начало очередной линии и во вложенном цикле по X смещаем указатель. Таким образом, выходим на следующий пиксель. Это идеальный вариант.

Но в жизни нам чаще всего приходится считать конечную точку (пиксель) на основании значений, расположенных и выше, и ниже, и по бокам заданной координаты. Также, эксперимент не совсем «чистый». В предыдущих методах мы обращаемся к пикселю по координатам, а тут «хитрим».

Напишем «честный» вариант:

Подсчет смещений для X и Y в вызовах TBitmap.ScanLine

[свернуть]

Время стало 3688 мсек. Хуже, чем Canvas.Pixels. То есть, хуже и быть не может…. Не рановато ли метод отправлен на свалку истории?

Проблема конечно не в «честности», а в том, чтобы брать пиксель от произвольной координаты. Поэтому снова модифицируем код.

Улучшенный метод ScanLine

Идея в следующем. ScanLine берет свое значение как смещение от поля bmBits внутренней структуры типа tagBITMAP, которая содержится внутри класса TBitmap, и наружу не торчит ни единым методом или свойством. Инициализация структуры происходит в том числе и в момент запроса ScanLine, если ранее не была создана.

Таким образом, если перед нашими циклами запросить нулевые ScanLine, тем самым получив указатель на bmBits (массив битов растрового изображения), и потом, в цикле, правильно находить нужное смещение, можно предположить выигрыш в скорости.

Произвольный X,У при использовании TBitmap.ScanLine

[свернуть]

Вуаля! Снова видим 62 мсек. И давайте сменим картинку. На аналогичную 900 x 900.

Рис.4. Скорость ScanLine по произвольным X,Y

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

Настала пора, пожалуй, заняться написанием класса.

Класс TxIPBitmapScan

Напишем очень простой класс, реализующий доступ к пикселю bitmap по координатам (X,Y) на основе предыдущего метода.

По большому счету, нам нужно в начале работы знать сколько байт у на пиксель, начало битового массива и посчитать «ширину» линии. Реализуем это в конструкторе:

Предполагаем, что работать будем только с двумя возможными форматами — 24 и 32 бита. Мы ведь можем назначать любой формат. 32 бита нам нужно только в случае использования альфа-канала. Во всех остальных случаях пусть будет 3 байта на пиксель.

Далее будем просто получать указатель на нужное место в битовом массиве. Чтобы видеть альфа составляющую, в случае 32 бит, используем тип PRGBQuad. И в случае 24 бит, и 32 бита, нам вернется структура, где R, G, B находятся на правильных местах и содержат правильное значение.

Суммируя все это, остальные свойства и методы выглядят так:

Весь листинг модуля под спойлером.

Реализация класса TxIPBitmapScan

[свернуть]

Тест класса показывает время, аналогичное «быстрым» ScanLine методам. Таблица победителей распределилась таким образом:

Рис.5. Победители

Оптимизация

Если внимательно посмотреть на код, увидим, что массу лишнего времени тратим на получение цветовых составляющих R, G, B и обратное преобразование. Хотя эти параметры у нас уже есть изначально.

Например:

Можно записать как:

Немного модифицируем методы, связанные со ScanLine. Добавим глобальную переменную GPixOptimization: Boolean. За ее инициализацию отвечает соответствующая галочка в интерфейсе. И внутри каждого цикла произведем вот такое усложнение:

Конечно, для каждого метода есть свои нюансы. Это можно посмотреть непосредственно в коде.

Правда, на картинке 900 x 900, выигрыш почти не ощутим. Что если взять побольше полотно?

Большая картинка. Большой тест.

В завершение проведем тест на большой картинке. Разрешение 900 x 900 это немало. Но у нас есть 6080 x 3413. Номер четыре. Это, прямо скажем, вызов.

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

Рис.6. Большая картинка. Без оптимизации.

Победитель явно первый ScanLine, который использует все преимущества идеального варианта и не использует координаты. А наш класс на 3-м месте. Однако, картина меняется, если включить оптимизацию.

Рис.7. Большая картинка. Оптимизация.

Сейчас оптимизация вполне себе ощутима. Выигрыш в 1.5-2 раза.

Теперь наш класс на первом месте. На самом деле все три победителя равнозначны. Просто с классом работать удобней, когда дело дойдет до фильтров и сверток. А время сильно зависит, от того, как карта у ОС ляжет. При всех прочих одинаковых условиях один и тот же метод может дать и 400 миллисекунд.

Выводы

Таким образом, для быстрой работы с пикселями, без привлечения сторонних библиотек, мало одной технологии, не помешает и методология. Допустим, если мы в большинстве случаев идем по исходной матрице, рассчитываем каждый пиксель на основании соседних, имеет смысл использовать класс. А при сохранении можно использовать тот самый идеальный вариант первого ScanLine. В этом случае никаких расчетов ведь не происходит. Надо просто присвоить получившийся цвет в конкретную точку bitmap.

Тема не закрыта

Изначально хотел рассказать больше. Но статья и без того получилась весьма объемна. Поэтому, тема еще не закрыта. В следующий раз планирую рассказать, как на самом деле обстоят дела с по-пиксельным доступом в GDI+ и показать, как это сделано в Graphics32.

Ах, да! Глобальные переменные — это нехорошо. Присутствуют в коде по причине того, что это пример и иллюстрация. В реальной жизни их надо избегать )))


Информация о новых статьях есть в моем телеграм-канале. На сайте не будет e-mail и прочих рассылок, потому что не люблю. ТГ-канал мне кажется самым демократичным — доставка мгновенная, в любое время можно отписаться без всяких заморочек и вопросов: типа, что не понравилось.

Надеюсь, информация была полезной.

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

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

Скачать

Исходники 2.96 Мб (Delphi XE 7-10)

Исполняемый файл 3.54 Мб

В версии для Delphi 7 убрана работа с GDI+. В D7 GDI+ из коробки нет, дополнительно ставить не стал.

Исполняемый файл + Исходник 5.57 Мб (Delphi 7)

Появилась возможность «безболезненно» использовать GDI+ для Delphi 7. Узнать как это сделать и скачать исходник для этой статьи можно по ссылке «Как подключить GDI+ для Delphi 7 и не иметь проблем в XE»


5 11 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

11 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Aleks

Просто супер! Нагрузка на ЦП минимальная или недолгая. Как изменить xIPBitmapScan чтобы быстро использовать в своей программе, нужно попроще код. Мне нужно искать пикселы нужного цвета , только и всего.

Александр

Я разобрался как использовать в своём проекте ваш класс. Спасибо, очень удобно. Я увижу разницу в нагрузке на ЦП если будут использовать GetPixel из класса, в цикле?

Александр

Спасибо! Это потрясающе! Я сделал тест на время и результат такой (конкретно в моём случае изображение 500x на 700y).
Canvas = 10 — 12ms
TxIPBitmapScan = 0 — 1 ms.

Теперь можно работать с видеопотоком. Пишу проект детектирования предметов для робота. Ваш чудесный код очень выручил.

Александр

Забыл добавить, что при использовании в фоновом потоке Canvas работает не точно, например перекрашивает только часть изображения. TxIPBitmapScan в фоновом потоке работет без проблем.

Владислав

Как будет выглядеть все это в Borland delphi 7? Не получается написать все то же самое, что и у вас.

Sturman

Всё очень интересно.
А вот получение TBitmap из TGPImage я делаю так:
GPImage.GetHBITMAP(aclTransparent, h);
Bitmap.Handle := h;
вроде бы работает.. (но для фоток ещё нужна проверка на поворот)

11
0
Не нашли ответ на свой вопрос? Задайте его здесь!...x