11

I am trying to remove the background of some images, tweaking some values and using some methods like morphologyEx gives me an aceptable result but some holes still remaining, in this last case, the holes doesn't fill even iterating on every contour and drawing it with -1. I can see that threshold image is really good, making the whole shape with lines, but I don't know how to continue...

Update I've changed my code so I get better results but I'm still getting some holes... If I could fill theese holes, the script would be perfect.

def get_contrasted(image, type="dark", level=3):
    maxIntensity = 255.0 # depends on dtype of image data
    phi = 1
    theta = 1

    if type == "light":
        newImage0 = (maxIntensity/phi)*(image/(maxIntensity/theta))**0.5
        newImage0 = array(newImage0,dtype=uint8)
        return newImage0
    elif type == "dark":
        newImage1 = (maxIntensity/phi)*(image/(maxIntensity/theta))**level
        newImage1 = array(newImage1,dtype=uint8)

        return newImage1

def sharp(image, level=3):
    f = cv2.GaussianBlur(image, (level,level), level)
    f = cv2.addWeighted(image, 1.5, f, -0.5, 0)
    return f

original_image = imread('imagen.jpg')
# 1 Convert to gray & Normalize
gray_img = cv2.cvtColor(original_image, cv2.COLOR_BGR2GRAY)
gray_img = sharp(get_contrasted(gray_img))
gray_img = normalize(gray_img, None, 0, 255, NORM_MINMAX, CV_8UC1)
imshow("Gray", gray_img)

# 2 Find Threshold
gray_blur = cv2.GaussianBlur(gray_img, (7, 7), 0)
adapt_thresh_im = cv2.adaptiveThreshold(gray_blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, 11, 1)
max_thresh, thresh_im = cv2.threshold(gray_img, 0, 255, cv2.THRESH_BINARY_INV | cv2.THRESH_OTSU)
thresh = cv2.bitwise_or(adapt_thresh_im, thresh_im)

# 3 Dilate
gray = cv2.Canny(thresh, 88, 400, apertureSize=3)
gray = cv2.dilate(gray, None, iterations=8)
gray = cv2.erode(gray, None, iterations=8)
imshow("Trheshold", gray)

# 4 Flood
contours, _ = cv2.findContours(gray, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
contour_info = []
for c in contours:
    contour_info.append((
        c,
        cv2.isContourConvex(c),
        cv2.contourArea(c),
    ))
contour_info = sorted(contour_info, key=lambda c: c[2], reverse=True)
max_contour = contour_info[0]
holes = np.zeros(gray_img.shape, np.uint8)
drawContours(holes, max_contour, 0, 255, -1)
imshow("Holes", holes)

mask = cv2.GaussianBlur(holes, (15, 15), 0)
mask = np.dstack([mask] * 3)  # Create 3-channel alpha mask

mask = mask.astype('float32') / 255.0  # Use float matrices,
img = original_image.astype('float32') / 255.0  # for easy blending
masked = (mask * img) + ((1 - mask) * (0,0,1))  # Blend
masked = (masked * 255).astype('uint8')

imshow("Maked", masked)
waitKey()

0 Original

enter image description here

1 Threshold

enter image description here

2 Holes

enter image description here

3 Final Image

enter image description here

Robert W. Hunter
  • 2,895
  • 7
  • 35
  • 73
  • Your code is not a valid Python code. Please post code without syntax errors (namely, a code that is able to run). – boardrider Jun 30 '15 at 10:07
  • Missing parenthesis, updated now. – Robert W. Hunter Jun 30 '15 at 10:08
  • You should look up some opencv tutorials on masking first. Also, look at this: http://stackoverflow.com/questions/18710428/how-to-remove-background-image-with-opencv – bad_keypoints Jun 30 '15 at 10:32
  • I've read some tutorials, and that stackoverflow question too but i can't get it working.. For example some tutorials are on C++ I'm using python and I dont know how to do this in python, for example `Mat(hsvImg.rows, hsvImg.cols, CV_8UC1, 200)` and other things... – Robert W. Hunter Jun 30 '15 at 10:34
  • @RobertW.Hunter then look into [this](http://opencv-python-tutroals.readthedocs.org/en/latest/py_tutorials/py_video/py_bg_subtraction/py_bg_subtraction.html) – bad_keypoints Jun 30 '15 at 10:35
  • Looked at that too, but that is meant to be used with an average or "model" image to substract whatever is not on that "model", so works perfectly for a video where every frame has the same "model" image and then something moves, you can remove that "moving" thing, but not on a static image where you can't tell opencv what is that average thing. – Robert W. Hunter Jun 30 '15 at 10:37
  • Well that *is* the theory of background subtraction. If you'd be able to have the background image or histogram, you'd be able to subtract it. Beyond this I'm sorry I have no sound knowledge to give you advice. But you should look at [this too](https://www.google.co.in/webhp?sourceid=chrome-instant&ion=1&espv=2&ie=UTF-8&client=ubuntu#q=background%20subtraction%20with%20dirichlet%20process%20mixture%20models) – bad_keypoints Jun 30 '15 at 10:44
  • Did you read http://docs.opencv.org/master/d8/d83/tutorial_py_grabcut.html? – boardrider Jun 30 '15 at 11:16
  • Yeah but as pointed in the page, that script somethimes removes stuff and you need to manually add a mask, my script is intended to be automatic so no human interaction at all, I don't want a perfect shape, but the most accurate thing possible. – Robert W. Hunter Jun 30 '15 at 12:33

5 Answers5

11

As I was tackling the same issue, and found a solution in Python (with opencv2), thought of just sharing this here as well. Hope it helps.

import numpy as np
import cv2

cv2.namedWindow('image', cv2.WINDOW_NORMAL)

#Load the Image
imgo = cv2.imread('koAl2.jpg')
height, width = imgo.shape[:2]

#Create a mask holder
mask = np.zeros(imgo.shape[:2],np.uint8)

#Grab Cut the object
bgdModel = np.zeros((1,65),np.float64)
fgdModel = np.zeros((1,65),np.float64)

#Hard Coding the Rect The object must lie within this rect.
rect = (10,10,width-30,height-30)
cv2.grabCut(imgo,mask,rect,bgdModel,fgdModel,5,cv2.GC_INIT_WITH_RECT)
mask = np.where((mask==2)|(mask==0),0,1).astype('uint8')
img1 = imgo*mask[:,:,np.newaxis]

#Get the background
background = imgo - img1

#Change all pixels in the background that are not black to white
background[np.where((background > [0,0,0]).all(axis = 2))] = [255,255,255]

#Add the background and the image
final = background + img1

#To be done - Smoothening the edges

cv2.imshow('image', final )

k = cv2.waitKey(0)

if k==27:
    cv2.destroyAllWindows()
9

Iteratively perform a morphological closing of your holes image using a kernel of increasing size. But, before doing this I suggest you resize the holes image (using nearest-neighbor interpolation) so you don't have to use huge kernels. In the following code (C++), I resized the holes image to 25% of its original dimensions.

To reduce the effects on borders add a constant border of zeros using copyMakeBorder before you apply the iterative closing. As we are using 15 iterations here, make the border around the image larger than 15.

So the steps are

  • Resize the holes image
  • Add a zero border
  • Iteratively close the image with a kernel of increasing size
  • Remove the border
  • Now we have a small mask. Resize this mask to original image size

The code is in C++. I'm not very familiar with python.

    // read the image and the holes
    Mat im = imread("koAl2.jpg");
    Mat holes = imread("GuICX.jpg", 0);
    // resize
    Mat small, bordered;
    resize(holes, small, Size(), .25, .25);
    // add a zero border
    int b = 20;
    copyMakeBorder(small, bordered, b, b, b, b, BORDER_CONSTANT, Scalar(0));
    // close
    for (int i = 1; i < 15; i++)
    {
        Mat kernel = getStructuringElement(MORPH_ELLIPSE, cv::Size(2*i+1, 2*i+1));
        morphologyEx(bordered, bordered, MORPH_CLOSE, kernel, Point(-1, -1), 1);
    }
    // remove border
    Mat mask = bordered(Rect(b, b, small.cols, small.rows));
    // resize the mask
    Mat largeMask;
    resize(mask, largeMask, Size(im.cols, im.rows));
    // the foreground
    Mat fg;
    im.copyTo(fg, largeMask);

The output (not to original scale) looks fine except that it takes the background region at the bottom as foreground.

enter image description here

dhanushka
  • 10,492
  • 2
  • 37
  • 47
  • 1
    I can't get it working on python, there are two things that I can't convert to Python: 1: `bordered` just became a method? When? `bordered(Rect(b, b, small.cols, small.rows))` and what is `Rect` ? 2: `resize(holes, small, Size(), .25, .25)` I haven't a `Size()` object, I imagine that this is a Tuple??, but I can't use an empty tuple `()` 3: `morphologyEx(bordered, bordered, MORPH_CLOSE, kernel, Point(-1, -1), 1)` becomes `bordered = cv2.morphologyEx(bordered, cv2.MORPH_CLOSE, kernel, (-1, -1), 1)` in python but error: `new style getargs format but argument is not a tuple` – Robert W. Hunter Jul 01 '15 at 08:51
  • `Mat mask = bordered(Rect(b, b, small.cols, small.rows));` extracts the specified ROI into mask. It's an operator of Mat class. You'll be able to get this working if you concentrate on the description I've given rather than trying to do a one-to-one mapping of the C++ code to Python. I added the code hoping that it would make the description clearer. – dhanushka Jul 01 '15 at 14:17
4

@dhanushka's method works fine. Here's my pythonic version:

def get_holes(image, thresh):
    gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY)

    im_bw = cv.threshold(gray, thresh, 255, cv.THRESH_BINARY)[1]
    im_bw_inv = cv.bitwise_not(im_bw)

    contour, _ = cv.findContours(im_bw_inv, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE)
    for cnt in contour:
        cv.drawContours(im_bw_inv, [cnt], 0, 255, -1)

    nt = cv.bitwise_not(im_bw)
    im_bw_inv = cv.bitwise_or(im_bw_inv, nt)
    return im_bw_inv


def remove_background(image, thresh, scale_factor=.25, kernel_range=range(1, 15), border=None):
    border = border or kernel_range[-1]

    holes = get_holes(image, thresh)
    small = cv.resize(holes, None, fx=scale_factor, fy=scale_factor)
    bordered = cv.copyMakeBorder(small, border, border, border, border, cv.BORDER_CONSTANT)

    for i in kernel_range:
        kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, (2*i+1, 2*i+1))
        bordered = cv.morphologyEx(bordered, cv.MORPH_CLOSE, kernel)

    unbordered = bordered[border: -border, border: -border]
    mask = cv.resize(unbordered, (image.shape[1], image.shape[0]))
    fg = cv.bitwise_and(image, image, mask=mask)
    return fg


