Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/config.local
/tmp
/cache
5 changes: 5 additions & 0 deletions experiments/temporal-models/temporal-fire-tube/.dvc/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[core]
remote = s3remote
analytics = false
['remote "s3remote"']
url = s3://pyro-vision-rd/dvc/experiments/temporal-fire-tube/
3 changes: 3 additions & 0 deletions experiments/temporal-models/temporal-fire-tube/.dvcignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Add patterns of files dvc should ignore, which could improve
# the performance. Learn more at
# https://dvc.org/doc/user-guide/dvcignore
2 changes: 2 additions & 0 deletions experiments/temporal-models/temporal-fire-tube/.gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.ipynb filter=nbstripout
*.ipynb diff=ipynb
12 changes: 12 additions & 0 deletions experiments/temporal-models/temporal-fire-tube/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
__pycache__/
*.pyc
.venv/
.ipynb_checkpoints/
*.egg-info/
.pytest_cache/
.ruff_cache/
.cache/
build/
dist/
htmlcov/
.coverage
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.11
19 changes: 19 additions & 0 deletions experiments/temporal-models/temporal-fire-tube/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.PHONY: install lint format test help

help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'

install: ## Install dependencies and set up notebook output stripping
uv sync
uv run nbstripout --install

lint: ## Run ruff linter on code and notebooks
uv run ruff check .
@find notebooks/ -name '*.ipynb' 2>/dev/null | grep -q . && uv run nbqa ruff check notebooks/ || true

format: ## Format code and notebooks with ruff
uv run ruff format .
@find notebooks/ -name '*.ipynb' 2>/dev/null | grep -q . && uv run nbqa ruff format notebooks/ || true

test: ## Run tests with pytest
uv run pytest tests/ -v
55 changes: 55 additions & 0 deletions experiments/temporal-models/temporal-fire-tube/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Temporal Fire-Tube

## Objective

Reduce false positives from YOLO-based smoke/fire detection by applying a temporal
verification step inspired by the fire-tube method (Park & Ko, 2020). This experiment builds 3D "tubes" from tracked detections across frames, extracts temporal features from each tube, and classifies with a Random Forest.

## Approach

Adapted two-step pipeline from "Two-Step Real-Time Night-Time Fire Detection
Using Static ELASTIC-YOLOv3 and Temporal Fire-Tube" (Park & Ko, Sensors 2020):

1. **YOLO inference** -- YOLOv11s detects smoke candidates in each frame.
2. **Padding** -- Short sequences padded to minimum length by repeating boundary frames.
3. **Tube construction** -- Detections tracked across frames via greedy IoU matching. For each tracked chain, the corresponding image regions are cropped and resized into a fire-tube.
4. **Feature extraction** -- Tabular features (24 dims) computed per tube: area change, centroid shift, intensity change, histogram distance, confidence -- aggregated across consecutive pairs plus global features.
5. **Classification** -- Random Forest (120 trees, depth 20) with balanced class weights classifies each tube as smoke/non-smoke.
6. **Sequence decision** -- Positive if any tube is classified positive.

**Key adaptation for Pyronear's 30s cadence:** The original paper uses optical flow on dense video (30fps). At 30-second intervals optical flow is meaningless, so we replace HoF features with tabular temporal features that capture how the detection region evolves over time.

## Data

Same dataset as `tracking-fsm-baseline` (shared `01_raw` DVC data):

- Train: ~1,034 wildfire + ~1,433 false positive sequences (Pyronear only)
- Val: ~112 wildfire + ~147 FP sequences (Pyronear only)
- Ground truth: sequence-level binary labels

## Results

*TBD -- run `dvc repro` to generate.*

## Pipeline

```
infer (01_raw -> 02_intermediate)
-> pad (02_intermediate -> 03_primary)
-> build_tubes (03_primary + 01_raw -> 04_feature)
-> extract_features (04_feature -> 05_model_input)
-> train (05_model_input/train -> 06_models)
-> predict (06_models + 05_model_input -> 07_model_output)
-> evaluate (07_model_output -> 08_reporting)
```

## How to Reproduce

```bash
make install # Install dependencies
dvc pull # Download data from S3
dvc repro # Run full pipeline
make lint # Run linter
make test # Run tests
```

Empty file.
Empty file.
Empty file.
Empty file.
235 changes: 235 additions & 0 deletions experiments/temporal-models/temporal-fire-tube/dvc.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
stages:
# --- YOLO Inference ---
infer:
foreach:
- train
- val
do:
desc: "Run YOLO inference on ${item} set images to produce raw smoke detections"
cmd: >-
uv run python scripts/infer.py
--data-dir data/01_raw/datasets/${item}
--model-path data/01_raw/models/${prepare.model_filename}
--output-dir data/02_intermediate/${item}
--confidence-threshold ${infer.confidence_threshold}
--iou-nms ${infer.iou_nms}
--image-size ${infer.image_size}
deps:
- scripts/infer.py
- src/data.py
- src/detector.py
- src/types.py
- data/01_raw/datasets/${item}
- data/01_raw/models/${prepare.model_filename}
params:
- prepare.model_filename
- infer
outs:
- data/02_intermediate/${item}

# --- Pad short sequences ---
pad:
foreach:
- train
- val
do:
desc: "Pad short ${item} sequences by repeating boundary frames"
cmd: >-
uv run python scripts/pad_sequences.py
--infer-dir data/02_intermediate/${item}
--output-dir data/03_primary/${item}
--min-sequence-length ${pad.min_sequence_length}
deps:
- scripts/pad_sequences.py
- src/data.py
- src/detector.py
- src/types.py
- data/02_intermediate/${item}
params:
- pad
outs:
- data/03_primary/${item}

