- Update ASR, face, OCR, pose processors - Add release pre-flight check script - Add synonym generation, chunk processing scripts - Add face recognition, stamp search utilities
259 lines
8.6 KiB
Python
259 lines
8.6 KiB
Python
#!/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}")
|