Türkçe sürüm için tıklayın (README.tr.md)
This project is an experimental computer vision system that investigates the automated positioning of UAV (Unmanned Aerial Vehicle) images on a reference orthophoto map. It combines deep learning-based image transformation with multi-scale template matching, supporting controlled GPS-centric benchmark and sequential visual tracking scenarios as separate operating modes.
Research prototype: The software is intended for academic experiments and method development. It is not a validated product for safety-critical navigation or standalone real flight control. Default parameters are specific to approximately 30 cm/pixel data around Ürgüp; recalibration is required for different regions, altitudes, and sensors.
- Overview
- Quickstart
- Academic Framework
- System Architecture
- Workflow (Pipeline)
- Repository Layout
- Code Map
- Configuration (RUN_CFG)
- Installation
- Dependencies
- Usage
- Technical Details
- Coordinate Transformations
- Evaluation Metrics
- Experiment Protocol and Reproducibility
- Research Tooling
- Visualization
- Output Files
- Camera Support
- Limitations and Notes
- Citation
- License
The system localizes instantaneous images taken by a drone by searching for them on a georeferenced orthophoto map. The basic idea is:
- Yaw (heading angle), GPS coordinates, focal length, and altitude information are read from the EXIF metadata of the drone image.
- Using a Digital Elevation Model (DEM), the terrain elevation is determined and the flight altitude is calculated.
- Using the flight altitude and camera parameters, the Ground Sample Distance (GSD) is calculated.
- The image is aligned to the North according to the yaw angle and scaled according to the GSD.
- Template patches are generated at three different scales (based on center, top-left, and bottom-right elevation values).
- Each patch is processed with a Keras deep learning model to extract a feature map.
- Template matching (Normalized Cross-Correlation) is performed in the relevant region of the reference map.
- The final position is determined from the intersection area of the three matches.
- The error between the estimated position and the actual GPS position is calculated.
Independently of this position channel, an optical-flow speed estimator tracks consecutive frames to report drone ground speed even when no reliable match is available. Optional layers (Kalman filtering, composite quality scoring, sensor fusion, diagnostics) sit on top of the core flow above, each gated by its own RUN_CFG flag — see Configuration.
The repository ships with one sample route (guzergahlar/1_tezde_ucus_5), one map (haritalar/), and one model (model/), so the default RUN_CFG runs out of the box once dependencies are installed:
conda create -n visual_navigation -c conda-forge python=3.11 \
gdal=3.8.4 rasterio=1.3.10 numpy=1.26.4 pandas=2.2.2 \
pyproj=3.6.1 affine=2.4.0 pillow=10.4.0 piexif
conda activate visual_navigation
pip install opencv-python==4.10.0.84 tensorflow==2.16.1
python template_matching_parallel_processing_560_hizli_solust_sagalt_koordinat_fonksiyonlar_icinde_cursor.pyThis opens the konum HUD window, processes every image under RUN_CFG["ANLIK_DIR"] against every map/model pair, and writes results to sonuclar.csv / sonuclar.txt / modele_gore_sonuclar.txt. See Installation for Windows GDAL notes, Configuration to point it at your own data, and Alternative Front-Ends for the Qt and headless entry points.
The fundamental problem is to estimate the position of an aerial image on a georeferenced reference map from visual content and to maintain this estimation stably across sequential frames. The project specifically focuses on these research questions:
- To what extent does GSD, calculated with DEM and camera geometry, reduce the scale difference between the query and the map?
- Can cross-source matching be performed when a learned image transformation is used together with classical normalized cross-correlation?
- Does the geometric intersection of three elevation/scale hypotheses produce a more stable position than a single match?
- Do quality gating, Kalman filtering, and re-acquisition reduce false jumps in sequential frames?
- Under what conditions do separate velocity channels derived from visual position and optical flow provide consistent results?
- Three scale hypotheses generated from center, top-left, and bottom-right DEM samples.
- Transformation of
544 × 544 × 1queries within a single batch using a Keras model. - Pyramid NCC search on CPU or appropriate OpenCV-CUDA setup.
- Optional quality measure combining match score and geometric spread of candidates.
- Confidence-weighted Kalman update, motion gating, and re-acquisition after loss.
- Optical flow velocity estimation based on Lucas–Kanade + RANSAC, independent of localization speed.
- A pure-Python, unit-tested policy layer (
localization_policy.py) that specifies a stricter GPS-denied evaluation contract, plus reproducible-run manifests (experiment_tracking.py). See Experiment Protocol and Reproducibility for its current integration status.
| Mode | Search Center | Appropriate Use | Scientific Boundary |
|---|---|---|---|
BENCHMARK=True |
Fixed window centered on EXIF/GPS at each frame | Controlled comparison of model and matcher components | Not an end-to-end GPS-denied autonomy result; bounds the true position search |
BENCHMARK=False |
Adaptive window tracking the previous visual/filtered position | Sequential tracking and re-acquisition experiment | Separation of initialization, search, and evaluation information must be verified separately |
In a fair GPS-denied experiment, the true position of future frames must not be used for search center, motion prior, recovery, or filter updates. Actual GPS should only be kept as an evaluation label, and USE_GPS_REVERT=False must be set.
+---------------------------------------------------------------------+
| DRONE IMAGES |
| (parcalar/ folder) |
+--------------------------------+------------------------------------+
|
+------------v------------+
| EXIF Reader |
| (yaw, GPS, alt, |
| focal length) |
+------------+------------+
|
+------------------+------------------+
| | |
+---------v--------+ +------v------+ +---------v---------+
| DEM Query | | GSD Calc | | Yaw Correction |
| (Find Elev.) | | (cm/pixel) | | (Align North) |
+---------+--------+ +------+------+ +---------+---------+
| | |
+------------------+------------------+
|
+------------v------------+
| Multi-Scale Patch |
| Generation (3 patches)|
| Top-left / Center / |
| Bottom-right Elev |
+------------+------------+
|
+------------v------------+
| Keras Model |
| (Feature Extraction) |
| Batch predict |
+------------+------------+
|
+------------v------------+
| Template Matching |
| (TM_CCOEFF_NORMED) |
| CPU or CUDA GPU |
| Pyramid search opt. |
+------------+------------+
|
+------------v------------+
| Intersection Analysis |
| (Combine 3 results) |
+------------+------------+
|
+------------------+------------------+
| | |
+---------v--------+ +------v------+ +---------v---------+
| Position Est. | | Error Calc | | Visualization |
| (Lat/Lon) | | (Haversine) | | (OpenCV HUD) |
+------------------+ +-------------+ +-------------------+
The box above still says
parcalar/for historical reasons; the folder the pipeline actually reads is whateverRUN_CFG["ANLIK_DIR"]points to (default:guzergahlar/1_tezde_ucus_5/).parcalar/is now an empty, legacy placeholder — see Repository Layout.
When the script is executed, the following steps are performed sequentially:
- All parameters are read from the
RUN_CFGconfiguration dictionary (plus anyROUTE_PROFILE_OVERRIDESmatching the current route). - Map files (
haritalar/), model files (model/), and instantaneous images (ANLIK_DIR) are listed. - The DEM (Digital Elevation Model) raster file is opened using GDAL.
- The CRS (Coordinate Reference System) information of the reference map is read and a
pixel -> coordinatetransformer is prepared.
For each map-model pair:
- The Keras model (
load_model) is loaded. - The reference map is read in grayscale using OpenCV.
- CRS/transform information of the map is obtained with Rasterio.
For each drone image:
- EXIF data is parsed using the PIL/Pillow library.
- Yaw angle (MakerNote -> FlightDegree), GPS coordinates, altitude, and focal length are extracted.
- The terrain elevation is read from the DEM pixel corresponding to the drone's GPS location.
- Elevation values at the top-left and bottom-right corners are also obtained separately.
Flight Altitude = GPS Altitude - Terrain Elevation + RAKIM_DUZELTMEGSD (cm/px) = (Sensor Width x Flight Altitude x 100) / (Focal Length x Image Width)
- The image is aligned North by rotating it by the inverse of the yaw angle (plus
YAW_OFFSET_DEGcalibration). - The largest internal rectangle is cropped from the rotated image (black corners are removed).
- It is scaled according to the GSD ratio to match the reference map resolution.
PATCH_SIZE(544) pixel patches are cropped at three different scales (based on center, top-left, bottom-right elevation).
- 3 patches are fed to the model as a single batch.
- The preprocessing selected by
MODEL_INPUT_NORMand [-1, 1] normalization are applied. Modes:HAM(raw),HISTEQ(global histogram equalization, default),CLAHE(contrast-limited local equalization),EDGE(Laplacian edge map). The mode can be switched while running with theNkey or the desktop viewer's ÖN İŞLEME drop-down. - The model output is converted to the 0-255 range and border pixels (
PRED_BORDER) are cropped.
- Matching is performed with
cv2.TM_CCOEFF_NORMEDin the search frame section of the reference map. - If a CUDA GPU is available, accelerated matching with
cv2.cuda.createTemplateMatchingis used. - Optional pyramid search: first a coarse search at low resolution, then a fine search in the found region.
- Concurrent matching for 3 templates (
ThreadPoolExecutor).
- The intersection area between the rectangles of the 3 matching results is calculated.
- Pixel coordinates of the intersection center -> geographic coordinate transformation is performed.
- The distance between prediction and actual is calculated using the Haversine formula.
- In normal mode: the search frame for the next frame is narrowed to the region close to the current prediction (optionally centered on the Kalman-filtered position, see
KALMAN_WINDOW_FOLLOWS). - If matching fails, the frame is expanded gradually (
KALMAN_LOST_GROWTH_PX, bounded byCERCEVE_BOYUTU_MAX). - In benchmark mode: a fixed frame centered around the EXIF GPS is used for every frame.
- Regardless of the outcome above, sparse Lucas–Kanade tracking between this frame and the previous one estimates ground speed; see Optical Flow Speed Estimation.
- RMSE, MAE, standard deviation are calculated.
- Precision, Recall, and F-score are calculated.
- Results are written to the
sonuclar.csv,sonuclar.txt, andmodele_gore_sonuclar.txtfiles.
Most binary assets (*.tif, *.h5, drone imagery, sweep/run outputs) and a few newer helper scripts are intentionally git-ignored (see .gitignore) and only exist in a local working copy; the tree below is what you actually work with day to day, not the much smaller set of files tracked in git.
template-matching/
├── template_matching_parallel_processing_560_hizli_solust_sagalt_
│ koordinat_fonksiyonlar_icinde_cursor.py # Main pipeline (run this)
├── gps_denied_autonomy.py # Quality / fusion / diagnostics (imported)
├── optical_flow_speed.py # Optical-flow speed channel (imported)
├── konum_ui_qt.py # PyQt5 control-panel front-end
├── run_localization.py # Reproducible-run CLI (standalone — see Code Map)
├── localization_policy.py # GPS-denied policy rules (standalone — see Code Map)
├── tracking_filter.py # Kalman filter, testable copy (standalone)
├── experiment_tracking.py # Run-manifest / frame-accounting utilities (standalone)
├── requirements.txt
│
├── headless/ # Ekransiz (headless) Linux runner
│ ├── README.md # Setup, systemd service, troubleshooting
│ ├── run_headless.py
│ └── requirements-linux.txt
├── tools/ # Kalman parameter-sweep + analysis scripts
├── tests/ # unittest suite (8 files — see Tests)
│
├── haritalar/ # Reference orthophoto map(s) (.tif) — HARITA_DIR
├── model/ # Keras model(s) (.h5) — MODEL_DIR
├── guzergahlar/ # Drone image routes
│ └── 1_tezde_ucus_5/ # Sample route shipped with the repo — default ANLIK_DIR
├── ana_harita_urgup_30_cm_utm_elevation.tif # Default DEM — DEM_PATH
│ # (additional large DEM/orthophoto GeoTIFFs live at the
│ # project root for the author's own reruns; only DEM_PATH
│ # and the folders above matter for a fresh run)
│
├── results/ # Archived manual/sweep run outputs (results/kalman_sweep/, ...)
├── run_artifacts/ # Reproducible-run manifests + frame CSVs (see below)
├── diagnostics/ # Triptych PNGs, only when DIAGNOSTIC_ENABLED=True
├── sonuclar.csv / sonuclar.txt # Latest run: detailed results
├── modele_gore_sonuclar.txt # Latest run: summary metrics per model
│
├── arşiv/ # Archived snapshot of the main script
├── old/, top_modeller/, anlik/, anlik_t/, # Legacy / alternate data folders (mostly empty
│ parcalar/, temp/ # placeholders today, kept for compatibility)
│
├── README.md / README.tr.md
└── .gitignore
| Module | Role | Status |
|---|---|---|
template_matching_parallel_processing_..._cursor.py |
Main pipeline: EXIF → DEM/GSD → preprocessing → Keras inference → template matching → intersection → Kalman/quality/fusion → visualization → CSV/TXT results. | Active — this is what you run. |
gps_denied_autonomy.py |
Composite localization quality scoring, sensor fusion blending, diagnostic triptych/CSV writers. | Imported by the main script; gated by USE_QUALITY / USE_FUSION / DIAGNOSTIC_ENABLED (all default False). |
optical_flow_speed.py |
Lucas–Kanade + RANSAC optical-flow speed estimator, independent of the localization channel. | Imported by the main script; gated by OPTICAL_FLOW_SPEED_ENABLED (default True). |
konum_ui_qt.py |
PyQt5 control-panel front-end (background QThread worker, live metric cards, toggles); dynamically loads the main script as a module and reuses its core functions. |
Alternative entry point (python konum_ui_qt.py); needs PyQt5 (not in requirements.txt). |
headless/run_headless.py |
Runs the same pipeline with all OpenCV GUI windows suppressed, for headless Linux servers; periodically dumps annotated PNGs instead. | Alternative entry point — see headless/README.md. |
run_localization.py |
CLI running two named, reproducible modes (benchmark, simulation) via RUN_CFG overrides injected through TM_RUN_CFG_JSON. |
Standalone, unit-tested (tests/test_run_localization.py). The main script only reacts to the subset of injected keys it already understands. |
localization_policy.py |
Pure-Python decision rules for a stricter GPS-denied protocol: altitude-source selection, whether GPS ground truth may reach the DEM query or ROI evaluation, adaptive ROI growth, validate_inference_policy guardrails. |
Standalone, unit-tested (tests/test_localization_policy.py). Not imported by the main pipeline yet. |
tracking_filter.py |
ConstantVelocityKalmanFilter — a clean, testable reimplementation of the Kalman logic embedded in the main script. |
Standalone, unit-tested (tests/test_tracking_filter.py); the main script keeps its own inline Kalman implementation. |
experiment_tracking.py |
Reproducible-run manifest builder: asset fingerprints, environment/package snapshot, git revision + dirty flag, per-frame status accounting with coverage/dropout/recovery stats. | Standalone, unit-tested (tests/test_experiment_tracking.py); not imported by the main pipeline yet. |
tools/kalman_sweep.py, tools/analyze_kalman_sweep.py |
Parameter-sweep runner (spawns the main script per route/config) and result summarizer/ranker for Kalman tuning. | Research tooling, run manually — see Research Tooling. |
The "standalone" modules above were ported from a related simulasyon (simulation) project and are unit-tested in isolation; wiring them into the main pipeline is tracked as follow-up work, not something already reflected in benchmark numbers. See Experiment Protocol and Reproducibility for what this means in practice.
All runtime parameters are managed from a single RUN_CFG dictionary near the top of the main script. After this dictionary is read, it is converted into type-safe constants (bool/int/float). A second, smaller dictionary, ROUTE_PROFILE_OVERRIDES, immediately follows it — see Per-Route Profile Overrides.
| Parameter | Default | Description |
|---|---|---|
BENCHMARK |
False |
If True, uses a fixed frame around EXIF GPS center for every frame (adaptive tracking is disabled) |
DEBUG |
False |
If True, intermediate images (patch, model output) are displayed on the screen |
PATCH_SIZE |
544 |
Model input size (pixels) |
PRED_BORDER |
16 |
Border to crop from the model output (pixels) |
MODEL_INPUT_NORM |
"HISTEQ" |
Preprocessing applied to the live image before inference: HAM / HISTEQ / CLAHE / EDGE |
MODEL_INPUT_NORM_BEFORE_RESIZE |
False |
If True, normalization runs before the resize (preserves the native pixel histogram) |
MODEL_INPUT_INTERP |
"NEAREST" |
Resize interpolation for the model input: NEAREST / AREA / LINEAR / CUBIC / AUTO |
MODEL_INPUT_CLAHE_CLIP |
2.0 |
CLAHE clipLimit (higher = more contrast and noise) |
MODEL_INPUT_CLAHE_TILE |
8 |
CLAHE tileGridSize (higher = more local equalization) |
USE_PYRAMID |
True |
Enable pyramid (coarse-to-fine) search |
COARSE_SCALE |
0.5 |
Coarse scale factor for pyramid search |
ROI_PAD_FACTOR |
0.4 |
Expansion coefficient for the fine search region in pyramid search |
PYRAMID_MULTI_CANDIDATE |
False |
If True, the coarse stage keeps the top-K NMS peaks instead of a single peak and refines each one (see Pyramid Search) |
PYRAMID_TOP_K |
5 |
Number of coarse candidates to refine. 1 falls back to the single-candidate path |
PYRAMID_NMS_RADIUS_PX |
0 |
Suppression radius between coarse candidates, in full-resolution pixels. 0 = auto (half the template edge) |
CERCEVE_BOYUTU_NORMAL |
2048 |
Search frame size in normal mode (pixels) |
CERCEVE_BOYUTU_BENCHMARK |
5000 |
Search frame size in benchmark mode (pixels) |
CERCEVE_BOYUTU_MAX |
15000 |
Maximum search-frame size allowed during adaptive growth (pixels) |
FARK_MAX |
200 |
Maximum pixel clamp applied when shifting a patch position |
USE_EXIF_MOTION_SEARCH_PRIOR |
False |
If True, shifts the next search-window center using pixel motion implied by consecutive EXIF GPS fixes, without exposing GPS as the reported output position (a softer prior than USE_GPS_REVERT) |
HARITA_DIR |
"haritalar" |
Folder containing map files |
MODEL_DIR |
"model" |
Folder containing Keras model files |
ANLIK_DIR |
"guzergahlar/1_tezde_ucus_5" |
Folder containing drone images |
DEM_PATH |
"ana_harita_urgup_30_cm_utm_elevation.tif" |
DEM raster file path |
HARITA_DOSYALARI |
[] |
List of specific map files (empty = all in folder) |
MODEL_DOSYALARI |
[] |
List of specific model files (empty = all in folder) |
SORT_INPUTS |
False |
Sort input files alphabetically |
MAX_FRAMES |
0 |
0: all frames; >0: only first N frames (for quick checks) |
| Parameter | Default | Description |
|---|---|---|
DEFAULT_FOCAL_LENGTH_MM |
8.8 |
Default focal length if not in EXIF (mm) |
DEFAULT_SENSOR_WIDTH_MM |
13.2 |
Default sensor width for unknown cameras (mm) |
CAMERA_SENSOR_BY_MODEL |
{"L1D-20c": 13.2, "FC2204": 6.17} |
Per-EXIF-camera-model sensor width overrides (Mavic 2 Pro / Mavic 2 Zoom); unknown models fall back to DEFAULT_SENSOR_WIDTH_MM |
YAW_OFFSET_DEG |
0.0 |
Calibration offset applied to EXIF yaw value (degrees) |
USE_ROUTE_PROFILES |
True |
Apply verified profile differences for known routes — see Per-Route Profile Overrides |
USE_GPS_ALT_REF_SIGN |
False |
Apply GPS altitude reference sign |
RAKIM_DUZELTME |
26 |
DEM datum offset (meters) added when resolving flight altitude — the + Correction term in the GSD formula |
BASARI_ESIGI_KM |
0.07 |
Distance (km) below which a prediction is classified as correct (70 m) |
MAP_RES_CM_PER_PX |
29.85 |
Reference map resolution (cm/pixel). Used for scale conversion and to turn optical-flow pixel displacement into real-world speed |
KENAR_SINIR_PX |
272 |
Positions this close to the map edge are skipped (pixels) |
| Parameter | Default | Description |
|---|---|---|
UI_BUTTONS_ENABLED |
True |
On-screen toggle buttons and mouse callback |
UI_BUTTON_FONT_SCALE |
1.0 |
Button text size |
UI_BUTTON_THICKNESS |
2 |
Button text/frame thickness |
UI_BUTTON_SCALE |
0.5 |
Overall scale of the whole UI panel |
UI_WINDOW_WIDTH |
1000 |
Width of the konum window |
UI_WINDOW_HEIGHT |
1000 |
Height of the konum window |
SHOW_INNER_FRAME |
False |
Inner frame visibility (initial state) |
SHOW_ROI_FRAME |
True |
ROI frame visibility (initial state) |
SHOW_TM_BOXES |
True |
Template matching boxes visibility (initial state) |
DRAW_TRAJECTORY |
True |
If False, the trajectory is never drawn |
TRAJECTORY_DRAW_POINTS |
True |
Also draw a point marker at each step |
TRAJECTORY_MAX_POINTS |
0 |
0: unlimited; >0: keep only the last N points |
TRAJECTORY_LINE_THICKNESS |
15 |
Trajectory line thickness |
TRAJECTORY_POINT_RADIUS |
20 |
Trajectory point marker radius |
| Parameter | Default | Description |
|---|---|---|
OPTICAL_FLOW_SPEED_ENABLED |
True |
Enables the optical-flow speed channel, independent of position estimation |
OPTICAL_FLOW_MAX_DIMENSION |
960 |
Long-edge cap (pixels) for the frame before tracking, to bound processing cost |
OPTICAL_FLOW_MAX_CORNERS |
800 |
Maximum number of Lucas–Kanade corner features to track |
OPTICAL_FLOW_MIN_TRACKS |
20 |
Minimum number of surviving tracks required to trust the estimate |
OPTICAL_FLOW_MIN_INLIER_RATIO |
0.45 |
Minimum RANSAC inlier ratio required to trust the estimate |
OPTICAL_FLOW_RANSAC_THRESHOLD_PX |
2.5 |
RANSAC reprojection threshold (pixels) for the similarity fit |
| Parameter | Default | Description |
|---|---|---|
USE_KALMAN |
True |
Filters position estimation with Kalman; active only in tracking mode when KALMAN_IN_BENCHMARK=False |
KALMAN_PROCESS_NOISE |
30.0 |
Process noise std (px). As it gets larger, it adapts to measurements faster (less smoothing) |
KALMAN_MEASUREMENT_NOISE |
12.0 |
Measurement noise std (px) — see the empirical note in Research Tooling for why this value was chosen |
KALMAN_CONF_GOOD |
1.0 |
3-way intersection confidence (measurement noise is divided by this; 1.0 = full confidence) |
KALMAN_CONF_OK |
0.5 |
2-way intersection confidence (lower -> less trust in measurement) |
KALMAN_WINDOW_FOLLOWS |
True |
True: search frame focuses on (filtered) Kalman position; tracks coasted good position in frames with no intersection -> recovers from outlier clusters |
KALMAN_REACQUIRE_FRAMES |
1 |
Filter is reseeded at this many distant and high-confidence measurements |
KALMAN_REACQUIRE_JUMP_PX |
700 |
If the measurement is this far from the filter, it is considered a candidate for reacquisition |
KALMAN_STEP_GATE_MULT |
8.0 |
Scales the adaptive motion gate with the median step of recent frames |
KALMAN_MAX_STEP_PX |
700 |
Base of the adaptive gate; absolute limit when adaptive is off |
KALMAN_USE_MOTION |
True |
Feeds motion derived from accepted measurements into the prediction |
KALMAN_MOTION_EMA |
0.2 |
EMA smoothing coefficient for motion speed |
KALMAN_MOTION_COAST_DECAY |
0.7 |
Decay rate of motion in coast frames |
KALMAN_LOST_GROWTH_PX |
800 |
Gradual growth step of the search window (px/frame) in case of loss (continuous coast), limited by CERCEVE_BOYUTU_MAX. Not used when KALMAN_COV_GATE is on |
KALMAN_COV_GATE |
False |
Covariance-based principled mode. When on, replaces ad-hoc gates with one derived from filter covariance |
KALMAN_GATE_SIGMA |
3.0 |
(COV mode) Innovation gate sigma |
KALMAN_ROI_SIGMA |
4.0 |
(COV mode) Search window half-width sigma |
KALMAN_COV_MOTION_FRAC |
0.4 |
(COV mode) Reduces q to this multiple of median step when USE_MOTION is on |
KALMAN_GAIN_MAX |
0.35 |
Maximum Kalman gain applied towards the measurement in a single update |
KALMAN_OUTPUT_WARMUP_FRAMES |
0 |
>0: for the first N Kalman frames the internal state warms up, but the reported output is still the raw measurement |
KALMAN_WARMUP_RESEED |
False |
True: during warmup, re-seed the state to the raw measurement every frame |
KALMAN_IN_BENCHMARK |
False |
True: also apply the Kalman output filter while BENCHMARK=True |
KALMAN_RAW_ON_UPDATE |
True |
True: report the raw measurement on accepted updates, and the Kalman output only on coast/outlier frames |
KALMAN_LOST_SCORE |
0.0 |
If >0, TM score below this means "lost" frame. Tried on this dataset and it didn't help — good and bad frames land in the same ~0.15–0.25 score band, so max_val2 isn't a reliable quality signal here. Left at 0; don't re-try without new evidence. |
USE_GPS_REVERT |
False |
Old recovery using true GPS error; must remain off in GPS-denied experiments |
Kalman parameters were tuned against this dataset's multi-route experiments; recalibrate for a different platform, frame rate, or map resolution.
Multi-route results (4 Ürgüp routes, all GPS-denied; RMSE, m):
Route Kalman ON Visual-only (no KF, revert off) OFF (GPS-revert crutch) 1_tezde_5 37 180 59 3_tezde_7 231 329 202 2_tezde_6 90 2926 88 6_tezde_4 295 694 205 Without using GPS, the Kalman filter absorbs coarse mismatch clusters by coasting, and recovers from lock-in via reacquisition. It matches or beats the GPS-crutched OFF baseline on two routes and stays close on the harder ones — without needing GPS.
Broader test (8 routes total): Kalman clearly helps on 5 routes, is neutral on 2 (negligible extra cost on an easy route), and is catastrophic on none. Two routes (
4_tezde_8,guz4_tezde_3) fail intrinsically (~0–50% accuracy): the visual match itself collapses (likely map coverage / model mismatch) — a data problem Kalman cannot fix. See the high-altitude domain-shift note in Limitations and Notes.Provenance note: these numbers are exploratory project records. Do not cite them as a published baseline result without matching them to a full configuration, source commit, data split, frame accounting, and asset fingerprints — see Experiment Protocol and Reproducibility.
Parameters enabling functions from the gps_denied_autonomy.py module. All flags default to False; WHEN OFF, existing behavior (including Kalman) is EXACTLY preserved.
Composite localization quality (USE_QUALITY) -- produces a continuous confidence [0,1] from the three templates.
| Parameter | Default | Description |
|---|---|---|
USE_QUALITY |
False |
True: composite confidence is calculated and fed to the Kalman gate |
QUALITY_SCORE_THRESHOLD |
0.35 |
BASE threshold for normalized score |
QUALITY_CONFIDENCE_THRESHOLD |
0.40 |
Composite confidence threshold |
QUALITY_SPREAD_THRESHOLD_PX |
120.0 |
Threshold for three box center spread (px) |
NO_INTERSECTION_USE_SEARCH_CENTER |
True |
If no intersection exists, fall back to the search-window center instead of a single/weak template box |
LOW_SCORE_USE_SEARCH_CENTER |
0.0 |
>0: if the TM score falls below this threshold, use the search center instead of the intersection |
Sensor fusion (USE_FUSION) -- blends raw measurement with previous output position based on confidence. Only active when Kalman is OFF.
| Parameter | Default | Description |
|---|---|---|
USE_FUSION |
False |
True: raw measurement is blended with the prior output position |
FUSION_BLEND_GAIN |
0.75 |
Blend gain |
FUSION_MAX_JUMP_PX |
600.0 |
Rejected if measurement is further than this * 1.75 from prior |
Diagnostics (DIAGNOSTIC_ENABLED / LOG_QUALITY_CSV) -- writes triptych PNGs and frame-level metrics.
| Parameter | Default | Description |
|---|---|---|
DIAGNOSTIC_ENABLED |
False |
True: frame-based triptych PNG + meta JSON + summary.json |
DIAGNOSTIC_OUTPUT_DIR |
"diagnostics" |
Output root folder |
LOG_QUALITY_CSV |
False |
True: frame-based quality metrics are written to tani_kalite_<timestamp>.csv |
When USE_ROUTE_PROFILES=True, the pipeline looks up the basename of ANLIK_DIR in the ROUTE_PROFILE_OVERRIDES dictionary (defined right after RUN_CFG) and applies any matching per-route RUN_CFG overrides. Today it contains three empirically-derived entries:
| Route | Override | Reason |
|---|---|---|
4_tezde_ucus_8 |
YAW_OFFSET_DEG = -8.5 |
This route's EXIF yaw needed a fixed calibration offset (found via sweep, 2026-06-11). |
3_tezde_ucus_7 |
KALMAN_WINDOW_FOLLOWS = False |
Letting the search window follow the Kalman prediction reduced accuracy on this route; using it as an output-only filter/hold worked better. |
5_tezde_ucus_9 |
KALMAN_WINDOW_FOLLOWS = False |
Same reasoning as above. |
Adding a new route does not automatically get a profile — if it needs a similar calibration fix, add its own entry.
| Parameter | Default | Description |
|---|---|---|
WAIT_PER_MODEL |
False |
Pause after each model |
WAIT_ON_EXIT |
False |
Pause at the end of the program |
LOG_LEVEL |
"WARNING" |
Optional logger level (DEBUG/INFO/WARNING/ERROR). Default is WARNING so it produces no extra output during normal operation |
LOG_TO_FILE |
False |
If True, logs are also written to tm_run.log |
GDAL/rasterio are unreliable to pip install on Windows; conda-forge is far more reliable and is what this project's other tooling (the headless runner) already assumes. Using one shared environment name, visual_navigation, keeps things consistent with headless/README.md and with the test suite:
conda create -n visual_navigation -c conda-forge python=3.11 \
gdal=3.8.4 rasterio=1.3.10 numpy=1.26.4 pandas=2.2.2 \
pyproj=3.6.1 affine=2.4.0 pillow=10.4.0 piexif
conda activate visual_navigation
pip install opencv-python==4.10.0.84 # GUI build; use opencv-python-headless on a headless server
pip install tensorflow==2.16.1 # CPU-only — see step 3 for GPUA plain venv + pip also works if prebuilt GDAL wheels are available for your platform:
python -m venv venv
venv\Scripts\activate # Windows
# source venv/bin/activate # Linux/Mac
pip install -r requirements.txtGDAL installation on Windows might require extra steps. If pip install GDAL fails, use conda:
conda install -c conda-forge gdal rasterioTo use CUDA acceleration:
- Install NVIDIA GPU drivers.
- Compile OpenCV with CUDA support or use the CUDA build of the
opencv-contrib-pythonpackage. - Install the appropriate CUDA Toolkit and cuDNN for TensorFlow GPU support.
- Qt control panel:
pip install PyQt5— required only forkonum_ui_qt.py, not listed inrequirements.txt. - Headless server:
pip install opencv-python-headlessinstead ofopencv-python, and see headless/README.md /headless/requirements-linux.txtfor the full server setup.
- Maps: Place georeferenced orthophoto maps (GeoTIFF) in the
haritalar/folder. - Models: Place trained Keras model files (
.h5) in themodel/folder. - Images: Place drone images (JPEG with EXIF data) in the folder pointed to by
ANLIK_DIR(defaultguzergahlar/1_tezde_ucus_5). - DEM: Place the Digital Elevation Model GeoTIFF file at the path in
DEM_PATH(default: project root).
| Package | Purpose |
|---|---|
tensorflow / keras |
Deep learning model inference |
opencv-python (cv2) |
Image processing, template matching, visualization |
rasterio |
GeoTIFF raster file reading, CRS transformations |
gdal (osgeo) |
DEM (Digital Elevation Model) reading |
numpy |
Numerical calculations |
pandas |
Writing results in table format |
Pillow (PIL) |
EXIF data reading, image size control |
piexif |
EXIF metadata processing |
pyproj |
Coordinate reference system transformations (WGS84 <-> UTM) |
affine |
Affine transformation matrix operations |
concurrent.futures |
Parallel template matching (Python standard library) |
multiprocessing |
Parallel processing support (Python standard library) |
PyQt5 (optional) |
Only for the konum_ui_qt.py control-panel front-end — not in requirements.txt |
python template_matching_parallel_processing_560_hizli_solust_sagalt_koordinat_fonksiyonlar_icinde_cursor.pySet the BENCHMARK value to True in RUN_CFG to enable benchmark mode.
Set the DEBUG value to True in RUN_CFG to display intermediate images on the screen.
- Qt control panel —
python konum_ui_qt.pyruns the same pipeline behind a PyQt5 window with a background worker thread, live metric cards, and the same visibility toggles as the OpenCV HUD. RequiresPyQt5(pip install PyQt5). The desktop viewer now shares the Mission Control design system with the Visual and TERCOM simulators. Start it withbaslat_ui.cmdin the Windowsvisual_navigationenvironment; see desktop UI and shared template for layout, dependencies and validation. - Headless / server mode —
python headless/run_headless.pyruns the identical pipeline with every OpenCV window suppressed, for a Linux server with no display; periodically saves annotated PNG frames instead. See headless/README.md for setup, asystemdservice example, and troubleshooting.
run_localization.py is a CLI wrapper around the main pipeline that locks a run to one of two named, reproducible modes instead of hand-editing RUN_CFG:
python run_localization.py --mode benchmark --max-frames 50
python run_localization.py --mode simulation --max-frames 50 --sha256-assets| Mode | Sets | Intent |
|---|---|---|
benchmark |
BENCHMARK=True, STRICT_GPS_DENIED_INFERENCE=False |
Oracle/legacy comparison — the BENCHMARK=True row in Academic Framework. |
simulation |
BENCHMARK=False, STRICT_GPS_DENIED_INFERENCE=True, ALTITUDE_SOURCE=exif_altitude_proxy |
Intended contract for a stricter GPS-denied evaluation — see below. |
Both modes set WRITE_RUN_ARTIFACTS=True and pass the resolved config through the TM_RUN_CFG_JSON environment variable, which the main script reads and merges into RUN_CFG.
Current status: the main pipeline only reacts to the subset of injected keys it already understands (
BENCHMARK,MAX_FRAMES, …).STRICT_GPS_DENIED_INFERENCE,ALTITUDE_SOURCE, andWRITE_RUN_ARTIFACTSare consumed by this CLI and bylocalization_policy.py's policy functions, but the 4098-line main script does not yet branch on them. Treatrun_localization.pyas a specified, unit-tested contract for the stricter protocol, not as something that changes the reported numbers today — see Experiment Protocol and Reproducibility.
In the konum (position) window, settings can be changed by clicking the on-screen panel buttons or with keyboard shortcuts:
T: Toggle TrajectoryI: Toggle Inner FrameO: Toggle ROI FrameR: Toggle TM BoxesH: Collapse/Expand UI panelN: Cycle the live-image preprocessing mode (HAM->HISTEQ->CLAHE->EDGE); the panel's On Isleme row shows the active mode and cycles when clickedP: Toggle the multi-candidate coarse search (PYRAMID_MULTI_CANDIDATE)K: Cycle the candidate count (1->3->5); the Aday K row shows the active value and lights up wheneverK > 1
Unlike the other keys, P and K change search behaviour, not just what is drawn. Both take
effect from the next frame: the pipeline mirrors them into RUN_CFG each frame
(apply_pyramid_ui_state), and match_three reads the settings at call time.
In the desktop viewer (konum_ui_qt.py) the same modes are available from the ÖN İŞLEME
drop-down in the control group; the change takes effect from the next frame. Headless runs
take --model-norm HAM|HISTEQ|CLAHE|EDGE. The viewer's ARAMA row carries the
Çok adaylı arama toggle and the Aday sayısı (K) drop-down, which map to the same two
settings.
The script has an optional logging infrastructure (tm logger). Default level is WARNING. Configure via RUN_CFG: LOG_LEVEL and LOG_TO_FILE.
python -m unittest discover -s tests -vEight suites, split by dependency weight:
| Needs only the standard library | Needs numpy / opencv / the main module |
|---|---|
test_quality.py (imports gps_denied_autonomy) |
test_core_functions.py (loads the main script) |
test_experiment_tracking.py |
test_kalman.py (loads the main script) |
test_localization_policy.py |
test_optical_flow_speed.py (cv2, numpy) |
test_run_localization.py |
test_tracking_filter.py (numpy) |
.github/workflows/unit-tests.yml runs only the left column, on a bare actions/setup-python runner with no install step — none of those four import a third-party package. The right column needs the full visual_navigation environment from Installation: those tests load the main script dynamically via importlib.util and are wrapped in unittest.skipIf, so a missing dependency (cv2/osgeo/tensorflow) skips them cleanly instead of failing.
The parse_exif() function reads EXIF data using the PIL library. It extracts Yaw, GPS, Altitude, Focal Length, Camera Model, and Timestamp.
To account for uneven terrain, patches are generated at three different scales corresponding to the Center, Top-Left, and Bottom-Right elevations derived from the DEM.
- Yaw Correction: Aligns the image North.
- Crop Inner Rectangle: Removes black corners created during rotation.
- Scaling: Scales to match reference map resolution based on GSD.
- Patch Extraction: Extracts 544x544 pixel patches.
Converts the patches to grayscale, applies the selected preprocessing (MODEL_INPUT_NORM:
raw / histogram equalization / CLAHE / Laplacian edges), normalizes to [-1, 1], and passes them
through the Keras model in a single batch to extract feature maps.
Uses cv2.TM_CCOEFF_NORMED to find the highest correlation point on the reference map.
Calculates the intersection of the matching rectangles from the 3 templates. The center of this intersection area is accepted as the final position estimate.
When USE_PYRAMID = True, a two-stage search (coarse-to-fine) is used to speed up the process on large maps. The coarse stage matches at COARSE_SCALE, and the full-resolution stage only searches a ROI_PAD_FACTOR-sized window around the coarse seed.
Multi-candidate coarse stage (PYRAMID_MULTI_CANDIDATE, default off). By default the coarse stage keeps a single peak, so a wrong coarse seed can never be recovered — the true match stays buried under the -1e9 fill outside the refinement window. This is a plausible silent failure mode over self-similar terrain (valley and fairy-chimney texture repeats across Cappadocia). With the flag on, the coarse response is reduced by greedy square-NMS to the PYRAMID_TOP_K strongest, mutually separated peaks (PYRAMID_NMS_RADIUS_PX), each is refined at full resolution, and the results are merged into one response map — so minMaxLoc picks the winner across candidates. The returned map keeps the exact shape and fill convention of the single-candidate path, so nothing downstream changes. The NMS core mirrors coarse_to_fine_search in the sibling benchmark/ project; the implementation lives in pyramid_search.py with unit tests in tests/test_pyramid_search.py.
Measured cost on synthetic frames (CPU, TM_CCOEFF_NORMED, 512 px template):
| Search frame | Exhaustive | Pyramid K=1 | Pyramid K=3 | Pyramid K=5 |
|---|---|---|---|---|
| 2048 px | 59 ms | 28 ms | 45 ms | 59 ms |
| 6000 px | 730 ms | 168 ms | — | 247 ms |
At the default 2048 px frame, K=5 costs about as much as an exhaustive search, so K=3 is the better trade there; the multi-candidate path pays off most on adaptively grown frames (CERCEVE_BOYUTU_MAX = 15000). Note the flag only affects the CPU path — the CUDA branch performs a full search and ignores USE_PYRAMID entirely.
The system automatically checks for CUDA GPU presence and transparently uses it for Template Matching, Image Resizing, and Image Rotation if available.
Independently of the position pipeline, optical_flow_speed.py tracks sparse Lucas–Kanade features between consecutive drone frames, filters them with a RANSAC similarity fit, and converts the inlier median displacement into a real-world speed using MAP_RES_CM_PER_PX and the EXIF timestamp delta. Enabled by default (OPTICAL_FLOW_SPEED_ENABLED); tunable via the OPTICAL_FLOW_* parameters (frame downscale, corner count, minimum tracks/inlier ratio, RANSAC threshold).
When USE_KALMAN=True, the raw intersection center is smoothed using a Constant-Position Kalman filter (state = (x, y) only, in map-pixel space) instead of the more common constant-velocity design. This was a deliberate choice, not the default: an earlier constant-velocity model plus innovation gating plus window-following diverged on this dataset — the filter would coast in the wrong direction, drag the search window with it, corrupt subsequent measurements, and feed the error back into itself (route-wide RMSE went from 59 m to 104 m). The constant-position model never extrapolates a velocity forward — every valid measurement pulls it back — so it cannot run away and lock onto the wrong region the same way. It handles noisy measurements and incorporates an adaptive window, motion predictions, and coasting, all without requiring GPS data. See the empirical results table in Configuration (RUN_CFG) for route-level numbers.
| Transformation | Function | Description |
|---|---|---|
| WGS84 -> Pixel | piksel_bul() / piksel_bul_fast() |
Converts a GPS coordinate to a map pixel position |
| Pixel -> WGS84 | koordinat_bul() / make_rc_to_ll() |
Converts a pixel position to a geographic coordinate |
| WGS84 -> UTM | latlon_to_utm() |
Converts latitude/longitude to a UTM coordinate |
| Haversine | haversine_distance() |
Great-circle distance between two geographic coordinates |
| Quick Distance | quick_distance() |
Approximate distance (fast path) |
| Quick Distance UTM | quick_distance_utm() |
UTM-based distance calculation |
CRS transforms: pyproj.Transformer converts between EPSG:4326 (WGS84) and the map's/DEM's local CRS.
The main run reports the following to sonuclar.csv / sonuclar.txt / modele_gore_sonuclar.txt:
Distance metrics — RMSE (root mean square error), MAE (mean absolute error), and standard deviation of the error distribution.
Classification metrics (threshold: 70 m) — a prediction is scored against whether it falls within BASARI_ESIGI_KM and whether a template intersection was found at all:
- True Positive (TP) — correct position found and an intersection exists
- False Positive (FP) — wrong position found but an intersection exists
- True Negative (TN) — wrong position and no intersection
- False Negative (FN) — correct position but no intersection
Derived metrics — Precision = TP / (TP + FP); Recall = TP / (TP + FN); F-score = 2 × (Precision × Recall) / (Precision + Recall); accuracy % = correct predictions / total × 100.
When run through run_localization.py, experiment_tracking.FrameStatusRecorder.summary() additionally derives coverage (accepted / attempted), p50/p95/max/MAE/RMSE over only the accepted frames, and dropout statistics (longest rejected streak, recovery-event count and length) — see Experiment Protocol and Reproducibility.
For academic runs, a single accuracy or RMSE number is not enough — the same route can look very different depending on configuration, code version, and which frames were even attempted. Two layers exist for this.
Available today, always applicable:
- Preserve the git commit ID and working-tree cleanliness alongside every reported number.
- Record the resolved
RUN_CFG(after any route-profile overrides) and the installed package versions. experiment_tracking.pyprovides the building blocks for this:environment_snapshot(),git_revision()/git_is_dirty(),file_fingerprint()(metadata or SHA-256 asset hashing), andbuild_run_manifest(), plus aFrameStatusRecorderthat records exactly one terminal status per frame (accepted/rejected_hold/skipped/failed) and derives coverage, dropout-streak, and recovery-time statistics from it. It has no image/GIS dependencies, so a manifest can still be written even if the main run crashes partway through.run_artifacts/<run_id>/(local, git-ignored) already holds examples of this output:run_manifest.json(schema, environment, asset fingerprints, resolved config) andframe_status_m0.csv/frame_summary_m0.jsonper model.
Specified and unit-tested, not yet wired into the main pipeline (see Code Map):
run_localization.py --mode simulationis meant to enforce a strict GPS-denied protocol: no true GPS position for the search center, motion prior, DEM query, or recovery aftert=0; only altitude is still trusted, and only through an explicit, declared source (select_inference_altitude()inlocalization_policy.py:initial_hold,exif_altitude_proxy, orexternal_csv— never a silent fallback to the GPS fix).localization_policy.validate_inference_policy()raises if an incompatible combination is requested (e.g.USE_EXIF_MOTION_SEARCH_PRIORorUSE_GPS_REVERTturned on inside strict mode).- These rules are covered by
tests/test_localization_policy.pyandtests/test_run_localization.py, but the main script does not currently importlocalization_policyor branch onSTRICT_GPS_DENIED_INFERENCE. Until that integration lands, onlyBENCHMARK=False(the existing adaptive-tracking mode) reflects real, running GPS-denied behavior — treatsimulationmode as a documented target, not a currently-enforced guarantee.
When citing numbers from this project (in a thesis, paper, or report), state explicitly which of the two you mean.
tools/kalman_sweep.py runs the main script as a subprocess once per (route × config) combination — overriding RUN_CFG the same way run_localization.py does, via TM_RUN_CFG_JSON — and collects each run's results under results/kalman_sweep/<run_id>/. tools/analyze_kalman_sweep.py then aggregates those CSVs and ranks candidate parameter sets against a baseline (off_visual_no_revert).
Empirical note from a 2026-06-07 multi-route sweep: switching the Kalman output to "output-only smoothing" reduced mean RMSE from 1073 m to 585 m across the 11 routes that produced valid results in that sweep. This is what motivated the current
KALMAN_MEASUREMENT_NOISEdefault (see the inline comment above that key inRUN_CFG). Treat this as a local tuning result, not a controlled thesis experiment — re-run the sweep before citing it.
Multiple OpenCV windows are used.
- Zoomed-in view of the reference map
- Red box: Template 1 (top-left scale) match position
- Green box: Template 2 (center scale) match position
- Blue box: Template 3 (bottom-right scale) match position
- Black box: search frame bounds
- Orange box: ROI frame (toggle)
- Yellow dot: predicted position
- Green dot: true GPS position
- Aircraft icon: true position and heading
- Yellow trail / Green trail: predicted/true trajectory (toggle)
- Light-blue arrow: computed speed vector
- HDG: yaw/heading angle (degrees)
- ALT: flight altitude (meters)
- ERR: position error (meters)
- SPD: speed (
m/sandkm/h) - Scale bar: 100-meter reference bar
- Crosshair: screen center
The Qt front-end (konum_ui_qt.py) renders the same information inside a PyQt5 window with additional live metric cards.
Note: the speed vector is computed and drawn as an arrow on the map; its components are not broken out separately in the HUD text.
- Top: cropped and rotated drone image (color)
- Bottom: Keras model output (grayscale)
sonuclar.csv/sonuclar.txt— latest run, detailed per-frame results.modele_gore_sonuclar.txt— latest run, summary metrics per model.run_artifacts/<run_id>/(git-ignored) —run_manifest.jsonplus per-modelframe_status_m*.csv/frame_summary_m*.json, written whenWRITE_RUN_ARTIFACTSis set (currently only viarun_localization.py).diagnostics/diag_<timestamp>_m<model_index>/(git-ignored) — per-frame triptych PNGs (crop | model output | matched reference region) + meta JSON, plus a run-endsummary.json, whenDIAGNOSTIC_ENABLED=True.tani_kalite_<timestamp>.csv— per-frame quality metrics whenLOG_QUALITY_CSV=True.results/(git-ignored) — archived manual/sweep runs, includingresults/kalman_sweep/.tm_run.log— optional logger output file whenLOG_TO_FILE=True.
Per-EXIF-camera-model sensor widths are declared in CAMERA_SENSOR_BY_MODEL: L1D-20c → 13.2 mm (DJI Mavic 2 Pro), FC2204 → 6.17 mm (DJI Mavic 2 Zoom). Any other EXIF camera model falls back to DEFAULT_SENSOR_WIDTH_MM (13.2 mm); DEFAULT_FOCAL_LENGTH_MM (8.8 mm) is used when EXIF has no focal length at all.
- Map and model counts in their respective folders must match (one Keras model is paired with one reference map per iteration).
- Images must contain GPS, altitude, and focal length in EXIF (MakerNote
FlightDegreefor yaw). - Query positions must fall within the DEM extent.
- Large maps/DEMs (multi-GB GeoTIFFs) require significant RAM.
- A 70 m threshold (
BASARI_ESIGI_KM=0.07) is used to classify a prediction as correct in the aggregate metrics. - Known high-altitude failure mode: routes flown at roughly ≥1000 m AGL (e.g.
4_tezde_ucus_8) currently perform poorly. This has been characterized as a model domain-shift issue — the Keras feature-extraction model was not trained on imagery at that altitude band — rather than a search-coverage or scale-calculation bug; recalibratingYAW_OFFSET_DEG(see Per-Route Profile Overrides) does not fix it. Retraining/fine-tuning on higher-altitude imagery is the likely fix, not aRUN_CFGchange. ROUTE_PROFILE_OVERRIDEScurrently hard-codes calibration fixes for three specific routes; a new route may need its own entry.- Default parameters are tuned for ~30 cm/px imagery around Ürgüp; expect to recalibrate
MAP_RES_CM_PER_PX,DEFAULT_SENSOR_WIDTH_MM/CAMERA_SENSOR_BY_MODEL, andRAKIM_DUZELTMEfor a different sensor, region, or altitude band. - See Experiment Protocol and Reproducibility for the gap between the currently-running GPS-denied behavior and the stricter, specified-but-not-yet-wired
simulationprotocol.
There is no published CITATION.cff or DOI for this repository yet. When the relevant thesis/article is published, citation should include author, title, institution, year, version, and permalink.
This project is dual-licensed under the MIT License and Apache License 2.0.