Search code examples
pythonopencvgraphicsdrawinggame-automation

OpenCV in Python: Rectangle with fill and stroke in one loop


OpenCV rectangle fill & stroke in a single loop

Introduction

Hi! I'm trying to add a rectangle with a translucent fill and a solid stroke over template matches in OpenCV with Python. The material I'm working with is from a game called Sun Haven by Pixel Sprout Studios, found on Steam.

Setup

Operating System: Microsoft Windows 11

Python version: 3.10.4

OpenCV version: 4.7.0

Problem

Currently, I have a script that will do the trick, but I don't like the way it's implemented as I currently have to loop through template results twice:

import cv2 as cv
import numpy as np

# Load the haystack and needle images in OpenCV.
haystack_image = cv.imread('sun_haven_14.jpg', cv.IMREAD_UNCHANGED)
needle_image = cv.imread('sun_haven_beecube.jpg', cv.IMREAD_UNCHANGED)

# Haystack image with drawings on.
scribble_image = haystack_image.copy()

# Grab the dimensions of the needle image.
needle_width = needle_image.shape[1]
needle_height = needle_image.shape[0]

# Match the needle image against the the haystack image.
result = cv.matchTemplate(haystack_image, needle_image, cv.TM_CCOEFF_NORMED)

# Define a standard to which a result is assumed positive.
threshold = 0.5

# Filter out image locations where OpenCV falls below the confidence threshold.
locations = np.where(result >= threshold)

# Convert the locations from arrays to X/Y-coordinate tuples.
locations = list(zip(*locations[::-1]))

# Debug-print the locations to the console.
#print(locations)

rectangles = []

# For all the locations found...
for location in locations:
    # Define a rectangle to draw around the location.
    rectangle = [int(location[0]), int(location[1]), needle_width, needle_height]

    # Append the rectangle to the rectangle collection.
    rectangles.append(rectangle)

# Group duplicate rectangles from overlapping results.
rectangles, weights = cv.groupRectangles(rectangles, 1, 1)

# If we have any rectangles at all...
if len(rectangles):
    print('Found needle(s)!')
    
    for rectangle in rectangles:
        # Determine the location position.
        top_left = (rectangle[0], rectangle[1])
        bottom_right = (rectangle[0] + rectangle[2], rectangle[1] + rectangle[3])

        # Draw a transparent, filled rectangle over the detected needle image.
        cv.rectangle(scribble_image, top_left, bottom_right, color = (0, 255, 0), thickness = -1)

    result_image = cv.addWeighted(scribble_image, 0.25, haystack_image, 1 - 0.25, 0)

    for rectangle in rectangles:
        # Determine the location position.
        top_left = (rectangle[0], rectangle[1])
        bottom_right = (rectangle[0] + rectangle[2], rectangle[1] + rectangle[3])

        # Draw a solid line around the detected needle image.
        cv.rectangle(result_image, top_left, bottom_right, color = (0, 255, 0), thickness = 2, lineType = cv.LINE_4)

    # Display the image in a window.
    cv.imshow('Result', result_image)
else:
    print('No needle found...')

# Pause the script.
cv.waitKey()

# Destroy all the OpenCV 2 windows.
cv.destroyAllWindows()

I'd prefer to combine the two "for rectangle in rectangles"-loops into a single loop, but if I do, I'm only getting the fill and not the stroke around the results.

Images

Haystack image (sun_haven_14.jpg)

Sun Haven. Credits: Pixel Sprout Studios

Needle image (sun_haven_beecube.jpg)

Beecube from Sun Haven. Credits: Pixel Sprout Studios

OpenCV script results (desired outcome)

The desired result, but with poor code.

As you can see, this detects and highlights the four beecubes from the original image, but I'd rather merge the two loops to not repeat the rectangle iteration and for performance.

Desired change

I've tried merging the two loops into this:

for rectangle in rectangles:
    # Determine the location position.
    top_left = (rectangle[0], rectangle[1])
    bottom_right = (rectangle[0] + rectangle[2], rectangle[1] + rectangle[3])

    # Draw a transparent, filled rectangle over the detected needle image.
    cv.rectangle(scribble_image, top_left, bottom_right, color = (0, 255, 0), thickness = -1)

    # Draw a solid line around the detected needle image.
    cv.rectangle(scribble_image, top_left, bottom_right, color = (0, 255, 0), thickness = 2, lineType = cv.LINE_4)

result_image = cv.addWeighted(scribble_image, 0.25, haystack_image, 1 - 0.25, 0)

# Display the image in a window.
cv.imshow('Result', result_image)

However, this does not yield the results I'm looking for, as it will only draw the transparent fill, not the stroke: Undesired results with fill, but no stroke.

So am I stuck with two almost identical for-loops, or is there a way to do this in a single loop? I'd really like to not repeat something that only needs to be done once, but if I have to, I have to.

Looking forward to feedback, cheers!


Solution

  • Problem solved

    Thanks to feedback from @Grismar, I now avoid drawing the solid-line rectangles on the same layer as the transparent fill rectangles, but instead draw directly on the haystack image:

    for rectangle in rectangles:
        # Determine the location position.
        top_left = (rectangle[0], rectangle[1])
        bottom_right = (rectangle[0] + rectangle[2], rectangle[1] + rectangle[3])
    
        # Draw a transparent, filled rectangle over the detected needle image.
        cv.rectangle(scribble_image, top_left, bottom_right, color = (0, 255, 0), thickness = -1)
    
        # Draw a solid line around the detected needle image.
        cv.rectangle(haystack_image, top_left, bottom_right, color = (0, 255, 0), thickness = 2, lineType = cv.LINE_4)
    
    result_image = cv.addWeighted(scribble_image, 0.25, haystack_image, 1 - 0.25, 0)
    
    # Display the image in a window.
    cv.imshow('Result', result_image)