setiastrosuitepro 1.7.3__py3-none-any.whl → 1.7.5__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/clonestamp.png +0 -0
- 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/blemish_blaster.py +54 -14
- setiastro/saspro/clone_stamp.py +753 -0
- setiastro/saspro/gui/main_window.py +27 -6
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +10 -15
- 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/resources.py +2 -0
- setiastro/saspro/sfcc.py +14 -8
- setiastro/saspro/stacking_suite.py +413 -174
- 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.5.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.dist-info}/RECORD +52 -50
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.dist-info}/entry_points.txt +1 -1
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.5.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,
|
|
@@ -2295,6 +2295,36 @@ def compute_safe_chunk(height, width, N, channels, dtype, pref_h, pref_w):
|
|
|
2295
2295
|
|
|
2296
2296
|
_DIM_RE = re.compile(r"\s*\(\d+\s*x\s*\d+\)\s*")
|
|
2297
2297
|
|
|
2298
|
+
|
|
2299
|
+
def build_rejection_layers(rejection_map: dict, ref_H: int, ref_W: int, n_frames: int,
|
|
2300
|
+
*, comb_threshold_frac: float = 0.02):
|
|
2301
|
+
"""
|
|
2302
|
+
Returns:
|
|
2303
|
+
rej_cnt : (H,W) uint16 (or uint32 if needed)
|
|
2304
|
+
rej_frac : (H,W) float32 in [0..1]
|
|
2305
|
+
rej_any : (H,W) bool (thresholded, not "ever rejected")
|
|
2306
|
+
"""
|
|
2307
|
+
if n_frames <= 0:
|
|
2308
|
+
raise ValueError("n_frames must be > 0")
|
|
2309
|
+
|
|
2310
|
+
# Use uint32 if you might exceed 65535 rejections (rare)
|
|
2311
|
+
rej_cnt = np.zeros((ref_H, ref_W), dtype=np.uint32)
|
|
2312
|
+
|
|
2313
|
+
# rejection_map keys are per-file, values are coords in REGISTERED space
|
|
2314
|
+
for _fpath, coords in (rejection_map or {}).items():
|
|
2315
|
+
for (x, y) in coords:
|
|
2316
|
+
xi = int(x); yi = int(y)
|
|
2317
|
+
if 0 <= xi < ref_W and 0 <= yi < ref_H:
|
|
2318
|
+
rej_cnt[yi, xi] += 1
|
|
2319
|
+
|
|
2320
|
+
rej_frac = (rej_cnt.astype(np.float32) / float(n_frames))
|
|
2321
|
+
|
|
2322
|
+
# “combined mask” becomes “rejected in >= threshold% of frames”
|
|
2323
|
+
rej_any = (rej_frac >= float(comb_threshold_frac))
|
|
2324
|
+
|
|
2325
|
+
return rej_cnt, rej_frac, rej_any
|
|
2326
|
+
|
|
2327
|
+
|
|
2298
2328
|
class _Responder(QObject):
|
|
2299
2329
|
finished = pyqtSignal(object) # emits the edited dict or None
|
|
2300
2330
|
|
|
@@ -2472,36 +2502,38 @@ def _save_master_with_rejection_layers(
|
|
|
2472
2502
|
hdr: "fits.Header",
|
|
2473
2503
|
out_path: str,
|
|
2474
2504
|
*,
|
|
2475
|
-
rej_any: "np.ndarray | None" = None, # 2D bool
|
|
2505
|
+
rej_any: "np.ndarray | None" = None, # 2D bool (recommended: thresholded)
|
|
2476
2506
|
rej_frac: "np.ndarray | None" = None, # 2D float32 [0..1]
|
|
2507
|
+
rej_cnt: "np.ndarray | None" = None, # 2D uint16/uint32 counts
|
|
2477
2508
|
):
|
|
2478
2509
|
"""
|
|
2479
2510
|
Writes a MEF (multi-extension FITS) file:
|
|
2480
2511
|
- Primary HDU: the master image (2D or 3D) as float32
|
|
2481
2512
|
* Mono: (H, W)
|
|
2482
|
-
* Color: (
|
|
2483
|
-
- Optional EXTNAME=REJ_COMB: uint8 (0/1)
|
|
2484
|
-
- Optional EXTNAME=REJ_FRAC: float32 fraction-of-frames rejected per pixel
|
|
2513
|
+
* Color: (C, H, W) channels-first for FITS
|
|
2514
|
+
- Optional EXTNAME=REJ_COMB: uint8 (0/1) thresholded rejection mask
|
|
2515
|
+
- Optional EXTNAME=REJ_FRAC: float32 fraction-of-frames rejected per pixel [0..1]
|
|
2516
|
+
- Optional EXTNAME=REJ_CNT : uint16/uint32 count of frames rejected per pixel
|
|
2485
2517
|
"""
|
|
2518
|
+
|
|
2519
|
+
from astropy.io import fits
|
|
2520
|
+
from datetime import datetime
|
|
2521
|
+
|
|
2486
2522
|
# --- sanitize/shape primary data ---
|
|
2487
2523
|
data = np.asarray(img_array, dtype=np.float32, order="C")
|
|
2488
2524
|
|
|
2489
2525
|
# If channels-last, move to channels-first for FITS
|
|
2490
2526
|
if data.ndim == 3:
|
|
2491
|
-
# squeeze accidental singleton channels
|
|
2492
2527
|
if data.shape[-1] == 1:
|
|
2493
|
-
data = np.squeeze(data, axis=-1)
|
|
2494
|
-
elif data.shape[-1] in (3, 4):
|
|
2495
|
-
data = np.transpose(data, (2, 0, 1))
|
|
2496
|
-
# If already (C, H, W) leave it as-is.
|
|
2528
|
+
data = np.squeeze(data, axis=-1) # (H, W)
|
|
2529
|
+
elif data.shape[-1] in (3, 4):
|
|
2530
|
+
data = np.transpose(data, (2, 0, 1)) # (C, H, W)
|
|
2497
2531
|
|
|
2498
|
-
# After squeeze/transpose, re-evaluate dims
|
|
2499
2532
|
if data.ndim not in (2, 3):
|
|
2500
2533
|
raise ValueError(f"Unsupported master image shape for FITS: {data.shape}")
|
|
2501
2534
|
|
|
2502
2535
|
# --- clone + annotate header, and align NAXIS* with 'data' ---
|
|
2503
2536
|
H = (hdr.copy() if hdr is not None else fits.Header())
|
|
2504
|
-
# purge prior NAXIS keys to avoid conflicts after transpose/squeeze
|
|
2505
2537
|
for k in ("NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "NAXIS4"):
|
|
2506
2538
|
if k in H:
|
|
2507
2539
|
del H[k]
|
|
@@ -2512,41 +2544,50 @@ def _save_master_with_rejection_layers(
|
|
|
2512
2544
|
H["CREATOR"] = "SetiAstroSuite"
|
|
2513
2545
|
H["DATE-OBS"] = datetime.utcnow().isoformat()
|
|
2514
2546
|
|
|
2515
|
-
# Fill NAXIS* to match data (optional; Astropy will infer if omitted)
|
|
2516
2547
|
if data.ndim == 2:
|
|
2517
2548
|
H["NAXIS"] = 2
|
|
2518
|
-
H["NAXIS1"] = int(data.shape[1])
|
|
2519
|
-
H["NAXIS2"] = int(data.shape[0])
|
|
2549
|
+
H["NAXIS1"] = int(data.shape[1])
|
|
2550
|
+
H["NAXIS2"] = int(data.shape[0])
|
|
2520
2551
|
else:
|
|
2521
|
-
# data.shape == (C, H, W)
|
|
2522
2552
|
C, Hh, Ww = data.shape
|
|
2523
2553
|
H["NAXIS"] = 3
|
|
2524
|
-
H["NAXIS1"] = int(Ww)
|
|
2525
|
-
H["NAXIS2"] = int(Hh)
|
|
2526
|
-
H["NAXIS3"] = int(C)
|
|
2554
|
+
H["NAXIS1"] = int(Ww)
|
|
2555
|
+
H["NAXIS2"] = int(Hh)
|
|
2556
|
+
H["NAXIS3"] = int(C)
|
|
2527
2557
|
|
|
2528
|
-
# --- build HDU list ---
|
|
2529
2558
|
prim = fits.PrimaryHDU(data=data, header=H)
|
|
2530
2559
|
hdul = [prim]
|
|
2531
2560
|
|
|
2532
|
-
# Optional layers: must be 2D (H, W)
|
|
2561
|
+
# --- Optional layers: all must be 2D (H, W) in REGISTERED space ---
|
|
2533
2562
|
if rej_any is not None:
|
|
2534
2563
|
rej_any_2d = np.asarray(rej_any, dtype=bool)
|
|
2535
2564
|
if rej_any_2d.ndim != 2:
|
|
2536
2565
|
raise ValueError(f"REJ_COMB must be 2D, got {rej_any_2d.shape}")
|
|
2537
2566
|
h = fits.Header()
|
|
2538
2567
|
h["EXTNAME"] = "REJ_COMB"
|
|
2539
|
-
h["COMMENT"] = "
|
|
2568
|
+
h["COMMENT"] = "Thresholded rejection mask (1=frequently rejected, not merely 'ever')"
|
|
2540
2569
|
hdul.append(fits.ImageHDU(data=rej_any_2d.astype(np.uint8, copy=False), header=h))
|
|
2541
2570
|
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2571
|
+
if rej_frac is not None:
|
|
2572
|
+
rej_frac_2d = np.asarray(rej_frac, dtype=np.float32)
|
|
2573
|
+
if rej_frac_2d.ndim != 2:
|
|
2574
|
+
raise ValueError(f"REJ_FRAC must be 2D, got {rej_frac_2d.shape}")
|
|
2575
|
+
h = fits.Header()
|
|
2576
|
+
h["EXTNAME"] = "REJ_FRAC"
|
|
2577
|
+
h["COMMENT"] = "Per-pixel fraction of frames rejected [0..1]"
|
|
2578
|
+
hdul.append(fits.ImageHDU(data=rej_frac_2d, header=h))
|
|
2579
|
+
|
|
2580
|
+
if rej_cnt is not None:
|
|
2581
|
+
rej_cnt_2d = np.asarray(rej_cnt)
|
|
2582
|
+
if rej_cnt_2d.ndim != 2:
|
|
2583
|
+
raise ValueError(f"REJ_CNT must be 2D, got {rej_cnt_2d.shape}")
|
|
2584
|
+
# keep integer type; clamp to uint16 if you prefer smaller files
|
|
2585
|
+
if rej_cnt_2d.dtype not in (np.uint16, np.uint32, np.int32, np.int64):
|
|
2586
|
+
rej_cnt_2d = rej_cnt_2d.astype(np.uint32, copy=False)
|
|
2587
|
+
h = fits.Header()
|
|
2588
|
+
h["EXTNAME"] = "REJ_CNT"
|
|
2589
|
+
h["COMMENT"] = "Per-pixel count of frames rejected"
|
|
2590
|
+
hdul.append(fits.ImageHDU(data=rej_cnt_2d, header=h))
|
|
2550
2591
|
|
|
2551
2592
|
fits.HDUList(hdul).writeto(out_path, overwrite=True)
|
|
2552
2593
|
|
|
@@ -6522,6 +6563,18 @@ class StackingSuiteDialog(QDialog):
|
|
|
6522
6563
|
w.blockSignals(True); w.setValue(v); w.blockSignals(False)
|
|
6523
6564
|
|
|
6524
6565
|
def _get_drizzle_scale(self) -> float:
|
|
6566
|
+
# UI combo wins if it exists
|
|
6567
|
+
combo = getattr(self, "drizzle_scale_combo", None)
|
|
6568
|
+
if combo is not None:
|
|
6569
|
+
try:
|
|
6570
|
+
txt = str(combo.currentText()).strip().lower()
|
|
6571
|
+
if txt.endswith("x"):
|
|
6572
|
+
txt = txt[:-1]
|
|
6573
|
+
return float(txt)
|
|
6574
|
+
except Exception:
|
|
6575
|
+
pass
|
|
6576
|
+
|
|
6577
|
+
# fallback to settings
|
|
6525
6578
|
val = self.settings.value("stacking/drizzle_scale", "2x")
|
|
6526
6579
|
if isinstance(val, (int, float)):
|
|
6527
6580
|
return float(val)
|
|
@@ -6536,6 +6589,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
6536
6589
|
return 2.0
|
|
6537
6590
|
|
|
6538
6591
|
|
|
6592
|
+
|
|
6539
6593
|
def _set_drizzle_scale(self, r: float | str) -> None:
|
|
6540
6594
|
if isinstance(r, str):
|
|
6541
6595
|
try: r = float(r.rstrip("xX"))
|
|
@@ -7202,6 +7256,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
7202
7256
|
)
|
|
7203
7257
|
main_layout.addWidget(self.clear_master_flat_selection_btn)
|
|
7204
7258
|
self.override_dark_combo.currentIndexChanged[int].connect(self.override_selected_master_dark_for_flats)
|
|
7259
|
+
# Connect selection change to update combobox
|
|
7260
|
+
self.flat_tree.itemSelectionChanged.connect(self._on_flat_tree_selection_changed)
|
|
7205
7261
|
self.update_override_dark_combo()
|
|
7206
7262
|
self.rebuild_flat_tree()
|
|
7207
7263
|
|
|
@@ -8846,8 +8902,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
8846
8902
|
tab.setLayout(layout)
|
|
8847
8903
|
|
|
8848
8904
|
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.
|
|
8905
|
+
self.drizzle_scale_combo.setCurrentText(f"{int(self._get_drizzle_scale())}x")
|
|
8906
|
+
self.drizzle_drop_shrink_spin.setValue(self._get_drizzle_pixfrac())
|
|
8851
8907
|
|
|
8852
8908
|
drizzle_on = self.settings.value("stacking/drizzle_enabled", False, type=bool)
|
|
8853
8909
|
self.cfa_drizzle_cb.setEnabled(drizzle_on)
|
|
@@ -8901,6 +8957,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
8901
8957
|
# wire it
|
|
8902
8958
|
self.comet_cb.toggled.connect(lambda _v: self._refresh_comet_starless_enable())
|
|
8903
8959
|
self.comet_save_starless_cb.toggled.connect(lambda _v: self._refresh_comet_starless_enable()) # optional belt/suspenders
|
|
8960
|
+
self.drizzle_drop_shrink_spin.valueChanged.connect(self._on_drizzle_param_changed)
|
|
8904
8961
|
|
|
8905
8962
|
# run once AFTER you restore all initial states from settings
|
|
8906
8963
|
self._refresh_comet_starless_enable()
|
|
@@ -8963,11 +9020,16 @@ class StackingSuiteDialog(QDialog):
|
|
|
8963
9020
|
self._update_drizzle_summary_columns()
|
|
8964
9021
|
|
|
8965
9022
|
def _on_drizzle_param_changed(self, *_):
|
|
8966
|
-
#
|
|
8967
|
-
self
|
|
8968
|
-
|
|
8969
|
-
|
|
8970
|
-
#
|
|
9023
|
+
# persist scale from UI
|
|
9024
|
+
if hasattr(self, "drizzle_scale_combo"):
|
|
9025
|
+
self._set_drizzle_scale(self.drizzle_scale_combo.currentText())
|
|
9026
|
+
|
|
9027
|
+
# (optional) persist pixfrac too if you want “global controls are canonical”
|
|
9028
|
+
if hasattr(self, "drizzle_drop_shrink_spin"):
|
|
9029
|
+
self._set_drizzle_pixfrac(float(self.drizzle_drop_shrink_spin.value()))
|
|
9030
|
+
|
|
9031
|
+
self.settings.sync()
|
|
9032
|
+
self._update_drizzle_summary_columns() # if you show “1x / 0.65” in the tree
|
|
8971
9033
|
|
|
8972
9034
|
def _on_star_trail_toggled(self, enabled: bool):
|
|
8973
9035
|
"""
|
|
@@ -11703,6 +11765,51 @@ class StackingSuiteDialog(QDialog):
|
|
|
11703
11765
|
|
|
11704
11766
|
|
|
11705
11767
|
|
|
11768
|
+
def _on_flat_tree_selection_changed(self):
|
|
11769
|
+
"""Update the override_dark_combo to reflect the value stored in the selected row."""
|
|
11770
|
+
items = self.flat_tree.selectedItems()
|
|
11771
|
+
if not items:
|
|
11772
|
+
return
|
|
11773
|
+
|
|
11774
|
+
# Get the first top-level item (group row, not a leaf)
|
|
11775
|
+
selected_item = None
|
|
11776
|
+
for it in items:
|
|
11777
|
+
if not it.parent(): # Top-level item (group row)
|
|
11778
|
+
selected_item = it
|
|
11779
|
+
break
|
|
11780
|
+
|
|
11781
|
+
if not selected_item:
|
|
11782
|
+
return
|
|
11783
|
+
|
|
11784
|
+
# Read the stored value from column 2
|
|
11785
|
+
ud = selected_item.data(2, Qt.ItemDataRole.UserRole)
|
|
11786
|
+
|
|
11787
|
+
# Block signals to avoid triggering override_selected_master_dark_for_flats
|
|
11788
|
+
self.override_dark_combo.blockSignals(True)
|
|
11789
|
+
try:
|
|
11790
|
+
# Find the matching index in the combobox
|
|
11791
|
+
if ud is None:
|
|
11792
|
+
# "None (Use Auto-Select)" should be at index 0
|
|
11793
|
+
self.override_dark_combo.setCurrentIndex(0)
|
|
11794
|
+
elif ud == "__NO_DARK__":
|
|
11795
|
+
# "None (Use no Dark to Calibrate)" should be at index 1
|
|
11796
|
+
self.override_dark_combo.setCurrentIndex(1)
|
|
11797
|
+
else:
|
|
11798
|
+
# Find the item with matching userData (path)
|
|
11799
|
+
found_idx = -1
|
|
11800
|
+
for i in range(self.override_dark_combo.count()):
|
|
11801
|
+
item_ud = self.override_dark_combo.itemData(i)
|
|
11802
|
+
if item_ud == ud:
|
|
11803
|
+
found_idx = i
|
|
11804
|
+
break
|
|
11805
|
+
if found_idx >= 0:
|
|
11806
|
+
self.override_dark_combo.setCurrentIndex(found_idx)
|
|
11807
|
+
else:
|
|
11808
|
+
# If not found, default to index 0
|
|
11809
|
+
self.override_dark_combo.setCurrentIndex(0)
|
|
11810
|
+
finally:
|
|
11811
|
+
self.override_dark_combo.blockSignals(False)
|
|
11812
|
+
|
|
11706
11813
|
def override_selected_master_dark_for_flats(self, idx: int):
|
|
11707
11814
|
"""Apply combo choice to selected flat groups; stores path/token in row + dict."""
|
|
11708
11815
|
items = self.flat_tree.selectedItems()
|
|
@@ -15752,7 +15859,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
15752
15859
|
filters = "TIFF (*.tif);;PNG (*.png);;JPEG (*.jpg *.jpeg);;FITS (*.fits);;XISF (*.xisf)"
|
|
15753
15860
|
path, chosen_filter = QFileDialog.getSaveFileName(
|
|
15754
15861
|
self, "Save Star-Trail Image",
|
|
15755
|
-
os.path.join(self.
|
|
15862
|
+
os.path.join(self._master_light_dir(), default_name),
|
|
15756
15863
|
"TIFF (*.tif);;PNG (*.png);;JPEG (*.jpg *.jpeg);;FITS (*.fits);;XISF (*.xisf)"
|
|
15757
15864
|
)
|
|
15758
15865
|
if not path:
|
|
@@ -15879,8 +15986,11 @@ class StackingSuiteDialog(QDialog):
|
|
|
15879
15986
|
"""
|
|
15880
15987
|
Take aligned OSC frames and produce per-band aligned mono FITS.
|
|
15881
15988
|
|
|
15882
|
-
|
|
15883
|
-
|
|
15989
|
+
IMPORTANT (for drizzle correctness):
|
|
15990
|
+
- We also populate self._split_drizzle_info so drizzle can deposit from the
|
|
15991
|
+
*parent original* pixels while using the same transforms solved from originals.
|
|
15992
|
+
Mapping:
|
|
15993
|
+
split_aligned_path -> {"orig": <parent original>, "chan": "R"|"G"|None}
|
|
15884
15994
|
|
|
15885
15995
|
Returns a new light_files-style dict:
|
|
15886
15996
|
{ "Ha - ...": [aligned_Ha_*.fit...],
|
|
@@ -15895,18 +16005,35 @@ class StackingSuiteDialog(QDialog):
|
|
|
15895
16005
|
out_dir = os.path.join(self.stacking_directory, "DualBand_Split")
|
|
15896
16006
|
os.makedirs(out_dir, exist_ok=True)
|
|
15897
16007
|
|
|
16008
|
+
# split_aligned_path -> {"orig": <parent original>, "chan": "R"|"G"|None}
|
|
16009
|
+
if not hasattr(self, "_split_drizzle_info") or self._split_drizzle_info is None:
|
|
16010
|
+
self._split_drizzle_info = {}
|
|
16011
|
+
else:
|
|
16012
|
+
# Clear any previous run’s mapping to avoid mixing sessions
|
|
16013
|
+
self._split_drizzle_info.clear()
|
|
16014
|
+
|
|
16015
|
+
split_info = self._split_drizzle_info
|
|
16016
|
+
|
|
15898
16017
|
# Collect per-band file lists (some may already be mono NB)
|
|
15899
16018
|
ha_files, sii_files, oiii_files, hb_files = [], [], [], []
|
|
15900
|
-
inherit_map: dict[str, set[str]] = {} # new_group_key -> set(parent_group)
|
|
15901
16019
|
parent_of: dict[str, str] = {} # new_path_or_existing -> parent_group
|
|
15902
16020
|
|
|
15903
16021
|
# Snapshot old drizzle configs so we can inherit to the new NB groups
|
|
15904
16022
|
old_drizzle = dict(getattr(self, "per_group_drizzle", {}))
|
|
15905
16023
|
selected_groups = set(getattr(self, "_reg_selected_groups", set()))
|
|
15906
16024
|
|
|
16025
|
+
# Normalized aligned->original map (should exist from your registration pipeline)
|
|
16026
|
+
oba = getattr(self, "orig_by_aligned", {}) or {}
|
|
16027
|
+
|
|
15907
16028
|
# --- Pass 1: classify + split each aligned frame ---
|
|
15908
16029
|
for group, files in aligned_light_files.items():
|
|
15909
16030
|
for fp in files:
|
|
16031
|
+
fpn = os.path.normpath(fp)
|
|
16032
|
+
|
|
16033
|
+
# Find parent original for this aligned frame (for drizzle)
|
|
16034
|
+
parent_orig = oba.get(fpn) or oba.get(os.path.normpath(fp))
|
|
16035
|
+
parent_orig = os.path.normpath(parent_orig) if parent_orig else None
|
|
16036
|
+
|
|
15910
16037
|
try:
|
|
15911
16038
|
with fits.open(fp, memmap=False) as hdul:
|
|
15912
16039
|
data = hdul[0].data
|
|
@@ -15927,16 +16054,12 @@ class StackingSuiteDialog(QDialog):
|
|
|
15927
16054
|
# ----------------------------------------
|
|
15928
16055
|
# Detect layout: mono vs RGB, CHW vs HWC
|
|
15929
16056
|
# ----------------------------------------
|
|
15930
|
-
layout = None
|
|
15931
|
-
|
|
15932
16057
|
if arr.ndim == 2:
|
|
15933
16058
|
layout = "MONO"
|
|
15934
16059
|
elif arr.ndim == 3:
|
|
15935
16060
|
c0, c1, c2 = arr.shape
|
|
15936
|
-
# channels-first (C, H, W)
|
|
15937
16061
|
if c0 in (1, 3) and c1 > 8 and c2 > 8:
|
|
15938
16062
|
layout = "CHW"
|
|
15939
|
-
# channels-last (H, W, C)
|
|
15940
16063
|
elif c2 in (1, 3) and c0 > 8 and c1 > 8:
|
|
15941
16064
|
layout = "HWC"
|
|
15942
16065
|
else:
|
|
@@ -15945,7 +16068,11 @@ class StackingSuiteDialog(QDialog):
|
|
|
15945
16068
|
layout = "UNKNOWN"
|
|
15946
16069
|
|
|
15947
16070
|
# ---- MONO narrowband frames (Ha/SII/OIII/Hb) ----
|
|
15948
|
-
if
|
|
16071
|
+
if (
|
|
16072
|
+
layout == "MONO"
|
|
16073
|
+
or (layout == "CHW" and arr.shape[0] == 1)
|
|
16074
|
+
or (layout == "HWC" and arr.shape[-1] == 1)
|
|
16075
|
+
):
|
|
15949
16076
|
# Treat as mono NB based on classifier
|
|
15950
16077
|
if cls == "MONO_HA":
|
|
15951
16078
|
ha_files.append(fp); parent_of[fp] = group
|
|
@@ -15955,7 +16082,14 @@ class StackingSuiteDialog(QDialog):
|
|
|
15955
16082
|
oiii_files.append(fp); parent_of[fp] = group
|
|
15956
16083
|
elif cls == "MONO_HB":
|
|
15957
16084
|
hb_files.append(fp); parent_of[fp] = group
|
|
15958
|
-
|
|
16085
|
+
else:
|
|
16086
|
+
# Unknown mono: ignore for NB split
|
|
16087
|
+
pass
|
|
16088
|
+
|
|
16089
|
+
# For drizzle mapping: if this is a split pipeline, we still may want
|
|
16090
|
+
# aligned mono to drizzle from parent original (chan None means “use mono as-is”)
|
|
16091
|
+
if parent_orig:
|
|
16092
|
+
split_info[fpn] = {"orig": parent_orig, "chan": None}
|
|
15959
16093
|
continue
|
|
15960
16094
|
|
|
15961
16095
|
# ---- RGB / OSC case (what we expect for dual-band) ----
|
|
@@ -15972,39 +16106,55 @@ class StackingSuiteDialog(QDialog):
|
|
|
15972
16106
|
R = arr[..., 0]
|
|
15973
16107
|
G = arr[..., 1]
|
|
15974
16108
|
else:
|
|
15975
|
-
#
|
|
15976
|
-
# C should be 3
|
|
16109
|
+
# (C, H, W)
|
|
15977
16110
|
R = arr[0, ...]
|
|
15978
16111
|
G = arr[1, ...]
|
|
15979
16112
|
|
|
15980
16113
|
base = os.path.splitext(os.path.basename(fp))[0]
|
|
15981
16114
|
|
|
16115
|
+
def _map_split(path_written: str, chan: str):
|
|
16116
|
+
"""Record drizzle info for this newly created split aligned file."""
|
|
16117
|
+
if parent_orig:
|
|
16118
|
+
split_info[os.path.normpath(path_written)] = {"orig": parent_orig, "chan": chan}
|
|
16119
|
+
|
|
15982
16120
|
if cls == "DUAL_HA_OIII":
|
|
15983
16121
|
ha_path = os.path.join(out_dir, f"{base}_Ha.fit")
|
|
15984
16122
|
oiii_path = os.path.join(out_dir, f"{base}_OIII.fit")
|
|
15985
|
-
self._write_band_fit(ha_path,
|
|
16123
|
+
self._write_band_fit(ha_path, R, hdr, "Ha", src_filter=filt)
|
|
15986
16124
|
self._write_band_fit(oiii_path, G, hdr, "OIII", src_filter=filt)
|
|
16125
|
+
|
|
15987
16126
|
ha_files.append(ha_path); parent_of[ha_path] = group
|
|
15988
16127
|
oiii_files.append(oiii_path); parent_of[oiii_path] = group
|
|
15989
16128
|
|
|
16129
|
+
_map_split(ha_path, "R")
|
|
16130
|
+
_map_split(oiii_path, "G")
|
|
16131
|
+
|
|
15990
16132
|
elif cls == "DUAL_SII_OIII":
|
|
15991
16133
|
sii_path = os.path.join(out_dir, f"{base}_SII.fit")
|
|
15992
16134
|
oiii_path = os.path.join(out_dir, f"{base}_OIII.fit")
|
|
15993
|
-
self._write_band_fit(sii_path,
|
|
16135
|
+
self._write_band_fit(sii_path, R, hdr, "SII", src_filter=filt)
|
|
15994
16136
|
self._write_band_fit(oiii_path, G, hdr, "OIII", src_filter=filt)
|
|
15995
|
-
|
|
15996
|
-
|
|
16137
|
+
|
|
16138
|
+
sii_files.append(sii_path); parent_of[sii_path] = group
|
|
16139
|
+
oiii_files.append(oiii_path); parent_of[oiii_path] = group
|
|
16140
|
+
|
|
16141
|
+
_map_split(sii_path, "R")
|
|
16142
|
+
_map_split(oiii_path, "G")
|
|
15997
16143
|
|
|
15998
16144
|
elif cls == "DUAL_SII_HB":
|
|
15999
16145
|
sii_path = os.path.join(out_dir, f"{base}_SII.fit")
|
|
16000
16146
|
hb_path = os.path.join(out_dir, f"{base}_Hb.fit")
|
|
16001
16147
|
self._write_band_fit(sii_path, R, hdr, "SII", src_filter=filt)
|
|
16002
16148
|
self._write_band_fit(hb_path, G, hdr, "Hb", src_filter=filt)
|
|
16149
|
+
|
|
16003
16150
|
sii_files.append(sii_path); parent_of[sii_path] = group
|
|
16004
16151
|
hb_files.append(hb_path); parent_of[hb_path] = group
|
|
16005
16152
|
|
|
16153
|
+
_map_split(sii_path, "R")
|
|
16154
|
+
_map_split(hb_path, "G")
|
|
16155
|
+
|
|
16006
16156
|
else:
|
|
16007
|
-
# UNKNOWN dual → ignore
|
|
16157
|
+
# UNKNOWN dual → ignore
|
|
16008
16158
|
continue
|
|
16009
16159
|
|
|
16010
16160
|
# --- Pass 2: group the new files (band + exposure + size), same as before ---
|
|
@@ -16020,7 +16170,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
16020
16170
|
return f"{band} - ? - ?x?"
|
|
16021
16171
|
|
|
16022
16172
|
new_groups: dict[str, list[str]] = {}
|
|
16023
|
-
inherit_map = {}
|
|
16173
|
+
inherit_map: dict[str, set[str]] = {}
|
|
16024
16174
|
|
|
16025
16175
|
for band, flist in (
|
|
16026
16176
|
("Ha", ha_files),
|
|
@@ -16043,7 +16193,6 @@ class StackingSuiteDialog(QDialog):
|
|
|
16043
16193
|
return aligned_light_files
|
|
16044
16194
|
|
|
16045
16195
|
# --- Replace light_files with NB groups & seed drizzle configs ---
|
|
16046
|
-
|
|
16047
16196
|
self.light_files = new_groups
|
|
16048
16197
|
|
|
16049
16198
|
self.per_group_drizzle = {}
|
|
@@ -16087,6 +16236,11 @@ class StackingSuiteDialog(QDialog):
|
|
|
16087
16236
|
return new_groups
|
|
16088
16237
|
|
|
16089
16238
|
|
|
16239
|
+
def _master_light_dir(self) -> str:
|
|
16240
|
+
out_dir = os.path.join(self.stacking_directory, "Master_Light")
|
|
16241
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
16242
|
+
return out_dir
|
|
16243
|
+
|
|
16090
16244
|
def on_registration_complete(self, success, msg):
|
|
16091
16245
|
|
|
16092
16246
|
self.update_status(self.tr(msg))
|
|
@@ -16504,7 +16658,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
16504
16658
|
|
|
16505
16659
|
group_key, frames = self._mf_queue.pop(0)
|
|
16506
16660
|
|
|
16507
|
-
out_dir =
|
|
16661
|
+
out_dir = self._master_light_dir()
|
|
16508
16662
|
os.makedirs(out_dir, exist_ok=True)
|
|
16509
16663
|
out_path = os.path.join(out_dir, f"MasterLight_{group_key}_MFDeconv.fit")
|
|
16510
16664
|
|
|
@@ -17020,22 +17174,35 @@ class StackingSuiteDialog(QDialog):
|
|
|
17020
17174
|
display_group = self._label_with_dims(group_key, W, H)
|
|
17021
17175
|
base = f"MasterLight_{display_group}_{n_frames_group}stacked"
|
|
17022
17176
|
base = self._normalize_master_stem(base)
|
|
17023
|
-
out_path_orig = self._build_out(self.
|
|
17177
|
+
out_path_orig = self._build_out(self._master_light_dir(), base, "fit")
|
|
17024
17178
|
|
|
17025
17179
|
# Try to attach rejection maps that were accumulated during integration
|
|
17026
17180
|
maps = getattr(self, "_rej_maps", {}).get(group_key)
|
|
17027
17181
|
save_layers = self.settings.value("stacking/save_rejection_layers", True, type=bool)
|
|
17028
17182
|
|
|
17029
|
-
|
|
17183
|
+
save_layers = self.settings.value("stacking/save_rejection_layers", True, type=bool)
|
|
17184
|
+
|
|
17185
|
+
if rejection_map and save_layers:
|
|
17030
17186
|
try:
|
|
17187
|
+
# integrated_image is already normalized above; rejection coords are assumed in this (H,W) space
|
|
17188
|
+
rej_cnt, rej_frac, rej_any = build_rejection_layers(
|
|
17189
|
+
rejection_map=rejection_map,
|
|
17190
|
+
ref_H=H,
|
|
17191
|
+
ref_W=W,
|
|
17192
|
+
n_frames=n_frames_group,
|
|
17193
|
+
comb_threshold_frac=0.02, # tweakable
|
|
17194
|
+
)
|
|
17195
|
+
|
|
17031
17196
|
_save_master_with_rejection_layers(
|
|
17032
|
-
integrated_image,
|
|
17033
|
-
hdr_orig,
|
|
17034
|
-
out_path_orig,
|
|
17035
|
-
rej_any
|
|
17036
|
-
rej_frac=
|
|
17197
|
+
img_array=integrated_image,
|
|
17198
|
+
hdr=hdr_orig,
|
|
17199
|
+
out_path=out_path_orig,
|
|
17200
|
+
rej_any=rej_any,
|
|
17201
|
+
rej_frac=rej_frac,
|
|
17202
|
+
rej_cnt=rej_cnt,
|
|
17037
17203
|
)
|
|
17038
17204
|
log(f"✅ Saved integrated image (with rejection layers) for '{group_key}': {out_path_orig}")
|
|
17205
|
+
|
|
17039
17206
|
except Exception as e:
|
|
17040
17207
|
log(f"⚠️ MEF save failed ({e}); falling back to single-HDU save.")
|
|
17041
17208
|
save_image(
|
|
@@ -17047,8 +17214,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
17047
17214
|
is_mono=is_mono_orig
|
|
17048
17215
|
)
|
|
17049
17216
|
log(f"✅ Saved integrated image (single-HDU) for '{group_key}': {out_path_orig}")
|
|
17217
|
+
|
|
17050
17218
|
else:
|
|
17051
|
-
# No maps available or feature disabled → single-HDU save
|
|
17052
17219
|
save_image(
|
|
17053
17220
|
img_array=integrated_image,
|
|
17054
17221
|
filename=out_path_orig,
|
|
@@ -17128,7 +17295,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17128
17295
|
display_group_crop = self._label_with_dims(group_key, Wc, Hc)
|
|
17129
17296
|
base_crop = f"MasterLight_{display_group_crop}_{n_frames_group}stacked_autocrop"
|
|
17130
17297
|
base_crop = self._normalize_master_stem(base_crop)
|
|
17131
|
-
out_path_crop = self._build_out(self.
|
|
17298
|
+
out_path_crop = self._build_out(self._master_light_dir(), base_crop, "fit")
|
|
17132
17299
|
|
|
17133
17300
|
save_image(
|
|
17134
17301
|
img_array=cropped_img,
|
|
@@ -17276,7 +17443,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17276
17443
|
Hc, Wc = comet_only.shape[:2]
|
|
17277
17444
|
display_group_c = self._label_with_dims(group_key, Wc, Hc)
|
|
17278
17445
|
comet_path = self._build_out(
|
|
17279
|
-
self.
|
|
17446
|
+
self._master_light_dir(),
|
|
17280
17447
|
f"MasterCometOnly_{display_group_c}_{len(usable)}stacked",
|
|
17281
17448
|
"fit"
|
|
17282
17449
|
)
|
|
@@ -17303,7 +17470,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17303
17470
|
Hcc, Wcc = comet_only_crop.shape[:2]
|
|
17304
17471
|
display_group_cc = self._label_with_dims(group_key, Wcc, Hcc)
|
|
17305
17472
|
comet_path_crop = self._build_out(
|
|
17306
|
-
self.
|
|
17473
|
+
self._master_light_dir(),
|
|
17307
17474
|
f"MasterCometOnly_{display_group_cc}_{len(usable)}stacked_autocrop",
|
|
17308
17475
|
"fit"
|
|
17309
17476
|
)
|
|
@@ -17332,7 +17499,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17332
17499
|
|
|
17333
17500
|
is_mono_blend = (blend.ndim == 2) or (blend.ndim == 3 and blend.shape[2] == 1)
|
|
17334
17501
|
blend_path = self._build_out(
|
|
17335
|
-
self.
|
|
17502
|
+
self._master_light_dir(),
|
|
17336
17503
|
f"MasterCometBlend_{display_group_c}_{len(usable)}stacked",
|
|
17337
17504
|
"fit"
|
|
17338
17505
|
)
|
|
@@ -17352,7 +17519,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
17352
17519
|
Hb, Wb = blend_crop.shape[:2]
|
|
17353
17520
|
display_group_bc = self._label_with_dims(group_key, Wb, Hb)
|
|
17354
17521
|
blend_path_crop = self._build_out(
|
|
17355
|
-
self.
|
|
17522
|
+
self._master_light_dir(),
|
|
17356
17523
|
f"MasterCometBlend_{display_group_bc}_{len(usable)}stacked_autocrop",
|
|
17357
17524
|
"fit"
|
|
17358
17525
|
)
|
|
@@ -17368,19 +17535,14 @@ class StackingSuiteDialog(QDialog):
|
|
|
17368
17535
|
sasr_path = os.path.join(self.stacking_directory, f"{group_key}_rejections.sasr")
|
|
17369
17536
|
self.save_rejection_map_sasr(rejection_map, sasr_path)
|
|
17370
17537
|
log(f"✅ Saved rejection map to {sasr_path}")
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
"integrated_image": integrated_image,
|
|
17380
|
-
"rejection_map": None,
|
|
17381
|
-
"n_frames": n_frames_group,
|
|
17382
|
-
"drizzled": False
|
|
17383
|
-
}
|
|
17538
|
+
|
|
17539
|
+
group_integration_data[group_key] = {
|
|
17540
|
+
"integrated_image": integrated_image,
|
|
17541
|
+
"rejection_map": rejection_map if drizzle_enabled_global else None,
|
|
17542
|
+
"n_frames": n_frames_group,
|
|
17543
|
+
"drizzled": False, # <-- ALWAYS false here
|
|
17544
|
+
}
|
|
17545
|
+
if not drizzle_enabled_global:
|
|
17384
17546
|
log(f"ℹ️ Skipping rejection map save for '{group_key}' (drizzle disabled).")
|
|
17385
17547
|
|
|
17386
17548
|
QApplication.processEvents()
|
|
@@ -17409,22 +17571,55 @@ class StackingSuiteDialog(QDialog):
|
|
|
17409
17571
|
|
|
17410
17572
|
rejections_for_group = group_integration_data[group_key]["rejection_map"]
|
|
17411
17573
|
n_frames_group = group_integration_data[group_key]["n_frames"]
|
|
17574
|
+
# Build drizzle entries for each group
|
|
17575
|
+
split_info = getattr(self, "_split_drizzle_info", {}) or {}
|
|
17576
|
+
oba = getattr(self, "orig_by_aligned", {}) or {}
|
|
17577
|
+
|
|
17578
|
+
entries_by_group = {}
|
|
17579
|
+
for gk, aligned_list in grouped_files.items():
|
|
17580
|
+
entries = []
|
|
17581
|
+
for ap in aligned_list:
|
|
17582
|
+
apn = os.path.normpath(ap)
|
|
17583
|
+
info = split_info.get(apn)
|
|
17584
|
+
|
|
17585
|
+
if info and info.get("orig"):
|
|
17586
|
+
entries.append({
|
|
17587
|
+
"orig": os.path.normpath(info["orig"]), # parent original pixels
|
|
17588
|
+
"aligned": apn, # split aligned (rej/weights)
|
|
17589
|
+
"chan": info.get("chan", None), # 'R'/'G'/None
|
|
17590
|
+
})
|
|
17591
|
+
else:
|
|
17592
|
+
# normal (non-split) case
|
|
17593
|
+
op = oba.get(apn)
|
|
17594
|
+
if op:
|
|
17595
|
+
entries.append({"orig": os.path.normpath(op), "aligned": apn, "chan": None})
|
|
17596
|
+
|
|
17597
|
+
entries_by_group[gk] = entries
|
|
17412
17598
|
|
|
17413
17599
|
log(f"📐 Drizzle for '{group_key}' at {scale_factor}× (drop={drop_shrink}) using {n_frames_group} frame(s).")
|
|
17414
17600
|
|
|
17415
|
-
|
|
17416
|
-
|
|
17417
|
-
|
|
17418
|
-
|
|
17419
|
-
|
|
17420
|
-
|
|
17421
|
-
|
|
17422
|
-
|
|
17423
|
-
|
|
17424
|
-
|
|
17425
|
-
|
|
17426
|
-
|
|
17427
|
-
|
|
17601
|
+
ok = False
|
|
17602
|
+
try:
|
|
17603
|
+
ok = bool(self.drizzle_stack_one_group(
|
|
17604
|
+
group_key=group_key,
|
|
17605
|
+
file_list=file_list,
|
|
17606
|
+
entries=entries_by_group.get(group_key, []),
|
|
17607
|
+
transforms_dict=transforms_dict,
|
|
17608
|
+
frame_weights=frame_weights,
|
|
17609
|
+
scale_factor=scale_factor,
|
|
17610
|
+
drop_shrink=drop_shrink,
|
|
17611
|
+
rejection_map=rejections_for_group,
|
|
17612
|
+
autocrop_enabled=autocrop_enabled,
|
|
17613
|
+
rect_override=global_rect,
|
|
17614
|
+
status_cb=log
|
|
17615
|
+
))
|
|
17616
|
+
except Exception as e:
|
|
17617
|
+
log(f"⚠️ Drizzle failed for '{group_key}': {e!r}")
|
|
17618
|
+
ok = False
|
|
17619
|
+
|
|
17620
|
+
if ok:
|
|
17621
|
+
group_integration_data[group_key]["drizzled"] = True
|
|
17622
|
+
|
|
17428
17623
|
|
|
17429
17624
|
# Build summary lines
|
|
17430
17625
|
for group_key, info in group_integration_data.items():
|
|
@@ -18019,7 +18214,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18019
18214
|
return
|
|
18020
18215
|
|
|
18021
18216
|
group_key, frames = self._mf_queue.pop(0)
|
|
18022
|
-
out_dir =
|
|
18217
|
+
out_dir = self._master_light_dir()
|
|
18023
18218
|
os.makedirs(out_dir, exist_ok=True)
|
|
18024
18219
|
|
|
18025
18220
|
# Settings snapshot
|
|
@@ -18648,8 +18843,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
18648
18843
|
self,
|
|
18649
18844
|
*,
|
|
18650
18845
|
group_key,
|
|
18651
|
-
file_list, #
|
|
18652
|
-
|
|
18846
|
+
file_list, # still the aligned split files (for headers/metadata)
|
|
18847
|
+
entries, # NEW: list of {orig, aligned, chan}
|
|
18653
18848
|
transforms_dict,
|
|
18654
18849
|
frame_weights,
|
|
18655
18850
|
scale_factor,
|
|
@@ -18659,8 +18854,15 @@ class StackingSuiteDialog(QDialog):
|
|
|
18659
18854
|
rect_override,
|
|
18660
18855
|
status_cb
|
|
18661
18856
|
):
|
|
18662
|
-
|
|
18663
|
-
|
|
18857
|
+
import os
|
|
18858
|
+
|
|
18859
|
+
import cv2
|
|
18860
|
+
from datetime import datetime
|
|
18861
|
+
from astropy.io import fits
|
|
18862
|
+
|
|
18863
|
+
log = status_cb or (lambda *_: None)
|
|
18864
|
+
|
|
18865
|
+
# ---- load SASD v2 transforms (keyed by ORIGINAL path) ----
|
|
18664
18866
|
sasd_path = os.path.join(self.stacking_directory, "alignment_transforms.sasd")
|
|
18665
18867
|
ref_H, ref_W, xforms = self._load_sasd_v2(sasd_path)
|
|
18666
18868
|
if not (ref_H and ref_W):
|
|
@@ -18668,26 +18870,33 @@ class StackingSuiteDialog(QDialog):
|
|
|
18668
18870
|
rs = getattr(self, "ref_shape_for_drizzle", None)
|
|
18669
18871
|
if isinstance(rs, tuple) and len(rs) == 2 and all(int(v) > 0 for v in rs):
|
|
18670
18872
|
ref_H, ref_W = int(rs[0]), int(rs[1])
|
|
18671
|
-
|
|
18873
|
+
log(f"ℹ️ Using in-memory REF_SHAPE fallback: {ref_H}×{ref_W}")
|
|
18672
18874
|
else:
|
|
18673
|
-
|
|
18875
|
+
log("⚠️ Missing REF_SHAPE in SASD; cannot drizzle.")
|
|
18674
18876
|
return
|
|
18675
18877
|
|
|
18676
18878
|
log(f"✅ SASD v2: loaded {len(xforms)} transform(s).")
|
|
18677
|
-
|
|
18879
|
+
|
|
18880
|
+
# ---- sanity: entries must exist ----
|
|
18881
|
+
if not entries:
|
|
18882
|
+
log(f"⚠️ Group '{group_key}' has no drizzle entries – skipping.")
|
|
18883
|
+
return False
|
|
18884
|
+
|
|
18885
|
+
# Debug (first few)
|
|
18678
18886
|
try:
|
|
18679
|
-
sample_need = [os.path.basename(
|
|
18887
|
+
sample_need = [os.path.basename(os.path.normpath(e.get("orig", ""))) for e in entries[:5]]
|
|
18680
18888
|
sample_have = [os.path.basename(p) for p in list(xforms.keys())[:5]]
|
|
18681
18889
|
log(f" originals needed (sample): {sample_need}")
|
|
18682
18890
|
log(f" sasd FILEs (sample): {sample_have}")
|
|
18683
18891
|
except Exception:
|
|
18684
18892
|
pass
|
|
18685
18893
|
|
|
18686
|
-
|
|
18894
|
+
# ---- output canvas ----
|
|
18895
|
+
canvas_H = int(ref_H * scale_factor)
|
|
18896
|
+
canvas_W = int(ref_W * scale_factor)
|
|
18687
18897
|
|
|
18688
|
-
|
|
18689
|
-
|
|
18690
|
-
kernel_name = self.settings.value("stacking/drizzle_kernel", "square", type=str).lower()
|
|
18898
|
+
# ---- kernel config from settings ----
|
|
18899
|
+
kernel_name = (self.settings.value("stacking/drizzle_kernel", "square", type=str) or "square").lower()
|
|
18691
18900
|
gauss_sigma = self.settings.value(
|
|
18692
18901
|
"stacking/drizzle_gauss_sigma", float(drop_shrink) * 0.5, type=float
|
|
18693
18902
|
)
|
|
@@ -18701,80 +18910,78 @@ class StackingSuiteDialog(QDialog):
|
|
|
18701
18910
|
total_rej = sum(len(v) for v in (rejection_map or {}).values())
|
|
18702
18911
|
log(f"🔭 Drizzle stacking for group '{group_key}' with {total_rej} total rejected pixels.")
|
|
18703
18912
|
|
|
18913
|
+
# Need at least 2 frames to be worth it
|
|
18704
18914
|
if len(file_list) < 2:
|
|
18705
18915
|
log(f"⚠️ Group '{group_key}' does not have enough frames to drizzle.")
|
|
18706
|
-
return
|
|
18916
|
+
return False
|
|
18707
18917
|
|
|
18708
|
-
#
|
|
18918
|
+
# ---- establish header + default dims from first aligned file (metadata only) ----
|
|
18709
18919
|
first_file = file_list[0]
|
|
18710
18920
|
first_img, hdr, _, _ = load_image(first_file)
|
|
18711
18921
|
if first_img is None:
|
|
18712
18922
|
log(f"⚠️ Could not load {first_file} to determine drizzle shape!")
|
|
18713
|
-
return
|
|
18923
|
+
return False
|
|
18924
|
+
|
|
18925
|
+
# Force mono if any entry is channel-split (dual-band drizzle output should be mono)
|
|
18926
|
+
force_mono = any((e.get("chan") in ("R", "G", "B")) for e in entries)
|
|
18714
18927
|
|
|
18715
|
-
if
|
|
18928
|
+
if force_mono:
|
|
18716
18929
|
is_mono = True
|
|
18717
|
-
h, w = first_img.shape
|
|
18930
|
+
h, w = first_img.shape[:2]
|
|
18718
18931
|
c = 1
|
|
18719
18932
|
else:
|
|
18720
|
-
|
|
18721
|
-
|
|
18933
|
+
if first_img.ndim == 2:
|
|
18934
|
+
is_mono = True
|
|
18935
|
+
h, w = first_img.shape
|
|
18936
|
+
c = 1
|
|
18937
|
+
else:
|
|
18938
|
+
is_mono = False
|
|
18939
|
+
h, w, c = first_img.shape
|
|
18722
18940
|
|
|
18723
18941
|
# --- choose depositor ONCE (and log it) ---
|
|
18724
|
-
|
|
18725
|
-
|
|
18942
|
+
use_kernelized = not (_kcode == 0 and drop_shrink >= 0.99)
|
|
18943
|
+
|
|
18944
|
+
if not use_kernelized:
|
|
18726
18945
|
deposit_func = drizzle_deposit_numba_naive if is_mono else drizzle_deposit_color_naive
|
|
18727
18946
|
kinf = "naive (square, pixfrac≈1)"
|
|
18728
18947
|
else:
|
|
18729
|
-
# Any other case → kernelized path (square/circular/gaussian)
|
|
18730
18948
|
deposit_func = drizzle_deposit_numba_kernel_mono if is_mono else drizzle_deposit_color_kernel
|
|
18731
18949
|
kinf = ["square", "circular", "gaussian"][_kcode]
|
|
18732
|
-
log(f"Using {kinf} kernel drizzle ({'mono' if is_mono else 'color'}).")
|
|
18733
18950
|
|
|
18734
|
-
|
|
18951
|
+
log(f"Using {kinf} drizzle ({'mono' if is_mono else 'color'}).")
|
|
18952
|
+
|
|
18953
|
+
# ---- allocate buffers ----
|
|
18735
18954
|
out_h = int(canvas_H)
|
|
18736
18955
|
out_w = int(canvas_W)
|
|
18737
18956
|
drizzle_buffer = np.zeros((out_h, out_w) if is_mono else (out_h, out_w, c), dtype=self._dtype())
|
|
18738
18957
|
coverage_buffer = np.zeros_like(drizzle_buffer, dtype=self._dtype())
|
|
18739
18958
|
finalize_func = finalize_drizzle_2d if is_mono else finalize_drizzle_3d
|
|
18740
18959
|
|
|
18741
|
-
|
|
18742
|
-
|
|
18743
|
-
|
|
18744
|
-
|
|
18960
|
+
# ---- main loop over ENTRIES ----
|
|
18961
|
+
# each entry: {"orig": <original path>, "aligned": <aligned path>, "chan": "R"/"G"/None}
|
|
18962
|
+
for i, ent in enumerate(entries):
|
|
18963
|
+
orig_file = os.path.normpath(ent.get("orig", ""))
|
|
18964
|
+
aligned_file = os.path.normpath(ent.get("aligned", "")) # this is what rejections/weights reference
|
|
18965
|
+
chan = ent.get("chan", None)
|
|
18745
18966
|
|
|
18746
|
-
|
|
18747
|
-
|
|
18748
|
-
|
|
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
|
-
|
|
18752
|
-
|
|
18753
|
-
# --- main loop ---
|
|
18754
|
-
# Map original (normalized) → aligned path (for weights & rejection map lookups)
|
|
18755
|
-
orig_to_aligned = {}
|
|
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)
|
|
18967
|
+
if not orig_file or not aligned_file:
|
|
18968
|
+
log(f"⚠️ Bad drizzle entry (missing orig/aligned) – skipping: {ent}")
|
|
18969
|
+
continue
|
|
18764
18970
|
|
|
18765
|
-
|
|
18971
|
+
# Weight: prefer aligned key (that matches your rejection/weights mapping)
|
|
18972
|
+
weight = frame_weights.get(aligned_file, frame_weights.get(orig_file, 1.0))
|
|
18766
18973
|
|
|
18767
|
-
kind, X = xforms.get(
|
|
18768
|
-
log(f"🧭 Drizzle uses {kind or '-'} for {os.path.basename(
|
|
18974
|
+
kind, X = xforms.get(orig_file, (None, None))
|
|
18975
|
+
log(f"🧭 Drizzle uses {kind or '-'} for {os.path.basename(orig_file)} (chan={chan or 'all'})")
|
|
18769
18976
|
if kind is None:
|
|
18770
18977
|
log(f"⚠️ No usable transform for {os.path.basename(orig_file)} – skipping")
|
|
18771
18978
|
continue
|
|
18772
18979
|
|
|
18773
|
-
#
|
|
18980
|
+
# choose pixel source + mapping
|
|
18774
18981
|
pixels_are_registered = False
|
|
18775
18982
|
img_data = None
|
|
18776
18983
|
|
|
18777
|
-
if isinstance(kind, str) and (kind.startswith("poly") or kind in ("tps","thin_plate_spline")):
|
|
18984
|
+
if isinstance(kind, str) and (kind.startswith("poly") or kind in ("tps", "thin_plate_spline")):
|
|
18778
18985
|
# Already warped to reference during registration
|
|
18779
18986
|
pixel_path = aligned_file
|
|
18780
18987
|
if not pixel_path:
|
|
@@ -18784,6 +18991,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18784
18991
|
pixels_are_registered = True
|
|
18785
18992
|
|
|
18786
18993
|
elif kind == "affine" and X is not None:
|
|
18994
|
+
# Use ORIGINAL pixels + affine subpixel mapping
|
|
18787
18995
|
pixel_path = orig_file
|
|
18788
18996
|
H_canvas = np.eye(3, dtype=np.float32)
|
|
18789
18997
|
H_canvas[:2] = np.asarray(X, np.float32).reshape(2, 3)
|
|
@@ -18796,6 +19004,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18796
19004
|
log(f"⚠️ Failed to read {os.path.basename(pixel_path)} – skipping")
|
|
18797
19005
|
continue
|
|
18798
19006
|
H = np.asarray(X, np.float32).reshape(3, 3)
|
|
19007
|
+
|
|
18799
19008
|
if raw_img.ndim == 2:
|
|
18800
19009
|
img_data = cv2.warpPerspective(
|
|
18801
19010
|
raw_img, H, (ref_W, ref_H),
|
|
@@ -18810,6 +19019,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18810
19019
|
borderMode=cv2.BORDER_CONSTANT, borderValue=0
|
|
18811
19020
|
) for ch in range(raw_img.shape[2])
|
|
18812
19021
|
], axis=2)
|
|
19022
|
+
|
|
18813
19023
|
H_canvas = np.eye(3, dtype=np.float32)
|
|
18814
19024
|
pixels_are_registered = True
|
|
18815
19025
|
|
|
@@ -18824,28 +19034,43 @@ class StackingSuiteDialog(QDialog):
|
|
|
18824
19034
|
log(f"⚠️ Failed to read {os.path.basename(pixel_path)} – skipping")
|
|
18825
19035
|
continue
|
|
18826
19036
|
|
|
18827
|
-
#
|
|
18828
|
-
if
|
|
18829
|
-
|
|
19037
|
+
# If this is a split dual-band entry, extract channel from the pixel source
|
|
19038
|
+
if chan in ("R", "G", "B"):
|
|
19039
|
+
if img_data.ndim == 3 and img_data.shape[2] >= 3:
|
|
19040
|
+
ch_idx = {"R": 0, "G": 1, "B": 2}[chan]
|
|
19041
|
+
img_data = img_data[..., ch_idx] # mono plane
|
|
19042
|
+
# if already mono, leave it alone
|
|
19043
|
+
|
|
19044
|
+
# Debug bbox once
|
|
19045
|
+
if i == 0:
|
|
19046
|
+
x0, y0 = 0, 0
|
|
19047
|
+
x1, y1 = ref_W - 1, ref_H - 1
|
|
18830
19048
|
p0 = H_canvas @ np.array([x0, y0, 1], np.float32); p0 /= max(p0[2], 1e-8)
|
|
18831
19049
|
p1 = H_canvas @ np.array([x1, y1, 1], np.float32); p1 /= max(p1[2], 1e-8)
|
|
18832
|
-
log(
|
|
18833
|
-
f"
|
|
19050
|
+
log(
|
|
19051
|
+
f" bbox(ref)→reg: ({p0[0]:.1f},{p0[1]:.1f}) to ({p1[0]:.1f},{p1[1]:.1f}); "
|
|
19052
|
+
f"canvas {int(ref_W * scale_factor)}×{int(ref_H * scale_factor)} @ {scale_factor}×"
|
|
19053
|
+
)
|
|
18834
19054
|
|
|
18835
|
-
#
|
|
19055
|
+
# ---- apply per-file rejections (lookup by ALIGNED file) ----
|
|
18836
19056
|
if rejection_map and aligned_file in rejection_map:
|
|
18837
19057
|
coords_for_this_file = rejection_map.get(aligned_file, [])
|
|
18838
19058
|
if coords_for_this_file:
|
|
18839
19059
|
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()
|
|
19060
|
+
dilate_shape = (self.settings.value("stacking/reject_dilate_shape", "square", type=str) or "square").lower()
|
|
19061
|
+
|
|
18841
19062
|
offsets = [(0, 0)]
|
|
18842
19063
|
if dilate_px > 0:
|
|
18843
19064
|
r = dilate_px
|
|
18844
|
-
offsets = [(dx, dy) for dx in range(-r, r+1) for dy in range(-r, r+1)]
|
|
19065
|
+
offsets = [(dx, dy) for dx in range(-r, r + 1) for dy in range(-r, r + 1)]
|
|
18845
19066
|
if dilate_shape.startswith("dia"):
|
|
18846
|
-
offsets = [(dx, dy) for (dx,dy) in offsets if (abs(dx)+abs(dy) <= r)]
|
|
19067
|
+
offsets = [(dx, dy) for (dx, dy) in offsets if (abs(dx) + abs(dy) <= r)]
|
|
18847
19068
|
|
|
18848
|
-
|
|
19069
|
+
# after optional channel extraction, img_data may be 2D
|
|
19070
|
+
if img_data.ndim == 2:
|
|
19071
|
+
Hraw, Wraw = img_data.shape
|
|
19072
|
+
else:
|
|
19073
|
+
Hraw, Wraw = img_data.shape[0], img_data.shape[1]
|
|
18849
19074
|
|
|
18850
19075
|
if pixels_are_registered:
|
|
18851
19076
|
# Directly zero registered pixels
|
|
@@ -18853,9 +19078,12 @@ class StackingSuiteDialog(QDialog):
|
|
|
18853
19078
|
for (ox, oy) in offsets:
|
|
18854
19079
|
xr, yr = x_r + ox, y_r + oy
|
|
18855
19080
|
if 0 <= xr < Wraw and 0 <= yr < Hraw:
|
|
18856
|
-
img_data
|
|
19081
|
+
if img_data.ndim == 2:
|
|
19082
|
+
img_data[yr, xr] = 0.0
|
|
19083
|
+
else:
|
|
19084
|
+
img_data[yr, xr, :] = 0.0
|
|
18857
19085
|
else:
|
|
18858
|
-
# Back-project via inverse affine
|
|
19086
|
+
# Back-project via inverse affine (H_canvas)
|
|
18859
19087
|
Hinv = np.linalg.inv(H_canvas)
|
|
18860
19088
|
for (x_r, y_r) in coords_for_this_file:
|
|
18861
19089
|
for (ox, oy) in offsets:
|
|
@@ -18864,35 +19092,43 @@ class StackingSuiteDialog(QDialog):
|
|
|
18864
19092
|
x_raw = int(round(v[0] / max(v[2], 1e-8)))
|
|
18865
19093
|
y_raw = int(round(v[1] / max(v[2], 1e-8)))
|
|
18866
19094
|
if 0 <= x_raw < Wraw and 0 <= y_raw < Hraw:
|
|
18867
|
-
img_data
|
|
19095
|
+
if img_data.ndim == 2:
|
|
19096
|
+
img_data[y_raw, x_raw] = 0.0
|
|
19097
|
+
else:
|
|
19098
|
+
img_data[y_raw, x_raw, :] = 0.0
|
|
19099
|
+
|
|
19100
|
+
# ---- deposit ----
|
|
19101
|
+
A23 = H_canvas[:2, :]
|
|
18868
19102
|
|
|
18869
|
-
# --- deposit (identity for registered pixels) ---
|
|
18870
19103
|
if deposit_func is drizzle_deposit_numba_naive:
|
|
18871
19104
|
drizzle_buffer, coverage_buffer = deposit_func(
|
|
18872
|
-
img_data,
|
|
19105
|
+
img_data, A23, drizzle_buffer, coverage_buffer,
|
|
19106
|
+
scale_factor, weight
|
|
18873
19107
|
)
|
|
19108
|
+
|
|
18874
19109
|
elif deposit_func is drizzle_deposit_color_naive:
|
|
18875
19110
|
drizzle_buffer, coverage_buffer = deposit_func(
|
|
18876
|
-
img_data,
|
|
19111
|
+
img_data, A23, drizzle_buffer, coverage_buffer,
|
|
19112
|
+
scale_factor, drop_shrink, weight
|
|
18877
19113
|
)
|
|
19114
|
+
|
|
18878
19115
|
else:
|
|
18879
|
-
|
|
19116
|
+
# kernel mono OR kernel color
|
|
18880
19117
|
drizzle_buffer, coverage_buffer = deposit_func(
|
|
18881
19118
|
img_data, A23, drizzle_buffer, coverage_buffer,
|
|
18882
19119
|
scale_factor, drop_shrink, weight, _kcode, float(gauss_sigma)
|
|
18883
19120
|
)
|
|
18884
19121
|
|
|
18885
|
-
|
|
18886
|
-
# --- finalize, save, optional autocrop ---
|
|
19122
|
+
# ---- finalize ----
|
|
18887
19123
|
final_drizzle = np.zeros_like(drizzle_buffer, dtype=np.float32)
|
|
18888
19124
|
final_drizzle = finalize_func(drizzle_buffer, coverage_buffer, final_drizzle)
|
|
18889
19125
|
|
|
18890
|
-
#
|
|
19126
|
+
# ---- save (single-HDU; no rejection layers here) ----
|
|
18891
19127
|
Hd, Wd = final_drizzle.shape[:2] if final_drizzle.ndim >= 2 else (0, 0)
|
|
18892
19128
|
display_group_driz = self._label_with_dims(group_key, Wd, Hd)
|
|
18893
19129
|
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.
|
|
19130
|
+
base_stem = self._normalize_master_stem(base_stem)
|
|
19131
|
+
out_path_orig = self._build_out(self._master_light_dir(), base_stem, "fit")
|
|
18896
19132
|
|
|
18897
19133
|
hdr_orig = hdr.copy() if hdr is not None else fits.Header()
|
|
18898
19134
|
hdr_orig["IMAGETYP"] = "MASTER STACK - DRIZZLE"
|
|
@@ -18929,7 +19165,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18929
19165
|
)
|
|
18930
19166
|
log(f"✅ Drizzle (original) saved: {out_path_orig}")
|
|
18931
19167
|
|
|
18932
|
-
#
|
|
19168
|
+
# ---- optional auto-crop ----
|
|
18933
19169
|
if autocrop_enabled:
|
|
18934
19170
|
cropped_drizzle, hdr_crop = self._apply_autocrop(
|
|
18935
19171
|
final_drizzle,
|
|
@@ -18939,12 +19175,14 @@ class StackingSuiteDialog(QDialog):
|
|
|
18939
19175
|
rect_override=rect_override
|
|
18940
19176
|
)
|
|
18941
19177
|
hdr_crop["NCOMBINE"] = (n_frames, "Number of frames combined")
|
|
18942
|
-
hdr_crop["NSTACK"] = (n_frames, "Alias of NCOMBINE (SetiAstro)")
|
|
19178
|
+
hdr_crop["NSTACK"] = (n_frames, "Alias of NCOMBINE (SetiAstro)")
|
|
19179
|
+
|
|
18943
19180
|
is_mono_crop = (cropped_drizzle.ndim == 2)
|
|
18944
19181
|
display_group_driz_crop = self._label_with_dims(group_key, cropped_drizzle.shape[1], cropped_drizzle.shape[0])
|
|
18945
19182
|
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.
|
|
19183
|
+
base_crop = self._normalize_master_stem(base_crop)
|
|
19184
|
+
out_path_crop = self._build_out(self._master_light_dir(), base_crop, "fit")
|
|
19185
|
+
|
|
18948
19186
|
cropped_drizzle = self._normalize_stack_01(cropped_drizzle)
|
|
18949
19187
|
save_image(
|
|
18950
19188
|
img_array=cropped_drizzle,
|
|
@@ -18958,6 +19196,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18958
19196
|
self._autocrop_outputs = []
|
|
18959
19197
|
self._autocrop_outputs.append((group_key, out_path_crop))
|
|
18960
19198
|
log(f"✂️ Drizzle (auto-cropped) saved: {out_path_crop}")
|
|
19199
|
+
return True
|
|
18961
19200
|
|
|
18962
19201
|
def _load_sasd_v2(self, path: str):
|
|
18963
19202
|
"""
|