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

Введение
Компьютерное зрение — обширная область анализа изображений и видео. Хотя многие, услышав о машинном зрении, в первую очередь думают о моделях машинного обучения, на самом деле существует гораздо больше алгоритмов, которые в некоторых случаях работают лучше, чем искусственный интеллект!
В компьютерном зрении область обнаружения признаков включает в себя определение отдельных интересующих областей на изображении. Эти результаты затем могут быть использованы для создания дескрипторов признаков — числовых векторов, представляющих локальные области изображения. После этого дескрипторы признаков нескольких фотографий одной сцены можно объединить для сопоставления изображений или даже реконструкции сцены.
В этой статье мы проведём аналогию из математического анализа, чтобы представить производные и градиенты изображений. Нам необходимо будет понять логику, лежащую в основе свёрточного ядра и, в частности, оператора Собеля — фильтра компьютерного зрения, используемого для обнаружения границ на изображении.
Интенсивность изображения
Интенсивность — одна из основных характеристик изображения. Каждый пиксель изображения имеет три компонента: R (красный), G (зелёный) и B (синий), принимающие значения от 0 до 255. Чем выше значение, тем ярче пиксель. Интенсивность пикселя — это средневзвешенное значение его компонентов R, G и B.
На самом деле существует несколько стандартов, определяющих различные веса. Поскольку мы собираемся сосредоточиться на OpenCV, мы будем использовать их формулу, которая приведена ниже:

изображение = cv2.imread('image.png') B, G, R = cv2.split(изображение) изображение_в_оттенках_серого = 0,299 * R + 0,587 * G + 0,114 * B изображение_в_оттенках_серого = np.clip(изображение_в_оттенках_серого, 0, 255).astype('uint8') интенсивность = изображение_в_оттенках_серого.mean() печать(f»Интенсивность_изображения: {intensity:2f}»)
Изображения в оттенках серого
Изображения могут быть представлены с использованием различных цветовых каналов. Если каналы RGB представляют собой исходное изображение, применение приведённой выше формулы интенсивности преобразует его в формат оттенков серого, содержащий только один канал.
Поскольку сумма весов в формуле равна 1, изображение в градациях серого будет содержать значения интенсивности от 0 до 255, как и каналы RGB.

В OpenCV каналы RGB можно преобразовать в формат оттенков серого с помощью функции cv2.cvtColor(), что является более простым способом, чем метод, который мы только что рассмотрели выше.
image = cv2.imread('image.png') grayscale_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) intensity = grayscale_image.mean() print(f»Интенсивность изображения: {intensity:2f}»)
Вместо стандартной палитры RGB OpenCV использует палитру BGR. Они идентичны, за исключением того, что элементы R и B меняются местами. Для простоты в этой и следующих статьях серии мы будем использовать термины RGB и BGR как взаимозаменяемые.
При расчёте интенсивности изображения обоими методами в OpenCV результаты могут немного различаться. Это совершенно нормально, поскольку при использовании функции cv2.cvtColor OpenCV округляет преобразованные пиксели до ближайших целых чисел. Вычисление среднего значения даст небольшую разницу.
Производное изображения
Производные изображения используются для измерения скорости изменения интенсивности пикселей на изображении. Изображения можно рассматривать как функцию двух аргументов: I(x, y), где x и y определяют положение пикселя, а I — интенсивность этого пикселя.
Мы могли бы записать формально:

Однако, учитывая тот факт, что изображения существуют в дискретном пространстве, их производные обычно аппроксимируются посредством сверточных ядер:
- Для горизонтальной оси X: [-1, 0, 1]
- Для вертикальной оси Y: [-1, 0, 1]ᵀ
Другими словами, мы можем переписать приведенные выше уравнения в следующем виде:

Чтобы лучше понять логику работы ядер, давайте обратимся к примеру ниже.
Пример
Предположим, у нас есть матрица размером 5×5 пикселей, представляющая фрагмент изображения в оттенках серого. Элементы этой матрицы показывают интенсивность пикселей.

Для вычисления производной изображения можно использовать свёрточные ядра. Идея проста: взяв пиксель изображения и несколько пикселей в его окрестности, мы находим сумму поэлементного умножения с заданным ядром, представляющим фиксированную матрицу (или вектор).
В нашем случае мы будем использовать трёхэлементный вектор [-1, 0, 1]. Из приведенного выше примера возьмём пиксель в позиции (1, 1) со значением, например, -3.
Поскольку размер ядра (выделено жёлтым) равен 3×1, нам потребуется, чтобы левый и правый элементы -3 соответствовали этому размеру, поэтому мы берём вектор [4, -3, 2]. Затем, суммируя поэлементное произведение, получаем значение -2:

