A fork from https://github.com/algorithmicgovernance/TentNetFA
This project processes high-resolution Planet satellite images of the Gaza Strip in combination with historic tent locations identified by Forensic Architecture — a multidisciplinary research group based at Goldsmiths, University of London.
The goal of this work is to develop a convolutional neural network (CNN) that can predict, at the pixel level, the locations of tents in the Gaza Strip from satellite imagery. These predictions use Gaussian densities to create highly granular maps of displacement patterns over time.
This automated detection supports population nowcasting in the Gaza strip in collaboration with the United Nations, Acted, IMPACT, and other partners integrating privacy-preserving telecommunications data and other geospatial data sources related to real-time population movements (see Gaza NowPop).
- Data Ingestion: Processes Planet GeoTIFF satellite images and GeoJSON files containing labeled tent locations.
- Data Processing:
- Scans satellite imagery based on geographic coordinates to extract image tiles.
- Generates paired datasets of image patches and corresponding label masks indicating tent locations.
- Creates HDF5 datasets for efficient handling during training.
- Model Training: Trains a custom CNN (
SimpleCNN) for pixel-wise semantic segmentation to predict tent presence as a density map. - Prediction & Evaluation:
- Generates GeoJSON point clouds of predicted tent locations from new satellite imagery.
- Includes tools for evaluating prediction accuracy against ground truth data and for performing spatial validation analysis.
This project uses Poetry for dependency management. Ensure you have Python 3.10+ installed.
-
Clone the repository:
git clone https://github.com/realgoodresearch/TentNetFA.git cd TentNetFA -
Install dependencies using Poetry:
poetry install
Alternatively, you can install from the
requirements.txtfile, although this is not the recommended method:pip install -r requirements.txt
-
Set up environment variables: Create a
.envfile in the project root to store necessary credentials, such as yourGOOGLE_API_KEYand the Google Drive folder ID (GDRIVE_ID) for downloading satellite imagery.GOOGLE_API_KEY="your_api_key_here" GDRIVE_ID="your_folder_id_here" DATA_DIR="/path/to/your/data"
If you modify dependencies in pyproject.toml, you must regenerate the requirements.txt file:
poetry export -f requirements.txt --output requirements.txt --without-hasheson a regular basis.
The individual stages below can also be run end-to-end through the pipeline runner, which handles config resolution and artifact layout for you. Every pipeline run creates a self-contained directory with fixed subfolder names:
<run root>/<pipeline>/<run name>/
config.yaml # fully resolved config used by all stages
logs/ # one log file per stage
manifests/ preds/ merged/ # prediction pipeline
manifests/ dataset/ model/ # training pipeline
The run root defaults to ${DATA_DIR}/results/TentNetFA/pipeline_runs; the run name defaults to a timestamp.
Both pipelines include an optional (default-off) download stage that fetches newly arrived GeoTIFFs from Google Drive into geotiff_dir before scanning, using the loading.files entries as search strings (requires GOOGLE_API_KEY and GDRIVE_ID in .env).
Training pipeline (config.yaml):
flowchart TB
drive[("Google Drive")]
ann["Tent annotations<br/>(geojson)"]
prewar["Pre-war raster<br/>(prewar_gaza)"]
tifs["GeoTIFFs<br/>(geotiff_dir)"]
scan["<b>scan</b> — b1_annotated_scanner<br/>tiles imagery inside boundaries, pairs it with<br/>pre-war tiles and rasterised tent-label masks"]
rebalance["<b>rebalance</b> — c_resample_manifest<br/>merges manifests, downsamples empty tiles<br/>(null_keep_fraction)"]
train["<b>train</b> — d_train_cnn<br/>trains SimpleCNN on (image, pre-war, diff)<br/>stacks vs. label density maps"]
model(["model/<timestamp>/best_model.pth"])
drive -. "download (optional)<br/>a_tif_loader" .-> tifs
tifs --> scan
ann --> scan
prewar --> scan
scan -- "manifests/*.parquet" --> rebalance
rebalance -- "dataset/balanced.parquet" --> train
train --> model
Prediction pipeline (predict_config.yaml):
flowchart TB
drive[("Google Drive")]
tifs["GeoTIFFs<br/>(geotiff_dir)"]
prewar["Pre-war raster<br/>(prewar_gaza)"]
ckpt["Trained checkpoint<br/>(prediction.model)"]
scan["<b>scan</b> — b2_image_scanner<br/>tiles new imagery inside boundaries,<br/>pairs it with pre-war tiles (no labels needed)"]
predict["<b>predict</b> — e_predict_json<br/>runs inference per tile, extracts tent points<br/>(NMS / centroids) above selection thresholds"]
merge["<b>merge</b> — h_merge_geojsons<br/>filters by adjusted peak, drops excluded zones,<br/>merges points within merge.min_distance_m"]
out(["merged/merged.gpkg"])
drive -. "download (optional)<br/>a_tif_loader" .-> tifs
tifs --> scan
prewar --> scan
scan -- "manifests/*.parquet" --> predict
ckpt --> predict
predict -- "preds/*.geojson<br/>(overlapping tiles!)" --> merge
merge --> out
The same diagrams, together with a full reference of every config key, are available in the UI's Help tab (sourced from displacement_tracker/pipelines/help.md).
poetry install --with ui # installs streamlit
poetry run pipeline-uiThis starts a local web server and opens a browser session where you pick the pipeline (training or prediction), override any parameter from config.yaml / predict_config.yaml in a form (plus a free-form YAML box for anything not exposed), toggle individual stages, and watch live logs while the run executes.
A run is tied to its browser session: switching pipeline, refreshing or closing the page cancels the running stage and terminates all of its child processes. Completed artifacts and per-stage logs remain in the run directory, so you can resume by re-running with only the remaining stages enabled. For long unattended runs prefer the headless CLI below inside tmux/screen.
Extra arguments are passed through to streamlit run (e.g. --server.port 8501).
Pipelines will typically run on a remote GPU machine. The UI is just a web server on that machine, so start it there and forward the port to your local browser:
# on the remote, inside the repo (tmux/screen recommended: the pipeline
# stages are children of the UI process and die with your SSH session)
poetry run pipeline-ui --remote
# it prints the matching tunnel command to run on your local machine, e.g.
# ssh -L 8501:localhost:8501 user@remote
# then open http://localhost:8501 locally--remote is shorthand for --server.headless true --server.address localhost: no browser is opened on the remote, and the UI is reachable only through the tunnel — recommended, since the UI has no authentication and can launch jobs and write to DATA_DIR. Individual flags can still be overridden (e.g. --remote --server.port 8600), and whenever an SSH session is detected the tunnel command is printed even without --remote. On a trusted LAN you can drop --remote and use the "Network URL" streamlit prints instead of a tunnel, but never expose the port to untrusted networks.
After pulling new code (or when a port is stuck), stop any stale backends before restarting:
poetry run pipeline-ui-stop # add --dry-run to only list themThis gracefully terminates every running pipeline-ui server including the pipeline stages it spawned, plus stage processes orphaned by an earlier hard kill. Deliberate headless pipeline-run sessions are left untouched.
The same orchestration is scriptable without a browser:
poetry run pipeline-run predict --set prediction.batch_size=16 --name 2026-02-rerun
poetry run pipeline-run train --skip download --set training.epochs=500
poetry run pipeline-run predict --dry-run # print the plan without executing--set takes dotted config paths (section.key=value, YAML-parsed); --only/--skip select stages. Pipeline definitions (stages, exposed parameters, artifact layout) live in displacement_tracker/pipelines/spec.py.
The core workflow is managed through a series of command-line scripts. Most scripts require a config.yaml file to specify paths and parameters.
Download GeoTIFF files from Google Drive based on the filenames specified in your configuration file.
poetry run tif-loader config.yamlScan the downloaded GeoTIFFs and corresponding GeoJSON labels to create an HDF5 dataset for training.
poetry run coordinate-scanner config.yamlTrain the CNN using the generated HDF5 dataset.
poetry run train-cnn config.yamlModel checkpoints and training logs will be saved to a timestamped directory inside runs/.
Use a trained model to predict tent locations on new satellite images. This script typically uses a separate predict_config.yaml.
poetry run predict-json predict_config.yamlThis will generate GeoJSON files containing the coordinates of predicted tents.
The repository includes several scripts for analyzing the results:
evaluate-geojson: Compare a prediction GeoJSON against a ground truth GeoJSON to compute metrics like precision, recall, and F1-score.validate-geojson: Perform spatial validation by comparing rasterized prediction counts against validation counts on a master grid.merge-geojsons: Merge multiple prediction GeoJSONs into a single, deduplicated GeoPackage file.
The pipeline is primarily configured via YAML files (config.yaml, predict_config.yaml). Below is a typical structure for config.yaml.
# --- Paths ---
geotiff_dir: ${DATA_DIR}/results/TentNetFA/2026-02/tiffs
boundaries: gaza_boundaries/GazaStrip_MunicipalBoundaries.shp
prewar_gaza: ${DATA_DIR}/results/TentNetFA/2026-02/prewar_gaza.tif
geojson: ${DATA_DIR}/results/TentNetFA/2026-02
hdf5: train_data_labelling_balanced.h5
artifact_dir: runs
# --- Data Loading ---
loading:
files:
- deir_el_balah_20250315_121122_ssc10_u0002_visual_clip_file_format.tif
- khan_yunis_20250315_065509_ssc13_u0001_visual_file_format.tif
processing:
individual: false # when true, ignore hdf5 and write one HDF5 per TIFF into hdf5_folder
step: 0.0005 # step size for each tile in degress lat and long
quality_thresholds:
min_valid_fraction: 0.9 # minimum fraction of the image that needs to be not black / NaN
training:
checkpoint: null # checkpoint to restart from, e.g. path/to/model.pth
epochs: 10000
batch_size: 8
learning_rate: 0.0005
training_frac: 0.7
validation_frac: 0.15
# --- Data Processing ---
processing:
prediction_only: false
step: 0.0005 # Step size for tiling in degrees latitude/longitude
quality_thresholds:
start_threshold: 0.2
max_missing_end: 1.0
min_valid_fraction: 0.9
complete: # List of TIFFs to process entirely, ignoring quality gates
- deir_el_balah_20250315_121122_ssc10_u0002_visual_clip_file_format.tif
# --- Training ---
training:
checkpoint: v1.0.pth # Optional path to a model checkpoint to resume training
device: cuda
epochs: 10000
batch_size: 32
learning_rate: 0.005
training_frac: 0.4
validation_frac: 0.1
memory: True # Cache dataset in RAM for faster training
model_kwargs:
kernel_size: 3Running predictions on new satellite imagery consists of three stages:
- Process GeoTIFF imagery and generate manifests
- Run model inference
- Merge prediction outputs
Example Collab Notebook to get started.
Before running predictions, ensure the following files are available locally:
- GeoTIFF files to be processed
- Gaza Strip boundaries folder including
GazaStrip_MunicipalBoundaries.shp prewar_gaza.tif
The GeoTIFF files can be stored in any folder structure, provided the correct paths are specified in the configuration files.
-
Download or copy the GeoTIFF files you want to run predictions on.
-
Store them anywhere within or outside the repository.
-
Update
config.yamlwith:geotiff_dir: directory containing the GeoTIFF filesmanifest_folder: directory where manifests should be writtenboundaries: path toGazaStrip_MunicipalBoundaries.shpprewar_gaza: path toprewar_gaza.tif
Run:
poetry run python -m displacement_tracker.b2_image_scanner config.yamlThis command scans all GeoTIFFs in geotiff_dir and generates manifests for files that do not already have a corresponding manifest in manifest_folder.
Update predict_config.yaml with the correct paths:
geotiff_dirinput_folderoutput_folder
Recommended selection parameters:
selection:
threshold: 0.0001
factor: 1.0
nms_kernel_size: 7
min_distance_m: 3.0Run:
poetry run python -m displacement_tracker.e_predict_json predict_config.yamlThis command runs inference on all imagery that has a manifest available in the configured manifest folder.
Output files are written as GeoJSON/JSON prediction files in the configured output directory.
Predictions are generated as overlapping tiles and must be merged before analysis.
Run:
poetry run -m displacement_tracker.h_merge_geojsons tif_files/historic/predictions merged.gpkg \
--min-adj-peak 0.003 \
--adjustment-factor 10This deduplicates overlapping predictions and produces a consolidated output. Note: This will merge everything in the input folder into a single gpkg file. Only do this if the predictions in the input folder are intended to be merged and dedeuplicated into one file. To merge by date instead, see below.
To merge predictions separately for each acquisition date, add:
--process-by-dateExample:
poetry run -m displacement_tracker.h_merge_geojsons tif_files/historic/predictions ignored.gpkg \
--process-by-date \
--min-adj-peak 0.003 \
--adjustment-factor 10With --process-by-date, outputs are grouped and merged independently for each date.
Without --process-by-date, all predictions in the input directory are merged into a single combined output containing all detected tents.
- HDF5 Datasets: The
coordinate-scannerscript produces HDF5 files containingfeature,prewar,label, andmetadatasets for training and prediction. - Model Checkpoints: The training script saves the best-performing model (
best_model.pth) and dataset split information in theruns/<timestamp>/directory. - Prediction GeoJSONs: The prediction script generates GeoJSON files with point coordinates for each detected tent, including a
peak_valueproperty. - Evaluation Reports: Validation and evaluation scripts produce CSV reports and difference rasters summarizing model performance.
- Developed in collaboration with Forensic Architecture, Goldsmiths, University of London.
- Satellite data provided by Planet Labs.
If you have any questions or want to contribute, please open an issue or submit a pull request.