DEMs with ISIS + ASP

lunar_toolkit wraps the ISIS and Ames Stereo Pipeline (ASP) tools used to build lunar DEMs, and chains them into two pipelines: a stereo DEM from one NAC pair, and a shape-from-shading (SfS) DEM from many NAC images.

Setup

Install the Ames Stereo Pipeline by following its installation guide. ASP ships its own ISIS, so a separate ISIS install is only needed without ASP. Then set up the ISIS data area for LRO (base and lro, see the ISIS data area docs) and set ISISDATA. The Docker image has all of this ready.

spiceinit uses the USGS SPICE web service by default (web=True), so the large LRO SPK and CK kernels are not needed, and --local-spice / web_spice=False uses local kernels. Check the camera stack with cam_test or ASP_ROOT=... pytest -m tools.

Tools are looked up in $ASP_ROOT/bin, then $ISISROOT/bin, then PATH. The env does not need to be activated: its conda env config vars (conda-meta/state) are read, ISISROOT, PROJ_DATA and GDAL_DATA default to the tool's env prefix, and its bin/ is put first on PATH for ASP's Python wrapper scripts.

export ASP_ROOT=~/miniforge3/envs/isis

Wrappers

Positional inputs are arguments; every other option is a keyword argument (max_iterations=5 -> --max-iterations 5, save_sparingly=True -> --save-sparingly, False / None dropped, tuples expand to several values, t_* GDAL-style options keep their underscores). ISIS programs take key=value parameters (from_ for from).

from lunar_toolkit import asp, isis

cube = isis.calibrate_lronac("raw/M1234567890LE.IMG", "work")  # .cal.echo.cub
cam = isis.isd_generate(cube)  # CSM camera (.json)
asp.mapproject("dem.tif", cube, "work/M1234567890LE.map.tif", cam, tr=5)
asp.bundle_adjust([cube], "ba/run", [cam], mapprojected=[...], dem="dem.tif", parallel=True)
asp.sfs("dem.tif", [cube], "sfs/run", [cam], parallel=True, smoothness_weight=0.08)
angles = asp.sun_angles("dem.tif", [cube], [cam])  # sfs --query

Any other ISIS program runs through isis.isis("program", **params) and any tool through tools.run(tool, args).

Dry runs

Inside tools.dry_run() commands are recorded instead of executed, so a whole pipeline can be inspected first:

import shlex
from lunar_toolkit.tools import dry_run

with dry_run() as cmds:
    pipeline.run(images)
print("\n".join(shlex.join(c) for c in cmds))

The CLI equivalent is --dry-run on lunar stereo, lunar sfs and lunar kaguya.

Stereo DEM

lunar stereo M181058717LE M181073012LE --res 2 --work-dir pair1
lunar stereo L.IMG R.IMG --dem ldem.tif --res 2 --image-res 1

Calibrate -> CSM cameras -> bundle_adjust -> parallel_stereo (asp_mgm, subpixel mode 9) -> point2dem. With a prior DEM the images are mapprojected onto it first (better on steep terrain) and the result is aligned to it with pc_align. Use lunar pairs to choose the pair.

Shape-from-shading DEM

SfsPipeline reproduces the workflow of Bertone et al. (2026, PSJ 7:118) behind the 5 m/px SfS DEMs of the 13 Artemis III candidate regions (preset="bertone", the default); preset="asp" gives ASP's large-area LRO NAC recipe.

