Spatial Platform for Efficient Cloud Tile Raster Access
Zero-copy inference · Accelerated mission detection · Cloud-Native Geospatial Intelligence
SPECTRA is built on open geospatial standards — no proprietary lock-in, no monolithic servers, no pre-tiling marathons.
A universal language for geospatial data. Every image is a STAC Item — a JSON record with geometry, datetime, and typed asset links (raw JPG, ortho thumbnail, COG). Catalogs are static JSON files served from any CDN, consumed by any compliant viewer.
Open Catalog →GeoTIFFs engineered for HTTP range requests. Internal tiling + overviews mean a client fetches only the pixels it needs — a 512×512 chip from a 4000×3000 scene costs one round-trip. No preprocessing, no mosaic server, no waiting.
Inspect a COG →A FastAPI tile server that reads COGs on the fly. Pass any COG URL and get back XYZ tiles, bbox crops, previews, or band statistics — all computed from range-reads against the source file. Zero data duplication.
Browse the API →Every drone image flows through a deterministic pipeline: raw capture → metadata extraction → three published assets → on-demand chips → CV model.
# SPECTRA CV pipeline — loop the catalog, infer on every COG import pystac, rasterio, numpy as np from ultralytics import YOLO # ── 1. load catalog (static JSON, no API server needed) ────────────────── catalog = pystac.read_file("https://drone-catalog.shadyknollcave.io/catalog.json") model = YOLO("yolo11n.pt") # ── 2. iterate items, range-read only the pixels we need ───────────────── for item in catalog.get_all_items(): cog_url = pystac.utils.make_absolute_href( item.assets["visual"].href, item.get_self_href() ) with rasterio.open(cog_url) as ds: img = ds.read([1,2,3]).transpose(1,2,0) # (H,W,3) uint8 transform = ds.transform # affine: pixel → CRS coords # ── 3. run inference ───────────────────────────────────────────────── results = model(img) # ── 4. project detections back to WGS-84 ───────────────────────────── for box in results[0].boxes.xyxy.tolist(): # [x0,y0,x1,y1] x0, y0, x1, y1 = box # ds.xy(row, col) — note y=row, x=col lon0, lat0 = rasterio.transform.xy(transform, y0, x0) lon1, lat1 = rasterio.transform.xy(transform, y1, x1) print(f"bbox ({lon0:.6f},{lat0:.6f}) → ({lon1:.6f},{lat1:.6f})")
These are real HTTP calls — click any card to see TiTiler fetch and render a section of the drone COG in real time, using HTTP range requests.
titiler.shadyknollcave.io/cog/preview.jpg?url=…/8d09ed3…_ortho.tif
titiler.shadyknollcave.io/cog/bbox/-77.3616,38.6152,
-77.3608,38.6156.jpg?url=…
titiler.shadyknollcave.io/cog/info?url=…/8d09ed3…_ortho.tif
The STAC → COG → TiTiler stack is not just convenient — it removes every bottleneck between a raw drone capture and a production CV pipeline.
COG range reads mean the CV pipeline never downloads a full scene. Fetch only the 512-px chip that covers your detection ROI. A 4000×3000 COG streams as 16 KB of headers, not 16 MB of pixels.
Every pixel has a coordinate. ds.xy(row, col) maps YOLO bounding boxes back to lon/lat — output is GeoJSON, not pixel rectangles. Detections land directly on a map.
Loop catalog.get_all_items() to process a full mission in one script. STAC metadata — GSD, altitude, gimbal pitch — flows into model confidence scoring and false-positive filtering.
facebook/detr-resnet-50 running on COG chips fetched via TiTiler HTTP range requests — no full-scene download, no preprocessing pipeline.
Each image is a 512-px chip fetched via a single TiTiler
/cog/preview call — no full GeoTIFF download.
The model is facebook/detr-resnet-50
(COCO, Apache-2.0), loaded from Hugging Face with transformers.
Bounding boxes are drawn with pixel coordinates from the orthorectified COG —
map them back to lon/lat with ds.xy(row, col).
Each layer in the SPECTRA stack is useful on its own — but the real value emerges when they chain together. Here is exactly how the output of one piece becomes the input of the next.
geometry (the footprint as GeoJSON), a datetime, and an assets dict whose values are typed URLs pointing to actual files.
// A minimal STAC Item — the atomic unit of the catalog { "type": "Feature", "id": "8d09ed33ab1fb1c6", "geometry": { "type": "Point", "coordinates": [-77.361, 38.615] }, "properties": { "datetime": "2026-03-29T15:03:14Z", "gsd": 0.03 // 3 cm/pixel ground resolution }, "assets": { "visual": { "href": "./8d09ed33_ortho.tif", "type": "image/tiff; cloud-optimized=true" }, "thumbnail": { "href": "./8d09ed33_thumb.jpg", "type": "image/jpeg" } } }
The assets["visual"].href is a Cloud Optimized GeoTIFF (COG). CV code reads this URL directly — no download, just HTTP range requests for the pixels it needs. The geometry tells the pipeline exactly where on Earth those pixels live.
rasterio, and reads only the pixels it needs via HTTP range requests — the COG's internal tile index means you fetch a 512-px chip with one round-trip, not the whole 4000×3000 scene.
import pystac, rasterio from ultralytics import YOLO model = YOLO("yolo11n.pt") catalog = pystac.read_file("https://drone-catalog.shadyknollcave.io/catalog.json") for item in catalog.get_all_items(): cog_url = item.assets["visual"].href # from the STAC Item ↑ with rasterio.open(cog_url) as ds: chip = ds.read([1,2,3]).transpose(1,2,0) transform = ds.transform # pixel → world coords for box in model(chip)[0].boxes.xyxy.tolist(): x0, y0, x1, y1 = box lon0, lat0 = rasterio.transform.xy(transform, y0, x0) # pixels → lon/lat print(f"detection at ({lon0:.6f}, {lat0:.6f})")
Raw detections (car×21, truck×4) are aggregated into scene-level labels and written back into the STAC Item's properties block. Now every item knows its scene_type, activity_level, and a features array — structured metadata an LLM can reason over.
# CV output: {"cars": 21, "trucks": 4} # LLM reads that context and produces: { "spectra:ai.scene_type": "commercial_parking", "spectra:ai.activity_level": "high", "spectra:ai.description": "Dense commercial parking lot, high vehicle turnover", "spectra:ai.features": ["vehicles", "parking_lot", "paved_surface"] } # ↑ written back into the STAC Item and indexed by stac-fastapi
The enriched description and search_text fields are also fed into an embedding model. Those 768-dimensional vectors — along with the structured labels — are indexed into Qdrant so that natural-language queries can find relevant scenes even when the exact keywords don't match.
description and search_text strings and passes them through an embedding model — nomic-embed-text-v1.5 via Ollama, with a sentence-transformers CPU fallback — producing a 768-dimensional vector that captures semantic meaning rather than surface keywords.
scene_type, activity_level, bbox, item_id), is upserted into a Qdrant collection named spectra-stac using Cosine similarity. Point IDs are deterministic UUID5 hashes of the item ID, so re-running enrichment is fully idempotent. The vector and the payload travel together — one lookup retrieves both the semantic rank and the filterable metadata.
# 1. Concatenate enriched text fields into embedding document embed_doc = f"{item['properties']['spectra:ai.description']} {item['properties']['spectra:ai.search_text']}" # 2. Embed → 768-dim vector (Ollama primary, sentence-transformers fallback) vector = embedder.embed(embed_doc) # [0.021, -0.147, …] × 768, Cosine metric # 3. Upsert into Qdrant with filterable payload (idempotent UUID5 IDs) qdrant.upsert( collection_name="spectra-stac", points=[PointStruct( id=uuid5(NAMESPACE_URL, item["id"]), vector=vector, payload={ "item_id": item["id"], "scene_type": item["properties"]["spectra:ai.scene_type"], "activity_level": item["properties"]["spectra:ai.activity_level"], "bbox": item["bbox"], } )] )
The STAC API independently indexes the same enriched properties for exact-match and spatial filtering via CQL2-JSON. At query time both systems are consulted in parallel — the vector index for semantic proximity, the STAC API for structural precision — and their ranked results are fused.
mcp-stac-api exposes a hybrid_search() MCP tool that fires two queries in parallel:
scene_type, activity_level, or bbox payload fields. The structural leg translates any exact filters into CQL2-JSON and posts them to the pgstac STAC API — catching items whose metadata matches precisely even if their description wouldn't rank highly on semantics alone.
_rrf_score, _semantic_score, _semantic_rank, and _structural_rank attached to every feature.
# Two legs run in parallel semantic_hits = qdrant_search(embed(query), filters=payload_filter) # vector proximity structural_hits = stac_api_search(cql2_filter, datetime_range, bbox) # exact metadata # Reciprocal Rank Fusion (k=60, Cormack et al. 2009) for item_id, sem_rank, str_rank in merged_ranks(semantic_hits, structural_hits): rrf_score = (0.6 / (60 + sem_rank + 1) # 60 % semantic weight + 0.4 / (60 + str_rank + 1)) # 40 % structural weight # Returns: GeoJSON FeatureCollection — each feature carries: # _rrf_score · _semantic_score · _semantic_rank · _structural_rank # Graceful degradation: Qdrant unreachable → structural-only results
hybrid_search is one of several tools the MCP layer exposes alongside search_stac. An LLM agent calls it with a plain-text query and optional filters, and receives a ranked FeatureCollection ready to render or reason over — without knowing about Qdrant, CQL2, or RRF.
stac-fastapi) solves this: it indexes all items into a database and exposes an OGC-compliant HTTP API with spatial search, datetime ranges, and arbitrary property filters via CQL2-JSON.
activity_level = 'high' and features contains 'parking_lot'" — and get back a GeoJSON FeatureCollection in one HTTP call.
{
"collections": ["drone-imagery"],
"datetime": "2026-03-01T00:00:00Z/..",
"filter-lang": "cql2-json",
"filter": {
"op": "and", "args": [
{ "op": "=", "args": [{ "property": "spectra:ai.activity_level" }, "high"] },
{ "op": "a_contains", "args": [{ "property": "spectra:ai.features" }, ["parking_lot"]] }
]
}
}
// → GeoJSON FeatureCollection with matching STAC Items
The STAC API is powerful but requires knowing HTTP endpoints, CQL2-JSON syntax, and pagination. The MCP layer wraps all of that into typed tool definitions that any LLM client can call without knowing the underlying API shape.
mcp-stac-api wraps the STAC API in exactly this form.
/search, an LLM calls search_stac(scene_type="commercial_parking", activity_level="high") and gets back a clean list of items. The MCP server handles pagination, URL construction, asset normalization, and error handling — none of that leaks into the model's context window.
mcp-stac-api.shadyknollcave.io/mcp and immediately query the drone catalog without writing a single line of integration code.
# Tools the LLM sees (not HTTP endpoints — just function signatures) list_collections() → ["drone-imagery"] get_collection_info(collection_id="drone-imagery") → { title, description, queryable_fields, item_count } search_stac( collection="drone-imagery", scene_type="commercial_parking", # from LLM enrichment ↑ activity_level="high", bbox=[-77.4, 38.5, -77.3, 38.7], limit=10 ) → [{ id, geometry, assets, properties }] # normalized STAC Items
Spectra Map's backend runs a LiteLLM agent loop. It connects to the MCP server (or calls the STAC API directly), calls these tools in response to user queries, and receives GeoJSON footprints it can render immediately on a map — no domain-specific API knowledge required.
# User types: "high-activity parking lots, March 2026" # Agent loop resolves it step by step: # 1. model chooses tool → search_stac() search_stac(filter={ "op": "and", "args": [ { "op": "=", "args": ["spectra:ai.activity_level", "high"] }, { "op": "a_contains", "args": ["spectra:ai.features", ["parking_lot"]] } ] }, datetime="2026-03-01T00:00:00Z/..") # 2. STAC API returns items with geometry + enriched properties ↑ # 3. agent returns GeoJSON → frontend renders polygons on map # 4. user clicks footprint → TiTiler serves COG preview tile
Production services running in the geoint namespace — each one a building block of the SPECTRA stack.
Natural-language geospatial search. Type a query — a LiteLLM agent loop calls the STAC API and renders matching scene footprints as GeoJSON polygons on a MapLibre GL map.
spectra-map.shadyknollcave.io
LLM_MODEL env var — no code changeModel Context Protocol server over HTTP exposing deterministic read-only tools for the STAC API. Plug any MCP-capable LLM client directly into the drone catalog — no bespoke integration code required.
mcp-stac-api.shadyknollcave.io/mcp
list_collections · get_collection_info · search_stac/healthz and /readyz probes for K8s liveness checksOGC-compliant SpatioTemporal Asset Catalog API powering the entire SPECTRA stack. Serves drone imagery collections, exposes CQL2-JSON spatial and attribute filters, and acts as the authoritative catalog backend.
stac-api.shadyknollcave.io
spectra:ai.scene_type, activity_level, featuresDynamic tile server that reads Cloud Optimized GeoTIFFs on demand via HTTP range requests. Turns any COG URL into XYZ map tiles, bbox crops, band statistics, or full-scene previews — zero preprocessing, zero data duplication.
titiler.shadyknollcave.io
/cog/tiles/{z}/{x}/{y} — XYZ tiles from any COG URL/cog/preview and /cog/bbox/{bbox} — chip extraction/cog/infoInteractive web UI for navigating STAC catalogs and collections. Browse every drone imagery item, inspect metadata, preview thumbnails, and link directly to COG assets — no code required.
stac-browser.shadyknollcave.io
Ultralytics YOLO11 running inference against COG chips fetched via TiTiler range reads. Detects vehicles, infrastructure, and activity in drone scenes — outputs geo-registered GeoJSON bounding boxes stored back into the STAC catalog.
drone_metadata_extractor · batch pipeline
lon/lat WGS-84 coordinates*_detections.geojson per STAC Itemspectra-enrich which writes scene labels back to the catalogEnrichment pipeline that reads drone imagery through a Vision AI fallback chain, writes structured semantic metadata into STAC Items, embeds the text, and indexes vectors into Qdrant — powering hybrid search across the catalog.
spectra-enrich · batch enrichment pipeline
spectra:ai fields: description, scene_type, activity_level, featuresnomic-embed-text-v1.5 (sentence-transformers CPU fallback)spectra-stac collection