Metadata for Camera and Pose Extraction#

weitsicht turns EXIF/XMP tags into usable camera models and image parameters. The package is prepared to support multiple metadata wrappers by defining a small tag interface (MetaTagsBase). Currently, weitsicht ships with PyExifToolTags, which works with metadata dictionaries produced by Phil Harvey’s exiftool (via CLI JSON output or Python wrappers).

Use the helpers as building blocks:

  • retrieve IOR (intrinsics) for a camera model,

  • extract EOR (position/orientation + CRS),

  • build a ready-to-use ImagePerspective from metadata.

Note

There is no universal metadata standard: vendors mix EXIF, XMP, and MakerNotes inconsistently. Standard GPS tags are often present but are insufficient without orientation tags. Always sanity-check by mapping an image footprint and comparing it with a known ortho or satellite layer before running large batches.

Minimal example#

Extract tags (e.g. with Phil Harvey’s exiftool) and wrap the resulting dictionary with PyExifToolTags.

from pathlib import Path
import json

# Option A: Python helper (needs exiftool on PATH)
from exiftool import ExifToolHelper
with ExifToolHelper() as et:
    meta_list = et.get_metadata([Path("DJI_0001.JPG")])

# Option B: CLI + JSON
# import subprocess
# raw = subprocess.check_output(["exiftool", "-json", "DJI_0001.JPG"])
# meta_list = json.loads(raw)

from weitsicht import PyExifToolTags
tags = PyExifToolTags(meta_list[0])

Parse data (PyExifToolTags)#

PyExifToolTags provides convenient accessors that return typed dataclasses. Other metadata sources can be supported by implementing MetaTagsBase and returning the same tag dataclasses.

# image dimensions (width, height)
tags.image_shape()

# group all resolved tags into one object
tags_all = tags.get_all()

# camera/intrinsics related tags
tags.get_ior_base()       # -> MetaTagIORBase
tags.get_ior_extended()   # -> MetaTagIORExtended

# GNSS and orientation tags
tags.get_standard_gps()         # -> MetaTagGPS
tags.get_orientation_values()   # -> MetaTagOrientation

# optional vendor alternatives
tags.get_z_alternatives()  # -> MetaTagZalternatives (e.g. XMP:RelativeAltitude)
tags.get_crs()             # -> MetaTagCRS (custom HorizCS/VertCS tags)

Camera IOR options#

Option 1 (baseline): estimate minimal intrinsics#

Estimate camera intrinsics from standard tags: estimate_camera(tags.get_ior_base()) returns width, height, focal_px, cx, cy.

from weitsicht import CameraOpenCVPerspective, estimate_camera

width, height, focal_px, cx, cy = estimate_camera(tags.get_ior_base())
camera = CameraOpenCVPerspective(
    width=width,
    height=height,
    fx=focal_px,
    fy=focal_px,
    cx=cx,
    cy=cy,
)

Option 2: use ior_from_meta (preferred)#

ior_from_meta wraps estimation and also checks for extended/vendor calibration tags when present.

from weitsicht import ior_from_meta

ior_res = ior_from_meta(
    tags_ior=tags.get_ior_base(),
    tags_ior_extended=tags.get_ior_extended(),
)
if ior_res.ok:
    camera = ior_res.camera
    width = ior_res.width
    height = ior_res.height
else:
    # ResultFailure[MetadataIssue]
    print(ior_res.error, ior_res.issues)

Exterior orientation (EOR)#

from weitsicht import eor_from_meta

eor_res = eor_from_meta(tags.get_all())
if eor_res.ok:
    position = eor_res.position
    orientation = eor_res.orientation
    crs = eor_res.crs
else:
    print(eor_res.error, eor_res.issues)

Important

The estimated image pose (EOR: position + orientation) is returned in WGS84 geocentric / ECEF (EPSG:4978) by default (i.e. unless you set to_utm=True). You can always inspect the CRS from the result (eor_res.crs) or, when building a full image, via image.crs.

CRS override (optional)#

By default, eor_from_meta, image_from_meta and ImageBuilder.eor() tries to interpret the position tags using CRS information found in metadata (custom XMP tags like HorizCS/VertCS). If those tags are missing, it falls back to WGS84 (typically EPSG:4979 for ellipsoidal heights; EPSG:4326+3855 for orthometric/relative modes).

You can override the CRS detection by passing crs=.... This is useful when:

  • the CRS tags are missing or wrong,

  • you want to enforce a specific vertical datum (use a 3D CRS or a compound CRS),

  • you inject positions from RTK/DGPS processing that are referenced to a known CRS from your base/reference setup.

