64

Image_1

I want to remove the background of this image to get the person only. I have thousand of images like this, basically, a person and a somewhat whitish background.

What I have done is to use edge detector like canny edge detector or sobel filter (from skimage library). Then what I think possible to do is, whiten the pixels within the edges and blacken the pixels without. Afterwards, the original image can be mask to get the picture of the person only.

However, it's hard to get a closed boundary using canny edge detector. Result using Sobel filter is not that bad, however I don't how to proceed from there.

Sobel_result

EDIT:

Is it possible to also remove the background between the right hand and the skirt and between hairs?

hans-t
  • 3,093
  • 8
  • 33
  • 39
  • What I have done is to use edge detector like canny edge detector or sobel filter (from skimage library). - can you maybe show that code? That's so cool ! – Tzvi Gregory Kaidanov Oct 11 '17 at 20:07
  • did you use this one? http://scikit-image.org/docs/dev/auto_examples/features_detection/plot_hog.html#sphx-glr-auto-examples-features-detection-plot-hog-py – Tzvi Gregory Kaidanov Oct 11 '17 at 20:08

6 Answers6

95

The following code should get you started. You may want to play around with the parameters at the top of the program to fine-tune your extraction:

import cv2
import numpy as np

#== Parameters =======================================================================
BLUR = 21
CANNY_THRESH_1 = 10
CANNY_THRESH_2 = 200
MASK_DILATE_ITER = 10
MASK_ERODE_ITER = 10
MASK_COLOR = (0.0,0.0,1.0) # In BGR format


#== Processing =======================================================================

#-- Read image -----------------------------------------------------------------------
img = cv2.imread('C:/Temp/person.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

#-- Edge detection -------------------------------------------------------------------
edges = cv2.Canny(gray, CANNY_THRESH_1, CANNY_THRESH_2)
edges = cv2.dilate(edges, None)
edges = cv2.erode(edges, None)

#-- Find contours in edges, sort by area ---------------------------------------------
contour_info = []
_, contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
# Previously, for a previous version of cv2, this line was: 
#  contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
# Thanks to notes from commenters, I've updated the code but left this note
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]

#-- Create empty mask, draw filled polygon on it corresponding to largest contour ----
# Mask is black, polygon is white
mask = np.zeros(edges.shape)
cv2.fillConvexPoly(mask, max_contour[0], (255))

#-- Smooth mask, then blur it --------------------------------------------------------
mask = cv2.dilate(mask, None, iterations=MASK_DILATE_ITER)
mask = cv2.erode(mask, None, iterations=MASK_ERODE_ITER)
mask = cv2.GaussianBlur(mask, (BLUR, BLUR), 0)
mask_stack = np.dstack([mask]*3)    # Create 3-channel alpha mask

#-- Blend masked img into MASK_COLOR background --------------------------------------
mask_stack  = mask_stack.astype('float32') / 255.0          # Use float matrices, 
img         = img.astype('float32') / 255.0                 #  for easy blending

masked = (mask_stack * img) + ((1-mask_stack) * MASK_COLOR) # Blend
masked = (masked * 255).astype('uint8')                     # Convert back to 8-bit 

cv2.imshow('img', masked)                                   # Display
cv2.waitKey()

#cv2.imwrite('C:/Temp/person-masked.jpg', masked)           # Save

Ouput: enter image description here

jedwards
  • 29,432
  • 3
  • 65
  • 92
  • 1
    I can't believe how good the result is. Do you care to explain what you've done? – hans-t Mar 28 '15 at 06:53
  • 1
    @user74158 I added some short comments about each main "step" of the processing -- it might be helpful to display the intermediary images (e.g. `edges`, `mask`) to see whats going on. The opencv documentation is quite good about the theory behind the different functions -- it's a bit messy in terms of example code, however, since it supports so many different bindings. I'm far from an image processing expert, but If you have any questions, I can do my best to answer them. – jedwards Mar 28 '15 at 17:54
  • Sure, how do I contact you? – hans-t Mar 29 '15 at 00:58
  • 1
    @user74158 just throw a comment on here, I'm on quite often (see!) – jedwards Mar 29 '15 at 00:59
  • How would you make it transparent instead of red? – Petah Dec 08 '16 at 08:44
  • It could be done adding alpha-channel to image - pls take a look on my answer below. – Eugene Lisitsky Dec 08 '16 at 21:09
  • 10
    If you are using current OpenCV's master branch then the above py code gives an error on line 25 stating 'a ValueError : too many values to unpack' Change line 25 to _, contours, _= cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) for it too work. – Tikkaty Oct 14 '17 at 05:12
  • Great stuff. I was wondering about the inside of hair and arm? I'm not image expert, so just reading out of interest. –  Apr 14 '19 at 19:55
  • 1
    I got this error with the current version of python2. `_, contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) ValueError: need more than 2 values to unpack`. How to fix the problem? – user1424739 Sep 14 '19 at 01:51
  • Im kinda new to open cv. How those `parameters` values are chosen ? Is it random or chosen specific to this image? – heisenbug47 Mar 05 '20 at 06:01
  • +0. +1 for one of the best documented code i've seen on SO or elsewhere, and for adding the parameters to change at the top, but -1 because it just didn't work for my use case. I wanted to basically separate the foreground from background in cases where people were posed against any color or hue wall. This unfortunatley did not work in many of these generic cases, however I do appreciate the effeort and gave me some other ideas on what to try. This approach of contouring (among other things) did not do it for me however. – rv.kvetch Sep 09 '21 at 01:01
