Spaces:
Sleeping
Sleeping
| """ | |
| 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 — Hand Tracking & 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) | |
| # --------------------------------------------------------------------------- | |
| 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, | |
| ) | |