Search code examples
pythonimage-processingimage-segmentationsobelbinary-image

How can I make all background white in a binary image


Binary Image

In the image I would like to have only the lungs in black not the background. The background [Top black and bottom black area of the image] must be white not black. How can I do that in python?. Code that resuls in the image above from an original grayscale image ("image") is:

from skimage.filters import sobel

img = cv2.GaussianBlur(image, (5, 5), 0)
img = cv2.erode(img, None, iterations=2)
img = cv2.dilate(img, None, iterations=2)

elevation_map = sobel(img)

markers = np.zeros_like(image)

markers[image < 70] = 1
markers[image > 254] = 2
segmentation = skimage.morphology.watershed(elevation_map, markers)

fig, ax = plt.subplots(figsize=(7, 7))
ax.imshow(segmentation, cmap=plt.cm.gray, interpolation='nearest')
ax.axis('off')
ax.set_title('segmentation')

Solution

  • Assuming we have the segmentation image as posted above, and we want to fill the surrounding background with while.

    One option is iterating the borders, and apply floodFill where pixel is black:

    import cv2
    import numpy as np
    
    gray = cv2.imread('segmentation.png', cv2.IMREAD_GRAYSCALE)  # Read image as grayscale
    
    for x in range(gray.shape[1]):
        # Fill dark top pixels:
        if gray[0, x] == 0:
            cv2.floodFill(gray, None, seedPoint=(x, 0), newVal=255, loDiff=3, upDiff=3)  # Fill the background with white color
    
        # Fill dark bottom pixels:
        if gray[-1, x] == 0:
            cv2.floodFill(gray, None, seedPoint=(x, gray.shape[0]-1), newVal=255, loDiff=3, upDiff=3)  # Fill the background with white color
    
    for y in range(gray.shape[0]):
        # Fill dark left side pixels:
        if gray[y, 0] == 0:
            cv2.floodFill(gray, None, seedPoint=(0, y), newVal=255, loDiff=3, upDiff=3)  # Fill the background with white color
    
        # Fill dark right side pixels:
        if gray[y, -1] == 0:
            cv2.floodFill(gray, None, seedPoint=(gray.shape[1]-1, y), newVal=255, loDiff=3, upDiff=3)  # Fill the background with white color
    
    cv2.imshow('gray', gray)
    cv2.waitKey()
    cv2.destroyAllWindows()
    

    Other option is use something as MATLAB imfill(BW,'holes').
    Fill the center black with white.
    In the original image, fill with while where both original and filled image are black:

    import cv2
    import numpy as np
    from skimage.morphology import reconstruction
    
    def imfill(img):
        # https://stackoverflow.com/questions/36294025/python-equivalent-to-matlab-funciton-imfill-for-grayscale
        # Use the matlab reference Soille, P., Morphological Image Analysis: Principles and Applications, Springer-Verlag, 1999, pp. 208-209.
        #  6.3.7  Fillhole
        # The holes of a binary image correspond to the set of its regional minima which
        # are  not  connected  to  the image  border.  This  definition  holds  for  grey scale
        # images.  Hence,  filling  the holes of a  grey scale image comes down  to remove
        # all  minima  which  are  not  connected  to  the  image  border, or,  equivalently,
        # impose  the  set  of minima  which  are  connected  to  the  image  border.  The
        # marker image 1m  used  in  the morphological reconstruction by erosion is set
        # to the maximum image value except along its border where the values of the
        # original image are kept:
    
        seed = np.ones_like(img)*255
        img[ : ,0] = 0
        img[ : ,-1] = 0
        img[ 0 ,:] = 0
        img[ -1 ,:] = 0
        seed[ : ,0] = 0
        seed[ : ,-1] = 0
        seed[ 0 ,:] = 0
        seed[ -1 ,:] = 0
    
        fill_img = reconstruction(seed, img, method='erosion')
    
        return fill_img
    
    gray = cv2.imread('segmentation.png', cv2.IMREAD_GRAYSCALE)  # Read image as grayscale
    
    fill_gray = imfill(gray)
    
    # Fill with white where both gray and fill_gray are zeros
    gray[(gray == 0) & (fill_gray == 0)] = 255;
    
    cv2.imshow('gray', gray)
    cv2.imshow('fill_gray', fill_gray)
    cv2.waitKey()
    cv2.destroyAllWindows()
    

    I also thought of solving it using findContours with hierarchy, but it's a bit more coding.


    Output:
    enter image description here

    For removing the black spot in the center (if needed), we may use connectedComponentsWithStats, and fill the small "spot" according to the area.


    Edit:

    Example for incorporating the above solution with your code:

    #!/usr/bin/env python3
    # -*- coding: utf-8 -*-
    
    import numpy as np
    import matplotlib.pyplot as plt
    import skimage.io
    import skimage.color
    import skimage.filters
    from skimage.io import imread
    from skimage.color import rgb2gray
    from skimage.filters import sobel
    import cv2
    
    from skimage import data
    
    image = cv2.imread('/content/drive/MyDrive/Covid_data/Training Set/covid/covid/ct_scan_100/22.jpg')
    
    fig, ax = plt.subplots(figsize=(7, 7))
    ax.imshow(image, cmap=plt.cm.gray, interpolation='nearest')
    ax.axis('off')
    ax.set_title('Original Image')
    
    # Image in grayscale
    image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    
    # Blurring the image [img]
    img = cv2.GaussianBlur(image, (5, 5), 0)
    img = cv2.erode(img, None, iterations=2)
    img = cv2.dilate(img, None, iterations=2)
    #img = image
    elevation_map = sobel(img)
    
    fig, ax = plt.subplots(figsize=(7, 7))
    ax.imshow(elevation_map, cmap=plt.cm.gray, interpolation='nearest')
    ax.axis('off')
    ax.set_title('elevation_map')
    
    # we find markers of the background and the coins based on the extreme parts of the histogram of grey values
    markers = np.zeros_like(image)
    # Choosing extreme parts of the histogram of grey values
    markers[image < 70] = 1
    markers[image > 254] = 2
    
    #we use the watershed transform to fill regions of the elevation map starting from the markers determined above:
    segmentation = skimage.morphology.watershed(elevation_map, markers)
    
    fig, ax = plt.subplots(figsize=(7, 7))
    ax.imshow(segmentation, cmap=plt.cm.gray, interpolation='nearest')
    ax.axis('off')
    ax.set_title('segmentation')
    
    
    
    #gray = cv2.imread('segmentation.png', cv2.IMREAD_GRAYSCALE)  # Read image as grayscale
    # Convert segmentation to uint8 image, where 255 is white and 0 is black (OpenCV style mask).
    ret, gray = ret, gray = cv2.threshold(segmentation.astype(np.uint8), 1, 255, cv2.THRESH_BINARY)
    
    for x in range(gray.shape[1]):
        # Fill dark top pixels:
        if gray[0, x] == 0:
            cv2.floodFill(gray, None, seedPoint=(x, 0), newVal=255, loDiff=3, upDiff=3)  # Fill the background with white color
    
        # Fill dark bottom pixels:
        if gray[-1, x] == 0:
            cv2.floodFill(gray, None, seedPoint=(x, gray.shape[0]-1), newVal=255, loDiff=3, upDiff=3)  # Fill the background with white color
    
    for y in range(gray.shape[0]):
        # Fill dark left side pixels:
        if gray[y, 0] == 0:
            cv2.floodFill(gray, None, seedPoint=(0, y), newVal=255, loDiff=3, upDiff=3)  # Fill the background with white color
    
        # Fill dark right side pixels:
        if gray[y, -1] == 0:
            cv2.floodFill(gray, None, seedPoint=(gray.shape[1]-1, y), newVal=255, loDiff=3, upDiff=3)  # Fill the background with white color
    
    fig, ax = plt.subplots(figsize=(7, 7))
    ax.imshow(gray, cmap=plt.cm.gray, interpolation='nearest')
    ax.axis('off')
    ax.set_title('gray')
    

    Using: ret, gray = cv2.threshold(segmentation.astype(np.uint8), 1, 255, cv2.THRESH_BINARY)
    Replaces all the white pixels in segmentation with 255 and all the black pixels with 0.