40

If you wish to fill background not with a red color but make it transparent, you may add following lines to solution:

# split image into channels
c_red, c_green, c_blue = cv2.split(img)

# merge with mask got on one of a previous steps
img_a = cv2.merge((c_red, c_green, c_blue, mask.astype('float32') / 255.0))

# show on screen (optional in jupiter)
%matplotlib inline
plt.imshow(img_a)
plt.show()

# save to disk
cv2.imwrite('girl_1.png', img_a*255)

# or the same using plt
plt.imsave('girl_2.png', img_a)

If you wish you may tweak some png compression parameters to make file smaller.

Image on a white background below. Or on a black one - https://i.stack.imgur.com/wGGaP.jpg

enter image description here

Eugene Lisitsky
  • 12,113
  • 5
  • 38
  • 59
16

As an alternative, you can use neural networks like this one: CRFRNN.

It gives the result like this:

enter image description here

Andrey Smorodov
  • 10,649
  • 2
  • 35
  • 42
  • 1
    Link in the post is broken, I found it on github, hope this is the one you meant? https://github.com/torrvision/crfasrnn – Raymond Dec 21 '21 at 23:11
5

enter image description hereWorking example with vs2017.
Sets the red background but saves blue..
Also added the transperent example in.

How can I remove the girls body and leave only the dress in the picture? Any ideas?

# == https://stackoverflow.com/questions/29313667/how-do-i-remove-the-background-from-this-kind-of-image

import cv2
import numpy as np
from matplotlib import pyplot as plt

#== Parameters =======================================================================
BLUR = 21
CANNY_THRESH_1 = 10
CANNY_THRESH_2 = 200
MASK_DILATE_ITER = 10
MASK_ERODE_ITER = 10
MASK_COLOR = (0.0,0.0,1.0) # In BGR format


#== Processing =======================================================================

#-- Read image -----------------------------------------------------------------------
img = cv2.imread('img/SYxmp.jpg')
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

#-- Edge detection -------------------------------------------------------------------
edges = cv2.Canny(gray, CANNY_THRESH_1, CANNY_THRESH_2)
edges = cv2.dilate(edges, None)
edges = cv2.erode(edges, None)

#-- Find contours in edges, sort by area ---------------------------------------------
contour_info = []
_, contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
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]

#-- Create empty mask, draw filled polygon on it corresponding to largest contour ----
# Mask is black, polygon is white
mask = np.zeros(edges.shape)
cv2.fillConvexPoly(mask, max_contour[0], (255))



#-- Smooth mask, then blur it --------------------------------------------------------
mask = cv2.dilate(mask, None, iterations=MASK_DILATE_ITER)
mask = cv2.erode(mask, None, iterations=MASK_ERODE_ITER)
mask = cv2.GaussianBlur(mask, (BLUR, BLUR), 0)

mask_stack = np.dstack([mask]*3)    # Create 3-channel alpha mask

#-- Blend masked img into MASK_COLOR background --------------------------------------
mask_stack  = mask_stack.astype('float32') / 255.0          # Use float matrices, 
img         = img.astype('float32') / 255.0                 #  for easy blending

masked = (mask_stack * img) + ((1-mask_stack) * MASK_COLOR) # Blend
masked = (masked * 255).astype('uint8')                     # Convert back to 8-bit 

plt.imsave('img/girl_blue.png', masked)
# split image into channels
c_red, c_green, c_blue = cv2.split(img)

# merge with mask got on one of a previous steps
img_a = cv2.merge((c_red, c_green, c_blue, mask.astype('float32') / 255.0))

# show on screen (optional in jupiter)
#%matplotlib inline
plt.imshow(img_a)
plt.show()

# save to disk
cv2.imwrite('img/girl_1.png', img_a*255)

# or the same using plt
plt.imsave('img/girl_2.png', img_a)

cv2.imshow('img', masked)                                   # Displays red, saves blue

cv2.waitKey()
Tzvi Gregory Kaidanov
  • 3,080
  • 3
  • 26
  • 33
  • were you able to achieve "How can I remove the girls body and leave only the dress in the picture? Any ideas?" Am also looking for something like this. Thank You. – ankush reddy Feb 23 '18 at 16:41
  • @Tzvi, your code deletes shadow nicely. I'm removing the shadow from images depicting sofas. The shadow is gone, but a creamy white sofa turns sort of blue-gray. Can this be prevented? – LucSpan Apr 26 '18 at 10:41
5

According to @jedwards answer, when using with opencv4, you will have this error:

Traceback (most recent call last):
  File "save.py", line 26, in <module>
    _, contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
ValueError: not enough values to unpack (expected 3, got 2)

The function cv2.findContours() has been changed to return only the contours and the hierarchy

You should change to this:

contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE)
huy
  • 1,648
  • 3
  • 14
  • 40
3
  • After obtaining your incomplete edges (as you have), you can run a closing morphology (a sequence of dilate and erode) (will have to set size and iterations based on needs/state of edges).

  • Now assuming that you have a constant edge all the way around the subject, use any type of fill algorithm (blob) to combine all points outside the edged object, then take the negative of that to give you the mask of the inside of the object.

approxiblue
  • 6,982
  • 16
  • 51
  • 59
Sneaky Polar Bear
  • 1,611
  • 2
  • 17
  • 29