Как очень быстро сделать истинное черно-белое изображение

Monro

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

Зачем нужно черно-белое изображение?

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

Черно-белое изображение, это по сути массив логических единиц — либо что-то есть, либо нет. Истина/Ложь. «Что-то есть» — это искомый объект поиска. Не обязательно лицо или буква. Это может быть любая область, границы которой необходимо получить или распознать. И это не обязательно битмап.

А фото, как черно-белое изображение, подойдет?

Нет. Строго говоря, черно-белая фотография — это изображение в оттенках серого. Что для ряда алгоритмов распознавания смерти подобно. Алгоритму нужно два состояния — да/нет, сказал, отрезал. А градации серого — это «ну я не знаю» в количестве 256 вариантов.

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

Картинки кликабельны.

Например, если к черно-белому портрету Монро (сегодня она главная) применить пороговую обработку с порогом 251 — получим такое «мохнатое» чудище. Казалось бы — идеальный черно-белый исходник, на любом пороге в интервале 1..254 должен быть идеальный результат. Просто это не черно-белый исходник и гистограмма справа наглядно это показывает — там дофига градаций серого.

А в чем проблема?

Проблема как обычно — в скорости. Допустим, надо обрабатывать видеопоток в реальном времени. Можно потратить уйму времени на поиск готового решения или библиотеки. Хотя, все как обычно, есть под рукой. Надо просто суметь все это вкусно приготовить. Вот в этом и проблема.

Общий подход к подготовке изображения

Вначале надо перевести изображение в оттенки серого. Нам не нужны значения по всем каналам, хочется работать с одним параметром — и это будет яркость. Как сделать изображение в оттенках серого, описано тут. Отбросим пугающее слово Direct2D, нам просто нужны коэффициенты.

Далее, возможно, надо сделать легкое размытие, чтобы мостики, связывающие разные кляксы, стали тоньше и более слились с фоном. Возможно надо что-то сделать с общей яркостью, применить фильтр резкости и т.д. Не суть. В каждом случае — это набор кадров, имеющих некое одно общее свойство. Освещенность, размытость, перекос. Набор мероприятий с изображением зависит от каждого конкретного случая применения.

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

На этом шаге должны исчезнуть все незначительные мостики между интересующими объектами. Если этого не произошло — вернуться назад и еще поработать с изображением.

Bitmap

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

Получить изображение в оттенках серого

Оттенки серого получаются, когда значения всех каналов равны. Если цвет — это комбинация красного(R), зеленого(G) и синего(B), то при значении всех каналов R=128, G=128, B=128, получим вот такой серый цвет, при значений всех каналов 191 — такой

Рассмотрим два пути. В первом, значения всех каналов суммируются и результат делится на 3. Во-втором, значение каждого канала необходимо умножить на некий свой коэффициент и сложить..

Таких коэффициентов на самом деле масса разновидностей. Рассмотрим два варианта.

Цикл по матрице разнесен в разные процедуры, чтобы избавиться от условных переходов внутри цикла. Для быстроты. Можно сделать через указатель на функцию расчета и обойтись одним циклом, но пока для ясности пусть будет так. Задействуем позже.

Листинг 1: Преобразовать изображение в оттенки серого

Развернуть код

[свернуть]
Рис.1. Монро в сером по коэффициентам PAL. Картинки кликабельны

Как видим, картинка 1000 x 988 обрабатывается порядка 16-20 миллисекунд. Справа присутствует черно-белая картинка. Получена следующим образом. Об этом уже упоминалось в начале статьи:

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

Листинг 2: Получить черно-белое изображение из оттенков серого

  • AGraphic должен быть заранее подготовленным изображением в оттенках серого;
  • AThreshold — пороговое значение в интервале 0..255;
  • AInvert — инвертировать результат, т.е. то что ниже порога станет белым, а не черным. И, соответственно, то что выше, станет черным.
Развернуть код

[свернуть]

На рисунке ниже представлен метод среднего арифметического для оттенков серого с последующей обработкой с порогом 185.

Рис.2. Оттенки серого по среднему арифметическому и черно-белая картинка с порогом 185.

