Reorganize repo: compartmentalize scripts by client/project
Move 150+ scripts from root and scripts/ into client/project directories: - clients/dataforth/scripts/ (110 files: AD2, sync, SSH, DB, DOS scripts) - clients/bg-builders/scripts/ (14 files: Lesley mgmt, Exchange, termination) - clients/internal-infrastructure/scripts/ (10 files: GDAP, Gitea, backups) - projects/msp-tools/scripts/ (9 files: CIPP, MSP onboarding, Datto) - projects/gururmm-agent/scripts/ (3 files: API test, JWT, record counts) - clients/glaztech/scripts/ (1 file: CentraStage removal) Also reorganized: - VPN scripts → infrastructure/vpn-configs/ - Retrieved API/JS files → api/ - Forum posts → projects/community-forum/forum-posts/ - SSH docs → clients/internal-infrastructure/docs/ - NWTOC/CTONW docs → projects/wrightstown-smarthome/docs/ - ACG website files → projects/internal/acg-website-2025/ - Dataforth docs → clients/dataforth/docs/ - schema-retrieved.sql → docs/database/ Deleted 24 tmp_*.ps1 one-off debug scripts (preserved in git history). Root reduced from 220+ files to 62 items (docs + directories only). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
237
tools/extract_license_plate.py
Normal file
237
tools/extract_license_plate.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
Extract and enhance license plate from Tesla dash cam video
|
||||
Target: Pickup truck at 25-30 seconds
|
||||
"""
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from pathlib import Path
|
||||
from PIL import Image, ImageEnhance, ImageFilter
|
||||
import os
|
||||
|
||||
def extract_frames_from_range(video_path, start_time, end_time, fps=10):
|
||||
"""Extract frames from specific time range at given fps"""
|
||||
cap = cv2.VideoCapture(str(video_path))
|
||||
video_fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
|
||||
frames = []
|
||||
timestamps = []
|
||||
|
||||
# Calculate frame numbers for the time range
|
||||
start_frame = int(start_time * video_fps)
|
||||
end_frame = int(end_time * video_fps)
|
||||
frame_interval = int(video_fps / fps)
|
||||
|
||||
print(f"[INFO] Video FPS: {video_fps}")
|
||||
print(f"[INFO] Extracting frames {start_frame} to {end_frame} every {frame_interval} frames")
|
||||
|
||||
cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
|
||||
current_frame = start_frame
|
||||
|
||||
while current_frame <= end_frame:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
if (current_frame - start_frame) % frame_interval == 0:
|
||||
timestamp = current_frame / video_fps
|
||||
frames.append(frame)
|
||||
timestamps.append(timestamp)
|
||||
print(f"[OK] Extracted frame at {timestamp:.2f}s (frame {current_frame})")
|
||||
|
||||
current_frame += 1
|
||||
|
||||
cap.release()
|
||||
return frames, timestamps
|
||||
|
||||
def detect_license_plates(frame):
|
||||
"""Detect potential license plate regions using multiple methods"""
|
||||
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# Method 1: Edge detection + contours
|
||||
edges = cv2.Canny(gray, 50, 200)
|
||||
contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
|
||||
|
||||
plate_candidates = []
|
||||
|
||||
for contour in contours:
|
||||
x, y, w, h = cv2.boundingRect(contour)
|
||||
aspect_ratio = w / float(h) if h > 0 else 0
|
||||
area = w * h
|
||||
|
||||
# License plate characteristics: aspect ratio ~2-5, reasonable size
|
||||
if 1.5 < aspect_ratio < 6 and 1000 < area < 50000:
|
||||
plate_candidates.append({
|
||||
'bbox': (x, y, w, h),
|
||||
'aspect_ratio': aspect_ratio,
|
||||
'area': area,
|
||||
'score': area * aspect_ratio # Simple scoring
|
||||
})
|
||||
|
||||
# Sort by score and return top candidates
|
||||
plate_candidates.sort(key=lambda x: x['score'], reverse=True)
|
||||
return plate_candidates[:10] # Return top 10 candidates
|
||||
|
||||
def enhance_license_plate(plate_img, upscale_factor=6):
|
||||
"""Apply multiple enhancement techniques to license plate image"""
|
||||
enhanced_versions = []
|
||||
|
||||
# Convert to PIL for some operations
|
||||
plate_pil = Image.fromarray(cv2.cvtColor(plate_img, cv2.COLOR_BGR2RGB))
|
||||
|
||||
# 1. Upscale first
|
||||
new_size = (plate_pil.width * upscale_factor, plate_pil.height * upscale_factor)
|
||||
upscaled = plate_pil.resize(new_size, Image.Resampling.LANCZOS)
|
||||
enhanced_versions.append(("upscaled", upscaled))
|
||||
|
||||
# 2. Sharpen heavily
|
||||
sharpened = upscaled.filter(ImageFilter.SHARPEN)
|
||||
sharpened = sharpened.filter(ImageFilter.SHARPEN)
|
||||
enhanced_versions.append(("sharpened", sharpened))
|
||||
|
||||
# 3. High contrast
|
||||
contrast = ImageEnhance.Contrast(sharpened)
|
||||
high_contrast = contrast.enhance(2.5)
|
||||
enhanced_versions.append(("high_contrast", high_contrast))
|
||||
|
||||
# 4. Brightness adjustment
|
||||
brightness = ImageEnhance.Brightness(high_contrast)
|
||||
bright = brightness.enhance(1.3)
|
||||
enhanced_versions.append(("bright_contrast", bright))
|
||||
|
||||
# 5. Adaptive thresholding (OpenCV)
|
||||
gray_cv = cv2.cvtColor(np.array(upscaled), cv2.COLOR_RGB2GRAY)
|
||||
adaptive = cv2.adaptiveThreshold(gray_cv, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
|
||||
cv2.THRESH_BINARY, 11, 2)
|
||||
enhanced_versions.append(("adaptive_thresh", Image.fromarray(adaptive)))
|
||||
|
||||
# 6. Bilateral filter + sharpen
|
||||
bilateral = cv2.bilateralFilter(np.array(upscaled), 9, 75, 75)
|
||||
bilateral_pil = Image.fromarray(bilateral)
|
||||
bilateral_sharp = bilateral_pil.filter(ImageFilter.SHARPEN)
|
||||
enhanced_versions.append(("bilateral_sharp", bilateral_sharp))
|
||||
|
||||
# 7. Unsharp mask
|
||||
unsharp = upscaled.filter(ImageFilter.UnsharpMask(radius=2, percent=200, threshold=3))
|
||||
enhanced_versions.append(("unsharp_mask", unsharp))
|
||||
|
||||
# 8. Extreme sharpening
|
||||
extreme_sharp = sharpened.filter(ImageFilter.SHARPEN)
|
||||
extreme_sharp = extreme_sharp.filter(ImageFilter.UnsharpMask(radius=3, percent=250, threshold=2))
|
||||
enhanced_versions.append(("extreme_sharp", extreme_sharp))
|
||||
|
||||
return enhanced_versions
|
||||
|
||||
def main():
|
||||
video_path = Path("E:/TeslaCam/SavedClips/2026-02-03_19-48-23/2026-02-03_19-42-36-front.mp4")
|
||||
output_dir = Path("D:/Scratchpad/pickup_truck_25-30s")
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"[INFO] Processing video: {video_path}")
|
||||
print(f"[INFO] Output directory: {output_dir}")
|
||||
|
||||
# Extract frames from 25-30 second range at 10 fps
|
||||
start_time = 25.0
|
||||
end_time = 30.0
|
||||
target_fps = 10
|
||||
|
||||
frames, timestamps = extract_frames_from_range(video_path, start_time, end_time, target_fps)
|
||||
print(f"[OK] Extracted {len(frames)} frames")
|
||||
|
||||
# Process each frame
|
||||
all_plates = []
|
||||
|
||||
for idx, (frame, timestamp) in enumerate(zip(frames, timestamps)):
|
||||
frame_name = f"frame_{timestamp:.2f}s"
|
||||
|
||||
# Save original frame
|
||||
frame_path = output_dir / f"{frame_name}_original.jpg"
|
||||
cv2.imwrite(str(frame_path), frame)
|
||||
|
||||
# Detect license plates
|
||||
plate_candidates = detect_license_plates(frame)
|
||||
print(f"[INFO] Frame {timestamp:.2f}s: Found {len(plate_candidates)} plate candidates")
|
||||
|
||||
# Process each candidate
|
||||
for plate_idx, candidate in enumerate(plate_candidates[:5]): # Top 5 candidates
|
||||
x, y, w, h = candidate['bbox']
|
||||
|
||||
# Extract plate region with some padding
|
||||
padding = 10
|
||||
x1 = max(0, x - padding)
|
||||
y1 = max(0, y - padding)
|
||||
x2 = min(frame.shape[1], x + w + padding)
|
||||
y2 = min(frame.shape[0], y + h + padding)
|
||||
|
||||
plate_crop = frame[y1:y2, x1:x2]
|
||||
|
||||
if plate_crop.size == 0:
|
||||
continue
|
||||
|
||||
# Draw bounding box on original frame
|
||||
frame_with_box = frame.copy()
|
||||
cv2.rectangle(frame_with_box, (x, y), (x+w, y+h), (0, 255, 0), 2)
|
||||
cv2.putText(frame_with_box, f"Candidate {plate_idx+1}", (x, y-10),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
|
||||
|
||||
# Save frame with detection box
|
||||
detection_path = output_dir / f"{frame_name}_detection_{plate_idx+1}.jpg"
|
||||
cv2.imwrite(str(detection_path), frame_with_box)
|
||||
|
||||
# Save raw crop
|
||||
crop_path = output_dir / f"{frame_name}_plate_{plate_idx+1}_raw.jpg"
|
||||
cv2.imwrite(str(crop_path), plate_crop)
|
||||
|
||||
# Enhance plate
|
||||
enhanced_versions = enhance_license_plate(plate_crop, upscale_factor=6)
|
||||
|
||||
for enhance_name, enhanced_img in enhanced_versions:
|
||||
enhance_path = output_dir / f"{frame_name}_plate_{plate_idx+1}_{enhance_name}.jpg"
|
||||
enhanced_img.save(str(enhance_path))
|
||||
|
||||
all_plates.append({
|
||||
'timestamp': timestamp,
|
||||
'candidate_idx': plate_idx,
|
||||
'bbox': (x, y, w, h),
|
||||
'aspect_ratio': candidate['aspect_ratio'],
|
||||
'area': candidate['area']
|
||||
})
|
||||
|
||||
print(f"[OK] Saved candidate {plate_idx+1} from {timestamp:.2f}s (AR: {candidate['aspect_ratio']:.2f}, Area: {candidate['area']})")
|
||||
|
||||
# Create summary
|
||||
summary_path = output_dir / "summary.txt"
|
||||
with open(summary_path, 'w') as f:
|
||||
f.write("License Plate Extraction Summary\n")
|
||||
f.write("=" * 60 + "\n\n")
|
||||
f.write(f"Video: {video_path}\n")
|
||||
f.write(f"Time Range: {start_time}-{end_time} seconds\n")
|
||||
f.write(f"Frames Extracted: {len(frames)}\n")
|
||||
f.write(f"Total Plate Candidates: {len(all_plates)}\n\n")
|
||||
|
||||
f.write("Candidates by Frame:\n")
|
||||
f.write("-" * 60 + "\n")
|
||||
for plate in all_plates:
|
||||
f.write(f"Time: {plate['timestamp']:.2f}s | ")
|
||||
f.write(f"Candidate #{plate['candidate_idx']+1} | ")
|
||||
f.write(f"Aspect Ratio: {plate['aspect_ratio']:.2f} | ")
|
||||
f.write(f"Area: {plate['area']}\n")
|
||||
|
||||
f.write("\n" + "=" * 60 + "\n")
|
||||
f.write("Enhancement Techniques Applied:\n")
|
||||
f.write("- Upscaled 6x (LANCZOS)\n")
|
||||
f.write("- Heavy sharpening\n")
|
||||
f.write("- High contrast boost\n")
|
||||
f.write("- Brightness adjustment\n")
|
||||
f.write("- Adaptive thresholding\n")
|
||||
f.write("- Bilateral filtering\n")
|
||||
f.write("- Unsharp masking\n")
|
||||
f.write("- Extreme sharpening\n")
|
||||
|
||||
print(f"\n[SUCCESS] Processing complete!")
|
||||
print(f"[INFO] Output directory: {output_dir}")
|
||||
print(f"[INFO] Total plate candidates processed: {len(all_plates)}")
|
||||
print(f"[INFO] Summary saved to: {summary_path}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
145
tools/review_best_plates.py
Normal file
145
tools/review_best_plates.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""
|
||||
Identify the best license plate candidates from extraction results
|
||||
Filter by ideal aspect ratio (2-5) and larger area
|
||||
"""
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
def parse_summary(summary_path):
|
||||
"""Parse summary.txt to find best candidates"""
|
||||
candidates = []
|
||||
|
||||
with open(summary_path, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
# Parse each candidate line
|
||||
pattern = r'Time: ([\d.]+)s \| Candidate #(\d+) \| Aspect Ratio: ([\d.]+) \| Area: (\d+)'
|
||||
|
||||
for match in re.finditer(pattern, content):
|
||||
timestamp = float(match.group(1))
|
||||
candidate_num = int(match.group(2))
|
||||
aspect_ratio = float(match.group(3))
|
||||
area = int(match.group(4))
|
||||
|
||||
# Score candidates based on ideal license plate characteristics
|
||||
# Ideal aspect ratio: 3-4.5 (most US license plates)
|
||||
# Prefer larger areas (closer to camera)
|
||||
ar_score = 0
|
||||
if 2.5 <= aspect_ratio <= 5.0:
|
||||
# Best score for aspect ratio between 3-4.5
|
||||
if 3.0 <= aspect_ratio <= 4.5:
|
||||
ar_score = 100
|
||||
else:
|
||||
ar_score = 50
|
||||
|
||||
# Area score (normalize to 0-100)
|
||||
area_score = min(area / 500, 100) # Scale area
|
||||
|
||||
# Combined score
|
||||
total_score = (ar_score * 0.6) + (area_score * 0.4)
|
||||
|
||||
candidates.append({
|
||||
'timestamp': timestamp,
|
||||
'candidate': candidate_num,
|
||||
'aspect_ratio': aspect_ratio,
|
||||
'area': area,
|
||||
'score': total_score
|
||||
})
|
||||
|
||||
return candidates
|
||||
|
||||
def main():
|
||||
summary_path = Path("D:/Scratchpad/pickup_truck_25-30s/summary.txt")
|
||||
output_dir = Path("D:/Scratchpad/pickup_truck_25-30s")
|
||||
|
||||
print("[INFO] Analyzing license plate candidates...")
|
||||
candidates = parse_summary(summary_path)
|
||||
|
||||
# Sort by score
|
||||
candidates.sort(key=lambda x: x['score'], reverse=True)
|
||||
|
||||
# Show top 20 candidates
|
||||
print("\n" + "=" * 80)
|
||||
print("TOP 20 LICENSE PLATE CANDIDATES")
|
||||
print("=" * 80)
|
||||
print(f"{'Rank':<6} {'Time':<10} {'Cand':<6} {'AR':<8} {'Area':<10} {'Score':<8} {'Files'}")
|
||||
print("-" * 80)
|
||||
|
||||
for idx, candidate in enumerate(candidates[:20], 1):
|
||||
timestamp = candidate['timestamp']
|
||||
cand_num = candidate['candidate']
|
||||
ar = candidate['aspect_ratio']
|
||||
area = candidate['area']
|
||||
score = candidate['score']
|
||||
|
||||
# Check which files exist for this candidate
|
||||
frame_name = f"frame_{timestamp:.2f}s"
|
||||
base_pattern = f"{frame_name}_plate_{cand_num}_"
|
||||
|
||||
# Count enhancement files
|
||||
enhancement_files = list(output_dir.glob(f"{base_pattern}*.jpg"))
|
||||
enhancement_count = len([f for f in enhancement_files if '_raw' not in f.name])
|
||||
|
||||
print(f"{idx:<6} {timestamp:<10.2f} {cand_num:<6} {ar:<8.2f} {area:<10} {score:<8.1f} {enhancement_count} enhanced")
|
||||
|
||||
# Create recommendation file
|
||||
recommendation_path = output_dir / "RECOMMENDATIONS.txt"
|
||||
with open(recommendation_path, 'w') as f:
|
||||
f.write("LICENSE PLATE EXTRACTION - TOP CANDIDATES\n")
|
||||
f.write("=" * 80 + "\n\n")
|
||||
f.write("These are the top 20 most likely license plate candidates based on:\n")
|
||||
f.write("- Aspect ratio (ideal: 3.0-4.5 for US plates)\n")
|
||||
f.write("- Area size (larger = closer to camera)\n\n")
|
||||
f.write("REVIEW THESE FILES FIRST:\n")
|
||||
f.write("-" * 80 + "\n\n")
|
||||
|
||||
for idx, candidate in enumerate(candidates[:20], 1):
|
||||
timestamp = candidate['timestamp']
|
||||
cand_num = candidate['candidate']
|
||||
ar = candidate['aspect_ratio']
|
||||
area = candidate['area']
|
||||
score = candidate['score']
|
||||
|
||||
f.write(f"RANK {idx}: Time {timestamp:.2f}s - Candidate #{cand_num}\n")
|
||||
f.write(f" Aspect Ratio: {ar:.2f} | Area: {area} | Score: {score:.1f}\n")
|
||||
f.write(f" Files to review:\n")
|
||||
|
||||
frame_name = f"frame_{timestamp:.2f}s"
|
||||
|
||||
# List specific enhancement files to check
|
||||
enhancements = [
|
||||
f"{frame_name}_detection_{cand_num}.jpg (shows detection box on frame)",
|
||||
f"{frame_name}_plate_{cand_num}_high_contrast.jpg (best for dark plates)",
|
||||
f"{frame_name}_plate_{cand_num}_extreme_sharp.jpg (best for clarity)",
|
||||
f"{frame_name}_plate_{cand_num}_adaptive_thresh.jpg (best for OCR)",
|
||||
f"{frame_name}_plate_{cand_num}_bilateral_sharp.jpg (balanced enhancement)",
|
||||
]
|
||||
|
||||
for enhancement in enhancements:
|
||||
f.write(f" - {enhancement}\n")
|
||||
|
||||
f.write("\n")
|
||||
|
||||
f.write("\n" + "=" * 80 + "\n")
|
||||
f.write("ENHANCEMENT TYPES EXPLAINED:\n")
|
||||
f.write("-" * 80 + "\n")
|
||||
f.write("- detection_X.jpg: Shows where the plate was detected on the frame\n")
|
||||
f.write("- high_contrast.jpg: Best for dark/low-contrast plates\n")
|
||||
f.write("- extreme_sharp.jpg: Best for overall clarity and readability\n")
|
||||
f.write("- adaptive_thresh.jpg: Black/white threshold - best for OCR\n")
|
||||
f.write("- bilateral_sharp.jpg: Noise reduction + sharpening\n")
|
||||
f.write("- unsharp_mask.jpg: Professional-grade sharpening\n")
|
||||
f.write("- bright_contrast.jpg: Brightness + contrast boost\n")
|
||||
|
||||
print("\n[SUCCESS] Analysis complete!")
|
||||
print(f"[INFO] Recommendations saved to: {recommendation_path}")
|
||||
print("\n[NEXT STEPS]")
|
||||
print("1. Open the output directory in File Explorer:")
|
||||
print(f" {output_dir}")
|
||||
print("2. Read RECOMMENDATIONS.txt for the best candidates")
|
||||
print("3. Start with Rank 1, review the enhancement files listed")
|
||||
print("4. The 'extreme_sharp' and 'adaptive_thresh' versions usually work best")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user