Matthew Kirkland
Matthew Kirkland

Reputation: 31

Change range of colors in an image using python

I'm working on automating changing image colors using python. The image I'm using is below, i'd love to move it from red to another range of colors, say green, keeping the detail and shading if possible. I've been able to convert some of the image to a solid color, losing all detail.

Blue instead of red.

The code I'm currently using is below, I can't quite figure out the correct range of red to make it work correctly, and also it only converts to a single color, again losing all detail and shade.

Any help is appreciated, thank you.

import cv2
import numpy as np
import skimage.exposure

# load image and get dimensions
img = cv2.imread("test5.jpg")

# convert to hsv
hsv = cv2.cvtColor(img,cv2.COLOR_BGR2HSV)

## mask of upper red (170,50,50) ~ (180,255,255)
## mask of lower red (0,50,50) ~ (10,255,255)
# threshold using inRange
range1 = (0,50,50)
range2 = (1,255,255)
mask = cv2.inRange(hsv,range1,range2)
mask = 255 - mask

# apply morphology opening to mask
kernel = np.ones((3,3), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_ERODE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

# antialias mask
mask = cv2.GaussianBlur(mask, (0,0), sigmaX=3, sigmaY=3, borderType = cv2.BORDER_DEFAULT)
mask = skimage.exposure.rescale_intensity(mask, in_range=(127.5,255), out_range=(0,255))

result = img.copy()
result[mask==0] = (255,255,255)

# write result to disk
cv2.imwrite("test6.jpg", result)

Upvotes: 3

Views: 3477

Answers (3)

fmw42
fmw42

Reputation: 53081

Starting with a blue image rather than red allows one to use an expanded range for inRange() and do a better job in Python/OpenCV. Here is a change from blue to red.

Input:

enter image description here

import cv2
import numpy as np

# load image
img = cv2.imread('blue_clothes.jpg')

# convert to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv)

red_hue = 0
blue_hue = 120

# diff hue (red_hue - blue_hue)
diff_hue = red_hue - blue_hue

# create mask for blue color in hsv
lower = (100,90,90)
upper = (140,255,255)
mask = cv2.inRange(hsv, lower, upper)
mask = cv2.merge([mask,mask,mask])

# apply morphology to clean mask
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9,9))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

# modify hue channel by adding difference and modulo 180 
hnew = np.mod(h + diff_hue, 180).astype(np.uint8)

# recombine channels
hsv_new = cv2.merge([hnew,s,v])

# convert back to bgr
bgr_new = cv2.cvtColor(hsv_new, cv2.COLOR_HSV2BGR)

# blend with original using mask
result = np.where(mask==(255, 255, 255), bgr_new, img)

# save output
cv2.imwrite('blue_clothes_mask.png', mask)
cv2.imwrite('blue_clothes_hue_shift.png', bgr_new)
cv2.imwrite('blue_clothes_blue2red.png', result)

# Display various images to see the steps
cv2.imshow('mask',mask)
cv2.imshow('bgr_new',bgr_new)
cv2.imshow('result',result)
cv2.waitKey(0)
cv2.destroyAllWindows()

Mask:

enter image description here

Blue to Red before masking:

enter image description here

Blue to Red after masking:

enter image description here

Upvotes: 1

fmw42
fmw42

Reputation: 53081

You can start with red, but the trick is to invert the image so red is now at hue 90 in OpenCV range and for example blue is at hue 30. So in Python/OpenCV, you can do the following:

Input:

enter image description here

import cv2
import numpy as np

# load image
img = cv2.imread('red_clothes.jpg')

# invert image
imginv = 255 - img

# convert to HSV
hsv = cv2.cvtColor(imginv, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv)

blueinv_hue = 30  #(=120+180/2=210-180=30)
redinv_hue = 90  #(=0+180/2=90)

# diff hue (blue_hue - red_hue)
diff_hue = blueinv_hue - redinv_hue

# create mask for redinv color in hsv
lower = (80,150,150)
upper = (100,255,255)
mask = cv2.inRange(hsv, lower, upper)
mask = cv2.merge([mask,mask,mask])

# apply morphology to clean mask
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9,9))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

# modify hue channel by adding difference and modulo 180 
hnew = np.mod(h + diff_hue, 180).astype(np.uint8)

# recombine channels
hsv_new = cv2.merge([hnew,s,v])

# convert back to bgr
bgrinv_new = cv2.cvtColor(hsv_new, cv2.COLOR_HSV2BGR)

# invert
bgr_new = 255 -bgrinv_new

# blend with original using mask
result = np.where(mask==(255, 255, 255), bgr_new, img)

# save output
cv2.imwrite('red_clothes_mask.png', mask)
cv2.imwrite('red_clothes_hue_shift.png', bgr_new)
cv2.imwrite('red_clothes_red2blue.png', result)

# Display various images to see the steps
cv2.imshow('mask',mask)
cv2.imshow('bgr_new',bgr_new)
cv2.imshow('result',result)
cv2.waitKey(0)
cv2.destroyAllWindows()

Mask:

enter image description here

Red to Blue before masking:

enter image description here

Red to Blue after masking:

enter image description here

However, one is still limited by the fact that red is close to skin tones, so the range for red is limited.

Upvotes: 1

fmw42
fmw42

Reputation: 53081

This is one way to approach the problem in Python/OpenCV. But for red, it is very hard to do because red spans 0 hue, which also is the hue for gray and white and black, which you have in your image. The other issue you have is that skin tones has red shades, so you cannot pick too large of ranges for your colors. Also when dealing with red ranges, you need two sets, one for hues up to 180 and another for hues above 0.

Input:

enter image description here

import cv2
import numpy as np

# load image
img = cv2.imread('red_clothes.jpg')

# convert to HSV
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
h,s,v = cv2.split(hsv)

blue_hue = 120
red_hue = 0

# diff hue (blue_hue - red_hue)
diff_hue = blue_hue - red_hue

# create mask for red color in hsv
lower1 = (150,150,150)
upper1 = (180,255,255)
mask1 = cv2.inRange(hsv, lower1, upper1)
lower2 = (0,150,150)
upper2 = (30,255,255)
mask2 = cv2.inRange(hsv, lower2, upper2)
mask = cv2.add(mask1,mask2)
mask = cv2.merge([mask,mask,mask])

# apply morphology to clean mask
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9,9))
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)

# modify hue channel by adding difference and modulo 180 
hnew = np.mod(h + diff_hue, 180).astype(np.uint8)

# recombine channels
hsv_new = cv2.merge([hnew,s,v])

# convert back to bgr
bgr_new = cv2.cvtColor(hsv_new, cv2.COLOR_HSV2BGR)

# blend with original using mask
result = np.where(mask==(255, 255, 255), bgr_new, img)

# save output
cv2.imwrite('red_clothes_mask.png', mask)
cv2.imwrite('red_clothes_hue_shift.png', bgr_new)
cv2.imwrite('red_clothes_red2blue.png', result)

# Display various images to see the steps
cv2.imshow('mask1',mask1)
cv2.imshow('mask2',mask2)
cv2.imshow('mask',mask)
cv2.imshow('bgr_new',bgr_new)
cv2.imshow('result',result)
cv2.waitKey(0)
cv2.destroyAllWindows()

Mask:

enter image description here

Hue Shifted Image:

enter image description here

Blend between Input and Hue Shifted Image using Mask to blend:

enter image description here

So the result is speckled because of the black mixed with the red and from limited ranges due to skin color.

Upvotes: 3

Related Questions