Mini Map Player Detection

Hello all,

I am trying to detect player movement on a mini-map over the course of a match. The minimap location and size is static. Player arrows are a static shape but change position and rotation.

Player arrows are typically a bright and saturated color while the background is dark and semi-transparent. I am having trouble removing the background since the transparent nature of it changes the background colors slightly each frame.

Here is my current script which can iterates through a video and tries to highlight the player arrows on each frame. The first few frames work as expected but as the background changes the mask struggles to work consistently.

Does anyone have any background removal techniques for backgrounds that change over time? I can get the hex color range of the arrows before conversion. Maybe there is a better way to remove everything besides the arrow color. Any help is greatly appreciated!

import cv2
import numpy as np

cap = cv2.VideoCapture("./videos/map1.mp4")

if not cap.isOpened():
    print("Error: Video file not opened.")

# Initialize a frame counter
frame_count = 0
max_frames = 25

while cap.isOpened() and frame_count < max_frames:
    ret, frame =

    if not ret:
        print("Error: Frame not read.")

    frame_height, frame_width, _ = frame.shape

    # Crop the bottom-left corner
    crop_height = 490  # Half of the image height
    crop_width = 330   # Half of the image width
    cropped_frame = frame[crop_height:, :crop_width]

    # Convert the cropped frame to HSV color space
    hsv_frame = cv2.cvtColor(cropped_frame, cv2.COLOR_BGR2HSV)

    # Define the lower and upper HSV color range
    lower_color = np.array([0, 0, 176])
    upper_color = np.array([179, 32, 255])

    # Create a mask to isolate the shapes
    mask = cv2.inRange(hsv_frame, lower_color, upper_color)
    cv2.imshow('Masked Image', mask)    
    # Apply the mask to the cropped frame
    shape_only = cv2.bitwise_and(cropped_frame, cropped_frame, mask=mask)

    # Convert the shape_only image to grayscale and apply thresholding
    gray_shape_only = cv2.cvtColor(shape_only, cv2.COLOR_BGR2GRAY)
    _, thresholded_image = cv2.threshold(gray_shape_only, 1, 255, cv2.THRESH_BINARY)

    # Find contours in the thresholded image
    contours, _ = cv2.findContours(thresholded_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # Create a copy of the cropped frame to draw the triangles on
    frame_with_triangles = cropped_frame.copy()

    # Loop through each contour and identify triangles
    for contour in contours:
        approx = cv2.approxPolyDP(contour, 0.06 * cv2.arcLength(contour, True), True)
        area = cv2.contourArea(contour)
        approxLength = len(approx)
        if ((2 < approxLength and approxLength <= 5) and (70 < area and area <= 90)):
            cv2.drawContours(frame_with_triangles, [approx], 0, (0, 255, 0), 2)  # Draw a green triangle

    # Display the frame with detected triangles
    cv2.imshow('Video with Triangles', frame_with_triangles)
    frame_count += 1

    if cv2.waitKey(1) & 0xFF == ord('q'):