В отличие от предыдущего листинга, в котором смело инкрементируем указатели, здесь перед каждым вхождением в цикл по горизонтали считаем указатель на начало строки. Связано с тем, что ранее для обеих матриц указывали формат pf32bit. При таком формате данные идут аккуратно друг за другом блоками по 4 байта. Сейчас же заказан формат pf8bit и ширина строки матрицы должна быть высчитана с помощью штатной функции BytesPerScanline, которая возвращает реальное количество байт в строке bitmap.

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

Картинки кликабельны.

Если закомментировать строку 48 в листинге 2, можно получить результат, как на рисунке выше.

Поэтому, когда работаем с указателем на начало массива пикселей битмапа, всегда надо помнить про реальную ширину строки и функцию BytesPerScanline.

Результаты и выводы

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

Рис.3. Метод HDTV. Картинки кликабельны

И результат, и время вполне приемлемы. Но следует учитывать, что во-первых общее время получения черно-белого изображения составило 17.020 + 17.947 = 34.967 миллисекунд, что в принципе можно считать неплохим показателем. Во-вторых, проводить математику прямо на битмапе как-то неправильно. Хочется иметь какой-то буфер с посчитанными значениями. Который можно многократно использовать для различных вычислений.

Также, в процессе вычислений получаем вещественные значения и всегда округляем к байту. Возможно, порог с плавающей запятой и хранение яркости в дробях даст более качественный результат. Давайте попробуем хранить яркость изображения в типе Single. И сравнивать с вещественным порогом.

Floatmap и кубок огня

Что объединяет эти два странных понятия? И то, и другое — творческий вымысел. Кубок — дело рук Дж. К. Роулинг, floatmap — моих.

Давайте напишем небольшой класс. Настолько небольшой, что изначально хотел делать вообще записью, но среди читателей оказывается немало почитателей Delphi 7, поэтому класс. Ничего не хочу сказать плохого про Delphi 7, сам сидел в ней долгое время. Когда-то соскочил в XE из-за работы, и понравилось, блин.

Класс всего лишь хранит вещественные значения и заменяет функции MakeGrayScaleBitmap и MakeBWBitmap, представленные выше.

Листинг 3: Класс TFloatMap

Много текста под спойлером скрыто

[свернуть]

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

Метод TFloatMap.Init

Получает на вход изображение AGraphic: TGraphic и тип преобразования в оттенки серого — AMode: TCalcGrayMode. Преобразованные данные хранятся внутри класса и доступны через указатель на начало массива в свойстве Data: PSingle.

Если используется режим пользовательской функции, необходимо ее передать в параметре ACalcFunc: TCalcGrayFunc.

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

Выбор нужной функции происходит очень просто — из массива. Если в константе EXCEPTION_GENERATE содержится TRUE при НЕуказанной функции будет сгенерировано исключение, иначе возьмется метод среднего арифметического.

Рис.4. Преобразование в оттенки серого работает очень быстро

Метод TFloatMap.ToBitmap

На вход получает желаемый формат AFormat: TPixelFormat и генерирует битмап в сохраненных оттенках серого. На самом деле результативная матрица имеет только 3 варианта формата: pf32bit, pf24bit и pf8bit. Потому что экзотика на 2 байта или 1-4 бита — это не нужно.

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

Метод TFloatMap.ToGrayScaleBitmap

Возвращает 24-битную матрицу в оттенках серого. Внутри вызова ToBitmap не происходит, чтобы сэкономить время выполнения.

Рис.5. А вот преобразование в оттенки серого и создание битмапа — не быстро

Метод TFloatMap.ToBWBitmap

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

На вход два параметра:

  • AThreshold: Single — пороговое значение
  • AInvert: Boolean — надо ли инвертировать.

Результаты и выводы

Рис.6. Интерфейс и метод HDTV для FloatMap

В итоге у нас есть такое приложение, интерфейс которого представлен на рис.6. Сравнивая с результатами аналогичного метода для Bitmap, видим, что FloatMap делает изображение в оттенках серого на 3-5 миллисекунд медленнее.

«Ну и зачем было тратить мое время?» — спросите Вы.

Не надо горячиться. Посмотрите на время получения черно-белого изображения. И суммарно получается 20.825 + 2.638 = 23.463. Это более чем на 10 миллисекунд быстрее, чем если бы делали через битмап. Напомню, там у нас получилось 34.967 миллисекунд.

Но и это не предел. Давайте еще разгоним. Скажем, до 11-12 миллисекунд. Заинтересовал? Читаем дальше, ставим звезды, комментируем.

Форсаж

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

Листинг 4: Сразу черно-белое изображение по цветному

В функцию грузим параметры и для оттенков серого, и для черно-белого изображения, и цветное изображение. Делаем выбор расчета яркости как в TFloatMap. Немного дублируем код. Просто не хочу добавлять ссылку на модуль с классом (IP76.FloatMap), а в модуле выносить описания в секцию interface. Пусть этот модуль (BWTools, в исходниках) для работы только с bitmap будет автономным, без зависимостей.

Получение черно-белого значения происходит очень просто:

Где calc — указатель на текущую функцию расчета. Получаем такое время.

Рис.7. Метод PAL и черно-белое изображение сразу по цветному (галка на Both at once)

Галка на Both at once отвечает за получение черно-белого изображения из цветного. То есть два-в-одном. Перевод в градации серого и определение по порогу происходит внутри одного цикла. Время сократилось на 10 миллисекунд. Крутняк!

Листинг 5: Получить черно-белое изображение FloatMap

Пишем аналогичный алгоритм для TFloatMap. Сразу спойлер — на самом деле пригодится другой, более быстрый метод, поэтому и помещаю код в спойлер. Сорян за каламбур.

Развернуть код на посмотреть

[свернуть]
Рис.8. Метод PAL для FloatMap. Если данные плохо видно — картинки кликабельны

Получилось 18.99 миллисекунд, что конечно лучше предыдущего показателя, но есть вопрос. Посмотрим на рисунок 4. Время на оттенки серого 8.692 и получение черно-белого 2.616. Ожидается 8.692 + 2.616 = 11.308.

Для начала именно так и поступим — вначале наполним данными FloatMap а потом получим из него черно-белое изображение. Потом проповедь.

Новых методов не пишем, используем то, что есть:

Image1 — цветное изображение Монро. Мы сделали ровно то, что описали выше и хотим получить в результате. Вначале инициализируем данными, а потом получаем на их основе результат.

Рис.9. Метод PAL для FloatMap. Галка на 1) Gray + 2) BW

Трудно поверить, но время даже меньше ожидаемого.

Итак, проповедь. Класс создавался для хранения данных с целью дальнейшего быстрого получения тех или иных результатов. Например ЧБ изображения. Когда класс применяется по назначению, все получается хорошо. Когда не по назначению, возникают косяки. Суть проповеди — топор использовать по назначению!

Что мы делали ранее: получали оттенок серого(долго), из него извлекали ЧБ, сохраняли в массив(долго), получали битмап, где округлялись вещественные значения из массива(очень долго).

Теперь у нас так: получили оттенок серого(долго), сохранили, получили битмап, внутри получения анализируем порог и либо 0, либо 255. Никаких округлений. Мега-шустро.

Давайте посмотрим как обстоят дела на большой картинке.

Рис.10. Картинка 6000 x 4000. Метод PAL. Bitmap 370 msec.
Рис.11. Картинка 6000 x 4000. Метод PAL. FloatMap 289 msec.

Разница почти в 100 миллисекунд. Это уже что-то да значит )

Что дальше

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

Видимо, уже когда-нибудь потом. И так три дня писал. Если есть интерес к этой теме, пишите в комментарии. Что больше интересует из перечисленного выше. Возможно, скорректирую планы и ускорюсь с выдачей )

Если есть желающие взять на себя часть тем, милости прошу. Сделаю авторизацию авторам и передохну )))


Скачать

Cпасибо за внимание!

Надеюсь, материал был полезен. Возможно, будет продолжение. Не пропустите, подписывайтесь на телегу.

Если есть вопросы, с удовольствием отвечу.

Если есть критика, многозначительно промолчу… Шутка. Критика очень приветствуется!


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

Исходники D7 (Delphi 7) 315 Кб

Исполняемый файл (zip) 1.23 Мб


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

1 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Андрей

мало что понял, но интересно

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