-
-
Save karnauskas/988014dc2172a2fbc4f631fa22b2be62 to your computer and use it in GitHub Desktop.
Automatic scanned image rotation/deskew with OpenCV
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
import cv2 | |
import numpy as np | |
def deskew(im, max_skew=10): | |
height, width = im.shape | |
# Create a grayscale image and denoise it | |
im_gs = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) | |
im_gs = cv2.fastNlMeansDenoising(im_gs, h=3) | |
# Create an inverted B&W copy using Otsu (automatic) thresholding | |
im_bw = cv2.threshold(im_gs, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)[1] | |
# Detect lines in this image. Parameters here mostly arrived at by trial and error. | |
lines = cv2.HoughLinesP( | |
im_bw, 1, np.pi / 180, 200, minLineLength=width / 12, maxLineGap=width / 150 | |
) | |
# Collect the angles of these lines (in radians) | |
angles = [] | |
for line in lines: | |
x1, y1, x2, y2 = line[0] | |
angles.append(np.arctan2(y2 - y1, x2 - x1)) | |
# If the majority of our lines are vertical, this is probably a landscape image | |
landscape = np.sum([abs(angle) > np.pi / 4 for angle in angles]) > len(angles) / 2 | |
# Filter the angles to remove outliers based on max_skew | |
if landscape: | |
angles = [ | |
angle | |
for angle in angles | |
if np.deg2rad(90 - max_skew) < abs(angle) < np.deg2rad(90 + max_skew) | |
] | |
else: | |
angles = [angle for angle in angles if abs(angle) < np.deg2rad(max_skew)] | |
if len(angles) < 5: | |
# Insufficient data to deskew | |
return im | |
# Average the angles to a degree offset | |
angle_deg = np.rad2deg(np.median(angles)) | |
# If this is landscape image, rotate the entire canvas appropriately | |
if landscape: | |
if angle_deg < 0: | |
im = cv2.rotate(im, cv2.ROTATE_90_CLOCKWISE) | |
angle_deg += 90 | |
elif angle_deg > 0: | |
im = cv2.rotate(im, cv2.ROTATE_90_COUNTERCLOCKWISE) | |
angle_deg -= 90 | |
# Rotate the image by the residual offset | |
M = cv2.getRotationMatrix2D((width / 2, height / 2), angle_deg, 1) | |
im = cv2.warpAffine(im, M, (width, height), borderMode=cv2.BORDER_REPLICATE) | |
return im |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment