lunar-toolkit 0.1.0.dev2__tar.gz → 0.1.0.dev4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/PKG-INFO +1 -1
  2. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/asp.py +20 -1
  3. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/sfs.py +2 -2
  4. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/sfs.py +32 -7
  5. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/static/index.html +486 -443
  6. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit.egg-info/PKG-INFO +1 -1
  7. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/pyproject.toml +1 -1
  8. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_sfs.py +32 -10
  9. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_static.py +2 -2
  10. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_tools.py +21 -0
  11. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_workspace.py +4 -4
  12. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/LICENSE +0 -0
  13. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/README.md +0 -0
  14. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/__init__.py +0 -0
  15. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/__init__.py +0 -0
  16. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/_jobs.py +0 -0
  17. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/_status.py +0 -0
  18. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/_utils.py +0 -0
  19. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/app.py +0 -0
  20. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/build_db.py +0 -0
  21. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/download.py +0 -0
  22. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/kaguya.py +0 -0
  23. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/ode_search.py +0 -0
  24. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/pairs.py +0 -0
  25. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/process.py +0 -0
  26. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/query.py +0 -0
  27. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/reference.py +0 -0
  28. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/search.py +0 -0
  29. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/serve.py +0 -0
  30. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/sfs_report.py +0 -0
  31. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/shadowcam.py +0 -0
  32. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/stereo.py +0 -0
  33. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/cli/workspace.py +0 -0
  34. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/config.py +0 -0
  35. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/constants.py +0 -0
  36. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/csm.py +0 -0
  37. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/downloader.py +0 -0
  38. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/geometry.py +0 -0
  39. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/isis.py +0 -0
  40. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/kaguya/__init__.py +0 -0
  41. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/kaguya/mi.py +0 -0
  42. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/kaguya/tc.py +0 -0
  43. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/lroc/__init__.py +0 -0
  44. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/lroc/cumindex.py +0 -0
  45. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/lroc/nac.py +0 -0
  46. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/observation.py +0 -0
  47. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/ode.py +0 -0
  48. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/provenance.py +0 -0
  49. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/reference.py +0 -0
  50. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/selection.py +0 -0
  51. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/sfs_products.py +0 -0
  52. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/sfs_report.py +0 -0
  53. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/shadowcam/__init__.py +0 -0
  54. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/shadowcam/sc.py +0 -0
  55. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/stereo.py +0 -0
  56. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/stereo_dem.py +0 -0
  57. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/tools.py +0 -0
  58. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit/workspace.py +0 -0
  59. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit.egg-info/SOURCES.txt +0 -0
  60. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit.egg-info/dependency_links.txt +0 -0
  61. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit.egg-info/entry_points.txt +0 -0
  62. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit.egg-info/requires.txt +0 -0
  63. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/lunar_toolkit.egg-info/top_level.txt +0 -0
  64. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/setup.cfg +0 -0
  65. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_aoi.py +0 -0
  66. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_app.py +0 -0
  67. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_build.py +0 -0
  68. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_config.py +0 -0
  69. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_csm.py +0 -0
  70. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_download.py +0 -0
  71. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_footprints.py +0 -0
  72. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_integration.py +0 -0
  73. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_kaguya.py +0 -0
  74. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_kaguya_mi.py +0 -0
  75. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_lbl.py +0 -0
  76. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_ode.py +0 -0
  77. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_reference.py +0 -0
  78. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_selection.py +0 -0
  79. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_sfs_products.py +0 -0
  80. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_sfs_report.py +0 -0
  81. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_shadowcam.py +0 -0
  82. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_stereo.py +0 -0
  83. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_tool_env.py +0 -0
  84. {lunar_toolkit-0.1.0.dev2 → lunar_toolkit-0.1.0.dev4}/tests/test_update.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lunar-toolkit
3
- Version: 0.1.0.dev2
3
+ Version: 0.1.0.dev4
4
4
  Summary: Find, download and process lunar orbital imagery (LROC, ShadowCam, Kaguya TC) and build stereo and shape-from-shading DEMs with ISIS + ASP
5
5
  License-Expression: BSD-3-Clause
6
6
  Keywords: moon,lroc,shadowcam,kaguya,isis,ames-stereo-pipeline,shape-from-shading,dem
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import re
7
+ import time
7
8
  from collections.abc import Sequence
