Skip to content

Instantly share code, notes, and snippets.

@AlekseyDurachenko
Last active January 15, 2017 09:08
Show Gist options
  • Save AlekseyDurachenko/a3194313f145321e03ada9b1c510dfcd to your computer and use it in GitHub Desktop.
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)
#!/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