Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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:

  • Image 1: Speckles on the top left and on the right.
  • Image 2: Rings on the left.
  • Image 3: Artifacts on the left.
  • Image 4: "N" in the top right and part of the music in the bottom left were removed.

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

like image 368
Samuel Bradshaw Avatar asked Jan 23 '26 16:01

Samuel Bradshaw


1 Answers

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

  • Read the input and convert to 3 channels if necessary

  • Add a 1 pixel wide black border to the image

  • Floodfill the image with red

  • Extract the red only as white and the rest as black using cv2.inRange() as a mask

  • Invert the mask so that the red area is now black and the rest is white

  • Compute the means of each of the 4 sides 1 pixel thick borders and find the minimum

  • Loop over all 4 borders and if not white, find the border that has the minimum mean. When found, remove that edge from the image and recompute all 4 border means and find the minimum. Stop when all 4 border means are white.

  • Save the result

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

like image 130
fmw42 Avatar answered Jan 25 '26 06:01

fmw42



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!