Выше крыши

Выше крыши

Ilya Osokin

Постановка задачи

Построим алгоритм приведения листка бумаги к "виду сверху", как будто в момент съёмки камера находилась прямо над ним.

Такое преобразование может быть полезно в частности в задачах распознавания (дорожных знаков, штрихкодов) в качестве предобработки, уменьшающей искажения.

Пример исходного изображения

Решение этой задачи было подробно рассмотрено в третьем семинаре открытого курса по компьютерному зрению в робототехнике, который проводит наша лаборатория в МФТИ.

Решение

Преобразование листка к виду сверху начинается с нахождения его маски. За этим следует выделение углов листка, которые используются для построения матрицы перспективного преобразования. Наконец, это преобразование отображает некоторые пиксели исходного изображения в пиксели результата.

Используя цветовой фильтр, построим одноканальное изображение, в котором пиксели, принадлежащие объекту (в данном случае листку) обозначены одним цветом, а пиксели фона другим.

low_th = (200, 200, 200)

high_th = (255, 255, 255)

mask = cv2.inRange (img, low_th, high_th)

Маска, полученная с помощью выделения по цвету

Затем найдём границу маски в виде так называемого контура, то есть последовательности точек. Среди всех контуров, в том числе и относящихся к буквам, нас интересует единственный - внешний. За это отвечает второй параметр функции, который равен cv2.RETR_EXTERNAL.

contours, hierarchy = cv2.findContours (mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

cnt = contours [0]

Построим выпуклую оболочку контура, чтобы исключить увеличение длины контура за счёт шума на границах. В противном случае она может сильно расходиться с видимой.

approx = cv2.convexHull (cnt)

Найдём длину получившегося контура и домножим её на 0.05.

epsilon = 0.05 * cv2.arcLength (approx, True)

Таким будет максимальное расстояние между исходным контуром и его приближением:

approx = cv2.approxPolyDP (approx, epsilon, True)

Это расстояние имеет смысл задавать не в абсолютных величинах, а в виде длины контура, умноженной на небольшую константу, чтобы метод работал для листков произвольного размера.

Нарисуем то, что получилось.

contour_marked = cv2.drawContours (img_rgb, approx, -1, (0, 255, 0), 33)

Выделенные углы

Листок в мире прямоугольный, а на изображении он выглядит как четырёхугольник, то есть фигура из четырёх отрезков. Все точки контура, лежащие близко к сторонам, оказались отброшены при построении его приближения, а углы сохранились, поскольку удаление любого из них слишком сильно отдалило бы результат от исходного контура.

После того, как получены координаты четырёх углов, их нужно упорядочить, то есть понять, какой из них левый верхний, правый верхний и далее. Это можно сделать, заметив, что у левого верхнего угла сумма координат минимальна среди всех (поскольку оси направлены вправо и вниз), у правого нижнего максимальна. После этого найденные точки нужно исключить из рассмотрения и упорядочить две оставшиеся. У левой нижней разность координат x и y меньше, чем у правой верхней.

rect = np.zeros((4, 2), dtype="float32")

s = pts.sum(axis=1)

rect[0] = pts[np.argmin(s)]

rect[2] = pts[np.argmax(s)]

pts_sort = [r for i, r in enumerate(pts) if i != np.argmin(s) and i != np.argmax(s)]

diff = np.diff(pts, axis=1)

rect[1] = pts[np.argmin(diff)]

rect[3] = pts[np.argmax(diff)]

Выпрямление изображения

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

(tl, tr, br, bl) = rect

widthA = np.sqrt(((br[0] - bl[0]) ** 2) + ((br[1] - bl[1]) ** 2))

widthB = np.sqrt(((tr[0] - tl[0]) ** 2) + ((tr[1] - tl[1]) ** 2))

maxWidth = max(int(widthA), int(widthB))

heightA = np.sqrt(((tr[0] - br[0]) ** 2) + ((tr[1] - br[1]) ** 2))

heightB = np.sqrt(((tl[0] - bl[0]) ** 2) + ((tl[1] - bl[1]) ** 2))

maxHeight = max(int(heightA), int(heightB))

Создадим массив для целевого изображения.   

dst = np.array([[0, 0],

     [maxHeight - 1, 0],

     [maxHeight - 1, maxWidth - 1],

     [0, maxWidth - 1]], dtype="float32")

Наконец, найдём матрицу перспективного преобразования и применим его к исходному изображению.

M = cv2.getPerspectiveTransform(rect, dst)

warped = cv2.warpPerspective(image, M, (maxHeight, maxWidth))

Результат работы. Послание относится также и к тем, кто дочитал статью :)

Ноутбук с полным кодом (данные тоже есть в репозитории), семинар, весь курс в виде плейлиста, лаборатория, инстаграм, телеграм-канал.

Report Page