MotionScope-Pro / app.py
3v324v23's picture
Fix 403 error (CORS) and change default mode to Hand Tracking
d72f50c
"""
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(
"""
<style>
/* ---- Global ---- */
.stApp {
background: linear-gradient(135deg, #0f0c29, #302b63, #24243e);
}
/* Hero header */
.hero {
text-align: center;
padding: 1.5rem 0 0.5rem;
}
.hero h1 {
font-size: 2.6rem;
background: linear-gradient(90deg, #00d2ff, #3a7bd5);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.2rem;
}
.hero p {
color: #b0b0cc;
font-size: 1.05rem;
}
/* Sidebar */
section[data-testid="stSidebar"] {
background: rgba(15, 12, 41, 0.95);
border-right: 1px solid rgba(58, 123, 213, 0.3);
}
/* Cards */
.metric-card {
background: rgba(255,255,255,0.06);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 12px;
padding: 1rem 1.2rem;
margin-bottom: 0.8rem;
}
.metric-card h3 {
margin: 0 0 0.3rem;
font-size: 0.95rem;
color: #7eb8f7;
}
.metric-card .val {
font-size: 1.6rem;
font-weight: 700;
color: #fff;
}
/* Feature badges */
.badge-row {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 1.2rem;
}
.badge {
background: rgba(58, 123, 213, 0.15);
border: 1px solid rgba(58, 123, 213, 0.35);
border-radius: 20px;
padding: 0.35rem 0.9rem;
font-size: 0.82rem;
color: #a0c4ff;
}
/* Hide default Streamlit branding */
#MainMenu, footer, header {visibility: hidden;}
</style>
""",
unsafe_allow_html=True,
)
# ---------------------------------------------------------------------------
# Hero header
# ---------------------------------------------------------------------------
st.markdown(
"""
<div class="hero">
<h1>🎥 MotionScope Pro</h1>
<p>Advanced Movement Detection &mdash; Hand Tracking &amp; Motion Analysis</p>
</div>
""",
unsafe_allow_html=True,
)
# Feature badges
st.markdown(
"""
<div class="badge-row">
<span class="badge">🖐️ Hand Tracking</span>
<span class="badge">🚗 Motion Detection</span>
<span class="badge">⚡ Combined Mode</span>
<span class="badge">📹 Video Upload</span>
<span class="badge">📷 Webcam Snapshots</span>
</div>
""",
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(
"<small style='color:#666'>Built with OpenCV · MediaPipe · Streamlit</small>",
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(
"""
<div style="text-align:center; padding:3rem 0; color:#888;">
<p style="font-size:3rem; margin-bottom:0.5rem;">📹</p>
<p>Upload a video above to get started</p>
</div>
""",
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(
"""
<div style="text-align:center; padding:3rem 0; color:#888;">
<p style="font-size:3rem; margin-bottom:0.5rem;">📷</p>
<p>Click the camera button above to capture a snapshot</p>
</div>
""",
unsafe_allow_html=True,
)