This project provides a proof-of-concept Raspberry Pi Visual Positioning System (VPS) that generates simple non-GNSS geolocation through LoFTR matching a query image against offline Google Maps tiles. It is based on the drone_photo_geolocate pipeline but adapted for offline operation on a Pi. The project provides a simple LoRa communication interface for the sending and recieving of commands between transmit and recieve nodes.
Please make sure to read Gotchas and Troubleshooting.
- Hardware
- Quickstart (Fresh Install)
- Operating Defaults
- First End-to-End Test
- Expected Output
- Run Artifacts
- Coordinate Fields
- Receiver Logs
- Manual Pipeline Run (No LoRa)
- Gotchas and Troubleshooting
transmitnode:- Raspberry Pi 4
- Waveshare SX1268 433M LoRa HAT
recievenode:- Raspberry Pi 4
- Waveshare SX1268 433M LoRa HAT
- Raspberry Pi Camera Module v1.3
- Adafruit ICM-20948 IMU
- A small power bank
- Airframe:
- any UAS that can carry the receiver payload (my DJI Mini 4K struggled)
Use this on recieve after cloning:
cd <REPO_ROOT>
python3 -m venv .venv_loftr
source .venv_loftr/bin/activate
python -m pip install --upgrade pip setuptools wheel
pip install --index-url https://download.pytorch.org/whl/cpu torch==2.11.0+cpu torchvision==0.26.0+cpu
pip install -r requirements.txtSanity check:
python -c "import torch, torchvision; print(torch.__version__, torchvision.__version__)"Expected:
2.11.0+cpu0.26.0+cpu
Important:
- do not mix apt/system torch with this venv
- service/runtime must use the same
.venv_loftr
Edit service template and replace <REPO_ROOT> with your actual clone path.
File:
services/lora-camera-rx.service
Install/restart:
cd <REPO_ROOT>
sudo cp services/lora-camera-rx.service /etc/systemd/system/lora-camera-rx.service
sudo systemctl daemon-reload
sudo systemctl enable lora-camera-rx
sudo systemctl restart lora-camera-rxVerify:
sudo systemctl status lora-camera-rx --no-pager
sudo journalctl -u lora-camera-rx -fThe transmit node currently uses a one-shot command sender, not a long-running service. A minimal setup looks like this:
cd <REPO_ROOT>
python3 -m venv .venv_tx
source .venv_tx/bin/activate
python -m pip install --upgrade pip setuptools wheel
pip install pyserial RPi.GPIOThen use commands directly:
cd <REPO_ROOT>
python3 lora/transmitter/send_lora_command.py "SET_POS <LAT> <LON> [ALT_M]"
python3 lora/transmitter/send_lora_command.py "TAKE_PICTURE_IMU"
python3 lora/transmitter/send_lora_command.py "GEOLOCATE_NOW 20"Run this while online before flight:
cd <REPO_ROOT>
source .venv_loftr/bin/activate
PYTHONPATH=src python -m photo_geolocate.run_prepare_offline_tiles \
--config configs/search_area.current.yaml \
--center-lat <LAT> --center-lon <LON> \
--radius-m <RADIUS_M> \
--zoom 20 \
--out-dir data/archive/offline_tilesetNotes:
- set your initial location first in
configs/search_area.current.yaml:prior_location.latitudeprior_location.longitude
- alternatively, pass
--center-latand--center-lonon the command line (overrides config) - writes
index.json+ tile images todata/archive/offline_tileset - if center/radius omitted, defaults come from config
- this pre-flight center is for tile archive generation only (not IMU runtime init)
Current runtime defaults used in this project:
- offline tile source only:
--offline-only --tiles-dir data/archive/offline_tileset - shortlist mode:
--shortlist-k all(0) so all spatially filtered tiles get LoFTR - retrieval is still computed/logged, but not trusted for candidate pruning
From the transmit node, send the following commands. I'd recommend keeping the radius as small as possible, about 50m:
python3 lora/transmitter/send_lora_command.py "SET_POS <LAT> <LON> [ALT_M]"
python3 lora/transmitter/send_lora_command.py "TAKE_PICTURE_IMU"
python3 lora/transmitter/send_lora_command.py "GEOLOCATE_NOW <RADIUS>"After GEOLOCATE_NOW, inspect the latest run in:
data/archive/mission_runs/
| Path | What it contains | What to look at first |
|---|---|---|
winner.json |
Final selected tile + run metadata | winner.estimated_query_center_latitude, winner.estimated_query_center_longitude, winner.score |
tile_match_summary.json |
Ranked candidate tiles | Top few entries and their score |
interactive_diagnostics.json |
Per-tile matching diagnostics | branches and patch diagnostics for debugging |
match_visualizations/ |
Top rank match overlay images | Quick visual sanity check |
patch_visualizations/ |
Patch-level match overlays | Patch behavior / failure modes |
winner.center_latitude/winner.center_longitude: center of the winning tile.winner.estimated_query_center_latitude/winner.estimated_query_center_longitude: refined query-center estimate from LoFTR homography on the winning tile.
Use:
sudo journalctl -u lora-camera-rx -fYou should see:
- LoRa command receipt (
SET_POS,TAKE_PICTURE_IMU,GEOLOCATE_NOW) - periodic IMU position updates
- camera capture success and image save path
cd <REPO_ROOT>
source .venv_loftr/bin/activate
PYTHONPATH=src python -m photo_geolocate.run_pipeline \
--config configs/search_area.current.yaml \
--query-image /path/to/query.jpg \
--center-lat <LAT> --center-lon <LON> \
--radius-m 50 --zoom 20 \
--shortlist-k all \
--offline-only --tiles-dir data/archive/offline_tileset \
--fixed-out-dir --out-dir data/archive/mission_runs/manual_testUseful flags:
--skip-match-visualizationsfor faster Pi CPU runs--no-offline-spatial-filterif radius filtering removes all candidates
- The Pi is slow to run this, so be patient. For a functional VPS, GPU compute is necessary.
- If LoFTR hangs/crashes, confirm torch/torchvision versions in
.venv_loftr. - If service fails at boot, check
ExecStartandWorkingDirectorypaths in service file. - If no tiles found, verify
data/archive/offline_tileset/index.jsonexists.

