OpenCVMiddleCoding

Что такое аффинное преобразование vs перспективное преобразование в OpenCV?

Аффинное преобразование сохраняет параллельность линий (поворот, масштаб, сдвиг) — матрица 2×3, 3 опорные точки. Перспективное искажает параллельность (проекция камеры) — матрица 3×3, 4 точки. warpAffine vs warpPerspective.

Геометрическая модель

Аффинное преобразование — линейная карта + трансляция. Оно сохраняет параллельность прямых, отношение площадей и коллинеарность точек. Формально: dst = M · [x, y, 1]ᵀ, где M — матрица 2×3. Для вычисления достаточно трёх соответствующих пар точек.

Перспективное (гомографическое) преобразование описывает проекцию плоскости на другую плоскость через точку наблюдения. Параллельные линии сходятся в «точке схода». Матрица — 3×3 (гомография), нормируется на w: [x', y', w'] = H · [x, y, 1]ᵀ, итоговые координаты (x'/w', y'/w'). Требует четыре пары точек.

API в OpenCV

import cv2 as cv
import numpy as np

img = cv.imread("document.jpg")
if img is None:
    raise FileNotFoundError("document.jpg not found")

h, w = img.shape[:2]

# --- Аффинное ---
# Три исходные точки → три целевые точки
src_affine = np.float32([[50, 50], [200, 50], [50, 200]])
dst_affine = np.float32([[10, 80], [210, 20], [20, 230]])
M_affine = cv.getAffineTransform(src_affine, dst_affine)
warped_affine = cv.warpAffine(img, M_affine, (w, h))
# M_affine.shape == (2, 3)
print("Affine M shape:", M_affine.shape)  # (2, 3)

# --- Перспективное ---
# Четыре угла листа на фото → прямоугольник
src_persp = np.float32([[120, 60], [480, 40], [500, 380], [100, 400]])
dst_persp = np.float32([[0, 0],   [400, 0],  [400, 300], [0, 300]])
M_persp = cv.getPerspectiveTransform(src_persp, dst_persp)
warped_persp = cv.warpPerspective(img, M_persp, (400, 300))
# M_persp.shape == (3, 3)
print("Perspective M shape:", M_persp.shape)  # (3, 3)

# Обратное перспективное преобразование (unwarping)
M_inv = cv.getPerspectiveTransform(dst_persp, src_persp)
unwarped = cv.warpPerspective(warped_persp, M_inv, (w, h))

# Если точек много — используем findHomography с RANSAC
pts_src = np.float32([[120,60],[480,40],[500,380],[100,400],[300,200]])
pts_dst = np.float32([[0,0],[400,0],[400,300],[0,300],[200,150]])
H, mask = cv.findHomography(pts_src, pts_dst, cv.RANSAC, 5.0)
print("Inliers:", mask.sum())  # число точек без выбросов

Когда что выбирать

Аффинное подходит, если камера смотрит почти перпендикулярно, или нужно выровнять изображение после небольшого поворота/масштабирования (аугментация датасета, выравнивание сканов). Параллельность сохраняется — удобно для текста.

Перспективное нужно при съёмке документа под углом, выравнивании дорожных знаков или склейке панорам. Исправляет «трапецию» — классический use-case для OCR-пайплайнов и мобильных сканеров документов.

Для больших облаков точек с выбросами — cv.findHomography(..., cv.RANSAC) автоматически отбрасывает аномалии.

Dtype и shape

Оба warpAffine и warpPerspective возвращают массив той же формы (dsize[1], dsize[0], C) и того же dtype, что и входное изображение. Входной uint8 даёт выходной uint8; float32float32. По умолчанию незаполненные пиксели — нули (чёрный); можно передать borderMode и borderValue.

Подводные камни

  • Перепутать порядок точек в getPerspectiveTransform — источник и цель должны идти в одинаковом порядке (например, по часовой стрелке). Иначе получается «бабочка».
  • Аффинное вместо перспективного для фото документа под углом — объект будет выглядеть искажённым, параллельные края не совпадут.
  • Целевой размер dsize=(w, h) в OpenCV — это (width, height), а не (rows, cols). Перепутать легко, особенно если привык к NumPy.
  • Точность точек: getAffineTransform и getPerspectiveTransform требуют np.float32, не float64 — иначе cv2.error.
  • Нет RANSAC в getPerspectiveTransform: если точки получены автоматически (детектор углов, feature matching), используйте findHomography с RANSAC или LMEDS.
  • Интерполяция по умолчаниюINTER_LINEAR. Для бинарных масок используйте INTER_NEAREST, иначе граница размоется.
  • Обратное преобразование: warpPerspective применяет обратный маппинг внутри, но если вам нужна явно обратная матрица — вызывайте np.linalg.inv(H) или меняйте местами src/dst в getPerspectiveTransform.
  • Накопленная ошибка при многократном применении аффинных преобразований: лучше скомпоновать матрицы через умножение, а не применять каждую по очереди.

Common mistakes

  • Объяснять affine vs perspective только синтаксисом без shape, dtype, состояния или режима выполнения.
  • Игнорировать leakage, воспроизводимость, пустые входы и скрытые копии данных.
  • Не проверять production-симптомы: latency, память, ретраи, дрейф качества и несовпадение версий.

What the interviewer is testing

  • Может ли связать affine vs perspective с реальным контрактом входов и выходов.
  • Упоминает ли тесты, метрики, reproducibility и диагностику ошибок.
  • Видит ли различие между demo-кодом в ноутбуке и production-пайплайном.

Sources

Related topics