Overview
EvolveX detects loitering and fighting in CCTV streams and raises alerts with snapshot evidence. It is written from scratch on NumPy and the Python standard library. It uses no pretrained models: both models are trained only on footage you label yourself in EvolveX Studio.
| Component | What it does | Where |
|---|---|---|
| Monitor | Per camera: capture → detect → track → analyse → alert. Dashboard on port 8080 | suspect/main.py |
| EVX-Det | Person detector. RGB + motion channel in, person boxes out. ~1 M parameters, ~33 ms per 320×180 frame on 4 CPU threads | models/evx_person.npz |
| Fight classifier | Small CNN over 8 crops of a tracked person plus 8 frame differences (16×48×48) | models/evx_fight.npz |
| Loitering rules | Dwell time inside a zone with a confined path | suspect/behaviour.py |
| EvolveX Studio | Label footage, train, test and promote models. Port 8090 | suspect/studio/ |
Generations. A Gen is a major version: Gen1 = 1.x.y, Gen2 = 2.0.0. Minor versions add features or retrained models; patch versions fix and tune. Every saved model file records the version that made it. See Generations.
Who uses what
Website visitors use neither. The monitor is for the people watching a site's cameras; Studio is for the people who train the models.
| Monitor dashboard | EvolveX Studio | |
|---|---|---|
| Who | The customer's security team (guards, control-room operators), and the EvolveX team while setting up and supporting a pilot | The EvolveX team that trains the models; at most a trusted person at the customer who helps label footage |
| What for | Watch every camera live with tracked people; see loitering and fighting alerts as they happen, with a snapshot; alerts can also go by email or webhook | Upload or record footage, box the people, mark the fights, train the person detector and the fight classifier, test them, and promote a better model to the monitor |
| How often | All day: it is the working screen at the site | During setup, then whenever new footage is labelled |
| Where | Port 8080, e.g. monitor.evolvex.my, or a device on the customer's premises | Port 8090, e.g. studio.evolvex.my |
Keep the logins separate. Give each app its own EVX_USER and EVX_PASSWORD, and give customers only the monitor login: Studio holds every site's training footage and can replace the models the monitor runs.
Install
You need Python 3 with NumPy, and the ffmpeg binary.
pip install numpy
sudo apt install ffmpeg
cp config.example.json config.json # add your cameras
cp .env.example .env # login, database, R2 and email settings
python -m suspect.main --version
- GPU (NVIDIA Jetson): install CuPy in its own virtual environment and point
training.gpu_pythoninconfig.jsonat it. Training is about 6× faster. - Server: the monitor and Studio run as Docker containers. Set
EVX_USERandEVX_PASSWORDwhenever they are reachable from the internet. - Storage: local files by default; PostgreSQL and Cloudflare R2 when
DATABASE_URLandR2_*are set.
Using the model
Start the monitor with python -m suspect.main --config config.json and open http://localhost:8080. Until you promote trained models, it finds people with its motion detector and scores fights with a motion-energy heuristic.
1. Add cameras
List them under cameras in config.json. Names must be unique.
| Source | Example source | Extra settings |
|---|---|---|
| IP camera | rtsp://user:password@192.168.1.10:554/stream1 | — |
| USB camera | /dev/video0 | "input_args": ["-f","v4l2"] |
| Video file | videos/test.mp4 | "loop": true to replay |
2. Choose the detector
Set processing.detector to "cnn" to use EVX-Det. It falls back to "motion" if the model file is missing. det_conf (default 0.3) is its score threshold.
3. Draw loitering zones
loiter_zones are polygons in 0–1 coordinates of the frame; the default is the whole frame.
4. Tune behaviour per camera
| Setting | Default | Meaning |
|---|---|---|
loiter_seconds | 30 | Dwell time in a zone before alerting |
loiter_confine_frac | 0.2 | Path must stay within this fraction of the frame |
loiter_repeat_seconds | 60 | Re-alert interval while still loitering |
fight_threshold | 0.7 | Smoothed fight probability needed |
fight_confirm | 3 | Consecutive positive evaluations needed |
fight_every_frames | 3 | Run the fight CNN every N frames per person |
fight_min_energy | 0.02 | Skip the CNN when the crop barely moves |
fight_smooth | 0.6 | Smoothing of the fight score over time |
5. Receive alerts
Each alert is written to evidence/alerts.jsonl with an annotated PNG snapshot. It can also be sent to a webhook_url and emailed with the snapshot (ALERT_EMAIL_TO and SMTP_*; test with python -m suspect.notify).
Using the models from Python
from suspect.nn.evxdet import load_evx_det
from suspect.nn.model import load_fight_model
det = load_evx_det("models/evx_person.npz")
for x0, y0, x1, y1, score in det.detect(rgb, motion, conf=0.3):
... # rgb: (H,W,3) uint8, motion: (H,W) uint8
fight = load_fight_model("models/evx_fight.npz")
The motion input must come from BackgroundModel.motion_u8(), computed exactly as the monitor computes it.
Training
Start Studio with python -m suspect.studio and open http://localhost:8090. A model only knows the scenes it was trained on, so label footage from every camera you deploy on.
- Collect. Upload videos or record from a configured camera. Cover different times of day; people walking, standing and queueing; and fight look-alikes such as running, hugging and play.
- Prepare. Each video is decoded at the monitor's width and frame rate, keeping every 5th frame for labelling. If you change
processing.widthorfps, re-prepare and retrain. - Label people. Drag a box around every person on each kept frame and press Enter. Press E for a frame with nobody in it. Press S for suggested boxes, then check them.
- Label fights. Press
[where a fight starts and]where it ends, then tick Fight labelling complete. Only ticked videos train the fight classifier. - Split.
autogives the first 70 % of a video to training, the next 15 % to validation and the last 15 % to test. With several videos, give each whole video to one split. - Train. Train the person detector first, then the fight classifier. The best epoch is kept: the person detector by (AP50 + F1) / 2, the fight classifier by F1.
- Test. Run the test split. Person: AP at IoU 0.5 and precision, recall and F1 by threshold. Fight: accuracy, precision, recall, F1, AUC and the confusion matrix.
- Promote. Promote a run that beats production. The old model is archived and
models/registry.jsonrecords the change. Restart the monitor. - Evolve. Pre-label new footage with the production model, correct it, retrain, test and promote.
Command line
python -m suspect.training import clip.mp4
python -m suspect.training prepare --step 5
python -m suspect.training status
python -m suspect.training train person --epochs 60 --batch 16
python -m suspect.training train fight --epochs 30
python -m suspect.training test person-20260929-101500
python -m suspect.training promote person-20260929-101500
# GPU on the Jetson
SUSPECT_BACKEND=cupy ~/.venvs/suspect/bin/python -m suspect.training train person
Training parameters
Override any of them with --key value on the command line, or in Studio's form.
| Parameter | Person | Fight | Meaning |
|---|---|---|---|
epochs | 60 | 30 | Passes over the training set |
batch | 16 | 32 | Samples per step |
lr | 0.002 | 0.001 | Adam learning rate |
width | 16 | 16 | Network width; sets size and speed |
seed | 0 | 0 | Random seed |
motion | true | — | Feed the motion channel (randomly dropped in training) |
eval_every | 2 | — | Validate every N epochs |
conf | 0.3 | — | Score threshold for validation F1 |
stride | — | 4 | Sample a clip every N frames per person |
min_energy | — | 0.03 | Skip crops that barely move |
detector | — | motion | Detector used to replay the videos (motion or cnn) |
Each run is saved in training/runs/<kind>-<timestamp>/. On a server CPU, expect about 0.2 s per labelled frame per epoch; start with a few hundred frames.
Smoke test without footage: python tools/make_synthetic.py && python -m suspect.training train person --epochs 12. The synthetic models are useless on real cameras; delete training/ afterwards.
Releasing a Gen
Each Gen has a permanent entry on the Generations page and a test report. python tools/gen_report.py --gen N writes the production models' test results, training-data counts and SHA-256 checksums into it. The model files are published as a GitHub release with a Zenodo DOI. The full checklist is docs/RELEASE.md in the repository.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Two people close together show as one box | The motion detector merges nearby blobs | Train EVX-Det and set detector to "cnn" |
| Trees, cars or shadows are tracked | The motion detector treats any movement as a person | Train EVX-Det; narrow loiter_zones |
| Loitering timer restarts | Person lost for more than max_misses frames | Raise max_misses (default 15) |
| Promoted model not used | The monitor loads models at startup | Restart the monitor |
prepare refuses a video | Its labels were made at another width, fps or step | Re-prepare with the original step, or relabel |
is not an evx-det-1 model | Model file from another architecture | Use weights trained for the current architecture |
| Training slow on the Jetson | All 8 cores in use | Keep OMP_NUM_THREADS=4 (the default), or use the GPU |
| Camera stream freezes | The camera stopped sending | The monitor reconnects after read_timeout seconds |
Citing EvolveX
Cite the generation you used. Each Gen's BibTeX, and its DOI once minted, is on the Generations page. List your paper on Publications.
@software{evolvex_gen1,
title = {EvolveX Gen1: Evolving Vigilance for Behavioural Exceptions},
author = {Alobaedy, Mustafa},
year = {2026},
url = {https://evolvex.my}
}
EvolveX is licensed under the PolyForm Noncommercial License 1.0.0.