Selphiron
Selphiron

Reputation: 929

Cannot crop correctly with openCV in Python

I want to crop the identity card off of this scan enter image description here Mind, that there is a lot of white space under the id (until here)

First, I preprocess the image:

def preprocess_before_crop_2(scan_path, output_dir):
    # Read the image
    original_image = cv2.imread(scan_path)
    # Grayscale
    gray = cv2.cvtColor(original_image, cv2.COLOR_BGR2GRAY)
    # Histogram Equalization on grayscale image
    equalized = cv2.equalizeHist(gray)
    # Initial Denoising
    denoised = cv2.fastNlMeansDenoising(equalized, None, h=20, templateWindowSize=7, searchWindowSize=21)
    # Sharpening kernel
    kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
    sharpened = cv2.filter2D(denoised, -1, kernel)
    # Bilateral filter
    bilateral_filtered = cv2.bilateralFilter(sharpened, d=9, sigmaColor=75, sigmaSpace=75)
    # Increase Contrast
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
    contrast = clahe.apply(bilateral_filtered)
    # Apply slight Gaussian blur before binarization for anti-aliasing
    blurred = cv2.GaussianBlur(contrast, (3, 3), 0)
    # Binary conversion with Otsu's thresholding
    _, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
    # Adaptive thresholding
    adaptive_thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
    return adaptive_thresh

then I use it to crop the image:

def crop_document(scan_path, output_dir):
    original_image = cv2.imread(scan_path)
    # preprocess image
    preprocessed_image = preprocess_before_crop(scan_path, output_dir)

    contours, hierarchy = cv2.findContours(preprocessed_image,cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
    
    # Find object with the biggest bounding box
    mx = (0,0,0,0)      # biggest bounding box so far
    mx_area = 0
    areas = []
    for cont in contours:
        x,y,w,h = cv2.boundingRect(cont)
        area = w*h
        ratio = float(w) / float(h)
        areas.append((area,ratio))
        if area > mx_area and ratio > 1:
            mx = x,y,w,h
            mx_area = area

    x,y,w,h = mx

    # Crop and save
    cropped_image=original_image[y:y+h,x:x+w]
    return cropped_image

However, the resulting cropped image looks like this: (I added a red rectangle of what I would like to crop instead) enter image description here

I tried to remove/change some steps in the preprocessing steps, but to no avail.

Edit: This was the image I originally uploaded, sorry if anyone was confused. enter image description here

Upvotes: -3

Views: 128

Answers (2)

mri
mri

Reputation: 590

The problem is that your object of interest is connected to that top left black corner. This already occurs at the cv2.equalizeHist() step:

import matplotlib.pyplot as plt
plt.imshow(equalized)
plt.axis('off')
plt.show()

equalized image

Simply ignoring that step and taking the SECOND largest contour will do the job:

denoised = cv2.fastNlMeansDenoising(gray, None, h=20, templateWindowSize=7, searchWindowSize=21)

# ...

preprocessed_image = adaptive_thresh

#sort by area
contours = cv2.findContours(preprocessed_image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
contours = sorted(contours, key=cv2.contourArea, reverse=True)

#select 2nd largest contour
x,y,w,h = cv2.boundingRect(contours[1])

cropped_image=original_image[y:y+h,x:x+w]

#display cropped image
plt.imshow(cropped_image)
plt.axis('off')
plt.show()

#display the respective contour
cv2.drawContours(original_image, [contours[1]], -1, (0, 255, 0), 20)
plt.imshow(original_image)
plt.axis('off')
plt.show()

The cropped image:

cropped image

The contours of interest:

2nd largest contours

EDIT: Instead of choosing the second largest contour you could further specify the object of interest by computing shape-related metrics, e.g.

area = list()
perimeter = list()
circularity = list()
aspectRatio = list()
cx = list() #centroid cooords
cy = list() #centroid coords
xydist = list() #distance of contour centroid to image center

for c in contours:
    area.append(cv2.contourArea(c))
    perimeter.append(cv2.arcLength(c, closed = True))
    if int(cv2.arcLength(c, closed = True)) == 0: #avoid division by '0'
       circularity.append(0) #dummy value
    else: 
       circularity.append((4*math.pi*cv2.contourArea(c))/ cv2.arcLength(c, closed = True)**2)
    
    x,y,w,h = cv2.boundingRect(c)
    aspectRatio.append(float(w)/h)
    M = cv2.moments(c)
  
    if int(M['m00']) == 0: #avoid division by '0'
        cx.append(0) #dummy value
        cy.append(0) #dummy value
        xydist.append(0) #dummy value
    else: 
        cx.append(int(M['m10']/M['m00']))
        cy.append(int(M['m01']/M['m00']))
        xydist.append(abs(int(M['m01']/M['m00']) - original_image.shape[0]/2) + abs(int(M['m01']/M['m00']) - original_image.shape[1]/2))

Now, for example, if the area of an object is much larger than expected, it can be simply removed by applying a threshold.

Upvotes: 1

toyota Supra
toyota Supra

Reputation: 4560

Cannot crop correctly with openCV2

I would recommend to use mouse events.

  • Just draw rectangle.
  • Press s to save it

Snippet:

import cv2 as cv

refPt = []
cropping = False

def click_crop(event, x, y, flags, param):
    global refPt, cropping, num
    if event == cv.EVENT_LBUTTONDOWN:
        refPt = [(x, y)]
        cropping = True

    if event == cv.EVENT_LBUTTONUP:
        num += 1
        refPt.append((x, y))
        cropping = False        
        cv.rectangle(img, refPt[0], refPt[1], (0, 255, 0), 2)

if __name__ == "__main__":
    num = 0

    windowName = 'Click and Crop'
    img = cv.imread('l.png', cv.IMREAD_COLOR)
    clone = img.copy()
    cv.namedWindow(windowName)

    cv.setMouseCallback(windowName, click_crop)

    num = 0
    if len(refPt) == 2:
        num += 1

    while True:
        cv.imshow(windowName, img)
        key = cv.waitKey(1)
        if key == ord("r"): 
            img = clone.copy()
        elif key == ord("s"):   
            roi = clone[refPt[0][1]:refPt[1][1], refPt[0][0]:refPt[1][0]]
            cv.imwrite('roi{}.jpg'.format(num), roi)            
        elif key == ord("c"):  
            break    
    cv.destroyAllWindows()

Screenshot:

enter image description here

Output:

enter image description here

Upvotes: 0

Related Questions