# Processing an image to extract green-screen mask

I am using OpenCV to process an image, and in my code, I have to check / edit each pixel separately:

import cv2, numpy

# we just use an empty image for the purpose of this MCVE
width = len(img[0])
height = len(img)

empty_img = numpy.zeros((height, width, 3), numpy.uint8)

i = 0
r = 0
c = 0

for line in img:
c = 0

for pixel in line:
blue = pixel[0]
green = pixel[1]
red = pixel[2]

if green != max(red, green, blue) or green < 35:
# this has a greenishy hue
empty_img.itemset((r, c, 0), 255)

c += 1
r += 1


This code works, but is quite slow (it takes about 23 seconds to process the image). How could/should I speed this up?

source.jpg:

result should be:

There's a simpler way to create the empty image using numpy.zeros_like:

empty_img = numpy.zeros_like(img)


As Austin Hastings correctly pointed out, the trick is to use vectorized operations provided by numpy:

RED, GREEN, BLUE = (2, 1, 0)

reds = img[:, :, RED]
greens = img[:, :, GREEN]
blues = img[:, :, BLUE]


mask = (greens < 35) | (reds > greens) | (blues > greens)


or, using numpy.amax

mask = (greens < 35) | (numpy.amax(img, axis=2) != greens)


Now, one option is to use conditional indexing to modify empty_img. Since it's a 3 channel image (represented as 3 dimensional array), and our mask is only 1 channel (represented as 2 dimensional array) there are two possibilities:

• assign 3-tuples: empty_img[mask] = (255,0,0)
• provide the 3rd index: empty_img[mask,0] = 255

If all you care about is just a single channel mask, then numpy.where is a possibility.

result = numpy.where(mask, 255, 0)


Use conditional indexing:

RED, GREEN, BLUE = (2, 1, 0) # Your numbers

empty_img = numpy.zeros((height, width, 3), numpy.uint8)

reds = img[:, :, RED]
greens = img[:, :, GREEN]
blues = img[:, :, BLUE]

empty_img[(greens < 35) | (reds <= greens >= blues)][BLUE] = 255


Edit:

empty_img[(greens < 35) | ((reds <= greens) & (blues <= greens))][BLUE] = 255


I was wondering if the compound relation would work. Sadly, not. This should. If it doesn't you can try composing it step by step - look at just the greens < 35 case, then the reds <=greens, etc.

• Curses! You'll have to break down the compound <= >= statement. Let me edit. – Austin Hastings Jan 1 '18 at 18:27
• I get some result, but not the same as with my own code. I uploaded source and result images. – Bart Friederichs Jan 1 '18 at 18:42
• @AustinHastings this doesn't seem to work at all. Looks like assignment doesn't work. Doesn't matter what I put in, the image always stays black. – Bart Friederichs Jan 1 '18 at 19:14
• @DanMašek great, thanks! That should be the answer. For my application it doesn't matter that it is blue (in fact white on black is better), so by just writing out the mask I shaved another 0.1s off. – Bart Friederichs Jan 1 '18 at 20:30
• @BartFriederichs No problem. I was just writing it up when you deleted the original on SO :D | There's one more problem with this answer: green != max(red, green, blue) means that (green < blue) or (green < red) – Dan Mašek Jan 1 '18 at 21:16

It seems like assigning a value like in the answer from @austin-hastings doesn't work. I fiddled a bit more and found that just using his conditionals in a mask does work (the boolean is a little different, but that is besides the point):

mask = ((greens < 35) | (reds >= greens) | (blues >= greens)) * 255


this creates a single layer (one color) mask that I can write out to a file:

cv2.imwrite("result.jpg", mmask)


I guess using it as a blue colour is not hard, but not relevant for my application. (In fact, a white mask is even better)

It may be a matter of preference, but considering the logic of identifying the green screen could be a better way of forming a mask. Usually, a green screen has high green values and low red values and low blue values. So, for an openCV BGR image a mask would be:

import cv2

inv_mask = ~mask