pythonopencvimage-processingcomputer-visionsemantic-segmentation

Cleaning up scanned book page edges with OpenCV


I have several scanned book images that I'm trying to clean up. They've been thresholded and partially cleaned with OpenCV, but there are artifacts around the edges that I've been struggling to remove.

I tried adding a black border around the edges, then using a flood fill to remove black pixels that were touching the edges. This helps, but it's not perfect. Any ideas how I could improve the process?

Original images

Before

After border and flood fill

After

These are the remaining issues I'd like to fix:

Here's my code:

import os
import cv2
import numpy as np

current_directory = os.path.abspath(os.path.dirname(__file__))
filenames = ['1.webp', '2.webp', '3.webp', '4.webp']

for filename in filenames:
  input_path = os.path.join(current_directory, filename)
  output_path = os.path.join(current_directory, filename + '-clean.webp')
  
  img = cv2.imread(input_path, cv2.IMREAD_GRAYSCALE)
  
  # Add a black border
  img = cv2.copyMakeBorder(img, 50, 50, 50, 50, cv2.BORDER_CONSTANT)
  
  # Flood fill with white, from the top left pixel
  cv2.floodFill(img, None, (0, 0), 255)
  
  # Trim the image by removing white pixels on all sides
  y_nonzero, x_nonzero = np.nonzero(img == 0)
  img = img[np.min(y_nonzero):np.max(y_nonzero), np.min(x_nonzero):np.max(x_nonzero)]
  
  cv2.imwrite(output_path, img)

Full resolution images:

Image 1 Image 2 Image 3 Image 4


Solution

  • Here is my code in Python/OpenCV to do the same as my interpretation of what Imagemagick does.

    import cv2
    import numpy as np
    
    # read image
    #img = cv2.imread('scan1.webp')
    #img = cv2.imread('scan2.webp')
    img = cv2.imread('scan3.webp')
    hh, ww, cc = img.shape
    
    if cc != 3:
        img = cv2.merge([img, img, img])
    
    # add black border to ensure black all around
    img2 = cv2.copyMakeBorder(img, 1, 1, 1, 1, borderType=cv2.BORDER_CONSTANT, value=(0,0,0))
    
    # apply floodfill from top-left corner with red
    floodfill = img2.copy()
    red = (0,0,255)
    loval = (0,0,0)
    hival = (0,0,0)
    cv2.floodFill(floodfill, None, (0,0), red, loval, hival, flags=8)
    
    # extract red color only and invert
    lower = (0,0,255)
    upper = (0,0,255)
    mask = cv2.inRange(floodfill, lower, upper)
    mask = 255 - mask
    print(mask[0:1, 0:1])
    
    # define top and left starting coordinates and starting width and height
    top = 0
    left = 0
    bottom = hh
    right = ww
    i=0
    
    # compute the mean of each side of the image and its stop test
    mean_top = np.mean( mask[top:top+1, left:right] )
    mean_left = np.mean( mask[top:bottom, left:left+1] )
    mean_bottom = np.mean( mask[bottom-1:bottom, left:right] )
    mean_right = np.mean( mask[top:bottom, right-1:right] )
    mean_minimum = min(mean_top, mean_left, mean_bottom, mean_right)
    print("mean_top=",mean_top, " mean_left=",mean_left, " mean_bottom=",mean_bottom, " mean_right=",mean_right, " mean_minimum=",mean_minimum)
    
    top_test = "stop" if (mean_top == 255) else "go"
    left_test = "stop" if (mean_left == 255) else "go"
    bottom_test = "stop" if (mean_bottom == 255) else "go"
    right_test = "stop" if (mean_right == 255) else "go"
    print(top_test,left_test,bottom_test,right_test)
    
    # iterate to compute new side coordinates if mean of given side is not 255 (all white) and it is the current darkest side
    while top_test == "go" or left_test == "go" or right_test == "go" or bottom_test == "go":
    
        # top processing
        if top_test == "go":
            if mean_top != 255:
                if mean_top == mean_minimum:
                    top += 1
                    mean_top = np.mean( mask[top:top+1, left:right] )
                    mean_left = np.mean( mask[top:bottom, left:left+1] )
                    mean_bottom = np.mean( mask[bottom-1:bottom, left:right] )
                    mean_right = np.mean( mask[top:bottom, right-1:right] )
                    mean_minimum = min(mean_top, mean_left, mean_right, mean_bottom)
                    i += 1
                    print("increment=",i, "top_count=", top, " top_mean=", mean_top)
                    continue
            else:
                top_test = "stop"
                print("top stop")   
    
        # left processing
        if left_test == "go":
            if mean_left != 255:
                if mean_left == mean_minimum:
                    left += 1
                    mean_top = np.mean( mask[top:top+1, left:right] )
                    mean_left = np.mean( mask[top:bottom, left:left+1] )
                    mean_bottom = np.mean( mask[bottom-1:bottom, left:right] )
                    mean_right = np.mean( mask[top:bottom, right-1:right] )
                    mean_minimum = min(mean_top, mean_left, mean_right, mean_bottom)
                    i += 1
                    print("increment=",i, "left_count=", left, " left_mean=", mean_left)
                    continue
            else:
                left_test = "stop"  
                print("left stop")
    
        # bottom processing
        if bottom_test == "go":
            if mean_bottom != 255:
                if mean_bottom == mean_minimum:
                    bottom -= 1
                    mean_top = np.mean( mask[top:top+1, left:right] )
                    mean_left = np.mean( mask[top:bottom, left:left+1] )
                    mean_bottom = np.mean( mask[bottom-1:bottom, left:right] )
                    mean_right = np.mean( mask[top:bottom, right-1:right] )
                    mean_minimum = min(mean_top, mean_left, mean_right, mean_bottom)
                    i += 1
                    print("increment=",i, "bottom_count=", bottom, " bottom_mean=", mean_bottom)
                    continue
            else:
                bottom_test = "stop"
                print("bottom stop")    
    
        # right processing
        if right_test == "go":
            if mean_right != 255:
                if mean_right == mean_minimum:
                    right -= 1
                    mean_top = np.mean( mask[top:top+1, left:right] )
                    mean_left = np.mean( mask[top:bottom, left:left+1] )
                    mean_bottom = np.mean( mask[bottom-1:bottom, left:right] )
                    mean_right = np.mean( mask[top:bottom, right-1:right] )
                    mean_minimum = min(mean_top, mean_left, mean_right, mean_bottom)
                    i += 1
                    print("increment=",i, "right_count=", right, " right_mean=", mean_right)
                    continue
            else:
                right_test = "stop" 
                print("right stop")
    
    
    # crop input
    result = img[top:bottom, left:right]
    
    # print crop values 
    print("top: ",top)
    print("bottom: ",bottom)
    print("left: ",left)
    print("right: ",right)
    print("height:",result.shape[0])
    print("width:",result.shape[1])
    
    # save cropped image
    #cv2.imwrite('scan1_cropped.png',result)
    #cv2.imwrite('scan2_cropped.png',result)
    cv2.imwrite('scan3_cropped.png',result)
    
    # show the images
    cv2.imshow("mask", mask)
    cv2.imshow("cropped", result)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    
    

    Here are the results of the first 3 images.

    enter image description here

    enter image description here

    enter image description here