docs: new repo URL (andrew/...), requirements.txt, pytest smoke tests + CI
This commit is contained in:
1 parent
0235a0f597
commit
a568ebb182
10 files changed
+403
-1
No files matched your search
@@ -0,0 +1,17 @@
|
|||||||
|
name: ci
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
smoke:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.10"
|
||||||
|
- run: pip install numpy python-dotenv pytest
|
||||||
|
- run: python -m pytest tests/ -q
|
||||||
|
- run: python -m compileall -q src/ counter_dashboard.py
|
||||||
@@ -20,8 +20,11 @@ batch_mode.json
|
|||||||
hasil_perhitungan.json
|
hasil_perhitungan.json
|
||||||
live_status.json
|
live_status.json
|
||||||
batch_*.json
|
batch_*.json
|
||||||
|
batch_history_folder/
|
||||||
|
output/
|
||||||
*.tmp
|
*.tmp
|
||||||
*.tmp.*
|
*.tmp.*
|
||||||
|
*.log
|
||||||
|
|
||||||
# Media & large model binaries
|
# Media & large model binaries
|
||||||
*.mp4
|
*.mp4
|
||||||
@@ -36,6 +39,11 @@ batch_*.json
|
|||||||
*.zip
|
*.zip
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
|
|
||||||
|
# Test & coverage artifacts
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
# IDE & OS
|
# IDE & OS
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# AGENTS.md — karung (sack counter)
|
||||||
|
|
||||||
|
Repo: `https://git.proit.id/andrew/karung-counting-feedmill-semarang` (transferred from `ervan/`; old remote redirects, but `git remote set-url origin` to the new URL).
|
||||||
|
|
||||||
|
AI video analytics (Jetson) counting feed sacks loaded onto trucks. UI/log strings are
|
||||||
|
Indonesian (`karung`=sack, `truk`=truck); YOLO class names are English
|
||||||
|
(`sack`, `box`, `truck`, `person`). Details: `README.md`, `docs/`.
|
||||||
|
|
||||||
|
## Which pipeline to touch
|
||||||
|
|
||||||
|
- `predict.py` = **production** (runs as `karung-counter.service`). Combined sack+truck
|
||||||
|
model + `src/` modules, shapely zones, SQLite, live-frame publish.
|
||||||
|
- `src/` = clean v3 pipeline (`python -m src.main --source VID --env .env`). Edit here
|
||||||
|
for pipeline logic; production only picks it up via the `src.*` imports in `predict.py`.
|
||||||
|
- `predict_new.py`, `rpo_iki/`, `simple_predict.py` = alternate/experimental runners.
|
||||||
|
Don't "unify" them unprompted.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **Two env-key dialects**: production `.env` uses `RTSP_URL`, `DB_PATH`, `MODEL_PATH`,
|
||||||
|
… (`predict.py`, `counter_dashboard.py`); `src/config.py` reads different keys
|
||||||
|
(`LOCAL_RTSP`, `MODEL_SACK_PATH`, `MODEL_TRUCK_PATH`, …). Check which loader your
|
||||||
|
entry point uses before adding config.
|
||||||
|
- **Counting filters by class name, not ID**: `SackDetector` keeps `name == "sack"`
|
||||||
|
only (`src/detection.py`); tracker keeps `("sack", "truck")` (`src/tracking.py`).
|
||||||
|
The new `yolo11n-bbox-100ep-sack+box-*.pt` has a `box` class that **nothing consumes
|
||||||
|
yet** — adding box support means extending those allow-lists plus counter semantics.
|
||||||
|
- Verified checkpoint classes: `truck-detector`={truck}, `model_karung_truk`/`v4-best`=
|
||||||
|
{sack,truck}, `karung-dimuat-*-seg-200e`={person,sack} (seg; persons drawn, never
|
||||||
|
counted), `best`={sack}. `predict.py` auto-picks `MODEL_PATH` env, else
|
||||||
|
`model_karung_truk.engine` > `.pt` > `v4-best.pt`.
|
||||||
|
- Counting trigger is the bbox **top edge (y1)** vs a truck-anchored line band
|
||||||
|
(`src/counting.py`, `src/truck_roi.py`); truck detect runs every 15th frame only.
|
||||||
|
- **Tests:** `python -m pytest tests/ -q` (smoke tests for `counting`, `batch`,
|
||||||
|
`config` — pure-Python, no cv2/ultralytics needed). CI (`.github/workflows/ci.yml`)
|
||||||
|
installs only `numpy python-dotenv pytest` + runs pytest + `compileall`.
|
||||||
|
Deps: `requirements.txt` (unpinned; **never** pip-install torch from PyPI on the
|
||||||
|
Jetson — use the NVIDIA wheels already on device). For visual checks run on a video
|
||||||
|
file (`--source`), not the live RTSP.
|
||||||
|
- **Don't commit binaries/state**: `.gitignore` excludes `*.pt/*.onnx/*.engine`,
|
||||||
|
`*.mp4/*.jpg/*.png`, `*.db`, `.env`, `batch_history_folder/`. Large weights and
|
||||||
|
videos already sit untracked in the working tree — leave them alone.
|
||||||
|
|
||||||
|
## Deploy (Jetson `192.168.192.96`, user `jetson`)
|
||||||
|
|
||||||
|
- `deploy_to_jetson.py` syncs only `predict.py`, `counter_dashboard.py`,
|
||||||
|
`templates/{operator,monitoring,base}.html`, `.env`, then restarts services.
|
||||||
|
Edits elsewhere (e.g. `src/`, `zones.json`) need manual sync.
|
||||||
|
- Services: `karung-counter` (`predict.py`), `karung-counter-dashboard`
|
||||||
|
(`counter_dashboard.py`, ports 5000/5721). TensorRT export: `export_model.py`
|
||||||
|
(`export_v4.py` variant) — run on the Jetson, then repoint the loader at `.engine`.
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
# Karung Counter
|
||||||
|
|
||||||
|
> Repo: `https://git.proit.id/andrew/karung-counting-feedmill-semarang`
|
||||||
|
|
||||||
|
AI video-analytics system that **counts feed sacks (`karung-pakan`) being loaded onto /
|
||||||
|
unloaded from trucks** at a feedmill loading gate. It watches an RTSP camera, detects the
|
||||||
|
truck bed, tracks sacks with persistent IDs, counts line-crossings per truck (batch),
|
||||||
|
persists results to SQLite/CSV, and serves a live Flask dashboard.
|
||||||
|
|
||||||
|
> **Language note:** UI strings, logs and comments are largely in Indonesian
|
||||||
|
> (`karung` = sack/bag, `truk` = truck, `muat` = load). Class names inside the YOLO
|
||||||
|
> models are English (`sack`, `box`, `truck`, `person`).
|
||||||
|
|
||||||
|
## Pipeline (v3, `src/`)
|
||||||
|
|
||||||
|
```
|
||||||
|
Frame → TruckDetect → ROI → SackTrack → Stabilize → Count → Dashboard / DB
|
||||||
|
```
|
||||||
|
|
||||||
|
| Stage | Module | What it does |
|
||||||
|
|---|---|---|
|
||||||
|
| Stream | `src/streaming.py` | RTSP (`RTSPSource`) or video file (`VideoFileSource`) |
|
||||||
|
| Truck detect | `src/detection.py` (`TruckDetector`) | Dedicated truck model, run every 15 frames |
|
||||||
|
| ROI | `src/truck_roi.py` (`TruckROITracker`) | Picks main truck in lane, EMA-smooths bbox, derives counting line |
|
||||||
|
| Sack track | `src/tracking.py` (`ByteTrackTracker`) | YOLO + FastTrack/ByteTrack, tuned via `cfg/tracker.yaml` |
|
||||||
|
| Stabilize | `src/stabilizer.py` (`BboxStabilizer`) | EMA bbox smoothing + occlusion hold + height clamps |
|
||||||
|
| Count | `src/counting.py` (`LineCrossCounter`) | Zone-based line crossing: loading / unloading / net, 3-layer dedup |
|
||||||
|
| Batch | `src/batch.py` (`BatchLifecycleManager`) | 4-state FSM: IDLE → TRUCK_STABILIZING → COUNTING_SACKS ⇄ WAITING_FOR_ACTIVITY |
|
||||||
|
| Output | `src/logger.py`, `src/dashboard.py` | CSV logs + on-frame overlay |
|
||||||
|
|
||||||
|
Full detail: [`docs/architecture.md`](docs/architecture.md).
|
||||||
|
|
||||||
|
## Quickstart (development machine)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install ultralytics opencv-python shapely flask python-dotenv openpyxl torch
|
||||||
|
cp .env.example .env # then edit RTSP_URL / paths
|
||||||
|
python -m src.main --source path/to/video.mp4 # offline test
|
||||||
|
python -m src.main --env .env # live via LOCAL_RTSP
|
||||||
|
```
|
||||||
|
|
||||||
|
Keys in the preview window: `q` quit, `r` reset counter.
|
||||||
|
|
||||||
|
Production on the Jetson runs `predict.py` + `counter_dashboard.py` as systemd services
|
||||||
|
(see [`docs/deployment.md`](docs/deployment.md)) — the `src/` package is the clean
|
||||||
|
re-implementation; `predict.py` / `predict_new.py` are the deployed legacy pipelines that
|
||||||
|
add SQLite persistence, live-frame publishing to `/dev/shm`, and zone polygons.
|
||||||
|
|
||||||
|
## Models
|
||||||
|
|
||||||
|
| File | Classes | Role |
|
||||||
|
|---|---|---|
|
||||||
|
| `truck-detector.pt` (`.engine`) | `truck` | Specialized truck detector (ROI, batch lifecycle) |
|
||||||
|
| `model_karung_truk.pt` / `v4-best.pt` (`.engine`, `.onnx`) | `sack`, `truck` | Combined sack+truck model (legacy default in `predict.py`) |
|
||||||
|
| `karung-dimuat-detection-di-feedmill-yolo26n-seg-200e.pt` (`.engine`) | `person`, `sack` | Segmentation model: sacks counted, persons drawn/skipped |
|
||||||
|
| `yolo11n-bbox-100ep-sack+box-20260909-best.pt` | `sack`, `box` | New bbox model: sacks **and** boxes in one model |
|
||||||
|
| `best.pt` (`.engine`, `.onnx`) | `sack` | Sack-only baseline |
|
||||||
|
|
||||||
|
`.pt` = PyTorch, `.engine` = TensorRT FP16 (Jetson GPU), `.onnx` = ONNX export.
|
||||||
|
See [`docs/models.md`](docs/models.md) for the multi-model / multi-class / hybrid
|
||||||
|
support matrix.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `.env` (see `.env.example`) | DB paths, camera name, object label, cutoff time, ports, RTSP URL |
|
||||||
|
| `zones.json` | Calibrated pallet / truck / counting polygons + tuning knobs |
|
||||||
|
| `cfg/tracker.yaml` | FastTrack/ByteTrack occlusion tuning (buffer 60, re-ID windows) |
|
||||||
|
| `rpo_iki/configs/` | Alternate counting engine configs (areas, params, model registry, cameras) |
|
||||||
|
|
||||||
|
See [`docs/configuration.md`](docs/configuration.md) for every variable.
|
||||||
|
|
||||||
|
## Dashboard (`counter_dashboard.py`, port 5000 / office 5721)
|
||||||
|
|
||||||
|
Pages in `templates/`: `monitoring.html`, `operator.html` (manual batch start/stop),
|
||||||
|
`history.html`, `analytics.html`. JSON APIs under `/api/*` (live video MJPEG, current/
|
||||||
|
previous batch, summary, daily data, CSV/Excel export). Data source:
|
||||||
|
`jetson_counter.db` + `current_batch.json` (+ `batch_mode.json`).
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/ Clean v3 pipeline (streaming, detection, tracking, stabilizer,
|
||||||
|
truck_roi, counting, batch, dashboard, logger, config, main)
|
||||||
|
cfg/tracker.yaml Tracker tuning
|
||||||
|
predict.py Production Jetson pipeline (SQLite + live frame + HTTP status)
|
||||||
|
predict_new.py Alternate duo-model pipeline (truck + sack models, state machine)
|
||||||
|
rpo_iki/ Alternate geometric counting engine (count.py, predict_rpo_iki.py, configs/)
|
||||||
|
counter_dashboard.py Flask dashboard + APIs | templates/*.html pages
|
||||||
|
export_model.py Export .pt → TensorRT .engine (FP16) | export_v4.py variant
|
||||||
|
deploy_to_jetson.py Paramiko sync + service restart
|
||||||
|
*.service systemd units (counter, dashboard, mediamtx)
|
||||||
|
zones.json Calibrated zones
|
||||||
|
batch_history_folder/ Per-batch JSON reports
|
||||||
|
```
|
||||||
|
|
||||||
|
Script-by-script reference: [`docs/scripts.md`](docs/scripts.md).
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
- [`docs/architecture.md`](docs/architecture.md) — pipeline stages, modules, batch FSM
|
||||||
|
- [`docs/models.md`](docs/models.md) — model inventory & detection-mode support matrix
|
||||||
|
- [`docs/configuration.md`](docs/configuration.md) — all config files/variables
|
||||||
|
- [`docs/deployment.md`](docs/deployment.md) — Jetson services, TensorRT, deploy flow
|
||||||
|
- [`docs/scripts.md`](docs/scripts.md) — entry points & utility scripts
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# Deployment (Jetson)
|
||||||
|
|
||||||
|
Repo: `https://git.proit.id/andrew/karung-counting-feedmill-semarang`
|
||||||
|
(`git remote set-url origin <url>` after the ervan → andrew transfer).
|
||||||
|
|
||||||
|
## Systemd services
|
||||||
|
|
||||||
|
| Unit | Runs | After |
|
||||||
|
|---|---|---|
|
||||||
|
| `karung-counter.service` | `/usr/bin/python3 predict.py` (cwd `/home/jetson/karung`, `QT_QPA_PLATFORM=offscreen`) | `network.target` |
|
||||||
|
| `karung-counter-dashboard.service` | `/usr/bin/python3 counter_dashboard.py` | `network.target` + counter |
|
||||||
|
| `mediamtx.service` | MediaMTX restream (see `zones.json:external_stream_url`) | — |
|
||||||
|
|
||||||
|
Both app units: `Restart=always`, `RestartSec=5`, load `EnvironmentFile=/home/jetson/karung/.env`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo systemctl enable --now karung-counter karung-counter-dashboard
|
||||||
|
sudo systemctl restart karung-counter karung-counter-dashboard
|
||||||
|
systemctl status karung-counter karung-counter-dashboard --no-pager
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deploy flow (`deploy_to_jetson.py`)
|
||||||
|
Paramiko sync of `templates/{operator,monitoring,base}.html`, `counter_dashboard.py`,
|
||||||
|
`predict.py`, `.env` → `192.168.192.96:/home/jetson/karung/`, then restarts both
|
||||||
|
services and checks status + ports (5000/5721). Run from the dev machine.
|
||||||
|
|
||||||
|
## TensorRT export
|
||||||
|
On the Jetson (needs CUDA): `python3 export_model.py` exports
|
||||||
|
`karung-dimuat-detection-di-feedmill-yolo26n-seg-200e.pt` → FP16 `.engine`;
|
||||||
|
`export_v4.py` is the `v4-best` variant. Point `MODEL_PATH` / loader constants at the
|
||||||
|
`.engine` afterwards.
|
||||||
|
|
||||||
|
## Runtime data files
|
||||||
|
- SQLite `jetson_counter.db`: `batches(counting_date, batch_number, camera_name,
|
||||||
|
object_label, count, start/end_time)`, `daily_summaries(...)`.
|
||||||
|
- `current_batch.json` (crash recovery), `batch_mode.json` (manual/auto),
|
||||||
|
`batch_history_folder/batch_<ts>.json` + `hasil_perhitungan.json` (per-batch reports).
|
||||||
|
- Live frame: `/dev/shm/jetson-counter/live_frame.jpg` (written every 2nd frame,
|
||||||
|
consumed by `/api/live-video` MJPEG).
|
||||||
|
- Helpers: `backup.py`, `dump_db.py`, `check_jetson_db.py`, `migrate_jetson_db.py`,
|
||||||
|
`merge_batches_*.py`, `update_batches.py`, `diagnose_truck_jetson.py`.
|
||||||
|
|
||||||
|
## Dashboard (`counter_dashboard.py`)
|
||||||
|
Pages: `/` + `/monitoring`, `/operator` (manual start/stop, mode switch),
|
||||||
|
`/history`, `/analytics`. Key APIs: `/api/live-video`, `/api/current-batch`,
|
||||||
|
`/api/previous-batch`, `/api/batch/{start,stop,mode}`, `/api/summary`,
|
||||||
|
`/api/daily-data`, `/api/day-detail/<date>`, `/api/recent-batches`,
|
||||||
|
`/api/available-dates`, `/api/export-daily-csv`, `/api/export-day-csv/<date>`
|
||||||
|
(CSV + Excel via openpyxl).
|
||||||
+3
-1
@@ -336,7 +336,9 @@ class SackCounterPipeline:
|
|||||||
def save_batch_report(self):
|
def save_batch_report(self):
|
||||||
"""Menulis file laporan batch JSON ketika truk meninggalkan area."""
|
"""Menulis file laporan batch JSON ketika truk meninggalkan area."""
|
||||||
timestamp_str = time.strftime("%Y%m%d_%H%M%S")
|
timestamp_str = time.strftime("%Y%m%d_%H%M%S")
|
||||||
batch_file = f"batch_{timestamp_str}.json"
|
batch_folder = "batch_history_folder"
|
||||||
|
os.makedirs(batch_folder, exist_ok=True)
|
||||||
|
batch_file = os.path.join(batch_folder, f"batch_{timestamp_str}.json")
|
||||||
report_data = {
|
report_data = {
|
||||||
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"total_masuk_truck": self.total_masuk,
|
"total_masuk_truck": self.total_masuk,
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Runtime deps. Install on Jetson with: pip install -r requirements.txt
|
||||||
|
# NOTE (Jetson): do NOT pip-install torch from PyPI — use the NVIDIA Jetson
|
||||||
|
# torch/torchvision/torchaudio wheels already on the device.
|
||||||
|
ultralytics
|
||||||
|
opencv-python
|
||||||
|
numpy
|
||||||
|
shapely
|
||||||
|
flask
|
||||||
|
python-dotenv
|
||||||
|
openpyxl
|
||||||
|
paramiko # deploy_to_jetson.py only
|
||||||
|
|
||||||
|
# Test-only (dev/CI):
|
||||||
|
# pytest
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
"""Smoke tests for BatchLifecycleManager (src/batch.py). Stdlib only."""
|
||||||
|
|
||||||
|
from src.batch import BatchLifecycleManager
|
||||||
|
|
||||||
|
|
||||||
|
def _mgr(**kw):
|
||||||
|
args = dict(
|
||||||
|
stabilize_seconds=5.0,
|
||||||
|
stabilize_threshold_px=15.0,
|
||||||
|
sack_idle_timeout=10.0,
|
||||||
|
min_batch_duration=0.0,
|
||||||
|
truck_gone_tolerance=3.0,
|
||||||
|
)
|
||||||
|
args.update(kw)
|
||||||
|
return BatchLifecycleManager(**args)
|
||||||
|
|
||||||
|
|
||||||
|
def test_idle_to_counting_after_stable_truck():
|
||||||
|
m = _mgr()
|
||||||
|
started = []
|
||||||
|
m.on_batch_start(lambda bid, ts: started.append(bid))
|
||||||
|
m.update_truck(True, (500.0, 300.0), timestamp=1000.0)
|
||||||
|
assert m.state == "TRUCK_STABILIZING"
|
||||||
|
assert not m.is_active
|
||||||
|
m.update_truck(True, (501.0, 301.0), timestamp=1006.0) # stable 6s
|
||||||
|
assert m.state == "COUNTING_SACKS"
|
||||||
|
assert m.is_active and m.is_counting
|
||||||
|
assert started == [1]
|
||||||
|
|
||||||
|
|
||||||
|
def test_truck_leaving_resets_to_idle():
|
||||||
|
m = _mgr(stabilize_seconds=0.0) # instant start
|
||||||
|
m.update_truck(True, (500.0, 300.0), timestamp=1000.0)
|
||||||
|
assert m.is_active
|
||||||
|
ended = []
|
||||||
|
m.on_batch_end(ended.append)
|
||||||
|
m.update_sacks(False, 1, timestamp=1001.0, loading_count=3, unloading_count=1)
|
||||||
|
m.update_sacks(False, 0, timestamp=1012.0, loading_count=3, unloading_count=1) # idle -> waiting
|
||||||
|
assert m.state == "WAITING_FOR_ACTIVITY"
|
||||||
|
m.update_truck(False, None, timestamp=1020.0) # truck gone past tolerance
|
||||||
|
assert m.state == "IDLE"
|
||||||
|
assert len(ended) == 1
|
||||||
|
assert (ended[0].loading_count, ended[0].unloading_count) == (3, 1)
|
||||||
|
assert ended[0].net_count == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_activity_resumes_same_batch():
|
||||||
|
m = _mgr(stabilize_seconds=0.0)
|
||||||
|
m.update_truck(True, (500.0, 300.0), timestamp=1000.0)
|
||||||
|
bid = m.current_batch_id
|
||||||
|
m.update_sacks(False, 0, timestamp=1012.0) # -> waiting
|
||||||
|
assert m.is_waiting
|
||||||
|
m.update_sacks(True, 2, timestamp=1015.0) # sacks resume
|
||||||
|
assert m.state == "COUNTING_SACKS"
|
||||||
|
assert m.current_batch_id == bid # same batch, not a new one
|
||||||
|
|
||||||
|
|
||||||
|
def test_legacy_update_shim():
|
||||||
|
m = _mgr(stabilize_seconds=0.0)
|
||||||
|
# shim passes centroid=None, so IDLE never leaves without a real centroid
|
||||||
|
m.update(truck_detected=True, timestamp=1000.0)
|
||||||
|
assert m.state == "IDLE"
|
||||||
|
# ...but once counting via the real API, the shim feeds update_sacks fine
|
||||||
|
m.update_truck(True, (500.0, 300.0), timestamp=1000.0)
|
||||||
|
assert m.is_active
|
||||||
|
m.update(truck_detected=True, timestamp=1001.0, loading_count=2)
|
||||||
|
assert m.is_active
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
"""Smoke tests for config loader (src/config.py). Needs python-dotenv only."""
|
||||||
|
|
||||||
|
from src.config import load_config
|
||||||
|
|
||||||
|
_KEYS = [
|
||||||
|
"LOCAL_RTSP", "JETSON_RTSP", "MODEL_SACK_PATH", "MODEL_TRUCK_PATH",
|
||||||
|
"COUNTING_LINE_Y", "COUNTING_LINE_X_START", "COUNTING_LINE_X_END",
|
||||||
|
"SACK_CONF_THRESHOLD", "TRUCK_CONF_THRESHOLD", "BATCH_TIMEOUT_SECONDS",
|
||||||
|
"CSV_OUTPUT_DIR", "DATA_SEED",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_from_env_file(tmp_path, monkeypatch):
|
||||||
|
for k in _KEYS:
|
||||||
|
monkeypatch.delenv(k, raising=False)
|
||||||
|
env = tmp_path / "test.env"
|
||||||
|
env.write_text(
|
||||||
|
"LOCAL_RTSP=rtsp://cam/1\n"
|
||||||
|
"MODEL_SACK_PATH=/m/sack.pt\n"
|
||||||
|
"MODEL_TRUCK_PATH=/m/truck.pt\n"
|
||||||
|
"SACK_CONF_THRESHOLD=0.55\n"
|
||||||
|
)
|
||||||
|
cfg = load_config(str(env))
|
||||||
|
assert cfg.local_rtsp == "rtsp://cam/1"
|
||||||
|
assert cfg.sack_model_path == "/m/sack.pt"
|
||||||
|
assert cfg.truck_model_path == "/m/truck.pt"
|
||||||
|
assert cfg.sack_conf == 0.55
|
||||||
|
assert cfg.truck_conf == 0.50 # default preserved
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
"""Smoke tests for LineCrossCounter (src/counting.py). Pure-Python: needs only numpy."""
|
||||||
|
|
||||||
|
from src.counting import LineCrossCounter
|
||||||
|
from src.interfaces import Detection
|
||||||
|
|
||||||
|
|
||||||
|
def _det(tid, y1, cx=500.0):
|
||||||
|
return Detection(
|
||||||
|
bbox=(cx - 20, y1, cx + 20, y1 + 60),
|
||||||
|
confidence=0.9,
|
||||||
|
class_id=0,
|
||||||
|
class_name="sack",
|
||||||
|
track_id=tid,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _counter():
|
||||||
|
return LineCrossCounter(line_y=100, line_x_start=0, line_x_end=1000, margin=20)
|
||||||
|
|
||||||
|
|
||||||
|
def test_loading_above_then_below():
|
||||||
|
c = _counter()
|
||||||
|
assert c.update([_det(1, y1=10)]) == [] # above line
|
||||||
|
events = c.update([_det(1, y1=150)]) # below line
|
||||||
|
assert len(events) == 1
|
||||||
|
assert events[0]["direction"] == "loading"
|
||||||
|
assert c.loading_count == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_below_first_counts_as_unloading_not_loading():
|
||||||
|
c = _counter()
|
||||||
|
assert c.update([_det(2, y1=150)]) == [] # appeared below first
|
||||||
|
assert c.update([_det(2, y1=10)]) != [] # moved above => unloading
|
||||||
|
assert c.unloading_count == 1
|
||||||
|
assert c.loading_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_track_counted_once_per_direction():
|
||||||
|
c = _counter()
|
||||||
|
c.update([_det(3, y1=10)])
|
||||||
|
c.update([_det(3, y1=150)])
|
||||||
|
c.update([_det(3, y1=10)])
|
||||||
|
c.update([_det(3, y1=150)])
|
||||||
|
assert c.loading_count == 1 # track_id counted once for loading
|
||||||
|
|
||||||
|
|
||||||
|
def test_outside_x_bounds_skipped():
|
||||||
|
c = _counter()
|
||||||
|
c.update([_det(4, y1=10, cx=500.0)])
|
||||||
|
assert c.update([_det(4, y1=150, cx=5000.0)]) == []
|
||||||
|
assert c.loading_count == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_net_and_reset():
|
||||||
|
c = _counter()
|
||||||
|
c.update([_det(5, y1=10)])
|
||||||
|
c.update([_det(5, y1=150)])
|
||||||
|
assert c.net_count == 1
|
||||||
|
c.reset()
|
||||||
|
assert (c.loading_count, c.unloading_count, c.net_count) == (0, 0, 0)
|
||||||
Reference in new issue
Block a user