Thumbnail Extraction¶
SSIM-based thumbnail matching and extraction API.
Module Documentation¶
thumbnail_extractor
¶
Extract thumbnail images from video files using SSIM-based frame matching.
Classes¶
ThumbnailResult
¶
Bases: NamedTuple
Result from a thumbnail extraction operation.
Attributes:
| Name | Type | Description |
|---|---|---|
success |
bool
|
Whether thumbnail extraction succeeded. |
frame_number |
int
|
Frame number where match was found. |
timestamp_ms |
float
|
Timestamp of matched frame in milliseconds. |
ssim_score |
float
|
SSIM similarity score of the match (0.0-1.0). |
output_path |
Path
|
Path where thumbnail image was saved. |
ThumbnailExtractor
¶
ThumbnailExtractor(
video_path: Path,
template_path: Optional[Path] = None,
resolution: int = 3,
scan_duration: int = 120,
threshold: float = 0.8,
)
Extract thumbnail frames from videos using SSIM-based template matching.
Initialize ThumbnailExtractor.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video_path
|
Path
|
Path to video file |
required |
template_path
|
Optional[Path]
|
Path to template image (uses default if None) |
None
|
resolution
|
int
|
Frames to check per second (matches Loups) |
3
|
scan_duration
|
int
|
Maximum seconds to scan from video start |
120
|
threshold
|
float
|
Minimum SSIM score to accept (0.0-1.0) |
0.8
|
Source code in loups/thumbnail_extractor.py
Functions¶
get_default_thumbnail_template
¶
Get path to bundled default thumbnail template.
Returns:
| Type | Description |
|---|---|
Path
|
Path to the bundled thumbnail_template.png file. |
load_template
¶
Load thumbnail template, using default if not specified.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
template_path
|
Optional[Path]
|
Path to template image (None for default) |
None
|
Returns:
| Type | Description |
|---|---|
ndarray
|
Template image as numpy array |
Raises:
| Type | Description |
|---|---|
FileNotFoundError
|
If template file doesn't exist |
Source code in loups/thumbnail_extractor.py
generate_default_output_path
¶
Generate default thumbnail output path in current working directory.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video_path
|
Path
|
Path to input video file |
required |
Returns:
| Type | Description |
|---|---|
Path
|
Path to output thumbnail in cwd |
Examples:
Input: '/path/to/game.mp4' → Output: './game-thumbnail.jpg' Input: 'softball.mp4' → Output: './softball-thumbnail.jpg'
Source code in loups/thumbnail_extractor.py
calculate_ssim
¶
Calculate SSIM (Structural Similarity Index) between frame and template.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
frame
|
ndarray
|
Video frame as numpy array |
required |
template
|
ndarray
|
Template image as numpy array |
required |
Returns:
| Type | Description |
|---|---|
float
|
SSIM score (0.0 to 1.0, where 1.0 is perfect match) |
Source code in loups/thumbnail_extractor.py
extract_thumbnail
¶
extract_thumbnail(
video_path: Path,
template_path: Optional[Path] = None,
output_path: Optional[Path] = None,
threshold: float = 0.35,
scan_duration: int = 120,
resolution: int = 3,
on_progress: Optional[
Callable[[int, int], None]
] = None,
quiet: bool = False,
) -> Optional[ThumbnailResult]
Extract first frame matching template above threshold.
Core thumbnail extraction logic used by both CLI commands. Scans video from start, checking frames at specified resolution. Stops immediately when a frame exceeds the SSIM threshold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
video_path
|
Path
|
Path to video file |
required |
template_path
|
Optional[Path]
|
Path to template image (uses default if None) |
None
|
output_path
|
Optional[Path]
|
Where to save thumbnail (generates default in cwd if None) |
None
|
threshold
|
float
|
Minimum SSIM score to accept (0.0-1.0) |
0.35
|
scan_duration
|
int
|
Maximum seconds to scan from video start |
120
|
resolution
|
int
|
Frames to process per second |
3
|
on_progress
|
Optional[Callable[[int, int], None]]
|
Optional callback for progress updates |
None
|
quiet
|
bool
|
Suppress output |
False
|
Returns:
| Type | Description |
|---|---|
Optional[ThumbnailResult]
|
ThumbnailResult on success, None if no frame exceeds threshold |
Source code in loups/thumbnail_extractor.py
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 | |
:material-flow-chart: Extraction Process¶
graph TD
A[Start: Video + Template] --> B{Load Template}
B --> C[Initialize VideoCapture]
C --> D{Scan Frames}
D --> E[Read Next Frame]
E --> F{Frame Valid?}
F -->|No| G[End: No Match Found]
F -->|Yes| H[Resize to Template Size]
H --> I[Calculate SSIM Score]
I --> J{Score >= Threshold?}
J -->|No| K{More Frames?}
K -->|Yes, within duration| E
K -->|No, exceeded duration| G
J -->|Yes| L[Match Found!]
L --> M[Save as JPEG]
M --> N[End: Success]
style A fill:#00ffff,stroke:#000,color:#000
style L fill:#66bb6a,stroke:#000,color:#000
style N fill:#00ffff,stroke:#000,color:#000
style G fill:#ef5350,stroke:#000,color:#fff
style I fill:#00b8d4,stroke:#000,color:#fff
Key Steps:
- Template Loading - Load and validate template image
- Frame Scanning - Iterate through video frames at specified FPS
- SSIM Calculation - Compute Structural Similarity Index
- Threshold Check - Compare against minimum threshold
- First-Match Strategy - Stop immediately on first match
- Save Output - Write matched frame as JPEG
Usage Examples¶
Basic Extraction¶
from loups.thumbnail_extractor import extract_thumbnail
# Extract with default template
thumbnail_path = extract_thumbnail(
video_path="game_video.mp4"
)
print(f"Thumbnail saved to: {thumbnail_path}")
Custom Template¶
from loups.thumbnail_extractor import extract_thumbnail
thumbnail_path = extract_thumbnail(
video_path="video.mp4",
template_path="title_screen_template.png",
output_path="custom_thumbnail.jpg"
)
Fine-Tuned Extraction¶
from loups.thumbnail_extractor import extract_thumbnail
thumbnail_path = extract_thumbnail(
video_path="video.mp4",
template_path="template.png",
output_path="thumb.jpg",
threshold=0.7, # Stricter matching (default: 0.35)
scan_duration=180, # Scan first 3 minutes (default: 120)
frames_per_second=5, # Sample 5 FPS (default: 3)
quiet=False # Show progress
)
Parameters Explained¶
Threshold (0.0 - 1.0)¶
The SSIM threshold determines how similar a frame must be to the template:
| Threshold | Matching Behavior | Use Case |
|---|---|---|
0.2 - 0.4 |
Loose - More matches | Varied title screens |
0.5 - 0.7 |
Balanced - Moderate | Most use cases |
0.8 - 1.0 |
Strict - Exact match | Identical frames only |
Default: 0.35 (loose, good for varied content)
Finding the Right Threshold
Start with default (0.35) and adjust:
- Too many false positives? → Increase threshold
- Can't find match? → Decrease threshold
- Test on sample to find sweet spot
Scan Duration¶
How many seconds to scan from the beginning:
# Scan first 2 minutes
extract_thumbnail(video_path="video.mp4", scan_duration=120)
# Scan first 5 minutes
extract_thumbnail(video_path="video.mp4", scan_duration=300)
# Scan entire video (slow!)
extract_thumbnail(video_path="video.mp4", scan_duration=999999)
Performance Impact
Longer scan duration = slower extraction. Most title screens appear in first 2 minutes.
Frames Per Second¶
Frame sampling rate:
# Sample every frame (slow, thorough)
extract_thumbnail(video_path="video.mp4", frames_per_second=30)
# Sample 3 FPS (default, balanced)
extract_thumbnail(video_path="video.mp4", frames_per_second=3)
# Sample 1 FPS (fast, might miss frame)
extract_thumbnail(video_path="video.mp4", frames_per_second=1)
Trade-off: Higher FPS = more accurate but slower
SSIM vs Template Matching¶
Loups uses SSIM (Structural Similarity Index) for thumbnails instead of template matching:
| Feature | Template Matching | SSIM |
|---|---|---|
| Purpose | Find specific regions | Compare full frames |
| Speed | Fast | Moderate |
| Accuracy | High for patterns | High for images |
| Use Case | Chapter detection | Thumbnail extraction |
| Sensitivity | Position-sensitive | Content-aware |
Why SSIM for thumbnails?
- Accounts for overall visual similarity
- Robust to minor variations
- Good for comparing full frames
- Perceptually meaningful
Testing & Debugging¶
Verify Template Quality¶
import cv2
from skimage.metrics import structural_similarity as ssim
# Load template and test frame
template = cv2.imread("template.png")
test_frame = cv2.imread("test_frame.png")
# Resize to same size
test_frame_resized = cv2.resize(
test_frame,
(template.shape[1], template.shape[0])
)
# Calculate SSIM
score = ssim(template, test_frame_resized, multichannel=True)
print(f"SSIM Score: {score:.3f}")
# Interpretation
if score >= 0.7:
print("✅ Strong match")
elif score >= 0.4:
print("⚠️ Moderate match")
else:
print("❌ Poor match")
Extract Multiple Frames¶
For testing, you might want to extract multiple frames:
import cv2
from pathlib import Path
def extract_frames(video_path, output_dir, fps=1, duration=120):
"""Extract frames for template testing."""
output_dir = Path(output_dir)
output_dir.mkdir(exist_ok=True)
cap = cv2.VideoCapture(video_path)
video_fps = cap.get(cv2.CAP_PROP_FPS)
frame_interval = int(video_fps / fps)
frame_count = 0
saved_count = 0
while frame_count < (duration * video_fps):
ret, frame = cap.read()
if not ret:
break
if frame_count % frame_interval == 0:
output_path = output_dir / f"frame_{saved_count:04d}.jpg"
cv2.imwrite(str(output_path), frame)
saved_count += 1
frame_count += 1
cap.release()
print(f"Extracted {saved_count} frames to {output_dir}")
# Usage
extract_frames("video.mp4", "test_frames", fps=1, duration=60)
Batch Thumbnail Extraction¶
Process Multiple Videos¶
from pathlib import Path
from loups.thumbnail_extractor import extract_thumbnail
video_dir = Path("videos")
thumb_dir = Path("thumbnails")
thumb_dir.mkdir(exist_ok=True)
for video in video_dir.glob("*.mp4"):
print(f"Extracting thumbnail for: {video.name}")
try:
thumbnail = extract_thumbnail(
video_path=str(video),
output_path=str(thumb_dir / f"{video.stem}.jpg"),
quiet=True
)
print(f" ✅ Saved: {thumbnail}")
except Exception as e:
print(f" ❌ Error: {e}")
Parallel Processing¶
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
from loups.thumbnail_extractor import extract_thumbnail
def extract_single(video_path):
"""Extract thumbnail for single video."""
try:
output = f"thumbnails/{video_path.stem}.jpg"
return extract_thumbnail(
video_path=str(video_path),
output_path=output,
quiet=True
)
except Exception as e:
return f"Error: {e}"
# Get all videos
videos = list(Path("videos").glob("*.mp4"))
# Process in parallel
with ThreadPoolExecutor(max_workers=4) as executor:
results = executor.map(extract_single, videos)
# Print results
for video, result in zip(videos, results):
print(f"{video.name}: {result}")
Tips & Best Practices¶
Best Practices
- Template from actual frame - Use a real frame from your video
- Start with defaults - Adjust only if needed
- Test on one video first - Verify settings work
- Use quiet mode in scripts - Cleaner output
Common Issues
No match found?
- Try lower threshold (0.2 - 0.3)
- Increase scan duration
- Increase frames_per_second
- Verify template matches video content
Wrong frame matched?
- Increase threshold for stricter matching
- Use more specific template
- Reduce scan duration if title appears early
Related¶
- Loups Class - Main API
- CLI Module - Command-line usage
- CLI Thumbnail Reference - User guide