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?
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:
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.