2025, Dec 17 00:02

Как найти угол и положение объекта в OpenCV: контуры, выпуклая оболочка и minAreaRect

Почему matchTemplate в OpenCV ломается при повороте и как получить угол, центр объекта с помощью бинаризации, контуров, выпуклой оболочки и minAreaRect.

Обнаружение повернутого шаблона через cv2.matchTemplate быстро упирается в пределы: метод учитывает только сдвиг. Проще говоря, он работает, пока шаблон не повернут относительно сцены, и ломается, как только объект появляется под углом. Если вам нужны и позиция, и угол объекта на большом изображении, переходите от корреляции к геометрии. Ниже — компактный, воспроизводимый путь, который извлекает из изображения угол поворота, центр и размеры повернутого ограничивающего прямоугольника.

Минимальная демонстрация ограничения

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

#!/usr/bin/env python3

import cv2
import numpy as np

scene = cv2.imread('ace_image.png', cv2.IMREAD_COLOR)
tpl = cv2.imread('template.png', cv2.IMREAD_COLOR)

# наивное сопоставление: только смещение, инвариантности к повороту нет
corr = cv2.matchTemplate(scene, tpl, cv2.TM_CCOEFF_NORMED)
_, maxval, _, maxloc = cv2.minMaxLoc(corr)

# рисуем область лучшего совпадения так, будто ориентация фиксирована
h, w = tpl.shape[:2]
scene_vis = scene.copy()
cv2.rectangle(scene_vis, maxloc, (maxloc[0] + w, maxloc[1] + h), (0, 255, 0), 2)

cv2.imshow('matchTemplate (translation only)', scene_vis)
cv2.waitKey(0)

Что именно идет не так

cv2.matchTemplate коррелирует шаблон фиксированной ориентации по сетке изображения. Он оценивает, где шаблон смещается в сцене; по поворотам поиск не ведется. Поэтому он «вполне неплох» без поворота и дает сбой, как только объект наклоняется. Если цель — получить угол и положение объекта, лучше работать с силуэтом и его геометрией, а не с корреляцией интенсивностей.

Решение: конвейер «сначала форма» с контурами, выпуклой оболочкой и minAreaRect

Метод ниже извлекает передний план, объединяет релевантные компоненты в единую маску формы и вычисляет повернутый прямоугольник минимальной площади по этой форме. Это сразу дает центроид, ширину/высоту ограничивающего прямоугольника и угол поворота. Шаги: чтение и преобразование в оттенки серого, пороговая обработка с инверсией, чтобы объект был белым на черном, оставляем внешние контуры выше малого порога площади, рисуем их заливкой, чтобы объединить в одну маску, вычисляем выпуклую оболочку по пикселям переднего плана и запускаем cv2.minAreaRect на контуре, полученном из оболочки. Результат включает угол, центр и размеры. В финале — визуализация наложением повернутого прямоугольника.

#!/usr/bin/env python3

import cv2
import numpy as np

# загружаем входное изображение в цвете и переводим в градации серого
source_bgr = cv2.imread('ace_image.png')
mono = cv2.cvtColor(source_bgr, cv2.COLOR_BGR2GRAY)

# бинаризуем и инвертируем, чтобы цель была белой на черном
bin_inv = cv2.threshold(mono, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]

# извлекаем внешние контуры
found = cv2.findContours(bin_inv, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
outline_list = found[0] if len(found) == 2 else found[1]

# отфильтровываем мелкие фрагменты и рисуем единую залитую маску
area_floor = 100
fill_mask = np.zeros_like(bin_inv)
for loop in outline_list:
    a = cv2.contourArea(loop)
    if a > area_floor:
        cv2.drawContours(fill_mask, [loop], 0, 255, -1)

# собираем координаты всех белых пикселей и считаем выпуклую оболочку
white_pts = np.column_stack(np.where(fill_mask.transpose() > 0))
outer_hull = cv2.convexHull(white_pts)

# растеризуем выпуклую оболочку, чтобы получить единый объединенный контур
hull_mask = np.zeros_like(bin_inv)
cv2.fillPoly(hull_mask, [outer_hull], 255)

# берем самый крупный контур и его повернутый прямоугольник минимальной площади
found2 = cv2.findContours(hull_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
merged_contours = found2[0] if len(found2) == 2 else found2[1]
blob = max(merged_contours, key=cv2.contourArea)
rb = cv2.minAreaRect(blob)
(ctr), (w_box, h_box), tilt = rb
print('width x height:', w_box, 'x', h_box)
print('center', ctr)
print('angle', tilt)

# рисуем повернутый прямоугольник на исходном изображении для наглядности
rb_pts = np.int32(cv2.boxPoints(rb))
vis_rb = source_bgr.copy()
cv2.drawContours(vis_rb, [rb_pts], 0, (0, 255, 0), 2)

# опционально: сохранить промежуточные результаты
cv2.imwrite('combined_mask.jpg', fill_mask)
cv2.imwrite('convex_hull_mask.jpg', hull_mask)
cv2.imwrite('rotated_rectangle_overlay.jpg', vis_rb)

# опционально: показать окна
cv2.imshow('combined mask', fill_mask)
cv2.imshow('convex hull mask', hull_mask)
cv2.imshow('rotated rectangle', vis_rb)
cv2.waitKey(0)

Код выводит ключевые измерения. Для референсного изображения консоль показывает:

width x height: 23.33452033996582 x 45.96194076538086
center (129.5, 95.0)
angle 45.0

Почему это работает

Как только передний план изолирован, выпуклая оболочка стягивает несколько разрозненных частей объекта в один плотный контур. Прямоугольник минимальной площади, подогнанный к этой форме, не зависит от внутренней структуры; он опирается только на геометрию силуэта. В результате напрямую получаем угол поворота вместе с центром объекта и ориентированным ограничивающим прямоугольником — именно то, что требуется в этой задаче.

Когда стоит выбирать этот подход

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

Выводы

Не полагайтесь на cv2.matchTemplate, когда важен поворот: он рассчитан на чистый перенос. Преобразуйте задачу в анализ формы: бинаризация, объединение внешних контуров, выпуклая оболочка и подгонка повернутого прямоугольника через cv2.minAreaRect. Вы получите угол, центр и размеры за один проход и наглядное наложение для проверки. Следите за порогом площади, чтобы подавлять мелкие соринки, и проверьте полярность пороговой обработки — перед началом убедитесь, что объект белый на черном. Часто этого достаточно, чтобы сделать поворот полноценным сигналом, а не причиной отказа.