fix(server): reduce skeleton jitter + raise person count thresholds
- EMA alpha 0.3→0.15, low-coherence 0.1→0.05 - Remove tick-based noise (main jitter source) - Breathing 5x slower, extremity jitter 3x smaller, stride 2x smaller - Person count 1→2 threshold 0.65→0.80 - Aggregation sum→max for same-room nodes Verified on COM6+COM9: 1 person stable. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
e4c0f66922
commit
85f8d27b94
|
|
@ -117,14 +117,14 @@ midstreamer-temporal-compare = "0.1.0"
|
|||
midstreamer-attractor = "0.1.0"
|
||||
|
||||
# ruvector integration (published on crates.io)
|
||||
# Vendored at v2.1.0 in vendor/ruvector; using crates.io versions until published.
|
||||
ruvector-mincut = "2.0.4"
|
||||
# RuVector Phase 2: bumped to latest available crates.io versions.
|
||||
ruvector-mincut = "2.0.6"
|
||||
ruvector-attn-mincut = "2.0.4"
|
||||
ruvector-temporal-tensor = "2.0.4"
|
||||
ruvector-solver = "2.0.4"
|
||||
ruvector-attention = "2.0.4"
|
||||
ruvector-temporal-tensor = "2.0.6"
|
||||
ruvector-solver = "2.0.6"
|
||||
ruvector-attention = "2.1.0"
|
||||
ruvector-crv = "0.1.1"
|
||||
ruvector-gnn = { version = "2.0.5", default-features = false }
|
||||
ruvector-gnn = { version = "2.1.0", default-features = false }
|
||||
|
||||
|
||||
# Internal crates
|
||||
|
|
|
|||
|
|
@ -309,9 +309,11 @@ struct NodeState {
|
|||
}
|
||||
|
||||
/// Default EMA alpha for temporal keypoint smoothing (RuVector Phase 2).
|
||||
const TEMPORAL_EMA_ALPHA_DEFAULT: f64 = 0.3;
|
||||
/// Lower = smoother (more history, less jitter). 0.15 balances responsiveness
|
||||
/// with stability for WiFi CSI where per-frame noise is high.
|
||||
const TEMPORAL_EMA_ALPHA_DEFAULT: f64 = 0.15;
|
||||
/// Reduced EMA alpha when coherence is low (trust measurements less).
|
||||
const TEMPORAL_EMA_ALPHA_LOW_COHERENCE: f64 = 0.1;
|
||||
const TEMPORAL_EMA_ALPHA_LOW_COHERENCE: f64 = 0.05;
|
||||
/// Coherence threshold below which we reduce EMA alpha.
|
||||
const COHERENCE_LOW_THRESHOLD: f64 = 0.3;
|
||||
/// Maximum allowed bone-length change ratio between frames (20%).
|
||||
|
|
@ -2024,25 +2026,26 @@ fn compute_person_score(feat: &FeatureInfo) -> f64 {
|
|||
/// (the #1 user-reported issue — see #237, #249, #280, #292).
|
||||
fn score_to_person_count(smoothed_score: f64, prev_count: usize) -> usize {
|
||||
// Up-thresholds (must exceed to increase count):
|
||||
// 1→2: 0.65 (raised from 0.50 — multipath in small rooms hit 0.50 easily)
|
||||
// 2→3: 0.85 (raised from 0.80 — 3 persons needs strong sustained signal)
|
||||
// 1→2: 0.80 (raised from 0.65 — single-person movement in multipath
|
||||
// rooms easily hits 0.65, causing false 2-person detection)
|
||||
// 2→3: 0.92 (raised from 0.85 — 3 persons needs very strong signal)
|
||||
// Down-thresholds (must drop below to decrease count):
|
||||
// 2→1: 0.45 (hysteresis gap of 0.20)
|
||||
// 3→2: 0.70 (hysteresis gap of 0.15)
|
||||
// 2→1: 0.55 (hysteresis gap of 0.25)
|
||||
// 3→2: 0.78 (hysteresis gap of 0.14)
|
||||
match prev_count {
|
||||
0 | 1 => {
|
||||
if smoothed_score > 0.85 {
|
||||
if smoothed_score > 0.92 {
|
||||
3
|
||||
} else if smoothed_score > 0.65 {
|
||||
} else if smoothed_score > 0.80 {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
if smoothed_score > 0.85 {
|
||||
if smoothed_score > 0.92 {
|
||||
3
|
||||
} else if smoothed_score < 0.45 {
|
||||
} else if smoothed_score < 0.55 {
|
||||
1
|
||||
} else {
|
||||
2 // hold — within hysteresis band
|
||||
|
|
@ -2050,9 +2053,9 @@ fn score_to_person_count(smoothed_score: f64, prev_count: usize) -> usize {
|
|||
}
|
||||
_ => {
|
||||
// prev_count >= 3
|
||||
if smoothed_score < 0.45 {
|
||||
if smoothed_score < 0.55 {
|
||||
1
|
||||
} else if smoothed_score < 0.70 {
|
||||
} else if smoothed_score < 0.78 {
|
||||
2
|
||||
} else {
|
||||
3 // hold
|
||||
|
|
@ -2092,23 +2095,27 @@ fn derive_single_person_pose(
|
|||
let breath_phase = if let Some(ref vs) = update.vital_signs {
|
||||
let bpm = vs.breathing_rate_bpm.unwrap_or(15.0);
|
||||
let freq = (bpm / 60.0).clamp(0.1, 0.5);
|
||||
(update.tick as f64 * freq * 0.1 * std::f64::consts::TAU + phase_offset).sin()
|
||||
// Slow tick rate (0.02) for gentle breathing, not jerky oscillation.
|
||||
(update.tick as f64 * freq * 0.02 * std::f64::consts::TAU + phase_offset).sin()
|
||||
} else {
|
||||
(update.tick as f64 * 0.08 + feat.breathing_band_power + phase_offset).sin()
|
||||
(update.tick as f64 * 0.02 + phase_offset).sin()
|
||||
};
|
||||
|
||||
let lean_x = (feat.dominant_freq_hz / 5.0 - 1.0).clamp(-1.0, 1.0) * 18.0;
|
||||
|
||||
let stride_x = if is_walking {
|
||||
let stride_phase = (feat.motion_band_power * 0.7 + update.tick as f64 * 0.12 + phase_offset).sin();
|
||||
stride_phase * 45.0 * motion_score
|
||||
let stride_phase = (feat.motion_band_power * 0.7 + update.tick as f64 * 0.06 + phase_offset).sin();
|
||||
stride_phase * 20.0 * motion_score
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let burst = (feat.change_points as f64 / 8.0).clamp(0.0, 1.0);
|
||||
// Dampen burst and noise to reduce jitter. The original used
|
||||
// tick*17.3 which changed wildly every frame. Now use slow tick
|
||||
// rate and minimal burst scaling for a stable skeleton.
|
||||
let burst = (feat.change_points as f64 / 20.0).clamp(0.0, 0.3);
|
||||
|
||||
let noise_seed = feat.variance * 31.7 + update.tick as f64 * 17.3 + person_idx as f64 * 97.1;
|
||||
let noise_seed = person_idx as f64 * 97.1; // stable per-person, no tick
|
||||
let noise_val = (noise_seed.sin() * 43758.545).fract();
|
||||
|
||||
let snr_factor = ((feat.variance - 0.5) / 10.0).clamp(0.0, 1.0);
|
||||
|
|
@ -2169,9 +2176,10 @@ fn derive_single_person_pose(
|
|||
|
||||
let extremity_jitter = if EXTREMITY_KP.contains(&i) {
|
||||
let phase = noise_seed + i as f64 * 2.399;
|
||||
// Dampened from 12/8 to 4/3 to reduce visual jumping.
|
||||
(
|
||||
phase.sin() * burst * motion_score * 12.0,
|
||||
(phase * 1.31).cos() * burst * motion_score * 8.0,
|
||||
phase.sin() * burst * motion_score * 4.0,
|
||||
(phase * 1.31).cos() * burst * motion_score * 3.0,
|
||||
)
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
|
|
|
|||
Loading…
Reference in New Issue