Интенсивность изображения
Компьютерное зрение представляет собой обширную сферу, посвященную анализу изображений и видео. Хотя многие ассоциируют компьютерное зрение в первую очередь с моделями машинного обучения, на деле существует множество других алгоритмов, которые в определенных ситуациях демонстрируют превосходство над искусственным интеллектом!
В компьютерном зрении обнаружение признаков подразумевает выделение уникальных областей интереса на изображении. Эти данные затем применяются для формирования дескрипторов признаков — числовых векторов, описывающих локальные участки изображения. Далее дескрипторы признаков с нескольких снимков одной сцены объединяются для сопоставления изображений или даже восстановления сцены.
В этой публикации мы проведем аналогию с исчислением, чтобы разобраться в производных изображений и градиентах. Это понимание окажется ключевым для осмысления принципов работы конволюционного ядра, особенно оператора Собеля — фильтра компьютерного зрения, предназначенного для выявления краев на изображении.
Интенсивность изображения
Интенсивность изображения является одной из ключевых характеристик. Каждый пиксель содержит три компонента: красный (R), зеленый (G) и синий (B), значения которых варьируются от 0 до 255. Чем выше значение, тем ярче пиксель. Интенсивность пикселя рассчитывается как взвешенное среднее его компонентов R, G и B.
Существует несколько стандартов с разными весами. Поскольку мы ориентируемся на OpenCV, применим их формулу, приведенную ниже.
image = cv2.imread('image.png') B, G, R = cv2.split(image) grayscale_image = 0.299 * R + 0.587 * G + 0.114 * B grayscale_image = np.clip(grayscale_image, 0, 255).astype('uint8') intensity = grayscale_image.mean() print(f"Image intensity: {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"Image intensity: {intensity:2f}")Вместо стандартной палитры RGB OpenCV использует BGR. Они идентичны, за исключением перестановки R и B. Для удобства в этой и последующих статьях серии мы будем использовать термины RGB и BGR как синонимы.
При вычислении интенсивности изображения обоими методами в OpenCV возможны незначительные расхождения. Это нормально, поскольку функция cv2.cvtColor округляет преобразованные пиксели до ближайших целых чисел, что приводит к малым отличиям в среднем значении.
Производная изображения
Производные изображений служат для оценки скорости изменения интенсивности пикселей по всему изображению. Изображение можно рассматривать как функцию двух переменных 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) и левым (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. Вот ее параметры:
derivative_x = cv2.Sobel(image, cv2.CV_64F, 1, 0) derivative_y = cv2.Sobel(image, 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) derivative_x = cv2.Scharr(image, cv2.CV_64F, 1, 0) derivative_y = cv2.Scharr(image, cv2.CV_64F, 0, 1) derivative_combined = cv2.addWeighted(derivative_x, 0.5, derivative_y, 0.5, 0) min_value = min(derivative_x.min(), derivative_y.min(), derivative_combined.min()) max_value = max(derivative_x.max(), derivative_y.max(), derivative_combined.max()) print(f"Value range: ({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("Horizontal derivative") axes[0].axis('off') image_1 = axes[1].imshow(derivative_y, cmap='gray', vmin=min_value, vmax=max_value) axes[1].set_title("Vertical derivative") axes[1].axis('off') image_2 = axes[2].imshow(derivative_combined, cmap='gray', vmin=min_value, vmax=max_value) axes[2].set_title("Combined derivative") 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()В результате горизонтальные и вертикальные производные отлично выявляют линии! Комбинация позволяет обнаруживать оба типа особенностей:
Оператор Шарра
Еще одна популярная альтернатива ядру Собеля — оператор Шарра:
Несмотря на структурное сходство с Собелем, ядро Шарра обеспечивает большую точность в задачах обнаружения краев. Оно обладает важными математическими свойствами, которые мы не будем детально разбирать здесь.
OpenCV
Использование фильтра Шарра в OpenCV аналогично Собелю, за исключением названия метода (остальные параметры те же):
derivative_x = cv2.Scharr(image, cv2.CV_64F, 1, 0) derivative_y = cv2.Scharr(image, cv2.CV_64F, 0, 1)Вот результат с фильтром Шарра:
Различия между операторами трудно заметить визуально. Однако по цветовой карте видно, что диапазон значений Шарра шире (-800, +800) по сравнению с Собелем (-200, +200). Это объясняется большими константами в ядре Шарра.
Это хороший пример необходимости типа cv2.CV_64F: без него значения обрезались бы до 0–255, и мы потеряли бы данные о градиентах.
Примечание. Прямое сохранение изображений cv2.CV_64F вызовет ошибку. Для записи на диск их нужно преобразовать в другой формат с значениями 0–255.
Заключение
Применяя основы исчисления к компьютерному зрению, мы изучили ключевые свойства изображений, позволяющие выявлять пики интенсивности. Эти знания полезны, поскольку обнаружение признаков — типичная задача в анализе изображений, особенно при ограничениях обработки или без машинного обучения.
Мы также рассмотрели пример с OpenCV, демонстрирующий работу обнаружения краев с операторами Собеля и Шарра. В следующих публикациях разберем продвинутые алгоритмы обнаружения признаков и примеры на OpenCV.