| name | run2_advanced_template_matching |
| description | Advanced OpenCV template matching with dynamic threshold tuning, robust NMS filtering, and multi-scale detection for object counting in images. |
Advanced Template Matching for Object Counting
Overview
Robust object detection and counting using OpenCV with improved threshold selection, non-maximum suppression, and diagnostic capabilities.
Installation
apt-get install python3-opencv
Core Matching Methods
TM_CCOEFF_NORMED (Recommended)
- Range: -1 to 1 (higher is better match)
- Best for: Objects similar to template
- Invariance: Not scale/rotation invariant
TM_SQDIFF_NORMED (Alternative)
- Range: 0 to 1 (lower is better match)
- Best for: Precise matching
Dynamic Threshold Selection
Method 1: Percentile-Based (Adaptive)
import numpy as np
def count_objects_adaptive(frame, template, percentile=95):
"""Count using adaptive threshold based on match distribution"""
result = cv2.matchTemplate(frame, template, cv2.TM_CCOEFF_NORMED)
threshold = np.percentile(result.flatten(), percentile)
loc = np.where(result >= threshold)
matches = list(zip(*loc[::-1]))
return matches, threshold
Method 2: Histogram Analysis
def find_optimal_threshold(result, min_matches=1):
"""Find threshold that yields reasonable match count"""
flat = result.flatten()
for threshold in np.arange(0.9, 0.5, -0.05):
count = np.sum(flat >= threshold)
if count >= min_matches:
return threshold
return 0.5
Robust Non-Maximum Suppression (NMS)
Basic NMS
def nms_simple(matches, min_distance=10):
"""Remove overlapping detections based on distance"""
if not matches:
return []
filtered = []
for match in matches:
overlaps = False
for existing in filtered:
dist = np.sqrt((match[0] - existing[0])**2 + (match[1] - existing[1])**2)
if dist < min_distance:
overlaps = True
break
if not overlaps:
filtered.append(match)
return filtered
Advanced NMS with Confidence Scores
def nms_with_confidence(frame, template, result, threshold=0.7, min_distance=10):
"""NMS considering match confidence scores"""
loc = np.where(result >= threshold)
matches = [(loc[1][i], loc[0][i], result[loc[0][i], loc[1][i]])
for i in range(len(loc[0]))]
matches = sorted(matches, key=lambda x: x[2], reverse=True)
filtered = []
for match in matches:
overlaps = False
for existing in filtered:
dist = np.sqrt((match[0] - existing[0])**2 + (match[1] - existing[1])**2)
if dist < min_distance:
overlaps = True
break
if not overlaps:
filtered.append(match)
return len(filtered)
Complete Detection Pipeline
import cv2
import numpy as np
def count_objects_robust(frame_path, template_path,
threshold=None,
min_distance=10,
debug=False):
"""
Robust object counting with automatic threshold and NMS
Args:
frame_path: Path to frame image
template_path: Path to template image
threshold: Match threshold (auto if None)
min_distance: Minimum distance between matches
debug: Print diagnostic information
Returns:
Count of detected objects
"""
frame = cv2.imread(frame_path, cv2.IMREAD_GRAYSCALE)
template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
if frame is None or template is None:
return 0
h, w = template.shape
if h > frame.shape[0] or w > frame.shape[1]:
if debug:
print(f"Template larger than frame!")
return 0
result = cv2.matchTemplate(frame, template, cv2.TM_CCOEFF_NORMED)
if threshold is None:
threshold = np.percentile(result.flatten(), 70)
if debug:
print(f"Auto threshold: {threshold:.3f}")
loc = np.where(result >= threshold)
matches = ((*loc[::-]))
debug:
()
filtered = nms_simple(matches, min_distance)
debug:
()
(filtered)
():
matches:
[]
filtered = []
matches:
overlaps =
existing filtered:
dist = np.sqrt(([] - existing[])** + ([] - existing[])**)
dist < min_distance:
overlaps =
overlaps:
filtered.append()
filtered
Tuning Guide
If Overcounting (Too Many Detections)
- Increase threshold: 0.65 → 0.70 → 0.75
- Increase min_distance: 10 → 15 → 20
- Verify template quality
If Undercounting (Too Few Detections)
- Decrease threshold: 0.70 → 0.65 → 0.60
- Check template matches video content
- Verify images are grayscale
Template Quality Checks
h, w = template.shape
aspect = w / h
print(f"Template size: {w}x{h}, aspect: {aspect:.2f}")
print(f"Template brightness range: {template.min()}-{template.max()}")
Debugging Visualizations
def visualize_matches(frame_path, template_path, threshold=0.7):
"""Visualize matches on frame (requires display)"""
frame = cv2.imread(frame_path)
template = cv2.imread(template_path)
frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY)
result = cv2.matchTemplate(frame_gray, template_gray, cv2.TM_CCOEFF_NORMED)
loc = np.where(result >= threshold)
h, w = template_gray.shape
for pt in zip(*loc[::-1]):
cv2.rectangle(frame, pt, (pt[0] + w, pt[1] + h), (0, 255, 0), 2)
cv2.imwrite('/tmp/matches.png', frame)
print(f"Saved visualization to /tmp/matches.png")