1

I have the follow image:

src

What I want to do is to keep all red figures.

Using this code..

import cv2
import numpy as np

def callback(x):
    pass

cap = cv2.VideoCapture(0)
cv2.namedWindow('image')

ilowH = 0
ihighH = 179
ilowS = 0
ihighS = 255
ilowV = 0
ihighV = 255

# create trackbars for color change
cv2.createTrackbar('lowH', 'image', ilowH, 179, callback)
cv2.createTrackbar('highH', 'image', ihighH, 179, callback)

cv2.createTrackbar('lowS', 'image', ilowS, 255, callback)
cv2.createTrackbar('highS', 'image', ihighS, 255, callback)

cv2.createTrackbar('lowV', 'image', ilowV, 255, callback)
cv2.createTrackbar('highV', 'image', ihighV, 255, callback)

while True:
    # grab the frame
    frame = cv2.imread('color_test.png')

    # get trackbar positions
    ilowH = cv2.getTrackbarPos('lowH', 'image')
    ihighH = cv2.getTrackbarPos('highH', 'image')
    ilowS = cv2.getTrackbarPos('lowS', 'image')
    ihighS = cv2.getTrackbarPos('highS', 'image')
    ilowV = cv2.getTrackbarPos('lowV', 'image')
    ihighV = cv2.getTrackbarPos('highV', 'image')

    hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
    lower_hsv = np.array([ilowH, ilowS, ilowV])
    higher_hsv = np.array([ihighH, ihighS, ihighV])
    mask = cv2.inRange(hsv, lower_hsv, higher_hsv)

    frame = cv2.bitwise_and(frame, frame, mask=mask)

    # show thresholded image
    cv2.imshow('image', frame)
    k = cv2.waitKey(1) & 0xFF  # large wait time to remove freezing
    if k == 113 or k == 27:
        break

cv2.destroyAllWindows()
cap.release()

... max I can obtain is this:

output

How can I get rid of yellow colour and keep the 3 red figures ? Is HSL a good alternative to use in cases like this ? Keep in mind that the center red it's not the same as the other 2; one is full red (255, 0, 0) the other are less (237, 28, 36) RGB.

Jeru Luke
  • 20,118
  • 13
  • 80
  • 87
lucians
  • 2,239
  • 5
  • 36
  • 64
  • 1
    the red hue range is split around 0 (0°) and 180 (360°) so you can't work with a lower-and-higher-threshold-area. Hue is a circular value range. – Micka May 07 '18 at 20:24
  • I tried to go up and down with the sliders but max I can obtain (by keeping all the 3 red figures) it's with the yellow included. If I delete also the yellow, the red square (upper left) and bottom ellipse disappear also. – lucians May 07 '18 at 20:31
  • 1
    Invert/negate the image (pixel=255-pixel) and choose cyan pixels using `inRange()` – Mark Setchell May 07 '18 at 20:45
  • Just like Micka hinted, the red hue is split. Simplest thing you can do is save the hue channel in grayscale and then inspect the values, e.g. using some paint program. You will notice that the top and bottom red objects have hue value of 179, and the middle square has hue value of 0. You can't select all 3 using a single `inRange` call -- you need two, and then combine the masks. – Dan Mašek May 07 '18 at 20:46
  • another way is to choose a target hue value and a range, then shift the hue channel so that your target value is at shifted value 90 and then process a single inRange call to 90-range/2 to 90+range/2. Probably not efficient but sweet :) – Micka May 07 '18 at 20:50
  • About HSV and masks I found [this](https://stackoverflow.com/questions/30331944/finding-red-color-using-python-opencv) which promise good.. – lucians May 07 '18 at 21:33

2 Answers2

5

I did something similar a while back and ended up defining my own basic colors in HSV space, including monochrome definitions (which I admit are a little arbitrary).

Anyway, as those in the comments stated, in HSV, the red hue is split, so I made a simple function to combine any colors I defined to easily create masks for them:

import cv2
import numpy as np

HSV_RANGES = {
    # red is a major color
    'red': [
        {
            'lower': np.array([0, 39, 64]),
            'upper': np.array([20, 255, 255])
        },
        {
            'lower': np.array([161, 39, 64]),
            'upper': np.array([180, 255, 255])
        }
    ],
    # yellow is a minor color
    'yellow': [
        {
            'lower': np.array([21, 39, 64]),
            'upper': np.array([40, 255, 255])
        }
    ],
    # green is a major color
    'green': [
        {
            'lower': np.array([41, 39, 64]),
            'upper': np.array([80, 255, 255])
        }
    ],
    # cyan is a minor color
    'cyan': [
        {
            'lower': np.array([81, 39, 64]),
            'upper': np.array([100, 255, 255])
        }
    ],
    # blue is a major color
    'blue': [
        {
            'lower': np.array([101, 39, 64]),
            'upper': np.array([140, 255, 255])
        }
    ],
    # violet is a minor color
    'violet': [
        {
            'lower': np.array([141, 39, 64]),
            'upper': np.array([160, 255, 255])
        }
    ],
    # next are the monochrome ranges
    # black is all H & S values, but only the lower 25% of V
    'black': [
        {
            'lower': np.array([0, 0, 0]),
            'upper': np.array([180, 255, 63])
        }
    ],
    # gray is all H values, lower 15% of S, & between 26-89% of V
    'gray': [
        {
            'lower': np.array([0, 0, 64]),
            'upper': np.array([180, 38, 228])
        }
    ],
    # white is all H values, lower 15% of S, & upper 10% of V
    'white': [
        {
            'lower': np.array([0, 0, 229]),
            'upper': np.array([180, 38, 255])
        }
    ]
}


def create_mask(hsv_img, colors):
    """
    Creates a binary mask from HSV image using given colors.
    """

    # noinspection PyUnresolvedReferences
    mask = np.zeros((hsv_img.shape[0], hsv_img.shape[1]), dtype=np.uint8)

    for color in colors:
        for color_range in HSV_RANGES[color]:
            # noinspection PyUnresolvedReferences
            mask += cv2.inRange(
                hsv_img,
                color_range['lower'],
                color_range['upper']
            )

    return mask

Applying it to your example (which I named "color_shapes.png"), I get good results:

img = cv2.imread('color_shapes.png')
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)

