20
\$\begingroup\$

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
img = cv2.imread("source.jpg")   
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: enter image description here

result should be: enter image description here

\$\endgroup\$
0

4 Answers 4

13
\$\begingroup\$

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)
\$\endgroup\$
14
\$\begingroup\$

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.

\$\endgroup\$
10
  • 1
    \$\begingroup\$ Curses! You'll have to break down the compound <= >= statement. Let me edit. \$\endgroup\$
    – aghast
    Commented Jan 1, 2018 at 18:27
  • 1
    \$\begingroup\$ I get some result, but not the same as with my own code. I uploaded source and result images. \$\endgroup\$ Commented Jan 1, 2018 at 18:42
  • 1
    \$\begingroup\$ @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. \$\endgroup\$ Commented Jan 1, 2018 at 19:14
  • 1
    \$\begingroup\$ @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. \$\endgroup\$ Commented Jan 1, 2018 at 20:30
  • 1
    \$\begingroup\$ @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) \$\endgroup\$
    – Dan Mašek
    Commented Jan 1, 2018 at 21:16
2
\$\begingroup\$

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)

\$\endgroup\$
1
\$\begingroup\$

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
img = cv2.imread("source.jpg")
b, g, r = img[:, :, 0], img[:, :, 1], img[:, :, 2]
mask = (g > 100) & (r < 100) & (b < 100)

The inverse of the mask, if required, is:

inv_mask = ~mask
\$\endgroup\$
2
  • 1
    \$\begingroup\$ How would this integrate into OP's code? Where would your two (or three) lines go, and how does this improve the performance? \$\endgroup\$ Commented Aug 3, 2019 at 17:11
  • \$\begingroup\$ I made a small edit to show how this replaces the OP's code. I haven't bench marked the performance, but I would expect several orders of magnitude improvement over the original. This is similar to the other answers but my point is more to do with the logic of identifying the background colour. \$\endgroup\$
    – daveg
    Commented Aug 3, 2019 at 18:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.