Initial commit: ASL handshape recognition project

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-19 22:19:15 -05:00
commit 41b3b95c42
11 changed files with 883 additions and 0 deletions

145
webcam_capture.py Executable file
View File

@@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""
capture_webcam.py
Show webcam preview and, given --letter L, count down 5s, then capture frames
every --interval seconds until --count images are saved.
Saves PNGs to ./captures as L001.PNG, L002.PNG, ...
Usage:
python capture_webcam.py --letter A
python capture_webcam.py --letter B --camera 1
python capture_webcam.py --letter C --count 10 --interval 1
# Default: 5 captures at 2s spacing, 640x480
python capture_webcam.py --letter A
# Ten captures, 1s apart
python capture_webcam.py --letter B --count 10 --interval 1
# USB camera index 1, HD override
python capture_webcam.py --letter C --camera 1 --width 1280 --height 720 --count 8 --interval 1.5
"""
import argparse
import os
import re
import time
from pathlib import Path
import cv2
COUNTDOWN_SECONDS = 5
def next_sequence_number(captures_dir: Path, letter: str) -> int:
"""Return next available sequence number for files like 'A001.PNG'."""
pattern = re.compile(rf"^{re.escape(letter)}(\d{{3}})\.PNG$", re.IGNORECASE)
max_idx = 0
if captures_dir.exists():
for name in os.listdir(captures_dir):
m = pattern.match(name)
if m:
try:
idx = int(m.group(1))
if idx > max_idx:
max_idx = idx
except ValueError:
pass
return max_idx + 1
def draw_text(img, text, org, scale=1.4, color=(0, 255, 0), thickness=2):
cv2.putText(img, text, org, cv2.FONT_HERSHEY_SIMPLEX, scale, color, thickness, cv2.LINE_AA)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--letter", required=True, help="Target letter AZ. Output files like A001.PNG")
ap.add_argument("--camera", type=int, default=0, help="OpenCV camera index (default: 0)")
ap.add_argument("--width", type=int, default=640, help="Requested capture width (default: 640)")
ap.add_argument("--height", type=int, default=480, help="Requested capture height (default: 480)")
ap.add_argument("--count", type=int, default=5, help="Number of captures to take (default: 5)")
ap.add_argument("--interval", type=float, default=2.0, help="Seconds between captures (default: 2.0)")
args = ap.parse_args()
letter = args.letter.upper().strip()
if not (len(letter) == 1 and "A" <= letter <= "Z"):
raise SystemExit("Please pass a single letter AZ to --letter (e.g., --letter A)")
if args.count <= 0:
raise SystemExit("--count must be >= 1")
if args.interval <= 0:
raise SystemExit("--interval must be > 0")
captures_dir = Path("./captures")
captures_dir.mkdir(parents=True, exist_ok=True)
start_idx = next_sequence_number(captures_dir, letter)
cap = cv2.VideoCapture(args.camera)
if not cap.isOpened():
raise SystemExit(f"❌ Could not open camera index {args.camera}")
# Try to set resolution (best-effort)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, args.width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, args.height)
window_title = f"Capture {letter} (press 'q' to quit)"
print(f"Showing webcam. Countdown {COUNTDOWN_SECONDS}s, then capturing {args.count} frame(s) every {args.interval}s...")
print(f"Saving to: {captures_dir.resolve()} as {letter}NNN.PNG starting at index {start_idx:03d}")
countdown_done_at = time.time() + COUNTDOWN_SECONDS
# Absolute times when we want to capture (after countdown)
capture_times = [countdown_done_at + i * args.interval for i in range(args.count)]
capture_taken = [False] * args.count
captures_made = 0
idx = start_idx
while True:
ok, frame = cap.read()
if not ok:
print("⚠️ Frame grab failed; ending.")
break
now = time.time()
# Countdown overlay
if now < countdown_done_at:
remaining = int(round(countdown_done_at - now))
overlay = frame.copy()
draw_text(overlay, f"Starting in: {remaining}s", (30, 60), scale=2.0, color=(0, 255, 255), thickness=3)
draw_text(overlay, f"Letter: {letter}", (30, 120), scale=1.2, color=(0, 255, 0), thickness=2)
cv2.imshow(window_title, overlay)
else:
# Check if it's time for any pending captures
for i, tcap in enumerate(capture_times):
if (not capture_taken[i]) and now >= tcap:
filename = f"{letter}{idx:03d}.PNG"
out_path = captures_dir / filename
cv2.imwrite(str(out_path), frame)
capture_taken[i] = True
captures_made += 1
idx += 1
print(f"📸 Saved {out_path.name}")
# Overlay progress
elapsed_after = now - countdown_done_at
total_duration = args.interval * (args.count - 1) if args.count > 1 else 0
remaining_after = max(0.0, total_duration - elapsed_after)
overlay = frame.copy()
draw_text(overlay, f"Capturing {letter}{captures_made}/{args.count}", (30, 60),
scale=1.5, color=(0, 255, 0), thickness=3)
draw_text(overlay, f"Time left: {int(round(remaining_after))}s", (30, 110),
scale=1.2, color=(0, 255, 255), thickness=2)
cv2.imshow(window_title, overlay)
# If finished all captures, keep preview up until user quits
if captures_made >= args.count:
draw_text(overlay, "Done! Press 'q' to close.", (30, 160),
scale=1.2, color=(0, 200, 255), thickness=2)
cv2.imshow(window_title, overlay)
# Quit on 'q'
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()