8
9
  from dataclasses import dataclass
9
10
  from pathlib import Path
@@ -11,7 +12,7 @@ from typing import Any
11
12
 
12
13
  import numpy as np
13
14
 
14
- from .tools import asp_options, gdal_options, is_dry_run, run
15
+ from .tools import ToolError, asp_options, gdal_options, is_dry_run, run
15
16
 
16
17
 
17
18
  def _paths(items: Sequence[Any]) -> list[str]:
@@ -49,12 +50,30 @@ def bundle_adjust(
49
50
  *mapprojected* (same order as *images*) and the *dem* they were projected
50
51
  onto become ``--mapprojected-data``, which finds matches between images of
51
52
  very different illumination. Returns *out_prefix*.
53
+
54
+ Raises :class:`~lunar_toolkit.tools.ToolError` when the run writes no fresh
55
+ ``.adjust`` file for some image: ASP exits 0 even when it finds nothing to
56
+ optimize ("No points to optimize ... Cannot continue").
52
57
  """
53
58
  _parent(out_prefix)
54
59
  if mapprojected is not None:
55
60
  options["mapprojected_data"] = " ".join(_paths([*mapprojected, *([dem] if dem else [])]))
56
61
  tool = "parallel_bundle_adjust" if parallel else "bundle_adjust"
62
+ start = time.time()
57
63
  run(tool, [*_paths(images), *_paths(cameras), *asp_options(options), "-o", out_prefix], cwd=cwd)
64
+ if not is_dry_run():
65
+ prefix = Path(cwd or ".") / out_prefix
66
+ cams: Sequence[Path | None] = cameras if len(cameras) == len(images) else [None] * len(images)
67
+ missing = [
68
+ p
69
+ for p in (bundle_adjust_file_name(prefix, im, cam) for im, cam in zip(images, cams))
70
+ if not p.is_file() or p.stat().st_mtime < start - 2
71
+ ]
72
+ if missing:
73
+ raise ToolError(
74
+ f"{tool} wrote no adjustment for {len(missing)} of {len(images)} images "
75
+ f"(e.g. {missing[0].name}); see {out_prefix}-log-bundle_adjust-*.txt"
76
+ )
58
77
  return Path(out_prefix)
59
78
 
60
79
 
@@ -242,7 +242,7 @@ def _effective(args: argparse.Namespace):
242
242
 
243
243
  def run(args: argparse.Namespace) -> int:
244
244
  from .. import config as cfgmod
245
- from ..sfs import SfsPipeline, aoi_projwin, search_images
245
+ from ..sfs import PipelineError, SfsPipeline, aoi_projwin, search_images
246
246
  from ..tools import ToolError
247
247
  from ._utils import print_dry_run, resolve_images
248
248
 
@@ -310,7 +310,7 @@ def run(args: argparse.Namespace) -> int:
310
310
  print_dry_run(lambda: pipeline.run(images))
311
311
  return 0
312
312
  out = pipeline.run(images)
313
- except ToolError as exc:
313
+ except (ToolError, PipelineError) as exc:
314
314
  print(exc, file=sys.stderr)
315
315
  return 1
316
316
  print(f"SfS DEM -> {out}")
@@ -168,11 +168,15 @@ class SfsConfig:
168
168
  ``--heights-from-dem`` (reusing the matches).
169
169
  max_offset_p85 : float, optional
170
170
  Drop images whose 85th-percentile mapprojected match offset (m) is larger.
171
+ Applied to the DEM-registered, refined cameras (ASP's ``ba_align_ref``
172
+ report); with *refine* off, to the unregistered ``ba/`` cameras.
171
173
  max_large_residual_fraction : float, optional
172
174
  Drop images with more than this fraction of reprojection errors over
173
175
  *large_residual_px* pixels.
174
176
  min_matches : int
175
177
  Drop images with fewer inlier matches.
178
+ min_images : int
179
+ Stop with :class:`PipelineError` when a quality filter leaves fewer images.
176
180
  selection : {"coverage", "image_subset", "all"}
177
181
  SfS image selection: greedy per-pixel illumination coverage
178
182
  (:func:`select_by_illumination`), ASP ``image_subset`` per Sun-azimuth bin,
@@ -236,6 +240,7 @@ class SfsConfig:
236
240
  max_large_residual_fraction: float | None = 0.85
237
241
  large_residual_px: float = 1.0
238
242
  min_matches: int = 1
243
+ min_images: int = 3
239
244
  selection: str = "coverage"
240
245
  max_shadow_fraction: float | None = 0.95
241
246
  selection_views: int = 3
@@ -281,6 +286,10 @@ class SfsConfig:
281
286
  return {**PRESETS[self.preset][step], **extra}
282
287
 
283
288
 
289
+ class PipelineError(RuntimeError):
290
+ """The SfS pipeline cannot continue (e.g. too few images left)."""
291
+
292
+
284
293
  @dataclass
285
294
  class SfsImage:
286
295
  """One input image: cube, optional CSM camera, Sun angles and derived files."""
@@ -315,11 +324,11 @@ class SfsPipeline:
315
324
  3. sort by Sun azimuth (``sfs --query``);
316
325
  4. mapproject at native resolution (``prj/``) and bundle-adjust with matches between
317
326
  the mapprojected images (``ba/``);
318
- 5. drop images with no matches, large mapprojected match offsets or mostly large
319
- residuals (``selection/removed.csv``);
327
+ 5. drop images with no matches or mostly large residuals (``selection/removed.csv``);
320
328
  6. register the cameras to the prior DEM, translation only, from stereo DEMs
321
329
  (``stereo/``) or the BA points; apply to all cameras (``align/``, ``ba_align/``);
322
- 7. refine with ``--heights-from-dem`` and filter again (``ba_refine/``);
330
+ 7. refine with ``--heights-from-dem`` and filter again, now also on mapprojected
331
+ match offsets (``ba_refine/``); fewer than ``min_images`` left stops the run;
323
332
  8. mapproject at the target resolution (``prj_sfs/``), write max-lit QA mosaics,
324
333
  all images and per 30° Sun-azimuth bin (``qa/``);
325
334
  9. select an illumination-diverse subset (``selection/``);
@@ -684,6 +693,11 @@ class SfsPipeline:
684
693
  (``ba``, ``ba_refine``) and removes the images of its ``image_rejections`` view,
685
694
  with this config's thresholds. Removed images are logged and appended to
686
695
  ``selection/removed.csv``.
696
+
697
+ At ``ba`` the match-offset rule waits for the refinement when that runs: before
698
+ registration the network floats tens of metres off the DEM, so convergent pairs
699
+ mapproject apart by ``dh * tan(convergence)`` however well they are tied (ASP:
700
+ "do not use this option for any initial evaluation of bundle adjustment").
687
701
  """
688
702
  cfg = self.config
689
703
  files = {
@@ -695,11 +709,17 @@ class SfsPipeline:
695
709
  )
696
710
  }
