CONTENT
Detecting champions on the League of Legends minimap involves several key steps and components. The goal is to identify champion icons on the minimap in a given gameplay video using traditional computer vision techniques.
First, we start with two main data sources:
Before attempting detection, we run a script crop_minimap.py that extracts only the minimap region from the full gameplay footage.
This results in a simplified, smaller video showing only the minimap—making our detection task more focused and efficient.
With the minimap-only video, we use LeagueVisionCV.py to apply OpenCV’s template matching technique.
champion_icons directory and resizes them based on a
ratio to ensure they match the scale of the minimap.
ICON_SEARCH_RATIO) so that only the central, most
distinctive part of the icon is used. This can improve matching accuracy.
cv2.matchTemplate against each champion icon. If a match exceeds the THRESHOLD value, we consider it a valid
detection.
Below are some key code snippets from LeagueVisionCV.py and their purposes:
# Compute icon_size based on minimap size
minimap_size = min(h, w)
icon_size = int(minimap_size * ICON_RATIO)
# Load and process icons
icons = load_icons(ICONS_FOLDER, icon_size)
Here, we determine icon_size by taking the smallest dimension of the minimap frame and multiplying by ICON_RATIO. This
ensures the icons scale proportionally to the minimap.
# Crop the icon according to ICON_SEARCH_RATIO
h, w = icon.shape[:2]
crop_margin_w = int(w * ICON_SEARCH_RATIO / 2)
crop_margin_h = int(h * ICON_SEARCH_RATIO / 2)
icon = icon[crop_margin_h:h - crop_margin_h, crop_margin_w:w - crop_margin_w]
This snippet takes the loaded icon and crops it to focus on the center. By removing some of the icon’s border areas, you may get more reliable template matches.
res = cv2.matchTemplate(gray_frame, gray_icon, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
if max_val >= THRESHOLD:
# Draw bounding box
...
Using cv2.matchTemplate, we scan the entire minimap frame for the presence of the champion icon. If max_val (the best
match score) is greater than our chosen threshold, we consider it a successful detection.
top_left = max_loc
bottom_right = (top_left[0] + w_i, top_left[1] + h_i)
cv2.rectangle(detected_frame, top_left, bottom_right, (0, 255, 255), 2)
cv2.putText(detected_frame, f"{champion} ({max_val:.2f})",
(top_left[0], top_left[1]-5),
cv2.FONT_HERSHEY_SIMPLEX,
0.5, (0, 255, 255), 1)
Once a valid match is found, this code draws a yellow bounding box and labels it with the champion’s name and confidence score. The result is a visual overlay on the minimap video.
We experimented with different values for THRESHOLD and ICON_RATIO to find a balance between detecting champions
accurately and minimizing false positives.
After these adjustments, the final output shows bounding boxes over recognized champions on the minimap. By initially showing a video of the raw
minimap and then showing it with detection overlays, readers can see how the code successfully identifies champions.
video1
video2
video3
In all of these examples we see we get a couple champions detected. This is at .8 threshold which i felt like yielded the best results. The
threshold is the confidence level of the match. If the match is below the threshold it will not show.
Lets talk about the first video. It accurately detects 6 of the 9 champions on screen. Along with this it also have some ghosts like for example
on the bottom left sion is detected but is never there. I couldn't get this to go away after tuning the threshold. Im surprised that it detected
some of the champions in the bottom right of the minimap since they were overlapping a lot and the screen indicator which is the white box was
obscuring the icon most of the time. With the other clips i had similar results where it would detect ghost champions that weren't on the map.
Funny enough sion and pantheon were the most consistent. I wonder if this is because the openCV algorithm thinks those sections resemble the
icons i provided.
import os
import cv2
import numpy as np
import argparse
ICONS_FOLDER = 'champion_icons'
THRESHOLD = 0.8
ICON_RATIO = 25 / 280 # Reduced ratio for smaller icons
ICON_SEARCH_RATIO = 0.5 # Percentage of the champion icon to use in matchTemplate
def load_icons(folder, icon_size):
icons = []
for fname in os.listdir(folder):
champ, ext = os.path.splitext(fname)
if ext.lower() == '.png':
path = os.path.join(folder, fname)
icon = cv2.imread(path, cv2.IMREAD_COLOR)
if icon is not None:
# Resize to the icon size
icon = cv2.resize(icon, (icon_size, icon_size), interpolation=cv2.INTER_CUBIC)
# Crop the icon according to ICON_SEARCH_RATIO
h, w = icon.shape[:2]
crop_margin_w = int(w * ICON_SEARCH_RATIO / 2)
crop_margin_h = int(h * ICON_SEARCH_RATIO / 2)
icon = icon[crop_margin_h:h - crop_margin_h, crop_margin_w:w - crop_margin_w]
icons.append((champ, icon))
return icons
def process_video(input_video_path, icons_folder):
cap = cv2.VideoCapture(input_video_path)
if not cap.isOpened():
print(f"Error: Cannot open video {input_video_path}")
return
# Read first frame to determine icon_size
ret, frame = cap.read()
if not ret:
print("Error: Cannot read the first frame.")
return
# Compute icon_size based on the frame's minimap size assumption
# Here we assume the entire frame is the minimap for simplicity.
h, w = frame.shape[:2]
minimap_size = min(h, w)
icon_size = int(minimap_size * ICON_RATIO)
# Now load and adjust icons based on computed icon_size
icons = load_icons(ICONS_FOLDER, icon_size)
if not icons:
print("No icons found. Place PNG icons in the champion_icons folder.")
return
print(f"Loaded {len(icons)} icons.")
# Reset video to first frame
cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
while True:
ret, frame = cap.read()
if not ret:
break
detected_frame = frame.copy()
gray_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
for champion, icon in icons:
gray_icon = cv2.cvtColor(icon, cv2.COLOR_BGR2GRAY)
# Ensure icon fits in the frame
if gray_icon.shape[0] > gray_frame.shape[0] or gray_icon.shape[1] > gray_frame.shape[1]:
continue
# Run matchTemplate on the entire frame
res = cv2.matchTemplate(gray_frame, gray_icon, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
# Debug info
print(f"{champion}: max_val = {max_val}")
if max_val >= THRESHOLD:
# Draw bounding box where match was found
h_i, w_i = gray_icon.shape
top_left = max_loc
bottom_right = (top_left[0] + w_i, top_left[1] + h_i)
cv2.rectangle(detected_frame, top_left, bottom_right, (0, 255, 255), 2)
cv2.putText(detected_frame, f"{champion} ({max_val:.2f})",
(top_left[0], top_left[1]-5),
cv2.FONT_HERSHEY_SIMPLEX,
0.5, (0, 255, 255), 1)
cv2.imshow("Debug Detected", detected_frame)
key = cv2.waitKey(1) & 0xFF
if key == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
def main():
parser = argparse.ArgumentParser(description="Minimal debug code for template matching with smaller icons.")
parser.add_argument("input_video", help="Path to the input .mp4 file (already cropped to minimap).")
args = parser.parse_args()
process_video(args.input_video, ICONS_FOLDER)
if __name__ == "__main__":
main()