Opencv - Apply Mask to a Color Image

OpenCV - Apply mask to a color image

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.

left-color,right-gray

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.

left-mask,right-bitwise_and_with_mask

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.

left-mask-3channel,right-bitwise_and_with_3channel-mask

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.

OpenCV - Apply color mask to color image

I think you misunderstood the properties of bitwise OR operation. The cv2.bitwise_or takes two source images plus an optional mask.

cv2.bitwise_or(src1, src2, dst, mask)

So if src1 has a pixel with value 1 and src2 has a pixel with value 2, then src1 | src2 is:

0001 = src1
0010 = src2
0011 = src1 | src2

which makes the resultant pixel value 3. For 8-bit unsigned char images, the maximum resultant pixel value will be 255 (135 | 235 is 255).

Adding a color to an image under a mask with OpenCV (cv.add)

You can do it just using Numpy:

import numpy as np 
import cv2 as cv

img = np.array([[[0, 0, 0], [0,0,0]]], dtype=np.uint8)
color = np.array([ 30, 50, 50], dtype=np.uint8)
mask = np.array([[255, 0]], dtype=np.uint8)

# Set pixels in "img" to "color" wherever "mask" > 0
img[mask>0] = color

Result

array([[[30, 50, 50],
[ 0, 0, 0]]], dtype=uint8)

How to Apply Mask to Image in OpenCV?

You don't apply a binary mask to an image. You (optionally) use a binary mask in a processing function call to tell the function which pixels of the image you want to process. If I'm completely misinterpreting your question, you should add more detail to clarify.

Good technique for color masks with opencv

Your first piece of code selects for exactly 0 degrees of hue. That stop sign will probably not have exactly that hue. Hence, not selected.

Your second piece of code gives some range of hues. That's why it can catch the stop sign.

The second piece of code has to split the hue range up into two segments because the desired hue goes from negative to positive (0 +- 20 degrees). cv.inRange has no logic that would allow you to express that in a single range. Hence two pieces, or-ed together (addition).

How to change the colour of an image using a mask?

Since they both have the same shape, you can mask the image of the face using mask image. We first need to perform binary thresholding on it, so it can be used as a b&w mask. Then we can perform boolean indexing based on whether a value is 0 or 255, and assign a new color, such as green?

import cv2
mask = cv2.imread('eBB2Q.jpg')
face = cv2.imread('luraB.jpg')

_, mask = cv2.threshold(mask, thresh=180, maxval=255, type=cv2.THRESH_BINARY)
# copy where we'll assign the new values
green_hair = np.copy(face)
# boolean indexing and assignment based on mask
green_hair[(mask==255).all(-1)] = [0,255,0]

fig, ax = plt.subplots(1,2,figsize=(12,6))
ax[0].imshow(cv2.cvtColor(face, cv2.COLOR_BGR2RGB))
ax[1].imshow(cv2.cvtColor(green_hair, cv2.COLOR_BGR2RGB))

Sample Image

Now we can combine the new image with the original using cv2.addWeighted, which will return the weighted sum of both images, hence we'll only see a difference on the masked region:

green_hair_w = cv2.addWeighted(green_hair, 0.3, face, 0.7, 0, green_hair)

fig, ax = plt.subplots(1,2,figsize=(12,6))
ax[0].imshow(cv2.cvtColor(face, cv2.COLOR_BGR2RGB))
ax[1].imshow(cv2.cvtColor(green_hair_w, cv2.COLOR_BGR2RGB))

Sample Image

Note that you can set the weights in the weighted sum via the alpha and beta parameters, depending on how much you want the new colour to predominate. Note that, as mentioned earlier the new image will be obtained from the weighted sum dst = src1*alpha + src2*beta + gamma. Let's try with another colour and setting the weights as a convex combination with alpha values ranging from say 0.5 and 0.9:

green_hair = np.copy(face)
# boolean indexing and assignment based on mask
green_hair[(mask==255).all(-1)] = [0,0,255]
fig, axes = plt.subplots(2,2,figsize=(8,8))
for ax, alpha in zip(axes.flatten(), np.linspace(.6, .95, 4)):
green_hair_w = cv2.addWeighted(green_hair, 1-alpha, face, alpha, 0, green_hair_w)
ax.imshow(cv2.cvtColor(green_hair_w, cv2.COLOR_BGR2RGB))
ax.axis('off')

Sample Image

How to properly insert mask into original image?

So the issue is how to use masking. There are two options, numpy and OpenCV.

numpy

Since you copied the noisy area into the result and now want to restore everything outside of the circle, I'll use mask == 0 to get a boolean array that is true everywhere outside of the circle.

mask ~mask

With numpy, boolean arrays can be used as indices. The result is a "view", it behaves like a slice. Operations through it affect the original data.

noised = add_noise(img[y:y+h,x:x+w])
new = img.copy()
new[y:y+h,x:x+w] = noised # whole rectangle affected

new[mask == 0] = img[mask == 0] # restore everything outside of the circle

All three arrays (mask, new, img) need to have the same shape.

OpenCV, masks

Not much point to it with Python and numpy available, but many of its C++ APIs take an optional mask argument that modifies that function's operation. Mat::copyTo() is one such method.

OpenCV, bitwise operations

With bitwise operations, the mask would no longer just label each pixel as true or false, but it would have to be 3-channels and all eight bits of every value count, so it must contain only 0 and 255 (0xFF).

I'll erase everything outside of the circle first, then add back the part of the source that is outside of the circle. Both operations use bitwise_and. Two operations are required because bitwise operations can't just "overwrite". They react to both operands. I'll also use numpy's ~ operator to bitwise-negate the mask.

bitwise_mask = cv.cvtColor(mask, cv.COLOR_GRAY2BGR) # blow it up
new = cv.bitwise_and(new, bitwise_mask)
new += cv.bitwise_and(img, ~bitwise_mask)

Sample Image Sample Image



Related Topics



Leave a reply



Submit