How can I apply mask to a color image in latest python binding (cv2)? In previous python binding the simplest way was to use cv.Copy e.g.
cv.Copy(dst, src, mask)
But this function is not available in cv2 binding. Is there any workaround without using boilerplate code?
Here, you could use cv2.bitwise_and function if you already have the mask image.
For check the below code:
img = cv2.imread('lena.jpg')
mask = cv2.imread('mask.png',0)
res = cv2.bitwise_and(img,img,mask = mask)
The output will be as follows for a lena image, and for rectangular mask.
Well, here is a solution if you want the background to be other than a solid black color. We only need to invert the mask and apply it in a background image of the same size and then combine both background and foreground. A pro of this solution is that the background could be anything (even other image).
This example is modified from Hough Circle Transform. First image is the OpenCV logo, second the original mask, third the background + foreground combined.
# http://opencv-python-tutroals.readthedocs.io/en/latest/py_tutorials/py_imgproc/py_houghcircles/py_houghcircles.html
import cv2
import numpy as np
# load the image
img = cv2.imread('E:\\FOTOS\\opencv\\opencv_logo.png')
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# detect circles
gray = cv2.medianBlur(cv2.cvtColor(img, cv2.COLOR_RGB2GRAY), 5)
circles = cv2.HoughCircles(gray, cv2.HOUGH_GRADIENT, 1, 20, param1=50, param2=50, minRadius=0, maxRadius=0)
circles = np.uint16(np.around(circles))
# draw mask
mask = np.full((img.shape[0], img.shape[1]), 0, dtype=np.uint8) # mask is only
for i in circles[0, :]:
cv2.circle(mask, (i[0], i[1]), i[2], (255, 255, 255), -1)
# get first masked value (foreground)
fg = cv2.bitwise_or(img, img, mask=mask)
# get second masked value (background) mask must be inverted
mask = cv2.bitwise_not(mask)
background = np.full(img.shape, 255, dtype=np.uint8)
bk = cv2.bitwise_or(background, background, mask=mask)
# combine foreground+background
final = cv2.bitwise_or(fg, bk)
Note: It is better to use the opencv methods because they are optimized.
import cv2 as cv
im_color = cv.imread("lena.png", cv.IMREAD_COLOR)
im_gray = cv.cvtColor(im_color, cv.COLOR_BGR2GRAY)
At this point you have a color and a gray image. We are dealing with 8-bit, uint8 images here. That means the images can have pixel values in the range of [0, 255] and the values have to be integers.
Let's do a binary thresholding operation. It creates a black and white masked image. The black regions have value 0 and the white regions 255
_, mask = cv.threshold(im_gray, thresh=180, maxval=255, type=cv.THRESH_BINARY)
im_thresh_gray = cv.bitwise_and(im_gray, mask)
The mask can be seen below on the left. The image on its right is the result of applying bitwise_and operation between the gray image and the mask. What happened is, the spatial locations where the mask had a pixel value zero (black), became pixel value zero in the result image. The locations where the mask had pixel value 255 (white), the resulting image retained its original gray value.
To apply this mask to our original color image, we need to convert the mask into a 3 channel image as the original color image is a 3 channel image.
mask3 = cv.cvtColor(mask, cv.COLOR_GRAY2BGR) # 3 channel mask
Then, we can apply this 3 channel mask to our color image using the same bitwise_and function.
im_thresh_color = cv.bitwise_and(im_color, mask3)
mask3 from the code is the image below on the left, and im_thresh_color is on its right.
You can plot the results and see for yourself.
cv.imshow("original image", im_color)
cv.imshow("binary mask", mask)
cv.imshow("3 channel mask", mask3)
cv.imshow("im_thresh_gray", im_thresh_gray)
cv.imshow("im_thresh_color", im_thresh_color)
cv.waitKey(0)
The original image is lenacolor.png that I found here.
Answer given by Abid Rahman K is not completely correct. I also tried it and found very helpful but got stuck.
This is how I copy image with a given mask.
x, y = np.where(mask!=0)
pts = zip(x, y)
# Assuming dst and src are of same sizes
for pt in pts:
dst[pt] = src[pt]
This is a bit slow but gives correct results.
EDIT:
Pythonic way.
idx = (mask!=0)
dst[idx] = src[idx]
The other methods described assume a binary mask. If you want to use a real-valued single-channel grayscale image as a mask (e.g. from an alpha channel), you can expand it to three channels and then use it for interpolation:
assert len(mask.shape) == 2 and issubclass(mask.dtype.type, np.floating)
assert len(foreground_rgb.shape) == 3
assert len(background_rgb.shape) == 3
alpha3 = np.stack([mask]*3, axis=2)
blended = alpha3 * foreground_rgb + (1. - alpha3) * background_rgb
Note that mask needs to be in range 0..1 for the operation to succeed. It is also assumed that 1.0 encodes keeping the foreground only, while 0.0 means keeping only the background.
If the mask may have the shape (h, w, 1), this helps:
alpha3 = np.squeeze(np.stack([np.atleast_3d(mask)]*3, axis=2))
Here np.atleast_3d(mask) makes the mask (h, w, 1) if it is (h, w) and np.squeeze(...) reshapes the result from (h, w, 3, 1) to (h, w, 3).
Related
all of you,
Here is the image;
The exact background color is in RGB; (246, 46, 100)
I have tried several methods but those are too slow, one of the methods is below;
new_image = Image.open("image-from-rawpixel-id-6649116-original.png")
img_up = np.asarray(new_image)
for ind1, i in enumerate(tqdm(img_up)):
for ind2, i2 in enumerate(i):
if list(i2[:3]) != a:
img_up2 = img_up.copy()
img_up2.setflags(write=1)
img_up2[ind1][ind2][:3] = [0,0,0]
cv2.imshow('', img_up2)
cv2.waitKey()
I want to make the background white and the foreground person black (masked), but unable to find a quick method.
Modified
I have tied another method to mask the foreground but I think, I am doing some mistakes while converting between RGBs. Below is the code;
path = 'image-from-rawpixel-id-2923073-png.png'
im = Image.open(path).convert('RGBA')
img = cv2.imread(path, cv2.IMREAD_UNCHANGED)
#--------------------------- To change the background color that is not present in the foreground ---------------------------------------------
lst_ch_a = []
for pixel in tqdm(im.getdata()):
lst_ch_a.append(pixel[:3])
break_out = True
while break_out:
a = random.sample(range(0, 255), 3)
if a not in lst_ch_a:
new_image = Image.new("RGBA", im.size, tuple(a))
print(tuple(a))
break_out = False
new_image.paste(im, mask=im)
new_image.convert("RGB").save("check6.jpg")
#--------------------------- Numpy ----------------------------------------------------------------
img_up = np.asarray(new_image)
img = img_up.copy()
img.setflags(write=1)
img[:,:,:3][img[:,:,:3] != tuple(a)] = 0
img[img!=0]=255
img = cv2.cvtColor(img, cv2.COLOR_BGRA2RGBA)
img = cv2.resize(img,(500,500), cv2.INTER_LINEAR)
cv2.imwrite('results1.jpg', img)
cv2.imshow('', img)
cv2.waitKey(0)
Below is the result, but I am getting pixels of Blue, green, red, and some other colors in the image. do you know why I am getting this?
You can see in the above first image, where I have changed the background. The image was transparent image, but then I changed the background color. There was no green, blue, or red colors but while masking the foreground the red, blue, and green color emerges.
Do you know why it is happening?
First you could read image using cv2.imread() and you get directly numpy.array.
You can use numpy image[ mask ] = [0,0,0] to assign value to many pixels in milliseconds.
For exact color you can create mask using img == (100, 46, 247).
cv2 keeps image as BGR instead of RGB so it needs (100,46,247) instead of (247,46,100).
It needs also .all(axis=-1) because it compares every value B,G,R separatelly and gets tuples (True, True, False) but it needs to reduce it to single True when all values are True.
import cv2
img = cv2.imread("image.png")
#print('color:', img[0, 0]) # [100 46 247]
mask = (img == (100, 46, 247)).all(axis=-1)
img1 = img.copy()
img2 = img.copy()
img1[ mask ] = [0,0,0]
img2[ ~mask ] = [0,0,0]
cv2.imshow('image1', img1)
cv2.imshow('image2', img2)
cv2.waitKey()
cv2.destroyAllWindows()
cv2.imwrite('image2-1.png', img1)
cv2.imwrite('image2-2.png', img2)
Result:
image1:
image2:
BTW:
cv2 has function inRange() to select colors in some ranges and it may give better result.
Example code but I didn't find good range for this image.
Besides it starts removing similar pixels in lips.
import cv2
import numpy as np
img = cv2.imread("image.jpg")
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
#print('hsv:', hsv[0, 0])
lower = np.array([92, 120, 147])
upper = np.array([172, 208, 247]) # for BGR (100, 46, 247) - value from hsv[0,0]
upper = np.array([202, 218, 247])
mask = cv2.inRange(hsv, lower, upper)
print('masking')
# `mask==255` `mask==0`
#img = cv2.bitwise_and(img, img, mask=~mask) # OK
#img[np.where(mask==255)] = [0,0,0] # OK
img[ mask==255 ] = [0,0,0] # OK
#img[ mask.astype(bool) ] = [0,0,0] # OK
#img[ mask ] = [0,0,0] # WRONG (hangs)
print('display')
#h, w = img.shape[:2]
#img = cv2.resize(img, (h//5, w//5))
cv2.imshow('image', img)
cv2.waitKey()
cv2.destroyAllWindows()
cv2.imwrite('image2b.jpg', img)
See also: Finding red color in image using Python & OpenCV - Stack Overflow
You should be able to do this more effectively with PIL.ImageDraw.floodfill() which is described here.
If you use a global replacement, you will catch reddish tones (such as the model's lips) anywhere and everywhere they occur in the image. If you use a floodfill from the top-left corner with a suitable tolerance, the flooding should get stopped by her dark hair before it can contaminate her lips.
The result should be like this:
I actually did that with ImageMagick equivalent operator in Terminal as I don't currently have Python to hand:
magick IXVJl.jpg -fuzz 30% -fill white -draw "color 0,0 floodfill" result.jpg
I was wondering if there was a way in openCV that would allow me to crop image between two circles, in a way that ignores everything in the smaller inner circle and everything outside of the larger circle. Like a donut shape.
Here is one way to do that in Python/OpenCV.
Read the input and get its dimensions
Define the radii of the two circles and the center coordinates
Create a white filled circle mask on a black background for each radius
Subtract the smaller radius mask from the larger radius mask
Put the resulting mask image into the alpha channel of the input
save the results
Input:
import cv2
import numpy as np
# read image
img = cv2.imread('lena.jpg')
hh, ww = img.shape[:2]
hh2 = hh // 2
ww2 = ww // 2
# define circles
radius1 = 25
radius2 = 75
xc = hh // 2
yc = ww // 2
# draw filled circles in white on black background as masks
mask1 = np.zeros_like(img)
mask1 = cv2.circle(mask1, (xc,yc), radius1, (255,255,255), -1)
mask2 = np.zeros_like(img)
mask2 = cv2.circle(mask2, (xc,yc), radius2, (255,255,255), -1)
# subtract masks and make into single channel
mask = cv2.subtract(mask2, mask1)
# put mask into alpha channel of input
result = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
result[:, :, 3] = mask[:,:,0]
# save results
cv2.imwrite('lena_mask1.png', mask1)
cv2.imwrite('lena_mask2.png', mask2)
cv2.imwrite('lena_masks.png', mask)
cv2.imwrite('lena_circle_masks.png', result)
cv2.imshow('image', img)
cv2.imshow('mask1', mask1)
cv2.imshow('mask2', mask2)
cv2.imshow('mask', mask)
cv2.imshow('masked image', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
Smaller radius mask:
Larger radius mask:
Difference mask:
Resulting image:
You should use masks. Create an empty image of the same size, draw the larger circle with value '1', then the smaller with value '0', you'll get a "donut", then use that donut as a mask to copy the part of your image you're interested in.
This might help as a tutorial:
https://note.nkmk.me/en/python-opencv-numpy-alpha-blend-mask/
I am trying to get the outline of an album cover and edge detectors (Canny, Laplace) pick up too much noise. I don't fully understand how image masking works and would like put a white mask over the image so I see only the black pixels
I have applied a GaussianBlur 5x5 and converted the image to hsv values. I have a range of values which are black, and I have filtered these out.
# imported image and processing (shorthand here)
image = cv2.imread(args["image"])
blur = cv2.GaussianBlur(image, (5,5), 0)
blur_hsv = cv2.cvtColor(blur, cv2.COLOR_BGR2HSV)
# set regions of color
boundaries = [
# black
([0,0,0],[180, 255, 40])
#pink
#([151, 80, 50], [174, 255, 255])
]
# loop over the boundaries
for (lower, upper) in boundaries:
# create NumPy arrays from the boundaries
lower = np.array(lower, dtype = "uint8")
upper = np.array(upper, dtype = "uint8")
# find the colors within the specified boundaries and apply
mask = cv2.inRange(blur_hsv, lower, upper)
output = cv2.bitwise_and(image, image, mask = mask)
# show the images
cv2.imshow("images", np.hstack([image, output]))
I was hoping for some distinction in the final output, but the window is just black. How can I create a different color mask?
Edit:
Not the exact image, but a sample LEFT: original; RIGHT: processed
From my understanding, you want to obtain a mask where all colored pixels (non-black) are white. When we use cv2.inRange(), we give it a lower/upper threshold that returns all pixels within the bounds in white. Then when we use cv2.bitwise_and() with the mask and the original image, the resulting image will be the areas where both the mask and original image are white. Essentially any pixels in white are the areas that we want to keep.
Your current output shows all areas in the original picture that have pixels between the lower/upper threshold. But if your goal is to display all non-black pixels, then you can simply invert the mask. Here's a visualization:
Here's your current mask and represents all pixels within the thresholds in the original image as white.
We can simply invert the mask or use cv2.bitwise_not() to get your desired mask. This new mask represents all colored pixels not within your lower/upper threshold as white. Therefore this mask is all the colored pixels.
final_mask = 255 - mask
Remember, any pixels that we want to keep, we should make it white whereas any pixels we want to throw away, we make it black. So if we cv2.bitwise_and() this new mask with the original image, we get this
Here's a good tutorial on bitwise operations and masking
import cv2
import numpy as np
image = cv2.imread('1.png')
blur = cv2.GaussianBlur(image, (5,5), 0)
blur_hsv = cv2.cvtColor(blur, cv2.COLOR_BGR2HSV)
# create NumPy arrays from the boundaries
lower = np.array([0,0,0], dtype = "uint8")
upper = np.array([180,255,40], dtype = "uint8")
# find the colors within the specified boundaries and apply
mask = cv2.inRange(blur_hsv, lower, upper)
mask = 255 - mask
output = cv2.bitwise_and(image, image, mask = mask)
# show the images
cv2.imshow("output", output)
cv2.imshow("mask", mask)
cv2.waitKey()
I'm trying to remove the background of an image using a mask where the alpha value of a pixel is proportional to the black intensity. For instance, given the following input image and mask, the result contains "faded" areas:
Result:
Note the faded areas. Basically I'm trying to imitate the layer mask function in Photoshop.
I'm able to turn the mask into alpha using binary threshold, but I wonder how to make the alpha proportional. The code for binary threshold is as follows:
mask = cv2.imread(mask_path, 0)
mask2 = np.where(mask<50, 0, 1).astype('uint8')
img = img * mask2[:, :, np.newaxis]
_, alpha = cv2.threshold(mask2, 0, 255, cv2.THRESH_BINARY)
png = np.dstack((img, alpha))
cv2.imwrite(dest_path, png)
I suppose it may perhaps be irrelevant as thresholds are probably not needed for layer masking.
I'm not sure if this is what you want, but you can get the proportional effect by subtracting the values of the mask from the image. That means you have to invert the mask, so the amount of alpha you want to remove is white. For subtract(), the input arrays need to have the same size, so convert the inverted mask to 3 color channels. If the size of the mask is not equal to the background image, you'll first have to create a subimage.
import cv2
import numpy as np
# load background image
img = cv2.imread('grass.jpg')
# load alpha mask as grayscale
mask = cv2.imread('a_mask.jpg',0)
# invert mask and convert to 3 color channels
mask = cv2.bitwise_not(mask)
fullmask = cv2.cvtColor(mask,cv2.COLOR_GRAY2BGR)
# create a subimage with the size of the mask
xOffset = 30
yOffset = 30
height, width = mask.shape[:2]
subimg = img[yOffset:yOffset+height,xOffset:xOffset+width]
#subtract mask values from subimage
res = cv2.subtract(subimg,fullmask)
# put subimage back in background
img[yOffset:yOffset+height,xOffset:xOffset+width] = res
#display result
cv2.imshow('Result',img)
cv2.waitKey(0)
cv2.destroyAllWindows()
I have a set of points that make a shape (closed polyline). Now I want to copy/crop all pixels from some image inside this shape, leaving the rest black/transparent. How do I do this?
For example, I have this:
and I want to get this:
*edit - updated to work with images that have an alpha channel.
This worked for me:
Make a mask with all black (all masked)
Fill a polygon with white in the shape of your ROI
combine the mask and your image to get the ROI with black everywhere else
You probably just want to keep the image and mask separate for functions that accept masks. However, I believe this does what you specifically asked for:
import cv2
import numpy as np
# original image
# -1 loads as-is so if it will be 3 or 4 channel as the original
image = cv2.imread('image.png', -1)
# mask defaulting to black for 3-channel and transparent for 4-channel
# (of course replace corners with yours)
mask = np.zeros(image.shape, dtype=np.uint8)
roi_corners = np.array([[(10,10), (300,300), (10,300)]], dtype=np.int32)
# fill the ROI so it doesn't get wiped out when the mask is applied
channel_count = image.shape[2] # i.e. 3 or 4 depending on your image
ignore_mask_color = (255,)*channel_count
cv2.fillPoly(mask, roi_corners, ignore_mask_color)
# from Masterfool: use cv2.fillConvexPoly if you know it's convex
# apply the mask
masked_image = cv2.bitwise_and(image, mask)
# save the result
cv2.imwrite('image_masked.png', masked_image)
The following code would be helpful for cropping the images and get them in a white background.
import cv2
import numpy as np
# load the image
image_path = 'input image path'
image = cv2.imread(image_path)
# create a mask with white pixels
mask = np.ones(image.shape, dtype=np.uint8)
mask.fill(255)
# points to be cropped
roi_corners = np.array([[(0, 300), (1880, 300), (1880, 400), (0, 400)]], dtype=np.int32)
# fill the ROI into the mask
cv2.fillPoly(mask, roi_corners, 0)
# The mask image
cv2.imwrite('image_masked.png', mask)
# applying th mask to original image
masked_image = cv2.bitwise_or(image, mask)
# The resultant image
cv2.imwrite('new_masked_image.png', masked_image)
Input Image:
Mask Image:
Resultant output image: