Spaces:
Runtime error
Runtime error
File size: 8,326 Bytes
6b7cf40 13747e9 6b7cf40 13747e9 6b7cf40 13747e9 286761c 13747e9 286761c 13747e9 6b7cf40 13747e9 2d25c7f 13747e9 9782e77 13747e9 9782e77 13747e9 9782e77 13747e9 9782e77 13747e9 7dda26b 9782e77 13747e9 6b7cf40 13747e9 6b7cf40 13747e9 6b7cf40 13747e9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | """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 βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@st.cache_resource
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")
|