Evidence fusion for determining where visual media was captured.
Analyze images and video by combining visual embeddings, zero-shot scene classification,
signage OCR, and embedded EXIF telemetry.
Architecture • Quick Start • Inference Pipeline • API Usage • Dashboard • Troubleshooting
Whereabouts is a high-throughput, multi-modal visual intelligence and geolocation platform designed to estimate the physical location represented by image and video payloads.
The platform uses evidence fusion rather than relying on a single visual signal. Its inference stack combines:
- Deep visual embeddings for coarse structural similarity.
- Zero-shot visual classification for botanical and architectural context.
- OCR-based signage extraction for physical textual evidence.
- EXIF telemetry as a sensor-fusion fallback when visual confidence is insufficient.
- PostGIS + Qdrant for spatial and vector persistence.
- FastAPI + Celery for asynchronous ingestion and long-running ML processing.
Core principle: weak signals become more useful when independently extracted evidence is combined into a single geolocation decision.
Whereabouts follows a four-stage inference pipeline:
┌──────────────────────────────────────────────────────────────────┐
│ MEDIA INGESTION │
│ Image / Video Payload │
└───────────────────────────────┬──────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ 01 COARSE STRUCTURAL VECTOR MATCHING │
│ DINOv2 · dinov2_vitl14 · 1024-D embeddings · Qdrant │
└───────────────────────────────┬──────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ 02 ZERO-SHOT VISUAL CLASSIFICATION │
│ CLIP · Botanical / Flora Context · Architecture │
└───────────────────────────────┬──────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ 03 PHYSICAL SIGNAGE OCR │
│ EasyOCR · Road Signs · Street Markers · Building Plaques │
└───────────────────────────────┬──────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ 04 SCORING + SENSOR FUSION │
│ Vector Similarity + OCR Bonus + EXIF Fallback │
└───────────────────────────────┬──────────────────────────────────┘
│
▼
GEOLOCATION RESULT
The composite decision combines raw vector similarity with extracted textual evidence.
- OCR detection contributes a
+0.15bonus. - When composite confidence falls below
0.35, the platform triggers Sensor Fusion Fallback. - The fallback routes to embedded hardware EXIF coordinates, while retaining extracted flora and structural context.
This preserves the available evidence instead of discarding lower-confidence visual inference.
┌──────────────────┐
│ Client │
│ Image / Video │
└────────┬─────────┘
│
▼
┌─────────────────────┐
│ FastAPI │
│ Async API Layer │
└─────────┬───────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ PostGIS │ │ RabbitMQ │
│ Spatial DB │ │ Message Bus │
└─────────────┘ └──────┬──────┘
│
▼
┌───────────────┐
│ Celery │
│ Workers │
└───────┬───────┘
│
▼
┌────────────────────┐
│ ML / VLM Stack │
│ DINOv2 · CLIP │
│ EasyOCR · EXIF │
└─────────┬──────────┘
│
┌──────────────────┴─────────────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Qdrant │ │ PostGIS │
│ Vector DB │ │ Geo Results │
└─────────────┘ └─────────────┘
The recommended development topology is Hybrid Local/Native Mode:
| Component | Runtime | Purpose |
|---|---|---|
| PostGIS | Docker | Spatial persistence |
| RabbitMQ | Docker | AMQP message broker |
| Qdrant | Docker | Vector database |
| FastAPI | Host venv |
Async HTTP gateway |
| Celery | Host venv |
ML task workers |
| Alembic | Host venv |
Schema migrations |
| PyTorch / CUDA | Host | Native GPU acceleration |
Keeping the ML components native preserves direct access to host GPU acceleration while Docker provides isolated infrastructure services.
Whereabouts/
├── .env.development
├── .dockerignore
├── docker-compose.yml
├── requirements.txt
├── alembic.ini
├── seed_vector.py
│
├── migrations/
│ ├── env.py
│ ├── README
│ ├── script.py.mako
│ └── versions/
│
├── qdrant_storage/
│
├── app/
│ ├── __init__.py
│ ├── main.py
│ │
│ ├── api/
│ │ ├── __init__.py
│ │ └── v1/
│ │ ├── __init__.py
│ │ └── endpoints/
│ │ ├── __init__.py
│ │ └── scanner.py
│ │
│ ├── core/
│ │ ├── __init__.py
│ │ ├── config.py
│ │ └── security.py
│ │
│ ├── db/
│ │ ├── __init__.py
│ │ └── session.py
│ │
│ ├── ml/
│ │ ├── __init__.py
│ │ ├── engine.py
│ │ ├── geo_scanner.py
│ │ └── weights/
│ │ └── .gitkeep
│ │
│ ├── models/
│ │ ├── __init__.py
│ │ └── spatial.py
│ │
│ ├── services/
│ │ ├── __init__.py
│ │ └── metadata_extractor.py
│ │
│ ├── templates/
│ │ └── map.html
│ │
│ └── workers/
│ ├── __init__.py
│ └── tasks.py
│
└── docker/
├── api.Dockerfile
└── worker.Dockerfile
| Path | Responsibility |
|---|---|
app/main.py |
FastAPI initialization and routing |
app/api/v1/endpoints/scanner.py |
Media ingestion and GeoJSON queries |
app/core/config.py |
Environment and runtime configuration |
app/core/security.py |
Sandboxing and cryptographic helpers |
app/db/session.py |
Async database engine/session infrastructure |
app/ml/engine.py |
Multi-modal VLM processing |
app/ml/geo_scanner.py |
DINOv2, CLIP, EasyOCR and sensor fusion |
app/models/spatial.py |
GeoAlchemy2/PostGIS mappings |
app/services/metadata_extractor.py |
Hardened ExifTool wrappers |
app/templates/map.html |
Leaflet telemetry dashboard |
app/workers/tasks.py |
Long-running Celery processing jobs |
The documented development environment assumes Linux with Docker, Python 3.12+, and native system dependencies.
Install the required host packages:
sudo apt update && sudo apt install -y \
build-essential \
libgl1-mesa-glx \
libglib2.0-0 \
exiftool \
libpq-dev \
netcat-openbsdCreate and activate the project's virtual environment:
python3 -m venv venv
source venv/bin/activateInstall Python dependencies:
pip install -r requirements.txtThe startup sequence is intentionally ordered to prevent service race conditions, duplicate schema initialization, and excessive ML worker memory consumption.
Terminal 1 · Outside venv
docker-compose down --volumes --remove-orphans
docker network prune -f
docker-compose up -dThis provisions:
whereabouts_postgres_1— PostGISwhereabouts_rabbitmq_1— RabbitMQwhereabouts_qdrant_1— Qdrant
Terminal 1 · Inside venv
source venv/bin/activate
echo "Checking daemon socket availability..."
until nc -z 127.0.0.1 5432 && \
nc -z 127.0.0.1 5672 && \
nc -z 127.0.0.1 6333; do
echo "⏳ Waiting for PostGIS (5432), RabbitMQ (5672), and Qdrant (6333)..."
sleep 2
done
echo "✅ All backing databases and message brokers are live."Terminal 1 · Inside venv
alembic upgrade headWarning
Do not run python3 init_db.py.
Alembic is the sole owner of the PostGIS schema migration state. Running an additional initialization utility can result in a DuplicateTableError because the geospatial_scans relation already exists.
Terminal 1 · Inside venv
Populate the urban_global_geoms vector collection:
python3 seed_vector.pyTerminal 1 can now remain available for database and infrastructure inspection.
Open Terminal 2.
Inside venv
source venv/bin/activate
celery -A app.workers.tasks.celery_app \
worker \
--loglevel=info \
-P prefork \
--concurrency=2Important
Keep worker concurrency explicitly capped at 2.
Each prefork child can maintain its own instances of the heavy DINOv2, CLIP, and EasyOCR models. Unbounded concurrency can exhaust system RAM/VRAM and cause the Linux OOM Killer to terminate workers with SIGKILL.
Open Terminal 3.
Inside venv
source venv/bin/activate
uvicorn app.main:app \
--host 127.0.0.1 \
--port 8000 \
--reloadThe API is now available at:
http://127.0.0.1:8000
| Phase | Terminal | Environment | Command |
|---|---|---|---|
| 0 | 1 | Host | docker-compose up -d |
| I | 1 | venv |
Socket readiness check |
| II | 1 | venv |
alembic upgrade head |
| III | 1 | venv |
python3 seed_vector.py |
| IV | 2 | venv |
celery ... --concurrency=2 |
| V | 3 | venv |
uvicorn app.main:app --reload |
Send a media payload to the asynchronous scanner:
curl -X POST \
"http://127.0.0.1:8000/api/v1/scanner/process-media" \
-F "file=@/home/alien/Videos/DSC04375.JPG;type=image/jpeg"The gateway stores the media, queues background processing through RabbitMQ/Celery, and returns a tracking ticket.
Example:
{
"tracking_id": "db431f16-e33d-4a4f-b052-853c665561ec",
"task_id": "c39cd5d4-e95d-4eb5-8718-aa00e653f987",
"status": "QUEUED",
"map_configuration": {
"tile_provider": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
"attribution": "© OpenStreetMap contributors",
"layer_format": "EPSG:4326"
}
}Query the committed spatial result using its tracking_id:
curl -X GET \
"http://127.0.0.1:8000/api/v1/scanner/scans/11c74158-1fed-4d47-8e9d-de08d05dcd11/geojson"Example response:
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-121.77858, 47.436018]
},
"properties": {
"tracking_id": "11c74158-1fed-4d47-8e9d-de08d05dcd11",
"confidence": 0.0192,
"accuracy_radius": 15.0,
"evidence_logs": {
"signage_text_found": "NONE",
"detected_foliage": "Subalpine Evergreen Meadows",
"architectural_era": "Pacific Northwest Timber Frame",
"logical_deduction_chain": "Visual vector confidence (0.02) below threshold (<0.35). Defaulting pipeline logic. [SENSOR FUSION FALLBACK: Rerouted to high-accuracy hardware EXIF metadata]."
},
"created_at": "2026-07-21T05:48:13.148000+00:00"
}
}Whereabouts includes an embedded Leaflet-based telemetry dashboard directly within the FastAPI application.
This architecture avoids the cross-origin and browser filesystem constraints that can complicate a separately hosted local frontend.
Open:
http://127.0.0.1:8000/map?id=<tracking_id>
Example:
http://127.0.0.1:8000/map?id=11c74158-1fed-4d47-8e9d-de08d05dcd11
| Feature | Description |
|---|---|
| 🎯 Tracking Controller | Look up and switch active tracking tickets |
| 📡 Live Polling | Non-blocking updates approximately every 4 seconds |
| 🧭 Spatial Visualization | Render resolved coordinates on an interactive map |
| 🔎 Evidence Logs | Display confidence, accuracy radius, flora, architecture and signage |
| 🧠 Deduction Chain | Surface the reasoning path associated with the geolocation result |
The Leaflet dashboard may display a black-and-white basemap containing repeating:
API KEY REQUIRED
This occurs when a CARTO raster tile endpoint requires authorization and no API key is supplied.
For local development, the simplest solution is to use standard OpenStreetMap tiles.
Edit:
nano app/templates/map.htmlLocate the L.tileLayer initialization and replace the CARTO endpoint with:
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap contributors'
}).addTo(map);Save the file and refresh the dashboard.
If the deployment requires CARTO styling, configure the appropriate CARTO API key in the tile URL:
L.tileLayer(
'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png?key=YOUR_ACTUAL_CARTO_KEY',
{
attribution: '© OpenStreetMap contributors © CARTO',
maxZoom: 20
}
).addTo(map);Then save app/templates/map.html and reload the dashboard.
Do not introduce a parallel database initialization workflow. Use:
alembic upgrade headas the authoritative schema initialization and migration path.
DINOv2, CLIP, and EasyOCR are heavyweight model components. Keep Celery prefork concurrency constrained:
--concurrency=2Adjust only after validating available RAM/VRAM under the target workload.
Whereabouts retains extracted contextual evidence even when the visual score is insufficient to produce a strong location estimate. Sensor-fusion fallback therefore augments the result rather than replacing the analytical context.
| Layer | Technology |
|---|---|
| API | FastAPI |
| Async Processing | Celery |
| Message Broker | RabbitMQ |
| Relational / Spatial DB | PostgreSQL + PostGIS |
| Vector DB | Qdrant |
| Visual Embeddings | DINOv2 |
| Zero-Shot Classification | OpenAI CLIP |
| OCR | EasyOCR |
| Metadata | ExifTool |
| Mapping | Leaflet.js |
| Containers | Docker Compose |
| ORM / Spatial Mapping | SQLAlchemy 2.0 + GeoAlchemy2 |
| ML Runtime | PyTorch |
| Configuration | Pydantic BaseSettings |
Whereabouts is open-source software licensed under the MIT License.