# --- Build fire-tubes ---
build_tubes:
foreach:
- train
- val
do:
desc: "Build fire-tubes from ${item} set detections and images"
cmd: >-
uv run python scripts/build_tubes.py
--infer-dir data/03_primary/${item}
--data-dir data/01_raw/datasets/${item}
--output-dir data/04_feature/${item}
--crop-size ${tube.crop_size}
--max-tube-length ${tube.max_tube_length}
--confidence-threshold ${tube.confidence_threshold}
--max-detection-area ${tube.max_detection_area}
--iou-threshold ${tube.iou_threshold}
deps:
- scripts/build_tubes.py
- src/tube.py
- src/data.py
- src/detector.py
- src/types.py
- data/03_primary/${item}
- data/01_raw/datasets/${item}
params:
- tube
outs:
- data/04_feature/${item}

# --- Extract features ---
extract_features:
foreach:
- train
- val
do:
desc: "Extract tabular features from ${item} set fire-tubes"
cmd: >-
uv run python scripts/extract_features.py
--tube-dir data/04_feature/${item}
--output-dir data/05_model_input/${item}
deps:
- scripts/extract_features.py
- src/features.py
- src/types.py
- data/04_feature/${item}
params:
- features
outs:
- data/05_model_input/${item}

# --- Train classifier ---
train:
desc: "Train Random Forest classifier on tube features"
cmd: >-
uv run python scripts/train_classifier.py
--feature-dir data/05_model_input/train
--output-dir data/06_models
--n-estimators ${classifier.n_estimators}
--max-depth ${classifier.max_depth}
--random-state ${classifier.random_state}
deps:
- scripts/train_classifier.py
- src/classifier.py
- data/05_model_input/train
params:
- classifier
outs:
- data/06_models

# --- Predict ---
predict:
foreach:
- train
- val
do:
desc: "Run fire-tube predictions on ${item} set"
cmd: >-
uv run python scripts/predict.py
--feature-dir data/05_model_input/${item}
--model-dir data/06_models
--data-dir data/01_raw/datasets/${item}
--infer-dir data/03_primary/${item}
--output-dir data/07_model_output/${item}
deps:
- scripts/predict.py
- src/classifier.py
- src/data.py
- src/detector.py
- src/types.py
- data/05_model_input/${item}
- data/06_models
- data/01_raw/datasets/${item}
- data/03_primary/${item}
outs:
- data/07_model_output/${item}

# --- Package ---
package:
desc: "Bundle YOLO weights and classifier into a single deployable archive"
cmd: >-
uv run python scripts/package.py
--weights-path data/01_raw/models/${prepare.model_filename}
--classifier-path data/06_models/classifier.pkl
--params-path params.yaml
--output-path data/06_models/model.zip
deps:
- scripts/package.py
- src/package.py
- data/01_raw/models/${prepare.model_filename}
- data/06_models/classifier.pkl
params:
- prepare.model_filename
- infer
- tube
outs:
- data/06_models/model.zip

# --- Evaluate ---
evaluate:
foreach:
train_all:
split: train
subset: all
filter_flag: ""
val_all:
split: val
subset: all
filter_flag: ""
train_pyronear:
split: train
subset: pyronear
filter_flag: "--filter-prefix pyronear"
val_pyronear:
split: val
subset: pyronear
filter_flag: "--filter-prefix pyronear"
do:
desc: "Compute sequence-level metrics on ${item.split} set (${item.subset})"
cmd: >-
uv run python scripts/evaluate.py
--results-dir data/07_model_output/${item.split}
--output-dir data/08_reporting/${item.split}/${item.subset}
${item.filter_flag}
deps:
- scripts/evaluate.py
- src/evaluator.py
- data/07_model_output/${item.split}
metrics:
- data/08_reporting/${item.split}/${item.subset}/metrics.json:
cache: false
plots:
- data/08_reporting/${item.split}/${item.subset}/plots

# --- Sweep ---
sweep:
foreach:
train_all:
split: train
subset: all
filter_flag: ""
train_pyronear:
split: train
subset: pyronear
filter_flag: "--filter-prefix pyronear"
do:
desc: "Grid-search fire-tube parameters on ${item.split} set (${item.subset})"
cmd: >-
uv run python scripts/sweep.py
--infer-dir data/03_primary/${item.split}
--data-dir data/01_raw/datasets/${item.split}
--output-dir data/08_reporting/sweep/${item.split}/${item.subset}
${item.filter_flag}
deps:
- scripts/sweep.py
- src/tube.py
- src/features.py
- src/classifier.py
- src/types.py
- data/03_primary/${item.split}
- data/01_raw/datasets/${item.split}
outs:
- data/08_reporting/sweep/${item.split}/${item.subset}
Empty file.
26 changes: 26 additions & 0 deletions experiments/temporal-models/temporal-fire-tube/params.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
prepare:
model_repo: "pyronear/yolo11s_mighty-mongoose_v5.1.0"
model_filename: "yolo11s_mighty-mongoose_v5.1.0.pt"

infer:
confidence_threshold: 0.01
iou_nms: 0.2
image_size: 1024

pad:
min_sequence_length: 10

tube:
crop_size: 64
max_tube_length: 50
confidence_threshold: 0.3
max_detection_area: 0.05
iou_threshold: 0.1

features:
method: "tabular"

classifier:
n_estimators: 120
max_depth: 20
random_state: 42
Loading