File size: 10,262 Bytes
166743f | 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 | # Development specification
## Scope
Fall Detection v1 classifier is a binary XGBoost model that classifies people as `Fall` / `Normal` from a **56-column pandas DataFrame**. Upstream stages β CCTV image capture, YOLO26x-Pose (yolo26x-pose.pt) 17-keypoint extraction, and bounding-box normalization producing the **51 keypoint features + bbox** as a DataFrame β are **out of scope for this model**. The **5 Priority 1 features are computed inside the inference pipeline** (`scripts/run.py`) from that DataFrame, which is then expanded to 56 columns and passed to XGBoost (never as a CSV into the model).
**Input contract (what goes IN to `run.py`):**
| Columns | Count | Source |
| --- | ---: | --- |
| `x_i, y_i, conf_i` for i = 0β¦16 | **51** | Upstream (input contract) |
| `x1, y1, x2, y2` | 4 | Upstream bbox (needed to compute the 5) |
| | **55** | **Total input to `run.py`** |
The **extra 5 features are extracted in `run.py`**, not received. Model input after extraction = **56 = 51 + 5**.
```
CCTV IMAGE β YOLO26x-Pose β 51 features + bbox (DataFrame) upstream (out of scope)
β
scripts/run.py: +5 engineered β 56-col DataFrame β XGBoost β Fall / Normal
β computed in this repo β direct input (DataFrame)
```
The system addresses a critical domain shift: raw-coordinate baselines reached 92.22% accuracy on standard splits but collapsed to 66.83% on real-world CCTV. The 56-feature representation (51 normalized keypoints + 5 Priority 1 CCTV-invariant features) resolves this, achieving 88.36% on out-of-distribution CCTV while maintaining 91% on standard test sets.
## Architecture decisions
YOLO26x-Pose is used **only upstream to generate training and inference vectors**, not as part of the classifier. It provides 17-keypoint COCO topology with confidence scores, enabling confidence-based filtering. Raw keypoints are transformed into normalized, camera-invariant features before reaching the classifier.
**Feature Engineering Strategy**: the Priority 1 set replaces camera-dependent absolute coordinates with relative geometric ratios:
1. **51 normalized keypoint features**: 17 joints Γ (`x_norm`, `y_norm`, `confidence`) where `norm_x = (x - x1) / w`, `norm_y = (y - y1) / h` relative to the person's bounding box. Makes features invariant to camera distance.
2. **5 Priority 1 CCTV-invariant features** (positions 52β56 of the 56-vector):
- **aspect_ratio**: `w / h` β horizontal spread. Upright < 1.0; lying > 1.0.
- **nose_relative_y**: `(Y_nose - y1) / h` β head vertical position. Standing 0.0β0.25; fallen 0.7β1.0.
- **torso_angle**: angle HipMidβShoulderMid vs. vertical Y-axis. Standing β0Β°; fallen 80β90Β°.
- **norm_com_y**: `(Ξ£(Y_i Γ conf_i) / Ξ£(conf_i) - y1) / h` β confidence-weighted vertical center-of-mass, robust to occlusion.
- **head_hip_v_dist**: `(Y_hip_mid - Y_nose) / h` β upper-body extension, near 0.0 when lying flat.
Together they provide scale invariance, angle invariance, and occlusion robustness.
**Classifier Selection**: XGBoost was chosen over Random Forest, SVM, and MLP because it natively handles `NaN` via `missing=np.nan`, eliminating imputation for occluded keypoints (confidence < 0.50 β `NaN` coordinates). 1:1 class balancing via undersampling prevents majority-class bias.
## Starting checkpoint and training
No pose-estimator fine-tuning. The XGBoost classifier was trained **from scratch** on the engineered 56-feature dataset (`pose_benchmark_priority1_dataset.csv`):
- **Training data**: 1:1 balanced train split from `pose_benchmark_priority1_dataset.csv`
- **Hyperparameters**:
- `n_estimators=150`
- `max_depth=5`
- `learning_rate=0.03`
- `scale_pos_weight=1.0` (data pre-balanced)
- `missing=np.nan` (native occlusion handling)
- `eval_metric="logloss"`
- `random_state=42`
Test set was preserved intact without resampling. Upstream YOLO26x-Pose checkpoint (`yolo26x-pose.pt`) was used only to generate the CSV, not as a model checkpoint.
Training achieves:
- **Standard test accuracy**: 91% (Precision: 0.91, Recall: 0.91, F1: 0.91)
- **Real-world accuracy**: 88.36% (Precision: 0.90, Recall: 0.88, F1: 0.87) β +21.53 pp over raw-keypoint baseline (66.83% β 88.36%)
## Preprocessing and post-processing
### Training Pipeline (to produce the classifier's training data)
1. **Keypoint Extraction (upstream, out of scope for classifier)**: Process image datasets (train/val/test with fall/normal subdirs) through YOLO26x-Pose with batch_size=32, detection confidence 0.20, keypoint confidence 0.25. This step is only to generate the dataset; the classifier does not run it.
2. **Feature Engineering (51 upstream; +5 computed in `scripts/run.py` at inference)**: For each detected person:
- Extract bounding box `(x1, y1, x2, y2)`, compute `w`, `h`
- Normalize all 17 keypoints: `x_norm = (x - x1) / w`, `y_norm = (y - y1) / h` β **51 features + bbox** (upstream deliverable)
- Set keypoints with `confidence < 0.25` (training) / `< 0.50` (inference) to `NaN` for `x`/`y` (confidence preserved)
- **`scripts/run.py` computes** the 5 Priority 1 features (`aspect_ratio`, `nose_relative_y`, `torso_angle`, `norm_com_y`, `head_hip_v_dist`) from the 51 + bbox
- Concatenate into **56-column DataFrame (51 + 5)** in fixed order β this is the XGBoost input (CSV only used to transport training data into a DataFrame)
Older docs described this as β58 features (52 + 5)β; correct count is **56 = 51 (17Γ3) + 5** (`config.json`, `scripts/train.py`).
3. **Data Balancing**: Undersample majority class to 1:1 Normal:Fall in training split only.
4. **Model Training**: Train XGBoost on 56 features with native NaN handling. Export to `models/xgboost_priority1_fall_model.pkl`.
### Classifier I/O
- **Input**: `(n, 56)` float **pandas DataFrame** (one row per person); `NaN` allowed. CSV is never fed to the model β upstream DataFrame β `run.py` extracts 5 features β 56-column DataFrame β `predict_proba`.
- **Output**: `Fall` / `Normal` via `model.predict` / `model.predict_proba`; decision rule `P(Fall) >= FALL_PROB_THRESH`.
## Approaches considered
**Version 1 (Raw Keypoints)**: Direct XGBoost on 51 raw normalized coordinates (sometimes miscounted as 52). Achieved 92.22% on standard splits but collapsed to 66.83% on CCTV due to camera sensitivity and lack of posture ratios.
**Alternative Feature Sets**: Priority 2 (joint velocities, inter-joint distances) and Priority 3 (full skeleton angles, convex hull) were considered but excluded β the minimal 5 Priority 1 features already resolved the domain shift.
**Alternative Classifiers**: Random Forest, SVM (RBF), MLP (64-32) were benchmarked. XGBoost outperformed and avoided imputation pipelines.
**Temporal Models**: LSTM/GRU and 10-frame buffers excluded as out of scope for the classifier; deployment-level smoothing is separate.
## Known design gaps
The classifier system does not include:
1. **Multi-person tracking**: each 56-vector is classified independently; identity tracking would need deployment logic.
2. **Environmental context**: ignores scene context (bed vs. floor, stairs).
3. **Performance profiling**: per-sample latency measured; multi-camera throughput not benchmarked.
4. **Confidence calibration**: `FALL_PROB_THRESH = 0.70` chosen via grid search; per-environment calibration recommended.
5. **Adversarial validation**: not tested against yoga/exercise floor activities β future βnon-fall floor activityβ class needed.
## Dataset structure
**Training/Validation/Test images (upstream only)**: `data/{train,val,test}/{fall,normal}/*.{jpg,jpeg,png,bmp}` β used only to generate the feature CSV.
**Classifier training file**: `pose_benchmark_priority1_dataset.csv` β 58 columns total:
- 51 pose features (17 joints Γ 3: x_norm, y_norm, confidence) β columns 1β51
- + 5 additional Priority 1 features (`aspect_ratio`, `nose_relative_y`, `torso_angle`, `norm_com_y`, `head_hip_v_dist`) β columns 52β56 (**56 classifier features**)
- + 1 `split` (metadata: `train` / `val` / `test`) β column 57
- + 1 `label` (`0` = Normal, `1` = Fall) β column 58
- = **58 columns**
In `pose_extract.py`: `row = norm_kpts + [aspect_ratio] + p1_features + [split, label]` where `norm_kpts` = 51 and `[aspect_ratio] + p1_features` = 5.
**Very important:** XGBoost uses only **51 + 5 = 56 classifier features** (columns 1β56). Columns 57 β `split` and 58 β `label` are **metadata, not model input**.
**Real-World Evaluation**: out-of-distribution CCTV-derived feature vectors at `/home/ctspl/model_training/fall/version3/data`, evaluated separately.
## Deployment configuration
**Hardware**: CPU-only for classifier; no GPU required. Upstream YOLO stage (if run locally for data generation) benefits from CUDA, but classifier inference/training does not need it.
**Thresholds** (tunable via `config.json`):
- `CONF_THRESH = 0.50` β Minimum keypoint confidence; below β `NaN` coordinates in 56-vector
- `FALL_PROB_THRESH = 0.70` β Minimum XGBoost `P(Fall)` for Fall classification (optimized from 0.95)
`YOLO_CONF_THRESH` is an upstream data-generation parameter (0.25 optimized) and **not a classifier threshold**.
### Grid Search Optimization Results
Grid search over classifier thresholds (YOLO stage excluded):
- `CONF_THRESH` (keypoint): [0.30, 0.40, 0.50]
- `FALL_PROB_THRESH`: [0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90, 0.95]
**Top Parameter Combinations (By F1-Score)**:
| Keypoint_Conf | Fall_Prob_Thresh | Accuracy | F1_Score | Precision | Recall | TN | FP | FN | TP |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 0.5 | 0.70 | 92.67 | 90.91 | 90.11 | 91.73 | 390 | 28 | 23 | 255 |
| 0.5 | 0.80 | 92.67 | 90.68 | 92.19 | 89.21 | 397 | 21 | 30 | 248 |
| 0.5 | 0.60 | 92.10 | 90.30 | 88.58 | 92.09 | 385 | 33 | 22 | 256 |
| 0.5 | 0.90 | 92.53 | 90.04 | 96.31 | 84.53 | 409 | 9 | 43 | 235 |
| 0.5 | 0.50 | 91.24 | 89.43 | 86.29 | 92.81 | 377 | 41 | 20 | 258 |
**Best Configuration** (selected):
- CONF_THRESH: 0.50
- FALL_PROB_THRESH: 0.70
Accuracy 92.67%, F1 90.91%, Precision 90.11%, Recall 91.73%.
|