from pyproj import CRS
from weitsicht import eor_from_meta

# Example: ETRS89 ECEF coordinates (adapt EPSG to your reference frame if needed)
crs_rtk = CRS.from_epsg(4936)

eor_res = eor_from_meta(tags.get_all(), crs=crs_rtk)

Important

Even if you pass a crs to the metadata helpers, the pose is still converted to WGS84 ECEF (EPSG:4978). The reason is that we assume the orientation angles describe a local tangent plane at the image position. Converting the position to ECEF first and then deriving the local tangent frame is robust and keeps the implementation simple. Building the local tangent plane directly in an arbitrary CRS would require deeper analysis of the CRS datum/ellipsoid and axis conventions.

Important

If you work with a specific CRS realization (e.g. a particular ETRS89/ETRF frame or a national realization), you must use the correct EPSG code for that system/realization. To avoid subtle offsets, make sure that for your mapper (rasters, meshes, reference layers) you also use then the correct realization/datum that mapper data was derived from.

This is a broad topic and we will not further at the moment cover that more specific.

Time-dependent transformations (e.g. plate motion) are currently not considered because the CoordinateTransformer do not accept a coordinate’s epoch/time. If you need epoch-aware transformations, use pyproj directly.

Vertical reference (Z) options#

eor_from_meta, image_from_meta and ImageBuilder.eor() support different ways to interpret the height component:

  • vertical_ref="ellipsoidal" (default): interpret metadata altitude as ellipsoidal height (WGS84).

  • vertical_ref="orthometric": interpret metadata altitude as orthometric/geoid height (EGM2008).

  • vertical_ref="relative": use a vendor RelativeAltitude tag (if present) and compute z = height_rel + RelativeAltitude.

height_rel is only used for vertical_ref="relative" and should be set to the reference height (meters) that the relative altitude is measured from (often the take-off height, if known).

UTM output (optional)#

By default, eor_from_meta returns the pose in WGS84 ECEF (EPSG:4978). If you prefer a local projected output, set to_utm=True. The UTM zone is chosen automatically from the WGS84 lon/lat:

from weitsicht import eor_from_meta

eor_res = eor_from_meta(tags.get_all(), to_utm=True)
if eor_res.ok:
    print(eor_res.crs)  # e.g. EPSG:32633+3855 (zone depends on lon/lat)
    utm_position = eor_res.position
    utm_orientation = eor_res.orientation  # aligned to the UTM grid (meridian convergence applied)
else:
    print(eor_res.error, eor_res.issues)

Note

to_utm=True outputs a compound CRS (UTM + EGM2008 height, +3855). This can require PROJ grid data. If you see missing-grid errors, enable network grids via pyproj.network.set_network_enabled(True) (see Top-Tips).

Complete image from metadata#

from weitsicht import image_from_meta

img_res = image_from_meta(tags)  # ECEF output (default)
# img_res = image_from_meta(tags, to_utm=True)  # projected UTM output
if img_res.ok:
    image = img_res.image
    # Assign a mapper later, e.g. image.mapper = MappingHorizontalPlane(...)
else:
    print(img_res.error, img_res.issues)

Return structures#

The metadata helpers return small result objects:

  • Success results are dataclasses with ok=True (e.g. IORFromMetaResultSuccess).

  • Failures are ResultFailure with ok=False and the fields: error (human-readable message) and issues (a set of MetadataIssue).

The concrete unions are:

  • IORFromMetaResult = IORFromMetaResultSuccess | ResultFailure[MetadataIssue]

  • EORFromMetaResult = EORFromMetaResultSuccess | ResultFailure[MetadataIssue]

  • ImageFromMetaResult = ImageFromMetaResultSuccess | ResultFailure[MetadataIssue]

Keeping IOR and EOR decoupled#

If you want to handle IOR and EOR separately (e.g. use a default IOR but read EOR from IMU), use the builder:

from weitsicht.metadata.image_from_meta import ImageFromMetaBuilder

builder = ImageFromMetaBuilder(tags)
ior_res = builder.ior()
eor_res = builder.eor()
# eor_res = builder.eor(to_utm=True)

# Optionally build an image only if both are available
img_res = builder.image()

Notes & tips#

  • Sensor database fallback (src/weitsicht/metadata/camera_database.py) supplies sensor sizes for common models when EXIF lacks focal-plane resolutions.

  • CRS is separate: metadata rarely contains a complete CRS definition; pass a pyproj.CRS explicitly if needed.

  • Validation: after building an image, map a footprint and compare it to known GIS layers to detect convention issues.