diegobc11
diegobc11

Reputation: 97

Change colors while maintining intensity of the original color in Opencv

I want to change the color of a bulldozer to a specific Hexadecimal color (red in this case) Original image while preserving the intensity (the effect of light on the bulldozer). I want to change only the yellow parts of it, so far I have created a mask with a lower and upper bounds for the light and darker yellow tones, and extracted that mask, however I don't know how to apply the color red to the mask and keep the relative intensity of the color, not just all the same tone of red.

Something like the following image would be great (better quality if possible, since there seems to be some color not correctly set) expected result

I have seen some workarounds using HSV but nothing really does the job.

My code so far involves creating a mask to extract the color that I want to change and convert that mask to HSV colors to preserve the intensity of the colors, after that I try to modify the HSV to achieve the desired color, however it is not precise, there is no exact way to change the HSV to the hexadecimal or rgb color that I want?

import cv2
import numpy as np
import os
import math
from numpy import inf

# set the bounds for the light and dark shade of color you want to mask
lower_yellow = np.array([20, 100, 100])
upper_yellow = np.array([30, 255, 255])  

all_images = sorted(os.listdir('data/audi_a6'))
for i, image in enumerate(all_images):
    print(image, end='\r')
    img = cv2.imread(f'data/lego/{image}')
    hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

    yellow_mask = cv2.inRange(hsv, lower_yellow, upper_yellow)
    inv_mask = cv2.bitwise_not(yellow_mask)

    h, s, v = cv2.split(hsv)
    h = np.mod(h - h + 182, 180)
    s = np.clip(s + 120, 0, 255)
    v = np.clip(v, 0, 255)
    hsv = cv2.merge([h, s, v])

    bgr = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)

    result = cv2.bitwise_or(cv2.bitwise_and(img, img, mask=inv_mask), cv2.bitwise_and(bgr, bgr, mask=yellow_mask))

    # Save the result
    cv2.imwrite(f'output_change_color/lego/frame_{i:03}.jpg', result)

Upvotes: 0

Views: 150

Answers (2)

Cris Luengo
Cris Luengo

Reputation: 60504

There's only one thing I need to do to get your code to produce the expected result. The H channel is around 25 for the yellow pixels. We want it to be 0 instead. So we need to subtract 25: H - H_yellow + H_red.

But since h is a uint8 array, subtracting 25 will cause some pixels to wrap around to 255, which is not a valid value for H (OpenCV defines it in the 0-180 range for 8-bit images). So instead if adding 0 we add 180, then the np.mod() operation can do the right thing:

h = np.mod(h + 180 - 25, 180)

This is obviously not generic. To do this more generically, we're better off using floating-point operations for arithmetic, as there's no overflow or wrapping to worry about. This has another advantage: OpenCV won't quantize the HSV space to integer values, which allows for a more precise reconstruction. But remember that H is now in the range 0-360, and S and V in the range 0-1.

img = cv2.imread(image)

imgf = img.astype(np.float32) / 255
hsv = cv2.cvtColor(imgf, cv2.COLOR_BGR2HSV)
h, s, v = cv2.split(hsv)

h = np.mod(h + h_target - h_source, 360)
s = np.clip(s + s_taget - s_source, 0, 1)

hsv = cv2.merge([h, s, v])
imgf = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
bgr = (imgf * 255).astype(np.uint8)

[Note we don't touch V.]

Upvotes: 1

Mark Setchell
Mark Setchell

Reputation: 207465

You seem to be having difficulty discriminating between the yellow machine and the "muddy track" beneath it. One way to work out these things is to look at different colourspaces and see which one is a good discriminant. So, I converted your image to many different colourspaces and split out the individual channels side-by-side for you:

enter image description here

So you could look at each channel of each colourspace and see which one works best for you. For example the third channel of CIELab, or the second channel of HCL...

Upvotes: 1

Related Questions