3

I'm trying to convert (shift) the values of every pixel in an HSV image (taken from a frame of a video).

The idea is to invert yellow and red colours into blue colour (to avoid using three threshold later in the program, when I can use just one) by inverting the red and yellow values into blue values using following equation.

(Hue + 90) % 180 (in OpenCV 3 Hue is in range [0,180])

Here's what I came up with:

hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV);
H = hsv[:,:,0]
mask= [H<75 and H>128]
print("orig",hsv[mask])
hsv[mask] = ((hsv[mask]+90) % 180)

Unfortunately It doesn't work as by this approach Im selecting the whole hue channel not its pixel values

GodIsAnAstronaut
  • 351
  • 5
  • 17
  • You are using confusing variable names. First you use H, S, V as 0, 1, 2 to be the first, second, and third channel---but then you reassign them to be the actual 2D array instead of the index for each channel. Then you use S and V to index into the 2nd and 3rd dimension, which is presumably not what you want to do (instead you want `[H, :, :]`, `[S, :, :]`, and `[V, :, :]`) but you can simply do `h, s, v = cv2.split(hsv)` and your life will be much easier. – alkasm Apr 06 '18 at 17:08
  • I already edited the question, since I figured out the code is very wrong – GodIsAnAstronaut Apr 06 '18 at 17:15
  • Oops also I meant `[:, :, H]` but it seems you caught that. Your new code should work, but since it's numpy arrays you'll want to do this: `mask = (H<75) & (H>128)`. Then your code above will work. – alkasm Apr 06 '18 at 17:27
  • Python's `and` keyword only works on booleans, but `H<75` is an *array of booleans*, not a direct bool. So for numpy arrays, `&` does what you expect it to do (*elementwise* boolean operations). – alkasm Apr 06 '18 at 17:29
  • Oh and you probably actually want to `OR` those together, not `AND`. Obviously no pixels can be both < 75 and > 128. – alkasm Apr 06 '18 at 17:32

2 Answers2

4

There's two different possibilities here, and I'm not sure which you want, but they're both trivial to implement. You can invert (reverse may be a better word) the hue rainbow, which you can just do by using 180 - hue. Or you can shift the color by 180 degrees by using (hue + 90) % 180 like you mention.

Reversing the colors:

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
rev_h = 180 - h
rev_hsv = cv2.merge([rev_h, s, v])
rev_img = cv2.cvtColor(rev_hsv, cv2.COLOR_HSV2BGR)

Shifting the colors:

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
shift_h = (h + 90) % 180
shift_hsv = cv2.merge([shift_h, s, v])
shift_img = cv2.cvtColor(shift_hsv, cv2.COLOR_HSV2BGR)

Those are the idiomatic ways to do it in OpenCV.

Now you want to do the same thing as above but only for some masked subset of pixels that meet a condition. This is not too hard to do; if you want to shift some masked pixels:

hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)
h_mask = (h < 75) | (h > 128)
h[h_mask] = (h[h_mask] + 90) % 180
shift_hsv = cv2.merge([h, s, v])
shift_img = cv2.cvtColor(shift_hsv, cv2.COLOR_HSV2BGR)
alkasm
  • 22,094
  • 5
  • 78
  • 94
  • Thank you for this answer ! Shifting the colors is what I needed, but there's one more thing - I need not to shift values in range (75,128) – GodIsAnAstronaut Apr 06 '18 at 17:23
  • Be careful - `h + 90` will overflow the byte range 256 for cases when h is already greater than 166. You might want to convert to uint16 before adding, otherwise the hue will get corrupted during the shift. – JustAMartin Sep 18 '20 at 14:58
2

Hue channel is uint8 type, value range is [0, 179]. Therefore, when add with a large number or a negative number, Python returns a garbage number. Here is my solution base on @alkasm color shifting code:

img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(img_hsv)
shift_h = random.randint(-50, 50)
h = ((h.astype('int16') + shift_h) % 180).astype('uint8')
shift_hsv = cv2.merge([h, s, v])

For random hue, saturation, and value shifting. Shift channel base on @bill-grates:

def shift_channel(c, amount):
   if amount > 0:
        lim = 255 - amount
        c[c >= lim] = 255
        c[c < lim] += amount
    elif amount < 0:
        amount = -amount
        lim = amount
        c[c <= lim] = 0
        c[c > lim] -= amount
    return c

rand_h, rand_s, rand_v = 50, 50, 50
img_hsv = cv2.cvtColor(img_bgr, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(img_hsv)
# Random shift hue
shift_h = random.randint(-rand_h, rand_h)
h = ((h.astype('int16') + shift_h) % 180).astype('uint8')
# Random shift saturation
shift_s = random.randint(-rand_s, rand_s)
s = shift_channel(s, shift_s)
# Random shift value
shift_v = random.randint(-rand_v, rand_v)
v = shift_channel(v, shift_v)
shift_hsv = cv2.merge([h, s, v])
print(shift_h, shift_s, shift_v)
img_rgb = cv2.cvtColor(shift_hsv, cv2.COLOR_HSV2RGB)
  • Nice catch - I was wondering why my hue was corrupted when shifting to red. But of course, 179 + 90 = 269, which is too much for uint8 bytes. Your `.astype('int16')` fixed the issue for me. – JustAMartin Sep 18 '20 at 14:57