Last active
January 15, 2017 09:08
-
-
Save AlekseyDurachenko/a3194313f145321e03ada9b1c510dfcd to your computer and use it in GitHub Desktop.
Скрипт для определения контуров отсканированных фотографий (http://alekseydurachenko.github.io/2017/01/13/scanned_photos_contours_with_opencv.html)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/python2 | |
# -*- coding: utf-8 -*- | |
# | |
# Copyright 2017, Durachenko Aleksey V. <durachenko.aleksey@gmail.com> | |
# | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program. If not, see <http://www.gnu.org/licenses/>. | |
import numpy as np | |
import argparse | |
import cv2 | |
import os | |
# исходное изображение очень большое: приблизительно 10x14 тысяч пикселей. | |
# во-первых, выделение контуров на таком большом изображении займет много | |
# времени, а во-вторых на нем будет слишком много избыточной информации. | |
# необходимы же только контуры больших фигур - фотографий, поэтому изображение | |
# можно смело уменьшать. | |
image_scale_factor = 0.1 # 1.0 - 100% изображения | |
# на рис.3 видно, что OpenCV решил выделить контур альбомного листа, что | |
# в нашем случае совершенно неприемлемо. для исключения таких ситуаций | |
# вводится ограничение на максимальный размер ширины и высоты контура фотографии. | |
contour_maximum_width_factor = 0.8 # 1.0 - 100% изображения | |
contour_maximum_height_factor = 0.8 # 1.0 - 100% изображения | |
# после работы алгоритма слияния контуров(о нем пойдет речь ниже) | |
# могут появиться области, которые ошибочно приняты за контуры фотографий. | |
# на рис.5 хорошо видно такие области. чтобы их исключить вводится | |
# правило, по которому контур фотографии не может быть меньше определенного | |
# размера | |
contour_minimum_width_factor = 0.1 # 1.0 - 100% изображения | |
contour_minimum_height_factor = 0.1 # 1.0 - 100% изображения | |
# найденные контуры фотографий будет расширены на указанное кол-во пикселей | |
# во все стороны | |
contour_additional_pixels = 50 | |
# цвет контуров на диагностических изображениях | |
contour_color = (0, 255, 0,) | |
# ширина линий контуров на диагностических изображениях | |
contour_width = 2 | |
# параметры фильтрации и детектора контуров (примера А) | |
#gaussian_blur_size = (7, 7) | |
#canny_threshold_1 = 15 | |
#canny_threshold_2 = 140 | |
# параметры фильтрации и детектора контуров (примера Б) | |
gaussian_blur_size = (3, 3) | |
canny_threshold_1 = 5 | |
canny_threshold_2 = 100 | |
# прямоугольник, описывающий контур фотографии | |
class MyRect: | |
x1 = 0 | |
y1 = 0 | |
x2 = 0 | |
y2 = 0 | |
def __init__(self, x1=0, y1=0, x2=0, y2=0): | |
self.x1 = x1 | |
self.y1 = y1 | |
self.x2 = x2 | |
self.y2 = y2 | |
def __str__(self): | |
return "((%d, %d), (%d, %d))" % (self.x1, self.y1, self.x2, self.y2,) | |
def __repr__(self): | |
return self.__str__() | |
# проверка пересечения двух прямоугольников | |
def overlaps(self, rect): | |
if rect.x1 <= self.x2 and rect.x2 >= self.x1 and rect.y1 <= self.y2 and rect.y2 >= self.y1: | |
return True | |
return False | |
# слияние двух прямоугольников. итоговый прямоугольник будет полностью | |
# вписывать два исходных | |
def join(self, rect): | |
result = MyRect() | |
result.x1 = min(rect.x1, self.x1) | |
result.y1 = min(rect.y1, self.y1) | |
result.x2 = max(rect.x2, self.x2) | |
result.y2 = max(rect.y2, self.y2) | |
return result | |
# алгоритм слияния контуров найденных OpenCV в итоговые контуры фотографий. | |
# его суть заключается в том, что для каждого контура строится прямоугольник | |
# полностью вписывающий его, а также слияние всех пересекающихся прямоугольников. | |
# в итоге должны получиться прямоугольники, описывающие контуры фотографий. | |
# так же алгоритм учитывает особый случай: слишком большие контуры | |
# (вероятно, контуры листа альбома) игнорируются | |
# примеры слияния: | |
# контуры рис.3 в прямоугольники рис.5, | |
# контуры рис.4 в прямоугольники рис.6 | |
def merge_contours(contours): | |
rectangles = [] | |
for contour in contours: | |
if len(contour) > 2: | |
x1 = x2 = contour.tolist()[0][0][0] | |
y1 = y2 = contour.tolist()[0][0][1] | |
for item in contour.tolist(): | |
x = item[0][0] | |
y = item[0][1] | |
x1 = min(x, x1) | |
y1 = min(y, y1) | |
x2 = max(x, x2) | |
y2 = max(y, y2) | |
if ((x2 - x1) / image_scale_factor > image_width * contour_maximum_width_factor | |
or (y2 - y1) / image_scale_factor > image_height * contour_maximum_height_factor): | |
continue; | |
# add rect to rectangles and compact them | |
rect = MyRect(x1, y1, x2, y2) | |
while True: | |
new_rect_arr = [] | |
found = False | |
for tmp_rect in rectangles: | |
if rect.overlaps(tmp_rect): | |
rect = rect.join(tmp_rect) | |
found = True | |
else: | |
new_rect_arr.append(tmp_rect) | |
rectangles = new_rect_arr | |
if not found: | |
break | |
rectangles.append(rect) | |
return rectangles | |
# сохранение изображения с выделенными прямоугольниками фотографий | |
def write_image_with_rects(filename, image, rectangles): | |
contours = [] | |
for rect in rectangles: | |
contours.append(np.array([[rect.x1, rect.y1], [rect.x1, rect.y2], [rect.x2, rect.y2], [rect.x2, rect.y1]])) | |
cv2.drawContours(image, contours, -1, contour_color, contour_width) | |
cv2.imwrite(filename, image) | |
# сохранение изображений с выделенными контурами OpenCV | |
def write_image_with_conts(filename, image, contours): | |
cv2.drawContours(image, contours, -1, contour_color, contour_width) | |
cv2.imwrite(filename, image) | |
# чтение аргументов командной строки | |
appargs = argparse.ArgumentParser() | |
appargs.add_argument("-i", "--image", required=True, help="path to the image") | |
args = vars(appargs.parse_args()) | |
# имя исходного файла | |
image_filename = args["image"] | |
# базовое имя файла без расширения | |
image_basefilename = os.path.splitext(os.path.basename(image_filename))[0] | |
# имя файла с отмеченными итоговыми контурами фотографий | |
image_rects_filename = image_basefilename + "_rects.tiff" | |
# имя файла с отмеченными контурами найденными OpenCV | |
image_conts_filename = image_basefilename + "_conts.tiff" | |
# чтение исходного изображения с диска | |
image = cv2.imread(image_filename) | |
# геометрия исходного изображения | |
image_height, image_width, image_channels = image.shape | |
# изменение размера изображения | |
image = cv2.resize(image, (0, 0), fx=image_scale_factor, fy=image_scale_factor) | |
# перевод изображения в Ч\Б | |
image_prepared = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
# размывание, чтобы скрыть мелкие детали (иначе они будут ошибочно приняты за конутры) | |
image_prepared = cv2.GaussianBlur(image_prepared, gaussian_blur_size, 0) | |
# поиск контуров на изображении | |
image_prepared = cv2.Canny(image_prepared, canny_threshold_1, canny_threshold_2) | |
(contours, _) = cv2.findContours(image_prepared, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) | |
# слияние найденных контуров в итоговые контуры фотографий | |
rectangles = merge_contours(contours) | |
# печать кол-ва найденных контуров фотографий на экран (включая ложные) | |
print("# %s has %s images" % (image_filename, len(rectangles),)) | |
# запись диагностических изображений с контурами на диск | |
write_image_with_conts(image_conts_filename, image.copy(), contours) | |
write_image_with_rects(image_rects_filename, image.copy(), rectangles) | |
# вывод на экран команд для вырезания фотографий по координатам их контуров | |
n = 1 | |
for rect in rectangles: | |
x1 = rect.x1 / image_scale_factor | |
x2 = rect.x2 / image_scale_factor | |
y1 = rect.y1 / image_scale_factor | |
y2 = rect.y2 / image_scale_factor | |
# слишком маленькие контуры фотографий игнорируются (считаются ложными) | |
if (x2 - x1 >= image_width * contour_minimum_width_factor | |
and y2 - y1 >= image_height * contour_minimum_height_factor): | |
x1 -= contour_additional_pixels | |
x2 += contour_additional_pixels | |
y1 -= contour_additional_pixels | |
y2 += contour_additional_pixels | |
if x1 < 0: | |
x1 = 0 | |
if y1 < 0: | |
y1 = 0 | |
if x2 >= image_width: | |
x2 = image_width - 1 | |
if y2 >= image_width: | |
y2 = image_height - 1 | |
w = x2 - x1 | |
h = y2 - y1 | |
print("convert %s -crop %dx%d+%d+%d %s_%d.%s" % (image_filename, w, h, x1, y1, image_basefilename, n, "ppm",)) | |
n += 1 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment