Search code examples
pythonopencvpython-imaging-librarycropcontour

Python: Contour around rectangle based on specific color on a dark image (OpenCV)


since I'm trying to enhance my skills with OpenCV in Python, I would like to know what's the best way of extracting a specific gray tone out of a image with mostly dark colors.

To start of, I created a test image in order to test different methods with OpenCV:

Lets say I want to extract a specific color in this image and add a border to it. For now I chose the gray rectangle in the middle with the color (33, 33, 34 RGB), see following:

(Here's the image without the red border in order you want to test your ideas: https://i.sstatic.net/Zf8Vb.png)

example image with border

This is what I've tried so far, but it's not quite working:

img = cv2.imread(path) #Read input image
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # Convert from BGR to HSV color space
saturation_plane = hsv[:, :, 1] # all black/white/gray pixels are zero, and colored pixels are above zero
_, thresh = cv2.threshold(saturation_plane, 8, 255, cv2.THRESH_BINARY) # Apply threshold on s
contours = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # draw all contours 
contours = contours[0] if len(contours) == 2 else contours[1]
result = img.copy()

for contour in contours:
   (x, y, w, h) = cv2.boundingRect(contour) # compute the bounding box for the contour
   if width is equal to the width of the rectangle i want to extract:
       draw contour

What if the size of the rectangle is not fixed, so that I won't be able to detect it through its width/height? Moreover, is it better to convert the image into a gray scale instead of HSV? I'm just new to it and I would like to hear your way of achieving this.

Thanks in advance.


Solution

  • In case the specific color is known, you may start with gray = np.all(img == (34, 33, 33), 2).

    The result is a logical matrix with True where BGR = (34, 33, 33), and False where it is not.
    Note: OpenCV color ordering is BGR and not RGB.

    • Convert the logical matrix to uint8: gray = gray.astype(np.uint8)*255
    • Use findContours on gray image.

    Converting the image to HSV in not going useful in case you want to find the blue rectangle, but not a gray rectangle with very specific RGB values.

    The following code finds the contour with maximum size with color (33, 33, 34 RGB):

    import numpy as np
    import cv2
    
    # Read input image
    img = cv2.imread('rectangles.png')
    
    # Gel all pixels in the image - where BGR = (34, 33, 33), OpenCV colors order is BGR not RGB
    gray = np.all(img == (34, 33, 33), 2)  # gray is a logical matrix with True where BGR = (34, 33, 33).
    
    # Convert logical matrix to uint8
    gray = gray.astype(np.uint8)*255
    
    # Find contours
    cnts = cv2.findContours(gray, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2]  # Use index [-2] to be compatible to OpenCV 3 and 4
    
    # Get contour with maximum area
    c = max(cnts, key=cv2.contourArea)
    
    x, y, w, h = cv2.boundingRect(c)
    
    # Draw green rectangle for testing
    cv2.rectangle(img, (x, y), (x+w, y+h), (0, 255, 0), thickness = 2)
    
    # Show result
    cv2.imshow('gray', gray)
    cv2.imshow('img', img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    Result:

    gray:
    enter image description here

    img:
    enter image description here


    In case you don't know the specific color of the mostly dark colors, you may find all contours, and search for the one with the lowest gray value:

    import numpy as np
    import cv2
    
    # Read input image
    img = cv2.imread('rectangles.png')
    
    # Convert from BGR to Gray
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    
    # Apply threshold on gray
    _, thresh = cv2.threshold(gray, 8, 255, cv2.THRESH_BINARY)
    
    # Find contours on thresh
    cnts = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)[-2]  # Use index [-2] to be compatible to OpenCV 3 and 4
    
    min_level = 255
    min_c = []
    
    #Iterate contours, and find the darkest:
    for c in cnts:
        x, y, w, h = cv2.boundingRect(c)
    
        # Ignore contours that are very thin (like edges)
        if w > 5 and h > 5:
            level = gray[y+h//2, x+w//2]  # Get gray level of center pixel
    
            if level < min_level:
                # Update min_level abd min_c
                min_level = level
                min_c = c
    
    x, y, w, h = cv2.boundingRect(min_c)
    
    # Draw red rectangle for testing
    cv2.rectangle(img, (x, y), (x+w, y+h), (0, 0, 255), thickness = 2)
    
    # Show result
    cv2.imshow('img', img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()
    

    Result:
    enter image description here