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.

Files changed (47) hide show
  1. setiastro/saspro/__init__.py +15 -4
  2. setiastro/saspro/__main__.py +23 -5
  3. setiastro/saspro/_generated/build_info.py +2 -2
  4. setiastro/saspro/abe.py +4 -4
  5. setiastro/saspro/autostretch.py +29 -18
  6. setiastro/saspro/gui/main_window.py +5 -5
  7. setiastro/saspro/gui/mixins/toolbar_mixin.py +2 -2
  8. setiastro/saspro/legacy/numba_utils.py +301 -119
  9. setiastro/saspro/numba_utils.py +998 -270
  10. setiastro/saspro/ops/settings.py +6 -6
  11. setiastro/saspro/pixelmath.py +1 -1
  12. setiastro/saspro/planetprojection.py +310 -105
  13. setiastro/saspro/sfcc.py +14 -8
  14. setiastro/saspro/stacking_suite.py +292 -111
  15. setiastro/saspro/subwindow.py +28 -35
  16. setiastro/saspro/translations/all_source_strings.json +2 -2
  17. setiastro/saspro/translations/ar_translations.py +3 -3
  18. setiastro/saspro/translations/de_translations.py +2 -2
  19. setiastro/saspro/translations/es_translations.py +2 -2
  20. setiastro/saspro/translations/fr_translations.py +2 -2
  21. setiastro/saspro/translations/hi_translations.py +2 -2
  22. setiastro/saspro/translations/it_translations.py +2 -2
  23. setiastro/saspro/translations/ja_translations.py +2 -2
  24. setiastro/saspro/translations/pt_translations.py +2 -2
  25. setiastro/saspro/translations/ru_translations.py +2 -2
  26. setiastro/saspro/translations/saspro_ar.ts +2 -2
  27. setiastro/saspro/translations/saspro_de.ts +4 -4
  28. setiastro/saspro/translations/saspro_es.ts +2 -2
  29. setiastro/saspro/translations/saspro_fr.ts +2 -2
  30. setiastro/saspro/translations/saspro_hi.ts +2 -2
  31. setiastro/saspro/translations/saspro_it.ts +4 -4
  32. setiastro/saspro/translations/saspro_ja.ts +2 -2
  33. setiastro/saspro/translations/saspro_pt.ts +2 -2
  34. setiastro/saspro/translations/saspro_ru.ts +2 -2
  35. setiastro/saspro/translations/saspro_sw.ts +2 -2
  36. setiastro/saspro/translations/saspro_uk.ts +2 -2
  37. setiastro/saspro/translations/saspro_zh.ts +2 -2
  38. setiastro/saspro/translations/sw_translations.py +2 -2
  39. setiastro/saspro/translations/uk_translations.py +2 -2
  40. setiastro/saspro/translations/zh_translations.py +2 -2
  41. setiastro/saspro/window_shelf.py +62 -1
  42. {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/METADATA +1 -1
  43. {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/RECORD +47 -47
  44. {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/entry_points.txt +1 -1
  45. {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/WHEEL +0 -0
  46. {setiastrosuitepro-1.7.3.dist-info → setiastrosuitepro-1.7.4.dist-info}/licenses/LICENSE +0 -0
  47. {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.settings.value("stacking/drizzle_scale", "2x", type=str))
8850
- self.drizzle_drop_shrink_spin.setValue(self.settings.value("stacking/drizzle_drop", 0.65, type=float))
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
- # Persist drizzle params whenever changed
8967
- self.settings.setValue("stacking/drizzle_scale", self.drizzle_scale_combo.currentText())
8968
- self.settings.setValue("stacking/drizzle_drop", float(self.drizzle_drop_shrink_spin.value()))
8969
- # If you reflect params to tree rows, update here:
8970
- # self._refresh_reg_tree_drizzle_column()
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.stacking_directory, default_name),
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
- Uses the same classifier and grouping behavior as _split_dual_band_osc,
15883
- but operates on the *aligned* frames.
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 layout == "MONO" or (layout == "CHW" and arr.shape[0] == 1) or (layout == "HWC" and arr.shape[-1] == 1):
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
- # UNKNOWN mono: ignore for NB split
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
- # layout == "CHW" → (C, H, W)
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, R, hdr, "Ha", src_filter=filt)
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, R, hdr, "SII", src_filter=filt)
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
- sii_files.append(sii_path); parent_of[sii_path] = group
15996
- oiii_files.append(oiii_path); parent_of[oiii_path] = group
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, they won't show up in NB groups
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 = os.path.join(self.stacking_directory, "Masters")
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.stacking_directory, base, "fit")
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.stacking_directory, base_crop, "fit")
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.stacking_directory,
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.stacking_directory,
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.stacking_directory,
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.stacking_directory,
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
- original_list=originals_by_group.get(group_key, []),
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 = os.path.join(self.stacking_directory, "Masters")
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, # registered _n_r.fit (keep for headers/metadata)
18652
- original_list, # NEW: originals (normalized), used as pixel sources
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
- # Load per-frame transforms (SASD v2)
18663
- log = status_cb or (lambda *_: None)
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
- status_cb(f"ℹ️ Using in-memory REF_SHAPE fallback: {ref_H}×{ref_W}")
18816
+ log(f"ℹ️ Using in-memory REF_SHAPE fallback: {ref_H}×{ref_W}")
18672
18817
  else:
18673
- status_cb("⚠️ Missing REF_SHAPE in SASD; cannot drizzle.")
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
- # Debug (first few):
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(p) for p in original_list[:5]]
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
- canvas_H, canvas_W = int(ref_H * scale_factor), int(ref_W * scale_factor)
18687
-
18837
+ # ---- output canvas ----
18838
+ canvas_H = int(ref_H * scale_factor)
18839
+ canvas_W = int(ref_W * scale_factor)
18688
18840
 
18689
- # --- kernel config from settings ---
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
- # --- establish geometry + is_mono before choosing depositor ---
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 first_img.ndim == 2:
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
- is_mono = False
18721
- h, w, c = first_img.shape
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
- if _kcode == 0 and drop_shrink >= 0.99:
18725
- # square + pixfrac≈1 → naive “one-to-one” deposit
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
- # --- allocate buffers ---
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
- def _invert_2x3(A23: np.ndarray) -> np.ndarray:
18742
- A23 = np.asarray(A23, np.float32).reshape(2, 3)
18743
- A33 = np.eye(3, dtype=np.float32); A33[:2] = A23
18744
- return np.linalg.inv(A33) # 3x3
18745
-
18746
- def _apply_H_point(H: np.ndarray, x: float, y: float) -> tuple[int, int]:
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
- # --- 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)
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
- weight = frame_weights.get(aligned_file, frame_weights.get(orig_key, 1.0))
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(orig_key, (None, None))
18768
- log(f"🧭 Drizzle uses {kind or '-'} for {os.path.basename(orig_key)}")
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
- # --- choose pixel source + mapping ---
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
- # --- debug bbox once ---
18828
- if orig_file is original_list[0]:
18829
- x0, y0 = 0, 0; x1, y1 = ref_W-1, ref_H-1
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(f" bbox(ref)→reg: ({p0[0]:.1f},{p0[1]:.1f}) to ({p1[0]:.1f},{p1[1]:.1f}); "
18833
- f"canvas {int(ref_W*scale_factor)}×{int(ref_H*scale_factor)} @ {scale_factor}×")
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
- # --- apply per-file rejections ---
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
- Hraw, Wraw = img_data.shape[0], img_data.shape[1]
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[yr, xr] = 0.0
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[y_raw, x_raw] = 0.0
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, H_canvas[:2], drizzle_buffer, coverage_buffer, scale_factor, weight
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, H_canvas[:2], drizzle_buffer, coverage_buffer, scale_factor, drop_shrink, weight
19054
+ img_data, A23, drizzle_buffer, coverage_buffer,
19055
+ scale_factor, drop_shrink, weight
18877
19056
  )
19057
+
18878
19058
  else:
18879
- A23 = H_canvas[:2, :]
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
- # Save original drizzle (single-HDU; no rejection layers here)
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.stacking_directory, base_stem, "fit")
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
- # Optional auto-crop (respects global rect if provided)
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.stacking_directory, base_crop, "fit")
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,