Deciding When to Fix Versus Optimise Intrinsics

Every reconstruction engine offers a choice: solve the camera parameters along with everything else, or hold them at supplied values. Most default to solving them, which is correct for the varied imagery the algorithms were developed on and incorrect for a nadir mapping grid — the geometry in which the parameters are least observable and most able to absorb a surface error.

The decision is not a preference. It follows from two properties of the survey that are known before processing starts, and getting it right costs nothing while getting it wrong costs a re-flight. This page sets out the rule, the middle position, and how to check the choice afterwards. It applies the analysis in camera calibration and lens models in Python.

The two properties that decide it

Observability is whether the flight geometry can distinguish a lens error from a scene error. Varied altitude separates focal length from distance; oblique views separate radial distortion from surface curvature; a cross pattern separates the principal point from a tilt. A single-altitude nadir grid has none of these.

Constraint is whether ground control pins the solution independently of the camera model. Control distributed across the interior of the site constrains the surface directly, so a self-calibration cannot bend it. Control only around the perimeter leaves the middle free, which is precisely where a dome lives.

High on both, self-calibration wins: it captures the lens as flown, including thermal drift, better than any prior calibration. Low on both, fixed intrinsics win by a wide margin. In between, a partial freedom is the right answer.

Choosing an intrinsics policy from observability and control A two-by-two grid of flight observability against control strength. With weak observability and weak control, fix the intrinsics from a prior calibration, because self-calibration will bend the surface. With weak observability and strong interior control, free the focal length and first radial term only. With strong observability and weak control, also free only those two. With strong observability and strong control, optimise everything, because a self-calibration then reflects the lens as flown better than any laboratory value. weak control strong interior control nadir only cross + obliques fix from a prior calibration the lens is not observable here and nothing constrains the surface self-calibration will dome it free focal and k1 only control pins the surface so the drifting terms can adapt the rest stay fixed free focal and k1 only geometry separates the terms but little external constraint partial freedom is the safe middle optimise everything both constraints present the lens as flown beats a stored value including thermal drift

Figure 1 — The rule. Both properties are known before processing, which is why the decision does not need a trial run.

Minimal reproducible solution

def intrinsics_policy(flight: dict, control: dict, calibration: dict | None) -> dict:
    """Decide which intrinsics to free for this survey.

    Observability and constraint are assessed separately because they fail
    separately: a well-controlled nadir survey and a poorly controlled
    oblique one both land in the middle position for different reasons.
    """
    observable = (flight.get("oblique_fraction", 0.0) > 0.05
                  or flight.get("altitude_range_m", 0.0) > 0.2 * flight.get("altitude_m", 1)
                  or flight.get("cross_pattern", False))
    constrained = (control.get("gcp_count", 0) >= 5
                   and control.get("interior_gcp_count", 0) >= 2)

    if observable and constrained:
        return {"free": ["focal", "cx", "cy", "k1", "k2"],
                "reason": "geometry and control both constrain the solve"}
    if observable or constrained:
        return {"free": ["focal", "k1"],
                "reason": "partial constraint — free only the terms that drift"}
    if calibration is None:
        return {"free": ["focal", "k1"],
                "reason": "no prior calibration available; partial freedom is the "
                          "least bad option, and flag the survey for checkpoints"}
    return {"free": [], "prior": calibration,
            "reason": "nadir-only with weak control — fix everything"}

The no-calibration branch matters because it is common and the temptation is to fall back on full self-calibration. Partial freedom with a flagged survey is safer: it constrains the terms that cannot absorb a dome while leaving the ones that genuinely vary.

Applying it to the engine

Most engines accept the policy as configuration, and the names differ.

def odm_options(policy: dict) -> dict:
    """Translate a policy into reconstruction engine options."""
    free = set(policy.get("free", []))
    if not free:
        return {"use-fixed-camera-params": True,
                "cameras": policy["prior"]}
    if free == {"focal", "k1"}:
        return {"camera-lens": "brown",
                "optimize-disabled-params": "cx,cy,k2,k3,p1,p2"}
    return {"camera-lens": "brown"}


def verify_applied(reconstruction_cameras: dict, policy: dict,
                   prior: dict | None, *, tol: float = 1e-6) -> dict:
    """Confirm the engine actually honoured the policy.

    Supplying a calibration and fixing it are separate settings in most
    software, and forgetting the second produces a self-calibration with a
    good initial estimate — which looks like success and is not.
    """
    if policy.get("free"):
        return {"checked": False, "note": "parameters were intended to be free"}
    problems = []
    for key in ("focal", "k1", "k2", "cx", "cy"):
        before = (prior or {}).get(key)
        after = reconstruction_cameras.get(key)
        if before is None or after is None:
            continue
        if abs(before - after) > tol:
            problems.append(f"{key} moved from {before:.6f} to {after:.6f} — "
                            "the parameters were not fixed")
    return {"checked": True, "problems": problems, "honoured": not problems}

That verification is worth running on every job with a fixed-intrinsics policy. The failure mode — supplied but not fixed — is silent, produces a plausible result, and is exactly what the policy was meant to prevent.

