- Fall Detection - XGBoost Classifier
Fall Detection - XGBoost Classifier
Model description
- Model name: XGBoost Priority 1 Fall Detection Classifier
- Version:
v1(Experiment 01) - Status:
experimantal - Repository visibility: internal
- Model type: binary gradient-boosted classifier (
xgboost.XGBClassifier) - Input contract: 51 keypoint features only (17 keypoints Γ
x, y, confidence) + bbox(x1,y1,x2,y2)β the extra 5 features (aspect_ratio,nose_relative_y,torso_angle,norm_com_y,head_hip_v_dist) are extracted inscripts/run.py, not received from upstream; no RGB image input - Output contract:
Fall/Normal(P(Fall) >= 0.70β Fall) - Model artifact:
models/xgboost_priority1_fall_model.pkl - Reference date: 2026-09-21
This repository documents only the classifier stage of the fall detection system. Input contract: 51 keypoint features + bbox only. The model itself is fed a 56-column pandas DataFrame that scripts/run.py builds by extracting the extra 5 features from that input β upstream never provides the 5, and the model never sees pixels, frames, or CSV files directly.
CCTV IMAGE β YOLO26x-Pose β 51 features + bbox (DataFrame) β run.py: +5 features β 56-col DataFrame β XGBoost β Fall / Normal
upstream (out of scope) this repo β direct model input
The classifier achieves 91.00% accuracy on the standard test set, 92.67% accuracy / 90.91% F1 on real-world CCTV evaluation, and up to 98.05% precision for fall events at a strict probability threshold.
Problem statement
Classifying falls directly from raw keypoint coordinates suffers from a severe domain shift: a raw-keypoint baseline reached 92.22% accuracy on a standard train/test split but collapsed to 66.83% on real-world CCTV footage.
Analysis of real-world failures revealed three critical bottlenecks:
- Camera Angle & Height Sensitivity: Raw coordinates (X, Y) shift significantly when camera pitch, installation height, or lens focal length changes
- Crowd & Limb Occlusion: Lower-body keypoints (knees, ankles) are frequently obscured by furniture or bystanders, causing standard feature vectors to drop key geometry
- Loss of Relative Posture Ratios: Single coordinates lack explicit representation of body aspect ratios, torso inclination, and center-of-mass vertical drops
This model resolves the problem with an engineered 56-feature representation incorporating 5 Priority 1 CCTV-invariant features, improving real-world accuracy to 88.36%+ while keeping 91% on standard test sets.
Input contract
This model/pipeline does not take an RGB image. The input contract is only the 51 keypoint features (+ bounding box needed to derive the engineered features). The extra 5 features are extracted inside scripts/run.py β they are never part of the upstream input.
What goes IN (upstream β run.py): 51 features + bbox
- Upstream (out of scope): YOLO-Pose β 17 keypoints β 51 features only (
x_i, y_i, conf_ifor i = 0β¦16, x/y normalized to bbox) + bounding box(x1, y1, x2, y2), delivered as a pandas DataFrame scripts/run.py(this repo): extracts the 5 engineered features from that input β builds the 56-column DataFrame β passes it to XGBoost
CCTV IMAGE
β
βΌ
YOLO26x-Pose (upstream)
β
βββ Person bounding box (x1, y1, x2, y2)
β
βββ 17 pose keypoints (x, y, confidence)
β
βΌ
INPUT CONTRACT: 51 features + bbox (pandas DataFrame)
β
β scripts/run.py extracts:
βββ aspect_ratio
βββ nose_relative_y
βββ torso_angle
βββ norm_com_y
βββ head_hip_v_dist
β
βΌ
56-column DataFrame β XGBoost.predict_proba
Input columns accepted by 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 |
- Dtype:
float;NaNallowed for occluded keypoints (conf < 0.50 β x/y = NaN) - Not accepted: RGB/BGR images, video frames, raw pixels β only the 51 features + bbox DataFrame
- Validation: each row should have β₯ 5 valid keypoints; otherwise prediction is skipped
- Cardinality: one row = one person = one prediction. No identity tracking across frames
What run.py extracts (the extra 5 β computed here, not received)
The 5 engineered features are derived inside run.py from the 51 + bbox input and appended as columns 52β56:
| Pos | Feature | Formula | Derived from |
|---|---|---|---|
| 52 | aspect_ratio |
w / h |
bbox |
| 53 | nose_relative_y |
(Y_nose - y1) / h |
keypoint 0 (nose) |
| 54 | torso_angle |
Angle HipMid β ShoulderMid vs. vertical Y-axis (degrees) | keypoints 5,6,11,12 + bbox |
| 55 | norm_com_y |
Confidence-weighted CoM Y: (Ξ£(Y_iΒ·conf_i)/Ξ£(conf_i) - y1)/h |
all visible keypoints |
| 56 | head_hip_v_dist |
(Y_hip_mid - Y_nose) / h |
keypoints 0,11,12 |
What XGBoost receives (after run.py)
- Type:
pandas.DataFrame(in-memory; not a CSV file) - Shape:
(n, 56)β 51 input features + 5 extracted inrun.py - Column order:
FEATURE_COLSinscripts/run.py(positions 1β51 upstream, 52β56 computed here) - CSV is never fed to the model β it is only a transport format into a DataFrame
Feature engineering rationale
The 5 Priority 1 CCTV-invariant features give the classifier a representation that raw coordinates cannot provide:
| Feature Name | Formula | CCTV Robustness | Physical Significance |
|---|---|---|---|
| aspect_ratio | W / H | Very High | Bounding box horizontal spread. Upright: < 1.0, Lying: > 1.0 |
| nose_relative_y | (Y_nose - Y1) / H | Very High | Head position in box. Standing: 0.0β0.25, Fallen: 0.7β1.0 |
| torso_angle | Angle(Shoulder-Mid β Hip-Mid) vs Y-axis | High | Resists perspective. Standing: ~0Β°, Fallen: 80β90Β° |
| norm_com_y | Weighted CoM using visible keypoints | High | Vertical center-of-mass. Robust to leg occlusion |
| head_hip_v_dist | (Y_hip - Y_nose) / H | High | Upper-body extension. Collapses near 0.0 when lying flat |
These features provide:
- Scale invariance: Ratios and normalized coordinates remain stable across camera distances
- Angle invariance: Geometric relationships resist perspective distortion
- Occlusion robustness: Upper-body features work even when the lower body is blocked
Pipeline architecture
CCTV IMAGE
β
βΌ
YOLO26x-Pose
β
βββ Person bounding box
β
βββ 17 pose keypoints
β
βββ x
βββ y
βββ confidence
β
βΌ
51 features (17 keypoints Γ 3) + bbox β upstream pandas DataFrame
β
β scripts/run.py:
β build_feature_frame(upstream_df)
βββ aspect_ratio
βββ nose_relative_y
βββ torso_angle
βββ norm_com_y
βββ head_hip_v_dist
β
βΌ
56-column pandas DataFrame βββΊ XGBoost.predict_proba(df) βββΊ Fall / Normal
Upstream (out of scope) produces the 51 keypoint features + bounding box as a DataFrame. scripts/run.py computes the 5 engineered features, builds the 56-column DataFrame, and passes that DataFrame to XGBoost β that stage is in scope for this repository. The model never receives a CSV at inference.
Output contract
For each row of the 56-column DataFrame the XGBoost classifier returns a binary decision:
56-column DataFrame β XGBoost β Fall / Normal
model.predict(X)β class id:0= Normal,1= Fallmodel.predict_proba(X)β[P(Normal), P(Fall)]- Decision rule:
P(Fall) >= FALL_PROB_THRESH (0.70)βFall, otherwiseNormal
Example output for one person:
{
"label_id": 1,
"prediction": "Fall",
"fall_probability": 0.97,
"normal_probability": 0.03,
"threshold": 0.70
}
One input vector yields exactly one Fall / Normal label. Class mapping is defined in config.json (0: Normal, 1: Fall). fall_probability can be threshold-tuned (see Configuration and thresholds).
Configuration and thresholds
Classifier configuration lives in config.json:
CONF_THRESH = 0.50 # Input-side: keypoint confidence below this -> NaN coordinates
FALL_PROB_THRESH = 0.70 # Decision threshold on P(Fall) (optimized via grid search)
Optimized performance at these thresholds:
- Accuracy: 92.67%
- F1-Score: 90.91%
- Precision: 90.11%
- Recall: 91.73%
Tunable threshold guidance β adjust FALL_PROB_THRESH based on deployment priorities:
- High precision (fewer false alarms): 0.95β0.98 (up to 98.05% precision)
- High recall (catch more falls): 0.80β0.90
- Balanced (default): 0.70
CONF_THRESH controls how aggressively occluded keypoints become NaN. Higher values tolerate less occlusion; lower values keep noisier coordinates.
Model architecture and training
Algorithm: XGBoost gradient-boosted decision trees, trained from scratch on the 56-feature dataset.
| Hyperparameter | Value |
|---|---|
n_estimators |
150 |
max_depth |
5 |
learning_rate |
0.03 |
scale_pos_weight |
1.0 (train split pre-balanced 1:1) |
missing |
np.nan (native occlusion handling) |
eval_metric |
logloss |
random_state |
42 |
Training procedure (scripts/train.py):
- Load
pose_benchmark_priority1_dataset.csv(56 features +split+label) - Undersample the train split to an exact 1:1 Normal:Fall ratio (
random_state=42); test split is left intact - Train XGBoost on raw features with native NaN support (no imputer/scaler)
- Benchmark against Random Forest, SVM (RBF) and MLP; export all artifacts to
models/
Why XGBoost: it outperformed Random Forest, SVM and MLP on both standard and real-world test sets, and its native missing=np.nan handling removes the need for an imputation pipeline when keypoints are occluded.
Runtime requirements
- Python: 3.10+
- Key dependencies (classifier only):
- XGBoost β₯ 2.0.0 (native NaN handling for occluded keypoints)
- NumPy β₯ 1.26.0
- joblib β₯ 1.3.0 (model serialization)
- pandas β₯ 2.0.0, scikit-learn β₯ 1.3.0 (training and evaluation only)
Install dependencies:
pip install -r requirements.txt
The classifier is lightweight and runs on CPU; no GPU is required for training or inference.
Performance
Training performance (test set, after 1:1 balancing)
Dataset: 6,607 samples (3,207 Normal, 3,400 Fall) Model: XGBoost with 56 engineered features
precision recall f1-score support
Normal (0) 0.90 0.91 0.91 3207
Fall (1) 0.92 0.91 0.91 3400
accuracy 0.91 6607
macro avg 0.91 0.91 0.91 6607
weighted avg 0.91 0.91 0.91 6607
Overall accuracy: 91.00%
Threshold grid search (real-world CCTV evaluation)
Grid search over the input keypoint confidence and the decision threshold:
- Keypoint confidence threshold: 0.25, 0.50
- Fall probability threshold: 0.20 β 0.95 (9 steps)
Top parameter combinations (by F1-score):
| Keypoint_Conf | Fall_Prob_Thresh | Accuracy | F1_Score | Precision | Recall | TN | FP | FN | TP |
|---|---|---|---|---|---|---|---|---|---|
| 0.50 | 0.70 | 92.67% | 90.91% | 90.11% | 91.73% | 390 | 28 | 23 | 255 |
| 0.50 | 0.80 | 92.67% | 90.68% | 92.19% | 89.21% | 397 | 21 | 30 | 248 |
| 0.50 | 0.60 | 92.10% | 90.30% | 88.58% | 92.09% | 385 | 33 | 22 | 256 |
| 0.50 | 0.90 | 92.53% | 90.04% | 96.31% | 84.53% | 409 | 9 | 43 | 235 |
| 0.50 | 0.50 | 91.24% | 89.43% | 86.29% | 92.81% | 377 | 41 | 20 | 258 |
| 0.50 | 0.40 | 90.80% | 89.19% | 84.08% | 94.96% | 368 | 50 | 14 | 264 |
| 0.50 | 0.30 | 89.51% | 88.09% | 80.60% | 97.12% | 353 | 65 | 8 | 270 |
| 0.50 | 0.20 | 88.07% | 86.80% | 77.78% | 98.20% | 340 | 78 | 5 | 273 |
| 0.50 | 0.95 | 88.36% | 83.23% | 98.05% | 72.30% | 414 | 4 | 77 | 201 |
Confusion matrix (selected configuration, threshold 0.70):
Predicted
Normal Fall
Actual Normal 390 28 (TN, FP)
Fall 23 255 (FN, TP)
Key insights:
- A keypoint confidence of 0.50 gives the best balance between occlusion tolerance and noisy coordinates
- A fall probability threshold of 0.70 achieves the best F1-score; stricter 0.95 maximizes precision (98.05%) but misses ~28% of falls
- This configuration (CONF_THRESH=0.50, FALL_PROB_THRESH=0.70) is the deployment default
Intended use
This classifier is intended for feature-vector-based fall detection in:
- Live concert environment
- On-the-road and touring conditions
- IN indoor condition
This is a safety assistance tool, not a replacement for human supervision.
Limitations
- Degrades on heavily degraded inputs: vectors where most keypoints are
NaN(severe occlusion, >70% of body blocked) or extreme camera angles (>60Β° from vertical) fall outside the training distribution - No temporal smoothing: single-vector classification without motion history. May produce momentary false positives during normal sitting/bending motions
- No identity tracking: each vector is classified independently; deployments need their own deduplication for repeated alerts on the same person
- Threshold-sensitive: precision/recall trade-offs depend on
FALL_PROB_THRESHand must be tuned per environment - Training-distribution bound: trained on a specific dataset; performance on new camera placements, demographics, or fall types should be validated before experimental use
- Oclusion: if a fallen person is ocluded by a perosn or a object it might don't detect the person as fallen
Dataset location
Training and evaluation datasets are stored at:
/home/ctspl/model_training/fall/version3/data/
Ownership
- Model trainer/developer: Nishant Prasad
- Dataset source: Internal CCTV footage collection
- Model architecture: XGBoost (
XGBClassifier, 56 input features) - Training date: 2026-09-21
- Status: experimental
Repository layout
fall-detection/
βββ README.md
βββ MODEL_CARD.md
βββ CHANGELOG.md
βββ config.json # Feature schema, thresholds and paths
βββ requirements.txt
βββ models/
β βββ xgboost_priority1_fall_model.pkl # Trained XGBoost classifier
βββ scripts/
β βββ train.py # Training + benchmark pipeline
β βββ evaluate.ipynb # Evaluation notebook (DataFrame path)
β βββ run.py # Inference: upstream DF (51+bbox) β +5 β 56-col DF β XGBoost
βββ docs/
β βββ spec.md # Development specification
β βββ data.md # Dataset schema
β βββ train.log.md # Training provenance
βββ data/ # Datasets (not committed)
- Downloads last month
- -