Spaces:
Runtime error
Runtime error
Download src/phyphox_app_block.py from Group3DActRecog/actRecog: direct link, hf CLI and curl.
- Browser
- Download file 8.33 kB
-
https://huggingface.co/spaces/Group3DActRecog/actRecog/resolve/2d25c7fbea7f4e7ff5b6b4005082b5af94de3869/src/phyphox_app_block.py
- Command line
-
hf download hf://spaces/Group3DActRecog/actRecog@2d25c7fbea7f4e7ff5b6b4005082b5af94de3869/src/phyphox_app_block.py
-
curl -L -o phyphox_app_block.py https://huggingface.co/spaces/Group3DActRecog/actRecog/resolve/2d25c7fbea7f4e7ff5b6b4005082b5af94de3869/src/phyphox_app_block.py
8.33 kB
| """Streamlit UI block for Tab 2: Phyphox live sensor upload. | |
| Call from streamlit_app.py: | |
| from phyphox_app_block import render_phyphox_tab | |
| with tab2: | |
| render_phyphox_tab(ffn_model, ffn_status, cnn_model, cnn_status) | |
| """ | |
| import json | |
| import os | |
| import numpy as np | |
| import pandas as pd | |
| import streamlit as st | |
| from phyphox_pipeline import process_phyphox_files, FS, WINDOW, STEP | |
| # ββ Constants βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| LABEL_MAP = { | |
| 0: "WALKING", | |
| 1: "WALKING_UPSTAIRS", | |
| 2: "WALKING_DOWNSTAIRS", | |
| 3: "SITTING", | |
| 4: "STANDING", | |
| 5: "LAYING", | |
| } | |
| EXPLANATIONS = { | |
| "LAYING": "Minimal movement detected across all axes: consistent with a stationary horizontal posture.", | |
| "SITTING": "Low dynamic acceleration with stable gravity: stationary upright posture.", | |
| "STANDING": "Similar to sitting with slight postural micro-movements.", | |
| "WALKING": "Rhythmic periodic acceleration on the vertical axis: level walking at normal cadence.", | |
| "WALKING_DOWNSTAIRS": "Downward gravitational shift with higher impact peaks: descending stairs.", | |
| "WALKING_UPSTAIRS": "Elevated vertical acceleration effort: climbing stairs.", | |
| } | |
| # ββ Normalisation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _load_norm_params(norm_path: str): | |
| """Load per-feature min/max from norm_params.json (keys are string indices 0-560).""" | |
| with open(norm_path) as f: | |
| d = json.load(f) | |
| min_vals = np.array([d[str(i)]["min"] for i in range(561)], dtype=np.float32) | |
| max_vals = np.array([d[str(i)]["max"] for i in range(561)], dtype=np.float32) | |
| return min_vals, max_vals | |
| def _normalize(features: np.ndarray, min_vals: np.ndarray, max_vals: np.ndarray) -> np.ndarray: | |
| """Best-effort feature-level min-max scaling to [-1, 1]. | |
| Uses per-feature min/max observed in the UCI HAR training set. This is | |
| an approximation: the UCI pipeline normalises raw signals before feature | |
| extraction, so physical-unit features may fall outside the training range. | |
| Values are clipped before scaling to keep outputs bounded. | |
| """ | |
| rng = max_vals - min_vals | |
| rng = np.where(rng < 1e-8, 1.0, rng) # avoid div-by-zero | |
| clipped = np.clip(features, min_vals, max_vals) | |
| return 2.0 * (clipped - min_vals) / rng - 1.0 | |
| # ββ Public render function ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def render_phyphox_tab( | |
| ffn_model, ffn_status: str, | |
| cnn_model, cnn_status: str, | |
| norm_params_path: str, | |
| ) -> None: | |
| st.subheader("Upload Phyphox sensor recording") | |
| st.markdown(""" | |
| **How to record your own data:** | |
| 1. Install [Phyphox](https://phyphox.org/) on your phone | |
| 2. Open **Acceleration (with g)** and **Gyroscope**: record simultaneously | |
| 3. Hold the phone at your waist (same position as the UCI dataset) | |
| 4. Record at least 3 seconds of a single activity | |
| 5. Export both experiments as CSV and upload below | |
| """) | |
| if "phyphox_run" not in st.session_state: | |
| st.session_state.phyphox_run = 0 | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| acc_file = st.file_uploader( | |
| "Accelerometer CSV", | |
| type=["csv"], | |
| key=f"acc_upload_{st.session_state.phyphox_run}", | |
| help="Columns: Time (s), X (m/sΒ²), Y (m/sΒ²), Z (m/sΒ²)", | |
| ) | |
| with col2: | |
| gyro_file = st.file_uploader( | |
| "Gyroscope CSV", | |
| type=["csv"], | |
| key=f"gyro_upload_{st.session_state.phyphox_run}", | |
| help="Columns: Time (s), X (rad/s), Y (rad/s), Z (rad/s)", | |
| ) | |
| if acc_file is not None or gyro_file is not None: | |
| if st.button("Clear / new recording"): | |
| st.session_state.phyphox_run += 1 | |
| st.rerun() | |
| if acc_file is None or gyro_file is None: | |
| st.info("Upload both files to continue.") | |
| return | |
| # ββ Feature extraction ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| try: | |
| with st.spinner("Extracting 561 features from sensor dataβ¦"): | |
| features, pipeline_warnings = process_phyphox_files(acc_file, gyro_file) | |
| except ValueError as err: | |
| st.error(str(err)) | |
| return | |
| except Exception as err: | |
| st.error(f"Unexpected error during feature extraction: {err}") | |
| return | |
| for w in pipeline_warnings: | |
| st.warning(w) | |
| n_windows = len(features) | |
| duration_s = (n_windows - 1) * (STEP / FS) + (WINDOW / FS) | |
| c1, c2, c3 = st.columns(3) | |
| c1.metric("Windows extracted", n_windows) | |
| c2.metric("Approx. duration", f"{duration_s:.1f} s") | |
| c3.metric("Features per window", 561) | |
| st.caption( | |
| f"Each window = {WINDOW / FS:.2f} s at {FS} Hz Β· " | |
| f"50% overlap ({STEP / FS:.2f} s hop)" | |
| ) | |
| # ββ Normalisation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if os.path.exists(norm_params_path): | |
| min_vals, max_vals = _load_norm_params(norm_params_path) | |
| features = _normalize(features, min_vals, max_vals) | |
| st.caption( | |
| "Accelerometer converted from m/sΒ² to g units to match UCI training data. " | |
| "Features scaled to [β1, 1] using physical-unit min/max computed from the " | |
| "UCI HAR training set raw inertial signals." | |
| ) | |
| else: | |
| st.warning( | |
| "norm_params.json not found: features are in physical units. " | |
| "Predictions will be unreliable until normalisation is applied." | |
| ) | |
| # ββ Predictions βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if ffn_status != "ready" and cnn_status != "ready": | |
| st.warning("Models not loaded: cannot predict yet.") | |
| return | |
| st.markdown("---") | |
| st.subheader("Model comparison") | |
| left, right = st.columns(2) | |
| def _render_model_col(col, model, status, name): | |
| with col: | |
| st.markdown(f"#### {name}") | |
| if status != "ready": | |
| st.error(f"Model not loaded: {status}") | |
| return | |
| probs_all = model.predict(features, verbose=0) # (n_windows, 6) | |
| pred_labels = [LABEL_MAP[int(np.argmax(p))] for p in probs_all] | |
| from collections import Counter | |
| vote = Counter(pred_labels).most_common(1)[0][0] | |
| avg_conf = float(np.mean(np.max(probs_all, axis=1))) * 100 | |
| st.success(f"**{vote}** Β· {avg_conf:.1f}% avg confidence") | |
| st.markdown(f"_{EXPLANATIONS[vote]}_") | |
| if n_windows > 1: | |
| with st.expander(f"Per-window breakdown ({n_windows} windows)"): | |
| rows = [] | |
| for i, (p, label) in enumerate(zip(probs_all, pred_labels)): | |
| t_start = i * STEP / FS | |
| rows.append({ | |
| "Window": i + 1, | |
| "Time (s)": f"{t_start:.1f}β{t_start + WINDOW/FS:.1f}", | |
| "Prediction": label, | |
| "Confidence": f"{float(np.max(p))*100:.1f}%", | |
| }) | |
| st.dataframe(pd.DataFrame(rows), use_container_width=True) | |
| mean_probs = probs_all.mean(axis=0) | |
| st.markdown("**Average confidence across all classes**") | |
| st.bar_chart(pd.DataFrame( | |
| {"Confidence (%)": [float(mean_probs[i]) * 100 for i in range(6)]}, | |
| index=[LABEL_MAP[i] for i in range(6)], | |
| )) | |
| _render_model_col(left, ffn_model, ffn_status, "Feedforward Network") | |
| _render_model_col(right, cnn_model, cnn_status, "1D Convolutional Network") | |