Значение -2 представляет собой производную для исходного пикселя. Если внимательно присмотреться, можно заметить, что производная пикселя -3 — это всего лишь разность между самым правым пикселем (2) -3 и самым левым пикселем (4).
Зачем использовать сложные формулы, если можно вычислить разность между двумя элементами? Действительно, в этом примере мы могли бы просто вычислить разность интенсивностей между элементами I(x, y + 1) и I(x, y – 1). Но в реальности мы можем справиться и с более сложными сценариями, когда нам нужно обнаружить более сложные и менее очевидные признаки. По этой причине удобно использовать обобщение ядер, матрицы которых уже известны, для обнаружения предопределённых типов признаков.
На основании значения производной можно сделать некоторые наблюдения:
- Если значение производной в данной области изображения значимо, это означает, что интенсивность там резко меняется. В противном случае заметных изменений яркости не наблюдается.
- Если значение производной положительно, то это означает, что слева направо область изображения становится ярче; если отрицательно, то область изображения становится темнее в направлении слева направо.
Проводя аналогию с линейной алгеброй, ядра можно рассматривать как линейные операторы на изображениях, которые преобразуют локальные области изображения.
Аналогично мы можем вычислить свёртку с вертикальным ядром. Процедура останется той же, за исключением того, что теперь мы перемещаем наше окно (ядро) вертикально по матрице изображения.

Вы можете заметить, что после применения фильтра свёртки к исходному изображению размером 5×5 оно стало размером 3×3. Это нормально, поскольку мы не можем применить свёртку таким же образом к пикселям на краях (иначе мы выйдем за границы).
Для сохранения размерности изображения обычно используется метод заполнения, заключающийся во временном расширении/интерполяции границ изображения или заполнении их нулями, благодаря чему свертку можно рассчитать и для граничных пикселей.
По умолчанию библиотеки, такие как OpenCV, автоматически заполняют границы, чтобы гарантировать одинаковую размерность входных и выходных изображений.
Градиент изображения
Градиент изображения показывает, насколько быстро изменяется интенсивность (яркость) в данном пикселе в обоих направлениях (X и Y).

Формально градиент изображения можно записать как вектор производных изображения по осям X и Y.
Величина градиента
Величина градиента представляет собой норму вектора градиента и может быть найдена по следующей формуле:

Градиентная ориентация
Используя найденные Gx и Gy, можно также вычислить угол вектора градиента:

Пример
Давайте рассмотрим, как вручную вычислить градиенты на основе приведённого выше примера. Для этого нам понадобятся вычисленные матрицы 3×3 после применения ядра свёртки.
Если мы возьмем верхний левый пиксель, он имеет значения Gₓ = -2 и Gᵧ = 11. Мы можем легко вычислить величину и ориентацию градиента:

Для всей матрицы 3×3 получаем следующую визуализацию градиентов:

На практике рекомендуется нормализовать ядра перед применением их к матрицам. Мы не стали этого делать ради простоты примера.
Оператор Собеля
Изучив основы производных и градиентов изображений, теперь пора перейти к оператору Собеля, который используется для их аппроксимации. В отличие от предыдущих ядер размером 3×1 и 1×3, оператор Собеля определяется парой ядер размером 3×3 (для обеих осей):

Это даёт оператору Собеля преимущество, поскольку ранее измеряемые ядра измеряли только одномерные изменения, игнорируя другие строки и столбцы в окрестности. Оператор Собеля учитывает больше информации о локальных регионах.
Ещё одно преимущество заключается в том, что алгоритм Собеля более устойчив к шуму. Рассмотрим фрагмент изображения ниже. Если вычислить производную вокруг красного элемента в центре, который находится на границе между тёмными (2) и светлыми (7) пикселями, то получим 5. Проблема в том, что есть шумный пиксель со значением 10.

Если применить горизонтальное одномерное ядро вблизи красного элемента, оно придаст значительную значимость пикселю со значением 10, который является явным выбросом. В то же время оператор Собеля более надёжен: он учитывает значение 10, а также пиксели со значением 7 вокруг него. В некотором смысле оператор Собеля применяет сглаживание.
При одновременном сравнении нескольких ядер рекомендуется нормализовать ядра матрицы, чтобы обеспечить их одинаковый масштаб. Одним из наиболее распространённых применений операторов в анализе изображений является обнаружение признаков.
В случае операторов Собеля и Шарра они обычно используются для обнаружения краев — зон, где интенсивность пикселей (и ее градиент) резко меняется.
OpenCV
Для применения операторов Собеля достаточно использовать функцию OpenCV cv2.Sobel. Рассмотрим её параметры:
производная_x = cv2.Собель(изображение, cv2.CV_64F, 1, 0) производная_y = cv2.Собель(изображение, cv2.CV_64F, 0, 1)
- Первый параметр — это входное изображение NumPy.
- Второй параметр (cv2.CV_64F) — это глубина данных выходного изображения. Проблема в том, что операторы, как правило, могут создавать выходные изображения, содержащие значения, выходящие за пределы диапазона 0–255. Поэтому нам необходимо указать тип пикселей, которые мы хотим получить в выходном изображении.
- Третий и четвёртый параметры представляют порядок производной по осям x и y соответственно. В нашем случае нам нужна только первая производная по осям x и y, поэтому мы передаём значения (1, 0) и (0, 1).
Давайте рассмотрим следующий пример, где нам дано входное изображение судоку:

