eddex
eddex

Reputation: 2152

Numpy where() creates spots of different color than the ones defined

I'm writing a script that creates a mask for an image. My input image looks like this:

input image

The original image is only 40x40px, here it is for reference: enter image description here

I want to create a mask of the purple area in the center of the image. This is what I do:

# read the 40x40 image and convert it to RGB
input_image = cv2.cvtColor(cv2.imread('image.png'), cv2.COLOR_BGR2RGB)
# get the value of the color in the center of the image
center_color = input_image[20, 20]
# create the mask: pixels with same color = 255 (white), other pixels = 0 (black)
mask_bw = np.where(input_image == center_color, 255, 0)
# show the image
plt.imshow(mask_bw)

Most of the time this works perfectly fine, but for some images (like the one I attached to this question) I consistently get some blue areas in my mask like on the image below. This is reproducible and the areas are always the same for the same input images.

mask with blue areas

This is already weird enough, but if I try to remove the blue areas, this doesn't work either.

mask_bw[mask_bw != (255, 255, 255)] = 0 # this doesn't change anything..

Why is this happening and how do I fix this?

Additional info

Upvotes: 3

Views: 5594

Answers (2)

Mark Setchell
Mark Setchell

Reputation: 207678

If you have a 3-channel image (i.e. RGB or BGR or somesuch) and you want to generate a single channel mask (i.e. you want 0/1 or True/False) for each pixel, then you effectively need to group the 3 values into a single using np.all() like this:

import cv2 
import numpy as np

# Load image and get centre colour
image = cv2.imread('40x40.png') 
cc = im[20, 20] 

print(image.shape)                                                                                
(40, 40, 3)

# Generate list of unique colours present in image so we know what we are dealing with
print(np.unique(im.reshape(-1,3), axis=0))

array([[140, 109, 142],
       [151, 106, 140],
       [160, 101, 137],
       [165, 134, 157],
       [175, 149, 171],
       [206,  87, 109],
       [206, 185, 193]], dtype=uint8)

# Generate mask of pixels matching centre colour
mask_bw = np.where(np.all(im==cc,axis=2), 255, 0)

# Check shape of mask - no 3rd dimension !!!
print(mask_bw.shape)

(40, 40)

# Check unique colours in mask
print(np.unique(mask_bw.reshape(-1,1), axis=0))

array([[  0],
       [255]])

Upvotes: 2

nathancy
nathancy

Reputation: 46630

The main problem is that you're trying to compare three channels but only setting the value for one channel. This is most likely causing the blue areas on the mask. When you use np.where() to set the other pixels to black, you are only setting this on the 1st channel instead of all three channels. You can visualize this by splitting each channel and printing the before/after arrays which will show you that the resulting array values are RGB(0,0,255). So to fix this problem, we need to compare each individual channel then set the desired area in white while setting any black areas on the mask to black for all three channels. Here is one way to do it:

enter image description here

import numpy as np
import cv2

image = cv2.imread('1.png')
center_color = image[20, 20]

b, g, r = cv2.split(image)
mask = (b == center_color[0]) & (g == center_color[1]) & (r == center_color[2])
image[mask] = 255 
image[mask==0] = 0

cv2.imshow('image', image)
cv2.waitKey()

A hotfix to remove the blue areas using your current code would be to convert the image to grayscale (1-channel) then change all non-white pixels to black.

import numpy as np
import cv2

# Load image, find color, create mask
image = cv2.imread('1.png')
center_color = image[20, 20]
mask = np.where(image == center_color, 255, 0)
mask = np.array(mask, dtype=np.uint8)

# Convert image to grayscale, convert all non-white pixels to black
mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
mask[mask != 255] = 0

cv2.imshow('mask', mask)
cv2.waitKey()

Here are two alternative methods to obtain a mask of the purple area

Method #1: Work in grayscale space

import numpy as np
import cv2

image = cv2.imread('1.png')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
center_color = gray[20, 20]

mask = np.array(np.where(gray == center_color, 255, 0), dtype=np.uint8)

cv2.imshow('mask', mask)
cv2.waitKey()

Method #2: Color thresholding

The idea is to convert the image to HSV color space then use a lower and upper color range to segment the image to create a binary mask

import numpy as np
import cv2

image = cv2.imread('1.png')
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
lower = np.array([0, 124, 0])
upper = np.array([179, 255, 255])
mask = cv2.inRange(hsv, lower, upper)

cv2.imshow('mask', mask)
cv2.waitKey()

Both methods should yield the same result

Upvotes: 3

Related Questions