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()
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()
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
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.
@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)
@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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With