-
-
Save tjw/cc657ba84b5caec78fae to your computer and use it in GitHub Desktop.
Python OpenCV program to extract ticket stub images from photographs, via automatic perspective correction for quadrilateral objects.
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/local/bin/python | |
# coding: utf-8 | |
import cv2 | |
import sys | |
import numpy | |
from matplotlib import pyplot as plt | |
from scipy.spatial import distance | |
""" | |
OpenCV program to extract ticket stub images from photographs, | |
via automatic perspective correction for quadrilateral objects. | |
Intended for use prior to running through OCR. | |
Developed for the website http://stub.town by Mark Boszko | |
Based in large part on Python port of ScannerLite | |
https://gist.github.com/scturtle/9052852 | |
original C++ | |
https://github.com/daisygao/ScannerLite | |
Also incorporates ideas from: | |
http://opencv-code.com/tutorials/automatic-perspective-correction-for-quadrilateral-objects/ | |
http://www.pyimagesearch.com/2015/04/06/zero-parameter-automatic-canny-edge-detection-with-python-and-opencv/ | |
""" | |
class Line: | |
""" | |
A line object | |
""" | |
def __init__(self, l): | |
self.point = l | |
x1, y1, x2, y2 = l | |
self.c_x = (x1 + x2) / 2 | |
self.c_y = (y1 + y2) / 2 | |
def show(image): | |
""" | |
Show any image. | |
""" | |
msg = 'press any key to continue' | |
cv2.namedWindow(msg, cv2.WINDOW_NORMAL) | |
cv2.imshow(msg, image) | |
cv2.waitKey(0) | |
cv2.destroyAllWindows() | |
def auto_canny(image, sigma=0.33): | |
""" | |
Get edges of an image | |
image: grayscale and blurred input image | |
edged: canny edge output image | |
""" | |
# compute the median of the single channel pixel intensities | |
v = numpy.median(image) | |
# apply automatic Canny edge detection using the computed median | |
lower = int(max(0, (1.0 - sigma) * v)) | |
upper = int(min(255, (1.0 + sigma) * v)) | |
edged = cv2.Canny(image, lower, upper) | |
# return the edged image | |
return edged | |
def intersection(l1, l2): | |
""" | |
Compute intersect point of two lines l1 and l2 | |
l1: line | |
l2: line | |
return: Intersect Point | |
""" | |
x1, y1, x2, y2 = l1.point | |
x3, y3, x4, y4 = l2.point | |
a1, b1 = y2 - y1, x1 - x2 | |
c1 = a1 * x1 + b1 * y1 | |
a2, b2 = y4 - y3, x3 - x4 | |
c2 = a2 * x3 + b2 * y3 | |
det = a1 * b2 - a2 * b1 | |
assert det, "lines are parallel" | |
return (1. * (b2 * c1 - b1 * c2) / det, 1. * (a1 * c2 - a2 * c1) / det) | |
def scanCrop(image, debug=False): | |
""" | |
Do the whole scanning thing. | |
image: input image | |
return: output image, cropped and perspective corrected | |
""" | |
# resize input image to img_proc to reduce computation | |
h, w = image.shape[:2] | |
min_w = 300 | |
scale = min(10., w * 1. / min_w) | |
h_proc = int(h * 1. / scale) | |
w_proc = int(w * 1. / scale) | |
image_dis = cv2.resize(image, (w_proc, h_proc)) | |
if debug: | |
print(image.shape) | |
print(image_dis.shape) | |
# make grayscale | |
gray = cv2.cvtColor(image_dis, cv2.COLOR_BGR2GRAY) | |
# blur | |
gray = cv2.GaussianBlur(gray, (5,5), 0) | |
# get edges of the image | |
canny = auto_canny(gray) | |
if debug: | |
show(canny) | |
# extract lines from the edge image | |
# TODO: Seem good for given scale, but need more test images to confirm | |
threshold = 70 | |
minLineLength = w_proc / 10 | |
maxLineGap = w_proc / 30 | |
lines = cv2.HoughLinesP(canny, 1, numpy.pi/180, threshold, None, minLineLength, maxLineGap) | |
if debug: | |
t = cv2.cvtColor(canny, cv2.COLOR_GRAY2BGR) | |
# classify lines into horizontal or vertical | |
hori, vert = [], [] | |
for l in lines[0]: | |
x1, y1, x2, y2 = l | |
if abs(x1 - x2) > abs(y1 - y2): | |
hori.append(Line(l)) | |
else: | |
vert.append(Line(l)) | |
if debug: | |
cv2.line(t, (x1, y1), (x2, y2), (0, 0, 255), 1) | |
if debug: | |
show(t) | |
# edge cases when not enough lines are detected | |
# extend the known lines to the edge of the image to create a new line | |
if len(hori) < 2: | |
if not hori or hori[0].c_y > h_proc / 2: | |
hori.append(Line((0, 0, w_proc - 1, 0))) | |
if not hori or hori[0].c_y <= h_proc / 2: | |
hori.append(Line((0, h_proc - 1, w_proc - 1, h_proc - 1))) | |
if len(vert) < 2: | |
if not vert or vert[0].c_x > w_proc / 2: | |
vert.append(Line((0, 0, 0, h_proc - 1))) | |
if not vert or vert[0].c_x <= w_proc / 2: | |
vert.append(Line((w_proc - 1, 0, w_proc - 1, h_proc - 1))) | |
# sort lines according to their center point | |
hori.sort(key=lambda l: l.c_y) | |
vert.sort(key=lambda l: l.c_x) | |
# find corners | |
if debug: | |
# Visualize corners for debug only | |
for l in [hori[0], vert[0], hori[-1], vert[-1]]: | |
x1, y1, x2, y2 = l.point | |
cv2.line(t, (x1, y1), (x2, y2), (0, 255, 255), 1) | |
# corners for the small scale | |
image_points = [intersection(hori[0], vert[0]), intersection(hori[0], vert[-1]), | |
intersection(hori[-1], vert[0]), intersection(hori[-1], vert[-1])] | |
if debug: | |
print("image_points small", image_points) | |
# scale corners to the original size | |
for i, p in enumerate(image_points): | |
x, y = p | |
image_points[i] = (x * scale, y * scale) | |
if debug: | |
cv2.circle(t, (int(x), int(y)), 1, (255, 255, 0), 3) | |
if debug: | |
print("image_points large", image_points) | |
show(t) | |
# perspective transform | |
# Proportional to the original image: | |
# image_points[0] is Upper Left corner | |
# image_points[1] is Upper Right corner | |
# image_points[2] is Lower Left corner | |
# image_points[3] is Lower Right corner | |
top_width = distance.euclidean(image_points[0], image_points[1]) | |
bottom_width = distance.euclidean(image_points[2], image_points[3]) | |
# Average | |
output_width = int((top_width + bottom_width) / 2) | |
left_height = distance.euclidean(image_points[0], image_points[2]) | |
right_height = distance.euclidean(image_points[1], image_points[3]) | |
# Average | |
output_height = int((left_height + right_height) / 2) | |
if debug: | |
print(top_width, bottom_width, output_width) | |
print(left_height, right_height, output_height) | |
dst_pts = numpy.array( | |
((0, 0), (output_width - 1, 0), (0, output_height - 1), (output_width - 1, output_height - 1)), | |
numpy.float32) | |
image_points = numpy.array(image_points, numpy.float32) | |
transmtx = cv2.getPerspectiveTransform(image_points, dst_pts) | |
return cv2.warpPerspective(image, transmtx, (output_width, output_height)) | |
if __name__ == '__main__': | |
""" | |
For testing | |
test.jpg: expect image in same folder as script, with rectangular object | |
test-crop.jpg: output cropped image; will overwrite if exists | |
""" | |
image = cv2.imread('test.jpg') | |
# If our test image needs to be rotated | |
image = numpy.rot90(image, 3) | |
show(image) | |
output_image = scanCrop(image, debug=True) | |
show(output_image) | |
cv2.imwrite('test-crop.jpg',output_image) | |
print("Saved.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment