Reputation: 929
I want to crop the identity card off of this scan
Mind, that there is a lot of white space under the id (until here)
First, I preprocess the image:
def preprocess_before_crop_2(scan_path, output_dir):
# Read the image
original_image = cv2.imread(scan_path)
# Grayscale
gray = cv2.cvtColor(original_image, cv2.COLOR_BGR2GRAY)
# Histogram Equalization on grayscale image
equalized = cv2.equalizeHist(gray)
# Initial Denoising
denoised = cv2.fastNlMeansDenoising(equalized, None, h=20, templateWindowSize=7, searchWindowSize=21)
# Sharpening kernel
kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
sharpened = cv2.filter2D(denoised, -1, kernel)
# Bilateral filter
bilateral_filtered = cv2.bilateralFilter(sharpened, d=9, sigmaColor=75, sigmaSpace=75)
# Increase Contrast
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
contrast = clahe.apply(bilateral_filtered)
# Apply slight Gaussian blur before binarization for anti-aliasing
blurred = cv2.GaussianBlur(contrast, (3, 3), 0)
# Binary conversion with Otsu's thresholding
_, binary = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
# Adaptive thresholding
adaptive_thresh = cv2.adaptiveThreshold(blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2)
return adaptive_thresh
then I use it to crop the image:
def crop_document(scan_path, output_dir):
original_image = cv2.imread(scan_path)
# preprocess image
preprocessed_image = preprocess_before_crop(scan_path, output_dir)
contours, hierarchy = cv2.findContours(preprocessed_image,cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
# Find object with the biggest bounding box
mx = (0,0,0,0) # biggest bounding box so far
mx_area = 0
areas = []
for cont in contours:
x,y,w,h = cv2.boundingRect(cont)
area = w*h
ratio = float(w) / float(h)
areas.append((area,ratio))
if area > mx_area and ratio > 1:
mx = x,y,w,h
mx_area = area
x,y,w,h = mx
# Crop and save
cropped_image=original_image[y:y+h,x:x+w]
return cropped_image
However, the resulting cropped image looks like this: (I added a red rectangle of what I would like to crop instead)
I tried to remove/change some steps in the preprocessing steps, but to no avail.
Edit:
This was the image I originally uploaded, sorry if anyone was confused.
Upvotes: -3
Views: 128
Reputation: 590
The problem is that your object of interest is connected to that top left black corner.
This already occurs at the cv2.equalizeHist()
step:
import matplotlib.pyplot as plt
plt.imshow(equalized)
plt.axis('off')
plt.show()
Simply ignoring that step and taking the SECOND largest contour will do the job:
denoised = cv2.fastNlMeansDenoising(gray, None, h=20, templateWindowSize=7, searchWindowSize=21)
# ...
preprocessed_image = adaptive_thresh
#sort by area
contours = cv2.findContours(preprocessed_image, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
contours = contours[0] if len(contours) == 2 else contours[1]
contours = sorted(contours, key=cv2.contourArea, reverse=True)
#select 2nd largest contour
x,y,w,h = cv2.boundingRect(contours[1])
cropped_image=original_image[y:y+h,x:x+w]
#display cropped image
plt.imshow(cropped_image)
plt.axis('off')
plt.show()
#display the respective contour
cv2.drawContours(original_image, [contours[1]], -1, (0, 255, 0), 20)
plt.imshow(original_image)
plt.axis('off')
plt.show()
The cropped image:
The contours of interest:
EDIT: Instead of choosing the second largest contour you could further specify the object of interest by computing shape-related metrics, e.g.
area = list()
perimeter = list()
circularity = list()
aspectRatio = list()
cx = list() #centroid cooords
cy = list() #centroid coords
xydist = list() #distance of contour centroid to image center
for c in contours:
area.append(cv2.contourArea(c))
perimeter.append(cv2.arcLength(c, closed = True))
if int(cv2.arcLength(c, closed = True)) == 0: #avoid division by '0'
circularity.append(0) #dummy value
else:
circularity.append((4*math.pi*cv2.contourArea(c))/ cv2.arcLength(c, closed = True)**2)
x,y,w,h = cv2.boundingRect(c)
aspectRatio.append(float(w)/h)
M = cv2.moments(c)
if int(M['m00']) == 0: #avoid division by '0'
cx.append(0) #dummy value
cy.append(0) #dummy value
xydist.append(0) #dummy value
else:
cx.append(int(M['m10']/M['m00']))
cy.append(int(M['m01']/M['m00']))
xydist.append(abs(int(M['m01']/M['m00']) - original_image.shape[0]/2) + abs(int(M['m01']/M['m00']) - original_image.shape[1]/2))
Now, for example, if the area of an object is much larger than expected, it can be simply removed by applying a threshold.
Upvotes: 1
Reputation: 4560
Cannot crop correctly with openCV2
I would recommend to use mouse events.
Snippet:
import cv2 as cv
refPt = []
cropping = False
def click_crop(event, x, y, flags, param):
global refPt, cropping, num
if event == cv.EVENT_LBUTTONDOWN:
refPt = [(x, y)]
cropping = True
if event == cv.EVENT_LBUTTONUP:
num += 1
refPt.append((x, y))
cropping = False
cv.rectangle(img, refPt[0], refPt[1], (0, 255, 0), 2)
if __name__ == "__main__":
num = 0
windowName = 'Click and Crop'
img = cv.imread('l.png', cv.IMREAD_COLOR)
clone = img.copy()
cv.namedWindow(windowName)
cv.setMouseCallback(windowName, click_crop)
num = 0
if len(refPt) == 2:
num += 1
while True:
cv.imshow(windowName, img)
key = cv.waitKey(1)
if key == ord("r"):
img = clone.copy()
elif key == ord("s"):
roi = clone[refPt[0][1]:refPt[1][1], refPt[0][0]:refPt[1][0]]
cv.imwrite('roi{}.jpg'.format(num), roi)
elif key == ord("c"):
break
cv.destroyAllWindows()
Screenshot:
Output:
Upvotes: 0