Coverage for lunar_toolkit/cli/stereo.py: 56%

39 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-19 02:58 +0000

1"""stereo - DEM from one LROC NAC stereo pair (ISIS + ASP).""" 

2 

3import argparse 

4import sys 

5from pathlib import Path 

6 

7COMMAND = "stereo" 

8 

9 

10def add_parser(sub: argparse._SubParsersAction) -> None: 

11 p = sub.add_parser( 

12 COMMAND, 

13 help="Build a DEM from a NAC stereo pair (bundle_adjust, parallel_stereo, point2dem).", 

14 formatter_class=argparse.RawDescriptionHelpFormatter, 

15 epilog=""" 

16Examples 

17-------- 

18 lunar stereo M181058717LE M181073012LE --res 2 --work-dir pair1 

19 lunar stereo L.IMG R.IMG --dem ldem.tif --res 2 # mapproject first, align to the DEM 

20 lunar stereo L.cub R.cub --dry-run 

21 

22Needs ISIS and ASP on PATH (or ASP_ROOT / ISISROOT). 

23""", 

24 ) 

25 p.add_argument("left", help="Left image: NAC EDR product ID, .IMG or .cub path.") 

26 p.add_argument("right", help="Right image: NAC EDR product ID, .IMG or .cub path.") 

27 p.add_argument("--work-dir", default="stereo_work", metavar="DIR") 

28 p.add_argument("--res", type=float, default=2.0, metavar="M", help="DEM m/px (default: 2).") 

29 p.add_argument("--dem", default=None, metavar="TIF", help="Prior DEM to mapproject onto and align to.") 

30 p.add_argument( 

31 "--image-res", 

32 type=float, 

33 default=1.0, 

34 metavar="M", 

35 help="Mapprojected image m/px when --dem is set (default: 1).", 

36 ) 

37 p.add_argument("--no-align", action="store_true", help="Skip pc_align to --dem.") 

38 p.add_argument("--max-displacement", type=float, default=200.0, metavar="M") 

39 p.add_argument("--no-csm", action="store_true", help="Use ISIS cameras instead of CSM.") 

40 p.add_argument("--local-spice", action="store_true") 

41 p.add_argument("--processes", type=int, default=None, metavar="N") 

42 p.add_argument("--threads", type=int, default=None, metavar="N") 

43 p.add_argument( 

44 "--stereo-opt", 

45 action="append", 

46 metavar="KEY=VALUE", 

47 help="Extra / overriding parallel_stereo option (repeatable).", 

48 ) 

49 p.add_argument("--force", action="store_true") 

50 p.add_argument("--dry-run", action="store_true", help="Print the commands without running them.") 

51 p.add_argument("--data-dir", type=Path, default=None, metavar="DIR") 

52 

53 

54def run(args: argparse.Namespace) -> int: 

55 from ..stereo_dem import stereo_dem 

56 from ..tools import ToolError 

57 from ._utils import parse_options, print_dry_run, resolve_images 

58 

59 work_dir = Path(args.work_dir) 

60 left, right = resolve_images( 

61 [args.left, args.right], work_dir / "raw", args.data_dir, download=not args.dry_run 

62 ) 

63 kwargs = dict( 

64 resolution=args.res, 

65 prior_dem=Path(args.dem) if args.dem else None, 

66 image_resolution=args.image_res, 

67 align=not args.no_align, 

68 max_displacement=args.max_displacement, 

69 csm=not args.no_csm, 

70 web_spice=not args.local_spice, 

71 processes=args.processes, 

72 threads=args.threads, 

73 stereo_options=parse_options(args.stereo_opt), 

74 force=args.force, 

75 ) 

76 try: 

77 if args.dry_run: 

78 print_dry_run(lambda: stereo_dem(left, right, work_dir, **kwargs)) 

79 return 0 

80 dem = stereo_dem(left, right, work_dir, **kwargs) 

81 except ToolError as exc: 

82 print(exc, file=sys.stderr) 

83 return 1 

84 print(f"Stereo DEM -> {dem}") 

85 return 0