Choosing an intrinsics policy from the survey's geometry Three rows. Fix the intrinsics when a recent calibration for that camera exists and the survey is a nadir grid, because the survey constrains the lens poorly and letting it try will absorb terrain error into the lens terms. Free the focal length and k1 when no calibration exists but the survey has some convergent geometry, such as oblique passes or varied altitude, which is enough to determine those two and nothing more. Free everything only when the survey itself has calibration-quality geometry, which in practice means a facade or structure survey flown with deliberate convergence. fix a recent calibration exists and the survey is a nadir grid free focal and k1 no calibration, but some convergence — obliques or varied altitude free everything only when the survey itself has calibration-quality geometry The policy follows what the survey's geometry can determine, not what the software offers.

Figure 3 — Three policies, selected by geometry rather than by preference.

Edge-case matrix

Situation Policy Reason
Nadir grid, perimeter control Fix Neither constraint present
Nadir grid, interior control Free focal and k1 Surface is pinned
Cross plus obliques, no control Free focal and k1 Geometry helps, scale does not
Cross plus obliques, good control Free all Best possible case
Corridor survey Fix, or free focal only Cross-geometry is weak by construction
Very small site Fix Too little geometry to separate anything
Camera changed mid-project Per-camera policy Two cameras, two calibrations
RTK positions, no ground control Free focal and k1 Positions constrain scale, not the surface

The RTK row is a common misconception. Accurate camera positions fix the scale and the absolute placement of a survey and do very little to prevent doming, because a dome is a deformation of the surface between the cameras rather than of the camera positions themselves.

Verification snippet

import numpy as np


def compare_policies(results: dict[str, dict]) -> dict:
    """Run the same survey under two policies and compare against checkpoints.

    The comparison that matters is checkpoint RMSE and residual curvature,
    not the reconstruction's own reprojection error — which will always
    favour the policy with more free parameters.
    """
    rows = {}
    for name, r in results.items():
        rows[name] = {
            "reprojection_px": r["reprojection_px"],
            "checkpoint_rmse_m": r["checkpoint_rmse_m"],
            "sag_m": r["sag_m"],
        }
    best = min(rows, key=lambda k: (abs(rows[k]["sag_m"]), rows[k]["checkpoint_rmse_m"]))
    return {"policies": rows, "best_by_checkpoints": best,
            "note": ("the policy with the lower reprojection error is not "
                     "necessarily the better survey")}
Reprojection error against checkpoint accuracy for three policies Three policies compared on the same nadir survey with perimeter control. Full self-calibration gives the lowest reprojection error at zero point three one pixels and the worst checkpoint accuracy at twenty-two centimetres with a large dome. Partial freedom gives zero point four zero pixels and eight centimetres. Fixed intrinsics give the highest reprojection error at zero point five two pixels and the best checkpoint accuracy at four centimetres with almost no dome. A note states that the two measures rank the policies in opposite orders. optimise everything 0.31 px reprojection 22 cm free focal and k1 0.40 px 8 cm fixed from a prior calibration 0.52 px 4 cm reprojection error checkpoint accuracy The two measures rank the three policies in exactly opposite orders.

Figure 2 — Why the engine’s own quality metric is the wrong one to choose a policy by.

Making the policy part of the job definition

A policy applied by whoever processed a survey is a policy that will be applied differently next time. Putting it in the job definition — alongside the survey identifier, the parameters and the control set — makes it an attribute of the work rather than of the operator.

Two fields are enough: the policy name and the calibration reference it depends on. A job that says “fixed, camera SN-4471 calibration of 2026-03-02” is one that can be re-run identically and audited without asking anybody.

def job_intrinsics_block(policy: dict, calibration_record: dict | None) -> dict:
    """The intrinsics section of a job definition."""
    return {
        "policy": "fixed" if not policy.get("free") else
                  ("partial" if set(policy["free"]) == {"focal", "k1"} else "full"),
        "free_parameters": policy.get("free", []),
        "calibration": {
            "camera_serial": (calibration_record or {}).get("camera_serial"),
            "measured_on": (calibration_record or {}).get("measured_on"),
        } if calibration_record else None,
        "reason": policy.get("reason"),
    }

Carrying the reason as well as the decision is a small habit with a real payoff. Six months later the question is never “what policy was used” — that is in the file — but “why”, and a sentence written at the time answers it better than any reconstruction of the reasoning.

A programme that records this consistently also gains something unexpected: a dataset relating policy, geometry and checkpoint accuracy across dozens of surveys, which is far more convincing evidence for the rule at the top of this page than any single comparison.

When to escalate

  • The policy says fix and no calibration exists. Fly the calibration pattern, or accept partial freedom and insist on checkpoints.
  • Two policies give similar checkpoints and different lens parameters. The survey does not constrain the lens; prefer the fixed policy for consistency across the programme.
  • A client requires the engine’s default. Provide it and the checkpoint comparison together. The number is more persuasive than the argument.

Camera Calibration and Lens Models in Python