Modules

Planet Ruler API modules organized by functionality.

planet_ruler

Core Modules

planet_ruler.geometry

planet_ruler.image

planet_ruler.observation

planet_ruler.annotate

Manual Limb Annotation Tool for Planet Ruler

planet_ruler.camera

Automatic extraction of camera parameters from smartphone images.

planet_ruler.fit

planet_ruler.uncertainty

planet_ruler.plot

planet_ruler.validation

Validation functions for planet_ruler.

planet_ruler.cli

Command-line interface for planet_ruler

planet_ruler.demo

Geometry Module

Mathematical functions for planetary geometry calculations.

planet_ruler.geometry.horizon_distance(r, h)[source]

Estimate the distance to the horizon (limb) given a height and radius.

Parameters:
  • r (float) – Radius of the body in question.

  • h (float) – Height above surface (units should match radius).

Returns:

Distance in same units as inputs.

Return type:

d (float)

planet_ruler.geometry.limb_camera_angle(r, h)[source]

The angle the camera must tilt in theta_x or theta_y to center the limb. Complement of theta (angle of limb down from the x-y plane).

Parameters:
  • r (float) – Radius of the body in question.

  • h (float) – Height above surface (units should match radius).

Returns:

Angle of camera (radians).

Return type:

theta_c (float)

planet_ruler.geometry.focal_length(w, fov)[source]

The size of the CCD (inferred) based on focal length and field of view.

Parameters:
  • w (float) – detector size (float): Width of CCD (m).

  • fov (float) – Field of view, assuming square (degrees).

Returns:

Focal length of the camera (m).

Return type:

f (float)

planet_ruler.geometry.detector_size(f, fov)[source]

The size of the CCD (inferred) based on focal length and field of view.

Parameters:
  • f (float) – Focal length of the camera (m).

  • radians (# todo really need to pick either degrees or)

  • fov (float) – Field of view, assuming square (degrees).

Returns:

Width of CCD (m).

Return type:

detector size (float)

planet_ruler.geometry.field_of_view(f, w)[source]

The size of the CCD (inferred) based on focal length and field of view.

Parameters:
  • f (float) – Focal length of the camera (m).

  • w (float) – Width of detector (m).

Returns:

Field of view, assuming square (degrees).

Return type:

fov (float)

planet_ruler.geometry.intrinsic_transform(camera_coords, f=1, px=1, py=1, x0=0, y0=0)[source]

Transform from camera coordinates into image coordinates.

Parameters:
  • camera_coords (np.ndarray) – Coordinates of the limb in camera space. Array has Nx4 shape where N is the number of x-axis pixels in the image.

  • f (float) – Focal length of the camera (m).

  • px (float) – The scale of x pixels.

  • py (float) – The scale of y pixels.

  • x0 (float) – The x-axis principle point (should be center of image in pixel coordinates).

  • y0 (float) – The y-axis principle point. (should be center of image in pixel coordinates).

Returns:

Coordinates in image space.

Return type:

pixel_coords (np.ndarray)

planet_ruler.geometry.extrinsic_transform(world_coords, theta_x=0, theta_y=0, theta_z=0, origin_x=0, origin_y=0, origin_z=0)[source]

Transform from world coordinates into camera coordinates. Note that for a limb calculation we will define origin_x/y/z as the camera position – these should all be set to zero.

Parameters:
  • world_coords (np.ndarray) – Coordinates of the limb in the world. Array has Nx4 shape where N is the number of x-axis pixels in the image.

  • theta_x (float) – Rotation around the x (horizontal lateral) axis, AKA pitch — tilts the camera up/down. (radians)

  • theta_y (float) – Rotation around the y (toward-limb) axis, AKA roll. When theta_z=0, acts as a pure phase shift in φ with no effect on the projected arc shape. When theta_z≠0, the z-rotation breaks that symmetry and theta_y has a genuine effect on the arc. (radians)

  • theta_z (float) – Rotation around the z (vertical) axis, AKA yaw. Use theta_z=π for the physically correct ∪-shaped horizon arc (near limb visible, more planet at image center). (radians)

  • origin_x (float) – Horizontal offset from the object in question to the camera (m).

  • origin_y (float) – Distance from the object in question to the camera (m).

  • origin_z (float) – Height difference from the object in question to the camera (m).

Returns:

Coordinates in camera space.

Return type:

camera_coords (np.ndarray)

planet_ruler.geometry.limb_arc_sample(r, n_pix_x, n_pix_y, h=1, f=None, fov=None, w=None, x0=0, y0=0, theta_x=0, theta_y=0, theta_z=0, origin_x=0, origin_y=0, origin_z=0, return_full=False, num_sample=5000)[source]

Calculate the limb orientation in an image given the physical parameters of the system.

Parameters:
  • n_pix_x (int) – Width of image (pixels).

  • n_pix_y (int) – Height of image (pixels).

  • r (float) – Radius of the body in question.

  • h (float) – Height above surface (units should match radius).

  • f (float) – Focal length of the camera (m).

  • fov (float) – Field of view, assuming square (degrees).

  • w (float) – detector size (float): Width of CCD (m).

  • x0 (float) – The x-axis principle point.

  • y0 (float) – The y-axis principle point.

  • theta_x (float) – Rotation around the x (horizontal lateral) axis, AKA pitch — tilts the camera up/down. (radians)

  • theta_y (float) – Rotation around the y (toward-limb) axis, AKA roll. When theta_z=0, acts as a pure phase shift in φ with no effect on the projected arc shape. When theta_z≠0, the z-rotation breaks that symmetry and theta_y has a genuine effect on the arc. (radians)

  • theta_z (float) – Rotation around the z (vertical) axis, AKA yaw. Use theta_z=π for the physically correct ∪-shaped horizon arc (near limb visible, more planet at image center). (radians)

  • origin_x (float) – Horizontal offset from the object in question to the camera (m).

  • origin_y (float) – Distance from the object in question to the camera (m).

  • origin_z (float) – Height difference from the object in question to the camera (m).

  • return_full (bool) – Return both the x and y coordinates of the limb in camera space. Note these will not be interpolated back on to the pixel grid.

  • num_sample (int) – The number of points sampled from the simulated limb – will be interpolated onto pixel grid. [default 1000]

Return type:

ndarray

planet_ruler.geometry.get_rotation_matrix(theta_x, theta_y, theta_z)[source]

Compute combined rotation matrix from Euler angles. Extracted from extrinsic_transform for reuse.

Returns:

3x3 rotation matrix

Return type:

R

planet_ruler.geometry.limb_arc(r, n_pix_x, n_pix_y, h=1, f=None, fov=None, w=None, x0=0, y0=0, theta_x=0, theta_y=0, theta_z=0, origin_x=0, origin_y=0, origin_z=0, return_full=False, x_coords=None, **kwargs)[source]

Calculate limb position analytically at each pixel x-coordinate.

No sampling or interpolation - directly solves for phi at each column. This eliminates edge artifacts and is sometimes faster than sampling methods.

Mathematical approach: 1. Limb is parameterized by angle phi around circle 2. For each x_pixel, solve: x_pixel = f(phi) for phi 3. This reduces to: a·cos(phi) + b·sin(phi) = c 4. Standard analytical solution exists!

Parameters:
  • n_pix_x (int) – Width of image (pixels).

  • n_pix_y (int) – Height of image (pixels).

  • r (float) – Radius of the body in question.

  • h (float) – Height above surface (units should match radius).

  • f (float) – Focal length of the camera (m).

  • fov (float) – Field of view, assuming square (degrees).

  • w (float) – detector size (float): Width of CCD (m).

  • x0 (float) – The x-axis principle point.

  • y0 (float) – The y-axis principle point.

  • theta_x (float) – Rotation around the x (horizontal lateral) axis, AKA pitch — tilts the camera up/down. (radians)

  • theta_y (float) – Rotation around the y (toward-limb) axis, AKA roll. When theta_z=0, acts as a pure phase shift in φ with no effect on the projected arc shape. When theta_z≠0, the z-rotation breaks that symmetry and theta_y has a genuine effect on the arc. (radians)

  • theta_z (float) – Rotation around the z (vertical) axis, AKA yaw. Use theta_z=π for the physically correct ∪-shaped horizon arc (near limb visible, more planet at image center). (radians)

  • origin_x (float) – Horizontal offset from the object in question to the camera (m).

  • origin_y (float) – Distance from the object in question to the camera (m).

  • origin_z (float) – Height difference from the object in question to the camera (m).

  • return_full (bool) – Return both the x and y coordinates of the limb in camera space. Note these will not be interpolated back on to the pixel grid.

  • x_coords (Optional[ndarray]) – Optional array of x-coordinates to compute (default: all pixels). For sparse computation (e.g., manual annotation), pass only the x-coordinates where you have data. Dramatically speeds up fitting when only a few points are annotated.

Returns:

Array of y-coordinates for each x-pixel column

Length matches len(x_coords) if provided, else n_pix_x

Return type:

y_pixel

planet_ruler.geometry.limb_arc_sagitta(u, theta_x, f_px, r, h)[source]

Exact projected sagitta of the planetary limb arc at pixel offset u.

Derivation (perspective geometry, theta_y=0, theta_z=pi for ∪-arc):

The camera is at altitude h above a sphere of radius r. In camera coordinates the visible horizon is the set of tangent points satisfying X·r̂ = r²/(r+h). After rotation R_x(theta_x)·R_z(pi) the horizon circle projects as a conic; solving the x-pixel equation for the azimuth angle phi and back-substituting for the y-pixel yields the closed form below.

The formula is verified to machine precision against limb_arc() for theta_x ∈ {-alpha, 0, alpha, 2*alpha} and is invariant to theta_y (verified to 2.7e-7 px residuals for theta_y ∈ {0, 0.01, 0.05, 0.1}).

At theta_x=0 this reduces exactly to

s(u) = kappa * (sqrt(f_px**2 + u**2) - f_px)

where kappa = sqrt(h*(2r+h)) / r = 1/K. Consequently, the OLS fit s = s0 - c*A(u) (A = sqrt(f²+u²) - f) recovers K = 1/|c| with zero residual — the fundamental reason the hyperbola model is exact at theta_x=0.

Parameters:
  • u (ndarray) – Horizontal pixel offsets from the image centre (u = x - x0).

  • theta_x (float) – Camera tilt in radians (0 = horizontal, positive = looking down toward the horizon).

  • f_px (float) – Focal length in pixels (f_mm / sensor_width_mm * n_pix_x).

  • r (float) – Planetary radius [m].

  • h (float) – Camera altitude above surface [m].

Return type:

ndarray

Returns:

Sagitta at each u [pixels]. Positive values are below the arc apex for ∪-arcs (theta_z=pi).

Exact formulas:

kappa = sqrt(h * (2*r + h)) / r g(u) = sqrt(f_px**2 + u**2 * (cos(theta_x)**2 - kappa**2*sin(theta_x)**2)) s(u) = (f_px**2 + u**2*cos(theta_x)*(cos(theta_x) + kappa*sin(theta_x)) - f_px*g(u))

/ ((f_px*sin(theta_x) + cos(theta_x)*g(u)/kappa) * (kappa*sin(theta_x) + cos(theta_x)))

Image Processing Module

Computer vision and image analysis functions.

planet_ruler.image.load_image(filepath)[source]

Load a 3 or 4-channel image from filepath into an array.

Parameters:

filepath (str) – Path to image file.

Return type:

ndarray

Returns:

image array (np.ndarray)

planet_ruler.image.directional_gradient_blur(image, sigma_base=2.0, streak_length=20, decay_rate=0.2, normalize_gradients=True)[source]

Blur along gradient directions to create smooth ‘streaks’ toward edges.

For each pixel, follow its gradient direction outward, averaging pixels along that ray with exponential decay. This: - Smooths noisy/weak gradients (striations with inconsistent directions) - Strengthens coherent gradients (limb where all gradients align) - Creates smooth ‘valleys’ guiding optimizer toward strong edges

Parameters:
  • image (np.ndarray) – Input image (H x W or H x W x 3)

  • sigma_base (float) – Initial gradient smoothing for direction estimation

  • streak_length (int) – How far to follow gradient direction (pixels)

  • decay_rate (float) – Exponential decay rate (higher = faster decay)

  • normalize_gradients (bool) –

    • True: Use unit vectors (direction only) - RECOMMENDED All pixels sample same distance regardless of gradient strength. Consistent, predictable behavior.

    • False: Use full gradient magnitude as direction Strong gradients sample further. Can create artifacts.

Returns:

Blurred gradient magnitude field grad_angle (np.ndarray): Gradient angle field

Return type:

blurred_grad_mag (np.ndarray)

Note: This is UNI-directional (samples in one direction only).

Use bidirectional_gradient_blur() to preserve peak locations.

planet_ruler.image.bilinear_interpolate(array, y, x)[source]

Bilinear interpolation for 2D array at non-integer coordinates.

Parameters:
  • array – 2D array to sample from

  • y – Arrays of y and x coordinates (can be non-integer)

  • x – Arrays of y and x coordinates (can be non-integer)

Returns:

Interpolated values at (y, x) positions

planet_ruler.image.bidirectional_gradient_blur(image, sigma_base=2.0, streak_length=20, decay_rate=0.2, normalize_gradients=True)[source]

Blur in BOTH directions along gradient (forward and backward).

This creates symmetric streaks on both sides of edges, preserving the location of gradient maxima while smoothing the field.

Parameters:
  • image – Input image

  • sigma_base – Initial smoothing for gradient estimation

  • streak_length – How far to sample in each direction

  • decay_rate – Exponential decay (higher = faster falloff)

  • normalize_gradients – Use unit vectors (direction only) vs full magnitude

Returns:

Smoothed gradient magnitude field grad_angle: Original gradient angle field

Return type:

blurred_mag

planet_ruler.image.gradient_break(im_arr, log=False, y_min=0, y_max=-1, window_length=None, polyorder=1, deriv=0, delta=1)[source]

Scan each vertical line of an image for the maximum change in brightness gradient – usually corresponds to a horizon.

Parameters:
  • im_arr (np.ndarray) – Image array.

  • log (bool) – Use the log(gradient). Sometimes good for smoothing.

  • y_min (int) – Minimum y-position to consider.

  • y_max (int) – Maximum y-position to consider.

  • window_length (int) – Width of window to apply smoothing for each vertical. Larger means less noise but less sensitivity.

  • polyorder (int) – Polynomial order for smoothing.

  • deriv (int) – Derivative level for smoothing.

  • delta (int) – Delta for smoothing.

Returns:

image array (np.ndarray)

class planet_ruler.image.MaskSegmenter(image, method='sam', downsample_factor=1, interactive=True, **backend_kwargs)[source]

Bases: object

Method-agnostic mask-based segmentation.

Supports pluggable backends (SAM, custom algorithms, etc.) with optional downsampling and interactive classification.

__init__(image, method='sam', downsample_factor=1, interactive=True, **backend_kwargs)[source]

Initialize mask segmenter.

Parameters:
  • image (ndarray) – Input image (H x W x 3)

  • method (str) – Backend method (‘sam’ or ‘custom’)

  • downsample_factor (int) – Downsample factor for speed (1 = no downsampling)

  • interactive (bool) – Use interactive GUI for classification

  • **backend_kwargs – Backend-specific arguments

segment()[source]

Run segmentation pipeline.

Return type:

ndarray

Returns:

Limb coordinates (1D array of y-values)

class planet_ruler.image.SegmentationBackend[source]

Bases: object

Base class for segmentation backends.

segment(image)[source]

Segment image into masks.

Parameters:

image (ndarray) – Input image (H x W x 3)

Return type:

list

Returns:

List of mask objects (format can vary by backend)

class planet_ruler.image.SAMBackend(model_size='vit_b')[source]

Bases: SegmentationBackend

Segment Anything Model backend.

__init__(model_size='vit_b')[source]

Initialize SAM backend.

Parameters:

model_size (str) – SAM model variant (‘vit_b’, ‘vit_l’, ‘vit_h’)

segment(image)[source]

Run SAM on image.

Return type:

list

class planet_ruler.image.CustomBackend(segment_fn)[source]

Bases: SegmentationBackend

Custom user-provided segmentation backend.

__init__(segment_fn)[source]

Initialize with custom segmentation function.

Parameters:

segment_fn – Function that takes image and returns list of masks

segment(image)[source]

Run custom segmentation function.

Return type:

list

planet_ruler.image.smooth_limb(y, method='rolling-median', window_length=50, polyorder=1, deriv=0, delta=1)[source]

Smooth the limb position values.

Parameters:
  • y (np.ndarray) – Y-locations of the string for each column.

  • method (str) – Smoothing method. Must be one of [‘bin-interpolate’, ‘savgol’, ‘rolling-mean’, ‘rolling-median’].

  • window_length (int) – The length of the filter window (i.e., the number of coefficients). If mode is ‘interp’, window_length must be less than or equal to the size of x.

  • polyorder (float) – The order of the polynomial used to fit the samples. polyorder must be less than window_length.

  • deriv (int) – The order of the derivative to compute. This must be a non-negative integer. The default is 0, which means to filter the data without differentiating.

  • delta (int) – The spacing of the samples to which the filter will be applied. This is only used if deriv > 0. Default is 1.0.

Returns:

Y-locations of the smoothed string for each column.

Return type:

position (np.ndarray)

planet_ruler.image.fill_nans(limb)[source]

Fill NaNs for the limb position values.

Parameters:

limb (np.ndarray) – Y-locations of the limb on the image.

Returns:

Y-locations of the limb on the image.

Return type:

limb (np.ndarray)

planet_ruler.image.gradient_field(image, kernel_smoothing=5.0, directional_smoothing=50, directional_decay_rate=0.15)[source]

Pre-compute gradient field AND its derivatives for fast sub-pixel interpolation. Uses directional blur for enhanced edge detection.

Parameters:
  • image (ndarray) – Input image

  • kernel_smoothing (float) – Sigma for initial gradient direction estimation

  • directional_smoothing (int) – Distance to sample in each direction (±pixels). Set to 0 to bypass directional smoothing entirely.

  • directional_decay_rate (float) – Exponential decay rate for directional blur

Returns:

Dictionary containing gradient field components:
  • grad_mag: Gradient magnitude array

  • grad_angle: Gradient angle array

  • grad_sin: Sin of gradient angle (for interpolation)

  • grad_cos: Cos of gradient angle (for interpolation)

  • grad_mag_dy, grad_mag_dx: Derivatives of gradient magnitude

  • grad_sin_dy, grad_sin_dx: Derivatives of sine component

  • grad_cos_dy, grad_cos_dx: Derivatives of cosine component

  • image_height, image_width: Dimensions

Return type:

dict

Observation Module

High-level observation classes and workflows.

class planet_ruler.observation.PlanetObservation(image_filepath)[source]

Bases: object

Base class for planet observations.

Parameters:

image_filepath (str) – Path to image file.

__init__(image_filepath)[source]
plot(gradient=False, show=True)[source]

Display the observation and all current features.

Parameters:
  • gradient (bool) – Show the image gradient instead of the raw image.

  • show (bool) – Show – useful as False if intending to add more to the plot before showing.

Return type:

None

crop_image(update_parameters=True)[source]

Interactively crop the observation image with automatic parameter scaling.

Opens a GUI tool allowing the user to select a rectangular region to crop. If the observation has camera parameters (e.g., LimbObservation), they are automatically scaled to match the cropped region.

Note: The principal point may end up outside the cropped region bounds (negative coordinates). This is geometrically valid - it means the camera’s optical axis was pointing at a location not visible in the cropped image.

Parameters:

update_parameters (bool) – If True and parameters exist, scale them for the crop

Returns:

For method chaining

Return type:

self

Example

>>> obs = LimbObservation("airplane.jpg", "config.yaml")
>>> obs.crop_image()  # Opens interactive crop tool
>>> obs.detect_limb()
>>> obs.fit_arc()

Notes

  • Works for any PlanetObservation subclass

  • For LimbObservation: automatically scales detector_size, principal_point, etc.

  • Principal point can be outside cropped bounds (mathematically valid)

class planet_ruler.observation.LimbObservation(image_filepath, fit_config, limb_detection='manual', minimizer='differential-evolution')[source]

Bases: PlanetObservation

Observation of a planet’s limb (horizon).

Parameters:
  • image_filepath (str) – Path to image file.

  • fit_config (str) – Path to fit config file.

  • limb_detection (str) – Method to locate the limb in the image.

  • minimizer (str) – Choice of minimizer. Supports ‘differential-evolution’, ‘dual-annealing’, and ‘basinhopping’.

__init__(image_filepath, fit_config, limb_detection='manual', minimizer='differential-evolution')[source]
analyze(detect_limb_kwargs=None, fit_method='arc', fit_method_kwargs=None, fit_stages=None)[source]

Perform complete limb analysis: detection + fitting in one call.

Parameters:
  • detect_limb_kwargs (Optional[dict]) – Optional kwargs for detect_limb().

  • fit_method (str) – Single-stage method – “arc” (default), “gradient”, or “sagitta”. Ignored when fit_stages is provided.

  • fit_method_kwargs (Optional[dict]) – Optional kwargs for the chosen fit method.

  • fit_stages (Optional[List[dict]]) – If provided, run fit_limb(fit_stages) (multi-stage orchestrator).

Returns:

For method chaining.

Return type:

self

property radius_km: float

Get the fitted planetary radius in kilometers.

Returns:

Planetary radius in km, or 0 if not fitted

Return type:

float

property altitude_km: float

Get the observer altitude in kilometers.

Returns:

Observer altitude in km, or 0 if not fitted

Return type:

float

property focal_length_mm: float

Get the camera focal length in millimeters.

Returns:

Focal length in mm, or 0 if not fitted

Return type:

float

property radius_uncertainty: float

Get parameter uncertainty for radius.

Automatically selects best method based on minimizer: - differential_evolution: Uses population spread (fast, exact) - dual_annealing/basinhopping: Uses Hessian approximation (fast, approximate)

Returns:

Radius uncertainty in km (1-sigma), or 0 if not available

Return type:

float

parameter_uncertainty(parameter, method='auto', scale_factor=1.0, confidence_level=0.68, **kwargs)[source]

Get uncertainty for any fitted parameter.

Parameters:
  • parameter (str) – Parameter name (e.g., ‘r’, ‘h’, ‘f’, ‘theta_x’)

  • method (Literal['auto', 'hessian', 'profile', 'bootstrap']) – Uncertainty method - ‘auto’: Choose based on minimizer (recommended) - ‘hessian’: Fast Hessian approximation - ‘profile’: Slow but accurate profile likelihood - ‘bootstrap’: Multiple fits (very slow)

  • scale_factor (float) – Scale result (e.g., 1000.0 for m→km)

  • confidence_level (float) – Confidence level (0.68=1σ, 0.95=2σ)

  • **kwargs – Additional arguments passed to uncertainty calculator

Return type:

Dict

Returns:

dict with ‘uncertainty’, ‘method’, ‘confidence_level’, ‘additional_info’

Examples

# Radius uncertainty in km (1-sigma) obs.parameter_uncertainty(‘r’, scale_factor=1000.0)

# Altitude uncertainty in km (2-sigma / 95% CI) obs.parameter_uncertainty(‘h’, scale_factor=1000.0, confidence_level=0.95)

# Focal length uncertainty in mm (using profile likelihood) obs.parameter_uncertainty(‘f’, scale_factor=1000.0, method=’profile’)

property methods: list

Fit methods applied, in order.

property minimizers: list

Minimizers used per stage (None for sagitta).

reset_params()[source]

Restore init_parameter_values to the original loaded values (cold start).

Return type:

LimbObservation

plot_3d(**kwargs)[source]

Create 3D visualization of the planetary geometry.

Parameters:

**kwargs – Arguments passed to plot_3d_solution

Return type:

None

load_fit_config(fit_config)[source]

Load the fit configuration from file, setting all parameters to their initial values. Missing values are filled with defaults.

Parameters:

fit_config (str or dict) – Path to configuration file.

Return type:

None

register_limb(limb)[source]

Register a detected limb.

Parameters:

limb (np.ndarray) – Limb vector (y pixel coordinates).

Return type:

LimbObservation

detect_limb(detection_method=None, log=False, y_min=0, y_max=-1, window_length=501, polyorder=1, deriv=0, delta=1, segmentation_method='sam', downsample_factor=1, interactive=True, **segmentation_kwargs)[source]

Use the instance-defined method to find the limb in our observation. Kwargs are passed to the method.

Parameters:
  • detection_method (literal) –

    Detection method. Must be one of
    • manual

    • gradient-break

    • gradient-field

    • segmentation

    Default (None) uses the class attribute self.limb_detection.

  • log (bool) – Use the log(gradient). Sometimes good for smoothing.

  • y_min (int) – Minimum y-position to consider.

  • y_max (int) – Maximum y-position to consider.

  • window_length (int) – Width of window to apply smoothing for each vertical. Larger means less noise but less sensitivity.

  • polyorder (int) – Polynomial order for smoothing.

  • deriv (int) – Derivative level for smoothing.

  • delta (int) – Delta for smoothing.

  • segmentation_method (str) – Model used for segmentation. Must be one of [‘sam’].

  • downsample_factor (int) – Downsampling used for segmentation.

  • interactive (bool) – Prompts user to verify segmentation via annotation tool.

  • segmentation_kwargs (dict) – Kwargs passed to segmentation engine.

Return type:

LimbObservation

smooth_limb(fill_nan=True, **kwargs)[source]

Apply the smooth_limb function to current observation.

Parameters:

fill_nan (bool) – Fill any NaNs in the limb.

Return type:

None

fit_sagitta(n_sigma=2.0, bias_correct=False, uncertainty='both')[source]

Estimate planetary radius using the sagitta (arc-height) method.

Runs SagittaFitter: a 2-D L-BFGS-B optimizer over (kappa, theta_x) using the exact projected-sagitta formula. Updates self.init_parameter_values and self.parameter_limits so that a subsequent fit_arc/fit_gradient stage warm-starts from the result automatically.

Parameters:
  • n_sigma (float) – Radius bound width in sigma units.

  • bias_correct (bool) – If True and y0 is provided, correct for camera tilt bias using the apex y-offset to estimate theta_x, then refine kappa via 1-D minimization.

  • uncertainty (str) – Bound-width method — “ols” (cheap, single pass), “jackknife” (leave-one-out), or “both” (quadrature sum, default).

Returns:

For method chaining.

Return type:

self

fit_arc(loss_function='l2', minimizer=None, minimizer_preset='balanced', minimizer_kwargs=None, max_iter=15000, seed=0, verbose=False, n_jobs=1, _dashboard=None)[source]

Fit the planet limb arc using a pixel-space cost function.

Requires a detected limb (call detect_limb() first, or register_limb()). Reads self.init_parameter_values as the initial guess and writes fitted free-parameter values back so a chained call warm-starts automatically.

Parameters:
  • loss_function (Literal['l2', 'l1', 'log-l1']) – “l2” (default), “l1”, or “log-l1”.

  • minimizer (Optional[Literal['differential-evolution', 'dual-annealing', 'basinhopping', 'scipy-minimize', 'shgo']]) – Minimizer name. Defaults to self.minimizer.

  • minimizer_preset (Literal['fast', 'balanced', 'robust', 'super_robust', 'scipy-default']) – Preset config.

  • minimizer_kwargs (Optional[Dict]) – Override specific minimizer kwargs.

  • max_iter (int) – Maximum iterations.

  • seed (int) – Random seed.

  • verbose (bool) – Print progress.

  • n_jobs (int) – Parallel workers. Effective only for differential-evolution and shgo; emits a UserWarning for other minimizers.

  • _dashboard – Internal — FitDashboard instance passed by fit_limb.

Returns:

For method chaining.

Return type:

self

fit_gradient(resolution_stages=None, minimizer=None, minimizer_preset='balanced', minimizer_kwargs=None, image_smoothing=None, kernel_smoothing=5.0, directional_smoothing=50, directional_decay_rate=0.15, prefer_direction=None, max_iter=15000, max_iter_per_stage=None, seed=0, verbose=False, n_jobs=1, _dashboard=None)[source]

Fit using gradient field alignment, optionally with coarse-to-fine stages.

Does not require a pre-detected limb; operates directly on the image. Multi-resolution stages warm-start from the previous stage automatically.

Parameters:
  • resolution_stages (Union[List[int], Literal['auto'], None]) – Downsampling factors, e.g. [4, 2, 1] (coarse→fine). None → single full-resolution stage. “auto” → inferred from image size.

  • minimizer (Optional[Literal['differential-evolution', 'dual-annealing', 'basinhopping', 'scipy-minimize', 'shgo']]) – Minimizer name. Defaults to self.minimizer.

  • minimizer_preset (Literal['fast', 'balanced', 'robust', 'super_robust', 'scipy-default']) – Preset config.

  • minimizer_kwargs (Optional[Dict]) – Override specific minimizer kwargs.

  • image_smoothing (Optional[float]) – Gaussian blur sigma applied before optimisation (removes high-frequency artifacts; original image restored after).

  • kernel_smoothing (float) – Gradient field kernel size.

  • directional_smoothing (int) – Directional sampling distance along gradients.

  • directional_decay_rate (float) – Exponential decay for directional samples.

  • prefer_direction (Optional[Literal['up', 'down']]) – “up” (dark sky / bright planet), “down”, or None.

  • max_iter (int) – Total maximum iterations (split across stages if multi-res).

  • max_iter_per_stage (Optional[List[int]]) – Explicit per-stage iteration budget.

  • seed (int) – Random seed.

  • verbose (bool) – Print progress.

  • n_jobs (int) – Parallel workers. Effective only for differential-evolution and shgo; emits a UserWarning for other minimizers.

Returns:

For method chaining.

Return type:

self

fit_limb(stages, dashboard=False, dashboard_kwargs=None, target_planet='earth', n_jobs=1)[source]

Run a multi-stage fit by sequentially calling fit_sagitta / fit_arc / fit_gradient.

Resets self.stage_results before running so the list reflects only this call. Individual fit_* calls accumulate into stage_results without resetting.

Parameters:
  • stages (List[dict]) – Ordered list of stage dicts, each with a “method” key (“sagitta”, “arc”, or “gradient”) plus kwargs for that method.

  • dashboard (bool) – Show a live FitDashboard during optimization.

  • dashboard_kwargs (Optional[Dict]) – Extra kwargs forwarded to FitDashboard.__init__.

  • target_planet (str) – Planet name for dashboard reference radius.

  • n_jobs (int) – Parallel workers forwarded to each arc/gradient stage. Effective only for differential-evolution and shgo.

Returns:

For method chaining.

Return type:

self

Example:

obs.fit_limb([
    {"method": "sagitta", "n_sigma": 2.0},
    {"method": "arc", "minimizer": "basinhopping",
     "minimizer_preset": "robust"},
])
save_limb(filepath)[source]

Save the detected limb position as a numpy array.

Parameters:

filepath (str) – Path to save file.

Return type:

None

load_limb(filepath)[source]

Load the detected limb position from a numpy array.

Parameters:

filepath (str) – Path to save file.

Return type:

None

planet_ruler.observation.package_results(observation)[source]

Consolidate the results of a fit to see final vs. initial values.

Parameters:

observation (object) – Instance of LimbObservation (must have used differential evolution minimizer).

Returns:

DataFrame of results including
  • fit value

  • initial value

  • parameter

Return type:

results (pd.DataFrame)

Annotation Module

Interactive manual limb detection and annotation.

Manual Limb Annotation Tool for Planet Ruler

Allows users to click points on a horizon image and generate a sparse target for fitting with the existing planet_ruler pipeline.

class planet_ruler.annotate.ToolTip(widget, text)[source]

Bases: object

Simple tooltip that appears on hover.

__init__(widget, text)[source]
show_tip(event=None)[source]
hide_tip(event=None)[source]
planet_ruler.annotate.create_tooltip(widget, text)[source]

Helper function to create tooltip for a widget.

class planet_ruler.annotate.TkLimbAnnotator(image_path, image=None, initial_stretch=1.0, initial_zoom=None, output_dir=None)[source]

Bases: object

Tkinter-based interactive tool for manually annotating planet limbs.

Features: - Zoom with scroll wheel (fit large images in window) - Vertical stretch buttons (stretch pixels vertically for precision) - Scrollable canvas for navigating - Click to add points, right-click to undo - Save/load points to JSON - Generate sparse target array for CostFunction

__init__(image_path, image=None, initial_stretch=1.0, initial_zoom=None, output_dir=None)[source]

Initialize the annotation tool.

Parameters:
  • image_path (str) – Path to the image to annotate

  • image (np.ndarray) – Optionally the already loaded image

  • initial_stretch (float) – Initial vertical stretch factor

  • initial_zoom (float) – Initial zoom level (None = auto-fit)

  • output_dir (str or Path, optional) – Directory for saved JSON files. Defaults to the same directory as image_path.

create_widgets()[source]

Create all UI widgets.

auto_fit_zoom()[source]

Automatically set zoom to fit image in window.

set_zoom(zoom)[source]

Set absolute zoom level.

adjust_zoom(factor)[source]

Adjust zoom by a multiplicative factor.

on_scroll_zoom(event)[source]

Handle scroll wheel for zooming.

set_stretch(stretch)[source]

Set absolute stretch level.

adjust_stretch(delta)[source]

Adjust stretch by an additive amount.

update_stretched_image()[source]

Update the displayed image with current zoom and stretch.

redraw_points()[source]

Redraw all annotation points at current zoom and stretch.

on_left_click(event)[source]

Add a point at click location.

on_right_click(event)[source]

Undo last point.

clear_all()[source]

Clear all points.

update_status()[source]

Update status text.

get_status_text()[source]

Generate status text.

generate_target()[source]

Generate sparse target array.

save_points()[source]

Save points to JSON, prompting user to confirm or edit the save path.

load_points()[source]

Load points from JSON.

get_target()[source]

Get the current sparse target array.

run()[source]

Start the application.

class planet_ruler.annotate.TkMaskSelector(image, masks, initial_zoom=None)[source]

Bases: object

Interactive mask classification tool for segmentation results.

Works with ANY segmentation method - completely backend-agnostic. Allows users to classify masks as ‘planet’, ‘sky’, or ‘exclude’ for horizon detection. Aligns with Planet Ruler’s educational philosophy of transparency over black-box automation.

Parameters:
  • image (ndarray) – Original image array (H x W x 3)

  • masks (list) – List of masks in any of these formats: - List of np.ndarray (boolean H x W arrays) - List of dicts with ‘mask’ or ‘segmentation’ key - Mixed formats are OK

  • initial_zoom (Optional[float]) – Initial zoom level (None = auto-fit to window)

__init__(image, masks, initial_zoom=None)[source]
update_canvas()[source]

Update canvas with current overlay at current zoom level.

update_mask_list()[source]

Update the mask listbox with text labels.

on_listbox_select(event)[source]

Handle mask selection from listbox.

on_canvas_click(event)[source]

Handle click on canvas to select mask.

on_key_press(event)[source]

Handle keyboard shortcuts.

start_pan(event)[source]

Start panning.

do_pan(event)[source]

Pan the canvas.

end_pan(event)[source]

End panning.

on_mousewheel(event)[source]

Zoom with mouse wheel.

reset_classifications()[source]

Reset all to exclude except first two.

finish()[source]

Close window - nuclear option for Jupyter compatibility.

run()[source]

Run the interactive selector with proper cleanup.

get_classified_masks()[source]

Return dictionary of classified masks with original mask objects.

planet_ruler.annotate.load_limb_points_from_json(json_path, return_metadata=False)[source]

Load limb points from JSON and convert to sparse target array.

Parameters:
  • json_path (str) – Path to JSON file saved by TkLimbAnnotator

  • return_metadata (bool) – If True, return (target, metadata) tuple

Returns:

Sparse target array with limb y-coordinates tuple: (target, metadata) if return_metadata=True

Return type:

np.ndarray

Example

>>> # Simple usage - just get the target array
>>> target = load_limb_points_from_json("image_limb_points.json")
>>>
>>> # Use with LimbObservation
>>> from planet_ruler.observation import LimbObservation
>>> obs = LimbObservation("image.jpg", "config.yaml")
>>> target = load_limb_points_from_json("image_limb_points.json")
>>> obs.register_limb(target)
>>> obs.fit_limb()
>>>
>>> # Get metadata too (for validation)
>>> target, metadata = load_limb_points_from_json(
...     "image_limb_points.json",
...     return_metadata=True
... )
>>> print(f"Loaded {metadata['n_points']} points from {metadata['image_path']}")

Camera Module

Automatic camera parameter extraction from EXIF data.

Automatic extraction of camera parameters from smartphone images. Uses EXIF data and a phone camera database.

planet_ruler.camera.extract_exif(image_path)[source]

Extract EXIF data from image.

Return type:

Dict

planet_ruler.camera.get_camera_model(exif_data)[source]

Extract camera model from EXIF data.

Return type:

Optional[str]

planet_ruler.camera.get_aperture(exif_data)[source]

Extract aperture (f-number) from EXIF data.

Parameters:

exif_data (Dict) – EXIF dictionary

Return type:

Optional[float]

Returns:

Aperture as float (e.g., 2.4 for f/2.4), or None if not found

planet_ruler.camera.match_camera_module(camera_data, focal_length_mm, aperture=None)[source]

Match specific camera module for multi-camera phones.

Uses focal length as primary matcher, aperture as secondary confirmation. Falls back to main camera if no match found.

Parameters:
  • camera_data (Dict) – Camera entry from CAMERA_DB

  • focal_length_mm (float) – Actual focal length from EXIF

  • aperture (Optional[float]) – F-number from EXIF (optional)

Return type:

Dict

Returns:

Camera module dict with sensor_width, sensor_height, etc.

planet_ruler.camera.get_focal_length_mm(exif_data)[source]

Extract focal length in mm from EXIF.

Return type:

Optional[float]

planet_ruler.camera.get_focal_length_35mm_equiv(exif_data)[source]

Get 35mm equivalent focal length (more reliable for phones).

Return type:

Optional[float]

planet_ruler.camera.get_image_dimensions(image_path)[source]

Get image width and height in pixels.

Return type:

Tuple[int, int]

planet_ruler.camera.get_image_orientation_from_exif(exif_data=None)[source]

Determine orientation from EXIF Orientation tag only.

Parameters:

exif_data (Optional[Dict]) – Pre-extracted EXIF data

Return type:

Optional[str]

Returns:

‘portrait’, ‘landscape’, or None if can’t determine

planet_ruler.camera.get_image_orientation_from_dimensions(width, height)[source]

Determine orientation from image dimensions (heuristic).

Parameters:
  • width (int) – Image width in pixels

  • height (int) – Image height in pixels

Return type:

str

Returns:

‘portrait’ or ‘landscape’

planet_ruler.camera.apply_orientation_correction(params, exif_data=None)[source]

Apply orientation correction to camera parameters.

This modifies params in-place and returns it. Call this RIGHT BEFORE each return statement in extract_camera_parameters().

Parameters:
  • params (Dict) – Camera parameters dict (must have image_width_px, image_height_px)

  • exif_data (Optional[Dict]) – Optional pre-extracted EXIF data

Returns:

Modified parameters (same object, modified in-place)

Return type:

params

planet_ruler.camera.calculate_sensor_dimensions(focal_length_mm, focal_length_35mm)[source]

Calculate sensor dimensions from focal length ratio. Uses the relationship: focal_length_mm / sensor_width = focal_length_35mm / 36mm

Return type:

Tuple[float, float]

planet_ruler.camera.get_sensor_statistics_by_type(camera_type)[source]

Calculate sensor dimension statistics for a given camera type. Returns median, min, and max for sensor width and height.

Return type:

Optional[Dict]

planet_ruler.camera.infer_camera_type(exif_data)[source]

Try to infer camera type from EXIF data even if exact model unknown.

Return type:

Optional[str]

planet_ruler.camera.extract_camera_parameters(image_path)[source]

Automatically extract all camera parameters from any camera image. Handles phones, point-and-shoot cameras, DSLRs, mirrorless, etc.

Returns:

Camera parameters including:
  • focal_length_mm: focal length in millimeters

  • sensor_width_mm: sensor width in millimeters

  • sensor_height_mm: sensor height in millimeters

  • sensor_width_min: minimum sensor width (if using type statistics)

  • sensor_width_max: maximum sensor width (if using type statistics)

  • image_width_px: image width in pixels

  • image_height_px: image height in pixels

  • camera_model: detected camera model

  • camera_type: ‘phone’, ‘compact’, ‘dslr’, ‘mirrorless’, or ‘unknown’

  • confidence: ‘high’, ‘medium’, or ‘low’

Return type:

dict

planet_ruler.camera.get_gps_altitude(image_path)[source]

Extract GPS altitude from EXIF data if available.

Returns:

Altitude in meters, or None if not available

Return type:

float

planet_ruler.camera.sample_param_from_bounds(lo, hi, ref=None, perturbation_factor=1.0, seed=None)[source]

Sample a parameter initial value uniformly from a sub-interval of [lo, hi].

Each side of the interval is scaled proportionally toward ref by (1 - perturbation_factor), preserving asymmetry in the original bounds. With perturbation_factor=1.0 the full [lo, hi] range is used; with 0.0 only ref is returned.

Example: lo=0.7, hi=1.2, ref=1.0, perturbation_factor=0.5

new_lo = 1.0 - (1.0 - 0.7) * 0.5 = 0.85 new_hi = 1.0 + (1.2 - 1.0) * 0.5 = 1.10

Uses a local Random instance to avoid polluting global random state.

Parameters:
  • lo (float) – Lower bound of search interval (inclusive)

  • hi (float) – Upper bound of search interval (inclusive)

  • ref (Optional[float]) – Reference value toward which bounds are scaled. Defaults to the midpoint (lo + hi) / 2, which is exact for symmetric bounds.

  • perturbation_factor (float) – 1.0 = full [lo, hi]; 0.0 = ref only. (default: 1.0)

  • seed (Optional[int]) – Optional seed for reproducibility

Return type:

float

planet_ruler.camera.init_params_from_bounds(param_limits, perturbation_factor=1.0, seed=None, ref_values=None, params=None)[source]

Sample initial values for parameters uniformly from their search bounds.

Each parameter is sampled via sample_param_from_bounds(). A single seeded RNG advances sequentially through a fixed parameter ordering so that each parameter’s draw is independent of which other parameters are included, and results are fully reproducible given the same seed.

Parameters:
  • param_limits (Dict[str, List[float]]) – Dict mapping parameter name → [lo, hi].

  • perturbation_factor (float) – Passed to sample_param_from_bounds(). 1.0 (default) samples the full [lo, hi] range; 0.0 returns ref only.

  • seed (Optional[int]) – Optional seed for reproducibility.

  • ref_values (Optional[Dict[str, float]]) – Optional dict of reference values used as the proportional centre for each parameter. Defaults to the midpoint of each parameter’s bounds when not provided.

  • params (Optional[List[str]]) – Optional list of parameter names to sample. When None, all keys present in param_limits are sampled in the fixed order below (unknown names are appended at the end).

Return type:

Dict[str, float]

Returns:

Dict mapping parameter name → sampled initial value.

planet_ruler.camera.get_initial_radius(planet='earth')[source]

Return the known radius for a planet (metres), or 10,000 km for unknown planets.

Return type:

float

planet_ruler.camera.check_planet_ruler_crop_metadata(image_path)[source]

Check for crop metadata in sidecar JSON file.

Parameters:

image_path (str) – Path to image file

Return type:

Optional[Dict]

Returns:

Dict with crop metadata if found, None otherwise

planet_ruler.camera.create_config_from_image(image_path, altitude_m=None, planet='earth', limits_preset='balanced', param_tolerances=None, param_tolerance=None)[source]

Create a complete planet_ruler configuration from an image. Works with any camera - phones, point-and-shoot, DSLRs, etc.

Parameters:
  • image_path (str) – Path to the image

  • altitude_m (Optional[float]) – Altitude in meters (REQUIRED if not in GPS data)

  • planet (str) – Planet name for initial radius guess (default: ‘earth’)

  • limits_preset (str) – How tightly to constrain camera/altitude parameters. "tight" — trust EXIF and GPS data; small search windows. "balanced" — default; moderate windows for most photos. "loose" — wide windows; use when metadata reliability is uncertain. Altitude (h) bounds are automatically tightened when GPS altitude is detected, regardless of preset.

  • param_tolerances (Optional[Dict[str, float]]) – Per-parameter fractional tolerance overrides, e.g. {"h": 0.30} to widen the altitude window without changing other parameters. Keys: "f", "w", "h".

  • param_tolerance (Optional[float]) – Deprecated. Pass limits_preset instead. A deprecation warning is raised; the value is ignored and "balanced" is used.

Returns:

Configuration ready for planet_ruler

Return type:

dict

Raises:
  • ValueError – If altitude cannot be determined from GPS and not provided manually

  • ValueError – If limits_preset is not a recognised preset name.

Notes

  • For uncertainty quantification, consider running multiple times (multi-start optimization)

  • Theta parameters (orientation) always span ±π; they have no prior

  • r bounds scale with the preset around the planet init radius

Fitting Module

Parameter optimization and cost function handling.

planet_ruler.fit.unpack_parameters(params, template)[source]

Turn a list of parameters back into a dict.

Parameters:
  • params (list) – Values of dictionary elements in a list.

  • template (list) – Ordered list of target keys.

Returns:

Parameter dictionary.

Return type:

param_dict (dict)

planet_ruler.fit.pack_parameters(params, template)[source]

Turn a dict of parameters (or defaults) into a list.

Parameters:
  • params (dict) – Parameter dictionary (subset or full keys of template).

  • template (dict) – Template (full) parameter dictionary.

Returns:

List of parameter values.

Return type:

param_list (list)

class planet_ruler.fit.BaseFitter[source]

Bases: ABC

Common interface for planet-ruler fitting strategies.

Each subclass encapsulates one strategy and returns a stage-result dict from .fit(). The dict always contains:

updated_init – {param: value} updates to warm-start the next stage updated_limits – {param: [lo, hi]} tighter bounds for the next stage status – “ok” | “flat_arc” | “too_few_points” | “error” warnings – list[str]

abstract fit()[source]
Return type:

dict

class planet_ruler.fit.BaseCostFunction(target, function, free_parameters, init_parameter_values)[source]

Bases: object

Shared evaluation logic for all cost function variants.

__init__(target, function, free_parameters, init_parameter_values)[source]
evaluate(params)[source]

Compute prediction given parameters.

For sparse manual annotation (is_sparse=True), only computes at annotated x-coordinates stored in self.x.

Return type:

ndarray

class planet_ruler.fit.L2CostFunction(target, function, free_parameters, init_parameter_values, loss_function='l2')[source]

Bases: BaseCostFunction

Cost function for l2, l1, and log-l1 loss against a detected limb.

__init__(target, function, free_parameters, init_parameter_values, loss_function='l2')[source]
cost(params)[source]
Return type:

float

class planet_ruler.fit.GradientFieldCostFunction(target, function, free_parameters, init_parameter_values, loss_function='gradient_field', kernel_smoothing=5.0, directional_smoothing=30, directional_decay_rate=0.15, prefer_direction=None)[source]

Bases: BaseCostFunction

Cost function using gradient field flux alignment.

__init__(target, function, free_parameters, init_parameter_values, loss_function='gradient_field', kernel_smoothing=5.0, directional_smoothing=30, directional_decay_rate=0.15, prefer_direction=None)[source]
cost(params)[source]
Return type:

float

class planet_ruler.fit.LimbFitter(target, free_parameters, init_parameter_values, parameter_limits, loss_function, minimizer, minimizer_kwargs, max_iter=15000, seed=0, verbose=False, kernel_smoothing=5.0, directional_smoothing=50, directional_decay_rate=0.15, prefer_direction=None, n_jobs=1)[source]

Bases: BaseFitter

Image-space fitter using a pixel-level cost function and a scipy minimizer.

Accepts one target (image for gradient_field, limb array for l2/l1/log-l1) at a fixed resolution. The caller is responsible for downsampling and parameter scaling before instantiation.

minimizer_kwargs must already have preset values resolved by the caller.

__init__(target, free_parameters, init_parameter_values, parameter_limits, loss_function, minimizer, minimizer_kwargs, max_iter=15000, seed=0, verbose=False, kernel_smoothing=5.0, directional_smoothing=50, directional_decay_rate=0.15, prefer_direction=None, n_jobs=1)[source]
fit()[source]
Return type:

dict

planet_ruler.fit.estimate_radius_via_sagitta(limb, h, f_px, x0=None, y0=None, sigma_px='auto', n_sigma=1.0, bias_correct=False, uncertainty='ols')[source]

Estimate planetary radius from a sparse limb arc using OLS on the sagitta.

Fits K = r / sqrt(h*(2r+h)) by regressing observed sagittae against the exact hyperbola model A(u) = sqrt(f_px^2 + u^2) - f_px.

Parameters:
  • limb (ndarray) – 1-D array of length n_pix_x; NaN where not annotated.

  • h (float) – Observer altitude in metres.

  • f_px (float) – Focal length in pixels.

  • x0 (Optional[float]) – Optional override for apex x-coordinate (auto-detected if None).

  • y0 (Optional[float]) – Image vertical centre in pixels, required for bias_correct=True.

  • sigma_px (float | str) – Annotation noise in pixels, or “auto” to derive from RMS residuals.

  • n_sigma (float) – Bound width in sigma units.

  • bias_correct (bool) – If True and y0 is provided, correct for camera tilt bias using the apex y-offset to estimate theta_x, then refine kappa via 1-D minimization.

  • uncertainty (str) – “ols” | “jackknife” | “both”. Controls which K_sigma drives bounds.

Returns:

r, r_low, r_high, r_sigma, K, K_sigma, K_sigma_jack,

n_points, residual_rms, arc_angle_deg, x_apex, y_apex, theta_x_est, status, warnings.

Return type:

dict with keys

class planet_ruler.fit.SagittaFitter(limb, h, f_px, y0, free_parameters, init_parameter_values, parameter_limits, sigma_px='auto', n_sigma=1.0, uncertainty='jackknife', bias_correct=False)[source]

Bases: BaseFitter

2-D optimizer over (kappa, theta_x) using the exact projected-sagitta formula.

Accurate to < 0.05 km for all tested camera tilts. Fits only r and theta_x; other free_parameters are handled by a subsequent LimbFitter stage.

__init__(limb, h, f_px, y0, free_parameters, init_parameter_values, parameter_limits, sigma_px='auto', n_sigma=1.0, uncertainty='jackknife', bias_correct=False)[source]
fit()[source]
Return type:

dict

planet_ruler.fit.calculate_parameter_uncertainty(observation, parameter='r', method='auto', uncertainty_type='std', scale_factor=1.0)[source]

Calculate parameter uncertainty from fitting results.

Provides a flexible interface for uncertainty estimation that works with different optimization methods and uncertainty metrics.

Parameters:
  • observation – LimbObservation object with completed fit

  • parameter (str) – Parameter name to calculate uncertainty for (default: “r”)

  • method (str) – Uncertainty calculation method: - “auto”: Automatically detect method from fit results - “differential_evolution”: Use DE population posteriors - “bootstrap”: Use bootstrap resampling (future implementation) - “hessian”: Use Hessian-based uncertainty (future implementation)

  • uncertainty_type (str) – Type of uncertainty measure: - “std”: Standard deviation of parameter distribution - “ptp”: Peak-to-peak range (max - min) - “iqr”: Interquartile range (75th - 25th percentile) - “ci”: Confidence interval (returns dict with bounds)

  • scale_factor (float) – Scale factor to apply to results (e.g., 1000 for km units)

Returns:

Dictionary containing uncertainty information:
  • ”value”: Fitted parameter value (scaled)

  • ”uncertainty”: Uncertainty estimate (scaled)

  • ”method”: Method used for uncertainty calculation

  • ”type”: Type of uncertainty measure used

  • ”raw_data”: Raw parameter samples if available

Return type:

dict

Raises:
  • ValueError – If uncertainty method is not supported or data is insufficient

  • AttributeError – If observation doesn’t have required fit results

planet_ruler.fit.format_parameter_result(uncertainty_result, units='')[source]

Format parameter uncertainty results for display.

Parameters:
  • uncertainty_result (dict) – Result from calculate_parameter_uncertainty

  • units (str) – Units to display (e.g., “km”, “m”, “degrees”)

Returns:

Formatted string representation of result

Return type:

str

planet_ruler.fit.unpack_diff_evol_posteriors(observation)[source]

Extract the final state population of a differential evolution minimization and organize as a DataFrame.

Parameters:

observation (object) – Instance of LimbObservation (must have used differential evolution minimizer).

Returns:

Population (rows) and properties (columns).

Return type:

population (pd.DataFrame)

Uncertainty Module

Parameter uncertainty estimation using multiple methods.

planet_ruler.uncertainty.calculate_parameter_uncertainty(observation, parameter, method='auto', scale_factor=1.0, confidence_level=0.68, n_bootstrap=20, **kwargs)[source]

Calculate uncertainty for a fitted parameter using appropriate method.

Automatically selects best method based on minimizer used: - differential_evolution: Population spread (fast, exact) - dual_annealing/basinhopping: Hessian approximation (fast, approximate) - any: Profile likelihood or bootstrap (slow, accurate)

Parameters:
  • observation – LimbObservation instance with completed fit

  • parameter (str) – Parameter name (e.g., ‘r’, ‘h’, ‘f’)

  • method (Literal['auto', 'hessian', 'profile', 'bootstrap']) – Uncertainty estimation method - ‘auto’: Choose based on minimizer and available data - ‘hessian’: Inverse Hessian at optimum (fast) - ‘profile’: Profile likelihood (slow but accurate) - ‘bootstrap’: Multiple fits with different seeds (slow)

  • scale_factor (float) – Scale result (e.g., 1000.0 to convert m→km)

  • confidence_level (float) – Confidence level (0.68=1σ, 0.95=2σ)

  • n_bootstrap (int) – Number of bootstrap iterations

Returns:

  • ‘uncertainty’: The uncertainty value

  • ’method’: Method used

  • ’confidence_level’: Confidence level

  • ’additional_info’: Method-specific details

Return type:

dict with keys

Plotting Module

Visualization and plotting functions.

planet_ruler.plot.plot_image(im_arr, gradient=False, show=True)[source]

Display an image using matplotlib.pyplot.imshow.

Parameters:
  • im_arr (np.ndarray) – Array of image values.

  • gradient (bool) – Display as gradient (y-axis).

  • show (bool) – Display the image.

Return type:

None

planet_ruler.plot.plot_limb(y, show=True, c='y', s=10, alpha=0.2)[source]

Display the limb (usually on top of an image).

Parameters:
  • y (np.ndarray) – Array of image values.

  • show (bool) – Display the image.

  • c (str) – Color of the limb.

  • s (int) – Size of marker.

  • alpha (float) – Opacity of markers.

Return type:

None

planet_ruler.plot.plot_3d_solution(r, h=1, zoom=1, savefile=None, legend=True, vertical_axis='z', azim=None, roll=None, x_axis=False, y_axis=True, z_axis=False, **kwargs)[source]

Plot a limb solution in 3D.

Parameters:
  • r (float) – Radius of the body in question.

  • h (float) – Height above surface (units should match radius).

  • zoom (float) – Shrink the height according to a zoom factor to make viewing easier.

  • savefile (str) – Path to optionally save figure.

  • legend (bool) – Display the legend.

  • vertical_axis (str) – Which axis will be used as the vertical (x, y, or z).

  • azim (float) – Viewing azimuth.

  • roll (float) – Viewing roll.

  • x_axis (bool) – Plot the x-axis.

  • y_axis (bool) – Plot the y-axis.

  • z_axis (bool) – Plot the z-axis.

  • kwargs – Absorbs other solution kwargs that don’t matter for physical space.

Return type:

None

planet_ruler.plot.plot_topography(image)[source]

Display the full limb, including the section not seen in the image.

Parameters:

image (np.ndarray) – Image array.

Return type:

None

planet_ruler.plot.plot_gradient_field_at_limb(y_pixels, image, image_smoothing=None, kernel_smoothing=5.0, directional_smoothing=50, directional_decay_rate=0.15, sample_spacing=50, arrow_scale=20)[source]

Visualize gradient field along a proposed limb curve.

Parameters:
  • y_pixels (np.ndarray) – Y-coordinates of limb at each x-position

  • image (np.ndarray) – Input image (H x W x 3 or H x W)

  • image_smoothing (int) – For gradient_field - Gaussian blur sigma applied to image before gradient computation. Removes high-frequency artifacts (crater rims, striations) that could mislead optimization. Different from kernel_smoothing.

  • kernel_smoothing (float) – Initial smoothing for gradient direction estimation

  • directional_smoothing (int) – How far to sample along gradients

  • directional_decay_rate (float) – Exponential decay rate for sampling

  • sample_spacing (int) – Sample every N pixels along x-axis

  • arrow_scale (float) – Scale factor for arrow lengths

Returns:

Matplotlib figure and axis objects

Return type:

fig, ax

planet_ruler.plot.compare_blur_methods(image, y_pixels=None)[source]

Compare gradient magnitude with different blur methods.

Parameters:
  • image – Input image

  • y_pixels – Optional limb curve to overlay

planet_ruler.plot.compare_gradient_fields(y_pixels_list, labels, image, image_smoothing=None, kernel_smoothing=5.0, directional_smoothing=30, directional_decay_rate=0.15)[source]

Compare gradient alignment for multiple proposed limbs.

Parameters:
  • y_pixels_list (list) – List of y-coordinate arrays (different limb proposals)

  • labels (list) – Labels for each limb

  • image (np.ndarray) – Input image

  • kernel_smoothing (float) – Initial smoothing for gradient direction estimation

  • directional_smoothing (int) – How far to sample along gradients

  • directional_decay_rate (float) – Exponential decay rate for sampling

planet_ruler.plot.plot_diff_evol_posteriors(observation, show_points=False, log=True)[source]

Extract and display the final state population of a differential evolution minimization.

Parameters:
  • observation (object) – Instance of LimbObservation (must have used differential evolution minimizer).

  • show_points (bool) – Show the individual population members in addition to the contour.

  • log (bool) – Set the y-scale to log.

Returns:

None

planet_ruler.plot.plot_full_limb(observation, x_min=None, x_max=None, y_min=None, y_max=None)[source]

Display the full limb, including the section not seen in the image.

Parameters:
  • observation (object) – Instance of LimbObservation.

  • x_min (int) – Left edge in pixels.

  • x_max (int) – Right edge in pixels.

  • y_min (int) – Bottom edge in pixels.

  • y_max (int) – Top edge in pixels.

Return type:

None

planet_ruler.plot.plot_residuals(observation, show_sparse_markers=True, marker_size=10, alpha=0.6, figsize=(16, 6), show_image=False, image_alpha=0.4, band_size=30)[source]

Plot residuals between the fitted limb and the detected target limb.

Parameters:
  • observation – Instance of LimbObservation that has been fitted.

  • show_sparse_markers (bool) – Use larger markers for sparse data.

  • marker_size (int) – Size of markers for sparse data.

  • alpha (float) – Transparency of residual markers/line.

  • figsize (tuple) – Figure size (width, height).

  • show_image (bool) – Show straightened image strip as background.

  • image_alpha (float) – Transparency of background image.

  • band_size (int) – Size of band around residuals to plot (in pixels)

Return type:

None

planet_ruler.plot.plot_gradient_field_quiver(image, step=2, scale=0.15, limb_y=None, roi_height=None, image_smoothing=None, kernel_smoothing=5.0, directional_smoothing=50, directional_decay_rate=0.15, downsample_factor=32, figsize=(16, 10), cmap='hot')[source]

Plot gradient field as quiver (arrow) plot.

Parameters:
  • image (ndarray) – Input image array.

  • step (int) – Spacing between arrows (every Nth pixel).

  • scale (Optional[float]) – Arrow scale factor (None = auto).

  • limb_y (Optional[ndarray]) – Optional limb curve to overlay (y-coordinates).

  • roi_height (Optional[int]) – If provided, only show ±roi_height pixels around limb.

  • image_smoothing (Optional[int]) – For gradient_field - Gaussian blur sigma applied to image before gradient computation. Removes high-frequency artifacts (crater rims, striations) that could mislead optimization. Different from kernel_smoothing.

  • kernel_smoothing (float) – Sigma for initial gradient direction estimation.

  • directional_smoothing (int) – Distance for directional blur sampling.

  • directional_decay_rate (float) – Exponential decay for directional blur.

  • downsample_factor (int) – Downsample image by this factor before computing gradients. Values > 1 reduce resolution (e.g., 2 = half resolution, 4 = quarter). Useful for visualizing gradient field at different optimization stages.

  • figsize (tuple) – Figure size (width, height).

  • cmap (str) – Colormap for gradient magnitude.

Return type:

None

planet_ruler.plot.plot_segmentation_masks(observation)[source]

Display all the classes/masks generated by the segmentation.

Parameters:

observation (object) – Instance of LimbObservation.

Return type:

None

planet_ruler.plot.plot_sam_masks(masks, labels=None, colors=None, alpha=0.5, image=None, figsize=(16, 10), title=None, show=True)[source]

Plot multiple SAM segmentation masks as separate labeled objects with legend.

Parameters:
  • masks (list) – List of SAM mask dictionaries with ‘segmentation’ keys containing boolean arrays. Can also be a list of boolean arrays directly.

  • labels (list) – Labels for each mask (for legend). If None, uses “Mask 0”, “Mask 1”, etc.

  • colors (list) – Colors for each mask. If None, uses a default color cycle.

  • alpha (float) – Transparency of mask overlays (0=transparent, 1=opaque).

  • image (np.ndarray) – Optional background image to display under masks.

  • figsize (tuple) – Figure size in inches.

  • title (str) – Optional title for the plot.

  • show (bool) – Whether to display the plot immediately.

Returns:

(fig, ax) matplotlib figure and axis objects.

Return type:

tuple

Example

>>> masks = [{'segmentation': planet_mask}, {'segmentation': sky_mask}]
>>> fig, ax = plot_sam_masks(
...     masks,
...     labels=['Planet', 'Sky'],
...     colors=['yellow', 'cyan'],
...     image=observation.image
... )

Validation Module

Configuration validation and consistency checking.

Validation functions for planet_ruler.

planet_ruler.validation.validate_limb_config(config, strict=True)[source]

Validate that a planet_ruler configuration is internally consistent.

Checks: 1. Initial parameter values are within their specified limits 2. Theta parameter limits are wide enough (avoid r-h coupling issues) 3. Radius limits span reasonable range for robust optimization

Parameters:
  • config (dict) – Configuration dictionary with keys: - ‘init_parameter_values’: dict of parameter initial values - ‘parameter_limits’: dict of [lower, upper] limits

  • strict (bool) – If True, raise exceptions. If False, only log warnings.

Raises:

AssertionError – If strict=True and validation fails

Return type:

None

Example

>>> config = {
...     'init_parameter_values': {'r': 6371000, 'theta_x': 0.1},
...     'parameter_limits': {'r': [1e6, 1e8], 'theta_x': [-3.14, 3.14]}
... }
>>> validate_config(config)  # Passes

Command-Line Interface

CLI tools for command-line access to planet_ruler.

Command-line interface for planet_ruler

This module provides a simple CLI for measuring planetary radii from horizon photographs.

planet_ruler.cli.load_config(config_path)[source]

Load configuration from YAML or JSON file.

Return type:

Dict[str, Any]

planet_ruler.cli.main()[source]

Main CLI entry point.

planet_ruler.cli.measure_command(args)[source]

Handle the measure command.

planet_ruler.cli.demo_command(args)[source]

Handle the demo command.

planet_ruler.cli.list_command(args)[source]

Handle the list command.

Demo Module

Example scenarios and configuration management.

planet_ruler.demo.make_dropdown()[source]
planet_ruler.demo.load_demo_parameters(demo)[source]
planet_ruler.demo.display_text(filepath)[source]