Fast RGB Thresholding in Python (possibly some smart OpenCV code?)

13,758

Solution 1

I think the inRange opencv method is what you are interested in. It will let you set multiple thresholds simultaneously.

So, with your example you would use

# Remember -> OpenCV stores things in BGR order
lowerBound = cv.Scalar(120, 80, 100);
upperBound = cv.Scalar(140, 85, 110);

# this gives you the mask for those in the ranges you specified,
# but you want the inverse, so we'll add bitwise_not...
cv.InRange(cv_im, lowerBound, upperBound, cv_rgb_thresh);
cv.Not(cv_rgb_thresh, cv_rgb_thresh);

Hope that helps!

Solution 2

You can do it with numpy in a much faster way if you don't use loops.

Here's what I came up with:

def better_way():
    img = Image.open("rainbow.jpg").convert('RGB')
    arr = np.array(np.asarray(img))

    R = [(90,130),(60,150),(50,210)]
    red_range = np.logical_and(R[0][0] < arr[:,:,0], arr[:,:,0] < R[0][1])
    green_range = np.logical_and(R[1][0] < arr[:,:,0], arr[:,:,0] < R[1][1])
    blue_range = np.logical_and(R[2][0] < arr[:,:,0], arr[:,:,0] < R[2][1])
    valid_range = np.logical_and(red_range, green_range, blue_range)

    arr[valid_range] = 200
    arr[np.logical_not(valid_range)] = 0

    outim = Image.fromarray(arr)
    outim.save("rainbowout.jpg")


import timeit
t = timeit.Timer("your_way()", "from __main__ import your_way")
print t.timeit(number=1)

t = timeit.Timer("better_way()", "from __main__ import better_way")
print t.timeit(number=1)

The omitted your_way function was a slightly modified version of your code above. This way runs much faster:

$ python pyrgbrange.py 
10.8999910355
0.0717720985413

That's 10.9 seconds vs. 0.07 seconds.

Solution 3

The PIL point function takes a table of 256 values for each band of the image and uses it as a mapping table. It should be pretty fast. Here's how you would apply it in this case:

def mask(low, high):
    return [x if low <= x <= high else 0 for x in range(0, 256)]

img = img.point(mask(100,110)+mask(80,85)+mask(120,140))

Edit: The above doesn't produce the same output as your numpy example; I followed the description rather than the code. Here's an update:

def mask(low, high):
    return [255 if low <= x <= high else 0 for x in range(0, 256)]

img = img.point(mask(100,110)+mask(80,85)+mask(120,140)).convert('L').point([0]*255+[200]).convert('RGB')

This does a few conversions on the image, making copies in the process, but it should still be faster than operating on individual pixels.

Share:
13,758
Happy
Author by

Happy

Updated on July 04, 2022

Comments

  • Happy
    Happy almost 2 years

    I need to do some fast thresholding of a large amount of images, with a specific range for each of the RGB channels, i.e. remove (make black) all R values not in [100;110], all G values not in [80;85] and all B values not in [120;140]

    Using the python bindings to OpenCV gives me a fast thresholding, but it thresholds all three RGP channels to a single value:

    cv.Threshold(cv_im,cv_im,threshold+5, 100,cv.CV_THRESH_TOZERO_INV)
    cv.Threshold(cv_im,cv_im,threshold-5, 100,cv.CV_THRESH_TOZERO)
    

    Alternatively I tried to do it manually by converting the image from PIL to numpy:

    arr=np.array(np.asarray(Image.open(filename).convert('RGB')).astype('float'))
    for x in range(img.size[1]):
        for y in range(img.size[0]):
            bla = 0
            for j in range(3):
                if arr[x,y][j] > threshold2[j] - 5 and arr[x,y][j] < threshold2[j] + 5 :
                    bla += 1
            if bla == 3:
                arr[x,y][0] = arr[x,y][1] = arr[x,y][2] = 200
            else:
                arr[x,y][0] = arr[x,y][1] = arr[x,y][2] = 0
    

    While this works as intended, it is horribly slow!

    Any ideas as to how I can get a fast implementation of this?

    Many thanks in advance, Bjarke