img = cv.imread('koAl2.jpg')
nb_img = remove_background(img, 230)

enter image description here

Alexander Lutsenko
  • 2,130
  • 8
  • 14
  • version of python? how to import cv? – grep Dec 16 '16 at 09:27
  • 1
    ", line 11, in get_holes contour, _ = cv2.findContours(im_bw_inv, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE) ValueError: too many values to unpack – grep Dec 16 '16 at 09:30
2

@grep, according to a post by Alexander Lutsenko, for python 3.6.3, to make the code work, you need to add one more returned value to the findContours() as follows:

contour, _ = cv.findContours(im_bw_inv, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE)

to

_, contour, _ = cv.findContours(im_bw_inv, cv.RETR_CCOMP, cv.CHAIN_APPROX_SIMPLE)
Jeru Luke
  • 20,118
  • 13
  • 80
  • 87
Supp.
  • 21
  • 1
0

try this morphological operation for dilation and erosion removing holes in C++

Mat erodeElement = getStructuringElement(MORPH_RECT, Size(4, 4));
morphologyEx(thresh, thresh, MORPH_CLOSE ,erodeElement);
morphologyEx(thresh, thresh, MORPH_OPEN, erodeElement);
morphologyEx(thresh, thresh, MORPH_CLOSE, erodeElement);
morphologyEx(thresh, thresh, MORPH_OPEN, erodeElement);
morphologyEx(thresh, thresh, MORPH_OPEN, erodeElement);
Mukesh Pareek
  • 33
  • 1
  • 7