I have 1000s of old postcards that I'd like to scan, and I think it might be a good idea to optimize my workflow using some kind of automatic crop/rotate tools, so I have started investigating OpenCV with Python.
Below is a sample of picture I can acquire using my scanner:
As you can imagine, my goal is to create, from this image, three images each containing one postcard. I have tried many OpenCV options and the best code I have been able to get so far is:
import cv2, sys, imutils
cv2.namedWindow('image', cv2.WINDOW_NORMAL)
image = cv2.imread("sample1600.jpg")
ratio = image.shape[0] / 300.0
image = imutils.resize(image, height = 800)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (5, 5), 0)
ret, th = cv2.threshold(gray, 220, 235, 1)
edged = cv2.Canny(th, 25, 200)
(cnts, _) = cv2.findContours(edged.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:5]
for c in cnts:
peri = cv2.arcLength(c, True)
approx = cv2.approxPolyDP(c, 0.05 * peri, True)
if len(approx) == 4:
cv2.drawContours(image, [approx], -1, (0, 255, 0), 3)
cv2.imshow("Image", image)
cv2.waitKey(0)
The produced image is:
The issue with this code is that:
What is the best way to make this code work better and be more generic to fulfill my requirement to process scanned images?
Individual postcards should have a ratio that is approximatively √2 between width and height. That won't always be the case, but if my script is able to deal efficiently with this type of postcards, I will be more than happy (they represent more than 99% of my collection).
Thanks to @Riccardo, I now have a script that works for my first sample image, so adding a new one in order to try to find a more robust solution:
As @Riccardo has been very efficient providing a solution for the two first samples, here are two others that seem to be a bit more complicated because of the limited space between image for this first one:
Or cards that are almost blank for some parts:
I would suggest to pass through the computation of the rotated bounding box of the contour instead of trying to identify fixed shapes. In my try, the script identifies a box-like figure and calculates its contourArea, then it selects the figures that possess a big area.
This should solve your problem, let us know if it doesn't.
cv2.namedWindow('image', cv2.WINDOW_NORMAL)
image = cv2.imread("sample1600.jpg")
ratio = image.shape[0] / 300.0
image = imutils.resize(image, height = 800)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (5, 5), 0)
ret, th = cv2.threshold(gray,220,235,1)
edged = cv2.Canny(th, 25, 200)
im2, cnts, hierarchy = cv2.findContours(edged.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(cnts, key = cv2.contourArea, reverse = True)
for c in cnts:
box = cv2.minAreaRect(c)
box = cv2.cv.BoxPoints(box) if imutils.is_cv2() else cv2.boxPoints(box)
box = np.array(box, dtype="int")
if cv2.contourArea(box) > 70000:
cv2.drawContours(image, [box], -1, (0, 255, 0), 2)
cv2.imshow("Image", image)
cv2.waitKey(0)
EDIT: I don't know if this is the right solution, probably there are some other. I encourage the other users to share their approaches. @Sylvain, here's another try with some tuning of the parameters:
calculation of the image area and playing around with the limit of the contour to be returned. In this particular example I imposed the contour to be larger than 1/10 of the image and smaller than 2/3.
image = cv2.imread(img)
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
gray = cv2.GaussianBlur(gray, (5, 5), 0)
ret, th = cv2.threshold(gray,210,235,1)
im2, cnts, hierarchy = cv2.findContours(th.copy(),cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)
cnts = sorted(cnts, key = cv2.contourArea, reverse = True)
for c in cnts:
box = cv2.minAreaRect(c)
box = cv2.cv.BoxPoints(box) if imutils.is_cv2() else cv2.boxPoints(box)
box = np.array(box, dtype="int")
Area = image.shape[0]*image.shape[1]
if Area/10 < cv2.contourArea(box) < Area*2/3:
cv2.drawContours(image, [box], -1, (0, 255, 0), 2)
cv2.imshow("Image", image)
cv2.waitKey(0)