5.25. Поиск блобов

Пороговая обработка превращает захваченный кадр в бинарную маску: каждый пиксель либо проходит проверку порога, либо нет. Это отвечает на вопрос какие интересующие приложение цвета присутствуют в сцене, но не где – маска представляет собой просто море единиц и нулей. Следующий шаг – обнаружение блобов: обход маски, поиск непрерывных областей из проходящих пикселей и возврат каждой из них в виде объекта с позицией, размером, ориентацией и другими свойствами, на основе которых приложение может действовать.

find_blobs() – это рабочая лошадка для данного шага и наиболее распространённая точка входа в мир объектов-результатов модуля image. Отслеживание цветного мяча, движение вдоль линии, нарисованной на полу, подсчёт того, сколько ярких пятен видит тепловой датчик, определение того, включён синий светодиод или выключен – один и тот же вызов охватывает все эти задачи. Входные данные меняются (пороги, искомая область, фильтры, применяемые к результату), но шаблон вызова остаётся прежним.

5.25.1. Базовый вызов

find_blobs принимает список порогов и возвращает список объектов-результатов блобов:

thresholds = [(30, 100, 15, 127, 15, 127)]  # LAB threshold for red
blobs = img.find_blobs(thresholds)

for b in blobs:
    img.draw_rectangle(b.rect, color=(255, 0, 0))
    img.draw_cross(b.cx, b.cy, color=(255, 0, 0))

Каждый кортеж порога имеет ту же форму, что и пороги, передаваемые в binary() – шесть элементов (l_lo, l_hi, a_lo, a_hi, b_lo, b_hi) для изображения RGB565 (границы заданы в LAB) и два элемента (lo, hi) для изображения в оттенках серого. В одном вызове можно указать до 32 порогов, что и делает find_blobs() столь гибким: красные, зелёные и синие маяки можно отслеживать одновременно, каждый из них вносит свои блобы в возвращаемый список, а свойство code каждого блоба указывает, какому порогу он соответствует.

Вызовы draw_rectangle() и draw_cross(), приведённые выше, аннотируют захваченный кадр для предпросмотра в IDE. Результат блоба уже содержит b.rect (ограничивающую рамку в виде кортежа из 4 элементов), а также b.cx / b.cy (целочисленный центроид), поэтому отрисовка обнаружения обратно в кадр – это два вызова методов.

5.25.2. Что содержит результат

Каждый Blob представляет собой кортеж атрибутов, упаковывающий вместе всё, что детектор измерил об области. Свойства делятся на четыре группы.

Группа ограничивающая рамка и центроидx, y, w, h, rect, cx, cy, cxf, cyf – описывает положение блоба. rect – это кортеж из 4 элементов (x, y, w, h), который ожидают методы рисования; cx и cy – центроид в целочисленных пиксельных координатах; cxf и cyf – центроид в субпиксельных координатах с плавающей точкой, что полезно, когда вышестоящая калибровка учитывает дробные положения.

Группа дескрипторы формыpixels, area, density, perimeter, roundness, elongation, compactness, rotation – описывает, как выглядит блоб. pixels – количество проходящих пикселей; area – площадь ограничивающей рамки, выровненной по осям (w * h); density – отношение этих двух величин, которое приближается к 1.0 для сплошного прямоугольника и падает к 0.0 для тонкого диагонального штриха. roundness и compactness обе оценивают, насколько блоб круглый, но с разных геометрических точек зрения (roundness – по моментам второго порядка, compactness – по отношению периметра к площади); elongation для удобства равно 1.0 - roundness. rotation – ориентация главной оси в радианах, которая наиболее точна на вытянутых блобах и становится зашумлённой на почти круглых (у неоднозначной оси нет чётко определённого направления).

Группа метаданные порога и объединенияcode, count – указывает, какому порогу соответствует блоб и сколько исходных блобов было объединено в возвращаемый. code – 32-битная битовая карта, в которой установлен один бит на каждый совпадающий порог (один порог даёт code == 1; объединённый многоцветный блоб может иметь несколько установленных битов); count равно 1, если только merge=True не объединил несколько обнаружений в одно.

Группа углыcorners, min_corners – задаёт повёрнутую геометрию блоба. corners – кортеж из 4 элементов (x, y) крайних точек, извлечённых из контура блоба и отсортированных по часовой стрелке начиная с верхнего левого; min_corners – кортеж из 4 углов повёрнутого прямоугольника минимальной площади, охватывающего блоб. Прямоугольник минимальной площади – это плотная подгонка; выровненный по осям rect – свободная подгонка, выровненная по сетке пикселей. Оба варианта полезны в зависимости от того, нужна ли следующему этапу ориентированная рамка или обычная.

Иллюстрация обнаружения блоба на фоне бинарной пороговой маски. Левая панель показывает наклонённую овальную маску из проходящих пикселей. Правая панель показывает ту же маску с аннотацией: выровненная по осям ограничивающая рамка нарисована вокруг неё, центроид отмечен крестом в середине, пунктирный повёрнутый прямоугольник минимальной площади плотно облегает овал под его истинным углом, а линия главной оси проходит через центроид вдоль длинного направления овала.