697
711
  if not any(p.exists() for p in files.values()):
698
- if not is_dry_run():
699
- log.warning("No bundle-adjustment reports under %s; keeping all images", prefix)
700
- return []
712
+ if is_dry_run():
713
+ return []
714
+ raise PipelineError(f"No bundle-adjustment reports under {prefix}")
715
+ overrides: dict[str, Any] = {}
716
+ if stage == "ba" and cfg.max_offset_p85 is not None:
717
+ if cfg.refine:
718
+ overrides["max_offset_p85"] = None
719
+ else:
720
+ log.warning("Match offsets of unregistered cameras overstate misalignment; consider refine")
701
721
  with self.workspace() as ws:
702
- ws.set_rejection_params(cfg)
722
+ ws.set_rejection_params(cfg, **overrides)
703
723
  ws.load_bundle_adjust(prefix, run=Path(prefix).parent.name)
704
724
  flags = ws.rejections(Path(prefix).parent.name)
705
725
  removed = [
@@ -721,6 +741,11 @@ class SfsPipeline:
721
741
  ["product_id", "stage", "reason"],
722
742
  [[r.image.product_id, r.stage, r.reason] for r in self.removed],
723
743
  )
744
+ if len(self.images) < self.config.min_images:
745
+ raise PipelineError(
746
+ f"{len(self.images)} images left after the {removed[0].stage} filter, fewer than "
747
+ f"min_images={self.config.min_images}; see {self.work_dir / 'selection' / 'removed.csv'}"
748
+ )
724
749
  return removed
725
750
 
726
751
  def register(self) -> Path: