Skip to content

Instantly share code, notes, and snippets.

@aplz
Created June 29, 2018 14:14
Show Gist options
  • Save aplz/efc8a01e5e667d051f3aee6117d9b5c0 to your computer and use it in GitHub Desktop.
Save aplz/efc8a01e5e667d051f3aee6117d9b5c0 to your computer and use it in GitHub Desktop.
Crop rectangular object from image.
# working version of https://bretahajek.com/2017/01/scanning-documents-photos-opencv/
import cv2
import numpy as np
def resize(img, height=800):
""" Resize image to given height """
ratio = height / img.shape[0]
return cv2.resize(img, (int(ratio * img.shape[1]), height))
def fourCornersSort(pts):
""" Sort corners: top-left, bot-left, bot-right, top-right """
# Difference and sum of x and y values
diffs_yx = np.diff(pts, axis=1)
sums_xy = pts.sum(axis=1)
# Top-left point has smallest sum...
# np.argmin() returns INDEX of min
return np.array([pts[np.argmin(sums_xy)],
pts[np.argmax(diffs_yx)],
pts[np.argmax(sums_xy)],
pts[np.argmin(diffs_yx)]])
def crop(input_path, minimum_percentage=0.1, show=False):
"""
:param input_path: the path to the image to process.
:param minimum_percentage: the item area should have at least 'minimum_percentage' of the overall image area.
Defaults to 0.1, i.e. 10 percent. If no such area can be found, return the original image.
:param show: if true, intermediate results are shown.
:return: the cropped image.
"""
image = cv2.imread(input_path)
img = resize(image)
if show:
cv2.imshow("original, resized", img)
cv2.waitKey(0)
# convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# Bilateral filter preserves edges
gray = cv2.bilateralFilter(gray, 9, 75, 75)
# Create black and white image based on adaptive threshold
gray = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 115, 4)
# Median filter clears small details
gray = cv2.medianBlur(gray, 11)
edges = cv2.Canny(gray, 200, 250)
_, contours, _ = cv2.findContours(edges.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
if show:
cv2.drawContours(img, contours, -1, [0, 255, 0], 2)
cv2.imshow('Contours', img)
cv2.waitKey(0)
height = edges.shape[0]
width = edges.shape[1]
MAX_CONTOUR_AREA = width * height
maxAreaFound = MAX_CONTOUR_AREA * minimum_percentage
pageContour = None
# Go through all contours
for cnt in contours:
# Simplify contour
perimeter = cv2.arcLength(cnt, True)
approx = cv2.approxPolyDP(cnt, 0.03 * perimeter, True)
approx_area = cv2.contourArea(approx)
if (len(approx) == 4 and cv2.isContourConvex(approx) # item has 4 corners and is convex
and maxAreaFound < approx_area < MAX_CONTOUR_AREA): # item area must be bigger than maxAreaFound
maxAreaFound = approx_area
pageContour = approx
if pageContour is None:
return image
else:
# Sort and offset corners
pageContour = fourCornersSort(pageContour[:, 0])
# Recalculate to original scale - start Points
sPoints = pageContour.dot(image.shape[0] / 800)
# Using Euclidean distance
# Calculate maximum height (maximal length of vertical edges) and width
height = max(np.linalg.norm(sPoints[0] - sPoints[1]),
np.linalg.norm(sPoints[2] - sPoints[3]))
width = max(np.linalg.norm(sPoints[1] - sPoints[2]),
np.linalg.norm(sPoints[3] - sPoints[0]))
# Create target points
tPoints = np.array([[0, 0], [0, height], [width, height], [width, 0]], np.float32)
# getPerspectiveTransform() needs float32
if sPoints.dtype != np.float32:
sPoints = sPoints.astype(np.float32)
# Warping perspective
M = cv2.getPerspectiveTransform(sPoints, tPoints)
cropped = cv2.warpPerspective(image, M, (int(width), int(height)))
if show:
cv2.imshow("cropped", cropped)
cv2.waitKey(0)
return cropped
if __name__ == '__main__':
input_file = "original.jpg"
output_file = "cropped.jpg"
result = crop(input_file, 0.1, True)
cv2.imwrite(output_file, result)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment