The goal of stac_airphoto_bc
is to serve georeferenced historical aerial photograph thumbnails for
British Columbia as a STAC
collection. Currently covering the Neexdzii Kwa (Upper Bulkley River)
watershed and three small southeast BC areas, 10,100 photos spanning
1967–2019. Queryable by location and time via rstac
and QGIS (v3.42+) at https://images.a11s.one.
This work is built on the fly R
package which handles airphoto footprint estimation, spatial filtering,
thumbnail downloading, and georeferencing. Sister collections on the
same endpoint:
stac_dem_bc
— LidarBC digital elevation models (~58k GeoTIFFs)stac_uav_bc
— UAV imagery organized by region / watershed-group / yearSource centroids from the BC Data Catalogue, process through five stages:
| Step | Script | What |
|---|---|---|
| Fetch | 01_fetch.R |
Query centroids, size DEM-corrected footprints, assign per-roll rotation and placement, select, download thumbnails |
| Georef | 02_georef.R |
Warp thumbnails to their footprints (BC Albers, EPSG:3005) |
| COG | 03_cog.py |
Apply the measured placement shift, embed metadata tags, write each Cloud-Optimized GeoTIFF once |
| STAC | 05_stac_register.py |
Generate items with checksums and provenance, merge into the collection, validate |
| S3 | 04_s3_upload.R |
Re-check every item against its COG, back up the published item JSONs, sync |
Run end-to-end: bash scripts/run_pipeline.sh
Each COG carries embedded metadata (visible in QGIS layer
properties): AIRP_ID, PHOTO_DATE,
SCALE, FILM_ROLL, FRAME_NUMBER,
FOCAL_LENGTH, FLYING_HEIGHT,
FILENAME, and how it was placed: ROTATION,
ROTATION_SOURCE, PLACEMENT_SOURCE,
SHIFT_X_M_3005, SHIFT_Y_M_3005,
HEIGHT_SOURCE, FOOTPRINT_BASIS,
FLY_VERSION, FLY_SHA,
PIPELINE_SHA.
Footprints come from fly,
sized with a DEM (MRDEM-30): checked against the province’s published
photogrammetric solutions it agrees on width to 0–2%, and without it
film is about 9% too small. Two measured corrections from a separate
validation against provincial orthophotos are then applied, and every
item says where its values came from, so frames can be filtered and
checked:
airphoto:rotation_source — how the scanned image sits
on its footprint, a per-roll constant: measured (fitted
against orthophotos), reviewed (a person looked),
disputed (a person disagrees with the measured value, kept
and flagged), or assumed_by_series (no measurement; the
default for the roll’s series). Absent on digital frames, whose
orientation fly derives itself.airphoto:placement_source — a per-frame shift in
EPSG:3005 metres (airphoto:shift_x_m_3005,
airphoto:shift_y_m_3005): correlator (the
frame’s own fit), roll_model (the roll’s measured bias,
only where held-out frames confirm it), manual, or
none (where the catalogue puts it).airphoto:height_source — whether fly used
the catalogue’s flying height, repaired a known unit slip, or refused an
implausible one.Each thumbnail asset carries file:checksum (a sha256
multihash) and file:size, and each item the
fly and pipeline commits that built it
(nge:fly_version, nge:fly_sha,
nge:pipeline_sha, nge:produced_datetime).
Every item also carries the BC Data Catalogue’s own metadata as
queryable airphoto: properties — media,
bcgs_tile, nts_tile,
ground_sample_distance (centimetres), and
georef_metadata, a boolean saying whether a published
photogrammetric solution exists for that frame. 1,775 of 9,976 frames
have one. Where the catalogue links a retrievable file — the PAT-B
solution, the camera calibration report, the scanned flight log page —
it appears as an asset with a metadata role, so a client
can fetch it without a second trip to the catalogue.
# frames with a photogrammetric solution
rstac::stac_search(
q, collections = "stac-airphoto-bc", limit = 100
) |> rstac::ext_filter(`airphoto:georef_metadata` == TRUE)
library(rstac)
library(sf)
# AOIs come from the registry in scripts/aoi.R, not from constants here.
# Swap the id for any other registered area — se_a, se_b, se_c.
source("scripts/aoi.R")
aoi <- aoi_resolve("neexdzii_kwa") |> sf::st_transform(4326)
# Search for photos between 1965 and 1975
q <- rstac::stac("https://images.a11s.one/") |>
rstac::stac_search(
collections = "stac-airphoto-bc",
intersects = jsonlite::fromJSON(
geojsonsf::sf_geojson(aoi, atomise = TRUE, simplify = FALSE),
simplifyVector = FALSE
) |> (\(x) x$geometry)(),
datetime = "1965-01-01T00:00:00Z/1975-12-31T00:00:00Z"
) |>
rstac::post_request()
r <- q |> rstac::items_fetch()
saveRDS(r, "data/stac_result.rds")
r <- readRDS("data/stac_result.rds")
tab <- tibble::tibble(
title = purrr::map_chr(r$features, ~ purrr::pluck(.x, "properties", "title", .default = .x$id)),
url = purrr::map_chr(r$features, ~ purrr::pluck(.x, "assets", "thumbnail", "href"))
) |>
dplyr::mutate(
link_view = ngr::ngr_str_link_url(
url_base = "https://viewer.a11s.one/?cog=",
url_resource = url,
url_resource_path = FALSE,
anchor_text = title
),
link_download = ngr::ngr_str_link_url(
url_base = url,
anchor_text = basename(url)
)
) |>
dplyr::select(link_view, link_download)
As of QGIS 3.42, STAC items can be accessed directly via the Data
Source Manager. Connect to https://images.a11s.one and
browse the stac-airphoto-bc collection. See this
blog for details.
Items display with descriptive titles
(airp_id -- roll_frame -- date) for easy
identification:
Browsing the airphoto collection in QGIS STAC Data Source Manager
assumed_by_series, and frames
stay where the catalogue puts them. Extending the measurement is how
those labels change.stac_*_bc repos (true-footprint
geometry, uv-based Python dependency management, structured logging +
benchmarking) apply here too.Browse open issues for the current backlog.