Features & Annotations

Vector data associated with overhead imagery — bounding boxes, polygons, points, and other geometries — can exist in two coordinate spaces. Image annotations live in pixel coordinates relative to the image grid: an ML model outputs a bounding box at pixel (512, 384), or an analyst draws a polygon around a building in row/column space. Geospatial features live in geographic coordinates (longitude, latitude, elevation) and can be plotted on a map, stored in a GIS database, or fused across multiple collects.

This package provides utilities for converting between the two:

  • Geolocator converts image annotations into geospatial features — assigning WGS-84 coordinates to pixel-space detections using the image’s sensor model.

  • Projector converts geospatial features into image annotations — computing pixel coordinates for known geographic locations so they can be rendered as overlays on the imagery.

  • STRFeature2DSpatialIndex provides efficient region queries over features in either coordinate space.

Both transforms delegate the actual coordinate math to a sensor model. This chapter focuses on the higher-level workflow: encoding detections, transforming batches of features, and querying the results spatially.

All input and output uses standard GeoJSON (RFC 7946), extended with image coordinate properties described in the encoding convention at the end of this chapter.

Geolocation (Image → World)

The Geolocator reads imageGeometry and imageBBox from each feature, transforms the pixel values through the sensor model, and populates the standard GeoJSON "geometry" and "bbox" members with WGS-84 coordinates. It also adds center_longitude and center_latitude properties (in degrees).

import geojson
from aws.osml.io import IO
from aws.osml.metadata import load_sensor_model
from aws.osml.features import Geolocator, ImagedFeaturePropertyAccessor

detections = [
    geojson.Feature(properties={
        "imageGeometry": {"type": "Point", "coordinates": [512, 384]},
        "imageBBox": [480, 352, 544, 416],
    }),
]

with IO.open("image.ntf", "r") as reader:
    sensor_model = load_sensor_model(reader)

geolocator = Geolocator(
    property_accessor=ImagedFeaturePropertyAccessor(),
    sensor_model=sensor_model,
)
geolocator.geolocate_features(detections)

# Each feature now has standard GeoJSON geometry with WGS-84 coordinates
print(detections[0]["geometry"])
# {"type": "Point", "coordinates": [-77.0365, 38.8977, 0.0]}

By default, the Geolocator skips features that already have a non-None geometry — avoiding overwrites from a previous geolocation pass or an external source. Pass force=True to re-geolocate all features regardless.

For batch geolocation the Geolocator builds a regular grid of sensor-model evaluations across the feature extent and uses bivariate spline interpolation for individual coordinates. This provides sub-pixel accuracy at a fraction of the cost of per-point evaluation. Increase approximation_grid_size (default 11) for large extents where higher interpolation fidelity is needed.

Without an elevation model, the geolocator assumes all detections are at sea level — introducing lateral error proportional to terrain height and sensor look angle. Supply an elevation model for improved accuracy:

from aws.osml.photogrammetry import ConstantElevationModel

geolocator = Geolocator(
    property_accessor=ImagedFeaturePropertyAccessor(),
    sensor_model=sensor_model,
    elevation_model=ConstantElevationModel(500.0),
)

See also

Elevation Models for DTED-based and other terrain sources. Sensor Models for details on the image-to-world coordinate transform.

Projection (World → Image)

The Projector is the inverse operation. Given features with geographic coordinates — from a GIS database, a reference layer, or a prior geolocation pass — it computes where they appear in a specific image and populates imageGeometry and imageBBox with pixel coordinates. Only features whose projected geometry intersects the provided image bounds are included in the result. Use cases include:

  • Rendering known annotations as overlays on a new collect

  • Pre-filtering a feature database to only features visible in a scene

  • Generating training labels by projecting ground truth into image space

import geojson
from aws.osml.io import IO
from aws.osml.metadata import load_sensor_model
from aws.osml.features import Projector, ImagedFeaturePropertyAccessor

features = [
    geojson.Feature(
        geometry=geojson.Point((-77.0365, 38.8977)),
        properties={"label": "building-A"},
    ),
]

with IO.open("image.ntf", "r") as reader:
    sensor_model = load_sensor_model(reader)
    width, height = reader.segments[0].width, reader.segments[0].height

projector = Projector(
    property_accessor=ImagedFeaturePropertyAccessor(),
    sensor_model=sensor_model,
    image_bounds=(0.0, 0.0, float(width), float(height)),
)
visible_features = projector.project_features(features)

for f in visible_features:
    print(f["properties"]["imageGeometry"])
    # {"type": "Point", "coordinates": [512.0, 384.0]}

Parameter

Description

property_accessor

Facade for reading/writing image coordinate properties.

sensor_model

Sensor model used for world_to_image() conversion.

image_bounds

Pixel-space bounding box (min_x, min_y, max_x, max_y). For full images use (0, 0, width, height). Add a buffer (e.g. (-50, -50, w+50, h+50)) to include features in a margin around the image.

elevation_model

Optional; queried for terrain height when a coordinate lacks an explicit Z value.

force

If False (default), features with existing imageGeometry are included without re-projection.

The Projector resolves vertex elevation by precedence: an explicit Z coordinate in the GeoJSON is used first; if absent and an elevation model is provided, it is queried; otherwise elevation defaults to 0.0 m.

Spatial Indexing

STRFeature2DSpatialIndex provides efficient spatial queries using Shapely’s Sort-Tile-Recursive tree. By default it indexes features by their imageGeometry, so query coordinates are in pixels:

import shapely
from aws.osml.features import STRFeature2DSpatialIndex

feature_collection = geojson.FeatureCollection(features)
index = STRFeature2DSpatialIndex(feature_collection)

query_box = shapely.box(400, 300, 600, 500)
results = index.find_intersects(query_box)

nearest = index.find_nearest(shapely.Point(500, 400), max_distance=100)

To index by geographic geometries instead (after geolocation), pass use_image_geometries=False. The STR tree construction is O(n log n) but subsequent queries are O(log n) — build the index once and reuse it for multiple queries.

Encoding Image Coordinates in GeoJSON

GeoJSON does not define a way to represent image pixel coordinates. This library adopts a convention that stores image-space locations alongside standard geographic geometry, allowing a single feature to carry both representations as it moves through the geolocation or projection workflow.

Features that have not yet been assigned geographic coordinates use a null "geometry" (section 3.2 of RFC 7946) and store image-space locations in "properties":

imageGeometry — A GeoJSON-like Geometry Object where coordinate values are in pixels. Origin (0, 0) is the top-left corner, x increases right, y increases down. Coordinates are ordered (x, y).

imageBBox — An axis-aligned bounding box in pixels: [min x, min y, max x, max y].

Supported Geometry Types

Type

imageGeometry coordinates

Geographic result after geolocation

Point

Single (x, y)

Single (lon, lat, elev)

LineString

List of (x, y)

List of (lon, lat, elev)

Polygon

Exterior + optional interior rings

Geographic polygon

Multi* / GeometryCollection

Collection of the above

Matching collection type

Examples

Point — a single detected object with bounding box:

{
    "type": "Feature",
    "geometry": null,
    "properties": {
        "imageGeometry": {"type": "Point", "coordinates": [105.0, 5.0]},
        "imageBBox": [100, 0, 110, 10]
    }
}

LineString — a road segment traced through the image:

{
    "type": "Feature",
    "geometry": null,
    "properties": {
        "imageGeometry": {
            "type": "LineString",
            "coordinates": [[170.0, 45.0], [180.0, 47.0], [182.0, 49.0]]
        }
    }
}

Polygon — a building footprint (first coordinate repeated to close the ring):

{
    "type": "Feature",
    "geometry": null,
    "properties": {
        "imageGeometry": {
            "type": "Polygon",
            "coordinates": [[[0, 0], [10, 2], [10, 12], [0, 12], [0, 0]]]
        },
        "imageBBox": [0, 0, 10, 12]
    }
}

Deprecated Property Formats

Earlier versions used different property names. The library still reads these for backwards compatibility, but new code should use imageGeometry and imageBBox exclusively.

Property

Format

Status

imageGeometry

GeoJSON geometry object

Preferred

imageBBox

[minx, miny, maxx, maxy]

Preferred

geom_imcoords

Coordinate list

Deprecated

bounds_imcoords

[minx, miny, maxx, maxy]

Deprecated

detection.pixelCoordinates

GeoJSON-like

Deprecated