Extract postcards from a scanned document using opencv?

Question:

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:

  • it does not find the bottom image which is too close to the border;
  • It only works for my test image, but it does not seem to be very generic. The line "ret, th = cv2.threshold(gray, 220, 235, 1)", for example, will prevent things from working on an image that has a different histogram I believe.

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

Asked By: Sylvain

||

Answers:

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:

  • decreasing of the threshold to 210;
  • removing of the canny function (it messes with the complex patterns of some images;
  • 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)
    
Answered By: el_Rinaldo
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.