Сила оттенков серого: компьютерное зрение с нуля |
||
|
МЕНЮ Главная страница Поиск Регистрация на сайте Помощь проекту Архив новостей ТЕМЫ Новости ИИ Голосовой помощник Разработка ИИГородские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Искусственный интеллект Слежка за людьми Угроза ИИ Атаки на ИИ Внедрение ИИИИ теория Компьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Нейронные сети начинающим Психология ИИ Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Промпты. Генеративные запросы Распознавание лиц Распознавание образов Распознавание речи Творчество ИИ Техническое зрение Чат-боты Авторизация |
2025-11-16 11:59 В обсуждениях компьютерного зрения обычно речь идёт об OpenCV или нейронных сетях глубокого обучения наподобие YOLO. Однако в большинстве случаев для работы с компьютерным зрением требуется понимание базовых алгоритмов, чтобы можно было адаптировать их под свои нужды. Мне захотелось понять, насколько далеко я смогу зайти, оставив в computer vision только самый минимум: одни лишь 8-битные изображения в градациях серого; никаких сложных структур данных, старый добрый C, немного байтовых массивов и единственный файл заголовка. В конце концов, изображение — это ведь просто прямоугольник из чисел, не так ли? Этот пост — экскурсия по алгоритмам, лежащим в основе Grayskull — минималистичной библиотеки компьютерного зрения, спроектированной для устройств с ограниченными ресурсами. Пиксели Пиксель в градациях серого обычно представлен одним байтом: По сути, изображение в градациях серого — это 2D-массив таких пикселей, задаваемый шириной и высотой, однако на языках с более простой структурой памяти наподобие C он часто представляется в виде 1D-массива размером Этот скромный фундамент уже позволяет нам выполнять определённые трюки, например, инвертировать или отзеркаливать изображения: Можно выполнять наивное изменение размера по ближайшим соседям, результат будет быстрым, но слишком грубым, или же выполнять билинейную интерполяцию, что медленнее и требует операций с плавающей запятой, зато часто выглядит красивее: ![]() Так исходное изображение (слева) выглядит после билинейного изменения размера (посередине) и изменения размера по ближайшим соседям (справа). Обработка изображений Теперь, когда мы можем манипулировать отдельными пикселями, можно приступать к более серьёзной обработке изображений. Полезным инструментом здесь будут свёрточные фильтры. Фильтр — это маленький 2D-массив (ядро), применяемый к каждому пикселю изображения. Новое значение пикселя вычисляется как взвешенная сумма соседних пикселей, где веса определяются ядром. Эту методику можно использовать для размытия, повышения резкости, распознавания краёв и многих других эффектов. Вот примеры одних из самых распространённых ядер. Учтите, что они определяются, как 8-битные integer со знаком: Аналогично, можно применить фильтры Собеля, которые полезны, если мы хотим распознавать на изображениях края: Вот примеры таких фильтров; обратите внимание, как некоторые из них устраняют шум или выделяют края: ![]() Первое изображение — это оригинал, далее идут box filter и фильтр гауссова размытия. Затем идёт фильтр повышения резкости, фильтр тиснения и, наконец, фильтр Собеля, выделяющий края. Пороговые значения Чтобы «видеть» объекты на изображении, нам нужно разделить его на передний и задний планы, а затем работать с передними сегментами, пытаясь найти интересующие нас объекты. Гораздо проще это делать, если каждый пиксель полностью чёрный или полностью белый. Такое преобразование из градаций серого в чёрно-белые данные называется thresholding (бинаризацией). В простейшем случае мы можем считать все пиксели со значениями больше 127 белыми, а ниже — чёрными. Это thresholding с фиксированным уровнем. Разумеется, если изображение тёмное, то такое значение thresholding окажется слишком высоким и многие значимые детали будут утеряны. Как же выбрать пороговое значение точнее? Одно из решений заключается в вычислении распределения яркости как гистограммы. Мы знаем, что на изображении в градациях серого есть 255 уникальных значений, поэтому можем итеративно обойти все пиксели и посчитать, какие из них имеют то или иное значение. Проанализировав получившуюся гистограмму, мы получим представление, какое значение пикселя лучше всего подойдёт в качестве порогового. Это можно сделать при помощи способа Оцу. Он автоматически определяет оптимальное пороговое значение, проверяя каждое возможное (от 0 до 255). Для каждого значения он разбивает пиксели изображения на два класса (фон и передний план), а затем вычисляет их межклассовую дисперсию. Пороговым значением, максимизирующим эту дисперсию, будет то, которое создаёт наилучшее разделение между двумя классами. Такой способ довольно неплохо работает для изображений с хорошей контрастностью: Однако в реальной жизни условия освещения часто неравномерны. В таких случаях единое глобальное пороговое значение может не подойти, и ни одно из потенциальных 255 пороговых значений не обеспечит хорошего результата. Для решения этой проблемы можно использовать адаптивный thresholding. Мы не будем использовать одно пороговое значение для всего изображения, а станем вычислять локальный порог для каждого пикселя на основании средней яркости соседних пикселей. Благодаря этому можно лучше учитывать разные условия освещения в изображении: Посмотрим, как разные способы вычисления thresholding выглядят на одном и том же изображении: ![]() Первое изображение — оригинал, за ним идут thresholding с фиксированным уровнем (80), способ Оцу и адаптивный thresholding. Обратите внимание, что адаптивный thresholding сохраняет больше деталей и в ярких, и в тёмных областях изображения, а у способа Оцу возникают проблемы с неравномерным освещением. Морфологические операции Из-за особенностей работы датчиков изображения в камерах снимки часто содержат шум. Это означает, что после thresholding в изображении будут встречаться случайные отдельные пиксели, не относящиеся к какому-то объекту, небольшие отверстия в объектах или небольшие разрывы между частями объектов. Всё это сбивает с толку алгоритмы распознавания объектов, однако очистке двоичного изображения могут способствовать морфологические операции. Две наиболее распространённые операции: это erosion (морфологическое сужение) и dilation (расширение). Сужение убирает пиксели на границах объектов (уменьшая их размеры), а расширение добавляет пиксели к их границам (увеличивая объекты). Также существует opening (erosion с последующим dilation) и closing (dilation с последующим erosion). Opening полезно для удаления маленьких объектов или шума, а closing — для заполнения небольших отверстий в объектах. Вот пример того, как морфологические операции могут подчистить довольно непонятное изображение с метками ArUco: ![]() Первое изображение — оригинал, за которым следует изображение с thresholding (способ Оцу). Далее идут erosion с последующим dilation. Маркеры чёрные, поэтому это может показаться странным, но так как морфологические операции работают с белыми пикселями, мы, по сути, выполняем closing, но для чёрных пикселей. Также можно предварительно инвертировать изображение, а затем выполнять opening и обратное инвертирование. В конце все маркеры уменьшаются до их исходных размеров и все их легко можно распознать по форме и размеру. Пятна и контуры Получив чистое двоичное изображение, можно приступать к распознаванию объектов на нём. Классический способ решения этой задачи — нахождение соединённых компонентов (пятен, blob). Blob — группа соединённых белых пикселей (255), образующих объект. Для разметки связанных пикселей проще всего находить blob при помощи алгоритма заливки (flood-fill) или поиска в глубину (depth-first search, DFS): Разумеется, при обработке большинства реальных изображений это мгновенно увеличит стек до огромных величин. Поэтому предпочтителен итеративный подход с очередью или стеком. Однако это всё равно не самый оптимальный способ поиска blob. Более эффективный способ — это алгоритм, состоящий из двух проходов, сканирующий изображение дважды: сначала он присваивает предварительные метки и записывает соответствия между ними, а затем выполняет второе сканирование для ресолвинга этих соответствий и присвоения каждому blob окончательной уникальной метки. Будет здорово, если мы сможем использовать один и тот же тип Прежде чем двигаться дальше, давайте поговорим о связанности. Существует два распространённых типа связанности пикселей: 4-connectivity и 8-connectivity. В 4-connectivity пиксель соединён с четырьмя непосредственными соседями (сверху, снизу, слева, справа). В 8-connectivity пиксель связан со всеми восемью окружающими его пикселями (те же, плюс по диагонали). То есть, в случае 8-connectivity показанный ниже пример будет одним blob, а в случае 4-connectivity — двумя отдельными blob: В нашей реализации для простоты будет использоваться 4-connectivity. Рассмотрим следующее изображение: Начнём сканировать его строка за строкой. Если найден белый пиксель, проверяем соседей слева и справа:
После первого прохода массив меток будет выглядеть так: Таблица соответствия будет выглядеть так: Во втором проходе мы разрешаем соответствия и присваиваем каждому пикселю окончательные метки. Массив окончательных меток будет выглядеть так: На втором проходе также можно вычислять такие свойства blob, как площадь, ограничивающий прямоугольник и центр масс. Площадь самого большого blob составляет 14 пикселей, его ограничивающий прямоугольник имеет координаты от (1,1) до (6,3). Центр масс вычисляется как среднее координат всех пикселей blob, в нашем случае по оси X это будет Эти геометрические свойства уже дают нам довольно много информации о blob. Например, мы можем фильтровать blob по площади, чтобы удалять небольшие пятна шума. Также можно вычислять соотношение сторон (ширина/высота) ограничивающего прямоугольника, чтобы отфильтровывать очень тонкие или очень широкие blob. Соотношение между реальной площадью и ограничивающим прямоугольником blob позволяет отфильтровывать недостаточно компактные blob. У прямоугольников соотношение близко к Другими подсказками будут позиция центра масс, ориентация с использованием моментов или форма контура. Существует достаточно простой способ трассировки контура blob при помощи алгоритма трассировки окрестности Мура. Он начинает с известного пикселя границы и следует по контуру по часовой стрелке, проверяя соседние пиксели, пока не вернётся в начальный: Таким образом можно получать контуры всех blob и анализировать их формы или сравнивать длины контуров (периметр) с площадью blob. Также можно аппроксимировать контуры прямыми отрезками при помощи алгоритма Рамера — Дугласа — Пекера, заменяющего кривую последовательностью отрезков прямых с сохранением общей формы. Всё это здорово, если мы хотим распознавать простые объекты на статическом изображении. Но что если нам нужно распознавать или отслеживать более сложные объекты, такие как лица, автомобили или пешеходы? Ключевые точки и дескрипторы Ключевая точка (keypoint) — это определённый участок изображения, уникальный и надёжно распознаваемый вне зависимости от масштаба, поворота и освещения объекта. На практике, ключевые точки часто оказываются углами (также называемыми «признаками», feature). Один из самых интуитивно понятных алгоритмов распознавания признаков — это FAST (Features from Accelerated Segment Test). Он исследует окружность из 16 пикселей вокруг пикселя-кандидата (в 4 пикселях от него). Если как минимум 9 смежных пикселей в этой окружности Этот алгоритм прост, однако на реальных изображениях находит слишком много признаков. Решить эту проблему можно, записывая «оценку» (score) каждого признака и сохраняя только точки с наибольшей оценкой. Оценку можно вычислить как сумму абсолютных разностей между пикселем-кандидатом и смежными пикселями в окружности или как минимальную разность между центральным пикселем и пикселями на окружности. ![]() Обратите внимание, как на фотографии кота распознаются ключевые точки в углах и таких уникальных признаках, как глаза, нос, усы и так далее. Но как использовать ключевые точки для распознавания объектов? Здесь на помощь приходит ORB. Он надстраивается поверх того же распознавателя углов FAST, пытается находить резкие изменения яркости, но также добавляет ещё два компонента: ориентацию и дескриптор. После нахождения углов ORB определяет их ориентацию, вычисляя моменты изображения на небольшом участке вокруг каждой ключевой точки. Благодаря этому у каждой ключевой точки появляется угол, по сути, сообщающий, где у неё «верх». Дескриптор — это компактное описание локального участка изображения вокруг ключевой точки, инвариантное относительно масштаба и поворота. В ORB используется модифицированная версия дескриптора BRIEF (Binary Robust Independent Elementary Features), позволяющая хитрым образом кодировать участок изображения в виде небольшой битовой строки. Дескриптор просто сравнивает яркости битов, и если один пиксель светлее другого, то соответствующий бит получает значение Дескриптор имеет длину 256 бит; для каждого из 256 «сэмплов» мы при помощи псевдослучайной таблицы поиска выбираем две точки сэмплирования внутри площади участка и кодируем следующий бит, как Сравнение ключевых точек становится тривиальной задачей, достаточно просто выполнить XOR двух битовых строк и посчитать биты. Так как ключевые точки не зависят от поворота и условий освещения, можно использовать их для распознавания объектов в различных сценариях. Последним дополнением этого алгоритма станет многократное изменение размера/масштаба изображения и распознавание ключевых точек в разных масштабах. Благодаря этому можно распознавать объекты, которые ближе или дальше от камеры, повёрнуты на любой угол или частично скрыты. ![]() LBP-каскады Ключевые точки и дескрипторы отлично подходят для распознавания произвольных объектов, но иногда нам нужен более специализированное решение для определённых типов объектов, например, лиц, транспорта или жестов. Тут нам приходят на помощь каскадные классификаторы, применяемые, например, в методе Виолы — Джонса. Вместо сложных признаков Хаара, применяемых в алгоритме Виолы — Джонса, можно использовать нечто более простое: локальные бинарные шаблоны (Local Binary Patterns, LBP). LBP — это мощный дескриптор текстур. Он исследует 8 соседей каждого пикселя, и если сосед ярче центрального пикселя, записывает «Каскад» — это последовательность простых классификаторов, или «этапов». На каждом из этапов исследуется окно изображения и применяется несколько признаков LBP для определения того, может ли это окно содержать интересующий объект (например, лицо).
Такая структура позволяет классификатору быстро отбрасывать подавляющую часть площади изображения и сосредоточить вычислительные ресурсы только на областях с высоким потенциалом. Перемещая это окно распознавания по всему изображению (и во множестве разных масштабов), можно находить объекты конкретного заранее обученного класса. В Grayskull есть предварительно обученный детектор лиц анфас, использующий именно эту методику. ![]() Выше показан результат работы LBP-каскадного классификатора, успешно распознавшего лицо сэра Гэри Олдмена во всём разнообразии его ролей. Слева выбрано минимальное количество соседей, равное 4, а справа — 14. Благодаря этому слева распознаётся больше лиц, но и возникает больше ложноположительных срабатываний, а справа выполняется более консервативное распознавание, обнаруживающее только полностью видимые лица анфас. Заключение Мы проделали путь от скромного пикселя до сложного распознавания объектов, использовав для всего этого лишь простые структуры C и фундаментальные алгоритмы. Мы попробовали манипулировать пикселями, применять фильтры для обработки изображений, сегментировать объекты при помощи thresholding и подчищать их. Также мы научились находить и анализировать blob, распознавать устойчивые ключевые точки при помощи FAST и ORB, и, наконец, использовали LBP-каскады для распознавания специализированных объектов. В этом и заключается фундаментальная философия Grayskull: срывание покрова таинственности с компьютерного зрения созданием минимального, свободного от зависимостей и понятного набора инструментов. Она доказывает, что для получения приемлемых результатов не всегда нужны тяжеловесные библиотеки или фреймворки глубокого обучения, особенно в системах с ограниченными ресурсами. По сути, изображения — это просто прямоугольники из чисел, поэтому благодаря основам знаний алгоритмов можно научить компьютер видеть их. Как всегда, я рекомендую вам изучить репозиторий, экспериментировать с кодом и, может быть, даже попробовать собрать собственный простой проект CV! Источник: habr.com Комментарии: |
|