Search code examples
pythonopencvpython-imaging-libraryimagemagickwand

Convert ImageMagick command to Python


I have the following ImageMagick command that works very well for removing grid lines from a table image:

convert test.png -background white -deskew 40% -write mpr:img \ 
\( mpr:img -morphology close rectangle:30x1 -negate \) \
\( mpr:img -morphology close rectangle:1x30 -negate \) \
-evaluate-sequence add \
result.png

I can, of course, invoke this code from my Python script.

However, I would like to be able to translate this command into something pythonic like Wand, PIL, cv2 or whatever.

Namely for portability sake but also to avoid having to save the output.

Even though Wand, for instance, has a morphology function I do not seem to be able to figure out the kernel and so on.

As the real images contain confidential information, I have added a similar mock example. I have verified that the above command produces the desired results of entirely clearing the grid lines without affecting the quality of the text.

As the real images contain confidential information, I have added a similar mock example.


Solution

  • Here is a solution in Python/OpenCV.

    Note I apply some (1 deg) rotation to your input for testing.

    • Read the input
    • Threshold on the gray lines
    • Get the convex hull from the thresholded image
    • Get the rotated rectangle from the convex hull
    • Create the affine matrix
    • Unrotate the input
    • Apply morphology to first copy of deskewed image and invert
    • Apply morphology to second copy of deskewed image and invert
    • Convert the 3 images to float and divide by 255 and add them together. Then multiply by 255 and convert back to uint8
    • Save added results

    Input:

    enter image description here

    import cv2
    import numpy as np
    
    # read the input
    img = cv2.imread('chart.png')
    hh, ww = img.shape[:2]
    
    # threshold on gray lines
    lower=(190,190,190)
    upper=(230,230,230)
    thresh = cv2.inRange(img, lower, upper)
    
    # get convex hull
    points = np.column_stack(np.where(thresh.transpose() > 0))
    hull = cv2.convexHull(points)
    
    # draw convex hull vertices on input image
    hull_img = img.copy()
    cv2.polylines(hull_img, [hull], True, (0,0,255), 2)
    
    # get rotated bounding box
    rotrect = cv2.minAreaRect(hull)
    box = cv2.boxPoints(rotrect)
    box = np.intp(box)
    
    # draw rotated rectangle on copy of img
    rot_bbox = img.copy()
    cv2.drawContours(rot_bbox,[box],0,(0,0,255),2)
    
    # get angle relative to horizontal from rotated rectangle
    angle = rotrect[-1]
    
    # from https://www.pyimagesearch.com/2017/02/20/text-skew-correction-opencv-python/
    # the `cv2.minAreaRect` function returns values in the
    # range [-90, 0); as the rectangle rotates clockwise the
    # returned angle tends to 0 -- in this special case we
    # need to add 90 degrees to the angle
    if angle < -45:
        angle = -(90 + angle)
     
    # otherwise, just take the negative of the angle to make
    # it positive
    else:
        angle = -angle
    
    print(angle,"deg")
    
    # negate the angle for deskewing
    neg_angle = -angle
    
    # Get rotation matrix
    (h, w) = img.shape[:2]
    center = (w // 2, h // 2)
    M = cv2.getRotationMatrix2D(center, neg_angle, scale=1.0)
    
    # rotate the image to deskew it
    deskewed = cv2.warpAffine(img, M, (ww,hh), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_CONSTANT, borderValue=(255,255,255))
    
    # create morphology close images
    img1 = deskewed.copy()
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT , (30,1))
    img1 = cv2.morphologyEx(img1, cv2.MORPH_CLOSE, kernel)
    img1 = np.invert(img1)
    
    img2 = deskewed.copy()
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT , (1,30))
    img2 = cv2.morphologyEx(img2, cv2.MORPH_CLOSE, kernel)
    img2 = np.invert(img2)
    
    # add input and morphology close images
    result = 255*(deskewed.astype("float32")/255 + img1.astype("float32")/255 + img2.astype("float32")/255)
    result = result.clip(0,255).astype("uint8")
    
    # save results
    cv2.imwrite("chart_threshold.jpg", thresh)
    cv2.imwrite("chart_convexhull.jpg", hull_img)
    cv2.imwrite("chart_rotated_rectangle.jpg", rot_bbox)
    cv2.imwrite("chart_deskewed.jpg", deskewed)
    cv2.imwrite("chart_result.png", deskewed)
    
    # show results
    cv2.imshow("THRESH", thresh)
    cv2.imshow("CONVEX_HULL", hull_img)
    cv2.imshow("ROTATED BBOX", rot_bbox)
    cv2.imshow("DESKEWED", deskewed)
    cv2.imshow("IMG1", img1)
    cv2.imshow("IMG2", img2)
    cv2.imshow("RESULT", result)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    Threshold Image

    enter image description here

    Convex Hull Image:

    enter image description here

    Rotated Rectangle Image:

    enter image description here

    Deskewed Image:

    enter image description here

    Result Image:

    enter image description here