red_mask = create_mask(img_hsv, ['red'])

mask_img = cv2.bitwise_and(img_hsv, img_hsv, mask=red_mask)

enter image description here

Fiver
  • 9,909
  • 9
  • 43
  • 63
  • I will try this as soon as possible. Seems a good example. Thanks. – lucians May 11 '18 at 07:14
  • Thanks. Works pretty good. About the values, did you find somewhere or you just calculated them ? What if I have do create about 20 HSV ranges ? I have to do this manually or can I find them ? – lucians May 12 '18 at 21:31
  • 1
    Yes, I calculated them, starting with figuring out the center point for the 3 "major" colors. I named these major because they span a larger range of the hue values before what my eye could discern as a different color. It ended up dividing up pretty nicely with a hue range of 40 for the major colors and 20 for the 3 minor colors, covering the hue space of 180. The monochrome values are a bit more subjective. – Fiver May 12 '18 at 21:36
2

Background

I would like to propose an alternate approach. For segmenting/isolating dominant colors (red, blue, green and yellow); the LAB color space gives you more freedom. In HSV space, you have to alter the values present across all three channels. But in LAB space, you can just focus on 1 channel, depending on the color you want to segment.

LAB space has 3 channels (2 color channels and 1 brightness channel):

  • L-channel: incorporates the brightness
  • A-channel: incorporates the red and green color
  • B-channel: incorporates the blue and yellow color

If you look closely at the a-axis in the following diagram, colors red and green can be segmented at the extremes of that axis.

enter image description here

Code:

img = cv2.imread('colored_blobs.png')
lab = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
a_channel = lab[:,:,1]

enter image description here

As you can see, the red colored objects are in the brighter end of the spectrum. For this image, I subtract pixel intensity 10 from the maximum in a_channel and set it as the threshold:

th = cv2.threshold(dst,int(np.max(a_channel) - 10),255,cv2.THRESH_BINARY)[1]

enter image description here

All that's left to be done is masking:

masked = cv2.bitwise_and(img, img, mask = th)

enter image description here

Added Info:

I have written a similar answer to detect green color here: How to define a threshold value to detect only green colour objects in an image with Python OpenCV?

Performing the same operation on the B-channel will help segment blue/yellow colors.

Jeru Luke
  • 20,118
  • 13
  • 80
  • 87