setiastrosuitepro 1.7.3__py3-none-any.whl → 1.7.4__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/saspro/__init__.py +15 -4
- setiastro/saspro/__main__.py +23 -5
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +4 -4
- setiastro/saspro/autostretch.py +29 -18
- setiastro/saspro/gui/main_window.py +5 -5
- setiastro/saspro/gui/mixins/toolbar_mixin.py +2 -2
- setiastro/saspro/legacy/numba_utils.py +301 -119
- setiastro/saspro/numba_utils.py +998 -270
- setiastro/saspro/ops/settings.py +6 -6
- setiastro/saspro/pixelmath.py +1 -1
- setiastro/saspro/planetprojection.py +310 -105
- setiastro/saspro/sfcc.py +14 -8
- setiastro/saspro/stacking_suite.py +292 -111
- setiastro/saspro/subwindow.py +28 -35
- setiastro/saspro/translations/all_source_strings.json +2 -2
- setiastro/saspro/translations/ar_translations.py +3 -3
- setiastro/saspro/translations/de_translations.py +2 -2
- setiastro/saspro/translations/es_translations.py +2 -2
- setiastro/saspro/translations/fr_translations.py +2 -2
- setiastro/saspro/translations/hi_translations.py +2 -2
- setiastro/saspro/translations/it_translations.py +2 -2
- setiastro/saspro/translations/ja_translations.py +2 -2
- setiastro/saspro/translations/pt_translations.py +2 -2
- setiastro/saspro/translations/ru_translations.py +2 -2
- setiastro/saspro/translations/saspro_ar.ts +2 -2
- setiastro/saspro/translations/saspro_de.ts +4 -4
- setiastro/saspro/translations/saspro_es.ts +2 -2
- setiastro/saspro/translations/saspro_fr.ts +2 -2
- setiastro/saspro/translations/saspro_hi.ts +2 -2
- setiastro/saspro/translations/saspro_it.ts +4 -4
- setiastro/saspro/translations/saspro_ja.ts +2 -2
- setiastro/saspro/translations/saspro_pt.ts +2 -2
- setiastro/saspro/translations/saspro_ru.ts +2 -2
- setiastro/saspro/translations/saspro_sw.ts +2 -2
- setiastro/saspro/translations/saspro_uk.ts +2 -2
- setiastro/saspro/translations/saspro_zh.ts +2 -2
- setiastro/saspro/translations/sw_translations.py +2 -2
- setiastro/saspro/translations/uk_translations.py +2 -2
- setiastro/saspro/translations/zh_translations.py +2 -2
- setiastro/saspro/window_shelf.py +62 -1
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/RECORD +47 -47
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/entry_points.txt +1 -1
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/licenses/license.txt +0 -0
|
@@ -84,7 +84,7 @@ from setiastro.saspro.legacy.numba_utils import (
|
|
|
84
84
|
finalize_drizzle_2d,
|
|
85
85
|
finalize_drizzle_3d,
|
|
86
86
|
)
|
|
87
|
-
from setiastro.saspro.numba_utils import (
|
|
87
|
+
from setiastro.saspro.legacy.numba_utils import (
|
|
88
88
|
bulk_cosmetic_correction_numba,
|
|
89
89
|
drizzle_deposit_numba_naive,
|
|
90
90
|
drizzle_deposit_color_naive,
|
|
@@ -6522,6 +6522,18 @@ class StackingSuiteDialog(QDialog):
|
|
|
6522
6522
|
w.blockSignals(True); w.setValue(v); w.blockSignals(False)
|
|
6523
6523
|
|
|
6524
6524
|
def _get_drizzle_scale(self) -> float:
|
|
6525
|
+
# UI combo wins if it exists
|
|
6526
|
+
combo = getattr(self, "drizzle_scale_combo", None)
|
|
6527
|
+
if combo is not None:
|
|
6528
|
+
try:
|
|
6529
|
+
txt = str(combo.currentText()).strip().lower()
|
|
6530
|
+
if txt.endswith("x"):
|
|
6531
|
+
txt = txt[:-1]
|
|
6532
|
+
return float(txt)
|
|
6533
|
+
except Exception:
|
|
6534
|
+
pass
|
|
6535
|
+
|
|
6536
|
+
# fallback to settings
|
|
6525
6537
|
val = self.settings.value("stacking/drizzle_scale", "2x")
|
|
6526
6538
|
if isinstance(val, (int, float)):
|
|
6527
6539
|
return float(val)
|
|
@@ -6536,6 +6548,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
6536
6548
|
return 2.0
|
|
6537
6549
|
|
|
6538
6550
|
|
|
6551
|
+
|
|
6539
6552
|
def _set_drizzle_scale(self, r: float | str) -> None:
|
|
6540
6553
|
if isinstance(r, str):
|
|
6541
6554
|
try: r = float(r.rstrip("xX"))
|
|
@@ -7202,6 +7215,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
7202
7215
|
)
|
|
7203
7216
|
main_layout.addWidget(self.clear_master_flat_selection_btn)
|
|
7204
7217
|
self.override_dark_combo.currentIndexChanged[int].connect(self.override_selected_master_dark_for_flats)
|
|
7218
|
+
# Connect selection change to update combobox
|
|
7219
|
+
self.flat_tree.itemSelectionChanged.connect(self._on_flat_tree_selection_changed)
|
|
7205
7220
|
self.update_override_dark_combo()
|
|
7206
7221
|
self.rebuild_flat_tree()
|
|
7207
7222
|
|
|
@@ -8846,8 +8861,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
8846
8861
|
tab.setLayout(layout)
|
|
8847
8862
|
|
|
8848
8863
|
self.drizzle_checkbox.setChecked(self.settings.value("stacking/drizzle_enabled", False, type=bool))
|
|
8849
|
-
self.drizzle_scale_combo.setCurrentText(self.
|
|
8850
|
-
self.drizzle_drop_shrink_spin.setValue(self.
|
|
8864
|
+
self.drizzle_scale_combo.setCurrentText(f"{int(self._get_drizzle_scale())}x")
|
|
8865
|
+
self.drizzle_drop_shrink_spin.setValue(self._get_drizzle_pixfrac())
|
|
8851
8866
|
|
|
8852
8867
|
drizzle_on = self.settings.value("stacking/drizzle_enabled", False, type=bool)
|
|
8853
8868
|
self.cfa_drizzle_cb.setEnabled(drizzle_on)
|
|
@@ -8901,6 +8916,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
8901
8916
|
# wire it
|
|
8902
8917
|
self.comet_cb.toggled.connect(lambda _v: self._refresh_comet_starless_enable())
|
|
8903
8918
|
self.comet_save_starless_cb.toggled.connect(lambda _v: self._refresh_comet_starless_enable()) # optional belt/suspenders
|
|
8919
|
+
self.drizzle_drop_shrink_spin.valueChanged.connect(self._on_drizzle_param_changed)
|
|
8904
8920
|
|
|
8905
8921
|
# run once AFTER you restore all initial states from settings
|
|
8906
8922
|
self._refresh_comet_starless_enable()
|
|
@@ -8963,11 +8979,16 @@ class StackingSuiteDialog(QDialog):
|
|
|
8963
8979
|
self._update_drizzle_summary_columns()
|
|
8964
8980
|
|
|
8965
8981
|
def _on_drizzle_param_changed(self, *_):
|
|
8966
|
-
#
|
|
8967
|
-
self
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
#
|
|
8982
|
+
# persist scale from UI
|
|
8983
|
+
if hasattr(self, "drizzle_scale_combo"):
|
|
8984
|
+
self._set_drizzle_scale(self.drizzle_scale_combo.currentText())
|
|
8985
|
+
|
|
8986
|
+
# (optional) persist pixfrac too if you want “global controls are canonical”
|
|
8987
|
+
if hasattr(self, "drizzle_drop_shrink_spin"):
|
|
8988
|
+
self._set_drizzle_pixfrac(float(self.drizzle_drop_shrink_spin.value()))
|
|
8989
|
+
|
|
8990
|
+
self.settings.sync()
|
|
8991
|
+
self._update_drizzle_summary_columns() # if you show “1x / 0.65” in the tree
|
|
8971
8992
|
|
|
8972
8993
|
def _on_star_trail_toggled(self, enabled: bool):
|
|
8973
8994
|
"""
|
|
@@ -11703,6 +11724,51 @@ class StackingSuiteDialog(QDialog):
|
|
|
11703
11724
|
|
|
11704
11725
|
|
|
11705
11726
|
|
|
11727
|
+
def _on_flat_tree_selection_changed(self):
|
|
11728
|
+
"""Update the override_dark_combo to reflect the value stored in the selected row."""
|
|
11729
|
+
items = self.flat_tree.selectedItems()
|
|
11730
|
+
if not items:
|
|
11731
|
+
return
|
|
11732
|
+
|
|
11733
|
+
# Get the first top-level item (group row, not a leaf)
|
|
11734
|
+
selected_item = None
|
|
11735
|
+
for it in items:
|
|
11736
|
+
if not it.parent(): # Top-level item (group row)
|
|
11737
|
+
selected_item = it
|
|
11738
|
+
break
|
|
11739
|
+
|
|
11740
|
+
if not selected_item:
|
|
11741
|
+
return
|
|
11742
|
+
|
|
11743
|
+
# Read the stored value from column 2
|
|
11744
|
+
ud = selected_item.data(2, Qt.ItemDataRole.UserRole)
|
|
11745
|
+
|
|
11746
|
+
# Block signals to avoid triggering override_selected_master_dark_for_flats
|
|
11747
|
+
self.override_dark_combo.blockSignals(True)
|
|
11748
|
+
try:
|
|
11749
|
+
# Find the matching index in the combobox
|
|
11750
|
+
if ud is None:
|
|
11751
|
+
# "None (Use Auto-Select)" should be at index 0
|
|
11752
|
+
self.override_dark_combo.setCurrentIndex(0)
|
|
11753
|
+
elif ud == "__NO_DARK__":
|
|
11754
|
+
# "None (Use no Dark to Calibrate)" should be at index 1
|
|
11755
|
+
self.override_dark_combo.setCurrentIndex(1)
|
|
11756
|
+
else:
|
|
11757
|
+
# Find the item with matching userData (path)
|
|
11758
|
+
found_idx = -1
|
|
11759
|
+
for i in range(self.override_dark_combo.count()):
|
|
11760
|
+
item_ud = self.override_dark_combo.itemData(i)
|
|
11761
|
+
if item_ud == ud:
|
|
11762
|
+
found_idx = i
|
|
11763
|
+
break
|
|
11764
|
+
if found_idx >= 0:
|
|
11765
|
+
self.override_dark_combo.setCurrentIndex(found_idx)
|
|
11766
|
+
else:
|
|
11767
|
+
# If not found, default to index 0
|
|
11768
|
+
self.override_dark_combo.setCurrentIndex(0)
|
|
11769
|
+
finally:
|
|
11770
|
+
self.override_dark_combo.blockSignals(False)
|
|
11771
|
+
|
|
11706
11772
|
def override_selected_master_dark_for_flats(self, idx: int):
|
|
11707
11773
|
"""Apply combo choice to selected flat groups; stores path/token in row + dict."""
|
|
11708
11774
|
items = self.flat_tree.selectedItems()
|
|
@@ -15752,7 +15818,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
15752
15818
|
filters = "TIFF (*.tif);;PNG (*.png);;JPEG (*.jpg *.jpeg);;FITS (*.fits);;XISF (*.xisf)"
|
|
15753
15819
|
path, chosen_filter = QFileDialog.getSaveFileName(
|
|
15754
15820
|
self, "Save Star-Trail Image",
|
|
15755
|
-
os.path.join(self.
|
|
15821
|
+
os.path.join(self._master_light_dir(), default_name),
|
|
15756
15822
|
"TIFF (*.tif);;PNG (*.png);;JPEG (*.jpg *.jpeg);;FITS (*.fits);;XISF (*.xisf)"
|
|
15757
15823
|
)
|
|
15758
15824
|
if not path:
|
|
@@ -15879,8 +15945,11 @@ class StackingSuiteDialog(QDialog):
|
|
|
15879
15945
|
"""
|
|
15880
15946
|
Take aligned OSC frames and produce per-band aligned mono FITS.
|
|
15881
15947
|
|
|
15882
|
-
|
|
15883
|
-
|
|
15948
|
+
IMPORTANT (for drizzle correctness):
|
|
15949
|
+
- We also populate self._split_drizzle_info so drizzle can deposit from the
|
|
15950
|
+
*parent original* pixels while using the same transforms solved from originals.
|
|
15951
|
+
Mapping:
|
|
15952
|
+
split_aligned_path -> {"orig": <parent original>, "chan": "R"|"G"|None}
|
|
15884
15953
|
|
|
15885
15954
|
Returns a new light_files-style dict:
|
|
15886
15955
|
{ "Ha - ...": [aligned_Ha_*.fit...],
|
|
@@ -15895,18 +15964,35 @@ class StackingSuiteDialog(QDialog):
|
|
|
15895
15964
|
out_dir = os.path.join(self.stacking_directory, "DualBand_Split")
|
|
15896
15965
|
os.makedirs(out_dir, exist_ok=True)
|
|
15897
15966
|
|
|
15967
|
+
# split_aligned_path -> {"orig": <parent original>, "chan": "R"|"G"|None}
|
|
15968
|
+
if not hasattr(self, "_split_drizzle_info") or self._split_drizzle_info is None:
|
|
15969
|
+
self._split_drizzle_info = {}
|
|
15970
|
+
else:
|
|
15971
|
+
# Clear any previous run’s mapping to avoid mixing sessions
|
|
15972
|
+
self._split_drizzle_info.clear()
|
|
15973
|
+
|
|
15974
|
+
split_info = self._split_drizzle_info
|
|
15975
|
+
|
|
15898
15976
|
# Collect per-band file lists (some may already be mono NB)
|
|
15899
15977
|
ha_files, sii_files, oiii_files, hb_files = [], [], [], []
|
|
15900
|
-
inherit_map: dict[str, set[str]] = {} # new_group_key -> set(parent_group)
|
|
15901
15978
|
parent_of: dict[str, str] = {} # new_path_or_existing -> parent_group
|
|
15902
15979
|
|
|
15903
15980
|
# Snapshot old drizzle configs so we can inherit to the new NB groups
|
|
15904
15981
|
old_drizzle = dict(getattr(self, "per_group_drizzle", {}))
|
|
15905
15982
|
selected_groups = set(getattr(self, "_reg_selected_groups", set()))
|
|
15906
15983
|
|
|
15984
|
+
# Normalized aligned->original map (should exist from your registration pipeline)
|
|
15985
|
+
oba = getattr(self, "orig_by_aligned", {}) or {}
|
|
15986
|
+
|
|
15907
15987
|
# --- Pass 1: classify + split each aligned frame ---
|
|
15908
15988
|
for group, files in aligned_light_files.items():
|
|
15909
15989
|
for fp in files:
|
|
15990
|
+
fpn = os.path.normpath(fp)
|
|
15991
|
+
|
|
15992
|
+
# Find parent original for this aligned frame (for drizzle)
|
|
15993
|
+
parent_orig = oba.get(fpn) or oba.get(os.path.normpath(fp))
|
|
15994
|
+
parent_orig = os.path.normpath(parent_orig) if parent_orig else None
|
|
15995
|
+
|
|
15910
15996
|
try:
|
|
15911
15997
|
with fits.open(fp, memmap=False) as hdul:
|
|
15912
15998
|
data = hdul[0].data
|
|
@@ -15927,16 +16013,12 @@ class StackingSuiteDialog(QDialog):
|
|
|
15927
16013
|
# ----------------------------------------
|
|
15928
16014
|
# Detect layout: mono vs RGB, CHW vs HWC
|
|
15929
16015
|
# ----------------------------------------
|
|
15930
|
-
layout = None
|
|
15931
|
-
|
|
15932
16016
|
if arr.ndim == 2:
|
|
15933
16017
|
layout = "MONO"
|
|
15934
16018
|
elif arr.ndim == 3:
|
|
15935
16019
|
c0, c1, c2 = arr.shape
|
|
15936
|
-
# channels-first (C, H, W)
|
|
15937
16020
|
if c0 in (1, 3) and c1 > 8 and c2 > 8:
|
|
15938
16021
|
layout = "CHW"
|
|
15939
|
-
# channels-last (H, W, C)
|
|
15940
16022
|
elif c2 in (1, 3) and c0 > 8 and c1 > 8:
|
|
15941
16023
|
layout = "HWC"
|
|
15942
16024
|
else:
|
|
@@ -15945,7 +16027,11 @@ class StackingSuiteDialog(QDialog):
|
|
|
15945
16027
|
layout = "UNKNOWN"
|
|
15946
16028
|
|
|
15947
16029
|
# ---- MONO narrowband frames (Ha/SII/OIII/Hb) ----
|
|
15948
|
-
if
|
|
16030
|
+
if (
|
|
16031
|
+
layout == "MONO"
|
|
16032
|
+
or (layout == "CHW" and arr.shape[0] == 1)
|
|
16033
|
+
or (layout == "HWC" and arr.shape[-1] == 1)
|
|
16034
|
+
):
|
|
15949
16035
|
# Treat as mono NB based on classifier
|
|
15950
16036
|
if cls == "MONO_HA":
|
|
15951
16037
|
ha_files.append(fp); parent_of[fp] = group
|
|
@@ -15955,7 +16041,14 @@ class StackingSuiteDialog(QDialog):
|
|
|
15955
16041
|
oiii_files.append(fp); parent_of[fp] = group
|
|
15956
16042
|
elif cls == "MONO_HB":
|
|
15957
16043
|
hb_files.append(fp); parent_of[fp] = group
|
|
15958
|
-
|
|
16044
|
+
else:
|
|
16045
|
+
# Unknown mono: ignore for NB split
|
|
16046
|
+
pass
|
|
16047
|
+
|
|
16048
|
+
# For drizzle mapping: if this is a split pipeline, we still may want
|
|
16049
|
+
# aligned mono to drizzle from parent original (chan None means “use mono as-is”)
|
|
16050
|
+
if parent_orig:
|
|
16051
|
+
split_info[fpn] = {"orig": parent_orig, "chan": None}
|
|
15959
16052
|
continue
|
|
15960
16053
|
|
|
15961
16054
|
# ---- RGB / OSC case (what we expect for dual-band) ----
|
|
@@ -15972,39 +16065,55 @@ class StackingSuiteDialog(QDialog):
|
|
|
15972
16065
|
R = arr[..., 0]
|
|
15973
16066
|
G = arr[..., 1]
|
|
15974
16067
|
else:
|
|
15975
|
-
#
|
|
15976
|
-
# C should be 3
|
|
16068
|
+
# (C, H, W)
|
|
15977
16069
|
R = arr[0, ...]
|
|
15978
16070
|
G = arr[1, ...]
|
|
15979
16071
|
|
|
15980
16072
|
base = os.path.splitext(os.path.basename(fp))[0]
|
|
15981
16073
|
|
|
16074
|
+
def _map_split(path_written: str, chan: str):
|
|
16075
|
+
"""Record drizzle info for this newly created split aligned file."""
|
|
16076
|
+
if parent_orig:
|
|
16077
|
+
split_info[os.path.normpath(path_written)] = {"orig": parent_orig, "chan": chan}
|
|
16078
|
+
|
|
15982
16079
|
if cls == "DUAL_HA_OIII":
|
|
15983
16080
|
ha_path = os.path.join(out_dir, f"{base}_Ha.fit")
|
|
15984
16081
|
oiii_path = os.path.join(out_dir, f"{base}_OIII.fit")
|
|
15985
|
-
self._write_band_fit(ha_path,
|
|
16082
|
+
self._write_band_fit(ha_path, R, hdr, "Ha", src_filter=filt)
|
|
15986
16083
|
self._write_band_fit(oiii_path, G, hdr, "OIII", src_filter=filt)
|
|
16084
|
+
|
|
15987
16085
|
ha_files.append(ha_path); parent_of[ha_path] = group
|
|
15988
16086
|
oiii_files.append(oiii_path); parent_of[oiii_path] = group
|
|
15989
16087
|
|
|
16088
|
+
_map_split(ha_path, "R")
|
|
16089
|
+
_map_split(oiii_path, "G")
|
|
16090
|
+
|
|
15990
16091
|
elif cls == "DUAL_SII_OIII":
|
|
15991
16092
|
sii_path = os.path.join(out_dir, f"{base}_SII.fit")
|
|
15992
16093
|
oiii_path = os.path.join(out_dir, f"{base}_OIII.fit")
|
|
15993
|
-
self._write_band_fit(sii_path,
|
|
16094
|
+
self._write_band_fit(sii_path, R, hdr, "SII", src_filter=filt)
|
|
15994
16095
|
self._write_band_fit(oiii_path, G, hdr, "OIII", src_filter=filt)
|
|
15995
|
-
|
|
15996
|
-
|
|
16096
|
+
|
|
16097
|
+
sii_files.append(sii_path); parent_of[sii_path] = group
|
|
16098
|
+
oiii_files.append(oiii_path); parent_of[oiii_path] = group
|
|
16099
|
+
|
|
16100
|
+
_map_split(sii_path, "R")
|
|
16101
|
+
_map_split(oiii_path, "G")
|
|
15997
16102
|
|
|
15998
16103
|
elif cls == "DUAL_SII_HB":
|
|
15999
16104
|
sii_path = os.path.join(out_dir, f"{base}_SII.fit")
|
|
16000
16105
|
hb_path = os.path.join(out_dir, f"{base}_Hb.fit")
|
|
16001
16106
|
self._write_band_fit(sii_path, R, hdr, "SII", src_filter=filt)
|
|
16002
16107
|
self._write_band_fit(hb_path, G, hdr, "Hb", src_filter=filt)
|
|
16108
|
+
|
|
16003
16109
|
sii_files.append(sii_path); parent_of[sii_path] = group
|
|
16004
16110
|
hb_files.append(hb_path); parent_of[hb_path] = group
|
|
16005
16111
|
|
|
16112
|
+
_map_split(sii_path, "R")
|
|
16113
|
+
_map_split(hb_path, "G")
|
|
16114
|
+
|
|
16006
16115
|
else:
|
|
16007
|
-
# UNKNOWN dual → ignore
|
|
16116
|
+
# UNKNOWN dual → ignore
|
|
16008
16117
|
continue
|
|
16009
16118
|
|
|
16010
16119
|
# --- Pass 2: group the new files (band + exposure + size), same as before ---
|
|
@@ -16020,7 +16129,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
16020
16129
|
return f"{band} - ? - ?x?"
|
|
16021
16130
|
|
|
16022
16131
|
new_groups: dict[str, list[str]] = {}
|
|
16023
|
-
inherit_map = {}
|
|
16132
|
+
inherit_map: dict[str, set[str]] = {}
|
|
16024
16133
|
|
|
16025
16134
|
for band, flist in (
|
|
16026
16135
|
("Ha", ha_files),
|
|
@@ -16043,7 +16152,6 @@ class StackingSuiteDialog(QDialog):
|
|
|
16043
16152
|
return aligned_light_files
|
|
16044
16153
|
|
|
16045
16154
|
# --- Replace light_files with NB groups & seed drizzle configs ---
|
|
16046
|
-
|
|
16047
16155
|
self.light_files = new_groups
|
|
16048
16156
|
|
|
16049
16157
|
self.per_group_drizzle = {}
|
|
@@ -16087,6 +16195,11 @@ class StackingSuiteDialog(QDialog):
|
|
|
16087
16195
|
return new_groups
|
|
16088
16196
|
|
|
16089
16197
|
|
|
16198
|
+
def _master_light_dir(self) -> str:
|
|
16199
|
+
out_dir = os.path.join(self.stacking_directory, "Master_Light")
|
|
16200
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
16201
|
+
return out_dir
|
|
16202
|
+
|
|
16090
16203
|
def on_registration_complete(self, success, msg):
|
|
16091
16204
|
|
|
16092
16205
|
self.update_status(self.tr(msg))
|
|
@@ -16504,7 +16617,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
16504
16617
|
|
|
16505
16618
|
group_key, frames = self._mf_queue.pop(0)
|
|
16506
16619
|
|
|
16507
|
-
out_dir =
|
|
16620
|
+
out_dir = self._master_light_dir()
|
|
16508
16621
|
os.makedirs(out_dir, exist_ok=True)
|
|
16509
16622
|
out_path = os.path.join(out_dir, f"MasterLight_{group_key}_MFDeconv.fit")
|
|
16510
16623
|
|
|
@@ -17020,7 +17133,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17020
17133
|
display_group = self._label_with_dims(group_key, W, H)
|
|
17021
17134
|
base = f"MasterLight_{display_group}_{n_frames_group}stacked"
|
|
17022
17135
|
base = self._normalize_master_stem(base)
|
|
17023
|
-
out_path_orig = self._build_out(self.
|
|
17136
|
+
out_path_orig = self._build_out(self._master_light_dir(), base, "fit")
|
|
17024
17137
|
|
|
17025
17138
|
# Try to attach rejection maps that were accumulated during integration
|
|
17026
17139
|
maps = getattr(self, "_rej_maps", {}).get(group_key)
|
|
@@ -17128,7 +17241,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17128
17241
|
display_group_crop = self._label_with_dims(group_key, Wc, Hc)
|
|
17129
17242
|
base_crop = f"MasterLight_{display_group_crop}_{n_frames_group}stacked_autocrop"
|
|
17130
17243
|
base_crop = self._normalize_master_stem(base_crop)
|
|
17131
|
-
out_path_crop = self._build_out(self.
|
|
17244
|
+
out_path_crop = self._build_out(self._master_light_dir(), base_crop, "fit")
|
|
17132
17245
|
|
|
17133
17246
|
save_image(
|
|
17134
17247
|
img_array=cropped_img,
|
|
@@ -17276,7 +17389,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17276
17389
|
Hc, Wc = comet_only.shape[:2]
|
|
17277
17390
|
display_group_c = self._label_with_dims(group_key, Wc, Hc)
|
|
17278
17391
|
comet_path = self._build_out(
|
|
17279
|
-
self.
|
|
17392
|
+
self._master_light_dir(),
|
|
17280
17393
|
f"MasterCometOnly_{display_group_c}_{len(usable)}stacked",
|
|
17281
17394
|
"fit"
|
|
17282
17395
|
)
|
|
@@ -17303,7 +17416,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17303
17416
|
Hcc, Wcc = comet_only_crop.shape[:2]
|
|
17304
17417
|
display_group_cc = self._label_with_dims(group_key, Wcc, Hcc)
|
|
17305
17418
|
comet_path_crop = self._build_out(
|
|
17306
|
-
self.
|
|
17419
|
+
self._master_light_dir(),
|
|
17307
17420
|
f"MasterCometOnly_{display_group_cc}_{len(usable)}stacked_autocrop",
|
|
17308
17421
|
"fit"
|
|
17309
17422
|
)
|
|
@@ -17332,7 +17445,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17332
17445
|
|
|
17333
17446
|
is_mono_blend = (blend.ndim == 2) or (blend.ndim == 3 and blend.shape[2] == 1)
|
|
17334
17447
|
blend_path = self._build_out(
|
|
17335
|
-
self.
|
|
17448
|
+
self._master_light_dir(),
|
|
17336
17449
|
f"MasterCometBlend_{display_group_c}_{len(usable)}stacked",
|
|
17337
17450
|
"fit"
|
|
17338
17451
|
)
|
|
@@ -17352,7 +17465,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17352
17465
|
Hb, Wb = blend_crop.shape[:2]
|
|
17353
17466
|
display_group_bc = self._label_with_dims(group_key, Wb, Hb)
|
|
17354
17467
|
blend_path_crop = self._build_out(
|
|
17355
|
-
self.
|
|
17468
|
+
self._master_light_dir(),
|
|
17356
17469
|
f"MasterCometBlend_{display_group_bc}_{len(usable)}stacked_autocrop",
|
|
17357
17470
|
"fit"
|
|
17358
17471
|
)
|
|
@@ -17409,13 +17522,37 @@ class StackingSuiteDialog(QDialog):
|
|
|
17409
17522
|
|
|
17410
17523
|
rejections_for_group = group_integration_data[group_key]["rejection_map"]
|
|
17411
17524
|
n_frames_group = group_integration_data[group_key]["n_frames"]
|
|
17525
|
+
# Build drizzle entries for each group
|
|
17526
|
+
split_info = getattr(self, "_split_drizzle_info", {}) or {}
|
|
17527
|
+
oba = getattr(self, "orig_by_aligned", {}) or {}
|
|
17528
|
+
|
|
17529
|
+
entries_by_group = {}
|
|
17530
|
+
for gk, aligned_list in grouped_files.items():
|
|
17531
|
+
entries = []
|
|
17532
|
+
for ap in aligned_list:
|
|
17533
|
+
apn = os.path.normpath(ap)
|
|
17534
|
+
info = split_info.get(apn)
|
|
17535
|
+
|
|
17536
|
+
if info and info.get("orig"):
|
|
17537
|
+
entries.append({
|
|
17538
|
+
"orig": os.path.normpath(info["orig"]), # parent original pixels
|
|
17539
|
+
"aligned": apn, # split aligned (rej/weights)
|
|
17540
|
+
"chan": info.get("chan", None), # 'R'/'G'/None
|
|
17541
|
+
})
|
|
17542
|
+
else:
|
|
17543
|
+
# normal (non-split) case
|
|
17544
|
+
op = oba.get(apn)
|
|
17545
|
+
if op:
|
|
17546
|
+
entries.append({"orig": os.path.normpath(op), "aligned": apn, "chan": None})
|
|
17547
|
+
|
|
17548
|
+
entries_by_group[gk] = entries
|
|
17412
17549
|
|
|
17413
17550
|
log(f"📐 Drizzle for '{group_key}' at {scale_factor}× (drop={drop_shrink}) using {n_frames_group} frame(s).")
|
|
17414
17551
|
|
|
17415
17552
|
self.drizzle_stack_one_group(
|
|
17416
17553
|
group_key=group_key,
|
|
17417
17554
|
file_list=file_list,
|
|
17418
|
-
|
|
17555
|
+
entries=entries_by_group.get(group_key, []),
|
|
17419
17556
|
transforms_dict=transforms_dict,
|
|
17420
17557
|
frame_weights=frame_weights,
|
|
17421
17558
|
scale_factor=scale_factor,
|
|
@@ -17426,6 +17563,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17426
17563
|
status_cb=log
|
|
17427
17564
|
)
|
|
17428
17565
|
|
|
17566
|
+
|
|
17429
17567
|
# Build summary lines
|
|
17430
17568
|
for group_key, info in group_integration_data.items():
|
|
17431
17569
|
n_frames_group = info["n_frames"]
|
|
@@ -18019,7 +18157,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18019
18157
|
return
|
|
18020
18158
|
|
|
18021
18159
|
group_key, frames = self._mf_queue.pop(0)
|
|
18022
|
-
out_dir =
|
|
18160
|
+
out_dir = self._master_light_dir()
|
|
18023
18161
|
os.makedirs(out_dir, exist_ok=True)
|
|
18024
18162
|
|
|
18025
18163
|
# Settings snapshot
|
|
@@ -18648,8 +18786,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
18648
18786
|
self,
|
|
18649
18787
|
*,
|
|
18650
18788
|
group_key,
|
|
18651
|
-
file_list, #
|
|
18652
|
-
|
|
18789
|
+
file_list, # still the aligned split files (for headers/metadata)
|
|
18790
|
+
entries, # NEW: list of {orig, aligned, chan}
|
|
18653
18791
|
transforms_dict,
|
|
18654
18792
|
frame_weights,
|
|
18655
18793
|
scale_factor,
|
|
@@ -18659,8 +18797,15 @@ class StackingSuiteDialog(QDialog):
|
|
|
18659
18797
|
rect_override,
|
|
18660
18798
|
status_cb
|
|
18661
18799
|
):
|
|
18662
|
-
|
|
18663
|
-
|
|
18800
|
+
import os
|
|
18801
|
+
|
|
18802
|
+
import cv2
|
|
18803
|
+
from datetime import datetime
|
|
18804
|
+
from astropy.io import fits
|
|
18805
|
+
|
|
18806
|
+
log = status_cb or (lambda *_: None)
|
|
18807
|
+
|
|
18808
|
+
# ---- load SASD v2 transforms (keyed by ORIGINAL path) ----
|
|
18664
18809
|
sasd_path = os.path.join(self.stacking_directory, "alignment_transforms.sasd")
|
|
18665
18810
|
ref_H, ref_W, xforms = self._load_sasd_v2(sasd_path)
|
|
18666
18811
|
if not (ref_H and ref_W):
|
|
@@ -18668,26 +18813,33 @@ class StackingSuiteDialog(QDialog):
|
|
|
18668
18813
|
rs = getattr(self, "ref_shape_for_drizzle", None)
|
|
18669
18814
|
if isinstance(rs, tuple) and len(rs) == 2 and all(int(v) > 0 for v in rs):
|
|
18670
18815
|
ref_H, ref_W = int(rs[0]), int(rs[1])
|
|
18671
|
-
|
|
18816
|
+
log(f"ℹ️ Using in-memory REF_SHAPE fallback: {ref_H}×{ref_W}")
|
|
18672
18817
|
else:
|
|
18673
|
-
|
|
18818
|
+
log("⚠️ Missing REF_SHAPE in SASD; cannot drizzle.")
|
|
18674
18819
|
return
|
|
18675
18820
|
|
|
18676
18821
|
log(f"✅ SASD v2: loaded {len(xforms)} transform(s).")
|
|
18677
|
-
|
|
18822
|
+
|
|
18823
|
+
# ---- sanity: entries must exist ----
|
|
18824
|
+
if not entries:
|
|
18825
|
+
log(f"⚠️ Group '{group_key}' has no drizzle entries – skipping.")
|
|
18826
|
+
return
|
|
18827
|
+
|
|
18828
|
+
# Debug (first few)
|
|
18678
18829
|
try:
|
|
18679
|
-
sample_need = [os.path.basename(
|
|
18830
|
+
sample_need = [os.path.basename(os.path.normpath(e.get("orig", ""))) for e in entries[:5]]
|
|
18680
18831
|
sample_have = [os.path.basename(p) for p in list(xforms.keys())[:5]]
|
|
18681
18832
|
log(f" originals needed (sample): {sample_need}")
|
|
18682
18833
|
log(f" sasd FILEs (sample): {sample_have}")
|
|
18683
18834
|
except Exception:
|
|
18684
18835
|
pass
|
|
18685
18836
|
|
|
18686
|
-
|
|
18687
|
-
|
|
18837
|
+
# ---- output canvas ----
|
|
18838
|
+
canvas_H = int(ref_H * scale_factor)
|
|
18839
|
+
canvas_W = int(ref_W * scale_factor)
|
|
18688
18840
|
|
|
18689
|
-
#
|
|
18690
|
-
kernel_name = self.settings.value("stacking/drizzle_kernel", "square", type=str).lower()
|
|
18841
|
+
# ---- kernel config from settings ----
|
|
18842
|
+
kernel_name = (self.settings.value("stacking/drizzle_kernel", "square", type=str) or "square").lower()
|
|
18691
18843
|
gauss_sigma = self.settings.value(
|
|
18692
18844
|
"stacking/drizzle_gauss_sigma", float(drop_shrink) * 0.5, type=float
|
|
18693
18845
|
)
|
|
@@ -18701,80 +18853,78 @@ class StackingSuiteDialog(QDialog):
|
|
|
18701
18853
|
total_rej = sum(len(v) for v in (rejection_map or {}).values())
|
|
18702
18854
|
log(f"🔭 Drizzle stacking for group '{group_key}' with {total_rej} total rejected pixels.")
|
|
18703
18855
|
|
|
18856
|
+
# Need at least 2 frames to be worth it
|
|
18704
18857
|
if len(file_list) < 2:
|
|
18705
18858
|
log(f"⚠️ Group '{group_key}' does not have enough frames to drizzle.")
|
|
18706
18859
|
return
|
|
18707
18860
|
|
|
18708
|
-
#
|
|
18861
|
+
# ---- establish header + default dims from first aligned file (metadata only) ----
|
|
18709
18862
|
first_file = file_list[0]
|
|
18710
18863
|
first_img, hdr, _, _ = load_image(first_file)
|
|
18711
18864
|
if first_img is None:
|
|
18712
18865
|
log(f"⚠️ Could not load {first_file} to determine drizzle shape!")
|
|
18713
18866
|
return
|
|
18714
18867
|
|
|
18715
|
-
if
|
|
18868
|
+
# Force mono if any entry is channel-split (dual-band drizzle output should be mono)
|
|
18869
|
+
force_mono = any((e.get("chan") in ("R", "G", "B")) for e in entries)
|
|
18870
|
+
|
|
18871
|
+
if force_mono:
|
|
18716
18872
|
is_mono = True
|
|
18717
|
-
h, w = first_img.shape
|
|
18873
|
+
h, w = first_img.shape[:2]
|
|
18718
18874
|
c = 1
|
|
18719
18875
|
else:
|
|
18720
|
-
|
|
18721
|
-
|
|
18876
|
+
if first_img.ndim == 2:
|
|
18877
|
+
is_mono = True
|
|
18878
|
+
h, w = first_img.shape
|
|
18879
|
+
c = 1
|
|
18880
|
+
else:
|
|
18881
|
+
is_mono = False
|
|
18882
|
+
h, w, c = first_img.shape
|
|
18722
18883
|
|
|
18723
18884
|
# --- choose depositor ONCE (and log it) ---
|
|
18724
|
-
|
|
18725
|
-
|
|
18885
|
+
use_kernelized = not (_kcode == 0 and drop_shrink >= 0.99)
|
|
18886
|
+
|
|
18887
|
+
if not use_kernelized:
|
|
18726
18888
|
deposit_func = drizzle_deposit_numba_naive if is_mono else drizzle_deposit_color_naive
|
|
18727
18889
|
kinf = "naive (square, pixfrac≈1)"
|
|
18728
18890
|
else:
|
|
18729
|
-
# Any other case → kernelized path (square/circular/gaussian)
|
|
18730
18891
|
deposit_func = drizzle_deposit_numba_kernel_mono if is_mono else drizzle_deposit_color_kernel
|
|
18731
18892
|
kinf = ["square", "circular", "gaussian"][_kcode]
|
|
18732
|
-
log(f"Using {kinf} kernel drizzle ({'mono' if is_mono else 'color'}).")
|
|
18733
18893
|
|
|
18734
|
-
|
|
18894
|
+
log(f"Using {kinf} drizzle ({'mono' if is_mono else 'color'}).")
|
|
18895
|
+
|
|
18896
|
+
# ---- allocate buffers ----
|
|
18735
18897
|
out_h = int(canvas_H)
|
|
18736
18898
|
out_w = int(canvas_W)
|
|
18737
18899
|
drizzle_buffer = np.zeros((out_h, out_w) if is_mono else (out_h, out_w, c), dtype=self._dtype())
|
|
18738
18900
|
coverage_buffer = np.zeros_like(drizzle_buffer, dtype=self._dtype())
|
|
18739
18901
|
finalize_func = finalize_drizzle_2d if is_mono else finalize_drizzle_3d
|
|
18740
18902
|
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18746
|
-
|
|
18747
|
-
v = H @ np.array([x, y, 1.0], dtype=np.float32)
|
|
18748
|
-
if abs(v[2]) < 1e-8:
|
|
18749
|
-
return (int(round(v[0])), int(round(v[1])))
|
|
18750
|
-
return (int(round(v[0] / v[2])), int(round(v[1] / v[2])))
|
|
18751
|
-
|
|
18903
|
+
# ---- main loop over ENTRIES ----
|
|
18904
|
+
# each entry: {"orig": <original path>, "aligned": <aligned path>, "chan": "R"/"G"/None}
|
|
18905
|
+
for i, ent in enumerate(entries):
|
|
18906
|
+
orig_file = os.path.normpath(ent.get("orig", ""))
|
|
18907
|
+
aligned_file = os.path.normpath(ent.get("aligned", "")) # this is what rejections/weights reference
|
|
18908
|
+
chan = ent.get("chan", None)
|
|
18752
18909
|
|
|
18753
|
-
|
|
18754
|
-
|
|
18755
|
-
|
|
18756
|
-
for op in original_list:
|
|
18757
|
-
ap = transforms_dict.get(os.path.normpath(op))
|
|
18758
|
-
if ap:
|
|
18759
|
-
orig_to_aligned[os.path.normpath(op)] = os.path.normpath(ap)
|
|
18760
|
-
|
|
18761
|
-
for orig_file in original_list:
|
|
18762
|
-
orig_key = os.path.normpath(orig_file)
|
|
18763
|
-
aligned_file = orig_to_aligned.get(orig_key)
|
|
18910
|
+
if not orig_file or not aligned_file:
|
|
18911
|
+
log(f"⚠️ Bad drizzle entry (missing orig/aligned) – skipping: {ent}")
|
|
18912
|
+
continue
|
|
18764
18913
|
|
|
18765
|
-
|
|
18914
|
+
# Weight: prefer aligned key (that matches your rejection/weights mapping)
|
|
18915
|
+
weight = frame_weights.get(aligned_file, frame_weights.get(orig_file, 1.0))
|
|
18766
18916
|
|
|
18767
|
-
kind, X = xforms.get(
|
|
18768
|
-
log(f"🧭 Drizzle uses {kind or '-'} for {os.path.basename(
|
|
18917
|
+
kind, X = xforms.get(orig_file, (None, None))
|
|
18918
|
+
log(f"🧭 Drizzle uses {kind or '-'} for {os.path.basename(orig_file)} (chan={chan or 'all'})")
|
|
18769
18919
|
if kind is None:
|
|
18770
18920
|
log(f"⚠️ No usable transform for {os.path.basename(orig_file)} – skipping")
|
|
18771
18921
|
continue
|
|
18772
18922
|
|
|
18773
|
-
#
|
|
18923
|
+
# choose pixel source + mapping
|
|
18774
18924
|
pixels_are_registered = False
|
|
18775
18925
|
img_data = None
|
|
18776
18926
|
|
|
18777
|
-
if isinstance(kind, str) and (kind.startswith("poly") or kind in ("tps","thin_plate_spline")):
|
|
18927
|
+
if isinstance(kind, str) and (kind.startswith("poly") or kind in ("tps", "thin_plate_spline")):
|
|
18778
18928
|
# Already warped to reference during registration
|
|
18779
18929
|
pixel_path = aligned_file
|
|
18780
18930
|
if not pixel_path:
|
|
@@ -18784,6 +18934,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18784
18934
|
pixels_are_registered = True
|
|
18785
18935
|
|
|
18786
18936
|
elif kind == "affine" and X is not None:
|
|
18937
|
+
# Use ORIGINAL pixels + affine subpixel mapping
|
|
18787
18938
|
pixel_path = orig_file
|
|
18788
18939
|
H_canvas = np.eye(3, dtype=np.float32)
|
|
18789
18940
|
H_canvas[:2] = np.asarray(X, np.float32).reshape(2, 3)
|
|
@@ -18796,6 +18947,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18796
18947
|
log(f"⚠️ Failed to read {os.path.basename(pixel_path)} – skipping")
|
|
18797
18948
|
continue
|
|
18798
18949
|
H = np.asarray(X, np.float32).reshape(3, 3)
|
|
18950
|
+
|
|
18799
18951
|
if raw_img.ndim == 2:
|
|
18800
18952
|
img_data = cv2.warpPerspective(
|
|
18801
18953
|
raw_img, H, (ref_W, ref_H),
|
|
@@ -18810,6 +18962,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18810
18962
|
borderMode=cv2.BORDER_CONSTANT, borderValue=0
|
|
18811
18963
|
) for ch in range(raw_img.shape[2])
|
|
18812
18964
|
], axis=2)
|
|
18965
|
+
|
|
18813
18966
|
H_canvas = np.eye(3, dtype=np.float32)
|
|
18814
18967
|
pixels_are_registered = True
|
|
18815
18968
|
|
|
@@ -18824,28 +18977,43 @@ class StackingSuiteDialog(QDialog):
|
|
|
18824
18977
|
log(f"⚠️ Failed to read {os.path.basename(pixel_path)} – skipping")
|
|
18825
18978
|
continue
|
|
18826
18979
|
|
|
18827
|
-
#
|
|
18828
|
-
if
|
|
18829
|
-
|
|
18980
|
+
# If this is a split dual-band entry, extract channel from the pixel source
|
|
18981
|
+
if chan in ("R", "G", "B"):
|
|
18982
|
+
if img_data.ndim == 3 and img_data.shape[2] >= 3:
|
|
18983
|
+
ch_idx = {"R": 0, "G": 1, "B": 2}[chan]
|
|
18984
|
+
img_data = img_data[..., ch_idx] # mono plane
|
|
18985
|
+
# if already mono, leave it alone
|
|
18986
|
+
|
|
18987
|
+
# Debug bbox once
|
|
18988
|
+
if i == 0:
|
|
18989
|
+
x0, y0 = 0, 0
|
|
18990
|
+
x1, y1 = ref_W - 1, ref_H - 1
|
|
18830
18991
|
p0 = H_canvas @ np.array([x0, y0, 1], np.float32); p0 /= max(p0[2], 1e-8)
|
|
18831
18992
|
p1 = H_canvas @ np.array([x1, y1, 1], np.float32); p1 /= max(p1[2], 1e-8)
|
|
18832
|
-
log(
|
|
18833
|
-
f"
|
|
18993
|
+
log(
|
|
18994
|
+
f" bbox(ref)→reg: ({p0[0]:.1f},{p0[1]:.1f}) to ({p1[0]:.1f},{p1[1]:.1f}); "
|
|
18995
|
+
f"canvas {int(ref_W * scale_factor)}×{int(ref_H * scale_factor)} @ {scale_factor}×"
|
|
18996
|
+
)
|
|
18834
18997
|
|
|
18835
|
-
#
|
|
18998
|
+
# ---- apply per-file rejections (lookup by ALIGNED file) ----
|
|
18836
18999
|
if rejection_map and aligned_file in rejection_map:
|
|
18837
19000
|
coords_for_this_file = rejection_map.get(aligned_file, [])
|
|
18838
19001
|
if coords_for_this_file:
|
|
18839
19002
|
dilate_px = int(self.settings.value("stacking/reject_dilate_px", 0, type=int))
|
|
18840
|
-
dilate_shape = self.settings.value("stacking/reject_dilate_shape", "square", type=str).lower()
|
|
19003
|
+
dilate_shape = (self.settings.value("stacking/reject_dilate_shape", "square", type=str) or "square").lower()
|
|
19004
|
+
|
|
18841
19005
|
offsets = [(0, 0)]
|
|
18842
19006
|
if dilate_px > 0:
|
|
18843
19007
|
r = dilate_px
|
|
18844
|
-
offsets = [(dx, dy) for dx in range(-r, r+1) for dy in range(-r, r+1)]
|
|
19008
|
+
offsets = [(dx, dy) for dx in range(-r, r + 1) for dy in range(-r, r + 1)]
|
|
18845
19009
|
if dilate_shape.startswith("dia"):
|
|
18846
|
-
offsets = [(dx, dy) for (dx,dy) in offsets if (abs(dx)+abs(dy) <= r)]
|
|
19010
|
+
offsets = [(dx, dy) for (dx, dy) in offsets if (abs(dx) + abs(dy) <= r)]
|
|
18847
19011
|
|
|
18848
|
-
|
|
19012
|
+
# after optional channel extraction, img_data may be 2D
|
|
19013
|
+
if img_data.ndim == 2:
|
|
19014
|
+
Hraw, Wraw = img_data.shape
|
|
19015
|
+
else:
|
|
19016
|
+
Hraw, Wraw = img_data.shape[0], img_data.shape[1]
|
|
18849
19017
|
|
|
18850
19018
|
if pixels_are_registered:
|
|
18851
19019
|
# Directly zero registered pixels
|
|
@@ -18853,9 +19021,12 @@ class StackingSuiteDialog(QDialog):
|
|
|
18853
19021
|
for (ox, oy) in offsets:
|
|
18854
19022
|
xr, yr = x_r + ox, y_r + oy
|
|
18855
19023
|
if 0 <= xr < Wraw and 0 <= yr < Hraw:
|
|
18856
|
-
img_data
|
|
19024
|
+
if img_data.ndim == 2:
|
|
19025
|
+
img_data[yr, xr] = 0.0
|
|
19026
|
+
else:
|
|
19027
|
+
img_data[yr, xr, :] = 0.0
|
|
18857
19028
|
else:
|
|
18858
|
-
# Back-project via inverse affine
|
|
19029
|
+
# Back-project via inverse affine (H_canvas)
|
|
18859
19030
|
Hinv = np.linalg.inv(H_canvas)
|
|
18860
19031
|
for (x_r, y_r) in coords_for_this_file:
|
|
18861
19032
|
for (ox, oy) in offsets:
|
|
@@ -18864,35 +19035,43 @@ class StackingSuiteDialog(QDialog):
|
|
|
18864
19035
|
x_raw = int(round(v[0] / max(v[2], 1e-8)))
|
|
18865
19036
|
y_raw = int(round(v[1] / max(v[2], 1e-8)))
|
|
18866
19037
|
if 0 <= x_raw < Wraw and 0 <= y_raw < Hraw:
|
|
18867
|
-
img_data
|
|
19038
|
+
if img_data.ndim == 2:
|
|
19039
|
+
img_data[y_raw, x_raw] = 0.0
|
|
19040
|
+
else:
|
|
19041
|
+
img_data[y_raw, x_raw, :] = 0.0
|
|
19042
|
+
|
|
19043
|
+
# ---- deposit ----
|
|
19044
|
+
A23 = H_canvas[:2, :]
|
|
18868
19045
|
|
|
18869
|
-
# --- deposit (identity for registered pixels) ---
|
|
18870
19046
|
if deposit_func is drizzle_deposit_numba_naive:
|
|
18871
19047
|
drizzle_buffer, coverage_buffer = deposit_func(
|
|
18872
|
-
img_data,
|
|
19048
|
+
img_data, A23, drizzle_buffer, coverage_buffer,
|
|
19049
|
+
scale_factor, weight
|
|
18873
19050
|
)
|
|
19051
|
+
|
|
18874
19052
|
elif deposit_func is drizzle_deposit_color_naive:
|
|
18875
19053
|
drizzle_buffer, coverage_buffer = deposit_func(
|
|
18876
|
-
img_data,
|
|
19054
|
+
img_data, A23, drizzle_buffer, coverage_buffer,
|
|
19055
|
+
scale_factor, drop_shrink, weight
|
|
18877
19056
|
)
|
|
19057
|
+
|
|
18878
19058
|
else:
|
|
18879
|
-
|
|
19059
|
+
# kernel mono OR kernel color
|
|
18880
19060
|
drizzle_buffer, coverage_buffer = deposit_func(
|
|
18881
19061
|
img_data, A23, drizzle_buffer, coverage_buffer,
|
|
18882
19062
|
scale_factor, drop_shrink, weight, _kcode, float(gauss_sigma)
|
|
18883
19063
|
)
|
|
18884
19064
|
|
|
18885
|
-
|
|
18886
|
-
# --- finalize, save, optional autocrop ---
|
|
19065
|
+
# ---- finalize ----
|
|
18887
19066
|
final_drizzle = np.zeros_like(drizzle_buffer, dtype=np.float32)
|
|
18888
19067
|
final_drizzle = finalize_func(drizzle_buffer, coverage_buffer, final_drizzle)
|
|
18889
19068
|
|
|
18890
|
-
#
|
|
19069
|
+
# ---- save (single-HDU; no rejection layers here) ----
|
|
18891
19070
|
Hd, Wd = final_drizzle.shape[:2] if final_drizzle.ndim >= 2 else (0, 0)
|
|
18892
19071
|
display_group_driz = self._label_with_dims(group_key, Wd, Hd)
|
|
18893
19072
|
base_stem = f"MasterLight_{display_group_driz}_{len(file_list)}stacked_drizzle"
|
|
18894
|
-
base_stem = self._normalize_master_stem(base_stem)
|
|
18895
|
-
out_path_orig = self._build_out(self.
|
|
19073
|
+
base_stem = self._normalize_master_stem(base_stem)
|
|
19074
|
+
out_path_orig = self._build_out(self._master_light_dir(), base_stem, "fit")
|
|
18896
19075
|
|
|
18897
19076
|
hdr_orig = hdr.copy() if hdr is not None else fits.Header()
|
|
18898
19077
|
hdr_orig["IMAGETYP"] = "MASTER STACK - DRIZZLE"
|
|
@@ -18929,7 +19108,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18929
19108
|
)
|
|
18930
19109
|
log(f"✅ Drizzle (original) saved: {out_path_orig}")
|
|
18931
19110
|
|
|
18932
|
-
#
|
|
19111
|
+
# ---- optional auto-crop ----
|
|
18933
19112
|
if autocrop_enabled:
|
|
18934
19113
|
cropped_drizzle, hdr_crop = self._apply_autocrop(
|
|
18935
19114
|
final_drizzle,
|
|
@@ -18939,12 +19118,14 @@ class StackingSuiteDialog(QDialog):
|
|
|
18939
19118
|
rect_override=rect_override
|
|
18940
19119
|
)
|
|
18941
19120
|
hdr_crop["NCOMBINE"] = (n_frames, "Number of frames combined")
|
|
18942
|
-
hdr_crop["NSTACK"] = (n_frames, "Alias of NCOMBINE (SetiAstro)")
|
|
19121
|
+
hdr_crop["NSTACK"] = (n_frames, "Alias of NCOMBINE (SetiAstro)")
|
|
19122
|
+
|
|
18943
19123
|
is_mono_crop = (cropped_drizzle.ndim == 2)
|
|
18944
19124
|
display_group_driz_crop = self._label_with_dims(group_key, cropped_drizzle.shape[1], cropped_drizzle.shape[0])
|
|
18945
19125
|
base_crop = f"MasterLight_{display_group_driz_crop}_{len(file_list)}stacked_drizzle_autocrop"
|
|
18946
|
-
base_crop = self._normalize_master_stem(base_crop)
|
|
18947
|
-
out_path_crop = self._build_out(self.
|
|
19126
|
+
base_crop = self._normalize_master_stem(base_crop)
|
|
19127
|
+
out_path_crop = self._build_out(self._master_light_dir(), base_crop, "fit")
|
|
19128
|
+
|
|
18948
19129
|
cropped_drizzle = self._normalize_stack_01(cropped_drizzle)
|
|
18949
19130
|
save_image(
|
|
18950
19131
|
img_array=cropped_drizzle,
|