pythonopencvscanning

Extract postcards from a scanned document using opencv?


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:

Sample scan

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:

Resulting picture

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:

Sample with less contrast

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:

Overlapping image

Or cards that are almost blank for some parts:

A lot of blank


Solution

  • 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)
    

    This is the output: enter image description here

    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: