I am working on scanned documents (ID card, Driver licenses, ...). The problem I faced while I apply some preprocessing on them is that the documents occupy just a small area of the image, all the rest area is whether white/blank space or noised space. For that reason I wanted to develop a Python code that automatically trims the unwanted area and keeps only the zone where the document is located (without I predefine the resolution). That's possible with using
findContours() from OpenCV. However, not all the documents (especially the old ones) have clear contour and not all the blank space is white, so this will not work.
The idea that came to me is:
- Read the image and convert it to gray-scale.
- Apply the
bitwise_not()function from OpenCV to separate the background from the foreground.
Apply adaptive mean threshold to remove as much possible of noise (and eventually to whiten the background).
At this level, I have the background almost white and the document is in black but containing some white gaps.
I applied erosion to fill the gaps.
- Read each row of the image and if 20% of it contains black, then keep it, if it is white, delete it. And do the same with each column of the image.
- Crop the image according to the min and max of the index of the black lines and columns.
Here is my code with some comments:
import cv2 import numpy as np def crop(filename): #Read the image img = cv2.imread(filename) #Convert to grayscale gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) #Separate the background from the foreground bit = cv2.bitwise_not(gray) #Apply adaptive mean thresholding amtImage = cv2.adaptiveThreshold(bit, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 35, 15) #Apply erosion to fill the gaps kernel = np.ones((15,15),np.uint8) erosion = cv2.erode(amtImage,kernel,iterations = 2) #Take the height and width of the image (height, width) = img.shape[0:2] #Ignore the limits/extremities of the document (sometimes are black, so they distract the algorithm) image = erosion[50:height - 50, 50: width - 50] (nheight, nwidth) = image.shape[0:2] #Create a list to save the indexes of lines containing more than 20% of black. index =  for x in range (0, nheight): line =  for y in range(0, nwidth): line2 =  if (image[x, y] < 150): line.append(image[x, y]) if (len(line) / nwidth > 0.2): index.append(x) #Create a list to save the indexes of columns containing more than 15% of black. index2 =  for a in range(0, nwidth): line2 =  for b in range(0, nheight): if image[b, a] < 150: line2.append(image[b, a]) if (len(line2) / nheight > 0.15): index2.append(a) #Crop the original image according to the max and min of black lines and columns. img = img[min(index):max(index) + min(250, (height - max(index))* 10 // 11) , max(0, min(index2)): max(index2) + min(250, (width - max(index2)) * 10 // 11)] #Save the image cv2.imwrite('res_' + filename, img)
Here is an example. I used an image from the internet to avoid any confidentiality problem. It is to notice here that the image quality is much better (the white space does not contain noise) than the examples I work on.
I tested this code with different documents, and it works well. The problem is that it takes a lot of time to process a single document (because of the loops and reading each pixel of the image twice: once with lines and the second with columns). I am sure that it is possible to do some modifications to optimize the code and reduce the processing time. But I am very beginner with Python and code optimization.
Maybe using numpy to process the matrix calculations or optimizing the loops would improve the code quality.