"""
MotionScope Pro — Streamlit front-end
Run with: streamlit run app.py
"""
import tempfile
import os
import cv2
import numpy as np
import streamlit as st
from detector import MovementDetector, DetectionConfig, DetectionMode
# ---------------------------------------------------------------------------
# Page config
# ---------------------------------------------------------------------------
st.set_page_config(
page_title="MotionScope Pro",
page_icon="🎥",
layout="wide",
initial_sidebar_state="expanded",
)
# ---------------------------------------------------------------------------
# Custom CSS — dark, polished look
# ---------------------------------------------------------------------------
st.markdown(
"""
""",
unsafe_allow_html=True,
)
# ---------------------------------------------------------------------------
# Hero header
# ---------------------------------------------------------------------------
st.markdown(
"""
🎥 MotionScope Pro
Advanced Movement Detection — Hand Tracking & Motion Analysis
""",
unsafe_allow_html=True,
)
# Feature badges
st.markdown(
"""
🖐️ Hand Tracking
🚗 Motion Detection
⚡ Combined Mode
📹 Video Upload
📷 Webcam Snapshots
""",
unsafe_allow_html=True,
)
# ---------------------------------------------------------------------------
# Sidebar — settings
# ---------------------------------------------------------------------------
with st.sidebar:
st.markdown("## ⚙️ Detection Settings")
mode_label = st.selectbox(
"Detection Mode",
options=[m.value for m in DetectionMode],
index=0,
help="Choose what the detector should look for.",
)
mode = DetectionMode(mode_label)
st.markdown("---")
st.markdown("### 🔧 Motion Parameters")
motion_threshold = st.slider(
"Motion threshold",
min_value=50, max_value=255, value=180, step=5,
help="Higher → less sensitive (ignores faint motion).",
)
min_contour_area = st.slider(
"Min object area (px²)",
min_value=100, max_value=10000, value=1000, step=100,
help="Ignore contours smaller than this area.",
)
st.markdown("---")
st.markdown("### 🖐️ Hand Parameters")
max_hands = st.slider("Max hands to detect", 1, 4, 2)
det_confidence = st.slider(
"Detection confidence", 0.1, 1.0, 0.5, 0.05,
)
track_confidence = st.slider(
"Tracking confidence", 0.1, 1.0, 0.5, 0.05,
)
st.markdown("---")
st.markdown(
"Built with OpenCV · MediaPipe · Streamlit",
unsafe_allow_html=True,
)
# Build config from sidebar values
config = DetectionConfig(
min_detection_confidence=det_confidence,
min_tracking_confidence=track_confidence,
max_num_hands=max_hands,
motion_threshold=motion_threshold,
min_contour_area=min_contour_area,
)
# ---------------------------------------------------------------------------
# Cached detector (rebuilt when config changes)
# ---------------------------------------------------------------------------
@st.cache_resource
def get_detector():
return MovementDetector()
detector = get_detector()
detector.rebuild(config)
# ---------------------------------------------------------------------------
# Tabs — Video Upload | Webcam Snapshot
# ---------------------------------------------------------------------------
tab_video, tab_webcam = st.tabs(["📹 Video Upload", "📷 Webcam Snapshot"])
# ======================== VIDEO UPLOAD TAB ==============================
with tab_video:
uploaded = st.file_uploader(
"Upload a video file",
type=["mp4", "avi", "mov", "mkv"],
help="Supported formats: MP4, AVI, MOV, MKV",
)
if uploaded is not None:
# Save upload to a temp file
tfile = tempfile.NamedTemporaryFile(delete=False, suffix=".mp4")
tfile.write(uploaded.read())
tfile.flush()
input_path = tfile.name
# Show the original video
with st.expander("🎬 Original video", expanded=False):
st.video(input_path)
# Process button
if st.button("🚀 Process Video", type="primary", use_container_width=True):
output_path = os.path.join(tempfile.gettempdir(), "motionscope_output.mp4")
progress_bar = st.progress(0, text="Processing…")
frame_placeholder = st.empty()
metrics_placeholder = st.empty()
total_objects = 0
frame_num = 0
try:
for display_frame, result_path, progress in detector.process_video(
input_path, mode=mode, output_path=output_path,
):
if display_frame is not None:
frame_num += 1
# Show every 4th frame for speed
if frame_num % 4 == 0 or progress >= 1.0:
frame_placeholder.image(
display_frame,
caption=f"Frame {detector.frame_count}",
use_container_width=True,
)
progress_bar.progress(
progress,
text=f"Processing… {int(progress * 100)}%",
)
if result_path is not None:
progress_bar.progress(1.0, text="✅ Done!")
st.success(
f"Processed **{detector.frame_count}** frames successfully!"
)
# Metrics row
col1, col2, col3 = st.columns(3)
col1.metric("Total Frames", detector.frame_count)
col2.metric("Mode", mode.value)
col3.metric("Status", "✅ Complete")
# Download button
with open(result_path, "rb") as f:
st.download_button(
"⬇️ Download Processed Video",
data=f,
file_name="motionscope_output.mp4",
mime="video/mp4",
use_container_width=True,
)
except Exception as e:
st.error(f"❌ Error during processing: {e}")
finally:
# Cleanup temp input
try:
os.unlink(input_path)
except OSError:
pass
else:
# Empty state
st.markdown(
"""
📹
Upload a video above to get started
""",
unsafe_allow_html=True,
)
# ======================== WEBCAM SNAPSHOT TAB ===========================
with tab_webcam:
st.markdown(
"Take a photo with your webcam and the detector will process it instantly."
)
if mode == DetectionMode.MOTION_DETECTION:
st.warning("⚠️ **Motion Detection** requires a video stream to compare frames. For a single photo, use **Hand Tracking** or **Combined** mode.")
camera_input = st.camera_input("📷 Take a photo")
if camera_input is not None:
# Decode the image
file_bytes = np.frombuffer(camera_input.getvalue(), dtype=np.uint8)
img_bgr = cv2.imdecode(file_bytes, cv2.IMREAD_COLOR)
if img_bgr is not None:
# Flip for mirror effect
img_bgr = cv2.flip(img_bgr, 1)
# Process
processed_bgr = detector.process_frame(img_bgr, mode)
processed_rgb = cv2.cvtColor(processed_bgr, cv2.COLOR_BGR2RGB)
col_orig, col_proc = st.columns(2)
with col_orig:
st.markdown("**Original**")
original_rgb = cv2.cvtColor(
cv2.flip(img_bgr, 1), cv2.COLOR_BGR2RGB # undo our flip for display
)
st.image(original_rgb, use_container_width=True)
with col_proc:
st.markdown("**Processed**")
st.image(processed_rgb, use_container_width=True)
# Download processed image
_, buf = cv2.imencode(".jpg", processed_bgr)
st.download_button(
"⬇️ Download Processed Image",
data=buf.tobytes(),
file_name="motionscope_snapshot.jpg",
mime="image/jpeg",
use_container_width=True,
)
else:
st.error("Could not decode the captured image.")
else:
st.markdown(
"""
📷
Click the camera button above to capture a snapshot
""",
unsafe_allow_html=True,
)