setiastrosuitepro 1.6.4__py3-none-any.whl → 1.7.1.post2__py3-none-any.whl
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.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/images/TextureClarity.svg +56 -0
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +364 -33
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/backgroundneutral.py +108 -40
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +150 -55
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +13 -7
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +181 -64
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +245 -15
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +706 -264
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +35 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +184 -8
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +68 -48
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +203 -82
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +81 -29
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +73 -0
- setiastro/saspro/rgbalign.py +460 -12
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/ser_stack_config.py +82 -0
- setiastro/saspro/ser_stacker.py +2321 -0
- setiastro/saspro/ser_stacker_dialog.py +1838 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1625 -0
- setiastro/saspro/sfcc.py +662 -216
- setiastro/saspro/shortcuts.py +171 -33
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1347 -485
- setiastro/saspro/star_alignment.py +247 -123
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +892 -129
- setiastro/saspro/subwindow.py +787 -363
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +84 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +209 -111
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +4 -2
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -631,7 +631,10 @@ class StellarAlignmentDialog(QDialog):
|
|
|
631
631
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
632
632
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
633
633
|
self.setModal(False)
|
|
634
|
-
|
|
634
|
+
try:
|
|
635
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
636
|
+
except Exception:
|
|
637
|
+
pass # older PyQt6 versions
|
|
635
638
|
|
|
636
639
|
self.settings = settings
|
|
637
640
|
self.parent_window = parent
|
|
@@ -4583,11 +4586,12 @@ def load_api_key():
|
|
|
4583
4586
|
class MosaicMasterDialog(QDialog):
|
|
4584
4587
|
def __init__(self, settings: QSettings, parent=None, image_manager=None,
|
|
4585
4588
|
doc_manager=None, wrench_path=None, spinner_path=None,
|
|
4586
|
-
list_open_docs_fn=None):
|
|
4589
|
+
list_open_docs_fn=None):
|
|
4587
4590
|
super().__init__(parent)
|
|
4588
4591
|
self.settings = settings
|
|
4589
4592
|
self.image_manager = image_manager
|
|
4590
4593
|
self._docman = doc_manager or getattr(parent, "doc_manager", None)
|
|
4594
|
+
|
|
4591
4595
|
# same pattern as StellarAlignmentDialog
|
|
4592
4596
|
if list_open_docs_fn is None:
|
|
4593
4597
|
cand = getattr(parent, "_list_open_docs", None)
|
|
@@ -4599,19 +4603,22 @@ class MosaicMasterDialog(QDialog):
|
|
|
4599
4603
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
4600
4604
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
4601
4605
|
self.setModal(False)
|
|
4606
|
+
|
|
4602
4607
|
self.wrench_path = wrench_path
|
|
4603
4608
|
self.spinner_path = spinner_path
|
|
4604
4609
|
|
|
4605
4610
|
self.resize(600, 400)
|
|
4606
|
-
self.loaded_images = []
|
|
4611
|
+
self.loaded_images = []
|
|
4607
4612
|
self.final_mosaic = None
|
|
4608
4613
|
self.weight_mosaic = None
|
|
4609
4614
|
self.wcs_metadata = None # To store mosaic WCS header
|
|
4610
4615
|
self.astap_exe = self.settings.value("paths/astap", "", type=str)
|
|
4616
|
+
|
|
4611
4617
|
# Variables to store stretching parameters:
|
|
4612
4618
|
self.stretch_original_mins = []
|
|
4613
4619
|
self.stretch_original_medians = []
|
|
4614
4620
|
self.was_single_channel = False
|
|
4621
|
+
|
|
4615
4622
|
self.initUI()
|
|
4616
4623
|
|
|
4617
4624
|
def initUI(self):
|
|
@@ -4632,12 +4639,11 @@ class MosaicMasterDialog(QDialog):
|
|
|
4632
4639
|
layout.addWidget(instructions)
|
|
4633
4640
|
|
|
4634
4641
|
btn_layout = QHBoxLayout()
|
|
4635
|
-
|
|
4642
|
+
|
|
4636
4643
|
add_btn = QPushButton("Add Image")
|
|
4637
4644
|
add_btn.clicked.connect(self.add_image)
|
|
4638
4645
|
btn_layout.addWidget(add_btn)
|
|
4639
4646
|
|
|
4640
|
-
# New button to add an image from one of the ImageManager slots
|
|
4641
4647
|
add_from_view_btn = QPushButton("Add from View")
|
|
4642
4648
|
add_from_view_btn.setToolTip("Add an image from any open View")
|
|
4643
4649
|
add_from_view_btn.clicked.connect(self.add_image_from_view)
|
|
@@ -4659,25 +4665,80 @@ class MosaicMasterDialog(QDialog):
|
|
|
4659
4665
|
save_btn.clicked.connect(self.save_mosaic_to_new_view)
|
|
4660
4666
|
btn_layout.addWidget(save_btn)
|
|
4661
4667
|
|
|
4662
|
-
# Add the wrench button for settings.
|
|
4663
4668
|
wrench_btn = QPushButton()
|
|
4664
|
-
|
|
4669
|
+
if self.wrench_path:
|
|
4670
|
+
wrench_btn.setIcon(QIcon(self.wrench_path))
|
|
4665
4671
|
wrench_btn.setToolTip("Mosaic Settings")
|
|
4666
4672
|
wrench_btn.clicked.connect(self.openSettings)
|
|
4667
4673
|
btn_layout.addWidget(wrench_btn)
|
|
4668
4674
|
|
|
4669
4675
|
layout.addLayout(btn_layout)
|
|
4670
4676
|
|
|
4671
|
-
#
|
|
4677
|
+
# ------------------------------------------------------------------
|
|
4678
|
+
# Mode checkboxes (mutually exclusive: Seestar vs WCS-only)
|
|
4679
|
+
# ------------------------------------------------------------------
|
|
4672
4680
|
checkbox_layout = QHBoxLayout()
|
|
4681
|
+
|
|
4673
4682
|
self.forceBlindCheckBox = QCheckBox("Force Blind Solve (ignore existing WCS)")
|
|
4674
4683
|
checkbox_layout.addWidget(self.forceBlindCheckBox)
|
|
4675
|
-
|
|
4684
|
+
|
|
4676
4685
|
self.seestarCheckBox = QCheckBox("Seestar Mode")
|
|
4677
|
-
self.seestarCheckBox.setToolTip(
|
|
4686
|
+
self.seestarCheckBox.setToolTip(
|
|
4687
|
+
"When enabled, images are aligned iteratively using astroalign without plate solving."
|
|
4688
|
+
)
|
|
4678
4689
|
checkbox_layout.addWidget(self.seestarCheckBox)
|
|
4690
|
+
|
|
4691
|
+
self.wcsOnlyCheckBox = QCheckBox("Disable Star Alignment (WCS placement only)")
|
|
4692
|
+
self.wcsOnlyCheckBox.setToolTip(
|
|
4693
|
+
"Skips astroalign + refined alignment.\n"
|
|
4694
|
+
"Panels are only reprojected into the mosaic celestial-sphere frame using WCS, then blended.\n"
|
|
4695
|
+
"Useful when panels have little/no overlap but have valid WCS."
|
|
4696
|
+
)
|
|
4697
|
+
checkbox_layout.addWidget(self.wcsOnlyCheckBox)
|
|
4698
|
+
|
|
4679
4699
|
layout.addLayout(checkbox_layout)
|
|
4680
4700
|
|
|
4701
|
+
# Persisted WCS-only
|
|
4702
|
+
_settings = QSettings("SetiAstro", "SASpro")
|
|
4703
|
+
self.wcsOnlyCheckBox.setChecked(_settings.value("mosaic/wcs_only", False, type=bool))
|
|
4704
|
+
|
|
4705
|
+
# Helpers ----------------------------------------------------------
|
|
4706
|
+
def _set_checked(cb: QCheckBox, checked: bool):
|
|
4707
|
+
cb.blockSignals(True)
|
|
4708
|
+
cb.setChecked(checked)
|
|
4709
|
+
cb.blockSignals(False)
|
|
4710
|
+
|
|
4711
|
+
def _sync_wcs_only_ui():
|
|
4712
|
+
# WCS-only disables transform selection (because refinement is unused)
|
|
4713
|
+
wcs_only = self.wcsOnlyCheckBox.isChecked()
|
|
4714
|
+
if hasattr(self, "transform_combo"):
|
|
4715
|
+
self.transform_combo.setEnabled(not wcs_only)
|
|
4716
|
+
self.transform_combo.setToolTip(
|
|
4717
|
+
"" if not wcs_only else "Disabled because WCS-only placement is enabled."
|
|
4718
|
+
)
|
|
4719
|
+
|
|
4720
|
+
def _on_seestar_changed(state: int):
|
|
4721
|
+
# If Seestar is turned ON, force WCS-only OFF
|
|
4722
|
+
if self.seestarCheckBox.isChecked():
|
|
4723
|
+
_set_checked(self.wcsOnlyCheckBox, False)
|
|
4724
|
+
_sync_wcs_only_ui()
|
|
4725
|
+
|
|
4726
|
+
def _on_wcs_only_changed(state: int):
|
|
4727
|
+
# Persist setting first (this one is user-visible preference)
|
|
4728
|
+
QSettings("SetiAstro", "SASpro").setValue("mosaic/wcs_only", self.wcsOnlyCheckBox.isChecked())
|
|
4729
|
+
|
|
4730
|
+
# If WCS-only is turned ON, force Seestar OFF
|
|
4731
|
+
if self.wcsOnlyCheckBox.isChecked():
|
|
4732
|
+
_set_checked(self.seestarCheckBox, False)
|
|
4733
|
+
_sync_wcs_only_ui()
|
|
4734
|
+
|
|
4735
|
+
# Wire handlers (NO lambdas, no duplicates)
|
|
4736
|
+
self.seestarCheckBox.stateChanged.connect(_on_seestar_changed)
|
|
4737
|
+
self.wcsOnlyCheckBox.stateChanged.connect(_on_wcs_only_changed)
|
|
4738
|
+
|
|
4739
|
+
# ------------------------------------------------------------------
|
|
4740
|
+
# Other controls
|
|
4741
|
+
# ------------------------------------------------------------------
|
|
4681
4742
|
self.normalizeCheckBox = QCheckBox("Normalize images (median match)")
|
|
4682
4743
|
self.normalizeCheckBox.setChecked(True)
|
|
4683
4744
|
layout.addWidget(self.normalizeCheckBox)
|
|
@@ -4701,24 +4762,21 @@ class MosaicMasterDialog(QDialog):
|
|
|
4701
4762
|
"Precise — Full WCS: astropy.reproject per channel; slowest, most exact."
|
|
4702
4763
|
)
|
|
4703
4764
|
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
_default_mode
|
|
4707
|
-
"Fast — SIP-aware (Exact Remap)")
|
|
4708
|
-
if _default_mode not in [self.reprojectModeCombo.itemText(i) for i in range(self.reprojectModeCombo.count())]:
|
|
4765
|
+
_default_mode = _settings.value("mosaic/reproject_mode", "Fast — SIP-aware (Exact Remap)")
|
|
4766
|
+
valid_modes = [self.reprojectModeCombo.itemText(i) for i in range(self.reprojectModeCombo.count())]
|
|
4767
|
+
if _default_mode not in valid_modes:
|
|
4709
4768
|
_default_mode = "Fast — SIP-aware (Exact Remap)"
|
|
4710
4769
|
self.reprojectModeCombo.setCurrentText(_default_mode)
|
|
4711
4770
|
self.reprojectModeCombo.currentTextChanged.connect(
|
|
4712
4771
|
lambda t: QSettings("SetiAstro", "SASpro").setValue("mosaic/reproject_mode", t)
|
|
4713
4772
|
)
|
|
4714
4773
|
|
|
4715
|
-
# Add to layout where the old checkbox lived
|
|
4716
4774
|
row = QHBoxLayout()
|
|
4717
4775
|
row.addWidget(self.reprojectModeLabel)
|
|
4718
4776
|
row.addWidget(self.reprojectModeCombo, 1)
|
|
4719
4777
|
layout.addLayout(row)
|
|
4720
4778
|
|
|
4721
|
-
|
|
4779
|
+
# Transform selection
|
|
4722
4780
|
self.transform_combo = QComboBox()
|
|
4723
4781
|
self.transform_combo.addItems([
|
|
4724
4782
|
"Partial Affine Transform",
|
|
@@ -4726,11 +4784,14 @@ class MosaicMasterDialog(QDialog):
|
|
|
4726
4784
|
"Homography Transform",
|
|
4727
4785
|
"Polynomial Warp Based Transform"
|
|
4728
4786
|
])
|
|
4729
|
-
# Set the default selection to "Affine Transform" (index 1)
|
|
4730
4787
|
self.transform_combo.setCurrentIndex(1)
|
|
4731
4788
|
layout.addWidget(QLabel("Select Transformation Method:"))
|
|
4732
4789
|
layout.addWidget(self.transform_combo)
|
|
4733
4790
|
|
|
4791
|
+
# Now that transform_combo exists, apply WCS-only UI state
|
|
4792
|
+
_sync_wcs_only_ui()
|
|
4793
|
+
|
|
4794
|
+
# List + status + spinner
|
|
4734
4795
|
self.images_list = QListWidget()
|
|
4735
4796
|
self.images_list.setSelectionMode(self.images_list.SelectionMode.SingleSelection)
|
|
4736
4797
|
layout.addWidget(self.images_list)
|
|
@@ -4740,13 +4801,15 @@ class MosaicMasterDialog(QDialog):
|
|
|
4740
4801
|
|
|
4741
4802
|
self.spinnerLabel = QLabel(self)
|
|
4742
4803
|
self.spinnerLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
4743
|
-
self.spinnerMovie = QMovie(self.spinner_path)
|
|
4744
|
-
self.
|
|
4745
|
-
|
|
4804
|
+
self.spinnerMovie = QMovie(self.spinner_path) if self.spinner_path else None
|
|
4805
|
+
if self.spinnerMovie:
|
|
4806
|
+
self.spinnerLabel.setMovie(self.spinnerMovie)
|
|
4807
|
+
self.spinnerLabel.hide()
|
|
4746
4808
|
layout.addWidget(self.spinnerLabel)
|
|
4747
4809
|
|
|
4748
4810
|
self.setLayout(layout)
|
|
4749
4811
|
|
|
4812
|
+
|
|
4750
4813
|
def _target_median_from_first(self, items):
|
|
4751
4814
|
# Pick a stable target (median of first image after safe clipping)
|
|
4752
4815
|
if not items:
|
|
@@ -5293,6 +5356,7 @@ class MosaicMasterDialog(QDialog):
|
|
|
5293
5356
|
|
|
5294
5357
|
# ---------- Align (Entry Point) ----------
|
|
5295
5358
|
def align_images(self):
|
|
5359
|
+
# Seestar mode is its own pipeline
|
|
5296
5360
|
if self.seestarCheckBox.isChecked():
|
|
5297
5361
|
self.align_images_seestar_mode()
|
|
5298
5362
|
return
|
|
@@ -5303,28 +5367,33 @@ class MosaicMasterDialog(QDialog):
|
|
|
5303
5367
|
|
|
5304
5368
|
# Show spinner and start animation.
|
|
5305
5369
|
self.spinnerLabel.show()
|
|
5306
|
-
self.spinnerMovie
|
|
5370
|
+
if self.spinnerMovie:
|
|
5371
|
+
self.spinnerMovie.start()
|
|
5307
5372
|
QApplication.processEvents()
|
|
5308
5373
|
|
|
5309
|
-
#
|
|
5374
|
+
# ------------------------------------------------------------
|
|
5375
|
+
# 1) Plate solve (unless already solved and not forcing blind)
|
|
5376
|
+
# ------------------------------------------------------------
|
|
5310
5377
|
force_blind = self.forceBlindCheckBox.isChecked()
|
|
5311
|
-
images_to_process = (
|
|
5312
|
-
|
|
5378
|
+
images_to_process = (
|
|
5379
|
+
self.loaded_images if force_blind
|
|
5380
|
+
else [item for item in self.loaded_images if item.get("wcs") is None]
|
|
5381
|
+
)
|
|
5313
5382
|
|
|
5314
|
-
# Process each image for plate solving.
|
|
5315
5383
|
for item in images_to_process:
|
|
5316
|
-
#
|
|
5384
|
+
# Ensure ASTAP path (or fall back to blind solve)
|
|
5317
5385
|
if not self.astap_exe or not os.path.exists(self.astap_exe):
|
|
5318
5386
|
executable_filter = "Executables (*.exe);;All Files (*)" if sys.platform.startswith("win") else "Executables (*)"
|
|
5319
5387
|
new_path, _ = QFileDialog.getOpenFileName(self, "Select ASTAP Executable", "", executable_filter)
|
|
5320
5388
|
if new_path:
|
|
5321
5389
|
self._save_astap_exe_to_settings(new_path)
|
|
5390
|
+
self.astap_exe = new_path
|
|
5322
5391
|
QMessageBox.information(self, "Mosaic Master", "ASTAP path updated successfully.")
|
|
5323
5392
|
else:
|
|
5324
|
-
|
|
5393
|
+
self.status_label.setText(f"No ASTAP path; blind solving {item['path']}...")
|
|
5394
|
+
QApplication.processEvents()
|
|
5325
5395
|
solved_header = self.perform_blind_solve(item)
|
|
5326
5396
|
if solved_header:
|
|
5327
|
-
# normalize + build WCS with relax=True so SIP is retained
|
|
5328
5397
|
solved_header = self._normalize_wcs_header(solved_header, item["image"].shape)
|
|
5329
5398
|
item["wcs"] = self._build_wcs(solved_header, item["image"].shape)
|
|
5330
5399
|
continue
|
|
@@ -5338,49 +5407,53 @@ class MosaicMasterDialog(QDialog):
|
|
|
5338
5407
|
self.status_label.setText(f"ASTAP failed for {item['path']}. Falling back to blind solve...")
|
|
5339
5408
|
QApplication.processEvents()
|
|
5340
5409
|
solved_header = self.perform_blind_solve(item)
|
|
5341
|
-
else:
|
|
5342
|
-
self.status_label.setText(f"Plate solve successful using ASTAP for {item['path']}.")
|
|
5343
5410
|
|
|
5344
5411
|
if solved_header:
|
|
5345
|
-
# Single, centralized sanitize → keeps SIP and fixes types
|
|
5346
5412
|
solved_header = self._normalize_wcs_header(solved_header, item["image"].shape)
|
|
5347
5413
|
item["wcs"] = self._build_wcs(solved_header, item["image"].shape)
|
|
5348
5414
|
else:
|
|
5349
|
-
print(f"Plate solving failed for {item['path']}.")
|
|
5415
|
+
print(f"[Mosaic] Plate solving failed for {item['path']}.")
|
|
5350
5416
|
|
|
5351
|
-
#
|
|
5417
|
+
# ------------------------------------------------------------
|
|
5418
|
+
# 2) Gather WCS-valid panels
|
|
5419
|
+
# ------------------------------------------------------------
|
|
5352
5420
|
wcs_items = [x for x in self.loaded_images if x.get("wcs") is not None]
|
|
5353
5421
|
if not wcs_items:
|
|
5354
|
-
print("No images have WCS
|
|
5355
|
-
self.spinnerMovie
|
|
5422
|
+
print("[Mosaic] No images have WCS; cannot build WCS mosaic.")
|
|
5423
|
+
if self.spinnerMovie:
|
|
5424
|
+
self.spinnerMovie.stop()
|
|
5356
5425
|
self.spinnerLabel.hide()
|
|
5357
5426
|
return
|
|
5358
5427
|
|
|
5428
|
+
# ------------------------------------------------------------
|
|
5429
|
+
# 3) Establish mosaic WCS + output bounding box
|
|
5430
|
+
# ------------------------------------------------------------
|
|
5431
|
+
reference_wcs = self._build_wcs(
|
|
5432
|
+
wcs_items[0]["wcs"].to_header(relax=True),
|
|
5433
|
+
wcs_items[0]["image"].shape
|
|
5434
|
+
).deepcopy()
|
|
5359
5435
|
|
|
5360
|
-
# Use the first image's WCS as reference and compute the mosaic bounding box.
|
|
5361
|
-
# (Rebuild with relax=True just in case, then deepcopy.)
|
|
5362
|
-
reference_wcs = self._build_wcs(wcs_items[0]["wcs"].to_header(relax=True), wcs_items[0]["image"].shape).deepcopy()
|
|
5363
5436
|
min_x, min_y, max_x, max_y = self.compute_mosaic_bounding_box(wcs_items, reference_wcs)
|
|
5364
|
-
mosaic_width
|
|
5437
|
+
mosaic_width = int(max_x - min_x)
|
|
5365
5438
|
mosaic_height = int(max_y - min_y)
|
|
5366
5439
|
|
|
5367
5440
|
if mosaic_width < 1 or mosaic_height < 1:
|
|
5368
|
-
print("ERROR: Computed mosaic size
|
|
5369
|
-
self.spinnerMovie
|
|
5441
|
+
print("[Mosaic] ERROR: Computed mosaic size invalid. Check WCS/inputs.")
|
|
5442
|
+
if self.spinnerMovie:
|
|
5443
|
+
self.spinnerMovie.stop()
|
|
5370
5444
|
self.spinnerLabel.hide()
|
|
5371
5445
|
return
|
|
5372
5446
|
|
|
5373
|
-
# Adjust the reference WCS so that (min_x, min_y) becomes (0,0).
|
|
5374
5447
|
mosaic_wcs = reference_wcs.deepcopy()
|
|
5375
5448
|
mosaic_wcs.wcs.crpix[0] -= min_x
|
|
5376
5449
|
mosaic_wcs.wcs.crpix[1] -= min_y
|
|
5377
|
-
# keep SIP in the stored header
|
|
5378
5450
|
self.wcs_metadata = mosaic_wcs.to_header(relax=True)
|
|
5379
5451
|
|
|
5380
|
-
#
|
|
5452
|
+
# ------------------------------------------------------------
|
|
5453
|
+
# 4) Allocate accumulators
|
|
5454
|
+
# ------------------------------------------------------------
|
|
5381
5455
|
is_color = any(not item["is_mono"] for item in wcs_items)
|
|
5382
5456
|
|
|
5383
|
-
# stats for optional "unstretch"
|
|
5384
5457
|
self.stretch_original_mins = []
|
|
5385
5458
|
self.stretch_original_medians = []
|
|
5386
5459
|
self.was_single_channel = (not is_color)
|
|
@@ -5391,49 +5464,77 @@ class MosaicMasterDialog(QDialog):
|
|
|
5391
5464
|
self.final_mosaic, _ = smart_zeros((mosaic_height, mosaic_width), dtype=np.float32)
|
|
5392
5465
|
self.weight_mosaic, _ = smart_zeros((mosaic_height, mosaic_width), dtype=np.float32)
|
|
5393
5466
|
|
|
5467
|
+
# ------------------------------------------------------------
|
|
5468
|
+
# 5) Normalization target — compute ONCE, apply ALWAYS
|
|
5469
|
+
# ------------------------------------------------------------
|
|
5470
|
+
did_normalize = bool(self.normalizeCheckBox.isChecked())
|
|
5471
|
+
|
|
5472
|
+
# IMPORTANT: compute from RAW first panel image, not from dict list in a way that can drift
|
|
5473
|
+
if did_normalize:
|
|
5474
|
+
try:
|
|
5475
|
+
first_raw = wcs_items[0]["image"]
|
|
5476
|
+
self._mosaic_target_median = self._target_median_from_first([{"image": first_raw}])
|
|
5477
|
+
except Exception:
|
|
5478
|
+
# fallback: compute directly
|
|
5479
|
+
a0 = wcs_items[0]["image"].astype(np.float32, copy=False)
|
|
5480
|
+
if a0.ndim == 3:
|
|
5481
|
+
a0m = np.mean(a0, axis=2)
|
|
5482
|
+
else:
|
|
5483
|
+
a0m = a0
|
|
5484
|
+
lo = np.percentile(a0m, 1)
|
|
5485
|
+
hi = np.percentile(a0m, 99)
|
|
5486
|
+
self._mosaic_target_median = float(max(np.median(np.clip(a0m, lo, hi)), 1e-6))
|
|
5487
|
+
|
|
5488
|
+
print(f"[Mosaic] normalization target median = {self._mosaic_target_median:.6g}")
|
|
5489
|
+
|
|
5490
|
+
# Reprojection helpers cache
|
|
5491
|
+
if not hasattr(self, "_H_cache"):
|
|
5492
|
+
self._H_cache = {}
|
|
5493
|
+
|
|
5494
|
+
# WCS-only toggle (no star alignment/refinement)
|
|
5495
|
+
wcs_only = bool(getattr(self, "wcsOnlyCheckBox", None) and self.wcsOnlyCheckBox.isChecked())
|
|
5496
|
+
|
|
5497
|
+
# ------------------------------------------------------------
|
|
5498
|
+
# 6) Main loop: normalize -> reproject -> (optional) star align -> accumulate
|
|
5499
|
+
# ------------------------------------------------------------
|
|
5394
5500
|
first_image = True
|
|
5501
|
+
|
|
5395
5502
|
for idx, itm in enumerate(wcs_items):
|
|
5396
5503
|
arr = itm["image"]
|
|
5504
|
+
|
|
5397
5505
|
self.status_label.setText(f"Mapping {itm['path']} into mosaic frame...")
|
|
5398
5506
|
QApplication.processEvents()
|
|
5399
5507
|
|
|
5400
5508
|
img_lin = arr.astype(np.float32, copy=False)
|
|
5401
5509
|
|
|
5402
|
-
# --- record original stats for optional
|
|
5510
|
+
# --- record original stats (pre-normalization) for optional unstretch ---
|
|
5403
5511
|
mono_for_stats = img_lin if img_lin.ndim == 2 else np.mean(img_lin, axis=2)
|
|
5404
5512
|
self.stretch_original_mins.append(float(np.min(mono_for_stats)))
|
|
5405
5513
|
self.stretch_original_medians.append(float(np.median(mono_for_stats)))
|
|
5406
5514
|
|
|
5407
|
-
#
|
|
5408
|
-
if
|
|
5409
|
-
|
|
5410
|
-
if target_med is None:
|
|
5411
|
-
self._mosaic_target_median = self._target_median_from_first(wcs_items)
|
|
5412
|
-
target_med = self._mosaic_target_median
|
|
5413
|
-
img_lin = self._normalize_linear(img_lin, target_med)
|
|
5414
|
-
|
|
5415
|
-
# 2) Reprojection (3 modes)
|
|
5416
|
-
if not hasattr(self, "_H_cache"):
|
|
5417
|
-
self._H_cache = {}
|
|
5515
|
+
# --- ALWAYS normalize here if enabled (even in WCS-only) ---
|
|
5516
|
+
if did_normalize:
|
|
5517
|
+
img_lin = self._normalize_linear(img_lin, float(self._mosaic_target_median))
|
|
5418
5518
|
|
|
5519
|
+
# --- Reprojection mode ---
|
|
5419
5520
|
mode = self.reprojectModeCombo.currentText()
|
|
5420
5521
|
|
|
5421
5522
|
if mode.startswith("Fast — SIP"):
|
|
5422
|
-
# Exact dense remap (SIP-aware), tiled; keep mono as 2D
|
|
5423
5523
|
reprojected = self._warp_via_wcs_remap_exact(
|
|
5424
5524
|
img_lin, itm["wcs"], mosaic_wcs, (mosaic_height, mosaic_width), tile=512
|
|
5425
|
-
).astype(np.float32)
|
|
5525
|
+
).astype(np.float32, copy=False)
|
|
5526
|
+
reprojected = self._polish_reprojected(reprojected)
|
|
5426
5527
|
reproj_red = reprojected[..., 0] if reprojected.ndim == 3 else reprojected
|
|
5427
5528
|
|
|
5428
5529
|
elif mode.startswith("Fast — Homography"):
|
|
5429
|
-
# Single global homography; keep mono as 2D
|
|
5430
5530
|
reprojected = self._warp_via_wcs_homography(
|
|
5431
5531
|
img_lin, itm["wcs"], mosaic_wcs, (mosaic_height, mosaic_width), H_cache=self._H_cache
|
|
5432
|
-
).astype(np.float32)
|
|
5532
|
+
).astype(np.float32, copy=False)
|
|
5533
|
+
reprojected = self._polish_reprojected(reprojected)
|
|
5433
5534
|
reproj_red = reprojected[..., 0] if reprojected.ndim == 3 else reprojected
|
|
5434
5535
|
|
|
5435
5536
|
else:
|
|
5436
|
-
# Precise — Full WCS
|
|
5537
|
+
# Precise — Full WCS
|
|
5437
5538
|
if img_lin.ndim == 3:
|
|
5438
5539
|
channels = []
|
|
5439
5540
|
for c in range(3):
|
|
@@ -5443,52 +5544,49 @@ class MosaicMasterDialog(QDialog):
|
|
|
5443
5544
|
reprojected = np.stack(channels, axis=-1)
|
|
5444
5545
|
reproj_red = reprojected[..., 0]
|
|
5445
5546
|
else:
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
reprojected = np.nan_to_num(
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
self.status_label.setText(f"WCS map: {itm['path']} processed.")
|
|
5452
|
-
QApplication.processEvents()
|
|
5453
|
-
|
|
5454
|
-
# --- Stellar Alignment ---
|
|
5455
|
-
if not first_image:
|
|
5456
|
-
transform_method = self.transform_combo.currentText()
|
|
5457
|
-
mosaic_gray = (self.final_mosaic if self.final_mosaic.ndim == 2
|
|
5458
|
-
else np.mean(self.final_mosaic, axis=-1))
|
|
5459
|
-
try:
|
|
5460
|
-
self.status_label.setText("Computing affine transform with astroalign...")
|
|
5461
|
-
QApplication.processEvents()
|
|
5462
|
-
transform_obj, (src_pts, dst_pts) = self._aa_find_transform_with_backoff(reproj_red, mosaic_gray)
|
|
5463
|
-
transform_matrix = transform_obj.params[0:2, :].astype(np.float32)
|
|
5464
|
-
self.status_label.setText("Astroalign computed transform successfully.")
|
|
5465
|
-
except Exception as e:
|
|
5466
|
-
self.status_label.setText(f"Astroalign failed: {e}. Using identity transform.")
|
|
5467
|
-
transform_matrix = np.eye(2, 3, dtype=np.float32)
|
|
5547
|
+
rpj, _ = reproject_interp((img_lin, itm["wcs"]), mosaic_wcs,
|
|
5548
|
+
shape_out=(mosaic_height, mosaic_width))
|
|
5549
|
+
reprojected = np.nan_to_num(rpj, nan=0.0).astype(np.float32)
|
|
5550
|
+
reproj_red = reprojected # 2D
|
|
5468
5551
|
|
|
5469
|
-
|
|
5470
|
-
|
|
5471
|
-
|
|
5472
|
-
|
|
5552
|
+
# --- Optional star alignment/refinement (skipped in WCS-only) ---
|
|
5553
|
+
if wcs_only:
|
|
5554
|
+
aligned = reprojected
|
|
5555
|
+
if first_image:
|
|
5556
|
+
first_image = False
|
|
5557
|
+
else:
|
|
5558
|
+
if first_image:
|
|
5559
|
+
aligned = reprojected
|
|
5560
|
+
first_image = False
|
|
5561
|
+
else:
|
|
5562
|
+
transform_method = self.transform_combo.currentText()
|
|
5563
|
+
mosaic_gray = (self.final_mosaic if self.final_mosaic.ndim == 2
|
|
5564
|
+
else np.mean(self.final_mosaic, axis=-1))
|
|
5473
5565
|
|
|
5474
|
-
|
|
5475
|
-
|
|
5476
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5566
|
+
try:
|
|
5567
|
+
self.status_label.setText("Computing affine transform with astroalign...")
|
|
5568
|
+
QApplication.processEvents()
|
|
5569
|
+
transform_obj, (src_pts, dst_pts) = self._aa_find_transform_with_backoff(reproj_red, mosaic_gray)
|
|
5570
|
+
transform_matrix = transform_obj.params[0:2, :].astype(np.float32)
|
|
5571
|
+
except Exception as e:
|
|
5572
|
+
self.status_label.setText(f"Astroalign failed: {e}. Using identity transform.")
|
|
5573
|
+
transform_matrix = np.eye(2, 3, dtype=np.float32)
|
|
5479
5574
|
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
|
|
5486
|
-
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
|
|
5490
|
-
|
|
5491
|
-
|
|
5575
|
+
affine_aligned = cv2.warpAffine(
|
|
5576
|
+
reprojected, transform_matrix, (mosaic_width, mosaic_height),
|
|
5577
|
+
flags=cv2.INTER_LANCZOS4
|
|
5578
|
+
)
|
|
5579
|
+
aligned = affine_aligned
|
|
5580
|
+
|
|
5581
|
+
if transform_method in ["Homography Transform", "Polynomial Warp Based Transform"]:
|
|
5582
|
+
self.status_label.setText(f"Starting refined alignment using {transform_method}...")
|
|
5583
|
+
QApplication.processEvents()
|
|
5584
|
+
refined_result = self.refined_alignment(affine_aligned, mosaic_gray, method=transform_method)
|
|
5585
|
+
if refined_result is not None:
|
|
5586
|
+
aligned, best_inliers2 = refined_result
|
|
5587
|
+
self.status_label.setText(f"Refined alignment succeeded with {best_inliers2} inliers.")
|
|
5588
|
+
else:
|
|
5589
|
+
self.status_label.setText("Refined alignment failed; falling back to affine alignment.")
|
|
5492
5590
|
|
|
5493
5591
|
# If mosaic is color but aligned is mono, expand for accumulation only
|
|
5494
5592
|
if is_color and aligned.ndim == 2:
|
|
@@ -5496,13 +5594,13 @@ class MosaicMasterDialog(QDialog):
|
|
|
5496
5594
|
|
|
5497
5595
|
gray_aligned = aligned[..., 0] if aligned.ndim == 3 else aligned
|
|
5498
5596
|
|
|
5499
|
-
#
|
|
5597
|
+
# --- Weight mask ---
|
|
5500
5598
|
binary_mask = (gray_aligned > 0).astype(np.uint8)
|
|
5501
5599
|
smooth_mask = cv2.distanceTransform(binary_mask, cv2.DIST_L2, 5)
|
|
5502
5600
|
smooth_mask = (smooth_mask / np.max(smooth_mask)) if np.max(smooth_mask) > 0 else binary_mask.astype(np.float32)
|
|
5503
5601
|
smooth_mask = cv2.GaussianBlur(smooth_mask, (15, 15), 0)
|
|
5504
5602
|
|
|
5505
|
-
# Accumulate
|
|
5603
|
+
# --- Accumulate ---
|
|
5506
5604
|
if is_color:
|
|
5507
5605
|
self.final_mosaic += aligned * smooth_mask[..., np.newaxis]
|
|
5508
5606
|
else:
|
|
@@ -5512,22 +5610,27 @@ class MosaicMasterDialog(QDialog):
|
|
|
5512
5610
|
self.status_label.setText(f"Processed: {itm['path']}")
|
|
5513
5611
|
QApplication.processEvents()
|
|
5514
5612
|
|
|
5515
|
-
#
|
|
5516
|
-
|
|
5613
|
+
# ------------------------------------------------------------
|
|
5614
|
+
# 7) Final blend
|
|
5615
|
+
# ------------------------------------------------------------
|
|
5517
5616
|
if is_color:
|
|
5518
|
-
self.final_mosaic = np.where(
|
|
5519
|
-
|
|
5520
|
-
|
|
5617
|
+
self.final_mosaic = np.where(
|
|
5618
|
+
self.weight_mosaic[..., None] > 0,
|
|
5619
|
+
self.final_mosaic / np.maximum(self.weight_mosaic[..., None], 1e-12),
|
|
5620
|
+
self.final_mosaic
|
|
5621
|
+
)
|
|
5521
5622
|
else:
|
|
5522
|
-
|
|
5623
|
+
nz = (self.weight_mosaic > 0)
|
|
5624
|
+
self.final_mosaic[nz] = self.final_mosaic[nz] / np.maximum(self.weight_mosaic[nz], 1e-12)
|
|
5523
5625
|
|
|
5524
|
-
|
|
5525
|
-
|
|
5626
|
+
self.status_label.setText("Mosaic built. De-normalizing mosaic...")
|
|
5627
|
+
QApplication.processEvents()
|
|
5526
5628
|
|
|
5527
|
-
#
|
|
5528
|
-
|
|
5629
|
+
# ------------------------------------------------------------
|
|
5630
|
+
# 8) Optional “unstretch” (your existing logic)
|
|
5631
|
+
# ------------------------------------------------------------
|
|
5529
5632
|
if (did_normalize and
|
|
5530
|
-
hasattr(self, "_mosaic_target_median") and self._mosaic_target_median > 0 and
|
|
5633
|
+
hasattr(self, "_mosaic_target_median") and float(self._mosaic_target_median) > 0 and
|
|
5531
5634
|
getattr(self, "stretch_original_medians", None) and len(self.stretch_original_medians) > 0 and
|
|
5532
5635
|
getattr(self, "stretch_original_mins", None) and len(self.stretch_original_mins) > 0):
|
|
5533
5636
|
self.final_mosaic = self.unstretch_image(self.final_mosaic)
|
|
@@ -5535,16 +5638,37 @@ class MosaicMasterDialog(QDialog):
|
|
|
5535
5638
|
self.status_label.setText("Final Mosaic Ready.")
|
|
5536
5639
|
QApplication.processEvents()
|
|
5537
5640
|
|
|
5538
|
-
display_image = (np.stack([self.final_mosaic]*3, axis=-1)
|
|
5641
|
+
display_image = (np.stack([self.final_mosaic] * 3, axis=-1)
|
|
5539
5642
|
if self.final_mosaic.ndim == 2 else self.final_mosaic)
|
|
5540
5643
|
display_image = self._autostretch_if_requested(display_image)
|
|
5541
|
-
MosaicPreviewWindow(display_image, title="Final Mosaic", parent=self,
|
|
5542
|
-
push_cb=self._push_mosaic_to_new_doc).show()
|
|
5543
5644
|
|
|
5544
|
-
|
|
5645
|
+
MosaicPreviewWindow(
|
|
5646
|
+
display_image,
|
|
5647
|
+
title=("Final Mosaic (WCS-only)" if wcs_only else "Final Mosaic"),
|
|
5648
|
+
parent=self,
|
|
5649
|
+
push_cb=self._push_mosaic_to_new_doc
|
|
5650
|
+
).show()
|
|
5651
|
+
|
|
5652
|
+
if self.spinnerMovie:
|
|
5653
|
+
self.spinnerMovie.stop()
|
|
5545
5654
|
self.spinnerLabel.hide()
|
|
5546
5655
|
QApplication.processEvents()
|
|
5656
|
+
|
|
5547
5657
|
|
|
5658
|
+
def _polish_reprojected(self, img: np.ndarray) -> np.ndarray:
|
|
5659
|
+
# 1) kill NaNs/Infs from reprojection
|
|
5660
|
+
out = np.nan_to_num(img, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
|
|
5661
|
+
|
|
5662
|
+
# 2) trim tiny negative ringing
|
|
5663
|
+
out[out < 0] = 0.0
|
|
5664
|
+
|
|
5665
|
+
# 3) optional: zero a 1px border to avoid remap edge seams
|
|
5666
|
+
if out.ndim == 2:
|
|
5667
|
+
out[:1, :] = 0; out[-1:, :] = 0; out[:, :1] = 0; out[:, -1:] = 0
|
|
5668
|
+
else:
|
|
5669
|
+
out[:1, :, :] = 0; out[-1:, :, :] = 0; out[:, :1, :] = 0; out[:, -1:, :] = 0
|
|
5670
|
+
|
|
5671
|
+
return out
|
|
5548
5672
|
|
|
5549
5673
|
def debayer_image(self, image, file_path, header):
|
|
5550
5674
|
from setiastro.saspro.legacy.numba_utils import debayer_raw_fast, debayer_fits_fast
|
setiastro/saspro/star_spikes.py
CHANGED
|
@@ -53,6 +53,10 @@ class StarSpikesDialogPro(QDialog):
|
|
|
53
53
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
54
54
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
55
55
|
self.setModal(False)
|
|
56
|
+
try:
|
|
57
|
+
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
58
|
+
except Exception:
|
|
59
|
+
pass # older PyQt6 versions
|
|
56
60
|
self.docman = doc_manager
|
|
57
61
|
self.doc = initial_doc or (self.docman.get_active_document() if self.docman else None)
|
|
58
62
|
self.jwstpupil_path = jwstpupil_path
|