pythonopencvimage-processingcomputer-visionfingerprint

Remove border contours from fingerprint image


how do i remove the outer contour lines at the edge of this fingerprint image without affecting ridges and valleys contours

before processing

original photo

after segmentation and ROI

segmentation and roi

result after applying CLAHE and enhancement

[fingerprint] (https://i.sstatic.net/TIMu6.jpg)

import cv2

image = cv2.imread('fingerprint.jpg')
original = image.copy()
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
blur = cv2.GaussianBlur(gray, (9,9), 0)
thresh = cv2.threshold(gray,0,255,cv2.THRESH_OTSU + cv2.THRESH_BINARY)[1]

kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (2,2))
opening = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
dilate_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9,9))
dilate = cv2.dilate(opening, dilate_kernel, iterations=5)

cnts = cv2.findContours(dilate, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
cnts = cnts[0] if len(cnts) == 2 else cnts[1]
cnts = sorted(cnts, key=cv2.contourArea, reverse=True)

for c in cnts:
    x,y,w,h = cv2.boundingRect(c)
    cv2.rectangle(image, (x, y), (x + w, y + h), (36,255,12), 2)
    ROI = original[y:y+h, x:x+w]
    break

cv2.imshow('ROI', ROI)

but am not getting the desired result.


Solution

  • Here’s a possible solution. I’m working with the binary image. You don’t show how you get this image, you mention segmentation and CLAHE but none of these operations are shown in your snippet. It might be easier to deal with the “borders” there, before actually getting the binary image of the finger ridges.

    Anyway, my solution assumes that the borders are the first and last blobs to be encountering while scanning the image left to right. It also assumes that the borders are contiguous. The idea is to locate them and then flood-fill them with any color, in this case black, to “erase” them.

    First, locate the most external contours. It can be done by reducing the image to a row. The reduced row will give you the exact horizontal location of the first and last white pixels if you reduce using the MAX mode – that should correspond to the external borders. As the borders seem to be located at the upper part of the image, you can just take a portion where you are sure the borders are located:

    import cv2
    # Set image path
    imagePath = "D://opencvImages//TIMu6.jpg"
    
    # Load image:
    image = cv2.imread(imagePath)
    
    # Get binary image:
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    binaryImage = cv2.threshold(gray, 0, 255, cv2.THRESH_OTSU + cv2.THRESH_BINARY)[1]
    showImage("Binary", binaryImage)
    
    # BGR of binary image:
    bgrImage = cv2.cvtColor(binaryImage, cv2.COLOR_GRAY2BGR)
    bgrCopy = bgrImage.copy()
    
    # Get image dimensions:
    imageHeight, imageWidth = binaryImage.shape[0:2]
    
    # Vertically divide in 4 parts:
    heightDivision = 4
    heightPortion = imageHeight // heightDivision
    
    # Store divisions here:
    imageDivisions = []
    
    # Check out divisions:
    for i in range(heightDivision):
        # Compute y coordinate:
        y = i * heightPortion
    
        # Set crop dimensions:
        x = 0
        w = imageWidth
        h = heightPortion
    
        # Crop portion:
        portionCrop = binaryImage[y:y + h, x:x + w]
    
        # Store portion:
        imageDivisions.append(portionCrop)
    
        # Draw rectangle:
        cv2.rectangle(bgrImage, (0, y), (w, y + h), (0, 255, 0), 1)
        cv2.imshow("Portions", bgrImage)
        cv2.waitKey(0)
    

    This first bit just vertically divides the image into four portions. Just for visual purposes, let’s see the four divisions:

    I stored each portion in the imageDivisions list, but you just need the first one. Next, reduce it to a row using the MAX mode:

    # Reduce first portion to a row:
    reducedImage = cv2.reduce(imageDivisions[0], 0, cv2.REDUCE_MAX)
    

    This will vertically “crush” the matrix into a row (i.e., a vertical projection), where each pixel value is the maximum value (in this case, 255 – white) of each column. The result is a kinda-hard to see tiny row:

    Let’s search for the first and last white pixels. You can just look for the black to white and white to black transitions in this array:

    # Get first and last white pixel positions:
    pastPixel = 0
    pixelCoordinates = []
    for i in range(imageWidth):
        # Get current pixel:
        currentPixel = reducedImage[0][i]
    
        # Search for first transition black to white:
        if currentPixel == 255 and pastPixel == 0:
            pixelCoordinates.append(i)
        else:
            # Search for last transition white to black:
            if currentPixel == 0 and pastPixel == 255:
                pixelCoordinates.append(i - 1)
    
        # Set last pixel:
        pastPixel = currentPixel
    

    The horizontal coordinates of the white pixels are stored in the pixelCoordinates list. Lastly, let’s use this as positions for locating the most external borders and flood-fill them:

    # Flood fill original image:
    color = (0, 0, 255)  # Red
    
    for i in range(len(pixelCoordinates)):
        # Get x coordinate:
        x = pixelCoordinates[i]
        # Set y coordinate:
        y = heightPortion
    
        # Set seed point:
        seedPoint = (x, y)
        # Flood-fill:
        cv2.floodFill(bgrCopy, None, seedPoint, color)
        cv2.imshow("Flood-filled", bgrCopy)
        cv2.waitKey(0)
    

    Here I’m actually flood-filling a deep copy of the original BGR Image, and using a red color:

    If you want to fill the borders with black, just change the color to (0,0,0). If you want to flood-fill the original binary image, just change the first argument of the floodFill function. This is the result: