lunar_toolkit.selection

Illumination-diverse image selection for shape-from-shading.

KMedoidsSelection(chosen, stage, medoids, labels, directions, uncovered, poorly_lit, history=list()) dataclass

Result of select_kmedoids (indices refer to the input rows).

Attributes:
  • chosen (list of int) –

    Selected images in pick order: medoids first, then the coverage and illumination refinements.

  • stage (list of str) –

    Per entry of chosen: "medoid", "coverage" or "illumination".

  • medoids, labels (array) –

    The k-medoids clustering (labels[i] indexes medoids; -1 for images without an azimuth).

  • directions ((n_cells,) int array) –

    Distinct illumination directions reached per cell (capped at views).

  • uncovered, poorly_lit (float) –

    Fractions of the potentially lit cells with no selected image, and with fewer than views directions.

sort_by_sun_azimuth(obs_df, start=0.0, column='sub_solar_azimuth')

Sort rows by Sun azimuth, measured clockwise from start degrees.

Rows with an unknown azimuth go last. The index is preserved.

aoi_coverage(obs_df, aoi)

Fraction (0–1) of the AOI covered by each row's footprint.

aoi is {"point": (lat, lon), "radius_km": r}, {"lat": (s, n), "lon": (w, e)} or anything lunar_toolkit.geometry.load_aoi accepts.

select_illumination_diverse(obs_df, azimuth_bin_deg=30.0, incidence=None, per_bin=1, max_images=None, prefer='balanced', aoi=None, min_aoi_coverage=0.0, incidence_bin_deg=None, max_emission=None, method='bins', azimuth_origin=0.0)

Choose images spanning diverse Sun azimuths (for SfS / PSR mapping).

Parameters:
  • obs_df (DataFrame) –

    Observations in the common schema.

  • azimuth_bin_deg (float, default: 30.0 ) –

    Bin width (method="bins") or minimum separation (method="greedy") in sub-solar azimuth, degrees.

  • incidence ((min, max), default: None ) –

    Keep only images with incidence in this range (e.g. (80, 95) at the poles; SfS needs lit but low-Sun images).

  • per_bin (int, default: 1 ) –

    Images kept per bin. With method="greedy": at most this many picks may lie within azimuth_bin_deg of each other.

  • max_images (int, default: None ) –

    Cap on the result. Excess picks are removed by repeatedly dropping the lower-scored image of the pair closest in azimuth, so the azimuth spread is kept.

  • prefer (('balanced', 'coverage', 'resolution'), default: "balanced" ) –

    How to rank images competing for a bin. With res = finest pixel_scale among candidates / own pixel_scale (0.5 if unknown) and cov = AOI coverage fraction (1 without aoi): "balanced" = cov * sqrt(res); "coverage" ranks by coverage then resolution; "resolution" by resolution then coverage. A callable gets the candidate DataFrame and returns the score (higher is better).

  • aoi ((dict, str, Path or Aoi), default: None ) –

    {"point": (lat, lon), "radius_km": r}, {"lat": ..., "lon": ...} or anything lunar_toolkit.geometry.load_aoi accepts; images not overlapping it are dropped.

  • min_aoi_coverage (float, default: 0.0 ) –

    Minimum fraction of the AOI an image must cover (with aoi).

  • incidence_bin_deg (float, default: None ) –

    Also bin by incidence (method="bins" only), giving azimuth × incidence cells — useful when shadow length diversity matters too.

  • max_emission (float, default: None ) –

    Drop images with larger emission angles (off-nadir views are foreshortened and complicate SfS).

  • method (('bins', 'greedy'), default: "bins" ) –

    "bins": fixed bins [origin + k*w, origin + (k+1)*w), best per_bin per bin (two picks in neighbouring bins may still be close in azimuth). "greedy": candidates in descending score; accept one unless per_bin accepted images already lie within azimuth_bin_deg of it, so with per_bin=1 all picks are at least azimuth_bin_deg apart (the Bertone et al. criterion).

  • azimuth_origin (float, default: 0.0 ) –

    Start of the first azimuth bin, degrees.

Returns:
  • DataFrame –

    The selected rows (original index kept) sorted by sub_solar_azimuth (see sort_by_sun_azimuth), with extra columns aoi_coverage (NaN without aoi), azimuth_bin (bin start, deg; NaN for greedy), incidence_bin (if binned) and selection_score.

Notes

This uses each image's single scene-centre Sun azimuth; for a large AOI where per-pixel diversity matters, use select_for_coverage.

select_for_coverage(obs_df, aoi, azimuth_sep_deg=30.0, views_per_cell=3, incidence=None, max_images=None, prefer='balanced', max_emission=None, grid_km=None, min_gain=1)

Greedy, per-location illumination-diverse selection over an AOI.

The AOI is sampled on a grid (grid_km spacing; default ~100 cells across). Each grid cell keeps the Sun azimuths of the selected images covering it. A candidate helps a cell if it covers the cell, the cell has fewer than views_per_cell views and the candidate's azimuth is at least azimuth_sep_deg from every azimuth already there. At each step the image maximising helped_cells * (0.5 + 0.5 * score) is taken (score as in select_illumination_diverse, normalised to 0–1), until no image helps min_gain cells or max_images is reached.

Returns:
  • DataFrame –

    Selected rows sorted by sub_solar_azimuth with extra columns aoi_coverage, selection_score, selection_order (0 = first pick) and cells_gained (fraction of AOI cells the pick helped). attrs["view_fractions"] maps a view count k to the fraction of AOI cells with exactly k non-redundant azimuths (views_per_cell = fully served), and attrs["grid_km"] / attrs["n_cells"] describe the grid.

selection_distances(coverage, azimuth, weights=(1.0, 1.0), chunk=256)

Image-to-image distance of Bertone et al. (2026, sec. 2) for k-medoids selection.

d_ij = sqrt((w_a * a_ij)**2 + (w_c * c_ij)**2) with a_ij the circular Sun azimuth (sub-solar longitude) difference and c_ij the fraction of grid cells covered by exactly one of the two images, each divided by its largest value over all pairs (so both span 0-1).

Parameters:
  • coverage ((n_images, n_cells) bool array) –

    Per-image coverage (or lit) masks on a common grid.

  • azimuth ((n_images,) array) –

    Sun azimuths, degrees (finite).

  • weights ((w_a, w_c), default: (1.0, 1.0) ) –

    Weights of the azimuth and coverage terms.

  • chunk (int, default: 256 ) –

    Rows per block of the coverage products (memory).

Returns:
  • (n_images, n_images) float array –

kmedoids(dist, k, init='build', seed=0, n_init=1, max_iter=100)

Alternating k-medoids (Park & Jun 2009) on a precomputed distance matrix.

Assign every object to its nearest medoid, move each medoid to the member with the smallest total distance to its cluster, repeat until the medoids stop changing.

Parameters:
  • dist ((n, n) array) –

    Symmetric distances.

  • k (int) –

    Number of clusters (all objects are medoids when k >= n).

  • init (('build', 'park_jun', 'random'), default: "build" ) –

    "build": the PAM BUILD step (most central object, then greedy cost reduction; deterministic and robust to duplicate images); "park_jun": the k objects with the smallest v_j = sum_i d_ij / sum_l d_il (Park & Jun's own start, which favours central objects); "random": k distinct objects drawn with seed, best of n_init runs.

  • seed (int, default: 0 ) –

    Random generator seed (init="random"; ties are always broken by the lowest index).

  • n_init (int, default: 1 ) –

    Random restarts (init="random").

  • max_iter (int, default: 100 ) –

    Iteration cap.

Returns:
  • (medoids, labels) –

    Sorted medoid indices and each object's cluster (index into medoids).

select_kmedoids(lit, azimuth, n_clusters=30, *, coverage=None, views=3, azimuth_sep=30.0, max_uncovered=0.005, max_poorly_lit=0.01, max_images=None, weights=(1.0, 1.0), init='build', seed=0, n_init=1)

k-medoids image selection with coverage and illumination refinement (Bertone et al. 2026, sec. 2).

  1. Cluster the images with kmedoids on selection_distances (Sun azimuth and coverage differences) and take the n_clusters medoids.
  2. Add the image lighting the most still-uncovered cells until fewer than max_uncovered of the potentially lit cells (lit in any image) are uncovered.
  3. Add the image giving the most cells a new direction (Sun azimuth at least azimuth_sep from the cell's directions so far, up to views) until fewer than max_poorly_lit of the potentially lit cells have fewer than views directions.

Steps 2-3 stop early when no image helps or max_images is reached; ties go to the lowest index, so the result is deterministic (for init="random" given seed).

Parameters:
  • lit ((n_images, n_cells) bool array) –

    Lit (valid and above the shadow threshold) cells per image on a common grid, e.g. SfsPipeline._lit_masks().

  • azimuth ((n_images,) array) –

    Sun azimuths in degrees; images with NaN are never selected.

  • n_clusters (int, default: 30 ) –

    Medoids (the paper's ~30 user-defined images).

  • coverage ((n_images, n_cells) bool array, default: None ) –

    Footprint masks for the distance (default lit).

  • views ((int, float), default: 3 ) –

    Directions wanted per cell and their minimum separation (3, 30 deg).

  • azimuth_sep ((int, float), default: 3 ) –

    Directions wanted per cell and their minimum separation (3, 30 deg).

  • max_uncovered (float, default: 0.005 ) –

    Stopping fractions of steps 2 and 3 (0.5 % and 1 %).

  • max_poorly_lit (float, default: 0.005 ) –

    Stopping fractions of steps 2 and 3 (0.5 % and 1 %).

  • max_images (int, default: None ) –

    Cap on the total (medoids are truncated to it first).

  • weights (tuple[float, float], default: (1.0, 1.0) ) –
  • init (tuple[float, float], default: (1.0, 1.0) ) –
  • seed (tuple[float, float], default: (1.0, 1.0) ) –
  • n_init (tuple[float, float], default: (1.0, 1.0) ) –

coverage_matrix(obs_df, aoi, grid_km=None)

Footprint coverage of the AOI grid cells: (cover[n_images, n_cells], x, y).

The grid is the one select_for_coverage uses (cell centres in the AOI's tangent plane, km).

select_kmedoids_footprints(obs_df, aoi, n_clusters=30, incidence=None, max_emission=None, grid_km=None, **options)

select_kmedoids on catalogue footprints over an AOI (footprint = lit, no shadows).

options go to select_kmedoids. Returns the selected rows sorted by sub_solar_azimuth with selection_order, selection_stage and cluster columns; attrs holds uncovered, poorly_lit and n_cells.