#!/opt/homebrew/bin/python3.11 """ Pure OpenCV Stamp Search - No neural networks, very fast Uses: skin detection (hands) + bright regions (paper/envelopes) + small rectangle detection (stamps) """ import os import cv2 import json import time import numpy as np UUID = "384b0ff44aaaa1f1" VIDEO_PATH = f"output/{UUID}/{UUID}.mp4" OUTPUT_DIR = f"output/{UUID}/opencv_stamp_search" os.makedirs(OUTPUT_DIR, exist_ok=True) CROPS_DIR = os.path.join(OUTPUT_DIR, "crops") os.makedirs(CROPS_DIR, exist_ok=True) FRAME_INTERVAL = 5 print("=" * 60) print("⚔ Pure OpenCV Stamp Search") print("=" * 60) cap = cv2.VideoCapture(VIDEO_PATH) fps = cap.get(cv2.CAP_PROP_FPS) total_sec = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) / fps) print(f"šŸ“¹ Video: {total_sec}s ({total_sec // 60} min), {fps:.1f} fps") def find_stamps_pure_opencv(frame): """ Find stamps using only OpenCV: 1. Find hands via skin color 2. Find paper/envelopes via bright rectangular regions 3. In those areas, look for small rectangles with complex patterns """ h, w = frame.shape[:2] results = [] # Collect container regions containers = [] # 1. Skin detection (hands) hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) skin_mask = cv2.inRange(hsv, np.array([0, 20, 60]), np.array([25, 180, 255])) skin_mask += cv2.inRange(hsv, np.array([160, 20, 60]), np.array([179, 180, 255])) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9, 9)) skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_CLOSE, kernel) skin_mask = cv2.morphologyEx(skin_mask, cv2.MORPH_OPEN, kernel) contours, _ = cv2.findContours( skin_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) for cnt in contours: area = cv2.contourArea(cnt) if 1500 < area < h * w * 0.35: x, y, cw, ch = cv2.boundingRect(cnt) containers.append( { "type": "hand", "bbox": [ max(0, x - 50), max(0, y - 50), min(w, x + cw + 50), min(h, y + ch + 50), ], } ) # 2. Bright rectangular regions (paper/envelope) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) _, bright = cv2.threshold(gray, 175, 255, cv2.THRESH_BINARY) kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) bright = cv2.morphologyEx(bright, cv2.MORPH_CLOSE, kernel_rect) contours, _ = cv2.findContours(bright, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area = cv2.contourArea(cnt) if 3000 < area < h * w * 0.5: x, y, cw, ch = cv2.boundingRect(cnt) aspect = cw / ch if ch > 0 else 0 if 0.2 < aspect < 4.0: containers.append( { "type": "paper", "bbox": [ max(0, x - 40), max(0, y - 40), min(w, x + cw + 40), min(h, y + ch + 40), ], } ) if not containers: return results # 3. In each container, search for small stamps for container in containers: cx1, cy1, cx2, cy2 = container["bbox"] region = frame[cy1:cy2, cx1:cx2] if region.size == 0: continue rh, rw = region.shape[:2] region_gray = cv2.cvtColor(region, cv2.COLOR_BGR2GRAY) # Find small rectangular shapes (15-120px) that could be stamps # Use Canny edge detection edges = cv2.Canny(region_gray, 50, 150) contours_s, _ = cv2.findContours( edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE ) for cnt in contours_s: area = cv2.contourArea(cnt) if 200 < area < 15000: # Small objects x, y, sw, sh = cv2.boundingRect(cnt) aspect = sw / sh if sh > 0 else 0 # Stamp-like aspect ratios if 0.4 < aspect < 2.5 and 15 < sw < 120 and 15 < sh < 120: # Check complexity: stamps have patterns, not solid colors roi = region_gray[y : y + sh, x : x + sw] if roi.size == 0: continue # Variance indicates pattern/texture variance = np.var(roi) if variance < 50: continue # Too uniform, probably not a stamp # Check for color diversity (stamps usually have multiple colors) roi_color = region[y : y + sh, x : x + sw] roi_hsv = cv2.cvtColor(roi_color, cv2.COLOR_BGR2HSV) # Count distinct hue values hue_vals = roi_hsv[:, :, 0] unique_hues = len(np.unique(hue_vals)) # Calculate saturation (stamps usually have color) sat_mean = np.mean(roi_hsv[:, :, 1]) # Score: higher variance + more colors = more likely a stamp score = min( 1.0, (variance / 500 + unique_hues / 50 + sat_mean / 200) / 3 ) if score > 0.15: # Threshold # Map back to original frame ox1 = cx1 + x oy1 = cy1 + y ox2 = cx1 + x + sw oy2 = cy1 + y + sh crop = frame[oy1:oy2, ox1:ox2] if crop.size == 0: continue results.append( { "timestamp": 0, # Will be set by caller "container": container["type"], "stamp_term": "opencv_rect", "score": score, "bbox": [ox1, oy1, ox2, oy2], "size": [sw, sh], "variance": float(variance), "unique_hues": int(unique_hues), "saturation": float(sat_mean), } ) return results all_results = [] start_time = time.time() for sec in range(0, total_sec, FRAME_INTERVAL): cap.set(cv2.CAP_PROP_POS_MSEC, sec * 1000) ret, frame = cap.read() if not ret: continue elapsed = time.time() - start_time progress = sec / total_sec * 100 eta = ( (elapsed / (sec / FRAME_INTERVAL + 1)) * (total_sec / FRAME_INTERVAL - sec / FRAME_INTERVAL - 1) if sec > 0 else 0 ) results = find_stamps_pure_opencv(frame) # Set timestamp for r in results: r["timestamp"] = sec if results: print( f" [{sec}s | {progress:.0f}% | ETA:{eta:.0f}s] Found {len(results)} candidates" ) for r in results: ox1, oy1, ox2, oy2 = r["bbox"] crop = frame[oy1:oy2, ox1:ox2] if crop.size > 0: crop_name = f"stamp_{sec}s_{r['container']}_{r['score']:.2f}.jpg" cv2.imwrite(os.path.join(CROPS_DIR, crop_name), crop) cv2.rectangle(frame, (ox1, oy1), (ox2, oy2), (0, 255, 0), 2) cv2.putText( frame, f"{r['score']:.2f}", (ox1, oy1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1, ) ann_path = os.path.join(OUTPUT_DIR, f"annotated_{sec}s.jpg") cv2.imwrite(ann_path, frame) all_results.extend(results) else: if sec % 120 == 0: print( f" [{sec // 60}min/{total_sec // 60}min | {progress:.0f}% | ETA:{eta:.0f}s] Scanning..." ) cap.release() # Sort and deduplicate all_results.sort(key=lambda x: x["score"], reverse=True) seen = set() unique = [] for r in all_results: ts = r["timestamp"] if ts not in seen: seen.add(ts) unique.append(r) print(f"\n{'=' * 60}") print(f"šŸ“Š Found {len(unique)} stamp candidates") for r in unique[:20]: print( f" šŸŽÆ {r['timestamp']}s | score:{r['score']:.2f} | via:{r['container']} | size:{r['size'][0]}x{r['size'][1]} | var:{r['variance']:.0f} hues:{r['unique_hues']}" ) with open(os.path.join(OUTPUT_DIR, "results.json"), "w") as f: json.dump(unique, f, indent=2) print(f"\nšŸ Done. Crops: {CROPS_DIR}")