Get mask of image without using OpenCV

Viewed 636

I'm trying the following to get the mask out of this image, but unfortunately I fail.

import numpy as np
import skimage.color
import skimage.filters
import skimage.io

# get filename, sigma, and threshold value from command line
filename = 'pathToImage'

# read and display the original image
image = skimage.io.imread(fname=filename)
skimage.io.imshow(image)
# blur and grayscale before thresholding
blur = skimage.color.rgb2gray(image)
blur = skimage.filters.gaussian(blur, sigma=2)
# perform inverse binary thresholding
mask = blur < 0.8
# use the mask to select the "interesting" part of the image
sel = np.ones_like(image)
sel[mask] = image[mask]

# display the result
skimage.io.imshow(sel)

How can I obtain the mask?

enter image description here enter image description here

Is there a general approach that would work for this image as well. without custom fine-tuning and changing parameters? enter image description here

3 Answers
  1. Apply high contrast (maximum possible value)

s1

  1. convert to black & white image using high threshold (I've used 250)

s2

  1. min filter (value=8)

s3

  1. max filter (value=8)

s4

Here is how you can get a rough mask using only the skimage library methods:

import numpy as np
from skimage.io import imread, imsave
from skimage.feature import canny
from skimage.color import rgb2gray
from skimage.filters import gaussian
from skimage.morphology import dilation, erosion, selem
from skimage.measure import find_contours
from skimage.draw import polygon

def get_mask(img):
    kernel = selem.rectangle(7, 6)
    dilate = dilation(canny(rgb2gray(img), 0), kernel)
    dilate = dilation(dilate, kernel)
    dilate = dilation(dilate, kernel)
    erode = erosion(dilate, kernel)
    mask = np.zeros_like(erode)
    rr, cc = polygon(*find_contours(erode)[0].T)
    mask[rr, cc] = 1
    return gaussian(mask, 7) > 0.74

def save_img_masked(file):
    img = imread(file)[..., :3]
    mask = get_mask(img)
    result = np.zeros_like(img)
    result[mask] = img[mask]
    imsave("masked_" + file, result)

save_img_masked('belt.png')
save_img_masked('bottle.jpg')

Resulting masked_belt.png:

enter image description here

Resulting masked_bottle.jpg:

enter image description here

One approach uses the fact that the background changes color only very slowly. Here I apply the gradient magnitude to each of the channels and compute the norm of the result, giving me an image highlighting the quicker changes in color. The watershed of this (with sufficient tolerance) should have one or more regions covering the background and touching the image edge. After identifying those regions, and doing a bit of cleanup we get these results (red line is the edge of the mask, overlaid on the input image):

case 1 output

case 2 output

I did have to adjust the tolerance, with a lower tolerance in the first case, more of the shadow is seen as object. I think it should be possible to find a way to set the tolerance based on the statistics of the gradient image, I have not tried.

There are no other parameters to tweak here, the minimum object area, 300, is quite safe; an alternative would be to keep only the one largest object.

This is the code, using DIPlib (disclaimer: I'm an author). out is the mask image, not the outline as displayed above.

import diplib as dip
import numpy as np

# Case 1:
img = dip.ImageRead('Pa9DO.png')
img = img[362:915, 45:877]  # cut out actual image
img = img(slice(0,2))       # remove alpha channel
tol = 7

# Case 2:
#img = dip.ImageRead('jTnVr.jpg')
#tol = 1

# Compute gradient
gm = dip.Norm(dip.GradientMagnitude(img))
# Compute watershed with tolerance
lab = dip.Watershed(gm, connectivity=1, maxDepth=tol, flags={'correct','labels'})
# Identify regions touching the image edge
ll = np.unique(np.concatenate((
      np.unique(lab[:,0]),
      np.unique(lab[:,-1]),
      np.unique(lab[0,:]),
      np.unique(lab[-1,:]))))
# Remove regions touching the image edge
out = dip.Image(lab.Sizes(), dt='BIN')
out.Fill(1)
for l in ll:
   if l != 0:  # label zero is for the watershed lines
      out = out - (lab == l)
# Remove watershed lines
out = dip.Opening(out, dip.SE(3, 'rectangular'))
# Remove small regions
out = dip.AreaOpening(out, filterSize=300)
# Display
dip.Overlay(img, dip.Dilation(out, 3) - out).Show()
Related