Время прочтения: 5 мин.
Допустим, вы фотографируете документ на какой-либо девайс, не поддерживающий функцию сканирования документов, ну или вам прислали фото документа. Очевидно, что получившееся изображение совсем не похоже на скан-копию: документ сфотографирован криво или на фото присутствуют посторонние предметы, например, часть стола. Преобразовать такую фотографию в аккуратную скан-копию поможет наш алгоритм.
Его работу можно глобально разложить на два этапа – обрезание документа на фото, т.е. избавление от всего лишнего и его «бинаризация» – преобразование пикселей к двум возможным значениям: «0» – пиксель черный, «255» – пиксель белый.
Сначала импортируем все необходимые библиотеки:
import numpy as np
import cv2 as cv
from PIL import Image
from PIL import ImageEnhance
Далее откроем изображение, которое мы хотим обработать с помощью следующей строки кода:
img = cv.imread('doc.jpg')
Чтобы код сработал поместите изображение в каталог, где находится файл с вашим кодом или укажите полностью путь до файла с изображением.
Запишем в отдельную переменную изображение в первоначальном виде, чтобы не потерять его при преобразованиях, с помощью метода copy():
first_img = img.copy()
Обрезание документа на изображении
Чтобы наш алгоритм правильно определял границы документа необходимо произвести некоторые преобразования с изображения:
- Для улучшения точности алгоритма нужно увеличить контраст изображения. В OpenCV нет встроенного метода для этой операции, но в библиотеке PIL – есть, именно для этого мы ее и импортировали.
pil_image_1 = Image.fromarray(img)
img = ImageEnhance.Contrast(pil_image_1).enhance(1.6)
При считывании изображения из файла через OpenCV он записывается в формате numpy.array, следовательно, к нему можно применять встроенные методы библиотеки numpy. Но библиотека PIL с этим типом данных не работает, поэтому необходимо сначала перевести массив numpy в тип данных класса Image. К преобразованному изображению уже применяем усилитель Contrast и вызываем метод enhance, в который в качестве параметра передается процент увеличения контраста, где 1.6 = 160%. Для дальнейшей работы с изображением преобразуем его обратно в массив:
img = np.array(img)
2. Если изображение имеет высокое разрешение (намного больше, чем 1280 пикселей), необходимо его уменьшить. Это делается для того, чтобы алгоритм «сконцентрировался» на крупном объекте (документ), и игнорировал мелкие детали, которые могут привести к неточности работы.
img = cv.resize(img, (1024, 1280))
В этом примере изображение уменьшено примерно в три раза. Размер вашего изображения можно проверить командой, которая выведет размеры в формате (w, h, n):
print(first_img.shape)
Где w – ширина, h – высота, n – количество каналов. Для цветного изображения n =3. Для того, чтобы после всех преобразований применить результат алгоритма к первоначальному изображению, необходимо вычислить коэффициенты масштабирования изображений:
x_ratio = first_img.shape[0] / img.shape[0]
y_ratio = first_img.shape[1] / img.shape[1]
Представим изображение в градациях серого, размоем его фильтром Гаусса и проведем бинаризацию изображения для того, чтобы отделить объект от фона:
gray = cv.cvtColor(img,cv.COLOR_BGR2GRAY)
blurred = cv.GaussianBlur(gray, (7, 7), 0)
thresh = cv.threshold(blurred, 135, 255, cv.THRESH_BINARY)[1]
(7,7) – ядро фильтра, отвечает за то, насколько сильно нужно размыть изображение. Параметры 135, 255 – пороговые значения, показывающие, что пиксели со значением яркости в интервале (135; 255) отнесутся к белому цвету, а в интервале (0; 135) – к черному.
Этапы предобработки представлены на следующих изображениях:
Чтобы обрезать наш документ, необходимо определить его границы. Для этих целей используем метод библиотеки OpenCV — goodFeaturesToTrack, который находит углы на изображении. Он принимает на вход массив с изображением, максимальное число детектируемых углов, уровень качества углов и минимальную дистанцию между углами, а на выходе формирует массив с координатами центров углов.
corners = cv.goodFeaturesToTrack(thresh,20,0.05,200)
Элементы массива получились с плавающей точкой, поэтому дабы избежать ошибок в будущем, приведем их все к целочисленному формату.
corners = np.int0(corners)
Можно проверить верно ли были найдены углы с помощью следующего фрагмента кода:
for i in corners:
x,y = i.ravel()
cv.circle(img,(x,y),20,255,-1)
cv.imshow(‘corners’, img)
cv.waitKey(0)
Результат поиска всех углов на изображении:
Чтобы обрезать документ на фото, требуется четыре координаты его углов, а у нас на выходе алгоритма их больше. Поэтому нужно отобрать, например, верхний левый угол и правый нижний. Происходит это с помощью поиска наибольшего и наименьшего элемента в массиве.
left_x = 10000
left_y = 10000
right_x = 0
right_y = 0
for corner in corners:
if corner[0][0] <= left_x:
left_x = corner[0][0]
elif corner[0][0] >= right_x:
right_x = corner[0][0]
if corner[0][1] <= left_y:
left_y = corner[0][1]
elif corner[0][1] >= right_y:
right_y = corner[0][1]
Далее следует перейти к первоначальной размерности. Для этого умножаем найденные координаты на коэффициенты масштабирования изображений, которые мы вычисляли, при этом переводя все к целочисленному типу:
left_x = int(left_x * x_ratio)
left_y = int(left_y * y_ratio)
right_x = int(right_x * x_ratio)
right_y = int(right_y * y_ratio)
Наложение на изображение эффекта скана и отображение результатов
Обрезаем первоначальное изображение по вычисленным координатам и заново бинаризируем его, с целью получения эффекта скана.
crop = first_img[left_y:right_y, left_x:right_x]
thresh2 = cv.threshold(crop, 50, 255, cv.THRESH_BINARY)[1]
cv.imshow('1', thresh2)
cv.waitKey(0)
Визуализируем полученное изображение, и вуаля!
В данном алгоритме присутствует много настраиваемых параметров, так что вы сможете перенастроить их под конкретные обстоятельства – освещение, фон и т.д.
Алгоритм легко обобщается для обработки нескольких фотографий. Для этого достаточно обернуть код алгоритма в цикл по названию фотографии, например, так:
import os
for img_name in [ filename for filename in os.listdir(os.getcwd()) if filename.endswith('.jpg')]:
В метод cv.imread() теперь нужно передавать не название одной фотографии, а переменную цикла:
img = cv.imread(img_name)
При такой модификации, вместо набора плохо сфотографированных документов, мы получим набор выровненных и приведенных к единому размеру чёрно-белых скан-копий.
Надеюсь, статья была полезна!