Блоб несёт в себе выровненную по осям ограничивающую рамку (rect, x, y, w, h), центроид (cx, cy или субпиксельные cxf, cyf), повёрнутый прямоугольник минимальной площади (min_corners плюс rotation), а также необязательные линии главной / малой оси, вычисляемые с помощью вспомогательных функций уровня модуля, описанных ниже.

5.25.4. Объединение перекрывающихся блобов

merge=True выполняет постобработку списка результатов для объединения блобов, ограничивающие прямоугольники которых перекрываются. Естественное применение – обнаружение цели, цвет которой камера видит как несколько пороговых областей из-за зеркальных бликов, теневых линий или несогласованного освещения по объекту: один красный мяч может вернуться в виде трёх-четырёх небольших красных блобов, которые вместе очерчивают мяч. При merge=True три блоба становятся одним большим, rect покрывает их объединение, code представляет собой побитовое ИЛИ кодов объединённых блобов (так что многоцветное объединение указывает, какие цвета внесли вклад), а count сообщает, сколько исходных блобов было объединено.

margin увеличивает или уменьшает ограничивающие прямоугольники перед проверкой перекрытия. При margin=2 блобы, ограничивающие прямоугольники которых находятся в пределах 2 пикселей друг от друга, всё равно объединяются; при margin=-2 объединяются только блобы, ограничивающие прямоугольники которых перекрываются как минимум на 2 пикселя. Естественная настройка: положительный отступ для обработки блобов, которые порог разбил на соседние части; отрицательный отступ, чтобы держать раздельно тесно сгруппированные различные объекты.

merge_cb выполняется для каждой пары-кандидата перед тем, как происходит объединение. Функция обратного вызова получает два блоба и возвращает True, чтобы разрешить объединение, или False, чтобы предотвратить его. Это правильный инструмент для перекрёстной проверки объединений, которые пропускает геометрическое правило – например, отказ от объединения двух блобов, чьи углы rotation расходятся более чем на порог, или отказ от объединения малого блоба в гораздо больший, если малый – это просто крапинка.

5.25.5. Проекционные гистограммы

x_hist_bins_max и y_hist_bins_max прикрепляют необязательные проекционные гистограммы к каждому блобу. Проекционная гистограмма – это подсчёт проходящих пикселей вдоль одной оси: гистограмма по оси X суммирует проходящие пиксели по столбцам внутри ограничивающей рамки блоба, а гистограмма по оси Y суммирует их по строкам. Оба значения по умолчанию равны нулю – гистограммы не вычисляются, пока не указан ненулевой max, поскольку иначе они добавляли бы работу к каждому обнаружению.

Когда они вычислены, гистограммы предоставляют дешёвый одномерный сигнал, над которым приложение может выполнять дальнейший анализ: обнаружение положения вертикальной полосы внутри блоба, поиск точки разрыва двухцветной цели, подсчёт того, сколько пропусков появляется вдоль длинной оси. Они заполняются как свойства x_hist_bins и y_hist_bins каждого Blob.

5.25.6. Дополнительные геометрические помощники

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

  • image.get_solidity() возвращает плотность заполнения блоба – пиксели, делённые на площадь выпуклой оболочки. Сплошная заполненная область близка к 1.0; блоб с вогнутостями (подкова, рука с расставленными пальцами) опускается значительно ниже.

  • image.get_convexity() возвращает выпуклость – периметр выпуклой оболочки, делённый на периметр блоба. Идеально выпуклый блоб равен 1.0; зазубренные или с вырезами блобы дают меньше.

  • image.get_major_axis_line() и image.get_minor_axis_line() возвращают объекты Line вдоль главной и малой осей блоба, выведенные из повёрнутого прямоугольника минимальной площади.

  • image.get_enclosing_circle() возвращает Circle, охватывающий блоб – полезно, когда следующему этапу нужна окружность для рисования или проверки.

  • image.get_enclosed_ellipse() возвращает кортеж из 5 элементов (cx, cy, rx, ry, rotation) для эллипса, вписанного в прямоугольник минимальной площади блоба. Эти значения напрямую передаются в draw_ellipse().

5.25.7. Автообучение порога

Детектор блобов хорош ровно настолько, насколько хороши пороги, с которыми он запускается, а работа по поиску правильного порога для цветовой цели – это отдельная задача. Два распространённых подхода сокращают эту работу.

Первый – это интерактивный выбор в IDE: захватите кадр, перетащите прямоугольник вокруг примера целевого цвета и позвольте редактору порогов IDE сообщить границы LAB, которые он видит. Эти границы вставляются в скрипт как пороги find_blobs(), и детектор готов.

Второй – это программное автообучение: процедура калибровки, работающая на камере, захватывает кадр, берёт гистограмму известного участка, где находится цель (get_histogram() с roi=), и считывает диапазон значений участка с гистограммы с помощью get_percentile(). 5-й процентиль задаёт нижнюю границу каждого канала, а 95-й – верхнюю, игнорируя случайные выбросы пикселей на обоих концах. На изображении RGB565 один вызов процентиля сообщает все три канала LAB сразу, поэтому два вызова дают шесть чисел, которые ожидает find_blobs():

h = img.get_histogram(roi=patch)
lo = h.get_percentile(0.05)
hi = h.get_percentile(0.95)
threshold = (lo.l_value, hi.l_value,
             lo.a_value, hi.a_value,
             lo.b_value, hi.b_value)