1. Преобразование цветов. Блендинг (Color Transformation. Blending)
Объяснение темы:
### Введение: Что такое цвет и как его видит компьютер?
**Физика и восприятие:**
Чтобы понять, как компьютер работает с цветом, нужно сначала понять, что такое цвет для человека. Цвет — это не неотъемлемое свойство объекта, а результат сложного процесса. Свет представляет собой электромагнитное излучение, и видимый нами спектр — это лишь узкая полоса длин волн. Когда свет от источника (например, солнца) попадает на объект, часть волн поглощается, а часть отражается. Отраженные волны попадают в наш глаз на сетчатку, где находятся миллионы светочувствительных клеток-фоторецепторов. Среди них есть 'палочки' (отвечают за зрение в сумерках, не различают цвета) и 'колбочки'. Существует три типа колбочек, каждый из которых наиболее чувствителен к своему диапазону длин волн: коротковолновому (синий), средневолновому (зеленый) и длинноволновому (красный). Мозг получает и анализирует интенсивность сигналов от этих трех типов колбочек и интерпретирует их комбинацию как определенный цвет. Этот трехкомпонентный механизм восприятия называется трихроматизмом и является основой для большинства цифровых цветовых моделей.
**Цифровое представление:**
Компьютер имитирует этот процесс. Цифровое изображение — это двумерная сетка (матрица) пикселей. Для каждого пикселя компьютер должен хранить информацию о его цвете. Основываясь на трихроматической теории, самый распространенный способ — хранить три числа, представляющие интенсивность красной, зеленой и синей компонент. Этот метод называется представлением в цветовом пространстве RGB. Каждое число (компонента) обычно кодируется 8 битами, что дает 2^8 = 256 возможных уровней интенсивности (от 0 до 255). Когда у нас есть три таких компоненты, мы получаем 256 x 256 x 256 ≈ 16.7 миллионов комбинаций, что составляет так называемый 'True Color' — палитру, достаточную для фотореалистичного отображения.
### Часть 1: Преобразование цветов (Color Transformation)
Преобразование цветов — это математическая процедура пересчета цветовых значений пикселя из одной цветовой модели (пространства) в другую. Это не изменение самого видимого цвета, а изменение способа его числового описания. Разные цветовые модели были разработаны для разных целей, и каждая из них имеет свои сильные и слабые стороны. Выбор правильной модели может кардинально упростить решение задачи компьютерного зрения. Например, если нам нужно найти все ярко-красные яблоки на фото, в модели RGB нам пришлось бы подбирать сложные диапазоны для R, G и B, которые сильно зависят от освещения. Преобразовав изображение в модель HSV, мы можем просто задать узкий диапазон для компоненты Hue (тон), соответствующей красному цвету, и более широкие диапазоны для Saturation (насыщенность) и Value (яркость), что сделает наш алгоритм гораздо более устойчивым к теням и бликам.
#### Глубокое погружение в цветовые пространства (Color Spaces)
**1. RGB и BGR: Модель для экранов**
Это аддитивная цветовая модель. 'Аддитивная' означает, что цвета получаются путем сложения (add) световых потоков. Представьте три прожектора — красный, зеленый и синий, светящие на белый экран. В месте, где их лучи пересекаются, цвета смешиваются. R+G = Желтый, G+B = Циан, B+R = Маджента. R+G+B = Белый. Отсутствие света (все компоненты равны 0) дает черный цвет. Эта модель идеально соответствует физическому устройству большинства экранов, каждый пиксель которых состоит из трех крошечных субпикселей: красного, зеленого и синего.
**Нюанс OpenCV:** Критически важный аспект при работе с библиотекой OpenCV: по историческим причинам стандартный порядок каналов в OpenCV — это BGR (Blue, Green, Red), а не RGB. Если вы загружаете изображение с помощью `cv2.imread()` и передаете его в другую библиотеку (например, Matplotlib), которая ожидает RGB, цвета будут искажены. Всегда необходимо помнить о конвертации `cv2.cvtColor(image, cv2.COLOR_BGR2RGB)`.
**Ограничения:** Главный недостаток RGB для анализа изображений — сильная корреляция между каналами и смешение информации о яркости и цвете. Небольшое изменение освещения меняет все три значения (R, G, B) одновременно, что усложняет создание робастных алгоритмов.
**2. Grayscale: Упрощение ради анализа**
Это модель, в которой цвет полностью отсутствует, и каждый пиксель описывается единственным значением — его яркостью (интенсивностью). Преобразование из цветного изображения в серое — это не простое усреднение, а взвешенная сумма, основанная на чувствительности человеческого глаза: `Luminance = 0.299*R + 0.587*G + 0.114*B`. Использование Grayscale значительно снижает сложность вычислений, фокусируясь на форме и текстуре.
**3. HSV и HSL: Интуитивные модели для человека**
Эти модели разделяют информацию о цвете на компоненты, которые более интуитивно понятны: 'какой цвет?', 'насколько он сочный?' и 'насколько он светлый/темный?'.
- **Hue (Тон):** Угол на цветовом круге (0-360°). Отвечает на вопрос 'какой это цвет?'. В OpenCV диапазон сжат до 0-179 для хранения в 8-битном значении.
- **Saturation (Насыщенность):** 'Чистота' или 'сочность' цвета (0-255). 0 — оттенок серого, 255 — самый чистый цвет.
- **Value (Значение, HSV) vs Lightness (Светлота, HSL):** Высота вдоль оси яркости. В HSV, Value=max может быть любым ярким цветом. В HSL, Lightness=max всегда белый. Для задач компьютерного зрения HSV часто удобнее.
**4. CIE L*a*b*: Перцепционно-однородная модель**
Разработана так, что числовое расстояние между двумя цветами хорошо коррелирует с воспринимаемой человеком разницей. Это пространство независимо от устройства.
- **L* (Lightness):** Ось яркости от 0 (черный) до 100 (белый).
- **a*:** Ось от зеленого (отрицательные значения) до красного (положительные значения).
- **b*:** Ось от синего (отрицательные значения) до желтого (положительные значения).
Идеально для измерения цветовых различий.
**5. YCrCb / YUV: Модели для сжатия**
Эти модели разделяют яркостную (люма - Y) и цветовую (хрома - Cr, Cb) информацию. Так как глаз менее чувствителен к деталям цвета, чем к деталям яркости, это позволяет сжимать цветовую информацию с меньшим разрешением (цветовая субдискретизация), что широко используется в форматах JPEG и MPEG.
#### Преобразования интенсивности (Point Processing) и Гамма-коррекция
Это техники, где значение каждого пикселя в выходном изображении зависит **только** от значения соответствующего пикселя во входном (`s = T(r)`). Они используются для улучшения контраста и коррекции яркости.
**Гамма-коррекция:**
Нелинейное преобразование, описываемое степенной функцией: `I_out = 255 * (I_in / 255)^γ`. Эта операция позволяет изменять соотношение между темными и светлыми тонами.
- `γ < 1`: Осветляет изображение, 'проявляя' детали в тенях. Используется для коррекции темных снимков.
- `γ > 1`: Затемняет изображение, 'приглушая' пересвеченные области. Используется для коррекции слишком светлых снимков.
Для эффективного вычисления используется **таблица подстановки (Look-Up Table, LUT)**.
**Другие преобразования:**
- **Растяжение контраста (Contrast Stretching):** Линейное преобразование, которое 'растягивает' диапазон яркостей изображения на всю доступную шкалу (0-255). Реализуется через `cv2.normalize`.
- **Эквализация гистограммы (Histogram Equalization):** Нелинейное преобразование, которое перераспределяет интенсивности так, чтобы гистограмма стала равномерной, что автоматически улучшает глобальный контраст. Более продвинутая версия - **CLAHE** (адаптивная эквализация), которая работает с локальными регионами и дает более естественные результаты.
### Часть 2: Блендинг изображений (Image Blending)
Блендинг — это процесс создания композитного изображения путем комбинирования пикселей из двух или более исходных изображений. Это фундаментальная операция в фотомонтаже, видеомонтаже и компьютерной графике.
**Математические основы:**
- **Взвешенная сумма:** Основа большинства операций блендинга, формула: `dst(I) = α * src1(I) + β * src2(I) + γ`. OpenCV предоставляет функцию `cv2.addWeighted`. Важным аспектом является **насыщение (saturation)**: функции OpenCV автоматически обрезают результат до диапазона [0, 255], чтобы избежать артефактов переполнения.
- **Альфа-канал:** Дополнительный канал в изображении (RGBA), который определяет степень непрозрачности каждого пикселя. **Альфа-композитинг** — это процесс наложения одного изображения (переднего плана, FG) на другое (фона, BG) с учетом прозрачности по формуле: `Output = α_fg * FG + (1 - α_fg) * BG`.
**Продвинутые техники:**
- **Режимы наложения:** Различные математические формулы для смешивания пикселей, знакомые по графическим редакторам, такие как **Multiply** (Умножение, для затемнения), **Screen** (Экран, для осветления), **Overlay** (Перекрытие, для увеличения контраста).
- **Маскирование и побитовые операции:** Побитовые операции (`AND`, `OR`, `XOR`, `NOT`) позволяют с помощью бинарных масок точно контролировать, какие части изображений будут скомбинированы. Это позволяет вставлять объекты сложной формы с одного изображения на другое.
### Заключение и области применения
Преобразование цветов и блендинг являются фундаментальными кирпичиками, из которых строятся практически все сложные системы компьютерного зрения. Понимание того, как цвет представлен в цифровом виде и как можно манипулировать этим представлением, открывает огромные возможности для анализа. Блендинг же дает инструменты для творческого и функционального синтеза новых изображений. Области применения включают:
- **Компьютерное зрение:** Сегментация по цвету для отслеживания объектов.
- **Медицинская визуализация:** Усиление контраста на снимках МРТ.
- **Дополненная реальность:** Наложение виртуальных объектов на реальный мир.
- **Видеопроизводство:** Хромакей (замена зеленого фона), создание переходов.
- **Фотография и дизайн:** Цветокоррекция, фотомонтаж, создание коллажей.
Методы и Примеры:
Преобразование цветового пространства (Color Space Conversion)
Описание: Это операция изменения цветовой модели изображения. Библиотека OpenCV предоставляет универсальную и высокооптимизированную функцию `cv2.cvtColor` для этой цели, поддерживающую более 150 различных преобразований.
Пример кода:
import cv2
import numpy as np
# Создаем простое цветное изображение (OpenCV использует BGR по умолчанию)
image_bgr = np.zeros((100, 300, 3), dtype=np.uint8)
image_bgr[:, :100] = [255, 0, 0] # Синий
image_bgr[:, 100:200] = [0, 255, 0] # Зеленый
image_bgr[:, 200:] = [0, 0, 255] # Красный
# 1. Преобразование из BGR в Grayscale
gray_image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# 2. Преобразование из BGR в HSV
hsv_image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2HSV)
# 3. Пример использования HSV для сегментации по цвету (поиск красного)
# В HSV красный цвет находится на границе диапазона Hue (около 0 и 179 в OpenCV)
lower_red1 = np.array([0, 120, 70])
upper_red1 = np.array([10, 255, 255])
mask_red1 = cv2.inRange(hsv_image, lower_red1, upper_red1)
lower_red2 = np.array([170, 120, 70])
upper_red2 = np.array([179, 255, 255])
mask_red2 = cv2.inRange(hsv_image, lower_red2, upper_red2)
# Объединение масок
red_mask = cv2.bitwise_or(mask_red1, mask_red2)
# Применение маски для выделения красных областей
result_red_only = cv2.bitwise_and(image_bgr, image_bgr, mask=red_mask)
Объяснение кода: - `cv2.cvtColor(source_image, flag)`: Основная функция. Первый аргумент — исходное изображение, второй — специальный флаг, указывающий тип преобразования (`cv2.COLOR_BGR2GRAY`, `cv2.COLOR_BGR2HSV` и т.д.).
- `cv2.inRange(hsv_image, lower_bound, upper_bound)`: Создает бинарную маску. Для каждого пикселя проверяет, лежит ли значение каждого из его каналов в заданном диапазоне. Если да, пиксель в маске становится белым (255), иначе — черным (0).
- `cv2.bitwise_or(mask1, mask2)`: Побитовая операция "ИЛИ", используется для объединения двух масок.
- `cv2.bitwise_and(src, src, mask=mask)`: Применяет маску к изображению. Пиксели `src` остаются без изменений там, где маска `mask` белая.
Пример из реальной жизни: **Отслеживание объектов по цвету:** Робот может отслеживать желтый мяч, используя HSV, чтобы быть устойчивым к теням (изменение V) и бликам (изменение S). **Сегментация кожи:** В HSV или YCrCb существуют определенные диапазоны значений, соответствующие цвету человеческой кожи, что используется для обнаружения лиц и рук.
Гамма-коррекция с помощью Look-Up Table (LUT)
Описание: Применение нелинейного преобразования интенсивности для осветления или затемнения изображения. Для эффективности используется предварительно вычисленная таблица подстановки (LUT), которая сопоставляет каждое из 256 возможных исходных значений яркости с новым значением.
Пример кода:
import cv2
import numpy as np
def adjust_gamma(image, gamma=1.0):
# Создаем LUT. Мы используем 1/gamma, чтобы gamma > 1 осветляло изображение.
invGamma = 1.0 / gamma
table = np.array([((i / 255.0) ** invGamma) * 255
for i in np.arange(0, 256)]).astype("uint8")
# Применяем LUT к изображению
return cv2.LUT(image, table)
# Создадим темное изображение с градиентом
dark_image = np.zeros((100, 256), dtype=np.uint8)
dark_image[0:100, 0:256] = np.arange(0, 256)
dark_image = (dark_image * 0.5).astype('uint8') # Делаем его темнее
# Осветляем изображение с помощью гамма-коррекции
gamma_brightened = adjust_gamma(dark_image, gamma=2.0)
Объяснение кода: - `adjust_gamma(image, gamma)`: Функция, которая принимает изображение и значение гаммы.
- `table = np.array([...])`: Создается LUT. Цикл `for i in np.arange(0, 256)` проходит по всем возможным значениям пикселя, вычисляет новое значение по формуле гамма-коррекции и сохраняет его в массив `table`.
- `cv2.LUT(image, table)`: Высокооптимизированная функция OpenCV, которая для каждого пикселя `image` находит его значение, использует это значение как индекс в `table` и заменяет пиксель на значение из `table`.
Пример из реальной жизни: **Улучшение фотографий:** Осветление недоэкспонированных (слишком темных) снимков, особенно для 'вытягивания' деталей из теней. **Компьютерная графика:** Использование линейного рабочего процесса (Linear Workflow) для физически корректного рендеринга.
Блендинг изображений (линейный и с помощью масок)
Описание: Комбинирование двух изображений. `cv2.addWeighted` используется для создания полупрозрачного наложения (плавного перехода). Побитовые операции с масками используются для точного 'вклеивания' объекта сложной формы с одного изображения на другое.
Пример кода:
import cv2
import numpy as np
# --- Пример 1: Плавный переход с cv2.addWeighted ---
# img1 = cv2.imread('image1.jpg')
# img2 = cv2.imread('image2.jpg')
# # ... (код изменения размера, если нужно)
# blended_image = cv2.addWeighted(img1, 0.5, img2, 0.5, 0)
# --- Пример 2: Наложение логотипа с помощью маски ---
# main_image = cv2.imread('main_image.jpg')
# logo = cv2.imread('logo.png')
# if main_image is not None and logo is not None:
# rows, cols, _ = logo.shape
# roi = main_image[0:rows, 0:cols]
# logo_gray = cv2.cvtColor(logo, cv2.COLOR_BGR2GRAY)
# ret, mask = cv2.threshold(logo_gray, 10, 255, cv2.THRESH_BINARY)
# mask_inv = cv2.bitwise_not(mask)
# main_bg = cv2.bitwise_and(roi, roi, mask=mask_inv)
# logo_fg = cv2.bitwise_and(logo, logo, mask=mask)
# dst = cv2.add(main_bg, logo_fg)
# main_image[0:rows, 0:cols] = dst
Объяснение кода: - `cv2.addWeighted(src1, alpha, src2, beta, gamma)`: Смешивает два изображения с заданными весами `alpha` и `beta`.
- **Побитовые операции для маскирования:**
- `cv2.threshold`: Используется для создания бинарной маски из серого изображения логотипа.
- `cv2.bitwise_not(mask)`: Инвертирует маску (черное становится белым и наоборот).
- `cv2.bitwise_and(src1, src2, mask=mask)`: Выполняет побитовое "И" между `src1` и `src2`, но только в тех местах, где пиксели маски `mask` не равны нулю.
- `cv2.add(img1, img2)`: Простое поэлементное сложение, удобное для совмещения 'вырезанных' частей.
Пример из реальной жизни: **Создание водяных знаков:** Полупрозрачный логотип накладывается на фото с помощью `addWeighted`. **Видеопереходы:** Плавный переход от одной сцены к другой. **Фотомонтаж:** Вставка объекта с одного изображения на другое с использованием масок и побитовых операций.
2. Преобразования Фурье. Восприятие изображений (Fourier Transform. Image Perception)
Объяснение темы:
Представьте, что у вас есть сложная музыкальная композиция. Преобразование Фурье — это как если бы вы разложили эту музыку на отдельные ноты (чистые тона) разной громкости. Каждая нота — это простая синусоидальная волна определенной частоты. Точно так же изображение можно рассматривать как сложный двумерный сигнал. Преобразование Фурье для изображений разлагает его на сумму простых синусоидальных "узоров" (плоских волн) различных пространственных частот, ориентаций и амплитуд.
**Что такое пространственные частоты?**
- **Низкие частоты** соответствуют плавным, медленным изменениям яркости на изображении. Это общие формы, фон, большие однородные области. Представьте размытое изображение — в нем доминируют низкие частоты.
- **Высокие частоты** соответствуют резким, быстрым изменениям яркости. Это детали, края объектов, текстуры, шум. Представьте четкие контуры или мелкий узор — это высокие частоты.
Результат преобразования Фурье называется **спектром Фурье** (или частотным представлением). Он показывает, какие частоты присутствуют в изображении и с какой интенсивностью (амплитудой). Обычно спектр визуализируют так, что центр соответствует нулевой частоте (средней яркости изображения), а по мере удаления от центра частоты увеличиваются.
**Восприятие изображений человеком и Фурье:** Человеческая зрительная система также в некотором роде выполняет частотный анализ. Мы очень чувствительны к определенным диапазонам пространственных частот, что позволяет нам эффективно распознавать контуры, текстуры и формы. Изучение преобразования Фурье помогает не только понять, как устроены изображения с точки зрения частот, но и как можно ими манипулировать, например, для улучшения качества, удаления шума или сжатия.
Методы и Примеры:
Дискретное преобразование Фурье (DFT) и его визуализация
Описание: Переводит изображение из пространственной области (пиксели) в частотную область (спектр). OpenCV использует `cv2.dft()` для этого. Результат — комплексный массив, содержащий реальную и мнимую части для каждой частоты. Для визуализации обычно вычисляют магнитудный спектр и применяют логарифмическое масштабирование, чтобы лучше видеть детали.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt # Для более удобного отображения
# Загрузка изображения в оттенках серого
# В качестве примера создадим простое изображение с квадратом
original_image = np.zeros((200, 200), dtype=np.uint8)
original_image[75:125, 75:125] = 255 # Белый квадрат в центре
# if original_image is None:
# print('Ошибка: Не удалось загрузить изображение.')
# exit()
# Преобразование в float32, т.к. dft работает с этим типом
image_float32 = np.float32(original_image)
# Выполнение ДПФ. cv2.DFT_COMPLEX_OUTPUT возвращает комплексный результат
dft_result = cv2.dft(image_float32, flags=cv2.DFT_COMPLEX_OUTPUT)
# Сдвиг компоненты с нулевой частотой (DC component) из левого верхнего угла в центр спектра
# Это делается для более интуитивной визуализации
dft_shifted = np.fft.fftshift(dft_result)
# Вычисление магнитудного спектра
# dft_shifted[:,:,0] - реальная часть, dft_shifted[:,:,1] - мнимая часть
magnitude_spectrum = 20 * np.log(cv2.magnitude(dft_shifted[:, :, 0], dft_shifted[:, :, 1]) + 1e-9) # +1e-9 для избежания log(0)
# Визуализация
# plt.figure(figsize=(10, 5))
# plt.subplot(121), plt.imshow(original_image, cmap='gray')
# plt.title('Исходное изображение'), plt.xticks([]), plt.yticks([])
# plt.subplot(122), plt.imshow(magnitude_spectrum, cmap='gray')
# plt.title('Магнитудный спектр (логарифм)'), plt.xticks([]), plt.yticks([])
# plt.show()
Объяснение кода: - `np.float32(original_image)`: Функция `cv2.dft` ожидает на вход массив типа `float32`.
- `cv2.dft(image_float32, flags=cv2.DFT_COMPLEX_OUTPUT)`: Выполняет ДПФ.
- `image_float32`: Входное изображение.
- `flags=cv2.DFT_COMPLEX_OUTPUT`: Указывает, что результат должен быть комплексным массивом с двумя каналами (реальная и мнимая части).
- `np.fft.fftshift(dft_result)`: Перемещает квадранты спектра так, чтобы компонента с нулевой частотой (средняя яркость, DC-компонента) оказалась в центре. Без этого сдвига DC-компонента находится в левом верхнем углу, что менее удобно для анализа.
- `cv2.magnitude(real_part, imaginary_part)`: Вычисляет амплитуду (величину) комплексного числа по формуле `sqrt(real_part^2 + imaginary_part^2)`.
- `20 * np.log(...)`: Логарифмическое масштабирование. Амплитуды в спектре Фурье могут иметь очень большой динамический диапазон (DC-компонента обычно намного больше остальных). Логарифм сжимает этот диапазон, делая видимыми и слабые высокочастотные компоненты. Добавление `1e-9` (очень маленькое число) нужно, чтобы избежать ошибки `log(0)`.
Пример из реальной жизни: 1. **Анализ текстур:** Различные текстуры имеют характерные паттерны в частотной области.
2. **Основа для фильтрации:** Прежде чем фильтровать, нужно получить частотное представление.
3. **Сжатие изображений:** Форматы вроде JPEG используют похожие идеи (Дискретное Косинусное Преобразование, ДКП), отбрасывая менее значимые высокочастотные компоненты.
Фильтрация в частотной области
Описание: Это процесс модификации изображения путем изменения его частотного спектра. Например, можно ослабить высокие частоты для размытия (ФНЧ — фильтр низких частот) или ослабить низкие частоты для выделения краев (ФВЧ — фильтр высоких частот).
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# original_image = cv2.imread('noisy_image.jpg', cv2.IMREAD_GRAYSCALE)
# if original_image is None: print('Ошибка загрузки'); exit()
# Для примера создадим изображение и добавим периодический шум
img_size = 256
original_image = np.zeros((img_size, img_size), dtype=np.uint8)
original_image[100:150, 100:150] = 200 # Квадрат
# Добавим синусоидальный шум
for i in range(img_size):
original_image[i,:] = np.clip(original_image[i,:] + 30 * np.sin(i / 5.0), 0, 255)
image_float32 = np.float32(original_image)
dft_result = cv2.dft(image_float32, flags=cv2.DFT_COMPLEX_OUTPUT)
dft_shifted = np.fft.fftshift(dft_result)
# Создание маски для фильтра (например, ФНЧ - пропускаем низкие частоты)
rows, cols = original_image.shape
crow, ccol = rows // 2 , cols // 2 # Центр
# Маска для ФНЧ (круговая)
radius_lpf = 30
mask_lpf = np.zeros((rows, cols, 2), np.float32) # Маска должна быть того же типа и размера, что и dft_shifted
cv2.circle(mask_lpf, (ccol, crow), radius_lpf, (1,1), -1) # (1,1) для реальной и мнимой части
# Применение маски ФНЧ
filtered_dft_shifted_lpf = dft_shifted * mask_lpf
# Маска для режекторного фильтра (удаление периодического шума)
# Предположим, мы знаем координаты пиков шума в спектре (относительно центра)
# (эти точки нужно найти, посмотрев на спектр зашумленного изображения)
# Для нашего примера шум горизонтальный, пики будут на вертикальной линии от центра
mask_notch = np.ones((rows, cols, 2), np.float32)
noise_freq_offset = int(img_size / (2 * 5.0)) # Примерная частота нашего синусоидального шума
notch_width = 5
cv2.rectangle(mask_notch, (ccol - notch_width//2, crow - noise_freq_offset - notch_width//2),
(ccol + notch_width//2, crow - noise_freq_offset + notch_width//2), (0,0), -1)
cv2.rectangle(mask_notch, (ccol - notch_width//2, crow + noise_freq_offset - notch_width//2),
(ccol + notch_width//2, crow + noise_freq_offset + notch_width//2), (0,0), -1)
# Применение режекторной маски
filtered_dft_shifted_notch = dft_shifted * mask_notch
# Обратное преобразование Фурье для ФНЧ
f_ishift_lpf = np.fft.ifftshift(filtered_dft_shifted_lpf)
img_back_lpf = cv2.idft(f_ishift_lpf)
img_back_lpf = cv2.magnitude(img_back_lpf[:, :, 0], img_back_lpf[:, :, 1])
img_back_lpf = cv2.normalize(img_back_lpf, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
# Обратное преобразование Фурье для режекторного фильтра
f_ishift_notch = np.fft.ifftshift(filtered_dft_shifted_notch)
img_back_notch = cv2.idft(f_ishift_notch)
img_back_notch = cv2.magnitude(img_back_notch[:, :, 0], img_back_notch[:, :, 1])
img_back_notch = cv2.normalize(img_back_notch, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
# Магнитудный спектр исходного и отфильтрованных
magnitude_spectrum_original = 20 * np.log(cv2.magnitude(dft_shifted[:,:,0], dft_shifted[:,:,1]) + 1e-9)
magnitude_spectrum_lpf_filtered = 20 * np.log(cv2.magnitude(filtered_dft_shifted_lpf[:,:,0], filtered_dft_shifted_lpf[:,:,1]) + 1e-9)
magnitude_spectrum_notch_filtered = 20 * np.log(cv2.magnitude(filtered_dft_shifted_notch[:,:,0], filtered_dft_shifted_notch[:,:,1]) + 1e-9)
# Визуализация
# plt.figure(figsize=(15, 10))
# plt.subplot(231), plt.imshow(original_image, cmap='gray'), plt.title('Исходное (с шумом)')
# plt.subplot(232), plt.imshow(img_back_lpf, cmap='gray'), plt.title('После ФНЧ (размытие)')
# plt.subplot(233), plt.imshow(img_back_notch, cmap='gray'), plt.title('После режекторного фильтра')
# plt.subplot(234), plt.imshow(magnitude_spectrum_original, cmap='gray'), plt.title('Спектр исходного')
# plt.subplot(235), plt.imshow(magnitude_spectrum_lpf_filtered, cmap='gray'), plt.title('Спектр после ФНЧ')
# plt.subplot(236), plt.imshow(magnitude_spectrum_notch_filtered, cmap='gray'), plt.title('Спектр после режекторного')
# plt.show()
Объяснение кода: 1. Изображение переводится в частотную область (`dft_shifted`).
2. **Создание маски:** Маска — это массив того же размера, что и `dft_shifted`. Значения в маске обычно 0 или 1.
- Для ФНЧ (Low-Pass Filter): Маска имеет 1 в центре (низкие частоты) и 0 по краям (высокие частоты). `cv2.circle` используется для создания круглой области единиц.
- Для режекторного фильтра (Notch Filter): Маска имеет 0 в тех местах спектра, где находятся нежелательные частоты (пики шума), и 1 в остальных местах. `cv2.rectangle` используется, чтобы "вырезать" эти частоты.
3. `filtered_dft_shifted = dft_shifted * mask`: Спектр изображения поэлементно умножается на маску. Где маска 0, там соответствующие частоты обнуляются.
4. `f_ishift = np.fft.ifftshift(filtered_dft_shifted)`: Обратный сдвиг, чтобы DC-компонента вернулась в левый верхний угол перед обратным ДПФ.
5. `img_back = cv2.idft(f_ishift)`: Выполняется обратное ДПФ. Результат снова комплексный.
6. `cv2.magnitude(...)`: Извлекается величина (амплитуда) из комплексного результата.
7. `cv2.normalize(...)`: Нормализует значения пикселей к диапазону 0-255 для корректного отображения как изображения.
Пример из реальной жизни: 1. **Размытие (ФНЧ):** Уменьшает шум, сглаживает изображение.
2. **Повышение резкости (ФВЧ):** Выделяет края и детали. ФВЧ можно получить, вычитая ФНЧ из единичной маски.
3. **Удаление периодического шума:** Если на изображении есть регулярный узор помех (например, сетка от сканера, муар), он проявится как яркие точки (пики) в спектре. Создав режекторный фильтр, который обнуляет эти пики, можно убрать шум.
4. **Полосовые фильтры:** Пропускают только определенный диапазон частот, полезны для выделения текстур определенного масштаба.
3. Проективные матричные преобразования координат на плоскости (Projective Matrix Transformations of Coordinates on a Plane)
Объяснение темы:
Представьте, что вы фотографируете плоский объект, например, картину на стене или лист бумаги на столе. Если вы фотографируете строго перпендикулярно (анфас), то форма объекта на фото будет такой же, как в реальности (прямоугольник останется прямоугольником). Но если вы сместите камеру в сторону или наклоните ее, то на фотографии прямоугольник превратится в трапецию или более сложный четырехугольник. Это искажение перспективы.
Проективные преобразования (также известные как гомографии или перспективные трансформации) математически описывают, как координаты точек на одной плоскости (например, на листе бумаги) отображаются на другую плоскость (например, на сенсор камеры, а затем на фотографию). Эти преобразования сохраняют прямые линии (прямая на объекте останется прямой на фото), но не сохраняют параллельность линий, углы или соотношения длин.
Ключевая идея — использование **однородных координат**. Вместо обычных 2D-координат `(x, y)` мы используем 3D-вектор `(x, y, 1)`. Это позволяет представить все виды преобразований, включая сдвиги и сложные перспективные искажения, единым образом — умножением на матрицу 3x3. После умножения мы получаем новые однородные координаты `(x', y', w')`. Чтобы вернуться к обычным 2D-координатам, мы делим первые два компонента на третий: `(x'/w', y'/w')`. Этот третий компонент `w'` и отвечает за эффект перспективы.
Методы и Примеры:
Вычисление матрицы гомографии (Homography Estimation)
Описание: Это процесс нахождения матрицы `H` 3x3, которая наилучшим образом отображает набор точек с одной плоскости на соответствующий набор точек на другой плоскости. В OpenCV для этого используется функция `cv2.getPerspectiveTransform`, которая требует ровно 4 пары точек, или `cv2.findHomography`, которая может работать с большим количеством точек и использовать методы типа RANSAC для устойчивости к ошибкам.
Пример кода:
import cv2
import numpy as np
# Координаты 4-х точек на исходном изображении (например, углы листа бумаги)
# Представим, что это углы прямоугольника размером 300x200 пикселей
# Точки должны быть в формате float32
# Порядок точек важен и должен соответствовать порядку pts_dst
# Например: верхний-левый, верхний-правый, нижний-правый, нижний-левый
pts_src = np.array([[50, 50], # Точка 1 (верхний-левый угол исходного объекта)
[250, 50], # Точка 2 (верхний-правый)
[250, 200], # Точка 3 (нижний-правый)
[50, 200]], # Точка 4 (нижний-левый)
dtype="float32")
# Координаты этих же 4-х точек на целевом изображении (куда мы хотим их трансформировать)
# Например, после перспективного искажения они стали выглядеть так:
pts_dst = np.array([[10, 100], # Точка 1 на целевом изображении
[280, 80], # Точка 2
[300, 250], # Точка 3
[30, 200]], # Точка 4
dtype="float32")
# Вычисление матрицы гомографии H с использованием 4-х пар точек
# Эта функция решает систему линейных уравнений для 8 неизвестных элементов матрицы H.
H_matrix = cv2.getPerspectiveTransform(pts_src, pts_dst)
# print("Вычисленная матрица гомографии H:\n", H_matrix)
# Пример выходных данных:
# [[ 1.20588235e+00 4.11764706e-01 -5.70588235e+01]
# [ 1.17647059e-01 1.11764706e+00 2.52941176e+01]
# [ 4.70588235e-04 1.17647059e-03 1.00000000e+00]]
Объяснение кода: - `pts_src`: NumPy массив, содержащий координаты четырех точек на исходной плоскости. Каждая точка — это пара `(x, y)`.
- `pts_dst`: NumPy массив, содержащий координаты соответствующих четырех точек на целевой плоскости.
- `cv2.getPerspectiveTransform(pts_src, pts_dst)`: Эта функция принимает два набора из четырех точек и возвращает матрицу гомографии `H` размером 3x3. Важно, чтобы точки в `pts_src` и `pts_dst` соответствовали друг другу (например, `pts_src[0]` преобразуется в `pts_dst[0]`). Если у вас больше 4 пар точек или есть возможные ошибки в координатах, лучше использовать `cv2.findHomography` с методом RANSAC (см. Билет 10).
Пример из реальной жизни: 1. **Ректификация изображений:** "Выпрямление" изображения, снятого под углом, чтобы оно выглядело так, будто снято анфас. Например, сканирование документов телефоном.
2. **Создание панорам:** Совмещение нескольких изображений с перекрывающимися областями. Гомография используется для трансформации одного изображения в систему координат другого.
3. **Дополненная реальность:** Наложение виртуальных объектов на реальные плоские поверхности (например, размещение виртуальной картины на стене).
Применение гомографии к изображению (Image Warping)
Описание: После того как матрица гомографии `H` вычислена, ее можно использовать для трансформации целого изображения. Этот процесс называется "варпинг" (warping). Каждый пиксель исходного изображения отображается в новое положение на целевом изображении согласно матрице `H`.
Пример кода:
import cv2
import numpy as np
# Создадим простое исходное изображение с цветным прямоугольником
src_image = np.zeros((250, 300, 3), dtype=np.uint8)
# Рисуем зеленый прямоугольник, соответствующий нашим pts_src из предыдущего примера
cv2.rectangle(src_image, (50, 50), (250, 200), (0, 255, 0), -1) # Зеленый
# cv2.imshow('Исходное изображение', src_image)
# Используем те же точки, что и в предыдущем примере для H_matrix
pts_src = np.array([[50, 50], [250, 50], [250, 200], [50, 200]], dtype="float32")
pts_dst = np.array([[10, 100], [280, 80], [300, 250], [30, 200]], dtype="float32")
H_matrix = cv2.getPerspectiveTransform(pts_src, pts_dst)
# Задаем размеры выходного изображения (куда будет трансформировано исходное)
output_width = 350
output_height = 300
# Применение проективного преобразования (варпинг)
warped_image = cv2.warpPerspective(src_image, H_matrix, (output_width, output_height))
# Отображение результата
# cv2.imshow('Трансформированное изображение', warped_image)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
Объяснение кода: - `src_image`: Исходное изображение, которое мы хотим трансформировать.
- `H_matrix`: Матрица гомографии 3x3, вычисленная ранее.
- `(output_width, output_height)`: Кортеж `(ширина, высота)`, задающий размер выходного (трансформированного) изображения.
- `cv2.warpPerspective(src_image, H_matrix, dsize, flags, borderMode, borderValue)`:
- `src_image`: Входное изображение.
- `H_matrix`: Матрица преобразования.
- `dsize`: Размер выходного изображения.
- `flags` (опционально): Комбинация методов интерполяции (например, `cv2.INTER_LINEAR` (по умолчанию) или `cv2.INTER_NEAREST`) и флага `cv2.WARP_INVERSE_MAP` (если `H_matrix` преобразует из целевого в исходное).
- `borderMode` (опционально): Метод экстраполяции пикселей за границами (например, `cv2.BORDER_CONSTANT` - заполнение константой).
- `borderValue` (опционально): Значение для заполнения, если `borderMode=cv2.BORDER_CONSTANT` (по умолчанию 0 - черный).
Функция проходит по каждому пикселю выходного изображения, находит его соответствующее положение в исходном изображении с помощью обратного преобразования `H_matrix_inverse * (x_out, y_out, 1)^T`, и берет цвет пикселя оттуда (используя интерполяцию, если координаты нецелые).
Пример из реальной жизни: 1. **Виртуальная замена билбордов:** Нахождение углов билборда на видео, вычисление гомографии и наложение нового рекламного изображения.
2. **Создание эффекта "вид сверху" (bird's-eye view):** Например, для камер парковки автомобиля, чтобы показать вид сверху, трансформируя изображения с нескольких камер.
3. **Удаление перспективных искажений с картин или текста:** Чтобы прочитать текст, написанный на поверхности под углом.
4. Детектор Харриса (Harris Corner Detector)
Объяснение темы:
Представьте, что вы пытаетесь отследить какую-то точку на изображении, пока камера немного двигается или изображение слегка меняется. Если вы выберете точку на гладкой, однородной поверхности (например, на белой стене), то при малейшем сдвиге будет очень трудно понять, куда именно она сместилась, так как все вокруг одинаковое. Если вы выберете точку на прямой линии (ребре), то вы легко заметите смещение перпендикулярно линии, но вдоль линии снова будет неопределенность.
А вот если вы выберете **угол** (corner) — место, где пересекаются две или более линий, или где текстура резко меняется в нескольких направлениях, — то такую точку будет легко отследить. При любом небольшом сдвиге окна вокруг этой точки, содержимое окна заметно изменится.
**Детектор углов Харриса** — это один из классических и популярных алгоритмов для автоматического нахождения таких "интересных" угловых точек на изображении. Эти точки являются хорошими признаками, так как они относительно устойчивы (стабильны) к изменениям освещения, вращению изображения и небольшим изменениям точки обзора. Найденные углы затем можно использовать для сопоставления изображений, отслеживания объектов, построения 3D-моделей и т.д.
Методы и Примеры:
Детектор углов Харриса в OpenCV (cv2.cornerHarris)
Описание: Функция `cv2.cornerHarris` в OpenCV реализует описанный выше алгоритм. Она принимает на вход изображение в градациях серого и возвращает карту "угловатости" (Harris response map), где значения пикселей соответствуют мере `R`.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Загрузка изображения или создание тестового
# image_bgr = cv2.imread('chessboard.png') # Хорошо подходит шахматная доска
# if image_bgr is None: print('Ошибка загрузки'); exit()
# Создадим тестовое изображение с углами
image_bgr = np.zeros((200, 250, 3), dtype=np.uint8)
image_bgr.fill(200) # Светло-серый фон
cv2.rectangle(image_bgr, (50, 50), (150, 150), (50, 50, 50), -1) # Темный квадрат
cv2.putText(image_bgr, "ABC", (170, 100), cv2.FONT_HERSHEY_SIMPLEX, 1, (0,0,0), 2)
gray_image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Преобразование в float32, так как cornerHarris работает с этим типом
gray_image_float = np.float32(gray_image)
# Применение детектора Харриса
# blockSize: Размер окна для вычисления матрицы M (например, 2x2 или 5x5).
# ksize: Размер (апертура) ядра Собеля для вычисления производных (должен быть нечетным, например, 3, 5).
# k: Свободный параметр в формуле Харриса (обычно 0.04 - 0.06).
harris_response_map = cv2.cornerHarris(gray_image_float, blockSize=2, ksize=3, k=0.04)
# Результат harris_response_map - это карта 'угловатости'. Углы будут иметь высокие значения.
# Для визуализации можно расширить (dilate) найденные отклики, чтобы точки были заметнее.
harris_response_map_dilated = cv2.dilate(harris_response_map, None)
# Создаем копию исходного цветного изображения для отрисовки результатов
image_with_corners = image_bgr.copy()
# Пороговая фильтрация для выделения сильных углов
# Мы помечаем пиксели на исходном изображении, где значение в harris_response_map_dilated
# превышает определенный процент от максимального значения отклика.
threshold_harris = 0.01 * harris_response_map_dilated.max()
image_with_corners[harris_response_map_dilated > threshold_harris] = [0, 0, 255] # Пометить углы красным (BGR)
# Визуализация
# plt.figure(figsize=(12, 6))
# plt.subplot(131), plt.imshow(cv2.cvtColor(image_bgr, cv2.COLOR_BGR2RGB)), plt.title('Исходное')
# plt.subplot(132), plt.imshow(harris_response_map_dilated, cmap='gray'), plt.title('Отклик Харриса (расширенный)')
# plt.subplot(133), plt.imshow(cv2.cvtColor(image_with_corners, cv2.COLOR_BGR2RGB)), plt.title('Найденные углы Харриса')
# plt.show()
Объяснение кода: - `gray_image_float = np.float32(gray_image)`: Детектор Харриса в OpenCV работает с изображениями типа `float32`.
- `harris_response_map = cv2.cornerHarris(src, blockSize, ksize, k)`:
- `src`: Входное одноканальное изображение типа `float32`.
- `blockSize`: Размер окрестности, рассматриваемой для каждого пикселя при вычислении матрицы `M`. Типичные значения: 2-5.
- `ksize`: Апертурный параметр оператора Собеля, используемого для вычисления производных `Ix` и `Iy`. Должен быть нечетным. Типичное значение: 3.
- `k`: Параметр Харриса в формуле `R = det(M) - k * (trace(M))^2`. Типичные значения: 0.04 - 0.06.
- `harris_response_map`: Результатом является изображение того же размера, что и входное, где каждый пиксель содержит значение `R` (меру угловатости).
- `cv2.dilate(harris_response_map, None)`: Операция дилатации (расширения). Она немного увеличивает яркие области на карте отклика, что может помочь сделать обнаруженные углы более заметными или объединить близкие отклики. `None` означает использование стандартного ядра 3x3.
- `threshold_harris = 0.01 * harris_response_map_dilated.max()`: Выбирается порог. Здесь он взят как 1% от максимального значения отклика на расширенной карте. Только пиксели, чей отклик Харриса выше этого порога, будут считаться углами.
- `image_with_corners[harris_response_map_dilated > threshold_harris] = [0, 0, 255]`: NumPy-индексация. Все пиксели в `image_with_corners`, для которых соответствующий пиксель в `harris_response_map_dilated` больше порога, окрашиваются в красный цвет (BGR `[0,0,255]`).
Пример из реальной жизни: 1. **Сопоставление изображений (Image Matching):** Углы Харриса могут служить ключевыми точками для нахождения соответствий между разными изображениями одной и той же сцены.
2. **Отслеживание объектов (Object Tracking):** Если объект имеет достаточно углов, их можно отслеживать от кадра к кадру для определения движения объекта.
3. **Создание панорам (Image Stitching):** Углы помогают найти общие области на перекрывающихся фотографиях.
4. **3D-реконструкция:** Положение углов на нескольких изображениях, снятых с разных ракурсов, используется для восстановления 3D-структуры сцены.
5. **Робототехника и навигация (SLAM - Simultaneous Localization and Mapping):** Углы как стабильные ориентиры в окружающей среде.
5. Преобразования Хаффа (Hough Transform)
Объяснение темы:
Представьте, что у вас есть изображение с множеством точек, и вы хотите найти на нем все прямые линии (или, например, окружности). Как это сделать автоматически? Преобразование Хаффа — это умный метод, который позволяет находить объекты определенных форм (линии, окружности, эллипсы и т.д.) на изображении, даже если они частично скрыты, прерывисты или зашумлены.
**Основная идея — голосование в пространстве параметров.**
Вместо того чтобы пытаться соединять точки на изображении напрямую, преобразование Хаффа переводит задачу из пространства изображения в пространство параметров, описывающих искомую фигуру. Каждая точка на изображении, которая могла бы принадлежать искомой фигуре, "голосует" за все возможные параметры фигуры, через которые она может проходить.
**Для линий:**
Обычное уравнение прямой `y = mx + c` неудобно, так как `m` (наклон) становится бесконечным для вертикальных линий. Поэтому используется параметризация в полярных координатах: `ρ = x*cos(θ) + y*sin(θ)`.
- `ρ` (ро) — это перпендикулярное расстояние от начала координат до линии.
- `θ` (тета) — это угол между осью X и нормалью (перпендикуляром) к линии.
Каждая точка `(x, y)` на изображении может принадлежать бесконечному числу линий. В пространстве параметров `(ρ, θ)` эта точка соответствует синусоидальной кривой. Если несколько точек на изображении лежат на одной и той же прямой, их соответствующие синусоиды в пространстве `(ρ, θ)` пересекутся в одной точке. Координаты этой точки пересечения `(ρ_0, θ_0)` и будут параметрами искомой линии.
Преобразование Хаффа создает 2D-массив (аккумулятор), где по одной оси откладывается `ρ`, а по другой — `θ`. Каждая граничная точка изображения "голосует", увеличивая значение в ячейках аккумулятора, соответствующих всем линиям `(ρ, θ)`, через которые она может проходить. После обработки всех точек, ячейки аккумулятора с наибольшим количеством голосов (локальные максимумы) соответствуют наиболее вероятным линиям на изображении.
Методы и Примеры:
Стандартное преобразование Хаффа для линий (cv2.HoughLines)
Описание: Обнаруживает прямые линии на бинарном изображении (обычно после детектора краев, например, Canny). Возвращает параметры линий в виде `(ρ, θ)`.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Загрузка изображения или создание тестового
# image_bgr = cv2.imread('sudoku.png')
# if image_bgr is None: print('Ошибка загрузки'); exit()
# Создадим тестовое изображение с линиями
image_bgr = np.zeros((200, 200, 3), dtype=np.uint8)
image_bgr.fill(255) # Белый фон
cv2.line(image_bgr, (50, 20), (150, 180), (0,0,0), 2) # Наклонная линия
cv2.line(image_bgr, (20, 100), (180, 100), (0,0,0), 2) # Горизонтальная линия
gray_image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Обнаружение краев с помощью детектора Canny
edges = cv2.Canny(gray_image, threshold1=50, threshold2=150, apertureSize=3)
# Применение стандартного преобразования Хаффа
# rho: разрешение по расстоянию ρ в пикселях (обычно 1).
# theta: разрешение по углу θ в радианах (обычно np.pi/180, т.е. 1 градус).
# threshold: минимальное количество голосов (пересечений в аккумуляторе) для признания линии.
lines = cv2.HoughLines(edges, rho=1, theta=np.pi/180, threshold=60)
image_with_lines = image_bgr.copy()
if lines is not None:
for line_params in lines:
rho, theta = line_params[0] # Берем параметры первой (и единственной в данном случае) линии из массива
a = np.cos(theta)
b = np.sin(theta)
x0 = a * rho
y0 = b * rho
# Находим две точки на линии для отрисовки (x1,y1) и (x2,y2)
# (смещаемся от точки (x0,y0) вдоль и против перпендикуляра к линии)
x1 = int(x0 + 1000 * (-b))
y1 = int(y0 + 1000 * (a))
x2 = int(x0 - 1000 * (-b))
y2 = int(y0 - 1000 * (a))
cv2.line(image_with_lines, (x1, y1), (x2, y2), (0, 0, 255), 1) # Рисуем красную линию
# Визуализация
# plt.figure(figsize=(10, 5))
# plt.subplot(121), plt.imshow(edges, cmap='gray'), plt.title('Края (Canny)')
# plt.subplot(122), plt.imshow(cv2.cvtColor(image_with_lines, cv2.COLOR_BGR2RGB)), plt.title('Найденные линии (HoughLines)')
# plt.show()
Объяснение кода: - `edges = cv2.Canny(...)`: Преобразование Хаффа обычно применяется к бинарному изображению краев.
- `lines = cv2.HoughLines(edges, rho, theta, threshold)`:
- `edges`: Входное бинарное изображение краев.
- `rho`: Разрешение аккумулятора по расстоянию `ρ` в пикселях.
- `theta`: Разрешение аккумулятора по углу `θ` в радианах.
- `threshold`: Минимальное число голосов (точек на кривой в пространстве Хаффа), чтобы линия была детектирована.
- `lines`: Возвращает массив массивов. Каждый внутренний массив содержит один элемент `[rho, theta]` для найденной линии.
- **Отрисовка линий:** Код после получения `lines` преобразует полярные координаты `(rho, theta)` в две точки на декартовой плоскости для вызова `cv2.line`. Это делается, находя точку `(x0, y0)` на линии, ближайшую к началу координат (`x0=ρ*cosθ, y0=ρ*sinθ`), а затем отступая от нее в обе стороны вдоль линии на большое расстояние.
Пример из реальной жизни: 1. **Обнаружение дорожной разметки:** В системах помощи водителю (ADAS).
2. **Анализ документов:** Выравнивание отсканированных документов по найденным линиям текста или краям страницы.
3. **Робототехника:** Обнаружение краев столов, стен.
Вероятностное преобразование Хаффа для линий (cv2.HoughLinesP)
Описание: Это оптимизированная версия стандартного преобразования Хаффа. Она не рассматривает все точки, а только случайную их выборку. Кроме того, она возвращает не параметры линий, а непосредственно координаты `(x1, y1, x2, y2)` начальной и конечной точек найденных отрезков. Обычно она быстрее и дает более практичные результаты (отрезки вместо бесконечных линий).
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Используем то же изображение и края, что и в предыдущем примере
image_bgr = np.zeros((200, 200, 3), dtype=np.uint8); image_bgr.fill(255)
cv2.line(image_bgr, (50, 20), (150, 180), (0,0,0), 2)
cv2.line(image_bgr, (20, 100), (180, 100), (0,0,0), 2)
gray_image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray_image, 50, 150, apertureSize=3)
# Применение вероятностного преобразования Хаффа
# minLineLength: Минимальная длина отрезка в пикселях. Отрезки короче этого значения отбрасываются.
# maxLineGap: Максимальный допустимый разрыв между сегментами линии, чтобы считать их одной линией.
lines_p = cv2.HoughLinesP(edges, rho=1, theta=np.pi/180, threshold=30,
minLineLength=20, maxLineGap=5)
image_with_lines_p = image_bgr.copy()
if lines_p is not None:
for line_segment in lines_p:
x1, y1, x2, y2 = line_segment[0]
cv2.line(image_with_lines_p, (x1, y1), (x2, y2), (0, 255, 0), 2) # Рисуем зеленый отрезок
# Визуализация
# plt.figure(figsize=(10, 5))
# plt.subplot(121), plt.imshow(edges, cmap='gray'), plt.title('Края (Canny)')
# plt.subplot(122), plt.imshow(cv2.cvtColor(image_with_lines_p, cv2.COLOR_BGR2RGB)), plt.title('Найденные отрезки (HoughLinesP)')
# plt.show()
Объяснение кода: - `lines_p = cv2.HoughLinesP(edges, rho, theta, threshold, minLineLength, maxLineGap)`:
- `edges`, `rho`, `theta`, `threshold`: Аналогичны `cv2.HoughLines`.
- `minLineLength`: Минимальная длина отрезка, который будет детектирован.
- `maxLineGap`: Максимально допустимый разрыв между точками на одной прямой, чтобы они все еще считались частью одного отрезка.
- `lines_p`: Возвращает массив отрезков. Каждый элемент — это массив `[[x1, y1, x2, y2]]`.
- Отрисовка здесь проще, так как мы сразу получаем координаты концов отрезков.
Пример из реальной жизни: Те же, что и для стандартного преобразования Хаффа, но часто предпочтительнее из-за получения конечных отрезков, что более практично для многих приложений (например, найти именно видимую часть дорожной разметки).
Преобразование Хаффа для окружностей (cv2.HoughCircles)
Описание: Обнаруживает окружности на изображении. В OpenCV реализован метод, называемый Hough Gradient Method, который более устойчив к шуму, чем стандартное голосование в 3D пространстве параметров.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Загрузка изображения или создание тестового
# image_bgr = cv2.imread('coins.png')
# if image_bgr is None: print('Ошибка загрузки'); exit()
# Создадим тестовое изображение с кругами
image_bgr = np.zeros((250, 350, 3), dtype=np.uint8)
image_bgr.fill(220) # Светло-серый фон
cv2.circle(image_bgr, (100, 100), 40, (50, 150, 50), -1) # Зеленый круг
cv2.circle(image_bgr, (250, 150), 60, (150, 50, 50), 2) # Синий контур круга
gray_image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Для лучшего результата часто применяют размытие, чтобы убрать шум и мелкие детали
gray_blurred = cv2.medianBlur(gray_image, 5) # Медианный фильтр хорошо убирает "соль и перец"
# Применение преобразования Хаффа для окружностей
# dp: обратное отношение разрешения аккумулятора к разрешению изображения (1 - то же, >1 - меньше, например, 2 - аккум. в 2 раза меньше).
# minDist: минимальное расстояние между центрами найденных окружностей. Если круги близко, будет найден только один.
# param1: верхний порог для внутреннего детектора Canny (используется в Hough Gradient Method).
# param2: порог аккумулятора для центров окружностей. Чем меньше, тем больше окружностей будет найдено (включая ложные).
# minRadius, maxRadius: диапазон радиусов для поиска (в пикселях).
circles = cv2.HoughCircles(gray_blurred, method=cv2.HOUGH_GRADIENT, dp=1.2, minDist=50,
param1=100, param2=35, minRadius=10, maxRadius=100)
image_with_circles = image_bgr.copy()
if circles is not None:
circles = np.uint16(np.around(circles)) # Преобразуем координаты и радиус в целые числа
for i in circles[0, :]: # circles[0,:] - это массив найденных кругов [[x,y,r], [x,y,r]...]
# Рисуем внешнюю окружность (контур найденного круга)
cv2.circle(image_with_circles, (i[0], i[1]), i[2], (0, 255, 0), 2) # Зеленый контур
# Рисуем центр окружности
cv2.circle(image_with_circles, (i[0], i[1]), 2, (0, 0, 255), 3) # Красная точка в центре
# Визуализация
# plt.figure(figsize=(10, 5))
# plt.subplot(121), plt.imshow(gray_blurred, cmap='gray'), plt.title('Размытое серое изображение')
# plt.subplot(122), plt.imshow(cv2.cvtColor(image_with_circles, cv2.COLOR_BGR2RGB)), plt.title('Найденные окружности')
# plt.show()
Объяснение кода: - `gray_blurred = cv2.medianBlur(gray_image, 5)`: Медианное размытие часто помогает улучшить результаты детекции окружностей, убирая шум.
- `circles = cv2.HoughCircles(image, method, dp, minDist, param1, param2, minRadius, maxRadius)`:
- `image`: Входное 8-битное одноканальное изображение (серое).
- `method`: Метод детектирования. В OpenCV это `cv2.HOUGH_GRADIENT`.
- `dp`: Обратное отношение разрешения аккумулятора к разрешению изображения. `dp=1` означает то же разрешение. `dp=2` означает, что аккумулятор в два раза меньше по ширине и высоте.
- `minDist`: Минимальное расстояние между центрами детектируемых окружностей. Используется для подавления близко расположенных дубликатов.
- `param1`: Первый специфический для метода параметр. Для `cv2.HOUGH_GRADIENT` это верхний порог для внутреннего детектора краев Canny.
- `param2`: Второй специфический для метода параметр. Для `cv2.HOUGH_GRADIENT` это порог аккумулятора для центров окружностей на этапе обнаружения. Чем он меньше, тем больше ложных окружностей может быть детектировано.
- `minRadius`, `maxRadius`: Минимальный и максимальный радиус искомых окружностей. Указание этих параметров может значительно ускорить поиск и улучшить точность.
- `circles`: Если окружности найдены, это массив NumPy формы `(1, N, 3)`, где `N` — количество найденных окружностей. Каждая окружность представлена как `[x_center, y_center, radius]`.
Пример из реальной жизни: 1. **Обнаружение монет, шаров, колес.**
2. **Медицинская диагностика:** Поиск круглых структур, таких как клетки или зрачки.
3. **Промышленный контроль качества:** Проверка наличия и правильности круглых деталей.
6. Пирамидальные преобразования. Гауссова пирамида, Даунсампл, апсампл. (Pyramidal Transformations. Gaussian Pyramid, Downsample, Upsample)
Объяснение темы:
Представьте, что вы смотрите на фотографию и хотите найти на ней объекты разного размера. Маленький объект может быть хорошо виден, если вы приблизите изображение, а большой объект лучше виден на общем плане. Пирамида изображений — это как раз способ представить одно и то же изображение в нескольких масштабах одновременно.
**Пирамида изображений** — это набор копий одного изображения, но с разным разрешением (размером). Обычно каждое следующее изображение в пирамиде в два раза меньше по ширине и высоте, чем предыдущее. Нижний уровень пирамиды — это исходное изображение в полном разрешении. По мере продвижения вверх по пирамиде разрешение уменьшается.
**Зачем это нужно?**
- **Анализ в разных масштабах:** Алгоритмы могут работать на разных уровнях пирамиды, чтобы эффективно находить объекты или признаки разного размера. Например, грубый поиск объекта на верхних (маленьких) уровнях, а затем уточнение его положения на нижних (больших) уровнях.
- **Уменьшение вычислительной сложности:** Обработка маленьких изображений на верхних уровнях пирамиды гораздо быстрее.
- **Смешивание изображений, сжатие, создание текстур.**
**Гауссова пирамида:** Это самый распространенный тип пирамиды. Она строится путем последовательного размытия изображения Гауссовым фильтром и затем уменьшения его размера вдвое (эта операция называется **даунсэмплинг** или `pyrDown` в OpenCV).
- **Гауссово размытие:** Перед уменьшением размера изображение сглаживается Гауссовым фильтром. Это важно для предотвращения **алиасинга** — появления нежелательных узоров или искажений из-за того, что при простом удалении пикселей теряется высокочастотная информация.
- **Даунсэмплинг (Downsampling, `pyrDown`):** После размытия, каждая вторая строка и каждый второй столбец пикселей удаляются, уменьшая изображение вдвое по каждой оси (вчетверо по площади).
**Апсэмплинг (Upsampling, `pyrUp`):** Это обратная операция — увеличение размера изображения (обычно вдвое). Сначала размер изображения увеличивается (например, путем вставки нулевых строк и столбцов между существующими), а затем результат размывается Гауссовым фильтром, чтобы сгладить появившиеся "ступеньки" и заполнить "пустоты". Апсэмплинг не восстанавливает потерянную при даунсэмплинге информацию, а скорее интерполирует ее.
Методы и Примеры:
Гауссово размытие (cv2.GaussianBlur)
Описание: Это фундаментальная операция сглаживания изображения с использованием Гауссова фильтра. Она необходима для построения Гауссовой пирамиды, а также широко используется для уменьшения шума и предварительной обработки перед другими алгоритмами.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Создадим изображение с резкими краями
original_image = np.zeros((200, 200), dtype=np.uint8)
original_image[50:150, 50:150] = 255 # Белый квадрат на черном фоне
# Применение Гауссова размытия
# ksize: размер ядра Гаусса (ширина, высота). Должен быть нечетным и положительным (например, (5,5), (3,3)).
# sigmaX: стандартное отклонение Гауссова ядра по оси X. Если 0, оно вычисляется из ksize.
# sigmaY (опционально): стандартное отклонение по Y. Если 0, равно sigmaX. Если оба 0, вычисляются из ksize.
kernel_size = (7, 7)
blurred_image = cv2.GaussianBlur(original_image, kernel_size, sigmaX=0)
# plt.figure(figsize=(10, 5))
# plt.subplot(121), plt.imshow(original_image, cmap='gray'), plt.title('Исходное изображение')
# plt.subplot(122), plt.imshow(blurred_image, cmap='gray'), plt.title(f'Гауссово размытие (ядро {kernel_size})')
# plt.show()
Объяснение кода: - `blurred_image = cv2.GaussianBlur(src, ksize, sigmaX, sigmaY, borderType)`:
- `src`: Исходное изображение.
- `ksize`: Кортеж `(ширина_ядра, высота_ядра)`. Оба значения должны быть положительными и нечетными.
- `sigmaX`: Стандартное отклонение Гауссовой функции по оси X. Большее значение `sigmaX` приводит к более сильному размытию.
- `sigmaY` (опционально): Стандартное отклонение по оси Y. Если не указано, оно принимается равным `sigmaX`.
- `borderType` (опционально): Определяет, как обрабатывать пиксели на границах изображения.
- Если `sigmaX` (и `sigmaY`) равны 0, то стандартные отклонения вычисляются из `ksize` по формулам OpenCV, что часто является удобным поведением по умолчанию.
Пример из реальной жизни: 1. **Уменьшение шума:** Гауссово размытие эффективно сглаживает случайный шум.
2. **Предобработка для детекторов краев:** Например, перед детектором Canny, чтобы уменьшить ложные срабатывания на шуме.
3. **Основа для Гауссовой пирамиды.**
4. **Создание эффекта "вне фокуса" (bokeh) или мягкого свечения.**
Даунсэмплинг (Уменьшение масштаба - cv2.pyrDown)
Описание: Эта функция уменьшает размер изображения вдвое по каждой оси. Внутренне она сначала применяет Гауссово размытие к исходному изображению (с использованием ядра примерно 5x5), а затем удаляет все четные строки и столбцы.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# original_image = cv2.imread('high_res_image.jpg')
# if original_image is None: print('Ошибка загрузки'); exit()
original_image = np.zeros((200, 300, 3), dtype=np.uint8)
cv2.putText(original_image, "OpenCV", (30, 100), cv2.FONT_HERSHEY_SIMPLEX, 2, (255,255,255), 3)
# Уменьшение изображения в 2 раза по каждой оси
# dstsize (опционально): желаемый размер выходного изображения. По умолчанию (ширина/2, высота/2).
downsampled_image = cv2.pyrDown(original_image)
# print(f"Размер исходного изображения: {original_image.shape}")
# print(f"Размер уменьшенного изображения: {downsampled_image.shape}")
# plt.figure(figsize=(12, 6))
# plt.subplot(121), plt.imshow(cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)), plt.title('Исходное')
# plt.subplot(122), plt.imshow(cv2.cvtColor(downsampled_image, cv2.COLOR_BGR2RGB)), plt.title('Уменьшенное (pyrDown)')
# plt.show()
Объяснение кода: - `downsampled_image = cv2.pyrDown(src, dstsize, borderType)`:
- `src`: Исходное изображение.
- `dstsize` (опционально): Целевой размер. Если не указан (или указан как `None`), то выходное изображение будет иметь размеры `(src.cols/2, src.rows/2)`. Если указан, он должен быть `(src.cols/2, src.rows/2)` или `((src.cols+1)/2, (src.rows+1)/2)` для нечетных размеров.
- `borderType` (опционально): Тип границы.
- Функция сначала размывает `src` Гауссовым фильтром 5x5, а затем выбирает пиксели `dst(i,j) = src(2i, 2j)`.
Пример из реальной жизни: 1. **Построение Гауссовой пирамиды:** Основной шаг для создания каждого следующего уровня.
2. **Ускорение обработки:** Если точные детали не важны, можно сначала уменьшить изображение и работать с ним.
3. **Поиск объектов разного масштаба:** Объект может быть маленьким на исходном изображении, но большим на одном из верхних уровней пирамиды, где его легче обнаружить.
Апсэмплинг (Увеличение масштаба - cv2.pyrUp)
Описание: Эта функция увеличивает размер изображения вдвое по каждой оси. Сначала она создает изображение вдвое большего размера, заполняя "новые" пиксели нулями (или используя интерполяцию). Затем к этому увеличенному изображению применяется Гауссово размытие (с ядром, обычно в 4 раза большим по площади, чем для `pyrDown`), чтобы сгладить результат и интерполировать значения.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# small_image = cv2.imread('low_res_image.jpg')
# if small_image is None: print('Ошибка загрузки'); exit()
small_image = np.zeros((100, 150, 3), dtype=np.uint8)
cv2.putText(small_image, "CV", (20, 70), cv2.FONT_HERSHEY_SIMPLEX, 2, (255,0,255), 3)
# Увеличение изображения в 2 раза по каждой оси
# dstsize (опционально): желаемый размер выходного изображения. По умолчанию (ширина*2, высота*2).
upsampled_image = cv2.pyrUp(small_image)
# print(f"Размер исходного (маленького) изображения: {small_image.shape}")
# print(f"Размер увеличенного изображения: {upsampled_image.shape}")
# plt.figure(figsize=(12, 6))
# plt.subplot(121), plt.imshow(cv2.cvtColor(small_image, cv2.COLOR_BGR2RGB)), plt.title('Маленькое исходное')
# plt.subplot(122), plt.imshow(cv2.cvtColor(upsampled_image, cv2.COLOR_BGR2RGB)), plt.title('Увеличенное (pyrUp)')
# plt.show()
Объяснение кода: - `upsampled_image = cv2.pyrUp(src, dstsize, borderType)`:
- `src`: Исходное изображение.
- `dstsize` (опционально): Целевой размер. Если не указан (или указан как `None`), то выходное изображение будет иметь размеры `(src.cols*2, src.rows*2)`. Если указан, он должен быть равен этому значению.
- `borderType` (опционально): Тип границы.
- Процесс: сначала изображение увеличивается вдвое, при этом новые строки и столбцы заполняются нулями. Затем результат размывается Гауссовым фильтром 5x5 (фактически, это эквивалентно применению фильтра, который является результатом свертки ядра Гаусса с фильтром, расширяющим изображение, поэтому размытие происходит эффективнее).
Пример из реальной жизни: 1. **Построение Лапласовой пирамиды (см. Билет 7):** `pyrUp` используется для расширения уровня Гауссовой пирамиды перед вычитанием.
2. **Некоторые техники масштабирования изображений:** Хотя для качественного увеличения есть более продвинутые методы интерполяции (например, бикубическая), `pyrUp` представляет собой один из способов.
3. **Реконструкция изображений в некоторых алгоритмах.**
7. Пирамидальные преобразования. Лаплассова и габорова пирамиды. Даунсампл-апсампл пирамиды (Pyramidal Transformations. Laplacian and Gabor Pyramids. Downsample-Upsample Pyramids)
Объяснение темы:
Мы уже познакомились с Гауссовой пирамидой (Билет 6), которая представляет изображение на разных уровнях размытия и разрешения. Теперь рассмотрим другие типы пирамид, которые извлекают иную информацию.
**Лапласова пирамида:**
Если Гауссова пирамида хранит "сглаженные" версии изображения, то Лапласова пирамида хранит **разностную информацию** — то, что было потеряно при переходе от одного уровня Гауссовой пирамиды к следующему, более размытому. Представьте, что `G_i` — это i-й уровень Гауссовой пирамиды. Мы можем взять следующий, более грубый уровень `G_{i+1}`, увеличить его обратно до размера `G_i` (используя `pyrUp`, который мы назовем `expand(G_{i+1})`), и вычесть результат из `G_i`. Эта разница `L_i = G_i - expand(G_{i+1})` и будет i-м уровнем Лапласовой пирамиды.
Каждый слой `L_i` Лапласовой пирамиды выглядит как изображение, содержащее только детали и края, которые присутствовали на уровне `G_i`, но отсутствовали на `expand(G_{i+1})`. Это похоже на применение оператора Лапласа (фильтра выделения краев). Самый верхний уровень Лапласовой пирамиды обычно совпадает с самым верхним уровнем Гауссовой пирамиды (самое маленькое, размытое изображение), так как для него нет следующего уровня для вычитания.
**Преимущество Лапласовой пирамиды:** Она позволяет восстановить исходное изображение с минимальными потерями, просто суммируя все слои Лапласиана (после их апсэмплинга до нужного размера) и самый верхний Гауссов слой. `G_i = L_i + expand(G_{i+1})`. Это свойство используется в сжатии изображений, смешивании изображений (например, для создания бесшовных коллажей) и улучшении деталей.
**Габорова пирамида (или Управляемая пирамида, Steerable Pyramid):**
Это более сложный тип пирамиды, который использует **фильтры Габора** вместо простых Гауссовых фильтров. Фильтр Габора — это, по сути, синусоидальная волна, модулированная (умноженная) Гауссовой функцией. Он чувствителен к определенным пространственным частотам И определенным **ориентациям** на изображении. Габорова пирамида разлагает изображение на компоненты, которые представляют информацию об интенсивности различных текстур и контуров на разных масштабах и под разными углами. Это делает ее очень полезной для анализа текстур, выделения контуров и извлечения признаков, чувствительных к ориентации.
**Даунсампл-апсампл пирамиды:**
Это общий термин, описывающий любой подход, где операции уменьшения (даунсампл) и увеличения (апсампл) разрешения используются для создания многомасштабного представления изображения. Гауссова и Лапласова пирамиды являются частными случаями таких пирамид.
Методы и Примеры:
Построение одного уровня Лапласовой пирамиды
Описание: Один уровень Лапласовой пирамиды, `L_i`, получается как разность между `i`-м уровнем Гауссовой пирамиды (`G_i`) и увеличенной (апсэмплированной и размытой) версией следующего, более грубого уровня Гауссовой пирамиды (`G_{i+1}`). Этот слой `L_i` содержит высокочастотную информацию ("детали"), которая была "потеряна" при переходе от `G_i` к `G_{i+1}`.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# original_image = cv2.imread('lena.png')
# if original_image is None: print('Ошибка загрузки'); exit()
# Создадим тестовое изображение
original_image = np.zeros((256, 256, 3), dtype=np.uint8)
original_image.fill(128) # Серый фон
cv2.rectangle(original_image, (50,50), (200,200), (255,255,255), -1) # Белый квадрат
cv2.putText(original_image, "Hi", (80,150), cv2.FONT_HERSHEY_SIMPLEX, 2, (0,0,0), 3)
# G0 - исходное изображение (нулевой уровень Гауссовой пирамиды)
G0 = original_image.copy()
# G1 - следующий уровень Гауссовой пирамиды
G1 = cv2.pyrDown(G0)
# expand(G1) - апсэмплинг G1 до размера G0
# Важно, чтобы размеры точно совпадали. pyrUp может дать размер чуть меньше, если исходный был нечетным.
# Поэтому лучше явно указать dstsize.
G1_expanded = cv2.pyrUp(G1, dstsize=(G0.shape[1], G0.shape[0]))
# L0 - нулевой уровень Лапласовой пирамиды
# cv2.subtract обеспечивает корректную обработку значений (насыщение)
L0 = cv2.subtract(G0, G1_expanded)
# Для визуализации L0 может потребоваться сдвиг, т.к. он содержит отрицательные значения
# L0_display = cv2.add(L0, np.array([128,128,128], dtype=np.uint8)) # Сдвиг на 128 для отображения
L0_display = cv2.normalize(L0, None, 0, 255, cv2.NORM_MINMAX, dtype=cv2.CV_8U)
# plt.figure(figsize=(15, 5))
# plt.subplot(131), plt.imshow(cv2.cvtColor(G0, cv2.COLOR_BGR2RGB)), plt.title('G0 (Исходное)')
# plt.subplot(132), plt.imshow(cv2.cvtColor(G1_expanded, cv2.COLOR_BGR2RGB)), plt.title('expand(G1)')
# plt.subplot(133), plt.imshow(cv2.cvtColor(L0_display, cv2.COLOR_BGR2RGB)), plt.title('L0 (Уровень Лапласиана)')
# plt.show()
Объяснение кода: 1. `G0 = original_image.copy()`: Исходное изображение — это нулевой уровень Гауссовой пирамиды.
2. `G1 = cv2.pyrDown(G0)`: Создаем первый уровень Гауссовой пирамиды путем даунсэмплинга `G0`.
3. `G1_expanded = cv2.pyrUp(G1, dstsize=(G0.shape[1], G0.shape[0]))`: Увеличиваем `G1` обратно до размера `G0`. Параметр `dstsize` важен, чтобы гарантировать совпадение размеров, так как `pyrDown` может немного изменять размер при нечетных исходных размерах, и `pyrUp` без `dstsize` может не точно восстановить исходный размер `G0`.
4. `L0 = cv2.subtract(G0, G1_expanded)`: Вычисляем разницу. `cv2.subtract` выполняет насыщенную арифметику, то есть значения обрезаются до диапазона [0, 255] для `uint8`. `L0` будет содержать детали, которые есть в `G0`, но сглажены в `G1_expanded`.
5. `L0_display = cv2.normalize(...)`: Поскольку `L0` содержит как положительные, так и отрицательные разности (хотя после `cv2.subtract` для `uint8` они будут обрезаны до 0), для корректной визуализации его значения часто нормализуют к диапазону [0, 255] или добавляют смещение (например, 128) и затем нормализуют.
Пример из реальной жизни: 1. **Смешивание изображений (Image Blending/Poisson Blending):** Лапласова пирамида используется для плавного смешивания частей одного изображения с другим, минимизируя видимые швы. Например, при создании фотоколлажей или замене лица на фотографии.
2. **Сжатие изображений:** Так как слои Лапласиана содержат много нулей или близких к нулю значений (особенно для гладких областей), их можно эффективно сжать.
3. **Улучшение резкости и деталей (Detail Enhancement):** Можно усилить коэффициенты в Лапласовой пирамиде перед реконструкцией, чтобы сделать детали более выраженными.
4. **Анализ текстур и выделение краев.**
Применение фильтра Габора (cv2.getGaborKernel, cv2.filter2D)
Описание: Фильтр Габора — это линейный фильтр, используемый для анализа текстур, выделения краев и извлечения признаков, чувствительных к ориентации и частоте. Ядро фильтра Габора создается функцией `cv2.getGaborKernel`, а затем применяется к изображению с помощью `cv2.filter2D`.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# original_image_gray = cv2.imread('textured_surface.jpg', cv2.IMREAD_GRAYSCALE)
# if original_image_gray is None: print('Ошибка загрузки'); exit()
# Создадим тестовое изображение с текстурой (горизонтальные линии)
original_image_gray = np.zeros((200, 200), dtype=np.uint8)
for i in range(0, 200, 10):
cv2.line(original_image_gray, (0, i), (199, i), 255, 2)
# Параметры для ядра Габора
ksize = 31 # Размер ядра (должен быть нечетным). Чем больше, тем точнее, но медленнее.
sigma = 4.0 # Стандартное отклонение гауссовой огибающей.
theta_rad = np.pi / 2 # Ориентация: 0 для вертикальных, np.pi/2 для горизонтальных линий.
lamda = 10.0 # Длина волны синусоидального фактора.
gamma = 0.5 # Коэффициент пространственного соотношения (эллиптичность). 1 - круг, <1 - вытянутый.
phi = 0 # Фазовый сдвиг (в радианах). 0 и np.pi/2 дают четный и нечетный фильтры.
# Создание ядра Габора
gabor_kernel = cv2.getGaborKernel((ksize, ksize), sigma, theta_rad, lamda, gamma, phi, ktype=cv2.CV_32F)
# Применение фильтра Габора к изображению
# ddepth=-1 означает, что выходное изображение будет иметь ту же глубину, что и входное.
filtered_image_gabor = cv2.filter2D(original_image_gray, cv2.CV_32F, gabor_kernel)
# Для визуализации ядра и результата
kernel_display = cv2.normalize(gabor_kernel, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
filtered_display = cv2.normalize(filtered_image_gabor, None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
# plt.figure(figsize=(15, 5))
# plt.subplot(131), plt.imshow(original_image_gray, cmap='gray'), plt.title('Исходное серое')
# plt.subplot(132), plt.imshow(kernel_display, cmap='gray'), plt.title('Ядро Габора (theta=pi/2)')
# plt.subplot(133), plt.imshow(filtered_display, cmap='gray'), plt.title('Отклик фильтра Габора')
# plt.show()
Объяснение кода: - `gabor_kernel = cv2.getGaborKernel(ksize, sigma, theta, lambd, gamma, psi, ktype)`:
- `ksize`: Кортеж `(width, height)` размера ядра.
- `sigma`: Стандартное отклонение Гауссовой огибающей.
- `theta`: Ориентация нормали к параллельным полосам функции Габора (в радианах).
- `lambd` (здесь `lamda` в коде): Длина волны синусоидального компонента.
- `gamma`: Пространственное соотношение сторон (эллиптичность). Определяет форму Гауссовой огибающей.
- `psi` (здесь `phi` в коде): Фазовый сдвиг (в радианах).
- `ktype`: Тип данных элементов ядра (например, `cv2.CV_32F` или `cv2.CV_64F`).
- `filtered_image_gabor = cv2.filter2D(src, ddepth, kernel)`:
- `src`: Входное изображение.
- `ddepth`: Желаемая глубина (тип данных) выходного изображения. `-1` означает, что глубина будет такой же, как у `src`. Рекомендуется использовать `cv2.CV_32F` или `cv2.CV_64F` для фильтрации, чтобы избежать потери точности, так как отклики Габора могут быть отрицательными.
- `kernel`: Ядро свертки (в данном случае, `gabor_kernel`).
- `cv2.normalize(...)`: Используется для масштабирования значений ядра и отфильтрованного изображения к диапазону [0, 255] для удобной визуализации в виде изображения `uint8`.
Пример из реальной жизни: 1. **Анализ и сегментация текстур:** Фильтры Габора очень эффективны для различения текстур, так как они могут быть настроены на определенные частоты и ориентации, характерные для текстуры.
2. **Распознавание отпечатков пальцев:** Линии на отпечатках пальцев имеют характерные ориентации и частоты.
3. **Выделение краев и контуров определенной ориентации.**
4. **Извлечение признаков для классификации изображений:** Банк фильтров Габора (набор фильтров с разными параметрами) может использоваться для создания богатого набора признаков.
5. **Габорова пирамида:** Применяет фильтры Габора на разных масштабах и ориентациях для многомасштабного анализа текстур и ориентаций.
8. Текстуры. Генерация текстур (Textures. Texture Generation)
Объяснение темы:
Представьте себе поверхность дерева, ткань, кирпичную стену, траву на лужайке или воду с рябью. Все это примеры **текстур**. Текстура — это визуальный узор, характеризующийся повторяющимися элементами (называемыми **текстонами** или текстурными примитивами), их цветом, формой, размером и пространственным расположением. Текстуры могут быть:
- **Регулярными:** Узор повторяется строго и предсказуемо (например, шахматная доска, кирпичная кладка).
- **Стохастическими (случайными):** Узор выглядит случайно, но имеет определенные статистические свойства (например, песок, гранит, облака).
- **Ориентированными:** Элементы текстуры имеют преобладающее направление (например, древесина, ткань в полоску).
**Анализ текстур** — это процесс описания и количественной оценки свойств текстуры. Это нужно, чтобы, например, классифицировать материалы (дерево, металл, ткань), сегментировать изображение на области с разными текстурами или находить дефекты на текстурированных поверхностях.
**Генерация (синтез) текстур** — это процесс создания новых изображений текстур, которые либо:
- Визуально похожи на заданный образец текстуры.
- Соответствуют определенным математическим правилам или статистическим свойствам.
Генерация текстур важна для компьютерной графики (создание реалистичных поверхностей для 3D-моделей), игр, заполнения недостающих областей на изображениях (inpainting), создания фонов и т.д.
Методы и Примеры:
Локальные бинарные шаблоны (Local Binary Patterns - LBP) - для анализа текстур
Описание: LBP — это простой, но очень эффективный дескриптор текстуры, который хорошо работает для описания локальных пространственных структур. Для каждого пикселя изображения сравнивается его яркость с яркостью его соседей. Если яркость соседа больше или равна яркости центрального пикселя, ему присваивается 1, иначе 0. Эти бинарные значения (обычно 8 от соседей по кругу) объединяются в бинарное число (LBP-код). Гистограмма этих LBP-кодов, собранная по всему изображению или его части, используется как признак текстуры. LBP инвариантен к монотонным изменениям освещения.
Пример кода:
from skimage.feature import local_binary_pattern # skimage предоставляет удобную реализацию
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Загрузка изображения или создание текстурного
# texture_image_gray = cv2.imread('fabric_texture.jpg', cv2.IMREAD_GRAYSCALE)
# if texture_image_gray is None: print('Ошибка загрузки'); exit()
# Создадим простое текстурное изображение
texture_image_gray = np.zeros((128, 128), dtype=np.uint8)
for r_idx, r_val in enumerate(range(0, 128, 16)):
for c_idx, c_val in enumerate(range(0, 128, 16)):
val = 50 + ((r_idx + c_idx) % 2) * 150 # Шахматный узор
texture_image_gray[r_val:r_val+16, c_val:c_val+16] = val
# Параметры для LBP
radius = 1 # Радиус окружности, на которой берутся соседи (1 - ближайшие 8 соседей)
n_points = 8 * radius # Количество точек-соседей (обычно 8, 16, 24)
# Вычисление LBP
# method='uniform': использует только "однородные" шаблоны (не более двух переходов 0->1 или 1->0 в бинарной строке).
# Это делает дескриптор более робастным к вращению и значительно уменьшает количество возможных LBP-кодов.
lbp_image = local_binary_pattern(texture_image_gray, n_points, radius, method='uniform')
lbp_image_display = lbp_image.astype(np.uint8) # Для визуализации (значения LBP могут быть > 255)
# Гистограмма LBP кодов как дескриптор текстуры
# Для 'uniform' метода с P точками, будет P+2 уникальных значения LBP
# (P однородных шаблонов, 1 для всех неоднородных, и 1 для центрального пикселя, если его сравнивают)
# Однако, skimage.feature.local_binary_pattern возвращает коды от 0 до n_points (для 'uniform'),
# и n_points+1 для всех неоднородных. Таким образом, n_bins = n_points + 2.
num_bins_lbp = int(lbp_image.max() + 1)
hist_lbp, _ = np.histogram(lbp_image.ravel(), bins=num_bins_lbp, range=(0, num_bins_lbp))
# Нормализация гистограммы (чтобы сумма была равна 1)
hist_lbp = hist_lbp.astype("float")
hist_lbp /= (hist_lbp.sum() + 1e-7) # + малое число для избежания деления на ноль
# print(f"LBP гистограмма (первые 10 бинов): {hist_lbp[:10]}")
# print(f"Всего бинов в LBP гистограмме: {len(hist_lbp)}")
# Визуализация
# plt.figure(figsize=(12, 6))
# plt.subplot(131), plt.imshow(texture_image_gray, cmap='gray'), plt.title('Исходная текстура')
# plt.subplot(132), plt.imshow(lbp_image_display, cmap='gray'), plt.title('LBP изображение')
# plt.subplot(133), plt.bar(np.arange(num_bins_lbp), hist_lbp), plt.title('LBP гистограмма')
# plt.tight_layout()
# plt.show()
Объяснение кода: - `from skimage.feature import local_binary_pattern`: Используем LBP из библиотеки scikit-image, так как она предоставляет гибкую реализацию, включая 'uniform' паттерны.
- `radius`, `n_points`: Параметры, определяющие окрестность. `radius=1, n_points=8` — это классическая 3x3 окрестность.
- `lbp_image = local_binary_pattern(image, n_points, radius, method)`:
- `image`: Входное изображение в градациях серого.
- `method`: Метод вычисления LBP. 'uniform' — популярный выбор. Другие варианты: 'default', 'ror' (rotation-invariant).
- `lbp_image`: Изображение, где каждый пиксель заменен на его LBP-код.
- `np.histogram(lbp_image.ravel(), ...)`: Вычисляет гистограмму LBP-кодов. `lbp_image.ravel()` преобразует 2D LBP-изображение в 1D массив.
- `bins`: Количество бинов. Для 'uniform' метода с `P` точками, это `P+2`.
- `range`: Диапазон значений для гистограммы.
- `hist_lbp /= (hist_lbp.sum() + 1e-7)`: Нормализация гистограммы, чтобы она стала вектором признаков, не зависящим от размера области.
Пример из реальной жизни: 1. **Классификация текстур:** Определение типа материала (ткань, дерево, камень) по его LBP-гистограмме.
2. **Распознавание лиц:** LBP используется для описания текстурных особенностей лица, что делает метод устойчивым к изменениям освещения.
3. **Обнаружение объектов:** Например, в биометрии для анализа текстуры радужной оболочки глаза или отпечатков пальцев.
4. **Медицинская диагностика:** Анализ текстуры тканей на медицинских снимках.
Синтез текстур методом "квилтинга" патчей (Patch Quilting - Концептуально)
Описание: Это непараметрический метод генерации текстур, который создает новое, большее изображение текстуры, копируя и "сшивая" небольшие прямоугольные участки (патчи) из предоставленного образца текстуры. Ключевой момент — выбор подходящих патчей и их аккуратное соединение вдоль "швов" минимальной ошибки в областях перекрытия, чтобы избежать видимых артефактов.
Пример кода:
# OpenCV не имеет встроенной функции для "квилтинга" патчей.
# Это более сложный алгоритм, требующий отдельной реализации или использования специализированных библиотек.
# Ниже представлен псевдокод, описывающий основную идею.
"""
Функция PatchQuilting(образец_текстуры, размер_выходной_текстуры, размер_патча, размер_перекрытия):
1. Инициализировать пустое выходное_изображение нужного размера.
2. Скопировать случайный патч из образца_текстуры в левый верхний угол выходного_изображения.
3. Для каждого следующего места под патч в выходном_изображении (проходя по нему, например, слева направо, сверху вниз):
a. Определить область(и) перекрытия с уже размещенными патчами.
b. Найти в образце_текстуры все патчи, которые могли бы подойти.
c. Для каждого кандидата из образца, вычислить ошибку (например, сумму квадратов разностей пикселей)
в области(ях) перекрытия с уже синтезированной частью выходного_изображения.
d. Выбрать патч-кандидат из образца, который дает минимальную ошибку (или один из нескольких лучших случайным образом).
e. Если есть перекрытие (например, слева и/или сверху):
i. В области перекрытия между выбранным патчем-кандидатом и существующей частью
выходного_изображения, найти путь минимальной ошибки (шов). Это можно сделать
с помощью динамического программирования (похоже на поиск кратчайшего пути в графе).
ii. Скопировать пиксели из патча-кандидата в выходное_изображение, используя найденный шов,
чтобы плавно соединить новый патч с существующим.
f. Иначе (если нет перекрытия, например, для первого патча в строке/столбце, кроме самого первого):
Просто скопировать выбранный патч-кандидат.
4. Вернуть синтезированное выходное_изображение.
"""
# print("Метод квилтинга патчей концептуальный и требует кастомной реализации.")
Объяснение кода: 1. **Инициализация:** Начинаем с пустого холста и копируем один случайный патч из образца.
2. **Итеративное заполнение:** Проходим по выходному изображению, заполняя его патчами.
3. **Выбор патча:** Для текущей пустой позиции ищем в образце такой патч, который лучше всего "стыкуется" с уже размещенными соседними патчами в областях их перекрытия. "Лучше всего" означает минимальную разницу (ошибку) между пикселями в перекрывающихся частях.
4. **Поиск шва (Minimum Error Boundary Cut):** Чтобы стык между новым и старым патчами был незаметным, в области перекрытия ищется "шов" — путь, по которому разница между пикселями двух патчей минимальна. Пиксели по одну сторону шва берутся из старого патча, по другую — из нового.
5. **Копирование:** Новый патч (или его части, определенные швом) копируется в выходное изображение.
Пример из реальной жизни: 1. **Создание бесшовных текстур для 3D-моделирования и игр:** Из небольшого образца можно сгенерировать большую текстуру, которая будет выглядеть естественно и не иметь видимых повторений.
2. **Заполнение недостающих областей на изображениях (Inpainting):** Если на фото есть поврежденный участок, его можно "залатать" текстурой, сгенерированной из окружающих областей.
3. **Художественная стилизация и передача стиля.**
4. **Создание фонов и обоев.
9. Локальные признаки. SIFT, ORB (Local Features. SIFT, ORB)
Объяснение темы:
Представьте, что вы пытаетесь распознать книгу на столе, даже если вы сфотографировали ее с другого ракурса, при другом освещении, или если часть книги закрыта. Вы не будете сравнивать все изображение целиком. Вместо этого ваш мозг, вероятно, найдет какие-то **характерные точки** или небольшие участки на книге (например, уголки букв, особые элементы дизайна обложки) и будет искать их на другом изображении.
**Локальные признаки (Local Features)** в компьютерном зрении работают похожим образом. Это "интересные" точки или области на изображении, которые обладают следующими свойствами:
- **Повторяемость (Repeatability):** Их можно надежно обнаружить на разных изображениях одного и того же объекта или сцены, даже если изменились условия съемки (масштаб, поворот, освещение, точка обзора).
- **Различимость (Distinctiveness):** Окрестность каждой такой точки должна быть достаточно уникальной, чтобы ее можно было отличить от окрестностей других точек.
- **Локальность:** Признаки описывают только небольшую область изображения, что делает их устойчивыми к частичным перекрытиям (окклюзиям) и беспорядку на фоне.
Каждый локальный признак обычно состоит из двух частей:
1. **Детектор ключевых точек (Keypoint Detector):** Алгоритм, который находит местоположение `(x,y)` этих характерных точек на изображении. Некоторые детекторы также определяют масштаб (размер характерной области) и ориентацию (направление) ключевой точки.
2. **Дескриптор (Descriptor):** Алгоритм, который вычисляет вектор чисел (дескриптор), описывающий внешний вид окрестности ключевой точки. Этот вектор должен быть таким, чтобы похожие окрестности давали похожие дескрипторы, а разные — разные.
**SIFT (Scale-Invariant Feature Transform — Масштабно-инвариантное преобразование признаков):**
Один из самых известных и мощных алгоритмов для обнаружения и описания локальных признаков. Ключевые точки SIFT инвариантны к масштабу и повороту изображения, а также частично устойчивы к изменениям освещения и небольшим изменениям точки обзора (аффинным искажениям).
**ORB (Oriented FAST and Rotated BRIEF):**
Более быстрая и, что важно, бесплатная (непатентованная) альтернатива SIFT и SURF. ORB стремится обеспечить хорошую производительность при меньших вычислительных затратах. Он также инвариантен к повороту и умеренно устойчив к масштабу и изменениям освещения.
Методы и Примеры:
SIFT (Scale-Invariant Feature Transform)
Описание: Обнаруживает и описывает локальные признаки, которые инвариантны к масштабу, повороту, изменениям освещения и аффинным искажениям. Возвращает набор ключевых точек и их 128-мерных дескрипторов.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Загрузка изображения или создание тестового
# image_bgr = cv2.imread('book_in_scene.jpg')
# if image_bgr is None: print('Ошибка загрузки'); exit()
# Создадим тестовое изображение
image_bgr = np.zeros((300, 400, 3), dtype=np.uint8)
image_bgr.fill(200) # Светло-серый фон
cv2.ellipse(image_bgr, (150,150), (80,40), 30, 0, 360, (50,100,150), -1)
cv2.putText(image_bgr, "SIFT", (180, 180), cv2.FONT_HERSHEY_SIMPLEX, 1.5, (255,255,255), 3)
gray_image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Для использования SIFT может потребоваться установка opencv-contrib-python:
# pip install opencv-contrib-python
# В некоторых версиях OpenCV, SIFT и SURF могут быть недоступны из-за патентов,
# или находиться в модуле xfeatures2d.
image_with_sift_keypoints = image_bgr.copy()
try:
# Пытаемся создать SIFT детектор
sift = cv2.SIFT_create() # Для OpenCV 4.4+ (если есть contrib)
# Если ошибка, пробуем старый путь для xfeatures2d (OpenCV 3.x, или 4.x с полным contrib)
# if 'xfeatures2d' not in cv2.__dict__:
# sift = cv2.xfeatures2d.SIFT_create()
# Обнаружение ключевых точек и вычисление дескрипторов
# keypoints: список объектов cv2.KeyPoint
# descriptors: NumPy массив, где каждая строка — 128-мерный вектор-дескриптор
keypoints, descriptors = sift.detectAndCompute(gray_image, None) # None - без маски
# Отрисовка ключевых точек на изображении
# Флаг DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS рисует круги с размером и ориентацией
image_with_sift_keypoints = cv2.drawKeypoints(gray_image, keypoints, image_bgr.copy(),
flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS)
# print(f"Найдено SIFT ключевых точек: {len(keypoints)}")
# if descriptors is not None:
# print(f"Размерность SIFT дескрипторов: {descriptors.shape}")
except AttributeError as e:
print(f"SIFT не доступен в вашей сборке OpenCV: {e}. Пропускаем пример SIFT.")
keypoints, descriptors = [], None # Устанавливаем пустые значения
except cv2.error as e: # Ловим cv2.error, если sift.create() не найден
print(f"Ошибка при создании SIFT (возможно, модуль xfeatures2d не найден или SIFT запатентован): {e}")
keypoints, descriptors = [], None
# Визуализация
# if keypoints: # Если точки были найдены
# plt.figure(figsize=(8, 6))
# plt.imshow(cv2.cvtColor(image_with_sift_keypoints, cv2.COLOR_BGR2RGB))
# plt.title('SIFT Ключевые точки')
# plt.show()
# else:
# print("SIFT точки не были найдены или SIFT недоступен.")
Объяснение кода: - `sift = cv2.SIFT_create()`: Инициализирует SIFT детектор/дескриптор. В старых версиях OpenCV (или если SIFT находится в `xfeatures2d` из-за патентов) может потребоваться `sift = cv2.xfeatures2d.SIFT_create()`.
- `keypoints, descriptors = sift.detectAndCompute(gray_image, mask)`:
- `gray_image`: Входное изображение в градациях серого.
- `mask` (опционально): Маска, указывающая, в какой области изображения искать признаки.
- `keypoints`: Список объектов `cv2.KeyPoint`. Каждый такой объект содержит информацию о точке: `pt` (координаты x,y), `size` (диаметр окрестности), `angle` (ориентация в градусах), `response` (сила отклика), `octave` (октава пирамиды), `class_id`.
- `descriptors`: NumPy массив типа `float32` размером `(N, 128)`, где `N` — количество найденных ключевых точек. Каждая строка — это 128-мерный вектор-дескриптор для соответствующей ключевой точки.
- `cv2.drawKeypoints(image, keypoints, outImage, color, flags)`: Рисует ключевые точки на изображении.
- `flags=cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS`: Рисует не просто точки, а круги, показывающие размер и ориентацию каждой ключевой точки.
Пример из реальной жизни: 1. **Распознавание объектов:** SIFT очень хорошо подходит для идентификации конкретных объектов (например, логотипов, достопримечательностей) на изображениях.
2. **Создание панорам (Image Stitching):** Нахождение соответствующих SIFT-признаков на перекрывающихся изображениях для их точного совмещения.
3. **3D-реконструкция (Structure from Motion):** Использование SIFT-соответствий между несколькими изображениями для восстановления 3D-сцены и траектории камеры.
4. **Робототехническая навигация (SLAM):** Построение карты окружающей среды и одновременная локализация робота на этой карте.
5. **Поиск изображений по содержанию (Content-Based Image Retrieval - CBIR).**
ORB (Oriented FAST and Rotated BRIEF)
Описание: Быстрый и эффективный детектор и дескриптор локальных признаков, являющийся хорошей бесплатной альтернативой SIFT/SURF. Он сочетает детектор FAST с модифицированным бинарным дескриптором BRIEF.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Используем то же тестовое изображение, что и для SIFT
image_bgr = np.zeros((300, 400, 3), dtype=np.uint8); image_bgr.fill(180)
cv2.ellipse(image_bgr, (200,150), (100,50), -20, 0, 360, (80,120,180), -1)
cv2.putText(image_bgr, "ORB", (80, 100), cv2.FONT_HERSHEY_SIMPLEX, 1.8, (255,255,0), 3)
gray_image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Инициализация ORB детектора
# nfeatures: Максимальное количество признаков для обнаружения (по умолчанию 500).
# scaleFactor: Коэффициент масштабирования пирамиды (по умолчанию 1.2).
# nlevels: Количество уровней пирамиды (по умолчанию 8).
# edgeThreshold: Размер границы, где признаки не детектируются.
# وغيرها من المعلمات...
orb = cv2.ORB_create(nfeatures=1000)
# Обнаружение ключевых точек и вычисление дескрипторов
# С ORB, detectAndCompute() является предпочтительным, так как он эффективнее, чем вызов detect() и compute() по отдельности.
keypoints_orb, descriptors_orb = orb.detectAndCompute(gray_image, None) # None - без маски
# Отрисовка ключевых точек ORB
image_with_orb_keypoints = cv2.drawKeypoints(gray_image, keypoints_orb, image_bgr.copy(),
color=(0,255,0), flags=0) # color - зеленый, flags=0 - простые круги
# print(f"Найдено ORB ключевых точек: {len(keypoints_orb)}")
# if descriptors_orb is not None:
# print(f"Размерность ORB дескрипторов: {descriptors_orb.shape} (бинарные, 32 байта = 256 бит)")
# Визуализация
# plt.figure(figsize=(8, 6))
# plt.imshow(cv2.cvtColor(image_with_orb_keypoints, cv2.COLOR_BGR2RGB))
# plt.title('ORB Ключевые точки')
# plt.show()
Объяснение кода: - `orb = cv2.ORB_create(nfeatures=..., scaleFactor=..., nlevels=...)`: Инициализирует ORB детектор/дескриптор.
- `nfeatures`: Желаемое (максимальное) количество ключевых точек. Алгоритм попытается найти это количество лучших точек.
- `scaleFactor`: Коэффициент масштабирования между уровнями пирамиды. Должен быть > 1.0.
- `nlevels`: Количество уровней в пирамиде изображений.
- `keypoints_orb, descriptors_orb = orb.detectAndCompute(gray_image, None)`: Работает аналогично SIFT, но:
- `descriptors_orb`: NumPy массив типа `uint8` размером `(N, 32)`. Каждая строка — это 32-байтный (256-битный) бинарный дескриптор.
- `cv2.drawKeypoints(...)`: Рисует ключевые точки. Для ORB обычно используют простые круги (`flags=0`), так как ориентация уже учтена в дескрипторе rBRIEF.
Пример из реальной жизни: 1. **Быстрое сопоставление изображений в реальном времени:** ORB значительно быстрее SIFT, что делает его подходящим для приложений, где важна скорость (например, на мобильных устройствах).
2. **Отслеживание объектов (Object Tracking).**
3. **Создание панорам (менее точное, чем SIFT, но быстрее).**
4. **Дополненная реальность (Augmented Reality).**
5. **Визуальная одометрия (Visual Odometry):** Оценка движения камеры по последовательности изображений.
10. Панорамы и RANSAC (Panoramas and RANSAC)
Объяснение темы:
Наверняка вы видели или даже сами делали **панорамные фотографии** — широкие снимки, охватывающие большой угол обзора, например, красивый пейзаж или городскую площадь. Такие фотографии обычно создаются путем "сшивания" нескольких обычных фотографий, сделанных с одной точки с небольшим поворотом камеры так, чтобы соседние кадры имели перекрывающиеся области.
**Процесс создания панорамы включает несколько ключевых шагов:**
1. **Съемка изображений:** Делается серия фотографий с перекрытием (обычно 20-50%).
2. **Обнаружение локальных признаков:** На каждом изображении находятся характерные точки (например, с помощью SIFT или ORB, см. Билет 9).
3. **Сопоставление признаков (Feature Matching):** Находятся пары одинаковых признаков на соседних (перекрывающихся) изображениях. То есть, для признака на одном фото ищется соответствующий ему признак на другом.
4. **Вычисление матрицы гомографии:** Для каждой пары соседних изображений вычисляется матрица проективного преобразования (гомография, см. Билет 3), которая отображает одно изображение на плоскость другого. Эта матрица рассчитывается на основе координат сопоставленных признаков.
5. **Робастная оценка гомографии с помощью RANSAC:** При сопоставлении признаков могут возникать ошибки (неверные пары). RANSAC помогает найти "правильную" гомографию, игнорируя эти ошибочные совпадения (выбросы).
6. **Трансформация (варпинг) изображений:** Изображения трансформируются (искажаются) с использованием вычисленных гомографий так, чтобы они все выровнялись относительно одного (например, центрального) изображения.
7. **Смешивание (блендинг):** Трансформированные изображения смешиваются в областях перекрытия, чтобы создать плавный, бесшовный переход и получить итоговую панораму.
**RANSAC (RANdom SAmple Consensus — Случайная Выборка и Согласие):**
Это очень важный и широко используемый итеративный алгоритм для оценки параметров математической модели по данным, которые содержат **выбросы (outliers)** — то есть, данные, которые не соответствуют модели (например, ошибочные измерения или неверные сопоставления признаков).
**Как работает RANSAC (на примере нахождения гомографии):**
1. **Случайная выборка:** Из всего набора сопоставленных пар точек случайно выбирается минимальное количество пар, необходимое для вычисления модели (для гомографии это 4 пары).
2. **Построение модели:** По этим 4 парам вычисляется матрица гомографии `H`.
3. **Проверка (консенсус):** Для всех остальных пар точек проверяется, насколько хорошо они описываются этой моделью `H`. Точки, которые "хорошо ложатся" на модель (т.е. ошибка перепроекции которых с помощью `H` меньше некоторого порога), называются **инлайерами (inliers)**. Подсчитывается количество инлайеров.
4. **Повторение:** Шаги 1-3 повторяются много раз (заданное число итераций).
5. **Выбор лучшей модели:** Выбирается та модель `H`, которая получила наибольшее количество инлайеров. Затем эта модель может быть дополнительно уточнена с использованием всех найденных для нее инлайеров (например, методом наименьших квадратов).
Методы и Примеры:
Сопоставление признаков (Feature Matching)
Описание: После того как локальные признаки (ключевые точки и их дескрипторы) извлечены из двух изображений, необходимо найти, какие признаки на первом изображении соответствуют каким признакам на втором. Это делается путем сравнения их дескрипторов. Чем меньше "расстояние" между двумя дескрипторами, тем более они похожи. OpenCV предлагает несколько матчеров, например, `BFMatcher` (Brute-Force Matcher) и `FlannBasedMatcher` (использует алгоритмы приближенного поиска ближайших соседей, такие как k-d деревья или LSH, что быстрее для больших наборов дескрипторов).
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Создадим два "изображения" и извлечем из них ORB-признаки (для примера)
img1_gray = np.zeros((200,200), np.uint8); cv2.putText(img1_gray, "CV1", (50,100), cv2.FONT_HERSHEY_SIMPLEX, 2, 255, 3)
img2_gray = np.zeros((200,200), np.uint8); cv2.putText(img2_gray, "CV1", (60,110), cv2.FONT_HERSHEY_SIMPLEX, 2, 180, 3) # Чуть смещено и тусклее
cv2.circle(img2_gray, (150,50), 30, 120, -1) # Добавим лишний элемент
orb = cv2.ORB_create()
keypoints1, descriptors1 = orb.detectAndCompute(img1_gray, None)
keypoints2, descriptors2 = orb.detectAndCompute(img2_gray, None)
matches_list = []
if descriptors1 is not None and descriptors2 is not None and len(descriptors1)>0 and len(descriptors2)>0:
# Инициализация BFMatcher.
# cv2.NORM_HAMMING используется для бинарных дескрипторов (ORB, BRIEF, BRISK).
# cv2.NORM_L1 или cv2.NORM_L2 для вещественных дескрипторов (SIFT, SURF).
# crossCheck=True: Улучшает качество совпадений. Совпадение (i,j) считается, только если
# дескриптор i из первого набора является лучшим совпадением для j из второго, И
# дескриптор j из второго набора является лучшим совпадением для i из первого.
bf_matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
# Сопоставление дескрипторов
# matches = bf_matcher.match(queryDescriptors, trainDescriptors)
matches_list = bf_matcher.match(descriptors1, descriptors2)
# Сортировка совпадений по расстоянию (чем меньше расстояние, тем лучше совпадение)
matches_list = sorted(matches_list, key=lambda x: x.distance)
# Взять, например, 15 лучших совпадений для отрисовки
num_good_matches_to_draw = min(len(matches_list), 15)
# Отрисовка совпадений (для этого нужны исходные цветные изображения или их серые версии)
# img_matches = cv2.drawMatches(img1_gray, keypoints1, img2_gray, keypoints2,
# matches_list[:num_good_matches_to_draw], None,
# flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
# plt.figure(figsize=(10,5)), plt.imshow(img_matches), plt.title('Лучшие совпадения (BFMatcher, crossCheck=True)')
# plt.show()
else:
print("Недостаточно дескрипторов для сопоставления.")
# print(f"Найдено {len(matches_list)} совпадений после crossCheck.")
Объяснение кода: - `bf_matcher = cv2.BFMatcher(normType, crossCheck)`:
- `normType`: Тип нормы для измерения расстояния между дескрипторами. `cv2.NORM_HAMMING` для бинарных (ORB, BRIEF), так как он подсчитывает количество отличающихся битов. `cv2.NORM_L2` (Евклидово расстояние) или `cv2.NORM_L1` (Манхэттенское расстояние) для вещественных (SIFT, SURF).
- `crossCheck=True`: Включает проверку взаимности совпадений. Это хороший способ отфильтровать много ложных совпадений, но он может отбросить и некоторые правильные.
- `matches_list = bf_matcher.match(descriptors1, descriptors2)`: Находит лучшие совпадения из `descriptors1` (query) для каждого дескриптора из `descriptors2` (train). Если `crossCheck=True`, то возвращаются только взаимно лучшие.
- Каждый элемент в `matches_list` — это объект `cv2.DMatch`. Важные атрибуты `DMatch`:
- `queryIdx`: Индекс дескриптора в `descriptors1`.
- `trainIdx`: Индекс дескриптора в `descriptors2`.
- `distance`: Расстояние между двумя дескрипторами. Чем меньше, тем лучше совпадение.
- `sorted(matches_list, key=lambda x: x.distance)`: Сортирует совпадения по их качеству (расстоянию).
- **Тест отношения Лоу (Lowe's Ratio Test):** Если `crossCheck=False`, то вместо `bf.match()` часто используют `bf.knnMatch(des1, des2, k=2)`, чтобы найти 2 лучших совпадения для каждого дескриптора `des1`. Затем хороший матч принимается, если `match1.distance < ratio * match2.distance` (где `ratio` обычно ~0.7-0.8). Это более гибкий способ фильтрации, чем `crossCheck`.
Пример из реальной жизни: 1. **Создание панорам:** Это ключевой шаг для нахождения общих областей.
2. **Распознавание и отслеживание объектов.**
3. **3D-реконструкция.**
4. **Поиск изображений по образцу (Query by Example).**
Вычисление гомографии с RANSAC (cv2.findHomography)
Описание: После нахождения сопоставленных пар ключевых точек, эта функция вычисляет матрицу гомографии `H`, которая преобразует точки с первого изображения на второе. Она использует метод RANSAC (или другой, например, LMEDS) для робастной оценки `H`, то есть для устойчивости к наличию неверных совпадений (выбросов) среди предоставленных пар точек.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Предположим, у нас есть 'matches_list' (отфильтрованный список DMatch объектов),
# 'keypoints1' и 'keypoints2' (списки KeyPoint объектов)
# Для примера, воспользуемся результатами из предыдущего кода
# (создадим img1_gray, img2_gray, kp1, des1, kp2, des2, matches_list как в примере выше)
img1_gray = np.zeros((200,200), np.uint8); cv2.putText(img1_gray, "CV1", (50,100), cv2.FONT_HERSHEY_SIMPLEX, 2, 255, 3)
img2_gray = np.zeros((200,200), np.uint8); cv2.putText(img2_gray, "CV1", (60,110), cv2.FONT_HERSHEY_SIMPLEX, 2, 180, 3)
cv2.circle(img2_gray, (150,50), 30, 120, -1)
orb = cv2.ORB_create(); kp1, des1 = orb.detectAndCompute(img1_gray, None); kp2, des2 = orb.detectAndCompute(img2_gray, None)
matches_list = []
if des1 is not None and des2 is not None and len(des1)>0 and len(des2)>0:
bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True); matches_list = bf.match(des1, des2)
matches_list = sorted(matches_list, key=lambda x: x.distance)
MIN_MATCH_COUNT = 4 # Минимальное количество совпадений для вычисления гомографии
H_matrix = None
mask_ransac = None
if len(matches_list) >= MIN_MATCH_COUNT:
# Извлечение координат сопоставленных точек
src_pts = np.float32([ kp1[m.queryIdx].pt for m in matches_list ]).reshape(-1,1,2)
dst_pts = np.float32([ kp2[m.trainIdx].pt for m in matches_list ]).reshape(-1,1,2)
# Вычисление матрицы гомографии с использованием RANSAC
# reprojThresh: Максимальное расстояние перепроекции (в пикселях) для اعتبار точки инлайером.
# Обычно от 1 до 10. Зависит от точности локализации признаков.
# maxIters: Максимальное количество итераций RANSAC.
# confidence: Желаемый уровень уверенности (0.0-1.0).
H_matrix, mask_ransac = cv2.findHomography(src_pts, dst_pts, method=cv2.RANSAC,
reprojectionThreshold=5.0)
# if H_matrix is not None:
# print("Матрица гомографии H, найденная с RANSAC:\n", H_matrix)
# num_inliers = np.sum(mask_ransac)
# print(f"Найдено инлайеров: {num_inliers} из {len(matches_list)}")
# Пример отрисовки только инлайеров
# draw_params = dict(matchColor = (0,255,0), # draw matches in green color
# singlePointColor = None,
# matchesMask = mask_ransac.ravel().tolist(), # draw only inliers
# flags = 2)
# img_inliers = cv2.drawMatches(img1_gray,kp1,img2_gray,kp2,matches_list,None,**draw_params)
# plt.imshow(img_inliers, 'gray'),plt.show()
# else:
# print("Гомографию найти не удалось (RANSAC).")
else:
print(f"Недостаточно совпадений для вычисления гомографии (найдено {len(matches_list)}, нужно {MIN_MATCH_COUNT})")
Объяснение кода: - `src_pts = np.float32([ kp1[m.queryIdx].pt ... ])`: Формируем массив NumPy с координатами `(x,y)` точек на первом изображении, участвующих в "хороших" совпадениях.
- `dst_pts = np.float32([ kp2[m.trainIdx].pt ... ])`: Аналогично для второго изображения.
- `H_matrix, mask_ransac = cv2.findHomography(srcPoints, dstPoints, method, reprojThreshold, mask, maxIters, confidence)`:
- `srcPoints`, `dstPoints`: Массивы координат сопоставленных точек.
- `method`: Метод вычисления. `cv2.RANSAC` — наиболее распространенный. Другие: 0 (обычный метод наименьших квадратов, без робастности), `cv2.LMEDS`.
- `reprojThreshold`: Порог для RANSAC. Если расстояние между точкой `dstPoints[i]` и точкой `H * srcPoints[i]` (после преобразования и деления на `w'`) меньше этого порога, то `i`-ая пара считается инлайером.
- `mask` (выходной параметр): Маска инлайеров. `mask[i] = 1`, если i-ая пара точек является инлайером, иначе 0.
- `maxIters` (опционально): Максимальное число итераций RANSAC.
- `confidence` (опционально): Требуемый уровень достоверности (0.0-1.0) результата.
- `H_matrix`: Найденная матрица гомографии 3x3. Если найти не удалось (например, слишком мало инлайеров), будет `None`.
Пример из реальной жизни: 1. **Создание панорам:** После сопоставления признаков, `findHomography` используется для вычисления преобразования между соседними кадрами.
2. **Ректификация изображений:** Если у вас есть 4 пары точек (углы искаженного объекта и соответствующие им углы идеального прямоугольника), `findHomography` поможет "выпрямить" объект.
3. **Дополненная реальность:** Для наложения виртуальных объектов на реальные плоские поверхности, сначала находится гомография между маркером/поверхностью и плоскостью изображения.
4. **Визуальная одометрия и SLAM.**
11. Эффекты и искажения камеры. Перспективные матричные преобразования. (Camera Effects and Distortions. Perspective Matrix Transformations)
Объяснение темы:
Камера, будь то в вашем смартфоне или профессиональный фотоаппарат, не является идеальным оптическим устройством, которое просто проецирует 3D-мир на 2D-плоскость без искажений. Существует несколько типов эффектов и искажений, которые влияют на получаемое изображение.
**Модель камеры-обскуры (Pinhole Camera Model):**
Это простейшая идеализированная модель камеры. Представьте себе темную коробку с очень маленьким отверстием (pinhole) с одной стороны и светочувствительной пленкой (или сенсором) с другой. Лучи света от объекта в 3D-мире проходят через это отверстие и создают перевернутое изображение на пленке. Математически, проекция 3D-точки `P_world = (X, Y, Z)` на 2D-плоскость изображения `p_image = (u, v)` описывается с помощью **матрицы проекции камеры**.
Эта матрица проекции `P` (обычно 3x4) может быть разложена на две части:
1. **Матрица внутренних параметров (Intrinsics Matrix, `K`):** Это матрица 3x3, которая описывает внутренние характеристики самой камеры и ее объектива. Она включает:
- **Фокусное расстояние (`fx`, `fy`):** Измеряется в пикселях. Определяет, насколько сильно камера "приближает" или "удаляет" объекты. `fx` и `fy` могут немного отличаться, если пиксели сенсора не квадратные.
- **Координаты главной точки (`cx`, `cy`):** Это проекция оптического центра объектива на плоскость изображения (обычно близко к центру изображения).
- **Коэффициент скоса (skew coefficient):** Учитывает неортогональность осей сенсора (обычно равен 0 для современных камер).
Матрица `K` выглядит так:
```
K = | fx s cx |
| 0 fy cy |
| 0 0 1 |
```
(где `s` — коэффициент скоса)
2. **Матрица внешних параметров (Extrinsics Matrix, `[R|t]`):** Это матрица 3x4, которая описывает положение и ориентацию камеры в мировой системе координат. Она состоит из:
- **Матрица поворота `R` (3x3):** Описывает ориентацию (поворот) камеры.
- **Вектор сдвига `t` (3x1):** Описывает положение оптического центра камеры в мировых координатах.
Тогда `[R|t]` — это матрица, где первые 3 столбца — это `R`, а последний столбец — это `t`.
Проекция 3D-точки `P_world` (в однородных координатах `(X,Y,Z,1)^T`) на 2D-точку `p_image` (в однородных координатах `(u',v',w')^T`) происходит так:
`p_image_homogeneous = K * [R|t] * P_world_homogeneous`
Затем `u = u'/w'` и `v = v'/w'`.
**Искажения объектива (Lens Distortions):**
Реальные объективы не идеальны и вносят искажения:
1. **Радиальные искажения:** Прямые линии на сцене могут выглядеть изогнутыми на изображении, особенно ближе к краям. Эффект похож на взгляд через бочку или подушку.
- **Бочкообразное (barrel) искажение:** Прямые линии изгибаются наружу от центра (характерно для широкоугольных объективов).
- **Подушкообразное (pincushion) искажение:** Прямые линии изгибаются внутрь к центру (характерно для телеобъективов).
Эти искажения моделируются полиномиальной функцией от расстояния `r` от главной точки: `x_distorted = x_corrected * (1 + k1*r^2 + k2*r^4 + k3*r^6)` и аналогично для `y`. Коэффициенты `k1, k2, k3` — это параметры радиальной дисторсии.
2. **Тангенциальные искажения:** Возникают, если линза и плоскость изображения не идеально параллельны. Они приводят к тому, что объекты выглядят растянутыми или сжатыми в определенных направлениях. Моделируются коэффициентами `p1, p2`.
**Калибровка камеры** — это процесс определения внутренних параметров `K` и коэффициентов искажения `(k1,k2,p1,p2,k3,...)`. Зная эти параметры, можно математически "исправить" искажения на изображении (undistortion) и выполнять точные 3D-измерения.
Методы и Примеры:
Калибровка камеры (Camera Calibration using cv2.calibrateCamera)
Описание: Это процесс определения матрицы внутренних параметров камеры `K` (фокусное расстояние, главная точка) и коэффициентов искажения объектива `(k1,k2,p1,p2,k3)`. Для калибровки обычно используется специальный калибровочный шаблон с известной геометрией (например, шахматная доска или сетка из кругов), который фотографируется с разных ракурсов. Алгоритм находит углы или центры элементов шаблона на каждом изображении и, зная их реальные 3D-координаты, вычисляет параметры камеры.
Пример кода:
import cv2
import numpy as np
import glob # Для поиска файлов по шаблону
# Концептуальный пример, так как реальная калибровка требует набора изображений
# и точного нахождения точек на них.
# 1. Подготовка: Определяем размеры шахматной доски (количество внутренних углов)
chessboard_size = (9, 6) # Например, 9x6 внутренних углов
# 2. Создание 3D-координат для точек объекта (углов шахматной доски в ее системе координат)
# Предполагаем, что доска лежит на плоскости Z=0, размеры квадратов, например, 1 (в любых единицах)
objp = np.zeros((chessboard_size[0] * chessboard_size[1], 3), np.float32)
objp[:,:2] = np.mgrid[0:chessboard_size[0], 0:chessboard_size[1]].T.reshape(-1,2)
# Массивы для хранения 3D-точек объекта и 2D-точек изображения со всех калибровочных изображений
object_points_list = [] # 3D точки в реальном мире
image_points_list = [] # 2D точки на плоскости изображения
# Загрузка калибровочных изображений (замените 'path/to/your/calibration_images/*.jpg' на реальный путь)
# image_files = glob.glob('path/to/your/calibration_images/*.jpg')
# if not image_files: print("Не найдены изображения для калибровки."); exit()
# for fname in image_files: # Цикл по всем калибровочным изображениям
# img = cv2.imread(fname)
# gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# # Найти углы шахматной доски
# ret_corners, corners = cv2.findChessboardCorners(gray, chessboard_size, None)
# # Если углы найдены, добавить точки объекта и точки изображения (после уточнения)
# if ret_corners == True:
# object_points_list.append(objp)
# # Уточнение координат углов для большей точности
# corners_refined = cv2.cornerSubPix(gray, corners, (11,11), (-1,-1),
# (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.001))
# image_points_list.append(corners_refined)
# # Отрисовка найденных углов (опционально, для проверки)
# # cv2.drawChessboardCorners(img, chessboard_size, corners_refined, ret_corners)
# # cv2.imshow('Найденные углы', img)
# # cv2.waitKey(500)
# cv2.destroyAllWindows()
# if len(object_points_list) > 0 and len(image_points_list) > 0:
# # Выполнение калибровки камеры
# # gray.shape[::-1] - это (ширина, высота) изображения
# ret_calib, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(
# object_points_list, image_points_list, gray.shape[::-1], None, None
# )
# if ret_calib:
# print("Камера успешно откалибрована.")
# print("Матрица камеры (K):\n", camera_matrix)
# print("Коэффициенты дисторсии (k1,k2,p1,p2,k3):\n", dist_coeffs)
# # rvecs (rotation vectors) и tvecs (translation vectors) - внешние параметры для каждого изображения
# else:
# print("Калибровка не удалась.")
# else:
# print("Недостаточно данных для калибровки (не найдены углы или нет изображений).")
print("Это концептуальный код. Для реальной калибровки нужны изображения шаблона.")
print("Пример camera_matrix: [[fx, 0, cx], [0, fy, cy], [0, 0, 1]]")
print("Пример dist_coeffs: [[k1, k2, p1, p2, k3]]")
Объяснение кода: - `chessboard_size`: Количество внутренних углов на шахматной доске (по ширине, по высоте).
- `objp`: Массив 3D-координат углов шаблона в его собственной системе координат (например, `(0,0,0), (1,0,0), ...`).
- `object_points_list`, `image_points_list`: Списки, хранящие `objp` (для каждого изображения, где найден шаблон) и соответствующие 2D-координаты найденных углов на этих изображениях.
- `glob.glob(...)`: Находит все файлы, соответствующие шаблону (например, все .jpg в папке).
- `cv2.findChessboardCorners(gray, patternSize, flags)`: Пытается найти углы шахматной доски на сером изображении.
- `cv2.cornerSubPix(...)`: Уточняет координаты найденных углов с субпиксельной точностью.
- `ret_calib, camera_matrix, dist_coeffs, rvecs, tvecs = cv2.calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, ...)`: Основная функция калибровки.
- `objectPoints`: Список 3D-координат точек объекта для каждого калибровочного вида.
- `imagePoints`: Список соответствующих 2D-координат точек изображения.
- `imageSize`: Размер изображений `(ширина, высота)`.
- `cameraMatrix` (вход/выход): Начальное предположение для матрицы камеры (можно `None`).
- `distCoeffs` (вход/выход): Начальное предположение для коэффициентов дисторсии (можно `None`).
- Возвращает:
- `ret_calib`: Статус успеха (True/False).
- `camera_matrix`: Вычисленная матрица камеры `K`.
- `dist_coeffs`: Вычисленные коэффициенты дисторсии.
- `rvecs`: Список векторов вращения (в формате Родригеса) для каждого калибровочного изображения, описывающих его ориентацию.
- `tvecs`: Список векторов смещения для каждого калибровочного изображения, описывающих его положение.
Пример из реальной жизни: 1. **3D-реконструкция:** Точная калибровка необходима для восстановления 3D-сцены из 2D-изображений.
2. **Робототехника и навигация:** Для точного определения положения объектов и самого робота.
3. **Дополненная реальность:** Для корректного наложения виртуальных объектов на реальный мир.
4. **Измерение размеров объектов по изображению.**
5. **Устранение искажений для улучшения визуального качества или для последующей обработки.**
Устранение искажений объектива (Image Undistortion using cv2.undistort)
Описание: После калибровки камеры и получения матрицы `K` и коэффициентов дисторсии `dist_coeffs`, можно исправить искажения на изображениях, снятых этой камерой. Функция `cv2.undistort` применяет обратное преобразование искажений.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Загрузка изображения с искажениями
# distorted_image = cv2.imread('distorted_photo.jpg')
# if distorted_image is None: print('Ошибка загрузки'); exit()
# Создадим "искаженное" изображение для примера (линии должны стать прямыми после коррекции)
distorted_image = np.zeros((300,400,3), np.uint8); distorted_image.fill(255)
for i in range(0, 400, 40): cv2.line(distorted_image, (i,0), (int(i + i*0.1 - 20) ,299), (0,0,0), 1)
for i in range(0, 300, 40): cv2.line(distorted_image, (0,i), (399, int(i + i*0.1 - 15)), (0,0,0), 1)
# Предположим, у нас есть camera_matrix (K) и dist_coeffs из калибровки
# (Используйте реальные значения, полученные из cv2.calibrateCamera)
# Примерные значения (могут не соответствовать созданному выше изображению)
cam_matrix = np.array([[500., 0., 200.],[0., 500., 150.],[0.,0.,1.]], dtype=np.float32)
# k1, k2, p1, p2, k3 (пример сильной бочкообразной дисторсии)
dist_coeffs = np.array([[-0.5, 0.2, 0.001, 0.001, 0.05]], dtype=np.float32)
h, w = distorted_image.shape[:2]
# Оптимизация матрицы камеры для лучшего отображения после коррекции
# alpha=0: обрезает изображение, чтобы остались только валидные пиксели без черных областей.
# alpha=1: сохраняет все пиксели исходного изображения, могут появиться черные области по краям.
# roi (Region Of Interest) - это прямоугольник, который можно использовать для обрезки.
optimal_new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix(cam_matrix, dist_coeffs,
(w,h), alpha=1, newImgSize=(w,h))
# Устранение искажений
undistorted_img = cv2.undistort(distorted_image, cam_matrix, dist_coeffs, None, optimal_new_camera_matrix)
# Обрезка изображения по ROI, если alpha=0 или если хотим убрать черные края при alpha=1
# x_roi, y_roi, w_roi, h_roi = roi
# undistorted_img_cropped = undistorted_img[y_roi:y_roi+h_roi, x_roi:x_roi+w_roi]
# Визуализация
# plt.figure(figsize=(12, 6))
# plt.subplot(121), plt.imshow(cv2.cvtColor(distorted_image, cv2.COLOR_BGR2RGB)), plt.title('Искаженное изображение')
# plt.subplot(122), plt.imshow(cv2.cvtColor(undistorted_img, cv2.COLOR_BGR2RGB)), plt.title('Исправленное изображение')
# plt.show()
# Если используется undistorted_img_cropped, отображайте его.
Объяснение кода: - `h, w = distorted_image.shape[:2]`: Получаем размеры входного изображения.
- `optimal_new_camera_matrix, roi = cv2.getOptimalNewCameraMatrix(cameraMatrix, distCoeffs, imageSize, alpha, newImgSize)`: Вычисляет "улучшенную" матрицу камеры. Это полезно, так как после устранения искажений изображение может быть деформировано так, что часть его выйдет за первоначальные границы, или наоборот, появятся пустые (черные) области.
- `alpha`: Контролирует, сколько пикселей исходного изображения останется. `alpha=0` означает, что результат будет обрезан так, чтобы не было черных областей. `alpha=1` означает, что все пиксели исходного изображения будут видны, но могут появиться черные области по краям.
- `roi`: Возвращает область интереса, которую можно использовать для обрезки изображения.
- `undistorted_img = cv2.undistort(src, cameraMatrix, distCoeffs, dst, newCameraMatrix)`:
- `src`: Искаженное входное изображение.
- `cameraMatrix`: Исходная матрица камеры `K`.
- `distCoeffs`: Коэффициенты дисторсии.
- `dst` (опционально): Выходное изображение (можно `None`).
- `newCameraMatrix`: Оптимизированная матрица камеры (из `getOptimalNewCameraMatrix`). Если не указана, используется `cameraMatrix`, что может привести к нежелательным эффектам масштабирования.
- После `cv2.undistort` можно дополнительно обрезать изображение с помощью `roi`, чтобы убрать черные области, если они есть.
Пример из реальной жизни: 1. **Улучшение качества фотографий:** Особенно для широкоугольных объективов (GoPro, камеры наблюдения), где радиальные искажения сильны.
2. **Подготовка изображений для алгоритмов машинного зрения:** Многие алгоритмы (например, детекторы объектов, системы измерения) предполагают, что изображение не имеет искажений.
3. **Создание панорам:** Перед сшиванием изображений полезно устранить их индивидуальные искажения.
4. **Фотограмметрия и 3D-сканирование.
12. Кластеризация изображений (Image Clustering)
Объяснение темы:
Представьте, что у вас есть большая коллекция фотографий, и вы хотите автоматически рассортировать их по группам так, чтобы похожие фотографии оказались вместе (например, все фотографии пляжа в одной группе, все портреты в другой, все фотографии гор в третьей), но вы заранее не знаете, какие именно группы (классы) там есть. **Кластеризация изображений** — это как раз такая задача: группировка набора изображений на основе их схожести без предварительно заданных меток классов. Это метод **обучения без учителя (unsupervised learning)**.
**Основные шаги кластеризации изображений:**
1. **Извлечение признаков (Feature Extraction):** Каждое изображение нужно представить в виде набора чисел — **вектора признаков**. Этот вектор должен как-то описывать содержание изображения.
- **Глобальные признаки:** Описывают изображение целиком (например, цветовая гистограмма, средняя текстура, см. Билет 14).
- **Локальные признаки:** Сначала извлекаются локальные дескрипторы (SIFT, ORB и т.д.), а затем они агрегируются в один вектор для всего изображения (например, с помощью метода "Мешок визуальных слов", см. Билет 13).
- **Признаки из глубоких нейронных сетей:** Выходные данные одного из слоев предобученной сверточной нейронной сети (CNN) могут служить очень мощным вектором признаков.
2. **Определение метрики схожести/расстояния:** Нужно выбрать способ измерения, насколько два вектора признаков (а значит, и два изображения) похожи или не похожи друг на друга. Распространенные метрики:
- **Евклидово расстояние:** Прямое расстояние между двумя точками в пространстве признаков.
- **Косинусное расстояние (или косинусное сходство):** Измеряет угол между двумя векторами. Хорошо подходит, когда важна ориентация векторов, а не их абсолютная величина.
- **Расстояние Хэмминга:** Для бинарных дескрипторов (как у ORB), подсчитывает количество различающихся битов.
3. **Применение алгоритма кластеризации:** После того как все изображения представлены векторами признаков и выбрана метрика, применяется один из алгоритмов кластеризации для их группировки. Популярные алгоритмы:
- **K-Means (K-средних):** Простой и популярный итеративный алгоритм.
- **Иерархическая кластеризация:** Строит дерево (дендрограмму) кластеров.
- **DBSCAN (Density-Based Spatial Clustering of Applications with Noise):** Группирует точки на основе плотности, может находить кластеры произвольной формы и выделять шум (выбросы).
- **Affinity Propagation, Spectral Clustering и др.**
Методы и Примеры:
K-Means кластеризация с использованием OpenCV (cv2.kmeans)
Описание: OpenCV предоставляет реализацию алгоритма K-Means. Она принимает на вход набор векторов признаков (каждая строка — один вектор) и возвращает метки кластеров для каждого вектора, а также координаты центроидов найденных кластеров.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 1. Подготовка данных (извлечение признаков)
# Предположим, у нас есть массив 'feature_vectors', где каждая строка - это вектор признаков для одного изображения.
# Для примера, создадим фиктивные 2D-данные, имитирующие 3 кластера.
np.random.seed(42) # для воспроизводимости
cluster1_data = np.random.randn(50, 2) + np.array([2, 2]) # 50 точек вокруг (2,2)
cluster2_data = np.random.randn(50, 2) + np.array([-2, -2]) # 50 точек вокруг (-2,-2)
cluster3_data = np.random.randn(50, 2) + np.array([2, -2]) # 50 точек вокруг (2,-2)
feature_vectors = np.vstack((cluster1_data, cluster2_data, cluster3_data))
# Данные должны быть типа float32 для cv2.kmeans
feature_vectors_float32 = np.float32(feature_vectors)
# 2. Применение K-Means
# Определение количества кластеров
K = 3
# Определение критериев остановки алгоритма:
# (тип_критерия, максимальное_число_итераций, желаемая_точность_эпсилон)
# cv2.TERM_CRITERIA_EPS: остановить, если центроиды сместились меньше чем на эпсилон.
# cv2.TERM_CRITERIA_MAX_ITER: остановить после макс. числа итераций.
# Здесь: остановить после 20 итераций ИЛИ если смещение < 0.5.
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 0.5)
# Количество запусков алгоритма с разными начальными центроидами
# (выбирается лучший результат по компактности)
attempts = 10
# Флаг инициализации центроидов (можно cv2.KMEANS_RANDOM_CENTERS или cv2.KMEANS_PP_CENTERS)
# KMEANS_PP_CENTERS (k-means++) обычно дает лучшие результаты, чем случайная инициализация.
init_flag = cv2.KMEANS_PP_CENTERS
# Запуск K-Means
# compactness: Сумма квадратов расстояний от каждой точки до ее центра (чем меньше, тем лучше).
# labels: Массив меток кластеров (0, 1, ..., K-1) для каждой входной точки.
# centers: Координаты центров найденных кластеров.
compactness, labels, centers = cv2.kmeans(data=feature_vectors_float32,
K=K,
bestLabels=None, # Выходной массив для меток, можно None
criteria=criteria,
attempts=attempts,
flags=init_flag)
# print(f"Компактность (сумма кв. расстояний): {compactness:.2f}")
# print(f"Метки кластеров (первые 10): {labels.ravel()[:10]}")
# print(f"Центры кластеров:\n {centers}")
# 3. Визуализация результатов (для 2D-данных)
# plt.figure(figsize=(8, 6))
# colors_map = ['r', 'g', 'b', 'y', 'c', 'm'] # Цвета для кластеров
# for i in range(K):
# # Выбираем точки, принадлежащие текущему кластеру i
# cluster_points = feature_vectors_float32[labels.ravel() == i]
# plt.scatter(cluster_points[:, 0], cluster_points[:, 1],
# s=50, c=colors_map[i], label=f'Кластер {i+1}')
# plt.scatter(centers[:, 0], centers[:, 1], s=200, c='yellow',
# marker='X', edgecolors='black', label='Центроиды')
# plt.title('Результаты K-Means кластеризации')
# plt.xlabel('Признак 1'); plt.ylabel('Признак 2')
# plt.legend()
# plt.grid(True)
# plt.show()
Объяснение кода: - `feature_vectors_float32 = np.float32(feature_vectors)`: Данные для `cv2.kmeans` должны быть типа `np.float32`.
- `K`: Задаваемое количество кластеров.
- `criteria`: Кортеж, определяющий условия остановки итеративного процесса. `(type, max_iter, epsilon)`.
- `type`: `cv2.TERM_CRITERIA_EPS` (остановка по достижении заданной точности `epsilon`), `cv2.TERM_CRITERIA_MAX_ITER` (остановка после `max_iter` итераций), или их комбинация `+`.
- `attempts`: Сколько раз алгоритм будет запущен с разными случайными начальными центроидами. Функция вернет лучший результат из всех попыток (по наименьшей компактности).
- `flags`: Определяет метод инициализации центроидов. `cv2.KMEANS_RANDOM_CENTERS` — случайный выбор. `cv2.KMEANS_PP_CENTERS` — использует более умный метод k-means++, который обычно приводит к лучшим и более стабильным результатам.
- `compactness, labels, centers = cv2.kmeans(...)`:
- `compactness`: Итоговая сумма квадратов расстояний от каждой точки до центроида ее кластера. Это мера "качества" кластеризации (чем меньше, тем лучше для данного K).
- `labels`: Одномерный массив (столбец), где `labels[i]` — это метка кластера (от 0 до K-1), к которому была отнесена i-ая точка из входных данных. `labels.ravel()` делает его плоским массивом.
- `centers`: Массив, где каждая строка — это координаты центроида одного из K кластеров.
Пример из реальной жизни: 1. **Сегментация изображений:** Кластеризация пикселей по их цвету или текстурным признакам для разделения изображения на смысловые области. Например, для квантования цветов (уменьшения количества цветов на изображении).
2. **Создание "мешка визуальных слов" (Bag of Visual Words, BoVW) для классификации изображений (см. Билет 13):** Локальные дескрипторы (SIFT, ORB) со всего набора обучающих изображений кластеризуются с помощью K-Means. Центры этих кластеров становятся "визуальными словами" в словаре.
3. **Организация больших коллекций изображений:** Группировка визуально похожих изображений без необходимости ручной разметки.
4. **Обнаружение аномалий:** Объекты, которые не попадают ни в один из хорошо сформированных кластеров, могут быть аномалиями.
5. **Анализ данных в других областях:** K-Means — это универсальный алгоритм кластеризации, применяемый не только к изображениям.
13. Классификация изображений. Метод мешка слов. (Image Classification. Bag of Words Method)
Объяснение темы:
**Классификация изображений** — это задача компьютерного зрения, цель которой — присвоить изображению одну или несколько меток класса из заранее определенного набора (например, "кошка", "собака", "автомобиль", "пейзаж"). Это типичная задача **обучения с учителем (supervised learning)**, так как для обучения модели требуется большой набор изображений, для каждого из которых уже известна правильная метка класса.
**Метод "Мешка визуальных слов" (Bag of Visual Words, BoVW, также Bag of Features, BoF):**
Это популярный подход к классификации изображений, который был особенно распространен до широкого внедрения сверточных нейронных сетей (CNN). Идея заимствована из обработки естественного языка (NLP), где для классификации текстов используется метод "мешка слов": текст представляется как неупорядоченный набор (мешок) слов, и частота встречаемости каждого слова используется как признак. Пространственное расположение слов игнорируется.
Аналогично, в BoVW для изображений:
1. **Извлечение "визуальных слов":** Вместо текстовых слов используются локальные признаки изображения (например, SIFT, SURF, ORB дескрипторы, см. Билет 9).
2. **Создание визуального словаря:** Из большого набора обучающих изображений извлекается множество таких локальных дескрипторов. Затем эти дескрипторы кластеризуются (например, с помощью K-Means, см. Билет 12). Центры этих кластеров и становятся нашими "визуальными словами" — это как бы типичные визуальные паттерны, встречающиеся на изображениях. Набор этих центров-кластеров образует **визуальный словарь (visual vocabulary)**.
3. **Представление изображений в виде гистограммы слов:** Каждое изображение (как из обучающей, так и из тестовой выборки) теперь представляется в виде **гистограммы частот встречаемости визуальных слов**. Для этого из изображения извлекаются его локальные дескрипторы. Каждый дескриптор "присваивается" ближайшему визуальному слову из словаря. Гистограмма показывает, сколько раз каждое слово из словаря встретилось на данном изображении. Эта гистограмма и становится вектором признаков для изображения, который затем используется для обучения классификатора.
4. **Обучение классификатора:** На полученных BoVW-гистограммах и соответствующих им метках классов (из обучающей выборки) обучается стандартный классификатор (например, SVM — метод опорных векторов, логистическая регрессия, случайный лес и т.д.).
5. **Классификация нового изображения:** Для нового, ранее не виденного изображения, также извлекаются локальные признаки, строится его BoVW-гистограмма, и обученный классификатор предсказывает его класс.
Методы и Примеры:
Создание словаря и BoVW-представления (Концептуально, используя K-Means и BFMatcher)
Описание: Этот этап включает сбор всех дескрипторов из обучающего набора, их кластеризацию для создания визуального словаря, и затем функцию для преобразования набора дескрипторов любого изображения в его BoVW-гистограмму с использованием этого словаря.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# --- Шаг 1: Имитация извлечения дескрипторов из обучающего набора ---
# В реальном приложении здесь был бы цикл по изображениям и извлечение SIFT/ORB
np.random.seed(0)
num_training_images = 5
descriptors_per_image = 10
descriptor_dim = 64 # Для примера, ORB дескрипторы (обычно 32 байта, но для float kmeans лучше float дескрипторы)
all_descriptors_list = []
for _ in range(num_training_images):
# Генерируем случайные float дескрипторы для имитации SIFT/SURF
# (для ORB были бы uint8, но kmeans в OpenCV ожидает float32)
# Если используете ORB, их нужно будет конвертировать в float32 для kmeans
# или использовать матчер, работающий с NORM_HAMMING для назначения слов
# Здесь для простоты используем float32
img_descriptors = np.random.rand(descriptors_per_image, descriptor_dim).astype(np.float32)
all_descriptors_list.append(img_descriptors)
# Объединяем все дескрипторы в один большой массив для кластеризации
if not all_descriptors_list:
print("Нет дескрипторов для построения словаря.")
exit()
all_descriptors_stacked = np.vstack(all_descriptors_list)
# --- Шаг 2: Создание визуального словаря с помощью K-Means ---
K_vocab_size = 10 # Размер словаря (количество визуальных слов)
if all_descriptors_stacked.shape[0] < K_vocab_size:
print(f"Количество дескрипторов ({all_descriptors_stacked.shape[0]}) меньше размера словаря ({K_vocab_size}). Уменьшите размер словаря или добавьте дескрипторы.")
exit()
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 0.1)
compactness, labels_vocab, vocabulary = cv2.kmeans(all_descriptors_stacked, K_vocab_size, None,
criteria, 10, cv2.KMEANS_PP_CENTERS)
# vocabulary - это наши визуальные слова (центроиды кластеров), размером (K_vocab_size, descriptor_dim)
# print(f"Словарь построен. Размер: {vocabulary.shape}")
# --- Шаг 3: Функция для представления изображения в виде BoVW-гистограммы ---
def compute_bovw_histogram(image_descriptors, vocabulary_centroids):
if image_descriptors is None or len(image_descriptors) == 0:
return np.zeros(len(vocabulary_centroids), dtype=np.float32)
# Для каждого дескриптора изображения находим ближайшее слово в словаре
# BFMatcher с NORM_L2 для float дескрипторов (SIFT, SURF-подобные)
bf = cv2.BFMatcher(cv2.NORM_L2)
# Если бы vocabulary был из ORB (uint8), то NORM_HAMMING
matches = bf.match(image_descriptors, vocabulary_centroids)
# bf.match() для каждого image_descriptor находит одно лучшее совпадение в vocabulary_centroids
histogram = np.zeros(len(vocabulary_centroids), dtype=np.float32)
for m in matches:
# m.trainIdx - это индекс слова в словаре (т.к. vocabulary_centroids был 'trainDescriptors')
histogram[m.trainIdx] += 1
# Нормализация гистограммы (L1-норма)
if np.sum(histogram) > 0:
histogram /= np.sum(histogram)
return histogram
# --- Пример использования для одного "нового" изображения ---
# Допустим, у нас есть дескрипторы для нового изображения
new_image_descriptors = np.random.rand(15, descriptor_dim).astype(np.float32)
bovw_hist_for_new_image = compute_bovw_histogram(new_image_descriptors, vocabulary)
# print(f"BoVW гистограмма для нового изображения (первые {K_vocab_size} бинов):\n {bovw_hist_for_new_image}")
# plt.figure(figsize=(8,4))
# plt.bar(np.arange(K_vocab_size), bovw_hist_for_new_image)
# plt.title('BoVW гистограмма для нового изображения')
# plt.xlabel('Индекс визуального слова')
# plt.ylabel('Нормализованная частота')
# plt.show()
Объяснение кода: 1. **Имитация извлечения дескрипторов:** `all_descriptors_stacked` собирает все дескрипторы со всех обучающих изображений в один NumPy массив. Каждый дескриптор — это строка.
2. **Создание словаря (`cv2.kmeans`):**
- `all_descriptors_stacked`: Входные данные для кластеризации.
- `K_vocab_size`: Желаемое количество кластеров (размер словаря).
- `criteria`: Критерии остановки для K-Means.
- `attempts`: Количество запусков K-Means с разной инициализацией.
- `cv2.KMEANS_PP_CENTERS`: Метод инициализации (k-means++).
- `vocabulary`: Результат — массив центроидов кластеров. Каждая строка — это одно "визуальное слово".
3. **Функция `compute_bovw_histogram`:**
- Принимает дескрипторы одного изображения (`image_descriptors`) и построенный словарь (`vocabulary_centroids`).
- `bf = cv2.BFMatcher(cv2.NORM_L2)`: Создает матчер. `NORM_L2` используется, так как мы имитировали float-дескрипторы (как у SIFT). Если бы словарь и дескрипторы изображения были бинарными (как у ORB), использовался бы `cv2.NORM_HAMMING`.
- `matches = bf.match(image_descriptors, vocabulary_centroids)`: Для каждого дескриптора из `image_descriptors` находит одно наиболее похожее (ближайшее) слово в `vocabulary_centroids`.
- `histogram = np.zeros(...)`: Инициализирует гистограмму нулями.
- `histogram[m.trainIdx] += 1`: `m.trainIdx` — это индекс ближайшего слова из словаря. Увеличиваем счетчик для этого слова.
- **Нормализация:** Гистограмма делится на ее сумму (L1-норма), чтобы получить распределение частот и сделать ее инвариантной к общему количеству дескрипторов на изображении.
Пример из реальной жизни: 1. **Классификация сцен:** Например, определение, изображен ли на фото "пляж", "город" или "лес".
2. **Распознавание объектов:** Классификация изображений по типу содержащихся на них объектов (например, "автомобиль", "велосипед", "пешеход").
3. **Поиск изображений по содержанию (CBIR):** Найти изображения, похожие на заданное, сравнивая их BoVW-гистограммы.
Обучение классификатора (например, SVM) на BoVW-гистограммах
Описание: После того как все обучающие изображения представлены в виде BoVW-гистограмм, эти гистограммы (векторы признаков) и соответствующие им метки классов используются для обучения стандартного классификатора. Метод Опорных Векторов (SVM) часто является хорошим выбором для этой задачи.
Пример кода:
from sklearn.svm import SVC # Support Vector Classifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
import numpy as np
# --- Имитация данных: BoVW-гистограммы и метки классов ---
# Предположим, у нас есть X_bovw (массив BoVW-гистограмм) и y_labels (массив меток)
# vocab_size должен соответствовать K_vocab_size из предыдущего шага
vocab_size_from_prev_step = 10 # K_vocab_size
num_total_images = 100
# Создаем фиктивные BoVW гистограммы
X_bovw = np.random.rand(num_total_images, vocab_size_from_prev_step)
# Создаем фиктивные метки классов (например, 2 класса: 0 и 1)
y_labels = np.array([0] * (num_total_images // 2) + [1] * (num_total_images - num_total_images // 2))
np.random.shuffle(y_labels) # Перемешиваем метки для реалистичности
# --- Разделение данных на обучающую и тестовую выборки ---
X_train, X_test, y_train, y_test = train_test_split(X_bovw, y_labels,
test_size=0.25, # 25% данных на тест
random_state=42, # для воспроизводимости
stratify=y_labels) # для сохранения пропорций классов
# print(f"Размер обучающей выборки: {X_train.shape}, меток: {y_train.shape}")
# print(f"Размер тестовой выборки: {X_test.shape}, меток: {y_test.shape}")
# --- Инициализация и обучение SVM классификатора ---
# C: параметр регуляризации. Меньшее C - более "мягкая" граница, большее C - стремится классифицировать все точки правильно.
# kernel: тип ядра. 'linear', 'poly', 'rbf' (часто хороший выбор по умолчанию), 'sigmoid'.
# gamma: коэффициент ядра для 'rbf', 'poly', 'sigmoid'. 'scale' (1 / (n_features * X.var())) или 'auto' (1 / n_features).
svm_classifier = SVC(kernel='rbf', C=1.0, gamma='scale', probability=True, random_state=42)
# Обучение модели
svm_classifier.fit(X_train, y_train)
# --- Оценка качества модели на тестовой выборке ---
y_pred_test = svm_classifier.predict(X_test)
accuracy = accuracy_score(y_test, y_pred_test)
# print(f"Точность SVM на тестовой выборке: {accuracy:.4f}")
# --- Предсказание для нового изображения (представленного BoVW-гистограммой) ---
# Допустим, new_image_bovw_hist - это BoVW-гистограмма нового изображения
# (должна быть той же размерности vocab_size_from_prev_step)
# new_image_bovw_hist = np.random.rand(1, vocab_size_from_prev_step)
# predicted_class = svm_classifier.predict(new_image_bovw_hist)
# predicted_probabilities = svm_classifier.predict_proba(new_image_bovw_hist)
# print(f"Предсказанный класс для нового изображения: {predicted_class[0]}")
# print(f"Вероятности классов для нового изображения: {predicted_probabilities[0]}")
Объяснение кода: 1. **Импорт библиотек:** `SVC` из `sklearn.svm` для классификатора, `train_test_split` для разделения данных, `accuracy_score` для оценки.
2. **Имитация данных:** `X_bovw` — это матрица, где каждая строка является BoVW-гистограммой для одного изображения. `y_labels` — соответствующий массив меток классов.
3. **Разделение на выборки (`train_test_split`):** Данные делятся на обучающую (для тренировки модели) и тестовую (для независимой оценки ее качества).
- `test_size`: Доля данных, которая пойдет в тестовую выборку.
- `random_state`: Для воспроизводимости разделения.
- `stratify=y_labels`: Гарантирует, что пропорции классов в обучающей и тестовой выборках будут такими же, как в исходных данных (важно для несбалансированных датасетов).
4. **Инициализация SVM (`SVC`):**
- `kernel='rbf'`: Радиально-базисная функция — популярное нелинейное ядро.
- `C`: Параметр регуляризации. Контролирует компромисс между максимизацией зазора между классами и минимизацией ошибок на обучающей выборке.
- `gamma`: Параметр ядра RBF. Определяет, насколько "далеко" распространяется влияние одной обучающей точки.
- `probability=True`: Позволяет получать оценки вероятностей принадлежности к классам (через `predict_proba`), но замедляет обучение.
5. **Обучение (`svm_classifier.fit(X_train, y_train)`):** Модель SVM обучается на тренировочных данных.
6. **Оценка (`svm_classifier.predict(X_test)`, `accuracy_score`):** Делаются предсказания на тестовой выборке, и вычисляется точность (доля правильных предсказаний).
7. **Предсказание для новых данных:** `svm_classifier.predict(new_data)` предсказывает класс для одной или нескольких новых BoVW-гистограмм.
Пример из реальной жизни: Это завершающий этап в пайплайне BoVW для классификации изображений. Обученный SVM (или другой классификатор) используется для присвоения меток новым изображениям на основе их BoVW-представлений. Примеры те же: классификация сцен, распознавание типов объектов и т.д.
14. Классификация изображений. Глобальные признаки. (Image Classification. Global Features)
Объяснение темы:
В отличие от метода "Мешка визуальных слов" (BoVW), который строится на основе множества *локальных* признаков, **глобальные признаки (Global Features)** описывают изображение **целиком**, представляя его в виде одного вектора признаков. Представьте, что вы пытаетесь описать картину одним предложением, передающим ее общую атмосферу, доминирующие цвета или основную текстуру, а не детали отдельных мазков.
Глобальные признаки стремятся охватить общие характеристики изображения, такие как:
- **Распределение цветов:** Преобладают ли теплые или холодные тона? Насколько разнообразны цвета?
- **Общая текстура:** Поверхность выглядит гладкой, шероховатой, узорчатой?
- **Общая форма объектов (если объект один и занимает большую часть кадра):** Насколько объект вытянут, компактен?
**Преимущества глобальных признаков:**
- Обычно их проще и быстрее вычислить, чем строить сложные модели на основе множества локальных признаков (как в BoVW).
- Требуют меньше памяти для хранения, так как каждое изображение представляется одним вектором.
**Недостатки глобальных признаков:**
- Менее устойчивы к частичным перекрытиям (окклюзиям), изменениям ракурса и беспорядку на фоне. Если важная часть изображения закрыта или объект снят под необычным углом, глобальный признак может сильно измениться.
- Могут плохо различать изображения, если их глобальные характеристики похожи, но локальные детали сильно отличаются.
Несмотря на недостатки, глобальные признаки могут быть эффективны для определенных задач, особенно когда изображения имеют относительно простую структуру, или когда требуется очень быстрая классификация. Они также могут использоваться в комбинации с локальными признаками.
Методы и Примеры:
Цветовая гистограмма (Color Histogram) как глобальный признак
Описание: Представляет распределение цветов в изображении. Для каждого цветового канала (или их комбинации в многомерной гистограмме) подсчитывается количество пикселей, попадающих в определенные диапазоны интенсивности (бины). Конкатенация одномерных гистограмм по нескольким каналам или "сглаживание" многомерной гистограммы в один вектор дает глобальный признак.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Загрузка изображения или создание тестового
# image_bgr = cv2.imread('colorful_image.jpg')
# if image_bgr is None: print('Ошибка загрузки'); exit()
# Создадим тестовое изображение
image_bgr = np.zeros((200, 300, 3), dtype=np.uint8)
# Левая треть - синяя
image_bgr[:, :100] = [255, 50, 50] # BGR: Преимущественно синий
# Средняя треть - зеленая
image_bgr[:, 100:200] = [50, 255, 50] # BGR: Преимущественно зеленый
# Правая треть - красная
image_bgr[:, 200:] = [50, 50, 255] # BGR: Преимущественно красный
# Параметры для гистограммы
num_bins_per_channel = 32 # Количество бинов (корзин) для каждого канала
color_channels = (0, 1, 2) # Индексы каналов (0-B, 1-G, 2-R)
color_ranges = [0, 256] # Диапазон значений пикселей для каждого канала
# Вычисление гистограммы для каждого канала и их конкатенация
global_feature_vector = []
# plt.figure(figsize=(12, 4))
# colors = ('b', 'g', 'r')
for i, channel_index in enumerate(color_channels):
# cv2.calcHist([images], [channels], mask, [histSize], [ranges])
hist_channel = cv2.calcHist([image_bgr], # Изображение (в списке)
[channel_index], # Индекс канала
None, # Маска (None - по всему изображению)
[num_bins_per_channel], # Количество бинов
color_ranges) # Диапазон значений
# Нормализация гистограммы (L2 норма, чтобы вектор имел единичную длину)
cv2.normalize(hist_channel, hist_channel, norm_type=cv2.NORM_L2)
global_feature_vector.extend(hist_channel.flatten()) # Добавляем к общему вектору
# Отрисовка гистограммы для текущего канала (опционально)
# plt.subplot(1, len(color_channels), i + 1)
# plt.plot(hist_channel, color=colors[i])
# plt.xlim(color_ranges)
# plt.title(f'Гистограмма канала {colors[i].upper()}')
# global_feature_vector теперь содержит конкатенированные нормализованные гистограммы
# Его длина будет num_bins_per_channel * len(color_channels)
global_feature_vector = np.array(global_feature_vector)
# print(f"Размер глобального вектора признаков (цвет. гистограмма): {global_feature_vector.shape[0]}")
# print(f"Первые 10 значений: {global_feature_vector[:10]}")
# plt.tight_layout()
# plt.show()
Объяснение кода: - `num_bins_per_channel`: Определяет, на сколько диапазонов будет разбит каждый цветовой канал. Большее количество бинов дает более детальное представление, но и более длинный вектор признаков.
- `cv2.calcHist([image_bgr], [channel_index], None, [num_bins_per_channel], color_ranges)`:
- `[image_bgr]`: Исходное изображение, обернутое в список.
- `[channel_index]`: Список каналов, для которых вычисляется гистограмма (здесь по одному каналу за раз).
- `None`: Маска (не используется, гистограмма по всему изображению).
- `[num_bins_per_channel]`: Список размеров гистограммы для каждого измеряемого канала.
- `color_ranges`: Список диапазонов значений для каждого канала.
- `cv2.normalize(hist_channel, hist_channel, norm_type=cv2.NORM_L2)`: Нормализует гистограмму. L2-нормализация (деление на евклидову длину вектора) часто используется, чтобы сделать признаки менее чувствительными к общим изменениям яркости или контраста. Можно использовать и другие нормы (L1, MINMAX).
- `global_feature_vector.extend(hist_channel.flatten())`: Преобразует 2D гистограмму канала (размером `num_bins_per_channel` x 1) в 1D массив и добавляет ее к общему вектору признаков.
Пример из реальной жизни: 1. **Поиск похожих изображений по цвету (Content-Based Image Retrieval - CBIR):** Если пользователь ищет изображения с преобладанием синего цвета, можно сравнить гистограммы.
2. **Простая классификация сцен:** Например, "пляж" (много синего и желтого/бежевого) против "леса" (много зеленого и коричневого).
3. **Фильтрация контента:** Определение изображений с определенной цветовой палитрой.
4. **Отслеживание объектов по цвету (если цвет объекта достаточно уникален и постоянен).**
HOG (Histogram of Oriented Gradients) - как глобальный признак всего изображения
Описание: HOG вычисляет гистограммы ориентаций градиентов в небольших ячейках изображения. Эти гистограммы затем нормализуются по блокам (состоящим из нескольких ячеек) и конкатенируются в один длинный вектор. Если применить этот процесс ко всему изображению (или к фиксированному окну, представляющему интерес), то полученный HOG-дескриптор будет глобальным признаком этой области. Он хорошо описывает локальные формы и силуэты через распределение интенсивности и направлений краев.
Пример кода:
from skimage.feature import hog # scikit-image предоставляет удобную реализацию HOG
from skimage import data, color, transform
import cv2 # Для загрузки и изменения размера, если нужно
import numpy as np
import matplotlib.pyplot as plt
# Загрузка тестового изображения из skimage или своего
# image_rgb = data.astronaut() # Пример из skimage
# image_rgb = cv2.imread('person_silhouette.jpg') # Загрузить свое
# if image_rgb is None: print('Ошибка загрузки'); exit()
# Создадим простое изображение с четкими градиентами
image_rgb = np.zeros((128, 64, 3), dtype=np.uint8)
image_rgb.fill(100)
cv2.rectangle(image_rgb, (10,10), (54, 118), (200,200,200), -1)
# HOG обычно работает с изображениями в градациях серого
image_gray = cv2.cvtColor(image_rgb, cv2.COLOR_BGR2GRAY) if len(image_rgb.shape) == 3 and image_rgb.shape[2] == 3 else image_rgb
# Опционально: изменение размера до фиксированного (часто делается для HOG)
# Например, классический HOG для детекции пешеходов использует размер 64x128
# resized_image_gray = transform.resize(image_gray, (128, 64)) # skimage.transform
resized_image_gray = cv2.resize(image_gray, (64, 128), interpolation=cv2.INTER_AREA)
# Вычисление HOG дескриптора
# orientations: Количество бинов для ориентаций градиента (обычно 8-9).
# pixels_per_cell: Размер ячейки в пикселях (например, (8, 8) или (16, 16)).
# cells_per_block: Количество ячеек в блоке для локальной нормализации контраста (например, (2, 2) или (3, 3)).
# block_norm: Метод нормализации ('L1', 'L2', 'L2-Hys' - L2 с последующим клиппингом и повторной нормализацией, часто лучший).
# visualize=True: также возвращает изображение для визуализации HOG.
fd, hog_image_visualization = hog(resized_image_gray,
orientations=9,
pixels_per_cell=(8, 8),
cells_per_block=(2, 2),
visualize=True,
block_norm='L2-Hys',
transform_sqrt=True) # Применение гамма-коррекции (sqrt)
# fd - это одномерный вектор признаков HOG для всего изображения (или окна)
# print(f"Размер HOG дескриптора: {fd.shape[0]}")
# print(f"Первые 10 значений HOG: {fd[:10]}")
# Визуализация HOG (если visualize=True)
# plt.figure(figsize=(10, 5))
# plt.subplot(121), plt.imshow(resized_image_gray, cmap='gray'), plt.title('Изображение (серое, изм. размер)')
# plt.subplot(122), plt.imshow(hog_image_visualization, cmap='gray'), plt.title('Визуализация HOG')
# plt.show()
Объяснение кода: - `from skimage.feature import hog`: Используем реализацию HOG из scikit-image.
- `image_gray = ...`: HOG вычисляется по одноканальному (серому) изображению.
- `resized_image_gray = ...`: Часто изображения приводятся к фиксированному размеру перед вычислением HOG, особенно если HOG-дескрипторы будут сравниваться или подаваться в классификатор, ожидающий фиксированную длину входа.
- `fd, hog_image_visualization = hog(...)`:
- `orientations`: Количество бинов в гистограмме ориентаций градиентов (например, 9 бинов покрывают 0-180 градусов с шагом 20 градусов).
- `pixels_per_cell`: Размер одной ячейки, для которой строится гистограмма ориентаций.
- `cells_per_block`: Количество ячеек, которые объединяются в один блок. Гистограммы ячеек внутри блока конкатенируются и затем нормализуются вместе. Блоки могут перекрываться.
- `block_norm`: Метод нормализации контраста для блока (например, 'L2-Hys' — сначала L2-норма, затем ограничение максимальных значений и повторная L2-норма; это делает HOG более устойчивым к изменениям освещения).
- `visualize=True`: Если установлено, функция также возвращает изображение, на котором визуализированы ориентации градиентов.
- `transform_sqrt=True`: Применение степенного преобразования (квадратный корень) к интенсивностям пикселей перед вычислением градиентов (гамма-коррекция), что может улучшить производительность.
- `fd`: Итоговый одномерный вектор признаков HOG. Его длина зависит от размера изображения и параметров HOG.
Пример из реальной жизни: 1. **Детекция пешеходов:** Это одно из классических и очень успешных применений HOG (в сочетании с линейным SVM классификатором).
2. **Распознавание объектов:** Для объектов с характерной формой и силуэтом (например, автомобили, животные).
3. **Классификация символов и цифр.**
4. **Анализ выражений лиц.**
15. Сравнение изображений. (Image Comparison)
Объяснение темы:
Сравнение изображений — это фундаментальная задача в компьютерном зрении, которая заключается в определении степени **сходства** или **различия** между двумя или более изображениями. "Сходство" может означать разные вещи в зависимости от контекста:
- **Идентичность:** Являются ли два изображения попиксельно одинаковыми?
- **Визуальное сходство:** Выглядят ли два изображения похожими для человека, даже если они не идентичны (например, две фотографии одной и той же кошки в разных позах)?
- **Содержательное сходство:** Содержат ли изображения одни и те же объекты или сцены, даже если они сняты по-разному?
- **Обнаружение изменений:** Есть ли различия между двумя изображениями одной и той же сцены, снятыми в разное время (например, для систем видеонаблюдения)?
Методы сравнения изображений можно условно разделить на несколько категорий:
1. **Попиксельное сравнение:** Сравниваются непосредственно значения интенсивностей соответствующих пикселей. Очень чувствительны к малейшим сдвигам, поворотам, изменениям масштаба и яркости.
2. **Сравнение на основе глобальных признаков:** Вычисляются глобальные дескрипторы для каждого изображения (например, цветовые гистограммы, текстурные признаки), а затем сравниваются эти дескрипторы с использованием метрик расстояния (Евклидово, косинусное и т.д.).
3. **Сравнение на основе локальных признаков:** Извлекаются локальные признаки (SIFT, ORB), сопоставляются между изображениями, и сходство оценивается по количеству и качеству совпадений.
4. **Структурное сходство:** Метрики, которые пытаются учесть восприятие сходства человеком, анализируя структуру, яркость и контраст (например, SSIM).
5. **Методы на основе глубокого обучения:** Используются нейронные сети (например, сиамские сети) для обучения пространства признаков, где похожие изображения отображаются в близкие точки, а непохожие — в далекие. Сходство измеряется как расстояние в этом выученном пространстве.
Методы и Примеры:
Среднеквадратичная ошибка (Mean Squared Error - MSE)
Описание: Простая и быстрая метрика, вычисляющая среднее значение квадратов разностей интенсивностей соответствующих пикселей двух изображений. Чем меньше MSE, тем более похожи изображения. Нулевое MSE означает, что изображения идентичны. Очень чувствительна к яркости и небольшим сдвигам.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Создадим два изображения для сравнения
imageA = np.zeros((100, 100), dtype=np.uint8)
imageA[25:75, 25:75] = 150 # Серый квадрат
imageB_identical = imageA.copy() # Идентичное изображение
imageB_slightly_different = imageA.copy()
imageB_slightly_different[50,50] = 160 # Одно отличие в пикселе
imageB_very_different = np.zeros((100, 100), dtype=np.uint8)
imageB_very_different[0:50, 0:50] = 200 # Другой квадрат
imageB_shifted = np.roll(imageA, 5, axis=1) # Сдвинутое изображение
def calculate_mse(img1, img2):
# Убедимся, что изображения одного размера
if img1.shape != img2.shape:
# print("Изображения должны быть одного размера для MSE.")
# Можно добавить изменение размера, но это повлияет на MSE
h,w = img1.shape[:2]
img2 = cv2.resize(img2, (w,h))
# return float('inf') # Или вызвать ошибку
# MSE - это сумма квадратов разностей, деленная на количество пикселей
err = np.sum((img1.astype("float") - img2.astype("float")) ** 2)
err /= float(img1.shape[0] * img1.shape[1])
return err
mse_identical = calculate_mse(imageA, imageB_identical)
mse_slight = calculate_mse(imageA, imageB_slightly_different)
mse_very_diff = calculate_mse(imageA, imageB_very_different)
mse_shifted = calculate_mse(imageA, imageB_shifted)
# print(f"MSE (ImageA vs Identical): {mse_identical:.2f}")
# print(f"MSE (ImageA vs Slightly Different): {mse_slight:.2f}")
# print(f"MSE (ImageA vs Very Different): {mse_very_diff:.2f}")
# print(f"MSE (ImageA vs Shifted): {mse_shifted:.2f}")
# plt.figure(figsize=(12,3))
# plt.subplot(141), plt.imshow(imageA, cmap='gray'), plt.title(f'A (MSE vs B_slight: {mse_slight:.0f})')
# plt.subplot(142), plt.imshow(imageB_slightly_different, cmap='gray'), plt.title('B_slight')
# plt.subplot(143), plt.imshow(imageB_very_different, cmap='gray'), plt.title(f'B_v_diff (MSE vs A: {mse_very_diff:.0f})')
# plt.subplot(144), plt.imshow(imageB_shifted, cmap='gray'), plt.title(f'B_shifted (MSE vs A: {mse_shifted:.0f})')
# plt.show()
Объяснение кода: - `calculate_mse(img1, img2)`: Функция для вычисления MSE.
- `img1.astype("float")`: Изображения преобразуются в тип `float` перед вычитанием, чтобы избежать проблем с переполнением/обрезанием для `uint8`.
- `(img1.astype("float") - img2.astype("float")) ** 2`: Вычисляется поэлементная разница, и результат возводится в квадрат.
- `np.sum(...)`: Суммируются все значения квадратов разностей.
- `err /= float(img1.shape[0] * img1.shape[1])`: Сумма делится на общее количество пикселей для получения среднего значения.
- **Важно:** MSE очень чувствительно к глобальным изменениям яркости. Если одно изображение просто немного темнее другого, MSE может быть большим, даже если структурно они идентичны. Также чувствительно к сдвигам: даже небольшой сдвиг даст большое MSE.
Пример из реальной жизни: 1. **Оценка качества сжатия с потерями:** Сравнение исходного изображения со сжатым/восстановленным.
2. **Проверка на точную идентичность изображений (если MSE=0).**
3. **Как компонент в более сложных алгоритмах:** Например, в некоторых алгоритмах отслеживания или выравнивания изображений MSE может служить функцией ошибки, которую нужно минимизировать.
Индекс структурного сходства (Structural Similarity Index - SSIM)
Описание: Метрика, разработанная для лучшего соответствия человеческому восприятию сходства изображений. SSIM оценивает сходство по трем компонентам: яркость, контраст и структура. Значения варьируются от -1 до 1, где 1 означает идеальное совпадение. Обычно SSIM вычисляется для небольших перекрывающихся окон по всему изображению, а затем усредняется.
Пример кода:
from skimage.metrics import structural_similarity as ssim_skimage # Используем skimage
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Используем те же изображения, что и для MSE
imageA = np.zeros((100, 100), dtype=np.uint8); imageA[25:75, 25:75] = 150
imageB_identical = imageA.copy()
imageB_slightly_different = imageA.copy(); imageB_slightly_different[50,50] = 160
imageB_very_different = np.zeros((100, 100), dtype=np.uint8); imageB_very_different[0:50, 0:50] = 200
imageB_shifted = np.roll(imageA, 5, axis=1)
# Для SSIM из skimage, изображения должны быть как минимум 7x7 пикселей.
# data_range: диапазон значений пикселей (255 для uint8). Если None, оценивается из данных.
ssim_A_vs_identical, _ = ssim_skimage(imageA, imageB_identical, full=True, data_range=255)
ssim_A_vs_slight, diff_slight = ssim_skimage(imageA, imageB_slightly_different, full=True, data_range=255)
ssim_A_vs_very_diff, diff_very_diff = ssim_skimage(imageA, imageB_very_different, full=True, data_range=255)
ssim_A_vs_shifted, diff_shifted = ssim_skimage(imageA, imageB_shifted, full=True, data_range=255)
# print(f"SSIM (ImageA vs Identical): {ssim_A_vs_identical:.4f}")
# print(f"SSIM (ImageA vs Slightly Different): {ssim_A_vs_slight:.4f}")
# print(f"SSIM (ImageA vs Very Different): {ssim_A_vs_very_diff:.4f}")
# print(f"SSIM (ImageA vs Shifted): {ssim_A_vs_shifted:.4f}")
# Визуализация изображений разницы (diff_image)
# diff_image показывает, где SSIM обнаружил наибольшие структурные различия.
# Оно нормализовано skimage к диапазону [0,1], поэтому умножаем на 255 для отображения.
# diff_slight_display = (diff_slight * 255).astype("uint8")
# diff_very_diff_display = (diff_very_diff * 255).astype("uint8")
# diff_shifted_display = (diff_shifted * 255).astype("uint8")
# plt.figure(figsize=(12,6))
# plt.subplot(231), plt.imshow(imageA, cmap='gray'), plt.title(f'A (SSIM vs B_slight: {ssim_A_vs_slight:.2f})')
# plt.subplot(232), plt.imshow(imageB_slightly_different, cmap='gray'), plt.title('B_slight')
# plt.subplot(233), plt.imshow(diff_slight_display, cmap='gray'), plt.title('Diff A vs B_slight')
# plt.subplot(234), plt.imshow(imageB_shifted, cmap='gray'), plt.title(f'B_shifted (SSIM vs A: {ssim_A_vs_shifted:.2f})')
# plt.subplot(235), plt.imshow(diff_shifted_display, cmap='gray'), plt.title('Diff A vs B_shifted')
# plt.tight_layout()
# plt.show()
Объяснение кода: - `from skimage.metrics import structural_similarity as ssim_skimage`: Импортируем функцию SSIM из scikit-image, так как в OpenCV она может быть не так легко доступна или отсутствовать в старых версиях.
- `score, diff_image = ssim_skimage(img1, img2, full=True, data_range=..., win_size=..., multichannel=...)`:
- `img1`, `img2`: Сравниваемые изображения (обычно в градациях серого, если `multichannel=False`).
- `full=True`: Если `True`, функция возвращает не только среднее значение SSIM по всему изображению (`score`), но и полное изображение SSIM (или изображение разницы, в зависимости от версии skimage), где каждый пиксель показывает локальное значение SSIM.
- `data_range`: Динамический диапазон входных изображений (например, `img.max() - img.min()`). Если `None`, вычисляется из данных. Для `uint8` часто `255`.
- `win_size` (опционально): Размер окна для вычисления локального SSIM (должен быть нечетным, по умолчанию 7).
- `multichannel=True` (опционально): Если `True`, вычисляет SSIM для многоканальных (цветных) изображений, усредняя SSIM по каналам. Если `False` (по умолчанию), ожидаются одноканальные изображения.
- `diff_image`: Изображение, показывающее локальные различия. Яркие области соответствуют большим структурным различиям.
Пример из реальной жизни: 1. **Оценка качества сжатия видео и изображений:** SSIM часто дает более адекватную оценку воспринимаемого качества, чем PSNR/MSE.
2. **Обнаружение изменений или подделок на изображениях:** Сравнивая изображение с его эталонной версией.
3. **Сравнение результатов различных алгоритмов обработки изображений:** Например, какой алгоритм восстановления изображения дает результат, наиболее структурно похожий на оригинал.
4. **Медицинская диагностика:** Сравнение снимков во времени для выявления изменений.
Сравнение по совпадению локальных признаков (Local Feature Matching Score)
Описание: Этот подход сравнивает изображения на основе количества и качества совпадений их локальных признаков (например, SIFT, ORB). Если между двумя изображениями найдено много надежных (хорошо сопоставленных) локальных признаков, это указывает на то, что они, вероятно, содержат общие объекты или сцены. Мерой сходства может служить просто количество хороших совпадений, или более сложная оценка, учитывающая геометрическую согласованность (например, можно ли найти гомографию между инлайерами).
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Создадим два "изображения" для демонстрации
img1_gray = np.zeros((200,200), np.uint8); cv2.putText(img1_gray, "OBJECT", (30,100), cv2.FONT_HERSHEY_SIMPLEX, 1.5, 255, 3)
img2_contains_object = img1_gray.copy() # img2 содержит тот же объект
img2_contains_object = np.roll(img2_contains_object, (10,15), axis=(0,1)) # немного сдвинем
img2_contains_object = cv2.resize(img2_contains_object, (180,180)) # немного изменим масштаб
noise = np.random.randint(0,50, size=img2_contains_object.shape, dtype=np.uint8)
img2_contains_object = cv2.add(img2_contains_object, noise) # добавим шум
img3_different = np.zeros((200,200), np.uint8); cv2.circle(img3_different, (100,100), 50, 200, -1)
# Инициализация детектора/дескриптора (например, ORB)
orb = cv2.ORB_create(nfeatures=500)
# Нахождение ключевых точек и дескрипторов
kp1, des1 = orb.detectAndCompute(img1_gray, None)
kp2, des2 = orb.detectAndCompute(img2_contains_object, None)
kp3, des3 = orb.detectAndCompute(img3_different, None)
def compare_by_features(d1, d2, kp_list1, kp_list2, img_ref1, img_ref2, ratio_thresh=0.75, draw_matches=False):
if d1 is None or d2 is None or len(d1) == 0 or len(d2) == 0:
return 0, None # Нет дескрипторов, сходство 0
# Сопоставление с помощью BFMatcher и Lowe's Ratio Test
bf = cv2.BFMatcher(cv2.NORM_HAMMING) # NORM_HAMMING для ORB
matches_knn = bf.knnMatch(d1, d2, k=2) # Находим k=2 лучших совпадений
good_matches = []
for m, n in matches_knn:
if len(matches_knn[0]) > 1 and m.distance < ratio_thresh * n.distance:
good_matches.append(m)
similarity_score = len(good_matches)
drawn_matches_img = None
if draw_matches:
drawn_matches_img = cv2.drawMatches(img_ref1, kp_list1, img_ref2, kp_list2,
good_matches, None, flags=cv2.DrawMatchesFlags_NOT_DRAW_SINGLE_POINTS)
return similarity_score, drawn_matches_img
# Сравнение img1 с img2 (должны быть похожи)
score12, matches_img12 = compare_by_features(des1, des2, kp1, kp2, img1_gray, img2_contains_object, draw_matches=True)
# print(f"Сходство (img1 vs img2_contains_object): {score12} хороших совпадений")
# Сравнение img1 с img3 (должны быть не похожи)
score13, matches_img13 = compare_by_features(des1, des3, kp1, kp3, img1_gray, img3_different, draw_matches=True)
# print(f"Сходство (img1 vs img3_different): {score13} хороших совпадений")
# Визуализация (если draw_matches=True)
# if matches_img12 is not None:
# plt.figure(figsize=(10,5)), plt.imshow(matches_img12), plt.title(f'Matches 1-2 (Score: {score12})')
# plt.show()
# if matches_img13 is not None:
# plt.figure(figsize=(10,5)), plt.imshow(matches_img13), plt.title(f'Matches 1-3 (Score: {score13})')
# plt.show()
Объяснение кода: 1. **Извлечение признаков:** Для каждого изображения (`img1_gray`, `img2_contains_object`, `img3_different`) извлекаются ключевые точки и дескрипторы с помощью ORB.
2. **Функция `compare_by_features`:**
- Принимает дескрипторы `d1`, `d2` и другие параметры.
- `bf = cv2.BFMatcher(cv2.NORM_HAMMING)`: Создает Brute-Force матчер с метрикой Хэмминга (подходит для бинарных дескрипторов ORB).
- `matches_knn = bf.knnMatch(d1, d2, k=2)`: Для каждого дескриптора из `d1` находит `k=2` лучших (ближайших) дескриптора из `d2`.
- **Тест отношения Лоу (Lowe's Ratio Test):** `if m.distance < ratio_thresh * n.distance: good_matches.append(m)`. Совпадение `m` (лучшее) считается "хорошим", если оно значительно лучше (т.е. его расстояние меньше), чем второе лучшее совпадение `n`. `ratio_thresh` (обычно 0.7-0.8) — это порог для этого отношения.
- `similarity_score = len(good_matches)`: Простое количество хороших совпадений используется как мера сходства.
- `cv2.drawMatches(...)`: Если `draw_matches=True`, рисует найденные хорошие совпадения между изображениями.
3. Функция вызывается для пар изображений, и выводится оценка сходства.
Пример из реальной жизни: 1. **Поиск дубликатов или почти дубликатов изображений в больших коллекциях.**
2. **Распознавание объектов:** Если на тестовом изображении найдено достаточно совпадений с эталонным изображением объекта.
3. **Контент-ориентированный поиск изображений (CBIR):** Найти изображения, визуально похожие на заданное изображение-образец.
4. **Основа для вычисления гомографии или других геометрических преобразований при сшивании панорам или 3D-реконструкции.
16. Локализация и детектирование объектов на изображениях (Object Localization and Detection)
Объяснение темы:
Представьте, что вы смотрите на фотографию улицы. Ваш мозг не просто видит "улицу", он также выделяет отдельные объекты: "вот автомобиль", "вот пешеход", "вот светофор". Более того, вы можете примерно указать, где каждый из этих объектов находится.
В компьютерном зрении связанные с этим задачи называются:
- **Локализация объекта (Object Localization):** Задача определения местоположения **одного** известного объекта на изображении. Обычно результат представляется в виде **ограничивающей рамки (bounding box)** — прямоугольника, который как можно точнее охватывает объект. При этом предполагается, что на изображении присутствует только один экземпляр этого объекта, или нас интересует только один, главный.
- **Детектирование объектов (Object Detection):** Более сложная задача. Цель — найти **все экземпляры** объектов, принадлежащих к одному или нескольким заданным классам (например, "кошки", "собаки", "автомобили"), на изображении и для каждого найденного экземпляра указать его класс и местоположение с помощью ограничивающей рамки. Изображение может содержать много объектов разных классов или не содержать их вовсе.
Эти задачи являются ключевыми для многих приложений, от автономного вождения до медицинской диагностики.
Методы и Примеры:
Детектирование объектов с помощью каскадов Хаара (Haar Cascade Object Detection)
Описание: Это классический и очень эффективный метод, предложенный Полом Виолой и Майклом Джонсом в 2001 году, который до сих пор широко используется, особенно для детекции лиц. Он основан на признаках Хаара, алгоритме обучения AdaBoost и каскадной структуре классификаторов. OpenCV предоставляет предварительно обученные каскады для детекции лиц, глаз, улыбок и т.д.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Загрузка изображения
# image_bgr = cv2.imread('group_photo.jpg') # Загрузите фото с лицами
# if image_bgr is None: print('Ошибка загрузки изображения'); exit()
# Создадим тестовое изображение с "лицами"
image_bgr = np.zeros((300, 400, 3), dtype=np.uint8)
image_bgr.fill(180) # Фон
# "Лицо" 1
cv2.rectangle(image_bgr, (50, 50), (150, 180), (120,120,120), -1)
cv2.circle(image_bgr, (80,90), 10, (0,0,0), -1); cv2.circle(image_bgr, (120,90), 10, (0,0,0), -1)
# "Лицо" 2
cv2.rectangle(image_bgr, (220, 80), (300, 200), (100,100,100), -1)
cv2.circle(image_bgr, (240,120), 8, (0,0,0), -1); cv2.circle(image_bgr, (280,120), 8, (0,0,0), -1)
gray_image = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
# Загрузка предварительно обученного каскада Хаара для детекции лиц
# Файлы .xml обычно находятся в директории данных OpenCV.
# Путь может отличаться в зависимости от вашей установки OpenCV.
# cv2.data.haarcascades должен указывать на правильную папку.
face_cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
face_cascade = cv2.CascadeClassifier()
if not face_cascade.load(cv2.samples.findFile(face_cascade_path)):
print(f"--! Ошибка загрузки каскада Хаара из: {face_cascade_path}")
print("Убедитесь, что путь к файлу XML корректен и OpenCV установлен правильно.")
exit(0)
# Детекция лиц на изображении
# scaleFactor: Насколько уменьшается размер изображения на каждом шаге масштабирования (e.g., 1.1 = 10%).
# Меньшие значения увеличивают точность, но и время работы.
# minNeighbors: Сколько "соседей" (перекрывающихся обнаружений) должно быть у каждого кандидата,
# чтобы он считался действительным лицом. Увеличивает надежность, убирает ложные срабатывания.
# minSize: Минимальный размер детектируемого объекта (ширина, высота) в пикселях.
faces_rects = face_cascade.detectMultiScale(gray_image,
scaleFactor=1.1,
minNeighbors=5,
minSize=(30, 30))
# print(f"Найдено {len(faces_rects)} лиц(а).")
# Отрисовка прямоугольников вокруг найденных лиц
image_with_detections = image_bgr.copy()
for (x, y, w, h) in faces_rects:
cv2.rectangle(image_with_detections, (x, y), (x+w, y+h), (0, 255, 0), 2) # Зеленая рамка
# Визуализация
# plt.figure(figsize=(8, 6))
# plt.imshow(cv2.cvtColor(image_with_detections, cv2.COLOR_BGR2RGB))
# plt.title('Детекция лиц с помощью каскада Хаара')
# plt.show()
Объяснение кода: - `face_cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'`: Формируем путь к XML-файлу с обученным каскадом.
- `face_cascade = cv2.CascadeClassifier()`: Создаем объект классификатора.
- `face_cascade.load(cv2.samples.findFile(face_cascade_path))`: Загружаем обученный каскад из XML-файла. `cv2.samples.findFile` помогает найти файл в стандартных путях OpenCV.
- `faces_rects = face_cascade.detectMultiScale(gray_image, scaleFactor, minNeighbors, minSize, flags)`:
- `gray_image`: Входное изображение в градациях серого (каскады Хаара обычно обучаются на серых изображениях).
- `scaleFactor`: Коэффициент масштабирования на каждом уровне пирамиды изображений. Чем ближе к 1.0, тем больше уровней и медленнее, но потенциально точнее для объектов разного размера.
- `minNeighbors`: Параметр, определяющий, сколько подтверждающих обнаружений в окрестности должно быть, чтобы текущее обнаружение считалось верным. Помогает отфильтровать ложные срабатывания.
- `minSize`: Минимальный размер объекта (ширина, высота) в пикселях, который будет детектироваться.
- `flags`: Флаги (обычно не используются или `cv2.CASCADE_SCALE_IMAGE`).
- `faces_rects`: Возвращает список (или NumPy массив) прямоугольников `(x, y, w, h)` для каждого найденного лица, где `(x,y)` — координаты левого верхнего угла, а `(w,h)` — ширина и высота.
Пример из реальной жизни: 1. **Детекция лиц в реальном времени:** В камерах смартфонов для автофокусировки, в системах безопасности, для автоматического тегирования людей на фото.
2. **Детекция глаз, носа, рта:** Используется для анализа выражений лиц, отслеживания взгляда.
3. **Детекция других объектов:** Существуют обученные каскады для автомобилей (полный кузов, номера), пешеходов, но для этих задач они часто уступают более современным методам (HOG+SVM, нейросети).
Детектирование объектов с помощью YOLO (You Only Look Once) / SSD (Single Shot MultiBox Detector) - Концептуально
Описание: Это современные детекторы на основе глубокого обучения, которые выполняют предсказание ограничивающих рамок и меток классов для объектов за один проход нейронной сети. Они известны своей высокой скоростью и хорошей точностью. OpenCV через свой модуль `dnn` (Deep Neural Network) позволяет загружать и использовать предварительно обученные модели YOLO, SSD и другие.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Этот код является концептуальным и требует наличия файлов модели:
# - .weights (веса модели)
# - .cfg (конфигурация архитектуры сети)
# - .names (файл с именами классов, по одному на строку)
# Эти файлы можно скачать для популярных моделей, таких как YOLOv3, YOLOv4-tiny и т.д.
# Пути к файлам модели (ЗАМЕНИТЕ НА СВОИ АКТУАЛЬНЫЕ ПУТИ)
# weights_path = "yolov3-tiny.weights"
# config_path = "yolov3-tiny.cfg"
# names_path = "coco.names"
# image_path = "street_scene.jpg" # Загрузите изображение с объектами
# try:
# net = cv2.dnn.readNet(weights_path, config_path) # Загрузка сети
# with open(names_path, "r") as f: # Загрузка имен классов
# classes = [line.strip() for line in f.readlines()]
# except cv2.error as e:
# print(f"Ошибка загрузки модели YOLO: {e}")
# print("Убедитесь, что пути к .weights и .cfg файлам верны и файлы существуют.")
# exit()
# # Получение имен всех слоев сети и определение выходных слоев
# layer_names = net.getLayerNames()
# # В старых версиях OpenCV getUnconnectedOutLayers() возвращал массив массивов [[idx]], в новых - просто массив [idx]
# output_layers_indices = net.getUnconnectedOutLayers()
# if isinstance(output_layers_indices, np.ndarray) and output_layers_indices.ndim > 1:
# output_layers_indices = output_layers_indices.flatten()
# output_layers = [layer_names[i - 1] for i in output_layers_indices]
# image = cv2.imread(image_path)
# if image is None: print(f"Ошибка загрузки изображения: {image_path}"); exit()
# height, width, _ = image.shape
# # Создание blob из изображения (предобработка для YOLO)
# # (0.00392 = 1/255 - нормализация, (416,416) - типичный размер входа для YOLO, swapRB=True т.к. OpenCV BGR)
# blob = cv2.dnn.blobFromImage(image, scalefactor=0.00392, size=(416, 416), mean=(0, 0, 0), swapRB=True, crop=False)
# net.setInput(blob)
# outs = net.forward(output_layers) # Прямой проход, получение предсказаний
# # Обработка результатов (декодирование выходов сети)
# class_ids = []
# confidences = []
# boxes = []
# conf_threshold = 0.5 # Порог уверенности для детекции
# nms_threshold = 0.4 # Порог для Non-Maximum Suppression
# for out_detection_layer in outs: # outs - это список выходов с каждого output_layer
# for detection in out_detection_layer: # detection - вектор предсказаний для одной рамки
# # detection[0:4] - координаты рамки (center_x, center_y, width, height) - нормированные
# # detection[4] - уверенность в наличии объекта (objectness score)
# # detection[5:] - уверенности для каждого класса
# scores = detection[5:]
# class_id = np.argmax(scores)
# confidence = scores[class_id]
# if confidence > conf_threshold:
# center_x = int(detection[0] * width)
# center_y = int(detection[1] * height)
# w_box = int(detection[2] * width)
# h_box = int(detection[3] * height)
# x_box = int(center_x - w_box / 2)
# y_box = int(center_y - h_box / 2)
# boxes.append([x_box, y_box, w_box, h_box])
# confidences.append(float(confidence))
# class_ids.append(class_id)
# # Применение Non-Maximum Suppression для удаления дублирующихся рамок
# indices = cv2.dnn.NMSBoxes(boxes, confidences, conf_threshold, nms_threshold)
# image_with_dnn_detections = image.copy()
# if len(indices) > 0:
# # В старых версиях OpenCV indices мог быть массивом массивов
# final_indices = indices.flatten() if isinstance(indices, np.ndarray) and indices.ndim > 1 else indices
# for i in final_indices:
# box = boxes[i]
# x, y, w, h = box[0], box[1], box[2], box[3]
# label = str(classes[class_ids[i]])
# confidence_val = confidences[i]
# color = (0, 255, 0) # Зеленый
# cv2.rectangle(image_with_dnn_detections, (x, y), (x + w, y + h), color, 2)
# cv2.putText(image_with_dnn_detections, f"{label} {confidence_val:.2f}", (x, y - 10 if y > 20 else y + h + 15),
# cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
# plt.figure(figsize=(10, 8))
# plt.imshow(cv2.cvtColor(image_with_dnn_detections, cv2.COLOR_BGR2RGB))
# plt.title('Детекция объектов с помощью YOLO (OpenCV DNN)')
# plt.show()
print("Это концептуальный код для YOLO/SSD с OpenCV DNN. Требуются файлы модели.")
Объяснение кода: 1. **Загрузка сети и классов:**
- `net = cv2.dnn.readNet(weights_path, config_path)`: Загружает предварительно обученную нейронную сеть (например, YOLO) из файлов весов и конфигурации.
- Имена классов загружаются из текстового файла.
2. **Получение выходных слоев:** Определяются имена слоев, с которых будут считываться предсказания.
3. **Предобработка изображения (`cv2.dnn.blobFromImage`):**
- Изображение преобразуется в `blob` — специальный формат для входа в сеть. Это включает изменение размера до ожидаемого сетью (например, 416x416 для многих YOLO), нормализацию пикселей (например, деление на 255), возможное вычитание среднего значения и изменение порядка каналов (OpenCV BGR -> RGB, если сеть обучалась на RGB).
4. **Прямой проход (`net.setInput(blob)`, `outs = net.forward(output_layers)`):** Blob подается в сеть, и выполняется прямой проход для получения предсказаний `outs`.
5. **Обработка результатов:**
- `outs` содержит информацию о предсказанных ограничивающих рамках, их уверенностях и вероятностях классов для каждой рамки.
- Эти "сырые" предсказания декодируются: координаты рамок переводятся из нормированных в пиксельные, отбрасываются рамки с низкой уверенностью (`conf_threshold`).
6. **Non-Maximum Suppression (`cv2.dnn.NMSBoxes`):**
- Удаляет избыточные, сильно перекрывающиеся рамки для одного и того же объекта, оставляя только одну с наилучшей уверенностью.
- `bboxes`: Список рамок `[x, y, w, h]`.
- `confidences`: Список соответствующих уверенностей.
- `score_threshold`: Тот же, что и `conf_threshold`.
- `nms_threshold`: Порог IoU (Intersection over Union). Если IoU двух рамок больше этого порога, одна из них (с меньшей уверенностью) подавляется.
- `indices`: Возвращает индексы рамок, которые нужно оставить.
7. **Отрисовка:** Оставшиеся после NMS рамки рисуются на изображении вместе с метками классов и уверенностями.
Пример из реальной жизни: 1. **Автономное вождение:** Детекция автомобилей, пешеходов, дорожных знаков, светофоров.
2. **Системы видеонаблюдения:** Обнаружение вторжений, отслеживание людей, подсчет объектов.
3. **Робототехника:** Распознавание объектов для взаимодействия с ними.
4. **Ритейл:** Подсчет посетителей, анализ поведения покупателей, контроль наличия товаров на полках.
5. **Медицинская диагностика:** Обнаружение опухолей, аномалий на рентгеновских снимках, КТ, МРТ.
17. Колоризация и сегментация изображений (Image Colorization and Segmentation)
Объяснение темы:
**Колоризация изображений (Image Colorization):**
Представьте себе старую черно-белую фотографию. Колоризация — это процесс добавления к ней реалистичных цветов, чтобы она выглядела так, как будто была изначально снята в цвете. Это сложная задача, так как для одного и того же оттенка серого может существовать множество правдоподобных цветов (например, серая трава, серое небо и серое платье могут иметь совершенно разные цвета в реальности).
Методы колоризации бывают:
- **Ручные или полуавтоматические:** Художник или пользователь вручную раскрашивает части изображения или ставит "цветные метки" (scribbles) в некоторых областях, а затем алгоритм распространяет эти цвета на остальные части изображения, стараясь сохранить плавность и учесть текстуру и яркость.
- **Автоматические:** Современные подходы используют модели машинного обучения, особенно **глубокие сверточные нейронные сети (CNN)**, обученные на огромных наборах пар "цветное изображение — его черно-белая версия". Такие модели учатся предсказывать вероятные цвета для каждого пикселя на основе его яркости и контекста (окружающих пикселей и глобальной структуры изображения). Часто используется цветовое пространство Lab, где канал L (Luminance - яркость) берется из черно-белого изображения, а каналы a и b (содержащие информацию о цвете) предсказываются сетью.
**Сегментация изображений (Image Segmentation):**
Это процесс разделения цифрового изображения на несколько **сегментов** (множеств пикселей), которые соответствуют различным объектам, частям объектов или областям с одинаковыми свойствами (например, цвет, текстура). Цель сегментации — упростить или изменить представление изображения на более осмысленное и легкое для анализа. Вместо работы с миллионами отдельных пикселей, мы можем работать с несколькими сегментами.
Типы сегментации:
- **Семантическая сегментация (Semantic Segmentation):** Каждому пикселю изображения присваивается метка класса, к которому он принадлежит (например, "дорога", "небо", "автомобиль", "дерево", "здание"). Все пиксели одного класса получают одну и ту же метку, независимо от того, являются ли они частью одного или разных экземпляров этого класса.
- **Экземплярная сегментация (Instance Segmentation):** Более сложная задача. Она не только классифицирует каждый пиксель, но и различает отдельные экземпляры объектов одного и того же класса. Например, если на фото три автомобиля, каждый из них будет выделен как отдельный сегмент "автомобиль_1", "автомобиль_2", "автомобиль_3".
- **Паноптическая сегментация (Panoptic Segmentation):** Объединяет семантическую и экземплярную сегментацию. Она присваивает каждому пикселю метку класса и идентификатор экземпляра (если пиксель принадлежит счетному объекту, как "автомобиль") или только метку класса (если пиксель принадлежит аморфной области, как "небо" или "трава").
Методы и Примеры:
Пороговая сегментация (Image Thresholding)
Описание: Это один из самых простых методов сегментации. Он преобразует изображение в градациях серого в бинарное изображение (черно-белое). Пиксели, интенсивность которых выше определенного порога, становятся белыми (например, 255), а те, что ниже — черными (0), или наоборот. Существует несколько типов пороговой обработки: простая глобальная, адаптивная, метод Оцу.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Загрузка изображения или создание тестового
# gray_image = cv2.imread('document_scan.png', cv2.IMREAD_GRAYSCALE)
# if gray_image is None: print('Ошибка загрузки'); exit()
# Создадим тестовое изображение с объектом на фоне разной яркости
gray_image = np.zeros((200, 300), dtype=np.uint8)
gray_image[:, :150] = 80 # Левая часть фона темнее
gray_image[:, 150:] = 160 # Правая часть фона светлее
# Объект (темнее фона слева, светлее фона справа)
cv2.rectangle(gray_image, (50,50), (250,150), 120, -1)
# 1. Простая глобальная пороговая обработка
# ret: само пороговое значение (здесь оно задано нами как 127)
# thresh_binary: результат - бинарное изображение
# cv2.THRESH_BINARY: если пиксель > порога, то max_val, иначе 0
# cv2.THRESH_BINARY_INV: если пиксель > порога, то 0, иначе max_val
global_threshold_value = 100
max_pixel_value = 255
ret, thresh_binary_global = cv2.threshold(gray_image, global_threshold_value, max_pixel_value, cv2.THRESH_BINARY)
# 2. Адаптивная пороговая обработка
# Порог вычисляется для каждого пикселя на основе его небольшой окрестности.
# Это хорошо работает для изображений с изменяющимися условиями освещения.
# cv2.ADAPTIVE_THRESH_MEAN_C: порог - среднее значение окрестности минус константа C.
# cv2.ADAPTIVE_THRESH_GAUSSIAN_C: порог - Гауссово взвешенное среднее окрестности минус C.
# blockSize: размер окрестности (должен быть нечетным, >1).
# C: константа, вычитаемая из среднего или взвешенной суммы.
block_size_adaptive = 15 # Должен быть нечетным
constant_C = 5
thresh_adaptive_mean = cv2.adaptiveThreshold(gray_image, max_pixel_value,
cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY,
block_size_adaptive, constant_C)
# 3. Метод Оцу (Otsu's Binarization)
# Автоматически вычисляет оптимальное глобальное пороговое значение из гистограммы изображения.
# Хорошо работает, если гистограмма имеет два пика (бимодальная).
# Возвращает оптимальный порог (ret_otsu) и бинарное изображение.
ret_otsu, thresh_otsu = cv2.threshold(gray_image, 0, max_pixel_value,
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# print(f"Оптимальный порог по Оцу: {ret_otsu}")
# Визуализация
# titles = ['Исходное серое', f'Глобальный порог ({global_threshold_value})',
# f'Адаптивный средний (Блок {block_size_adaptive}, C={constant_C})', f'Оцу (Порог={ret_otsu:.0f})']
# images_to_show = [gray_image, thresh_binary_global, thresh_adaptive_mean, thresh_otsu]
# plt.figure(figsize=(15, 5))
# for i in range(len(images_to_show)):
# plt.subplot(1, len(images_to_show), i + 1)
# plt.imshow(images_to_show[i], cmap='gray')
# plt.title(titles[i])
# plt.xticks([]), plt.yticks([])
# plt.tight_layout()
# plt.show()
Объяснение кода: - `ret, dst = cv2.threshold(src, thresh_val, max_val, type)` (для глобального порога и Оцу):
- `src`: Входное одноканальное (серое) изображение.
- `thresh_val`: Пороговое значение. Для Оцу оно игнорируется (можно поставить 0), так как вычисляется автоматически.
- `max_val`: Значение, присваиваемое пикселям, которые удовлетворяют условию (например, если пиксель > `thresh_val` при `cv2.THRESH_BINARY`).
- `type`: Тип пороговой обработки.
- `cv2.THRESH_BINARY`: `dst(x,y) = max_val` если `src(x,y) > thresh_val`, иначе 0.
- `cv2.THRESH_BINARY_INV`: Наоборот.
- `cv2.THRESH_TRUNC`: `dst(x,y) = thresh_val` если `src(x,y) > thresh_val`, иначе `src(x,y)`.
- `cv2.THRESH_TOZERO`: `dst(x,y) = src(x,y)` если `src(x,y) > thresh_val`, иначе 0.
- `cv2.THRESH_TOZERO_INV`: Наоборот.
- `cv2.THRESH_OTSU`: Используется в комбинации с одним из вышеуказанных типов (например, `cv2.THRESH_BINARY + cv2.THRESH_OTSU`). Автоматически определяет оптимальный порог.
- `dst = cv2.adaptiveThreshold(src, max_val, adaptiveMethod, thresholdType, blockSize, C)`:
- `adaptiveMethod`: `cv2.ADAPTIVE_THRESH_MEAN_C` (порог — среднее по окрестности минус C) или `cv2.ADAPTIVE_THRESH_GAUSSIAN_C` (порог — Гауссово взвешенное среднее по окрестности минус C).
- `thresholdType`: Тип бинаризации (обычно `cv2.THRESH_BINARY`).
- `blockSize`: Размер квадратной окрестности для вычисления локального порога (нечетное число).
- `C`: Константа, вычитаемая из вычисленного среднего/взвешенного среднего. Позволяет тонко настроить результат.
Пример из реальной жизни: 1. **Выделение текста на отсканированных документах:** Преобразование документа в черно-белый вид, где текст — один цвет, фон — другой.
2. **Простая сегментация объектов:** Если объекты имеют хороший контраст с фоном (например, темные объекты на светлом фоне).
3. **Предварительная обработка для других алгоритмов:** Например, перед поиском контуров или анализом форм.
4. **Детекция дефектов:** Если дефекты проявляются как области с аномальной яркостью.
Семантическая сегментация с помощью U-Net (Концептуально, с использованием OpenCV DNN)
Описание: U-Net — это популярная архитектура сверточной нейронной сети, специально разработанная для биомедицинской сегментации, но успешно применяемая и в других областях. Она имеет симметричную U-образную структуру (энкодер-декодер) с "пробросом соединений" (skip connections), которые помогают дешифратору лучше использовать информацию о локализации из энкодера. OpenCV DNN модуль может запускать предварительно обученные модели U-Net (например, в формате ONNX, TensorFlow, Caffe).
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Этот код является концептуальным и требует наличия файла обученной модели U-Net
# (например, .pb, .onnx) и понимания ее входных/выходных форматов.
# Путь к модели (ЗАМЕНИТЕ НА СВОЙ АКТУАЛЬНЫЙ ПУТЬ К МОДЕЛИ U-NET)
# model_path = "path/to/your/u-net-model.onnx"
# image_path = "image_for_segmentation.jpg"
# # Предполагаемые параметры модели (могут отличаться для вашей модели)
# INPUT_WIDTH = 256
# INPUT_HEIGHT = 256
# NUM_CLASSES = 2 # Например, фон + объект
# try:
# net = cv2.dnn.readNet(model_path) # Загрузка сети (для ONNX: cv2.dnn.readNetFromONNX)
# except cv2.error as e:
# print(f"Ошибка загрузки модели U-Net: {e}")
# exit()
# original_image = cv2.imread(image_path)
# if original_image is None: print(f"Ошибка загрузки изображения: {image_path}"); exit()
# h_orig, w_orig = original_image.shape[:2]
# # 1. Предобработка изображения
# # Изменение размера до ожидаемого моделью, нормализация, создание blob
# blob = cv2.dnn.blobFromImage(original_image, scalefactor=1.0/255.0,
# size=(INPUT_WIDTH, INPUT_HEIGHT),
# mean=(0,0,0), swapRB=True, crop=False)
# net.setInput(blob)
# # 2. Прямой проход через сеть
# # Выходной слой U-Net обычно имеет форму (batch_size, num_classes, height, width)
# output_logits = net.forward() # Имя выходного слоя может потребоваться явно указать для некоторых моделей
# # 3. Постобработка выхода
# # output_logits[0] - берем предсказания для первого (и единственного) изображения в батче
# # np.argmax(output_logits[0], axis=0) - для каждого пикселя находим класс с максимальной вероятностью
# class_map = np.argmax(output_logits[0], axis=0) # (height_out, width_out)
# # Изменение размера карты классов обратно к исходному размеру изображения
# # Используем cv2.INTER_NEAREST, чтобы не вводить промежуточные значения классов
# class_map_resized = cv2.resize(class_map.astype(np.uint8),
# (w_orig, h_orig),
# interpolation=cv2.INTER_NEAREST)
# # 4. Визуализация (раскрашивание сегментов)
# # Создадим палитру цветов для классов (здесь для NUM_CLASSES=2: фон-черный, объект-зеленый)
# colors = np.array([[0,0,0], [0,255,0]], dtype=np.uint8) if NUM_CLASSES == 2 else
# np.random.randint(0, 255, size=(NUM_CLASSES, 3), dtype=np.uint8)
# segmented_visualization = colors[class_map_resized]
# # Наложение маски на исходное изображение для наглядности
# alpha = 0.6
# blended_image = cv2.addWeighted(original_image, alpha, segmented_visualization, 1-alpha, 0)
# plt.figure(figsize=(12, 6))
# plt.subplot(121), plt.imshow(cv2.cvtColor(original_image, cv2.COLOR_BGR2RGB)), plt.title('Исходное изображение')
# plt.subplot(122), plt.imshow(cv2.cvtColor(blended_image, cv2.COLOR_BGR2RGB)), plt.title('Семантическая сегментация (U-Net)')
# plt.show()
print("Это концептуальный код для семантической сегментации с U-Net. Требует обученной модели.")
Объяснение кода: 1. **Загрузка модели (`cv2.dnn.readNet`):** Загружается предварительно обученная модель U-Net.
2. **Предобработка (`cv2.dnn.blobFromImage`):** Входное изображение подготавливается: изменяется размер до того, который ожидает модель (например, 256x256), значения пикселей нормализуются (часто делением на 255, чтобы были в диапазоне [0,1]), и создается `blob`.
3. **Прямой проход (`net.setInput(blob)`, `net.forward()`):** Blob подается в сеть. Выход `output_logits` обычно имеет размерность `(batch_size, num_classes, height_out, width_out)`, где `num_classes` — количество классов, на которые сегментируется изображение.
4. **Постобработка:**
- `class_map = np.argmax(output_logits[0], axis=0)`: Для каждого пикселя на выходной карте выбирается индекс канала (класса) с максимальным значением (логитом или вероятностью). Это дает 2D-карту классов.
- `cv2.resize(...)`: Карта классов масштабируется обратно к исходному размеру изображения. Важно использовать интерполяцию `cv2.INTER_NEAREST`, чтобы не создавать новые, недействительные значения классов.
5. **Визуализация:** Карта классов раскрашивается, присваивая каждому классу свой цвет, для наглядного отображения результата сегментации.
Пример из реальной жизни: 1. **Медицинская диагностика:** Сегментация опухолей, органов, сосудов, клеток на различных медицинских снимках (МРТ, КТ, рентген, микроскопия).
2. **Автономное вождение:** Сегментация дороги, тротуаров, пешеходов, автомобилей, зданий для понимания сцены.
3. **Анализ спутниковых и аэрофотоснимков:** Картографирование лесов, водных объектов, сельскохозяйственных угодий, застроенных территорий.
4. **Робототехника:** Понимание окружения для навигации и взаимодействия с объектами.
5. **Редактирование изображений:** Например, для отделения переднего плана от фона.
18. Оптический поток (Optical Flow)
Объяснение темы:
Представьте, что вы смотрите видеозапись движущегося автомобиля или просто двигаете камерой, снимая неподвижную сцену. Точки на объектах или на фоне будут смещаться от кадра к кадру. **Оптический поток** — это видимое движение объектов, поверхностей и краев на изображении между двумя последовательными кадрами видео. Это движение вызвано либо относительным движением между камерой и сценой, либо движением самих объектов в сцене.
Оптический поток обычно представляется как **2D векторное поле**. Для каждого пикселя (или для некоторых выбранных пикселей) на первом кадре вычисляется вектор, который показывает, куда этот пиксель (или соответствующая ему точка на объекте) сместился на втором кадре. Этот вектор имеет две компоненты: смещение по оси X (`dx`) и смещение по оси Y (`dy`).
**Основные допущения, лежащие в основе многих алгоритмов вычисления оптического потока:**
1. **Яркостная константность (Brightness Constancy):** Яркость (или цвет) пикселя, соответствующего одной и той же точке на объекте, не сильно меняется между двумя последовательными кадрами. То есть, если точка `(x,y)` на первом кадре `I1` имеет яркость `I1(x,y)`, то на втором кадре `I2` эта же точка, сместившаяся на `(dx,dy)`, будет иметь примерно ту же яркость: `I1(x,y) ≈ I2(x+dx, y+dy)`.
2. **Малые смещения (Small Motion):** Объекты смещаются на небольшие расстояния между кадрами. Это позволяет использовать аппроксимации на основе рядов Тейлора для линеаризации уравнений.
3. **Пространственная гладкость (Spatial Coherence):** Соседние пиксели на изображении, принадлежащие одному и тому же объекту, обычно имеют схожее движение. Это допущение помогает решать проблему неопределенности (aperture problem), когда локальной информации недостаточно для однозначного определения вектора потока (например, для точки на прямой линии).
**Типы оптического потока:**
- **Разреженный (Sparse) оптический поток:** Векторы потока вычисляются только для некоторых, заранее выбранных (обычно характерных, легко отслеживаемых) точек изображения, таких как углы (например, найденные детектором Харриса или Ши-Томаси). Пример алгоритма: Лукаса-Канаде.
- **Плотный (Dense) оптический поток:** Векторы потока вычисляются для **всех** пикселей изображения. Пример алгоритма: Фарнебека, Horn-Schunck.
Методы и Примеры:
Разреженный оптический поток Лукаса-Канаде (Lucas-Kanade Sparse Optical Flow)
Описание: Вычисляет оптический поток для заданного набора 2D точек. Обычно эти точки — это углы или другие хорошо отслеживаемые признаки, найденные на первом кадре. Алгоритм итеративно уточняет положение этих точек на втором кадре, минимизируя разницу яркостей в небольшой окрестности.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Загрузка видео или создание двух кадров для примера
# cap = cv2.VideoCapture('slow_traffic_small.mp4')
# ret, old_frame_bgr = cap.read()
# if not ret: print('Не удалось прочитать видео/кадр'); exit()
# old_gray = cv2.cvtColor(old_frame_bgr, cv2.COLOR_BGR2GRAY)
# Создадим два кадра: old_gray и new_gray со смещенным объектом
old_frame_bgr = np.zeros((200, 300, 3), dtype=np.uint8); old_frame_bgr.fill(50)
cv2.rectangle(old_frame_bgr, (100, 75), (150, 125), (200,200,200), -1)
old_gray = cv2.cvtColor(old_frame_bgr, cv2.COLOR_BGR2GRAY)
new_frame_bgr = np.zeros((200, 300, 3), dtype=np.uint8); new_frame_bgr.fill(50)
cv2.rectangle(new_frame_bgr, (110, 80), (160, 130), (200,200,200), -1) # Смещенный
new_gray = cv2.cvtColor(new_frame_bgr, cv2.COLOR_BGR2GRAY)
# --- Шаг 1: Найти характерные точки на первом кадре (например, углы Ши-Томаси) ---
# Параметры для детектора углов ShiTomasi
# maxCorners: максимальное количество углов для возврата.
# qualityLevel: минимально допустимое качество угла (0-1).
# minDistance: минимальное евклидово расстояние между возвращаемыми углами.
# blockSize: размер окна для вычисления производных.
feature_params = dict(maxCorners=100, qualityLevel=0.3, minDistance=7, blockSize=7)
p0 = cv2.goodFeaturesToTrack(old_gray, mask=None, **feature_params)
if p0 is None:
print("Не найдено точек для отслеживания на первом кадре.")
exit()
# --- Шаг 2: Вычислить оптический поток с помощью Лукаса-Канаде ---
# Параметры для оптического потока Лукаса-Канаде
# winSize: размер окна поиска на каждом уровне пирамиды.
# maxLevel: 0-based максимальный уровень пирамиды (0 - только исходное изображение, без пирамиды).
# criteria: критерии остановки итеративного поиска (точность и/или макс. число итераций).
lk_params = dict(winSize=(15,15), maxLevel=2,
criteria=(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03))
# p1: вычисленные новые положения точек p0 на втором кадре (new_gray).
# st: статусный вектор (1, если поток для соответствующей точки найден, 0 иначе).
# err: вектор ошибок (не всегда используется).
p1, status, error = cv2.calcOpticalFlowPyrLK(old_gray, new_gray, p0, None, **lk_params)
# --- Шаг 3: Отобрать "хорошие" точки (для которых поток найден) ---
# status.ravel()==1 создает булеву маску
good_new_points = p1[status.ravel() == 1] if p1 is not None else np.array([])
good_old_points = p0[status.ravel() == 1] if p0 is not None else np.array([])
# --- Шаг 4: Визуализация (рисование векторов потока) ---
# Создание маски для рисования траекторий
flow_visualization_mask = np.zeros_like(old_frame_bgr)
output_frame_with_flow = new_frame_bgr.copy() # Рисуем на втором кадре
for i, (new_pt, old_pt) in enumerate(zip(good_new_points, good_old_points)):
a, b = new_pt.ravel().astype(int)
c, d = old_pt.ravel().astype(int)
# Рисуем линию от старой позиции к новой
flow_visualization_mask = cv2.line(flow_visualization_mask, (a,b), (c,d), (0,255,0), 2) # Зеленая линия
# Рисуем кружок в новой позиции точки
output_frame_with_flow = cv2.circle(output_frame_with_flow, (a,b), 5, (0,0,255), -1) # Красный кружок
combined_display = cv2.add(output_frame_with_flow, flow_visualization_mask)
# plt.figure(figsize=(10, 7))
# plt.imshow(cv2.cvtColor(combined_display, cv2.COLOR_BGR2RGB))
# plt.title('Разреженный оптический поток (Лукас-Канаде)')
# plt.show()
# В цикле обработки видео:
# old_gray = new_gray.copy()
# p0 = good_new_points.reshape(-1, 1, 2) # Новые точки становятся старыми для следующего шага
Объяснение кода: 1. **Нахождение точек для отслеживания (`cv2.goodFeaturesToTrack`):**
- На первом кадре (`old_gray`) находятся характерные точки (углы) с помощью детектора Ши-Томаси. `p0` — это массив 2D-координат этих точек.
2. **Вычисление оптического потока (`cv2.calcOpticalFlowPyrLK`):**
- `old_gray`, `new_gray`: Предыдущий и текущий кадры в градациях серого.
- `p0`: Входной массив 2D-точек, для которых нужно найти поток.
- `None`: Маска для `p1` (не используется здесь).
- `lk_params`: Словарь параметров:
- `winSize`: Размер окна поиска вокруг каждой точки. Большее окно может справиться с большими движениями, но усредняет поток по большей области.
- `maxLevel`: Количество уровней в Гауссовой пирамиде. Использование пирамиды помогает отслеживать большие смещения.
- `criteria`: Критерии остановки итеративного алгоритма (например, `(cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, 10, 0.03)` означает остановить после 10 итераций или если ошибка меньше 0.03).
- `p1`: Выходной массив 2D-координат — предсказанные положения точек `p0` на кадре `new_gray`.
- `status`: Выходной массив статусов. `status[i] = 1`, если поток для i-ой точки найден, иначе 0.
- `error`: Выходной массив ошибок для каждой точки.
3. **Отбор "хороших" точек:** Используя `status`, выбираются только те точки из `p0` и `p1`, для которых поток был успешно вычислен.
4. **Визуализация:** Для каждой пары `(good_old_point, good_new_point)` рисуется линия от старого положения к новому, показывая вектор смещения.
Пример из реальной жизни: 1. **Отслеживание объектов (Object Tracking):** Если объект имеет достаточно характерных точек, их можно отслеживать для определения его траектории.
2. **Видеостабилизация (Video Stabilization):** Оценивается глобальное движение камеры по потоку фоновых точек, и затем это движение компенсируется.
3. **Анализ движения:** Изучение паттернов движения в спорте, биологии.
4. **Управление жестами (Gesture Recognition).**
5. **Оценка движения камеры (Egomotion Estimation) в робототехнике и SLAM.**
Плотный оптический поток Фарнебека (Farneback Dense Optical Flow)
Описание: Вычисляет оптический поток для всех пикселей изображения, используя алгоритм, предложенный Гуннаром Фарнебеком. Этот метод аппроксимирует окрестность каждого пикселя на двух последовательных кадрах полиномом и затем отслеживает смещение этих полиномиальных представлений. Результатом является двумерное векторное поле потока, имеющее ту же размерность, что и исходные изображения.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Используем те же два кадра, что и для Лукаса-Канаде
old_frame_bgr = np.zeros((200, 300, 3), dtype=np.uint8); old_frame_bgr.fill(50)
cv2.rectangle(old_frame_bgr, (100, 75), (150, 125), (200,200,200), -1)
prvs_gray = cv2.cvtColor(old_frame_bgr, cv2.COLOR_BGR2GRAY)
new_frame_bgr = np.zeros((200, 300, 3), dtype=np.uint8); new_frame_bgr.fill(50)
cv2.rectangle(new_frame_bgr, (110, 80), (160, 130), (200,200,200), -1) # Смещенный
next_gray = cv2.cvtColor(new_frame_bgr, cv2.COLOR_BGR2GRAY)
# Вычисление плотного оптического потока Фарнебека
# flow: Выходной массив потока (размер как у next_gray, 2 канала: dx, dy).
# pyr_scale: Масштаб пирамиды (<1, например, 0.5 для классической пирамиды).
# levels: Количество уровней пирамиды.
# winsize: Размер окна усреднения; большие значения дают более гладкий, но размытый поток.
# iterations: Количество итераций на каждом уровне пирамиды.
# poly_n: Размер окрестности пикселя для полиномиальной аппроксимации (5 или 7).
# poly_sigma: Стандартное отклонение Гауссианы для сглаживания производных (1.1 для poly_n=5, 1.5 для poly_n=7).
# flags: Флаги операции (0 по умолчанию, или cv2.OPTFLOW_USE_INITIAL_FLOW, cv2.OPTFLOW_FARNEBACK_GAUSSIAN).
flow_vectors = cv2.calcOpticalFlowFarneback(prvs_gray, next_gray, None,
pyr_scale=0.5, levels=3, winsize=15,
iterations=3, poly_n=5, poly_sigma=1.1, flags=0)
# Визуализация плотного оптического потока (преобразование векторов в цвета HSV)
# mag: величина (скорость) смещения.
# ang: угол (направление) смещения.
mag, ang_rad = cv2.cartToPolar(flow_vectors[...,0], flow_vectors[...,1])
# Создаем HSV изображение для визуализации
hsv_viz_mask = np.zeros_like(old_frame_bgr) # (высота, ширина, 3 канала)
# Устанавливаем насыщенность (Saturation) в максимум для ярких цветов
hsv_viz_mask[..., 1] = 255
# Угол определяет Тон (Hue) - цвет на цветовом круге
hsv_viz_mask[..., 0] = ang_rad * 180 / np.pi / 2 # Преобразование радиан в диапазон 0-179 для OpenCV Hue
# Величина определяет Яркость (Value) - чем быстрее движение, тем ярче
hsv_viz_mask[..., 2] = cv2.normalize(mag, None, 0, 255, cv2.NORM_MINMAX)
# Преобразование из HSV в BGR для отображения
bgr_flow_visualization = cv2.cvtColor(hsv_viz_mask, cv2.COLOR_HSV2BGR)
# plt.figure(figsize=(10, 7))
# plt.imshow(cv2.cvtColor(bgr_flow_visualization, cv2.COLOR_BGR2RGB))
# plt.title('Плотный оптический поток (Фарнебек) - HSV визуализация')
# plt.show()
# В цикле обработки видео:
# prvs_gray = next_gray.copy()
Объяснение кода: - `flow_vectors = cv2.calcOpticalFlowFarneback(prev, next, flow, pyr_scale, levels, winsize, iterations, poly_n, poly_sigma, flags)`:
- `prev`, `next`: Предыдущий и текущий одноканальные 8-битные изображения.
- `flow`: Входной/выходной массив потока. Если `None`, вычисляется с нуля. Если передать результат предыдущего шага, может использоваться как инициализация (`flags=cv2.OPTFLOW_USE_INITIAL_FLOW`).
- Параметры (`pyr_scale`, `levels`, `winsize`, `iterations`, `poly_n`, `poly_sigma`, `flags`) контролируют поведение алгоритма, включая использование пирамид, размер окна, степень полинома и т.д. Значения по умолчанию часто работают неплохо.
- `flow_vectors`: Результат — двумерный массив той же высоты и ширины, что и входные изображения, но с двумя каналами. `flow_vectors[y, x, 0]` — это смещение `dx` для пикселя `(x,y)`, а `flow_vectors[y, x, 1]` — смещение `dy`.
- **Визуализация:**
- `mag, ang_rad = cv2.cartToPolar(dx_component, dy_component)`: Преобразует декартовы компоненты потока `(dx, dy)` в полярные координаты: величину (магнитуду) `mag` и угол `ang_rad` (в радианах).
- Угол используется для определения Тона (Hue) в цветовой модели HSV, а величина — для Яркости (Value). Насыщенность (Saturation) обычно устанавливается в максимум для получения ярких цветов.
- Полученное HSV-изображение затем конвертируется в BGR для отображения. Разные цвета будут соответствовать разным направлениям движения, а яркость цвета — скорости движения.
Пример из реальной жизни: 1. **Сегментация по движению:** Области с когерентным (согласованным) оптическим потоком могут быть сгруппированы в сегменты, соответствующие движущимся объектам.
2. **Сжатие видео:** Информация об оптическом потоке используется для предсказания следующих кадров на основе предыдущих.
3. **Создание эффектов замедленного/ускоренного движения (motion interpolation/extrapolation).**
4. **Анализ динамики жидкостей или газов в научных визуализациях.**
5. **3D-реконструкция из видео (Structure from Motion), где плотный поток может дать больше информации, чем разреженный.
19. Оценка позы (Pose Estimation)
Объяснение темы:
**Оценка позы (Pose Estimation)** — это задача определения пространственного **положения (position)** и **ориентации (orientation)** объекта относительно камеры или некоторой эталонной системы координат. В более широком смысле, это может также означать определение конфигурации сочлененного (шарнирного) объекта, такого как человеческое тело, путем нахождения положений его ключевых точек (суставов).
Можно выделить два основных типа задач оценки позы:
1. **Оценка позы жесткого объекта (Rigid Body Pose Estimation):**
- Относится к объектам, форма которых не меняется (например, книга, чашка, автомобиль).
- Цель — найти 6 степеней свободы (6DoF) объекта: 3 координаты для положения (сдвиг `tx, ty, tz`) и 3 параметра для ориентации (например, углы Эйлера, кватернион или вектор вращения Родригеса `rx, ry, rz`).
- Обычно требует наличия 3D-модели объекта (например, набора 3D-координат его характерных точек) и установления соответствий между этими 3D-точками и их 2D-проекциями на изображении.
- После нахождения этих соответствий, задача решается с помощью алгоритмов **Perspective-n-Point (PnP)**, которые вычисляют вращение `R` и сдвиг `t`, переводящие 3D-точки из системы координат объекта в систему координат камеры. Для этого необходимо знать внутренние параметры камеры (матрицу `K` и коэффициенты дисторсии, см. Билет 11).
2. **Оценка позы человека (Human Pose Estimation):**
- Задача обнаружения и локализации ключевых точек человеческого тела (суставов), таких как нос, глаза, уши, плечи, локти, запястья, бедра, колени, лодыжки и т.д.
- Может быть **2D Human Pose Estimation:** координаты ключевых точек предсказываются в пикселях на 2D-изображении.
- Может быть **3D Human Pose Estimation:** предсказываются 3D-координаты ключевых точек в мировой системе координат или относительно камеры.
- Современные методы для оценки позы человека в основном базируются на **глубоком обучении (Deep Learning)**. Популярные модели включают OpenPose, PoseNet (от Google), AlphaPose, HRNet, MoveNet и др. Эти модели обычно представляют собой сверточные нейронные сети, обученные на больших датасетах изображений людей с размеченными ключевыми точками.
Методы и Примеры:
Оценка позы жесткого объекта с помощью cv2.solvePnP / cv2.solvePnPRansac
Описание: Эти функции OpenCV решают задачу Perspective-n-Point. Они находят вектор вращения и вектор смещения, которые преобразуют набор 3D-точек объекта из его локальной системы координат в систему координат камеры так, чтобы их проекции наилучшим образом совпали с заданными 2D-точками на изображении. Требуется знание внутренних параметров камеры и как минимум 4 (для `solvePnP` без RANSAC, если точки не копланарны) или более пар 2D-3D соответствий. `solvePnPRansac` использует RANSAC для устойчивости к неверным соответствиям.
Пример кода:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 1. 3D-координаты точек объекта в его собственной системе координат (object_points)
# Например, для простого прямоугольного объекта (как маркер или лицевая сторона коробки)
# Предположим, начало координат объекта в его левом верхнем углу, ось X вправо, Y вниз.
object_points_3d = np.array([
(0.0, 0.0, 0.0), # Левый верхний
(10.0, 0.0, 0.0), # Правый верхний (ширина 10 ед.)
(10.0, 5.0, 0.0), # Правый нижний (высота 5 ед.)
(0.0, 5.0, 0.0) # Левый нижний
], dtype=np.float64)
# 2. Соответствующие 2D-координаты этих точек на изображении (image_points)
# Эти точки нужно было бы найти на реальном изображении (например, детекцией углов маркера)
# Здесь приведем фиктивные данные, имитирующие перспективную проекцию
image_points_2d = np.array([
(100, 100), # Левый верхний на изображении
(300, 120), # Правый верхний
(280, 250), # Правый нижний
(80, 220) # Левый нижний
], dtype=np.float64)
# 3. Матрица камеры (K) и коэффициенты дисторсии (dist_coeffs)
# (Предположим, они получены из калибровки камеры - см. Билет 11)
# Для примера возьмем идеальную камеру (без дисторсии) и примерные параметры
image_height, image_width = 300, 400 # Примерные размеры изображения
focal_length_guess = image_width
camera_matrix = np.array([[focal_length_guess, 0, image_width/2],
[0, focal_length_guess, image_height/2],
[0, 0, 1]], dtype=np.float64)
dist_coeffs = np.zeros((4,1)) # Нулевые коэффициенты дисторсии
# Решение PnP задачи
# flags: метод решения, например, cv2.SOLVEPNP_ITERATIVE, cv2.SOLVEPNP_EPNP, cv2.SOLVEPNP_P3P, cv2.SOLVEPNP_SQPNP
success, rotation_vector, translation_vector = cv2.solvePnP(
object_points_3d, image_points_2d, camera_matrix, dist_coeffs, flags=cv2.SOLVEPNP_ITERATIVE
)
if success:
# print("Поза объекта успешно оценена (solvePnP).")
# print("Вектор вращения (Rodrigues):\n", rotation_vector)
# print("Вектор смещения (относительно камеры):\n", translation_vector)
# Для визуализации можно спроецировать оси координат объекта на изображение
axis_length = 3.0 # Длина осей для отрисовки
object_axes_3d = np.float32([[0,0,0], [axis_length,0,0], [0,axis_length,0], [0,0,-axis_length]]).reshape(-1,3)
image_axes_points_2d, _ = cv2.projectPoints(object_axes_3d, rotation_vector, translation_vector,
camera_matrix, dist_coeffs)
# Создание изображения для демонстрации
# display_image = np.zeros((image_height, image_width, 3), dtype=np.uint8); display_image.fill(200)
# for pt in image_points_2d.astype(int):
# cv2.circle(display_image, tuple(pt), 5, (0,0,255), -1) # Красные - точки на изображении
# pt_origin = tuple(image_axes_points_2d[0].ravel().astype(int))
# pt_x_axis = tuple(image_axes_points_2d[1].ravel().astype(int))
# pt_y_axis = tuple(image_axes_points_2d[2].ravel().astype(int))
# pt_z_axis = tuple(image_axes_points_2d[3].ravel().astype(int))
# cv2.line(display_image, pt_origin, pt_x_axis, (255,0,0), 2) # X-ось синяя
# cv2.line(display_image, pt_origin, pt_y_axis, (0,255,0), 2) # Y-ось зеленая
# cv2.line(display_image, pt_origin, pt_z_axis, (0,0,255), 2) # Z-ось красная
# plt.imshow(cv2.cvtColor(display_image, cv2.COLOR_BGR2RGB))
# plt.title('Оценка позы объекта (PnP) и проекция осей')
# plt.show()
# Пример с solvePnPRansac
# success_r, rvec_r, tvec_r, inliers = cv2.solvePnPRansac(
# object_points_3d, image_points_2d, camera_matrix, dist_coeffs, iterationsCount=100, reprojectionError=8.0)
# if success_r:
# print(f"solvePnPRansac: успех, {len(inliers) if inliers is not None else 0} инлайеров")
else:
print("Оценка позы не удалась (solvePnP).")
Объяснение кода: 1. **`object_points_3d`**: NumPy массив 3D-координат известных точек на объекте в его собственной системе координат.
2. **`image_points_2d`**: NumPy массив соответствующих 2D-координат этих точек на изображении.
3. **`camera_matrix` (K)** и **`dist_coeffs` (D)**: Внутренние параметры камеры.
4. `success, rotation_vector, translation_vector = cv2.solvePnP(...)`:
- `success`: Булево значение, `True` если поза найдена.
- `rotation_vector` (`rvec`): Вектор вращения (3x1) в формате Родригеса. Направление вектора — ось вращения, длина вектора — угол вращения в радианах. Может быть преобразован в матрицу вращения 3x3 с помощью `cv2.Rodrigues(rvec)`.
- `translation_vector` (`tvec`): Вектор смещения (3x1), описывающий положение начала координат объекта в системе координат камеры.
- `flags`: Задает используемый алгоритм PnP (например, `cv2.SOLVEPNP_ITERATIVE`, `cv2.SOLVEPNP_EPNP`, `cv2.SOLVEPNP_AP3P`, `cv2.SOLVEPNP_SQPNP`). `SOLVEPNP_SQPNP` — это новый быстрый и точный алгоритм (если доступен).
5. `cv2.projectPoints(objectPoints, rvec, tvec, cameraMatrix, distCoeffs)`: Используется для проверки и визуализации. Проецирует 3D-точки (`object_axes_3d`) на плоскость изображения, используя вычисленные `rvec`, `tvec` и параметры камеры. Позволяет нарисовать, например, систему координат объекта на изображении.
6. **`cv2.solvePnPRansac`**: Имеет схожие параметры, но добавляет параметры RANSAC (например, `iterationsCount`, `reprojectionError` — максимальная ошибка перепроекции для инлайера) и возвращает также `inliers` (индексы 2D-3D пар, которые были признаны инлайерами). Это предпочтительнее, если есть неточности в `image_points_2d`.
Пример из реальной жизни: 1. **Дополненная реальность (AR):** Наложение виртуальных 3D-объектов на реальные маркеры или известные объекты (например, на коробку, книгу). Поза маркера вычисляется, и виртуальный объект рисуется с той же позой.
2. **Робототехника:** Определение положения и ориентации объектов для захвата манипулятором, навигация.
3. **3D-трекинг объектов.**
4. **Определение положения камеры относительно известного объекта (если объект неподвижен, а камера движется).**
Оценка позы человека с помощью MoveNet (Концептуально, через TensorFlow Hub / OpenCV DNN)
Описание: MoveNet — это быстрая и точная модель для оценки 2D-позы человека (17 ключевых точек: нос, глаза, уши, плечи, локти, запястья, бедра, колени, лодыжки). Она доступна через TensorFlow Hub, а также модели в формате ONNX могут быть запущены с помощью модуля OpenCV DNN. Подобные модели принимают на вход изображение и выводят координаты ключевых точек и их уверенности.
Пример кода:
# Для использования MoveNet напрямую из TensorFlow Hub требуется TensorFlow.
# OpenCV DNN может запускать ONNX-версии MoveNet или других моделей оценки позы.
# Ниже приведен концептуальный код, иллюстрирующий идею работы с такой моделью через OpenCV DNN.
# Для этого потребуется скачать .onnx файл модели (например, MoveNet SinglePose Lightning/Thunder).
import cv2
import numpy as np
import matplotlib.pyplot as plt
# Путь к ONNX модели MoveNet (ЗАМЕНИТЕ НА СВОЙ ПУТЬ)
# model_onnx_path = "path/to/your/movenet_model.onnx"
# image_path = "person_standing.jpg"
# # Параметры, специфичные для модели MoveNet (могут отличаться для разных версий)
# MOVENET_INPUT_SIZE = 192 # Lightning: 192, Thunder: 256
# KEYPOINT_NAMES = [
# "nose", "left_eye", "right_eye", "left_ear", "right_ear",
# "left_shoulder", "right_shoulder", "left_elbow", "right_elbow",
# "left_wrist", "right_wrist", "left_hip", "right_hip",
# "left_knee", "right_knee", "left_ankle", "right_ankle"
# ]
# # Соединения для отрисовки скелета (пары индексов KEYPOINT_NAMES)
# SKELETON_CONNECTIONS = [
# (0,1), (0,2), (1,3), (2,4), # Голова
# (5,6), (5,7), (7,9), (6,8), (8,10), # Руки
# (5,11), (6,12), (11,12), # Торс
# (11,13), (13,15), (12,14), (14,16) # Ноги
# ]
# KEYPOINT_CONFIDENCE_THRESHOLD = 0.3
# try:
# net = cv2.dnn.readNetFromONNX(model_onnx_path)
# except cv2.error as e:
# print(f"Ошибка загрузки ONNX модели: {e}")
# exit()
# original_image = cv2.imread(image_path)
# if original_image is None: print(f"Ошибка загрузки изображения: {image_path}"); exit()
# h_orig, w_orig = original_image.shape[:2]
# # 1. Предобработка изображения
# input_image_resized = cv2.resize(original_image, (MOVENET_INPUT_SIZE, MOVENET_INPUT_SIZE))
# # Модели MoveNet обычно ожидают uint8 или float32 тензор, нормализация не всегда нужна,
# # или специфична. Для TF Lite моделей часто [0,255] uint8.
# # Для ONNX может быть float32 и нормализация -1 до 1 или 0 до 1.
# # В данном примере предполагаем, что модель ожидает uint8 [0,255] и сама делает нормализацию.
# # blob = cv2.dnn.blobFromImage(input_image_resized, size=(MOVENET_INPUT_SIZE, MOVENET_INPUT_SIZE), swapRB=False, ddepth=cv2.CV_8U)
# # Для моделей, ожидающих float и нормализацию:
# blob = cv2.dnn.blobFromImage(input_image_resized, scalefactor=1.0/255.0,
# size=(MOVENET_INPUT_SIZE, MOVENET_INPUT_SIZE),
# mean=(0,0,0), swapRB=True, crop=False) # Пример для float
# net.setInput(blob)
# # 2. Прямой проход
# # Выход MoveNet: (1, 1, 17, 3) -> (batch, num_instances, num_keypoints, [y, x, score])
# # Координаты y,x нормированы к [0,1] относительно входного размера (MOVENET_INPUT_SIZE)
# output_data = net.forward()
# # 3. Извлечение ключевых точек и их уверенностей
# keypoints_with_scores = output_data[0, 0, :, :]
# # 4. Визуализация
# image_with_pose = original_image.copy()
# for i in range(len(KEYPOINT_NAMES)):
# y_norm, x_norm, score = keypoints_with_scores[i]
# if score > KEYPOINT_CONFIDENCE_THRESHOLD:
# # Перевод нормированных координат в координаты исходного изображения
# x_coord = int(x_norm * w_orig)
# y_coord = int(y_norm * h_orig)
# cv2.circle(image_with_pose, (x_coord, y_coord), 5, (0, 255, 0), -1) # Зеленые точки
# # cv2.putText(image_with_pose, f"{KEYPOINT_NAMES[i]} ({score:.2f})", (x_coord+7, y_coord-7),
# # cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0,0,255), 1)
# for (idx1, idx2) in SKELETON_CONNECTIONS:
# y1, x1, score1 = keypoints_with_scores[idx1]
# y2, x2, score2 = keypoints_with_scores[idx2]
# if score1 > KEYPOINT_CONFIDENCE_THRESHOLD and score2 > KEYPOINT_CONFIDENCE_THRESHOLD:
# pt1 = (int(x1 * w_orig), int(y1 * h_orig))
# pt2 = (int(x2 * w_orig), int(y2 * h_orig))
# cv2.line(image_with_pose, pt1, pt2, (255, 0, 0), 2) # Синие линии скелета
# plt.figure(figsize=(8, 10))
# plt.imshow(cv2.cvtColor(image_with_pose, cv2.COLOR_BGR2RGB))
# plt.title('Оценка позы человека (MoveNet через OpenCV DNN)')
# plt.show()
print("Это концептуальный код для оценки позы человека. Требует обученной ONNX модели.")
Объяснение кода: 1. **Загрузка модели (`cv2.dnn.readNetFromONNX`):** Загружается ONNX-файл с предварительно обученной моделью оценки позы (например, MoveNet).
2. **Предобработка изображения:**
- Изображение изменяется до размера, который ожидает модель (например, `192x192` для MoveNet Lightning).
- Создается `blob` с помощью `cv2.dnn.blobFromImage`. Параметры `scalefactor`, `mean`, `swapRB`, `ddepth` должны соответствовать требованиям конкретной модели (они указываются при конвертации модели в ONNX или в ее документации). Некоторые модели могут ожидать вход `uint8` без нормализации, другие `float32` с нормализацией к [0,1] или [-1,1].
3. **Прямой проход (`net.setInput(blob)`, `net.forward()`):** Получаем выходные данные модели.
4. **Извлечение ключевых точек:** Выход `output_data` для MoveNet обычно имеет форму `(1, 1, 17, 3)`, где 17 — количество ключевых точек, а последние 3 значения для каждой точки — это `(y_norm, x_norm, score)`. Координаты `y_norm`, `x_norm` нормированы к диапазону [0,1] относительно входного размера изображения для сети.
5. **Визуализация:**
- Для каждой ключевой точки, если ее уверенность `score` выше порога (`KEYPOINT_CONFIDENCE_THRESHOLD`), ее нормированные координаты `(y_norm, x_norm)` переводятся в координаты исходного изображения (`x_coord = x_norm * w_orig`, `y_coord = y_norm * h_orig`).
- Точки рисуются на изображении (например, кружками).
- Линии скелета рисуются между связанными точками (согласно `SKELETON_CONNECTIONS`), если обе точки имеют достаточную уверенность.
Пример из реальной жизни: 1. **Фитнес-приложения:** Подсчет повторений упражнений (отжимания, приседания), анализ правильности выполнения, виртуальный тренер.
2. **Анализ спортивных движений:** Изучение техники бега, плавания, игры в гольф и т.д.
3. **Управление аватарами в играх и VR/AR.**
4. **Системы мониторинга безопасности:** Детекция падений, необычного поведения.
5. **Взаимодействие человек-компьютер:** Управление интерфейсами с помощью жестов тела.
6. **Анимация персонажей.**