Step Bertone et al. (2026) Config / CLI
Image search < 1.5 m/px, NAC-L and NAC-R separate; slewed images kept for BA / stereo; capped before download --aoi, --max-pixel-scale, --max-images
SfS images nadir (slew < 5 deg) max_sfs_slew, --max-sfs-slew
Cameras CSM csm, --no-csm
Mapproject for BA native ~1 m/px onto the LDEM ba_resolution, --ba-res
Bundle adjust next 200 images in Sun order, 100 m camera position uncertainty, intrinsics fixed PRESETS["bertone"]["bundle_adjust"], --ba-opt
Image rejection match-offset p85 > 1.8 m (on the registered, refined cameras, as ASP's ba_align_ref), no matches, > 85 % of residuals > 1 px; stops if fewer than min_images remain max_offset_p85, max_large_residual_fraction, min_matches, min_images
QA max-lit mosaics, all + 30 deg bins qa/
Registration stereo DEMs from up to 30 pairs (convergence >= 5 deg), translation to the LDEM registration, --stereo-pairs
Refinement quick re-alignment refine, --refine-opt
SfS selection k-medoids seeding, then < 0.5 % uncovered and < 1 % with < 3 directions >= 30 deg apart; drop >= 95 % shadowed selection="kmedoids" (or "coverage"), selection_clusters, selection_views, max_shadow_fraction
Camera downsampling reduce to the target resolution, CSM cameras patched to match reduce="auto", sfs_cameras="csm" (lunar_toolkit.csm, matches ISIS reduce to ~0.001 px median)
SfS mu 0.05-0.1, lambda 1e-3, 5 iterations, shadow threshold 0.002, fixed albedo PRESETS["bertone"]["sfs"], --sfs-opt
Products Table 1 (GLD01-GLD10) products/
Uncertainty ensemble of 4-6 subsets on 2.5 km crops, <= 40 % overlap ensemble_members, --ensemble
lunar sfs --aoi site.geojson --dem LDEM_87S_5MPP_ADJ.TIF --work-dir malapert --processes 8
lunar sfs M1234567890LE ... --dem ldem.tif --projwin -10000 50000 0 40000 --res 5 --preset asp
lunar sfs --aoi site.geojson --dem ldem.tif --dry-run
from lunar_toolkit.sfs import SfsConfig, SfsPipeline

cfg = SfsConfig(
    resolution=5, projwin=(-10000, 50000, 0, 40000), preset="bertone", ensemble_members=4, processes=8
)
pipeline = SfsPipeline(
    "malapert", "LDEM_87S_5MPP_ADJ.TIF", cfg, observations=obs
)  # obs: common schema, for stereo pairs
dem = pipeline.run(images)
print(pipeline.removed, pipeline.selected, pipeline.unselected)

Differences from the paper: the CSM re-mapping for reduced cubes is our own implementation (lunar_toolkit.csm, validated with cam_test against ISIS reduce); the default selection is greedy coverage (kmedoids follows the paper); QA bins use the sfs --query Sun azimuth rather than sub-solar longitude. cameras_from="published" uses their published adjusted cameras instead of running bundle adjustment.

Work directory:

Path Contents
images/ calibrated cubes, CSM cameras, reduced cubes
dem/ cropped and resampled prior DEM
prj/, prj_sfs/ images mapprojected for BA (native) and for SfS (target resolution)
ba/, ba_align/, ba_refine/ bundle adjustment, registered and refined cameras
stereo/, align/ registration stereo DEMs, mosaic and transform
qa/ max-lit mosaics, all and per 30 deg Sun-azimuth bin
selection/ removed.csv, selection.csv, selected.txt, unselected.txt
sfs_cameras/, sfs/ SfS camera adjustments, run-DEM-final.tif
products/, ensemble/ product suite, ensemble members

Steps whose outputs exist are skipped, so an interrupted run resumes (force=True / --force recomputes).

Reproducible runs

lunar sfs --config examples/malapert_crop.yaml
lunar sfs --config run.yaml --dump-config > effective.yaml
lunar sfs --config run.yaml --check
lunar sfs-report malapert_crop --region malapert --fetch-published

examples/malapert_crop.yaml and examples/asp_recipe.yaml are complete starting points. A config holds the run keys (work_dir, dem or dem: pgda, region, crop_km, crop_center, aoi, sources, from_file, max_slew, max_pixel_scale, checksums), every SfsConfig field (including seed) and the three option-override dicts. Window precedence: projwin, then aoi, then crop_km, then the region. Each run saves config.yaml and appends to provenance.json (schema in lunar_toolkit.provenance); --check reports any difference in tool versions, settings or input checksums before a resume or re-run.

lunar sfs-report turns a work dir into report.html, metrics.json, CSV tables and PNG figures covering Figs. 3, 5-9 and Table 2 of Bertone et al.

SfS products

lunar_toolkit.sfs_products.make_products writes the Table 1 suite of Bertone et al., named <ROI>_GLD<code>_<RES>: ELEV (elevation), MASK (lit-image count), ISGM (ASP height error, when given), DIFF (minus the prior), TSLP / TRGH (slope and VRM roughness at selectable baselines), HILL_<RES>_315_45, OMOS (max-lit orthomosaic), BRES (best input resolution) and SBCT (30 deg Sun-azimuth bins with a lit image). ensemble_uncertainty writes the per-pixel standard deviation of elevation and slope across ensemble members, and compare_to_points compares a DEM with LOLA points (geodiff --csv-format).

Reproducing Bertone et al. (2026)

lunar reference provides the regions and reference data:

lunar reference list
lunar reference pgda malapert --kind surf toterr ldec xyzi -o ref        # a priori LDEM, uncertainty, LOLA counts, points
lunar reference pgda malapert --kind xyzi --csv --crop-km 2.5 -o ref     # LOLA points for validation
lunar reference sdem malapert --remote --member GLDELEV GLDSIGM -o ref   # their published SDEM members
lunar sfs --aoi "$(lunar reference crop malapert --crop-km 2.5 --format wkt)" \
    --dem ref/Site23/Site23_final_adj_5mpp_surf.tif --res 5 --work-dir malapert_crop

Start with a 2.5 km crop (a few dozen images, hours) before a full region (hundreds of images; the paper reports 3-5 days of bundle adjustment and about a day of SfS on 36 CPUs). A full region needs several hundred GB for calibrated cubes. The published zips also contain the bundle-adjusted NAC cameras (LROC-NAC-adjust/), which can be used to start from their registration.