Search code examples
pythonopencvimutils

My Python OpenCV program is not detecting the whole page but rather a singular rectangle on the page


I'm trying to write a program in Python that can mark exam papers using opencv and imutils, I have attached an example. I'm trying to get a top down view of the page but it's not detecting the right contours.Contours drawn imutils four_point_transform applied I would like it to apply the function to the whole page and not to the questions. Is it possible to use the corner borders for four_point_transform? It may not be very visible so here is the original image.

# Import libraries
import cv2
import numpy as np
import imutils
from imutils import contours
from imutils.perspective import four_point_transform

# Verify exam sheet location
image = cv2.imread("test4.jpg")
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (5, 5), 0)
edge = cv2.Canny(blur, 100, 200)

# Locate contours
contours = cv2.findContours(edge.copy(), cv2.RETR_EXTERNAL,
    cv2.CHAIN_APPROX_SIMPLE)
contours = imutils.grab_contours(contours)
docCnt = None

# Verify main object is exam
if len(contours) > 0:
    contours = sorted(contours, key=cv2.contourArea, reverse=True)
    for c in contours:
        peri = cv2.arcLength(c, True)
        approx = cv2.approxPolyDP(c, 0.02 * peri, True)
        if len(approx) == 4:
            docCnt = approx
            break

# Create a top-down view of paper
paper = four_point_transform(image, docCnt.reshape(4,2))
warped = four_point_transform(edge, docCnt.reshape(4,2))

# Draw contours
cv2.drawContours(image, contours, -1, (0,255,0), 3)
cv2.imwrite("Contours.jpg", image)

# Display images
cv2.imshow("Original", image)
# cv2.imshow("Perspective", paper)
# cv2.imshow("Processed", edge)
# cv2.imshow("Processed + Perspective", warped)
cv2.waitKey(0)
cv2.destroyAllWindows

Solution

  • The contour just needed a bit of dilating and then it was able to capture the paper in itself. Here is the code:

    image = cv2.imread('Example.jpg') # read image and convert to rgb for each plotting
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # turn to gray
    blurred = cv2.GaussianBlur(gray, (5, 5), 0) # blurring
    edges = cv2.Canny(blurred, 50, 150) # edges
    kernel = np.ones((5,5), np.uint8) # kernel for dilating
    edges = cv2.dilate(edges, kernel, iterations=3) # dilating
    contours, _ = cv2.findContours(edges.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # get contours
    largest_contour = max(contours, key = cv2.contourArea) # get largest
    im_cont = image.copy() # copy image for contouring
    

    In the above code, I went through a standard canny edge detection. Then, I got the approximation of the contour:

    hull = cv2.convexHull(largest_contour) # get the hull
    epsilon = 0.02 * cv2.arcLength(largest_contour, True) # epsilon
    pts1 = np.float32(cv2.approxPolyDP(hull, epsilon, True).reshape(-1, 2)) # get the points
    pts2 = np.float32([[0, 0], [image.shape[1], 0], [image.shape[1], image.shape[0]], [0, image.shape[0]]]) # get original edges
    matrix = cv2.getPerspectiveTransform(pts1, pts2) # get perspective transform matrix
    result = cv2.warpPerspective(image, matrix, (image.shape[1], image.shape[0])) # apply matrix to image
    

    Plotting, I got the following results:

    Transformed

    Maybe think also a bit about stretching to make the aspect ratio a bit better. This is a nice tutorial as well

    V2.0:Using imutils

    As you can see from the function, all you require for the function are the image itself and the coordinates of the detected corner. Hence, here is the adjusted code:

    import cv2
    import numpy as np
    %matplotlib notebook 
    import matplotlib.pyplot as plt
    from imutils.perspective import four_point_transform
    #####################################################################################################
    image = cv2.imread('Example.jpg') # read image and convert to rgb for each plotting
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # turn to gray
    blurred = cv2.GaussianBlur(gray, (5, 5), 0) # blurring
    edges = cv2.Canny(blurred, 50, 150) # edges
    kernel = np.ones((5,5), np.uint8) # kernel for dilating
    edges = cv2.dilate(edges, kernel, iterations=3) # dilating
    contours, _ = cv2.findContours(edges.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) # get contours
    largest_contour = max(contours, key = cv2.contourArea) # get largest
    im_cont = image.copy() # copy image for contouring
    cv2.drawContours(im_cont, largest_contour, -1, (255,0,0), 2)
    #####################################################################################################
    hull = cv2.convexHull(largest_contour) # get the hull
    epsilon = 0.02 * cv2.arcLength(largest_contour, True) # epsilon
    pts1 = np.float32(cv2.approxPolyDP(hull, epsilon, True).reshape(-1, 2)) # get the points
    result = four_point_transform(image, pts1) # using imutils
    

    Here is the result, the nice addition is that the imutils function additionally scales the results. Imutils results