0
votes

I'm trying to compare every pixel's hue value with a threshold given. If the pixel's hue is between the threshold value given, it will draw a small circle to that particular pixel.

So, what I did is to iterate over every pixel from the photo and compare every pixel to see whether the pixel's hue is between the threshold or not. But, when I was doing this, the speed of iterating over the pixels are very slow. Is there any way to speed up the iterating process?

Here's what I did:

img = cv2.imread("green bottle.jpg")
imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, w, d = imgHSV.shape

for i in range(h):
    for j in range(w):
        k = imgHSV[i, j]
        if 26 <= k[0] <= 35:   # the hue value threshold between 26 to 57
            cv2.circle(img, (j, i), 1, (255, 0, 0))   # draw a small circle for every matching result
        elif 36 <= k[0] <=77:
            cv2.circle(img, (j, 1), 1, (0, 255, 0))   # draw a small circle for every matching result

cv2.imshow("Image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

Thanks in advance!

2
The bottleneck here must be your drawing function, which doesn't seem to be vectorized.Quang Hoang
Why not just use cv2.inRange() to threshold your image. If you want to draw circles, then find each white pixel's coordinates from the thresholded image (using np.argwhere) and use those as centers to draw your circles.fmw42
@fmw42 Hi! Thanks for answering, I'm sorry but I think I have to rephrase my question, I've thought of using cv2.inRange() before, but actually I have several if-statement as shown above (edited version). So, for every different threshold, there will be different color circle.Yun Bei
I think use fmw42's suggestion. Just do this multiple times for each threshold. Then plot them separately. That would be the fastest I think.gnodab
You can use inRange() twice (once for each condition) and then combine masks. Then proceed as I mentioned above.fmw42

2 Answers

1
votes

I would suggest making this vectorized where you first find the circles that have a hue between 26 and 35 as well as hues between 36 and 77, then draw them on your image. cv2.circle is unfortunately not vectorized as it is designed to only take a single pair of coordinates but the search for the pixels of interest can be done faster.

Try:

import numpy as np
import cv2

img = cv2.imread("green bottle.jpg")
imgHSV = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
hue = imgHSV[...,0]
(rows1, cols1) = np.where(np.logical_and(hue >= 26, hue <= 35))
(rows2, cols2) = np.where(np.logical_and(hue >= 36, hue <= 77))

for r, c in zip(rows1, cols1):
    cv2.circle(img, (c, r), 1, (255, 0, 0))

for r, c in zip(rows2, cols2):
    cv2.circle(img, (c, r), 1, (0, 255, 0))

cv2.imshow("Image", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

numpy.where takes in a NumPy array and a condition to check for such that any values that meet this condition, we can obtain the row and column locations. I also use numpy.logical_and to properly incorporate the range search for the hues. I do this twice, once per range of hues which gives me two sets of coordinates. I then loop over each set of coordinates and plot the circles in their respective colours.

1
votes

I had a try and came up with something pretty much identical to @rayryeng like this but it was very slow for my image:

# Load image
im = cv2.imread('smooth.png')

# Convert to HSV and extract H
H = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)[..., 0]

# Mask Hues in range [26..35]
rangeA = np.logical_and(H>=26, H<=35)

# Get coordinates of selected pixels and plot circle at each
for y,x in np.argwhere(rangeA):
    cv2.circle(im, (x,y), 1, (255,255,255))

When I timed it, I realised all the time was taken by cv2.circle(). So I looked at the circle of radius 1 and it looks like this:

0 1 0
1 0 1
0 1 0

which is extremely similar to a 3x3 morphological cross:

0 1 0
1 1 1
0 1 0

So, I drew the circles with morphology rather than cv2.circle(), and I got this:

#!/usr/bin/env python3

import cv2
import numpy as np

# Load image
im = cv2.imread('smooth.png')

# Convert to HSV and extract H
H = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)[..., 0]

# Mask Hues in range [26..35]
rangeA = np.logical_and(H>=26, H<=35)
    
# Note that a circle radius 1 is a 3x3 cross, so rather than
# draw circles we can convolve with a ring
ring = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3))
ring[1,1]=0

res = cv2.morphologyEx((rangeA*255).astype(np.uint8), cv2.MORPH_DILATE, ring)

# Save result
cv2.imwrite('result.png', res)

So, if I start with this:

enter image description here

I got this in a fraction of the time:

enter image description here

Timings on my image were 5ms as follows with for loop:

In [133]: %%timeit
     ...: for y,x in np.argwhere(rangeA):
     ...:     cv2.circle(im, (x,y), 1, (255,255,255))
     ...: 
4.98 ms ± 42.6 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

and 159 microseconds with dilation:

%timeit cv2.morphologyEx((rangeA*255).astype(np.uint8), cv2.MORPH_DILATE, ring)
159 µs ± 4.13 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each)