Применим фильтр Собеля:
import cv2 import matplotlib.pyplot as plt image = cv2.imread(«data/input/sudoku.png») image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) derived_x = cv2.Scharr(image, cv2.CV_64F, 1, 0) derived_y = cv2.Scharr(image, cv2.CV_64F, 0, 1) derived_combined = cv2.addWeighted(derivative_x, 0.5, derived_y, 0.5, 0) min_value = min(derivative_x.min(), derived_y.min(), derived_combined.min()) max_value = max(derivative_x.max(), derived_y.max(), derived_combined.max()) print(f»Диапазон значений: ({min_value:.2f}, {max_value:.2f})») fig, axes = plt.subplots(1, 3, figsize=(16, 6), constrained_layout=True) axes[0].imshow(derivative_x, cmap='gray', vmin=min_value, vmax=max_value) axes[0].set_title(«Горизонтальная производная») axes[0].axis('off') image_1 = axes[1].imshow(derivative_y, cmap='gray', vmin=min_value, vmax=max_value) axes[1].set_title(«Вертикальная производная») axes[1].axis('off') image_2 = axes[2].imshow(derivative_combined, cmap='gray', vmin=min_value, vmax=max_value) axes[2].set_title(«Комбинированная производная») axes[2].axis('off') color_bar = fig.colorbar(image_2, ax=axes.ravel().tolist(), orientation='vertical', fraction=0.025, pad=0.04) plt.savefig(«data/output/sudoku.png») plt.show()
В результате мы видим, что горизонтальные и вертикальные производные очень хорошо распознают линии! Более того, сочетание этих линий позволяет нам обнаружить оба типа объектов:

Оператор Шарра
Другой популярной альтернативой ядру Sober является оператор Шарра:

Несмотря на существенное сходство со структурой оператора Собеля, ядро Шарра обеспечивает более высокую точность в задачах выделения контуров. Оно обладает рядом важных математических свойств, которые мы не будем рассматривать в этой статье.
OpenCV
Использование фильтра Шарра в OpenCV очень похоже на то, что мы видели выше с фильтром Собеля. Разница лишь в названии метода (остальные параметры те же):
производная_x = cv2.Scharr(изображение, cv2.CV_64F, 1, 0) производная_y = cv2.Scharr(изображение, cv2.CV_64F, 0, 1)
Вот результат, который мы получаем с помощью фильтра Шарра:

В данном случае сложно заметить разницу в результатах для обоих операторов. Однако, взглянув на цветовую карту, можно увидеть, что диапазон возможных значений, получаемых оператором Шарра, значительно шире (-800, +800), чем для оператора Собеля (-200, +200). Это нормально, поскольку ядро Шарра имеет большие константы.
Это также хороший пример того, почему нам нужно использовать специальный тип cv2.CV_64F. В противном случае значения были бы ограничены стандартным диапазоном от 0 до 255, и мы бы потеряли ценную информацию о градиентах.
Примечание. Применение методов сохранения непосредственно к изображениям cv2.CV_64F приведёт к ошибке. Чтобы сохранить такие изображения на диске, их необходимо преобразовать в другой формат и оставить только значения от 0 до 255.
Заключение
Применяя основы математического анализа к компьютерному зрению, мы изучили важнейшие свойства изображений, позволяющие обнаруживать пики интенсивности. Эти знания полезны, поскольку обнаружение особенностей — распространённая задача анализа изображений, особенно в условиях ограничений на обработку изображений или когда алгоритмы машинного обучения не используются.
Мы также рассмотрели пример использования OpenCV, чтобы понять, как работает обнаружение рёбер с операторами Собеля и Шарра. В следующих статьях мы изучим более сложные алгоритмы обнаружения объектов и рассмотрим примеры OpenCV.
Ресурсы
- Преобразования цветов | OpenCV
- Производные Собеля | OpenCV
- Градиенты изображений | OpenCV
- Оператор Собеля | Википедия
Все изображения, если не указано иное, принадлежат автору.
Источник: towardsdatascience.com


























