Skip to content

ds_msp.adapt

Model conversion — re-express a calibrated model's parameters in another camera family. See Convert between models for task recipes and Are two models the same camera? for why this is possible at all.

ds_msp.adapt

Model conversion ("adapter"): convert calibrated params between models.

convert_best

convert_best(source: CameraModel, *, width: int, height: int, candidates: Optional[Sequence[Type[CameraModel]]] = None, target_rms: float = 3.0, n_samples: int = 2000, n_restarts: int = 6, max_fov_deg: Optional[float] = None) -> Tuple[Optional[CameraModel], dict, List[Tuple[Optional[CameraModel], dict]]]

Convert source to the simplest target model meeting target_rms.

Parameters:

Name Type Description Default
source CameraModel

The calibrated source model to convert.

required
width int

Image size for sampling and reporting.

required
height int

Image size for sampling and reporting.

required
candidates sequence of model classes

Capability-ordered ladder to try. Defaults to :func:default_ladder.

None
target_rms float

Reprojection-RMS tolerance in pixels. The first candidate at or under this value is returned.

3.0
n_samples int

Forwarded to :func:convert.

2000
n_restarts int

Forwarded to :func:convert.

2000
max_fov_deg int

Forwarded to :func:convert.

2000

Returns:

Type Description
(model, report, all_results)

model/report for the selected target; all_results is the list of (model, report) for every candidate attempted, in order. The chosen report carries selected/selected_reason keys.

Source code in ds_msp/adapt/autoselect.py
def convert_best(source: CameraModel, *, width: int, height: int,
                 candidates: Optional[Sequence[Type[CameraModel]]] = None,
                 target_rms: float = 3.0, n_samples: int = 2000,
                 n_restarts: int = 6, max_fov_deg: Optional[float] = None,
                 ) -> Tuple[Optional[CameraModel], dict,
                            List[Tuple[Optional[CameraModel], dict]]]:
    """Convert ``source`` to the simplest target model meeting ``target_rms``.

    Parameters
    ----------
    source : CameraModel
        The calibrated source model to convert.
    width, height : int
        Image size for sampling and reporting.
    candidates : sequence of model classes, optional
        Capability-ordered ladder to try. Defaults to :func:`default_ladder`.
    target_rms : float
        Reprojection-RMS tolerance in pixels. The first candidate at or under
        this value is returned.
    n_samples, n_restarts, max_fov_deg
        Forwarded to :func:`convert`.

    Returns
    -------
    (model, report, all_results)
        ``model``/``report`` for the selected target; ``all_results`` is the list
        of ``(model, report)`` for every candidate attempted, in order. The
        chosen ``report`` carries ``selected``/``selected_reason`` keys.
    """
    cands = tuple(candidates) if candidates is not None else default_ladder()
    results: List[Tuple[Optional[CameraModel], dict]] = []
    for cls in cands:
        try:
            model, report = convert(source, cls, width=width, height=height,
                                    n_samples=n_samples, n_restarts=n_restarts,
                                    max_fov_deg=max_fov_deg)
        except Exception as exc:  # a model that cannot be fitted at all is skipped
            results.append((None, {"target_model": cls.name, "error": str(exc),
                                    "rms_px": float("inf")}))
            continue
        results.append((model, report))
        if report["rms_px"] <= target_rms:
            report["selected"] = True
            report["selected_reason"] = (
                f"simplest model with RMS {report['rms_px']:.3f}px <= {target_rms}px")
            return model, report, results

    # Nothing met the tolerance: return the best available, flagged honestly.
    best_model, best_report = min(
        results, key=lambda mr: mr[1].get("rms_px", float("inf")))
    best_report["selected"] = True
    best_report["selected_reason"] = (
        f"target {target_rms}px not met by any candidate; best available "
        f"is {best_report['target_model']} at RMS {best_report['rms_px']:.3f}px")
    return best_model, best_report, results

default_ladder

default_ladder() -> Tuple[Type[CameraModel], ...]

Capability-ordered target models, simplest (fewest params) first.

Source code in ds_msp/adapt/autoselect.py
def default_ladder() -> Tuple[Type[CameraModel], ...]:
    """Capability-ordered target models, simplest (fewest params) first."""
    from ..models import DoubleSphereModel, EUCMModel, OCamModel, UCMModel
    # UCM(5) < DS(6) = EUCM(6) < OCam(10). DS before EUCM keeps the namesake
    # sphere model preferred when a 2-extra-param sphere fit suffices.
    return (UCMModel, DoubleSphereModel, EUCMModel, OCamModel)

reprojection_report

reprojection_report(source, target, width: int, height: int, n_samples: int = 2000, max_fov_deg: Optional[float] = None, gt_params: Optional[ndarray] = None) -> dict

Measure how well target reproduces source over the image.

Returns a dict with rms/max/median pixel error, sample counts, FOV coverage, and (if gt_params given) parameter error. max_fov_deg restricts the evaluated region to match a narrower target (e.g. pinhole/RadTan), so the report reflects the region the target is meant to cover rather than being dominated by unrepresentable peripheral rays.

Source code in ds_msp/adapt/evaluate.py
def reprojection_report(source, target, width: int, height: int,
                        n_samples: int = 2000,
                        max_fov_deg: Optional[float] = None,
                        gt_params: Optional[np.ndarray] = None) -> dict:
    """Measure how well ``target`` reproduces ``source`` over the image.

    Returns a dict with rms/max/median pixel error, sample counts, FOV coverage,
    and (if ``gt_params`` given) parameter error. ``max_fov_deg`` restricts the
    evaluated region to match a narrower target (e.g. pinhole/RadTan), so the
    report reflects the region the target is meant to cover rather than being
    dominated by unrepresentable peripheral rays.
    """
    pixels = sample_image_grid(width, height, n_samples)
    rays, valid = source.unproject(pixels)
    keep = valid & (rays[:, 2] > 1e-6)
    if max_fov_deg is not None:
        ang_all = np.degrees(np.arccos(np.clip(rays[:, 2], -1.0, 1.0)))
        keep &= ang_all <= (max_fov_deg / 2.0)
    pixels_k, rays_k = pixels[keep], rays[keep]

    uv, vt = target.project(rays_k)
    ok = vt
    err = np.linalg.norm(uv[ok] - pixels_k[ok], axis=1)

    ang = np.degrees(np.arccos(np.clip(rays_k[:, 2], -1.0, 1.0)))
    report = {
        "rms_px": float(np.sqrt(np.mean(err ** 2))) if err.size else float("nan"),
        "max_px": float(err.max()) if err.size else float("nan"),
        "median_px": float(np.median(err)) if err.size else float("nan"),
        "n_sampled": int(len(pixels)),
        "n_forward": int(keep.sum()),
        "n_target_valid": int(ok.sum()),
        "fov_covered_deg": float(2.0 * ang.max()) if ang.size else float("nan"),
    }
    if gt_params is not None:
        report["param_error"] = float(np.linalg.norm(np.asarray(gt_params) - target.params))
    return report

sample_image_grid

sample_image_grid(width: int, height: int, n_samples: int = 500) -> np.ndarray

Regular grid of pixel centers, aspect-ratio preserving (FCA-style).

Returns (M, 2) float64 pixels with M ≈ n_samples.

Source code in ds_msp/adapt/sampling.py
def sample_image_grid(width: int, height: int, n_samples: int = 500) -> np.ndarray:
    """Regular grid of pixel centers, aspect-ratio preserving (FCA-style).

    Returns ``(M, 2)`` float64 pixels with ``M ≈ n_samples``.
    """
    nx = max(2, int(round(np.sqrt(n_samples * width / height))))
    ny = max(2, int(round(np.sqrt(n_samples * height / width))))
    cw = width / nx
    ch = height / ny
    xs = (np.arange(nx) + 0.5) * cw
    ys = (np.arange(ny) + 0.5) * ch
    gx, gy = np.meshgrid(xs, ys, indexing="xy")
    return np.stack([gx.ravel(), gy.ravel()], axis=-1).astype(np.float64)