setiastrosuitepro 1.6.2__py3-none-any.whl → 1.6.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.
- setiastro/images/rotatearbitrary.png +0 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/backgroundneutral.py +10 -1
- setiastro/saspro/blink_comparator_pro.py +474 -251
- setiastro/saspro/crop_dialog_pro.py +11 -1
- setiastro/saspro/doc_manager.py +1 -1
- setiastro/saspro/function_bundle.py +16 -16
- setiastro/saspro/gui/main_window.py +93 -64
- setiastro/saspro/gui/mixins/dock_mixin.py +31 -18
- setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
- setiastro/saspro/multiscale_decomp.py +710 -256
- setiastro/saspro/remove_stars_preset.py +55 -13
- setiastro/saspro/resources.py +30 -11
- setiastro/saspro/selective_color.py +79 -20
- setiastro/saspro/shortcuts.py +94 -21
- setiastro/saspro/stacking_suite.py +296 -107
- setiastro/saspro/star_alignment.py +275 -330
- setiastro/saspro/status_log_dock.py +1 -1
- setiastro/saspro/swap_manager.py +77 -42
- setiastro/saspro/translations/all_source_strings.json +1588 -516
- setiastro/saspro/translations/ar_translations.py +915 -684
- setiastro/saspro/translations/de_translations.py +442 -463
- setiastro/saspro/translations/es_translations.py +277 -47
- setiastro/saspro/translations/fr_translations.py +279 -47
- setiastro/saspro/translations/hi_translations.py +253 -21
- setiastro/saspro/translations/integrate_translations.py +3 -2
- setiastro/saspro/translations/it_translations.py +1211 -161
- setiastro/saspro/translations/ja_translations.py +3340 -3107
- setiastro/saspro/translations/pt_translations.py +3315 -3337
- setiastro/saspro/translations/ru_translations.py +351 -117
- setiastro/saspro/translations/saspro_ar.qm +0 -0
- setiastro/saspro/translations/saspro_ar.ts +15902 -138
- setiastro/saspro/translations/saspro_de.qm +0 -0
- setiastro/saspro/translations/saspro_de.ts +14428 -133
- setiastro/saspro/translations/saspro_es.qm +0 -0
- setiastro/saspro/translations/saspro_es.ts +11503 -7821
- setiastro/saspro/translations/saspro_fr.qm +0 -0
- setiastro/saspro/translations/saspro_fr.ts +11168 -7812
- setiastro/saspro/translations/saspro_hi.qm +0 -0
- setiastro/saspro/translations/saspro_hi.ts +14733 -135
- setiastro/saspro/translations/saspro_it.qm +0 -0
- setiastro/saspro/translations/saspro_it.ts +14347 -7821
- setiastro/saspro/translations/saspro_ja.qm +0 -0
- setiastro/saspro/translations/saspro_ja.ts +14860 -137
- setiastro/saspro/translations/saspro_pt.qm +0 -0
- setiastro/saspro/translations/saspro_pt.ts +14904 -137
- setiastro/saspro/translations/saspro_ru.qm +0 -0
- setiastro/saspro/translations/saspro_ru.ts +11766 -168
- setiastro/saspro/translations/saspro_sw.qm +0 -0
- setiastro/saspro/translations/saspro_sw.ts +15115 -135
- setiastro/saspro/translations/saspro_uk.qm +0 -0
- setiastro/saspro/translations/saspro_uk.ts +11206 -6729
- setiastro/saspro/translations/saspro_zh.qm +0 -0
- setiastro/saspro/translations/saspro_zh.ts +10581 -7812
- setiastro/saspro/translations/sw_translations.py +282 -56
- setiastro/saspro/translations/uk_translations.py +264 -35
- setiastro/saspro/translations/zh_translations.py +282 -47
- setiastro/saspro/view_bundle.py +17 -17
- setiastro/saspro/widgets/minigame/game.js +11 -6
- setiastro/saspro/widgets/resource_monitor.py +26 -0
- setiastro/saspro/widgets/spinboxes.py +18 -0
- setiastro/saspro/wimi.py +65 -65
- setiastro/saspro/wims.py +33 -33
- setiastro/saspro/window_shelf.py +2 -2
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +7 -7
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +72 -71
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
|
@@ -154,64 +154,107 @@ _FITS_EXTS = ('.fits', '.fit', '.fts', '.fits.gz', '.fit.gz', '.fts.gz', '.fz')
|
|
|
154
154
|
|
|
155
155
|
def get_valid_header(path: str):
|
|
156
156
|
"""
|
|
157
|
-
|
|
157
|
+
Fast header-only FITS peek with a targeted fallback:
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
159
|
+
1) Header-only scan (lazy_load_hdus=True, never touches .data)
|
|
160
|
+
2) If NAXIS1/2 still missing/invalid, fallback to reading ONE image HDU's data
|
|
161
|
+
to get shape, then patch NAXIS/NAXIS1/NAXIS2.
|
|
162
|
+
|
|
163
|
+
Returns: (hdr, ok_bool)
|
|
162
164
|
"""
|
|
163
165
|
try:
|
|
164
166
|
from astropy.io import fits
|
|
165
167
|
|
|
166
|
-
|
|
168
|
+
def _is_good_dim(v):
|
|
169
|
+
try:
|
|
170
|
+
return int(v) > 0
|
|
171
|
+
except Exception:
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
# ---------------------------
|
|
175
|
+
# Pass 1: header-only
|
|
176
|
+
# ---------------------------
|
|
177
|
+
with fits.open(path, mode="readonly", memmap=True, lazy_load_hdus=True) as hdul:
|
|
167
178
|
science_hdu = None
|
|
168
179
|
|
|
169
|
-
# Prefer the first HDU that actually has 2D+ image data
|
|
170
180
|
for hdu in hdul:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
181
|
+
hdr = hdu.header
|
|
182
|
+
|
|
183
|
+
# Prefer HDUs that *declare* 2D+ via header
|
|
184
|
+
naxis = hdr.get("NAXIS", None)
|
|
185
|
+
znaxis = hdr.get("ZNAXIS", None)
|
|
186
|
+
|
|
187
|
+
looks_2d = False
|
|
188
|
+
try:
|
|
189
|
+
if naxis is not None and int(naxis) >= 2:
|
|
190
|
+
looks_2d = True
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
try:
|
|
194
|
+
if znaxis is not None and int(znaxis) >= 2:
|
|
195
|
+
looks_2d = True
|
|
196
|
+
except Exception:
|
|
197
|
+
pass
|
|
198
|
+
|
|
199
|
+
if looks_2d:
|
|
175
200
|
science_hdu = hdu
|
|
176
201
|
break
|
|
177
202
|
|
|
178
203
|
if science_hdu is None:
|
|
179
|
-
# Fallback: just use primary
|
|
180
204
|
science_hdu = hdul[0]
|
|
181
205
|
|
|
182
206
|
hdr = science_hdu.header.copy()
|
|
183
|
-
data = science_hdu.data
|
|
184
207
|
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
ny, nx = shape[-2], shape[-1]
|
|
191
|
-
hdr["NAXIS"] = int(data.ndim)
|
|
192
|
-
hdr["NAXIS1"] = int(nx)
|
|
193
|
-
hdr["NAXIS2"] = int(ny)
|
|
194
|
-
except Exception:
|
|
195
|
-
pass
|
|
208
|
+
# Prefer normal NAXISn; fallback to ZNAXISn for tile-compressed
|
|
209
|
+
if not _is_good_dim(hdr.get("NAXIS1")) and _is_good_dim(hdr.get("ZNAXIS1")):
|
|
210
|
+
hdr["NAXIS1"] = int(hdr["ZNAXIS1"])
|
|
211
|
+
if not _is_good_dim(hdr.get("NAXIS2")) and _is_good_dim(hdr.get("ZNAXIS2")):
|
|
212
|
+
hdr["NAXIS2"] = int(hdr["ZNAXIS2"])
|
|
196
213
|
|
|
197
|
-
#
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
zkey = f"ZNAXIS{ax}"
|
|
201
|
-
val = hdr.get(key, None)
|
|
202
|
-
if (val is None or (isinstance(val, str) and not val.strip())) and zkey in hdr:
|
|
203
|
-
try:
|
|
204
|
-
hdr[key] = int(hdr[zkey])
|
|
205
|
-
except Exception:
|
|
206
|
-
pass
|
|
214
|
+
# If we already have good dims, we are done (FAST PATH)
|
|
215
|
+
if _is_good_dim(hdr.get("NAXIS1")) and _is_good_dim(hdr.get("NAXIS2")):
|
|
216
|
+
return hdr, True
|
|
207
217
|
|
|
208
|
-
|
|
218
|
+
# ---------------------------
|
|
219
|
+
# Pass 2: slow fallback (ONLY if needed)
|
|
220
|
+
# ---------------------------
|
|
221
|
+
# Re-open without lazy semantics and read ONE image-like HDU's data to infer shape.
|
|
222
|
+
with fits.open(path, mode="readonly", memmap=False) as hdul:
|
|
223
|
+
target_hdu = None
|
|
224
|
+
for hdu in hdul:
|
|
225
|
+
# data access is expensive; try to choose wisely by header first
|
|
226
|
+
naxis = hdu.header.get("NAXIS", 0)
|
|
227
|
+
znaxis = hdu.header.get("ZNAXIS", 0)
|
|
209
228
|
|
|
210
|
-
|
|
211
|
-
|
|
229
|
+
try:
|
|
230
|
+
if int(naxis) >= 2 or int(znaxis) >= 2:
|
|
231
|
+
target_hdu = hdu
|
|
232
|
+
break
|
|
233
|
+
except Exception:
|
|
234
|
+
continue
|
|
212
235
|
|
|
236
|
+
if target_hdu is None:
|
|
237
|
+
target_hdu = hdul[0]
|
|
213
238
|
|
|
239
|
+
# Now (and only now) touch data
|
|
240
|
+
data = getattr(target_hdu, "data", None)
|
|
241
|
+
|
|
242
|
+
hdr2 = target_hdu.header.copy()
|
|
243
|
+
if data is not None and getattr(data, "ndim", 0) >= 2:
|
|
244
|
+
try:
|
|
245
|
+
ny, nx = data.shape[-2], data.shape[-1]
|
|
246
|
+
hdr2["NAXIS"] = int(getattr(data, "ndim", hdr2.get("NAXIS", 2)))
|
|
247
|
+
hdr2["NAXIS1"] = int(nx)
|
|
248
|
+
hdr2["NAXIS2"] = int(ny)
|
|
249
|
+
return hdr2, True
|
|
250
|
+
except Exception:
|
|
251
|
+
pass
|
|
214
252
|
|
|
253
|
+
# If still unknown, return header anyway (caller can show "Unknown")
|
|
254
|
+
return hdr2, True
|
|
255
|
+
|
|
256
|
+
except Exception:
|
|
257
|
+
return None, False
|
|
215
258
|
|
|
216
259
|
def _read_tile_stack(file_list, y0, y1, x0, x1, channels, out_buf):
|
|
217
260
|
"""
|
|
@@ -3916,7 +3959,11 @@ class StackingSuiteDialog(QDialog):
|
|
|
3916
3959
|
self._wrench_path = wrench_path
|
|
3917
3960
|
self._spinner_path = spinner_path
|
|
3918
3961
|
self._post_progress_label = None
|
|
3919
|
-
|
|
3962
|
+
self._dark_group_item = {} # key -> QTreeWidgetItem
|
|
3963
|
+
self._flat_filter_item = {} # filter_name -> QTreeWidgetItem
|
|
3964
|
+
self._flat_exp_item = {} # (filter_name, want_label) -> QTreeWidgetItem
|
|
3965
|
+
self._light_filter_item = {} # filter_name -> QTreeWidgetItem
|
|
3966
|
+
self._light_exp_item = {} # (filter_name, want_label) -> QTreeWidgetItem
|
|
3920
3967
|
|
|
3921
3968
|
self.setWindowTitle(self.tr("Stacking Suite"))
|
|
3922
3969
|
self.setGeometry(300, 200, 800, 600)
|
|
@@ -5123,8 +5170,9 @@ class StackingSuiteDialog(QDialog):
|
|
|
5123
5170
|
disto_form.addRow(self.tr("Max control points:"), self.align_max_cp)
|
|
5124
5171
|
|
|
5125
5172
|
self.align_downsample = QSpinBox()
|
|
5126
|
-
self.align_downsample.setRange(1,
|
|
5127
|
-
self.align_downsample.setValue(self.settings.value("stacking/align/downsample",
|
|
5173
|
+
self.align_downsample.setRange(1, 64) # or 1..32; 64 if you want “any integer”
|
|
5174
|
+
self.align_downsample.setValue(self.settings.value("stacking/align/downsample", 3, type=int))
|
|
5175
|
+
self.align_downsample.setToolTip(self.tr("Alignment solve downsample. 1 = full res; higher = faster but less accurate."))
|
|
5128
5176
|
disto_form.addRow(self.tr("Solve downsample:"), self.align_downsample)
|
|
5129
5177
|
|
|
5130
5178
|
# Homography / Similarity-specific RANSAC reprojection threshold
|
|
@@ -6012,12 +6060,19 @@ class StackingSuiteDialog(QDialog):
|
|
|
6012
6060
|
w.blockSignals(True); w.setValue(v); w.blockSignals(False)
|
|
6013
6061
|
|
|
6014
6062
|
def _get_drizzle_scale(self) -> float:
|
|
6015
|
-
|
|
6016
|
-
|
|
6017
|
-
|
|
6018
|
-
|
|
6019
|
-
|
|
6020
|
-
|
|
6063
|
+
val = self.settings.value("stacking/drizzle_scale", "2x")
|
|
6064
|
+
if isinstance(val, (int, float)):
|
|
6065
|
+
return float(val)
|
|
6066
|
+
if isinstance(val, str):
|
|
6067
|
+
s = val.strip().lower()
|
|
6068
|
+
if s.endswith("x"):
|
|
6069
|
+
s = s[:-1]
|
|
6070
|
+
try:
|
|
6071
|
+
return float(s)
|
|
6072
|
+
except Exception:
|
|
6073
|
+
return 2.0
|
|
6074
|
+
return 2.0
|
|
6075
|
+
|
|
6021
6076
|
|
|
6022
6077
|
def _set_drizzle_scale(self, r: float | str) -> None:
|
|
6023
6078
|
if isinstance(r, str):
|
|
@@ -6033,6 +6088,25 @@ class StackingSuiteDialog(QDialog):
|
|
|
6033
6088
|
self.drizzle_scale_combo.setCurrentText(txt)
|
|
6034
6089
|
self.drizzle_scale_combo.blockSignals(False)
|
|
6035
6090
|
|
|
6091
|
+
def _get_drizzle_enabled(self) -> bool:
|
|
6092
|
+
# UI checkbox wins if it exists (most “live” truth)
|
|
6093
|
+
cb = getattr(self, "drizzle_checkbox", None)
|
|
6094
|
+
if cb is not None:
|
|
6095
|
+
try:
|
|
6096
|
+
return bool(cb.isChecked())
|
|
6097
|
+
except Exception:
|
|
6098
|
+
pass
|
|
6099
|
+
# fallback to settings (headless / older flows)
|
|
6100
|
+
return bool(self.settings.value("stacking/drizzle_enabled", False, type=bool))
|
|
6101
|
+
|
|
6102
|
+
def _set_drizzle_enabled(self, on: bool) -> None:
|
|
6103
|
+
on = bool(on)
|
|
6104
|
+
self.settings.setValue("stacking/drizzle_enabled", on)
|
|
6105
|
+
cb = getattr(self, "drizzle_checkbox", None)
|
|
6106
|
+
if cb is not None and cb.isChecked() != on:
|
|
6107
|
+
cb.blockSignals(True)
|
|
6108
|
+
cb.setChecked(on)
|
|
6109
|
+
cb.blockSignals(False)
|
|
6036
6110
|
|
|
6037
6111
|
def closeEvent(self, e):
|
|
6038
6112
|
# Graceful shutdown for any running workers
|
|
@@ -8873,9 +8947,13 @@ class StackingSuiteDialog(QDialog):
|
|
|
8873
8947
|
# ensure attrs exist
|
|
8874
8948
|
if not hasattr(self, "_reg_excluded_files"):
|
|
8875
8949
|
self._reg_excluded_files = set()
|
|
8876
|
-
if not hasattr(self, "deleted_calibrated_files"):
|
|
8877
|
-
self.deleted_calibrated_files = []
|
|
8878
8950
|
|
|
8951
|
+
# Track "removed from Registration tab" for this session so stacking won't use them
|
|
8952
|
+
if (not hasattr(self, "deleted_calibrated_files")) or (self.deleted_calibrated_files is None):
|
|
8953
|
+
self.deleted_calibrated_files = set()
|
|
8954
|
+
elif isinstance(self.deleted_calibrated_files, list):
|
|
8955
|
+
# backward compat if you previously used list
|
|
8956
|
+
self.deleted_calibrated_files = set(self.deleted_calibrated_files)
|
|
8879
8957
|
removed_paths = []
|
|
8880
8958
|
|
|
8881
8959
|
for item in selected_items:
|
|
@@ -8931,18 +9009,23 @@ class StackingSuiteDialog(QDialog):
|
|
|
8931
9009
|
# If you want a separate "Exclude" feature later, keep _reg_excluded_files for that.
|
|
8932
9010
|
# For now, removing should be reversible via "Add Light Files".
|
|
8933
9011
|
|
|
9012
|
+
# Persist "removed from registration" list (session)
|
|
9013
|
+
dead = {os.path.normcase(os.path.abspath(p)) for p in removed_paths if isinstance(p, str)}
|
|
9014
|
+
if dead:
|
|
9015
|
+
self.deleted_calibrated_files |= dead
|
|
9016
|
+
|
|
8934
9017
|
# Also prune manual list so it doesn't re-inject removed files *in this session*
|
|
8935
9018
|
if hasattr(self, "manual_light_files") and self.manual_light_files:
|
|
8936
|
-
dead = {os.path.normcase(os.path.abspath(p)) for p in removed_paths if isinstance(p, str)}
|
|
8937
9019
|
self.manual_light_files = [
|
|
8938
9020
|
p for p in self.manual_light_files
|
|
8939
9021
|
if os.path.normcase(os.path.abspath(p)) not in dead
|
|
8940
9022
|
]
|
|
8941
9023
|
|
|
8942
9024
|
# refresh UI
|
|
8943
|
-
|
|
9025
|
+
# IMPORTANT: do NOT call populate_calibrated_lights() here, it can resurrect removed items
|
|
8944
9026
|
self._refresh_reg_tree_summaries()
|
|
8945
9027
|
|
|
9028
|
+
|
|
8946
9029
|
def rebuild_flat_tree(self):
|
|
8947
9030
|
"""Regroup flat frames in the flat_tree based on the exposure tolerance."""
|
|
8948
9031
|
self.flat_tree.clear()
|
|
@@ -9287,6 +9370,22 @@ class StackingSuiteDialog(QDialog):
|
|
|
9287
9370
|
return (f"Drizzle: True, Scale: {scale:g}x, Drop: {drop:.2f}"
|
|
9288
9371
|
if enabled else "Drizzle: False")
|
|
9289
9372
|
|
|
9373
|
+
def _get_group_key(self, top_item) -> str:
|
|
9374
|
+
"""Stable key for a group item; survives UI text decoration."""
|
|
9375
|
+
key = top_item.data(0, Qt.ItemDataRole.UserRole)
|
|
9376
|
+
if key:
|
|
9377
|
+
return str(key)
|
|
9378
|
+
# fallback to visible text if older items don't have it yet
|
|
9379
|
+
return str(top_item.text(0)).strip()
|
|
9380
|
+
|
|
9381
|
+
def _ensure_group_key(self, top_item, group_key: str | None = None) -> str:
|
|
9382
|
+
"""Set canonical key on item if missing."""
|
|
9383
|
+
if group_key is None:
|
|
9384
|
+
group_key = str(top_item.text(0)).strip()
|
|
9385
|
+
if not top_item.data(0, Qt.ItemDataRole.UserRole):
|
|
9386
|
+
top_item.setData(0, Qt.ItemDataRole.UserRole, group_key)
|
|
9387
|
+
return str(group_key)
|
|
9388
|
+
|
|
9290
9389
|
def _set_drizzle_on_items(self, items, enabled: bool, scale: float, drop: float):
|
|
9291
9390
|
txt_on = self._format_drizzle_text(True, scale, drop)
|
|
9292
9391
|
txt_off = self._format_drizzle_text(False, scale, drop)
|
|
@@ -9294,7 +9393,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
9294
9393
|
# dedupe child selection → parent group
|
|
9295
9394
|
if it.parent() is not None:
|
|
9296
9395
|
it = it.parent()
|
|
9297
|
-
|
|
9396
|
+
# Canonical key stored on the item (NOT display label)
|
|
9397
|
+
group_key = self._ensure_group_key(it)
|
|
9298
9398
|
it.setText(2, txt_on if enabled else txt_off)
|
|
9299
9399
|
self.per_group_drizzle[group_key] = {
|
|
9300
9400
|
"enabled": bool(enabled),
|
|
@@ -9319,11 +9419,10 @@ class StackingSuiteDialog(QDialog):
|
|
|
9319
9419
|
return
|
|
9320
9420
|
|
|
9321
9421
|
for item in selected_items:
|
|
9322
|
-
# If the user selected a child row, go up to its parent group
|
|
9323
9422
|
if item.parent() is not None:
|
|
9324
9423
|
item = item.parent()
|
|
9325
9424
|
|
|
9326
|
-
group_key =
|
|
9425
|
+
group_key = self._ensure_group_key(item) # ✅ stable key
|
|
9327
9426
|
|
|
9328
9427
|
if drizzle_enabled:
|
|
9329
9428
|
# Show scale + drop shrink
|
|
@@ -9355,7 +9454,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
9355
9454
|
seen, targets = set(), []
|
|
9356
9455
|
for it in sel:
|
|
9357
9456
|
top = it if it.parent() is None else it.parent()
|
|
9358
|
-
key =
|
|
9457
|
+
key = self._ensure_group_key(top)
|
|
9359
9458
|
if key not in seen:
|
|
9360
9459
|
seen.add(key); targets.append(top)
|
|
9361
9460
|
else:
|
|
@@ -9378,7 +9477,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
9378
9477
|
|
|
9379
9478
|
out = {}
|
|
9380
9479
|
for top in self._iter_group_items():
|
|
9381
|
-
group_key =
|
|
9480
|
+
group_key = self._ensure_group_key(top) # ✅ stable key
|
|
9382
9481
|
state = self.per_group_drizzle.get(group_key)
|
|
9383
9482
|
if not state:
|
|
9384
9483
|
state = {"enabled": global_enabled, "scale": global_scale, "drop": global_drop}
|
|
@@ -10000,24 +10099,32 @@ class StackingSuiteDialog(QDialog):
|
|
|
10000
10099
|
manual_session_name = self._resolve_manual_session_name_for_ingest()
|
|
10001
10100
|
|
|
10002
10101
|
added = 0
|
|
10003
|
-
|
|
10004
|
-
|
|
10005
|
-
|
|
10006
|
-
|
|
10007
|
-
|
|
10008
|
-
|
|
10009
|
-
|
|
10102
|
+
tree.setUpdatesEnabled(False)
|
|
10103
|
+
tree.blockSignals(True)
|
|
10104
|
+
try:
|
|
10105
|
+
for i, path in enumerate(paths, start=1):
|
|
10106
|
+
if dlg.wasCanceled():
|
|
10107
|
+
break
|
|
10108
|
+
try:
|
|
10109
|
+
base = os.path.basename(path)
|
|
10110
|
+
dlg.setLabelText(f"{base} ({i}/{total})")
|
|
10111
|
+
QCoreApplication.processEvents()
|
|
10010
10112
|
|
|
10011
|
-
|
|
10012
|
-
|
|
10013
|
-
|
|
10014
|
-
|
|
10015
|
-
|
|
10016
|
-
|
|
10017
|
-
|
|
10113
|
+
self.process_fits_header(
|
|
10114
|
+
path, tree, expected_type,
|
|
10115
|
+
manual_session_name=manual_session_name
|
|
10116
|
+
)
|
|
10117
|
+
added += 1
|
|
10118
|
+
except Exception:
|
|
10119
|
+
pass
|
|
10120
|
+
|
|
10121
|
+
dlg.setValue(i)
|
|
10122
|
+
QCoreApplication.processEvents()
|
|
10123
|
+
finally:
|
|
10124
|
+
tree.blockSignals(False)
|
|
10125
|
+
tree.setUpdatesEnabled(True)
|
|
10126
|
+
tree.viewport().update()
|
|
10018
10127
|
|
|
10019
|
-
dlg.setValue(i)
|
|
10020
|
-
QCoreApplication.processEvents()
|
|
10021
10128
|
|
|
10022
10129
|
dlg.setValue(total)
|
|
10023
10130
|
QCoreApplication.processEvents()
|
|
@@ -10220,16 +10327,16 @@ class StackingSuiteDialog(QDialog):
|
|
|
10220
10327
|
if expected_type_u == "DARK":
|
|
10221
10328
|
key = f"{exposure_text} ({image_size})"
|
|
10222
10329
|
self.dark_files.setdefault(key, []).append(path)
|
|
10223
|
-
self.session_tags[path] = session_tag # not strictly needed, but consistent
|
|
10224
10330
|
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10331
|
+
exposure_item = self._dark_group_item.get(key)
|
|
10332
|
+
if exposure_item is None:
|
|
10333
|
+
exposure_item = QTreeWidgetItem([key])
|
|
10228
10334
|
tree.addTopLevelItem(exposure_item)
|
|
10335
|
+
self._dark_group_item[key] = exposure_item
|
|
10229
10336
|
|
|
10230
10337
|
leaf = QTreeWidgetItem([os.path.basename(path), meta_text])
|
|
10231
10338
|
leaf.setData(0, Qt.ItemDataRole.UserRole, path)
|
|
10232
|
-
leaf.setData(0, Qt.ItemDataRole.UserRole + 1, session_tag)
|
|
10339
|
+
leaf.setData(0, Qt.ItemDataRole.UserRole + 1, session_tag)
|
|
10233
10340
|
exposure_item.addChild(leaf)
|
|
10234
10341
|
|
|
10235
10342
|
# === FLATs ===
|
|
@@ -10239,20 +10346,20 @@ class StackingSuiteDialog(QDialog):
|
|
|
10239
10346
|
self.flat_files.setdefault(composite_key, []).append(path)
|
|
10240
10347
|
self.session_tags[path] = session_tag
|
|
10241
10348
|
|
|
10242
|
-
|
|
10243
|
-
|
|
10244
|
-
|
|
10349
|
+
filter_item = self._flat_filter_item.get(filter_name)
|
|
10350
|
+
if filter_item is None:
|
|
10351
|
+
filter_item = QTreeWidgetItem([filter_name])
|
|
10245
10352
|
tree.addTopLevelItem(filter_item)
|
|
10353
|
+
self._flat_filter_item[filter_name] = filter_item
|
|
10246
10354
|
|
|
10247
10355
|
want_label = f"{exposure_text} ({image_size})"
|
|
10248
|
-
|
|
10249
|
-
|
|
10250
|
-
|
|
10251
|
-
exposure_item = filter_item.child(i)
|
|
10252
|
-
break
|
|
10356
|
+
exp_key = (filter_name, want_label)
|
|
10357
|
+
|
|
10358
|
+
exposure_item = self._flat_exp_item.get(exp_key)
|
|
10253
10359
|
if exposure_item is None:
|
|
10254
10360
|
exposure_item = QTreeWidgetItem([want_label])
|
|
10255
10361
|
filter_item.addChild(exposure_item)
|
|
10362
|
+
self._flat_exp_item[exp_key] = exposure_item
|
|
10256
10363
|
|
|
10257
10364
|
leaf = QTreeWidgetItem([os.path.basename(path), meta_text])
|
|
10258
10365
|
leaf.setData(0, Qt.ItemDataRole.UserRole, path)
|
|
@@ -10266,23 +10373,25 @@ class StackingSuiteDialog(QDialog):
|
|
|
10266
10373
|
self.light_files.setdefault(composite_key, []).append(path)
|
|
10267
10374
|
self.session_tags[path] = session_tag
|
|
10268
10375
|
|
|
10269
|
-
|
|
10270
|
-
filter_item =
|
|
10271
|
-
if
|
|
10376
|
+
# Cached filter item
|
|
10377
|
+
filter_item = self._light_filter_item.get(filter_name)
|
|
10378
|
+
if filter_item is None:
|
|
10379
|
+
filter_item = QTreeWidgetItem([filter_name])
|
|
10272
10380
|
tree.addTopLevelItem(filter_item)
|
|
10381
|
+
self._light_filter_item[filter_name] = filter_item
|
|
10273
10382
|
|
|
10274
10383
|
want_label = f"{exposure_text} ({image_size})"
|
|
10275
|
-
|
|
10276
|
-
|
|
10277
|
-
|
|
10278
|
-
|
|
10279
|
-
break
|
|
10384
|
+
exp_key = (filter_name, want_label)
|
|
10385
|
+
|
|
10386
|
+
# Cached exposure item
|
|
10387
|
+
exposure_item = self._light_exp_item.get(exp_key)
|
|
10280
10388
|
if exposure_item is None:
|
|
10281
10389
|
exposure_item = QTreeWidgetItem([want_label])
|
|
10282
10390
|
filter_item.addChild(exposure_item)
|
|
10391
|
+
self._light_exp_item[exp_key] = exposure_item
|
|
10283
10392
|
|
|
10284
10393
|
leaf = QTreeWidgetItem([os.path.basename(path), meta_text])
|
|
10285
|
-
leaf.setData(0, Qt.ItemDataRole.UserRole, path) # ✅
|
|
10394
|
+
leaf.setData(0, Qt.ItemDataRole.UserRole, path) # ✅ keep this
|
|
10286
10395
|
leaf.setData(0, Qt.ItemDataRole.UserRole + 1, session_tag)
|
|
10287
10396
|
exposure_item.addChild(leaf)
|
|
10288
10397
|
|
|
@@ -12816,6 +12925,20 @@ class StackingSuiteDialog(QDialog):
|
|
|
12816
12925
|
"drop": float(self.drizzle_drop_shrink_spin.value())
|
|
12817
12926
|
}
|
|
12818
12927
|
|
|
12928
|
+
def _global_drizzle_state(self) -> dict:
|
|
12929
|
+
# UI is the source of truth at runtime
|
|
12930
|
+
enabled = bool(self.drizzle_checkbox.isChecked())
|
|
12931
|
+
|
|
12932
|
+
# Scale from combo text like "1x", "2x", "3x"
|
|
12933
|
+
try:
|
|
12934
|
+
scale = float(self.drizzle_scale_combo.currentText().replace("x", "", 1).strip())
|
|
12935
|
+
except Exception:
|
|
12936
|
+
scale = 1.0
|
|
12937
|
+
|
|
12938
|
+
drop = float(self.drizzle_drop_shrink_spin.value())
|
|
12939
|
+
|
|
12940
|
+
return {"enabled": enabled, "scale": scale, "drop": drop}
|
|
12941
|
+
|
|
12819
12942
|
def _split_dual_band_osc(self, selected_groups=None):
|
|
12820
12943
|
"""
|
|
12821
12944
|
Create mono Ha/SII/OIII frames from dual-band OSC files and
|
|
@@ -13578,6 +13701,24 @@ class StackingSuiteDialog(QDialog):
|
|
|
13578
13701
|
self.update_status(self.tr("🔄 Image Registration Started..."))
|
|
13579
13702
|
self.extract_light_files_from_tree(debug=True)
|
|
13580
13703
|
|
|
13704
|
+
# --- Apply "removed from Registration tab" exclusions (session-level) ---
|
|
13705
|
+
dead = set()
|
|
13706
|
+
if hasattr(self, "deleted_calibrated_files") and self.deleted_calibrated_files:
|
|
13707
|
+
dead = set(self.deleted_calibrated_files)
|
|
13708
|
+
|
|
13709
|
+
if dead:
|
|
13710
|
+
for g in list(self.light_files.keys()):
|
|
13711
|
+
self.light_files[g] = [
|
|
13712
|
+
p for p in self.light_files[g]
|
|
13713
|
+
if os.path.normcase(os.path.abspath(p)) not in dead
|
|
13714
|
+
]
|
|
13715
|
+
if not self.light_files[g]:
|
|
13716
|
+
del self.light_files[g]
|
|
13717
|
+
|
|
13718
|
+
self.update_status(self.tr(f"🚫 Excluding {len(dead)} removed frame(s) from registration/stacking."))
|
|
13719
|
+
QApplication.processEvents()
|
|
13720
|
+
|
|
13721
|
+
|
|
13581
13722
|
comet_mode = bool(getattr(self, "comet_cb", None) and self.comet_cb.isChecked())
|
|
13582
13723
|
if comet_mode:
|
|
13583
13724
|
self.update_status(self.tr("🌠 Comet mode: please click the comet center to continue…"))
|
|
@@ -15190,6 +15331,14 @@ class StackingSuiteDialog(QDialog):
|
|
|
15190
15331
|
# Snapshot UI-dependent settings (your existing code)
|
|
15191
15332
|
# ----------------------------
|
|
15192
15333
|
drizzle_dict = self.gather_drizzle_settings_from_tree()
|
|
15334
|
+
try:
|
|
15335
|
+
self.update_status(self.tr(
|
|
15336
|
+
"🧾 Drizzle dict: " + ", ".join(f"{k}:{'ON' if v.get('drizzle_enabled') else 'off'}"
|
|
15337
|
+
for k, v in drizzle_dict.items())
|
|
15338
|
+
))
|
|
15339
|
+
except Exception:
|
|
15340
|
+
pass
|
|
15341
|
+
QApplication.processEvents()
|
|
15193
15342
|
try:
|
|
15194
15343
|
autocrop_enabled = self.autocrop_cb.isChecked()
|
|
15195
15344
|
autocrop_pct = float(self.autocrop_pct.value())
|
|
@@ -15604,6 +15753,22 @@ class StackingSuiteDialog(QDialog):
|
|
|
15604
15753
|
|
|
15605
15754
|
self._set_registration_busy(False)
|
|
15606
15755
|
|
|
15756
|
+
def _on_after_align_finished(self, success: bool, message: str):
|
|
15757
|
+
# Stop thread/progress UI first (whatever you already do)
|
|
15758
|
+
|
|
15759
|
+
if success:
|
|
15760
|
+
QMessageBox.information(
|
|
15761
|
+
self,
|
|
15762
|
+
self.tr("Stacking Complete"),
|
|
15763
|
+
message
|
|
15764
|
+
)
|
|
15765
|
+
else:
|
|
15766
|
+
QMessageBox.critical(
|
|
15767
|
+
self,
|
|
15768
|
+
self.tr("Stacking Failed"),
|
|
15769
|
+
message
|
|
15770
|
+
)
|
|
15771
|
+
|
|
15607
15772
|
def _on_mf_progress(self, s: str):
|
|
15608
15773
|
# Mirror non-token messages
|
|
15609
15774
|
if not s.startswith("__PROGRESS__"):
|
|
@@ -15632,25 +15797,48 @@ class StackingSuiteDialog(QDialog):
|
|
|
15632
15797
|
|
|
15633
15798
|
@pyqtSlot(bool, str)
|
|
15634
15799
|
def _on_post_pipeline_finished(self, ok: bool, message: str):
|
|
15800
|
+
# ---- close progress dialog ----
|
|
15635
15801
|
try:
|
|
15636
|
-
if getattr(self, "post_progress", None):
|
|
15802
|
+
if getattr(self, "post_progress", None) is not None:
|
|
15637
15803
|
self.post_progress.close()
|
|
15804
|
+
self.post_progress.deleteLater()
|
|
15638
15805
|
self.post_progress = None
|
|
15639
15806
|
except Exception:
|
|
15640
15807
|
pass
|
|
15641
15808
|
|
|
15809
|
+
# ---- stop thread ----
|
|
15810
|
+
try:
|
|
15811
|
+
if getattr(self, "post_thread", None) is not None:
|
|
15812
|
+
self.post_thread.quit()
|
|
15813
|
+
self.post_thread.wait()
|
|
15814
|
+
except Exception:
|
|
15815
|
+
pass
|
|
15816
|
+
|
|
15817
|
+
# ---- cleanup objects ----
|
|
15642
15818
|
try:
|
|
15643
|
-
self
|
|
15644
|
-
|
|
15819
|
+
if getattr(self, "post_worker", None) is not None:
|
|
15820
|
+
self.post_worker.deleteLater()
|
|
15821
|
+
self.post_worker = None
|
|
15822
|
+
if getattr(self, "post_thread", None) is not None:
|
|
15823
|
+
self.post_thread.deleteLater()
|
|
15824
|
+
self.post_thread = None
|
|
15645
15825
|
except Exception:
|
|
15646
15826
|
pass
|
|
15827
|
+
|
|
15828
|
+
# ---- update status (keep this behavior) ----
|
|
15647
15829
|
try:
|
|
15648
|
-
|
|
15649
|
-
self.
|
|
15830
|
+
# message already includes "Post-alignment complete..." text
|
|
15831
|
+
self.update_status(self.tr(message))
|
|
15650
15832
|
except Exception:
|
|
15651
15833
|
pass
|
|
15652
15834
|
|
|
15653
|
-
|
|
15835
|
+
# ---- popup summary ----
|
|
15836
|
+
# (Do this after progress dialog is gone so it doesn't hide behind it)
|
|
15837
|
+
if ok:
|
|
15838
|
+
QMessageBox.information(self, self.tr("Post-Alignment Complete"), message)
|
|
15839
|
+
else:
|
|
15840
|
+
QMessageBox.critical(self, self.tr("Post-Alignment Failed"), message)
|
|
15841
|
+
|
|
15654
15842
|
self._cfa_for_this_run = None
|
|
15655
15843
|
QApplication.processEvents()
|
|
15656
15844
|
|
|
@@ -15757,6 +15945,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
15757
15945
|
log(f"📁 Post-align: {n_groups} group(s), {n_frames} aligned frame(s).")
|
|
15758
15946
|
QApplication.processEvents()
|
|
15759
15947
|
|
|
15948
|
+
drizzle_enabled_global = self._get_drizzle_enabled()
|
|
15949
|
+
|
|
15760
15950
|
# Precompute a single global crop rect if enabled (pure computation, no UI).
|
|
15761
15951
|
global_rect = None
|
|
15762
15952
|
if autocrop_enabled:
|
|
@@ -16190,8 +16380,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
16190
16380
|
log(f"✂️ Saved CometBlend (auto-cropped) → {blend_path_crop}")
|
|
16191
16381
|
|
|
16192
16382
|
# ---- Drizzle bookkeeping for this group ----
|
|
16193
|
-
|
|
16194
|
-
if dconf.get("drizzle_enabled", False):
|
|
16383
|
+
if drizzle_enabled_global:
|
|
16195
16384
|
sasr_path = os.path.join(self.stacking_directory, f"{group_key}_rejections.sasr")
|
|
16196
16385
|
self.save_rejection_map_sasr(rejection_map, sasr_path)
|
|
16197
16386
|
log(f"✅ Saved rejection map to {sasr_path}")
|
|
@@ -16223,17 +16412,17 @@ class StackingSuiteDialog(QDialog):
|
|
|
16223
16412
|
originals_by_group[group] = orig_list
|
|
16224
16413
|
# ---- Drizzle pass (only for groups with drizzle enabled) ----
|
|
16225
16414
|
for group_key, file_list in grouped_files.items():
|
|
16226
|
-
|
|
16227
|
-
|
|
16228
|
-
log(f"✅ Group '{group_key}' not set for drizzle. Integrated image already saved.")
|
|
16415
|
+
if not drizzle_enabled_global:
|
|
16416
|
+
log(f"✅ Drizzle disabled (checkbox off). Group '{group_key}' integrated image already saved.")
|
|
16229
16417
|
continue
|
|
16230
16418
|
|
|
16419
|
+
# Use your existing getters (they can read UI/settings)
|
|
16231
16420
|
scale_factor = self._get_drizzle_scale()
|
|
16232
16421
|
drop_shrink = self._get_drizzle_pixfrac()
|
|
16233
16422
|
|
|
16234
|
-
# Optional: also read kernel for logging/branching
|
|
16235
16423
|
kernel = (self.settings.value("stacking/drizzle_kernel", "square", type=str) or "square").lower()
|
|
16236
|
-
|
|
16424
|
+
log(f"Drizzle cfg → scale={scale_factor}×, pixfrac={drop_shrink:.3f}, kernel={kernel}")
|
|
16425
|
+
|
|
16237
16426
|
rejections_for_group = group_integration_data[group_key]["rejection_map"]
|
|
16238
16427
|
n_frames_group = group_integration_data[group_key]["n_frames"]
|
|
16239
16428
|
|
|
@@ -16241,8 +16430,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
16241
16430
|
|
|
16242
16431
|
self.drizzle_stack_one_group(
|
|
16243
16432
|
group_key=group_key,
|
|
16244
|
-
file_list=file_list,
|
|
16245
|
-
original_list=originals_by_group.get(group_key, []),
|
|
16433
|
+
file_list=file_list,
|
|
16434
|
+
original_list=originals_by_group.get(group_key, []),
|
|
16246
16435
|
transforms_dict=transforms_dict,
|
|
16247
16436
|
frame_weights=frame_weights,
|
|
16248
16437
|
scale_factor=scale_factor,
|