setiastrosuitepro 1.7.5__py3-none-any.whl → 1.7.5.post1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/blink_comparator_pro.py +146 -2
- setiastro/saspro/stacking_suite.py +539 -115
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/RECORD +9 -9
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/licenses/license.txt +0 -0
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Auto-generated at build time. Do not edit.
|
|
2
|
-
BUILD_TIMESTAMP = "2026-01-
|
|
3
|
-
APP_VERSION = "1.7.5"
|
|
2
|
+
BUILD_TIMESTAMP = "2026-01-24T17:02:32Z"
|
|
3
|
+
APP_VERSION = "1.7.5.post1"
|
|
@@ -63,6 +63,7 @@ class MetricsPanel(QWidget):
|
|
|
63
63
|
self.flags = None # list of bools
|
|
64
64
|
self._threshold_initialized = [False]*4
|
|
65
65
|
self._open_previews = []
|
|
66
|
+
self._show_guides = True # default on (or False if you prefer)
|
|
66
67
|
|
|
67
68
|
self.plots, self.scats, self.lines = [], [], []
|
|
68
69
|
titles = [self.tr("FWHM (px)"), self.tr("Eccentricity"), self.tr("Background"), self.tr("Star Count")]
|
|
@@ -86,11 +87,101 @@ class MetricsPanel(QWidget):
|
|
|
86
87
|
lambda ln, m=idx: self._on_line_move(m, ln))
|
|
87
88
|
pw.addItem(line)
|
|
88
89
|
|
|
90
|
+
# --- dashed reference lines: median + ±3σ (robust) ---
|
|
91
|
+
median_ln = pg.InfiniteLine(pos=0, angle=0, movable=False,
|
|
92
|
+
pen=pg.mkPen((220, 220, 220, 170), width=1, style=Qt.PenStyle.DashLine))
|
|
93
|
+
sigma_lo = pg.InfiniteLine(pos=0, angle=0, movable=False,
|
|
94
|
+
pen=pg.mkPen((220, 220, 220, 120), width=1, style=Qt.PenStyle.DashLine))
|
|
95
|
+
sigma_hi = pg.InfiniteLine(pos=0, angle=0, movable=False,
|
|
96
|
+
pen=pg.mkPen((220, 220, 220, 120), width=1, style=Qt.PenStyle.DashLine))
|
|
97
|
+
|
|
98
|
+
# keep them behind points/threshold visually
|
|
99
|
+
median_ln.setZValue(-10)
|
|
100
|
+
sigma_lo.setZValue(-10)
|
|
101
|
+
sigma_hi.setZValue(-10)
|
|
102
|
+
|
|
103
|
+
pw.addItem(median_ln)
|
|
104
|
+
pw.addItem(sigma_lo)
|
|
105
|
+
pw.addItem(sigma_hi)
|
|
106
|
+
|
|
107
|
+
# create the lists once
|
|
108
|
+
if not hasattr(self, "median_lines"):
|
|
109
|
+
self.median_lines = []
|
|
110
|
+
self.sigma_lines = [] # list of (lo, hi)
|
|
111
|
+
|
|
112
|
+
self.median_lines.append(median_ln)
|
|
113
|
+
self.sigma_lines.append((sigma_lo, sigma_hi))
|
|
89
114
|
grid.addWidget(pw, idx//2, idx%2)
|
|
90
115
|
self.plots.append(pw)
|
|
91
116
|
self.scats.append(scat)
|
|
92
117
|
self.lines.append(line)
|
|
93
118
|
|
|
119
|
+
def set_guides_visible(self, on: bool):
|
|
120
|
+
self._show_guides = bool(on)
|
|
121
|
+
|
|
122
|
+
if not self._show_guides:
|
|
123
|
+
# ✅ hide immediately
|
|
124
|
+
if hasattr(self, "median_lines"):
|
|
125
|
+
for ln in self.median_lines:
|
|
126
|
+
ln.hide()
|
|
127
|
+
if hasattr(self, "sigma_lines"):
|
|
128
|
+
for lo, hi in self.sigma_lines:
|
|
129
|
+
lo.hide()
|
|
130
|
+
hi.hide()
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# ✅ turning ON: recompute/restore based on what’s currently plotted
|
|
134
|
+
self._refresh_guides_from_current_plot()
|
|
135
|
+
|
|
136
|
+
def _refresh_guides_from_current_plot(self):
|
|
137
|
+
"""Recompute/position guide lines using current plot data (if any)."""
|
|
138
|
+
if not getattr(self, "_show_guides", True):
|
|
139
|
+
return
|
|
140
|
+
if not hasattr(self, "median_lines") or not hasattr(self, "sigma_lines"):
|
|
141
|
+
return
|
|
142
|
+
# Use the scatter data already in each panel
|
|
143
|
+
for m, scat in enumerate(self.scats):
|
|
144
|
+
x, y = scat.getData()[:2]
|
|
145
|
+
if y is None or len(y) == 0:
|
|
146
|
+
self.median_lines[m].hide()
|
|
147
|
+
lo, hi = self.sigma_lines[m]
|
|
148
|
+
lo.hide(); hi.hide()
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
med, sig = self._median_and_robust_sigma(np.asarray(y, dtype=np.float32))
|
|
152
|
+
mline = self.median_lines[m]
|
|
153
|
+
lo_ln, hi_ln = self.sigma_lines[m]
|
|
154
|
+
|
|
155
|
+
if np.isfinite(med):
|
|
156
|
+
mline.setPos(med); mline.show()
|
|
157
|
+
else:
|
|
158
|
+
mline.hide()
|
|
159
|
+
|
|
160
|
+
if np.isfinite(med) and np.isfinite(sig) and sig > 0:
|
|
161
|
+
lo = med - 3.0 * sig
|
|
162
|
+
hi = med + 3.0 * sig
|
|
163
|
+
if m == 3:
|
|
164
|
+
lo = max(0.0, lo)
|
|
165
|
+
lo_ln.setPos(lo); hi_ln.setPos(hi)
|
|
166
|
+
lo_ln.show(); hi_ln.show()
|
|
167
|
+
else:
|
|
168
|
+
lo_ln.hide(); hi_ln.hide()
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@staticmethod
|
|
172
|
+
def _median_and_robust_sigma(y: np.ndarray):
|
|
173
|
+
"""Return (median, sigma) using MAD-based robust sigma. Ignores NaN/Inf."""
|
|
174
|
+
y = np.asarray(y, dtype=np.float32)
|
|
175
|
+
finite = np.isfinite(y)
|
|
176
|
+
if not finite.any():
|
|
177
|
+
return np.nan, np.nan
|
|
178
|
+
v = y[finite]
|
|
179
|
+
med = float(np.nanmedian(v))
|
|
180
|
+
mad = float(np.nanmedian(np.abs(v - med)))
|
|
181
|
+
sigma = 1.4826 * mad # robust sigma estimate
|
|
182
|
+
return med, float(sigma)
|
|
183
|
+
|
|
184
|
+
|
|
94
185
|
@staticmethod
|
|
95
186
|
def _compute_one(i_entry):
|
|
96
187
|
"""
|
|
@@ -324,6 +415,15 @@ class MetricsPanel(QWidget):
|
|
|
324
415
|
line.setPos(0)
|
|
325
416
|
pw.getPlotItem().getViewBox().update()
|
|
326
417
|
pw.repaint()
|
|
418
|
+
|
|
419
|
+
# ✅ hide guides too
|
|
420
|
+
if hasattr(self, "median_lines"):
|
|
421
|
+
for ln in self.median_lines:
|
|
422
|
+
ln.hide()
|
|
423
|
+
if hasattr(self, "sigma_lines"):
|
|
424
|
+
for lo, hi in self.sigma_lines:
|
|
425
|
+
lo.hide()
|
|
426
|
+
hi.hide()
|
|
327
427
|
return
|
|
328
428
|
|
|
329
429
|
# compute & cache on first call or new image list
|
|
@@ -358,6 +458,41 @@ class MetricsPanel(QWidget):
|
|
|
358
458
|
]
|
|
359
459
|
scat.setData(x=x, y=y, brush=brushes, pen=pg.mkPen(None), size=8)
|
|
360
460
|
|
|
461
|
+
# --- update dashed reference lines (median + ±3σ) ---
|
|
462
|
+
if getattr(self, "_show_guides", True):
|
|
463
|
+
try:
|
|
464
|
+
med, sig = self._median_and_robust_sigma(y)
|
|
465
|
+
mline = self.median_lines[m]
|
|
466
|
+
lo_ln, hi_ln = self.sigma_lines[m]
|
|
467
|
+
|
|
468
|
+
if np.isfinite(med):
|
|
469
|
+
mline.setPos(med)
|
|
470
|
+
mline.show()
|
|
471
|
+
else:
|
|
472
|
+
mline.hide()
|
|
473
|
+
|
|
474
|
+
if np.isfinite(med) and np.isfinite(sig) and sig > 0:
|
|
475
|
+
lo = med - 3.0 * sig
|
|
476
|
+
hi = med + 3.0 * sig
|
|
477
|
+
if m == 3:
|
|
478
|
+
lo = max(0.0, lo)
|
|
479
|
+
lo_ln.setPos(lo); hi_ln.setPos(hi)
|
|
480
|
+
lo_ln.show(); hi_ln.show()
|
|
481
|
+
else:
|
|
482
|
+
lo_ln.hide(); hi_ln.hide()
|
|
483
|
+
except Exception:
|
|
484
|
+
if hasattr(self, "median_lines") and m < len(self.median_lines):
|
|
485
|
+
self.median_lines[m].hide()
|
|
486
|
+
a, b = self.sigma_lines[m]
|
|
487
|
+
a.hide(); b.hide()
|
|
488
|
+
else:
|
|
489
|
+
# guides disabled -> force-hide
|
|
490
|
+
if hasattr(self, "median_lines") and m < len(self.median_lines):
|
|
491
|
+
self.median_lines[m].hide()
|
|
492
|
+
a, b = self.sigma_lines[m]
|
|
493
|
+
a.hide(); b.hide()
|
|
494
|
+
|
|
495
|
+
|
|
361
496
|
# initialize threshold line once
|
|
362
497
|
if not self._threshold_initialized[m]:
|
|
363
498
|
mx, mn = np.nanmax(y), np.nanmin(y)
|
|
@@ -456,7 +591,10 @@ class MetricsWindow(QWidget):
|
|
|
456
591
|
instr.setWordWrap(True)
|
|
457
592
|
instr.setStyleSheet("color: #ccc; font-size: 12px;")
|
|
458
593
|
vbox.addWidget(instr)
|
|
459
|
-
|
|
594
|
+
self.chk_guides = QCheckBox(self.tr("Show median and ±3σ guides"), self)
|
|
595
|
+
self.chk_guides.setChecked(True) # default on
|
|
596
|
+
self.chk_guides.toggled.connect(self._on_toggle_guides)
|
|
597
|
+
vbox.addWidget(self.chk_guides)
|
|
460
598
|
# → filter selector
|
|
461
599
|
self.group_combo = QComboBox(self)
|
|
462
600
|
self.group_combo.addItem(self.tr("All"))
|
|
@@ -479,6 +617,10 @@ class MetricsWindow(QWidget):
|
|
|
479
617
|
self._all_images = []
|
|
480
618
|
self._current_indices: Optional[List[int]] = None
|
|
481
619
|
|
|
620
|
+
def _on_toggle_guides(self, on: bool):
|
|
621
|
+
if hasattr(self, "metrics_panel") and self.metrics_panel is not None:
|
|
622
|
+
self.metrics_panel.set_guides_visible(on)
|
|
623
|
+
|
|
482
624
|
|
|
483
625
|
def _update_status(self, *args):
|
|
484
626
|
"""Recompute and show: Flagged Items X / Y (Z%). Robust to stale indices."""
|
|
@@ -537,6 +679,7 @@ class MetricsWindow(QWidget):
|
|
|
537
679
|
self._current_indices = self._order_all
|
|
538
680
|
self._apply_thresholds("All")
|
|
539
681
|
self.metrics_panel.plot(self._all_images, indices=self._current_indices)
|
|
682
|
+
self.metrics_panel.set_guides_visible(self.chk_guides.isChecked())
|
|
540
683
|
self._update_status()
|
|
541
684
|
|
|
542
685
|
def _reindex_list_after_remove(self, lst: List[int] | None, removed: List[int]) -> List[int] | None:
|
|
@@ -666,7 +809,8 @@ class MetricsWindow(QWidget):
|
|
|
666
809
|
grp = self.group_combo.currentText()
|
|
667
810
|
# save it for this group
|
|
668
811
|
self._thresholds_per_group[grp][metric_idx] = new_val
|
|
669
|
-
|
|
812
|
+
self.metrics_panel.plot(self._all_images, indices=self._current_indices)
|
|
813
|
+
self.metrics_panel.set_guides_visible(self.chk_guides.isChecked())
|
|
670
814
|
# (if you also want immediate re-flagging in the tree, keep your BlinkTab logic hooked here)
|
|
671
815
|
|
|
672
816
|
def _apply_thresholds(self, group_name: str):
|
|
@@ -2296,35 +2296,91 @@ def compute_safe_chunk(height, width, N, channels, dtype, pref_h, pref_w):
|
|
|
2296
2296
|
_DIM_RE = re.compile(r"\s*\(\d+\s*x\s*\d+\)\s*")
|
|
2297
2297
|
|
|
2298
2298
|
|
|
2299
|
-
def build_rejection_layers(
|
|
2300
|
-
|
|
2299
|
+
def build_rejection_layers(
|
|
2300
|
+
rejection_map: dict,
|
|
2301
|
+
ref_H: int,
|
|
2302
|
+
ref_W: int,
|
|
2303
|
+
n_frames: int,
|
|
2304
|
+
*,
|
|
2305
|
+
comb_threshold_frac: float = 0.02
|
|
2306
|
+
):
|
|
2301
2307
|
"""
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2308
|
+
Supports BOTH formats:
|
|
2309
|
+
|
|
2310
|
+
Old format:
|
|
2311
|
+
rejection_map[file] = [(x, y), (x, y), ...]
|
|
2312
|
+
|
|
2313
|
+
New format (tile masks):
|
|
2314
|
+
rejection_map[file] = [(x0, y0, mask_bool_thx_tw), ...]
|
|
2315
|
+
where mask_bool is (th, tw) boolean array for the tile at (x0,y0) in ref space.
|
|
2306
2316
|
"""
|
|
2317
|
+
import numpy as np
|
|
2318
|
+
|
|
2307
2319
|
if n_frames <= 0:
|
|
2308
2320
|
raise ValueError("n_frames must be > 0")
|
|
2309
2321
|
|
|
2310
|
-
# Use uint32 if you might exceed 65535 rejections (rare)
|
|
2311
2322
|
rej_cnt = np.zeros((ref_H, ref_W), dtype=np.uint32)
|
|
2312
2323
|
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2324
|
+
for _fpath, items in (rejection_map or {}).items():
|
|
2325
|
+
if not items:
|
|
2326
|
+
continue
|
|
2327
|
+
|
|
2328
|
+
# Detect format by peeking at first item
|
|
2329
|
+
first = items[0]
|
|
2319
2330
|
|
|
2320
|
-
|
|
2331
|
+
# ---- NEW tile-mask format ----
|
|
2332
|
+
if isinstance(first, (tuple, list)) and len(first) == 3:
|
|
2333
|
+
for it in items:
|
|
2334
|
+
try:
|
|
2335
|
+
x0, y0, m = it
|
|
2336
|
+
except Exception:
|
|
2337
|
+
continue
|
|
2338
|
+
if m is None:
|
|
2339
|
+
continue
|
|
2340
|
+
|
|
2341
|
+
mm = np.asarray(m, dtype=np.bool_)
|
|
2342
|
+
if mm.ndim != 2:
|
|
2343
|
+
# if somehow stored as (th,tw,1) etc.
|
|
2344
|
+
mm = np.squeeze(mm)
|
|
2345
|
+
if mm.ndim != 2:
|
|
2346
|
+
continue
|
|
2347
|
+
|
|
2348
|
+
th, tw = mm.shape
|
|
2349
|
+
if th <= 0 or tw <= 0:
|
|
2350
|
+
continue
|
|
2351
|
+
|
|
2352
|
+
x0i = int(x0)
|
|
2353
|
+
y0i = int(y0)
|
|
2354
|
+
if x0i >= ref_W or y0i >= ref_H:
|
|
2355
|
+
continue
|
|
2356
|
+
|
|
2357
|
+
x1i = min(ref_W, x0i + tw)
|
|
2358
|
+
y1i = min(ref_H, y0i + th)
|
|
2359
|
+
if x1i <= x0i or y1i <= y0i:
|
|
2360
|
+
continue
|
|
2361
|
+
|
|
2362
|
+
# add 1 rejection count wherever mask is True
|
|
2363
|
+
sub = rej_cnt[y0i:y1i, x0i:x1i]
|
|
2364
|
+
sub += mm[: (y1i - y0i), : (x1i - x0i)].astype(np.uint32)
|
|
2321
2365
|
|
|
2322
|
-
|
|
2366
|
+
# ---- OLD coord-list format ----
|
|
2367
|
+
else:
|
|
2368
|
+
for it in items:
|
|
2369
|
+
try:
|
|
2370
|
+
x, y = it
|
|
2371
|
+
except Exception:
|
|
2372
|
+
# tolerate weird entries
|
|
2373
|
+
continue
|
|
2374
|
+
xi = int(x)
|
|
2375
|
+
yi = int(y)
|
|
2376
|
+
if 0 <= xi < ref_W and 0 <= yi < ref_H:
|
|
2377
|
+
rej_cnt[yi, xi] += 1
|
|
2378
|
+
|
|
2379
|
+
rej_frac = (rej_cnt.astype(np.float32) / float(n_frames))
|
|
2323
2380
|
rej_any = (rej_frac >= float(comb_threshold_frac))
|
|
2324
2381
|
|
|
2325
2382
|
return rej_cnt, rej_frac, rej_any
|
|
2326
2383
|
|
|
2327
|
-
|
|
2328
2384
|
class _Responder(QObject):
|
|
2329
2385
|
finished = pyqtSignal(object) # emits the edited dict or None
|
|
2330
2386
|
|
|
@@ -3835,6 +3891,94 @@ class _MMImage:
|
|
|
3835
3891
|
return arr / 65535.0
|
|
3836
3892
|
return arr
|
|
3837
3893
|
|
|
3894
|
+
def read_tile_into(self, dst: np.ndarray, y0, y1, x0, x1, channels: int):
|
|
3895
|
+
"""
|
|
3896
|
+
Fill dst (shape (th,tw,C) float32) with the tile.
|
|
3897
|
+
No per-tile allocations.
|
|
3898
|
+
"""
|
|
3899
|
+
import numpy as np
|
|
3900
|
+
|
|
3901
|
+
th = y1 - y0
|
|
3902
|
+
tw = x1 - x0
|
|
3903
|
+
|
|
3904
|
+
# dst is (th,tw,C)
|
|
3905
|
+
if self._kind == "fits":
|
|
3906
|
+
d = self._fits_data
|
|
3907
|
+
|
|
3908
|
+
if self.ndim == 2:
|
|
3909
|
+
tile = d[y0:y1, x0:x1] # may be view (memmap) or array
|
|
3910
|
+
# scale BEFORE float cast (tile dtype still int if int on disk)
|
|
3911
|
+
if tile.dtype.kind in ("u", "i"):
|
|
3912
|
+
if self._bitpix == 8:
|
|
3913
|
+
np.multiply(tile, (1.0/255.0), out=dst[..., 0], casting="unsafe")
|
|
3914
|
+
elif self._bitpix == 16:
|
|
3915
|
+
np.multiply(tile, (1.0/65535.0), out=dst[..., 0], casting="unsafe")
|
|
3916
|
+
else:
|
|
3917
|
+
dst[..., 0] = tile
|
|
3918
|
+
else:
|
|
3919
|
+
dst[..., 0] = tile # astropy already float-scaled
|
|
3920
|
+
|
|
3921
|
+
if channels == 3:
|
|
3922
|
+
dst[..., 1] = dst[..., 0]
|
|
3923
|
+
dst[..., 2] = dst[..., 0]
|
|
3924
|
+
return
|
|
3925
|
+
|
|
3926
|
+
# ndim==3
|
|
3927
|
+
sl = [slice(None)] * 3
|
|
3928
|
+
sl[self._spat_axes[0]] = slice(y0, y1)
|
|
3929
|
+
sl[self._spat_axes[1]] = slice(x0, x1)
|
|
3930
|
+
tile = d[tuple(sl)]
|
|
3931
|
+
if (self._color_axis is not None) and (self._color_axis != 2):
|
|
3932
|
+
tile = np.moveaxis(tile, self._color_axis, -1) # now HWC-ish
|
|
3933
|
+
|
|
3934
|
+
# tile might be (th,tw,3) or (3,th,tw) in weird cases; handle quick
|
|
3935
|
+
if tile.ndim == 3 and tile.shape[-1] != 3 and tile.shape[0] == 3:
|
|
3936
|
+
tile = np.moveaxis(tile, 0, -1)
|
|
3937
|
+
|
|
3938
|
+
# scale if integer
|
|
3939
|
+
if tile.dtype.kind in ("u", "i"):
|
|
3940
|
+
div = 1.0
|
|
3941
|
+
if self._bitpix == 8:
|
|
3942
|
+
div = 255.0
|
|
3943
|
+
elif self._bitpix == 16:
|
|
3944
|
+
div = 65535.0
|
|
3945
|
+
# dst float32 = tile / div
|
|
3946
|
+
np.divide(tile, div, out=dst[..., :3], casting="unsafe")
|
|
3947
|
+
else:
|
|
3948
|
+
dst[..., :3] = tile
|
|
3949
|
+
|
|
3950
|
+
# if caller requested mono, collapse to dst[...,0]
|
|
3951
|
+
if channels == 1:
|
|
3952
|
+
# simple luma or first channel; you can choose policy
|
|
3953
|
+
dst[..., 0] = dst[..., 0] # keep R
|
|
3954
|
+
return
|
|
3955
|
+
|
|
3956
|
+
# XISF
|
|
3957
|
+
if self._xisf_memmap is not None:
|
|
3958
|
+
C = 1 if self.ndim == 2 else self.shape[2]
|
|
3959
|
+
if C == 1:
|
|
3960
|
+
tile = self._xisf_memmap[0, y0:y1, x0:x1]
|
|
3961
|
+
dst[..., 0] = tile
|
|
3962
|
+
if channels == 3:
|
|
3963
|
+
dst[..., 1] = dst[..., 0]
|
|
3964
|
+
dst[..., 2] = dst[..., 0]
|
|
3965
|
+
else:
|
|
3966
|
+
tile = np.moveaxis(self._xisf_memmap[:, y0:y1, x0:x1], 0, -1)
|
|
3967
|
+
dst[..., :3] = tile
|
|
3968
|
+
if channels == 1:
|
|
3969
|
+
dst[..., 0] = dst[..., 0]
|
|
3970
|
+
else:
|
|
3971
|
+
tile = self._xisf_arr[y0:y1, x0:x1]
|
|
3972
|
+
if tile.ndim == 2:
|
|
3973
|
+
dst[..., 0] = tile
|
|
3974
|
+
if channels == 3:
|
|
3975
|
+
dst[..., 1] = dst[..., 0]
|
|
3976
|
+
dst[..., 2] = dst[..., 0]
|
|
3977
|
+
else:
|
|
3978
|
+
dst[..., :3] = tile
|
|
3979
|
+
if channels == 1:
|
|
3980
|
+
dst[..., 0] = dst[..., 0]
|
|
3981
|
+
|
|
3838
3982
|
|
|
3839
3983
|
def read_tile(self, y0, y1, x0, x1) -> np.ndarray:
|
|
3840
3984
|
import os
|
|
@@ -11382,10 +11526,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
11382
11526
|
tw = x1 - x0
|
|
11383
11527
|
ts = buf[:N, :th, :tw, :channels]
|
|
11384
11528
|
for i, src in enumerate(sources):
|
|
11385
|
-
|
|
11386
|
-
if sub.ndim == 2:
|
|
11387
|
-
sub = sub[:, :, None] if channels == 1 else sub[:, :, None].repeat(3, axis=2)
|
|
11388
|
-
ts[i, :, :, :] = sub
|
|
11529
|
+
src.read_tile_into(ts[i], y0, y1, x0, x1, channels)
|
|
11389
11530
|
return th, tw
|
|
11390
11531
|
|
|
11391
11532
|
tp = ThreadPoolExecutor(max_workers=1)
|
|
@@ -16965,33 +17106,217 @@ class StackingSuiteDialog(QDialog):
|
|
|
16965
17106
|
QApplication.processEvents()
|
|
16966
17107
|
|
|
16967
17108
|
|
|
16968
|
-
def save_rejection_map_sasr(self, rejection_map,
|
|
17109
|
+
def save_rejection_map_sasr(self, rejection_map: dict, out_path: str):
|
|
16969
17110
|
"""
|
|
16970
|
-
|
|
16971
|
-
|
|
16972
|
-
|
|
16973
|
-
|
|
16974
|
-
|
|
16975
|
-
|
|
16976
|
-
|
|
16977
|
-
|
|
17111
|
+
Save per-file rejection information to .sasr (JSON).
|
|
17112
|
+
|
|
17113
|
+
Supports BOTH formats:
|
|
17114
|
+
Old: rejection_map[file] = [(x, y), ...]
|
|
17115
|
+
New: rejection_map[file] = [(x0, y0, mask_bool_thx_tw), ...]
|
|
17116
|
+
where mask_bool is a 2D boolean ndarray for that tile in REF space.
|
|
17117
|
+
|
|
17118
|
+
For the new format we bit-pack masks to keep files small.
|
|
16978
17119
|
"""
|
|
16979
|
-
|
|
16980
|
-
|
|
16981
|
-
|
|
16982
|
-
|
|
16983
|
-
|
|
16984
|
-
|
|
16985
|
-
|
|
17120
|
+
import os, json, base64
|
|
17121
|
+
import numpy as np
|
|
17122
|
+
|
|
17123
|
+
def _is_tile_item(it) -> bool:
|
|
17124
|
+
return isinstance(it, (tuple, list)) and len(it) == 3
|
|
17125
|
+
|
|
17126
|
+
def _is_xy_item(it) -> bool:
|
|
17127
|
+
return isinstance(it, (tuple, list)) and len(it) == 2
|
|
17128
|
+
|
|
17129
|
+
payload = {
|
|
17130
|
+
"version": 2, # bump because we now support tile masks
|
|
17131
|
+
"kind": "mixed", # per-file can be xy or tiles
|
|
17132
|
+
"files": {}
|
|
17133
|
+
}
|
|
17134
|
+
|
|
17135
|
+
for fpath, items in (rejection_map or {}).items():
|
|
17136
|
+
if not items:
|
|
17137
|
+
continue
|
|
17138
|
+
|
|
17139
|
+
# detect which format this file uses
|
|
17140
|
+
first = items[0]
|
|
17141
|
+
if _is_tile_item(first):
|
|
17142
|
+
out_items = []
|
|
17143
|
+
for it in items:
|
|
17144
|
+
try:
|
|
17145
|
+
x0, y0, m = it
|
|
17146
|
+
except Exception:
|
|
17147
|
+
continue
|
|
17148
|
+
if m is None:
|
|
17149
|
+
continue
|
|
17150
|
+
|
|
17151
|
+
mm = np.asarray(m, dtype=np.bool_)
|
|
17152
|
+
if mm.ndim != 2:
|
|
17153
|
+
mm = np.squeeze(mm)
|
|
17154
|
+
if mm.ndim != 2:
|
|
17155
|
+
continue
|
|
17156
|
+
|
|
17157
|
+
th, tw = mm.shape
|
|
17158
|
+
if th <= 0 or tw <= 0:
|
|
17159
|
+
continue
|
|
17160
|
+
|
|
17161
|
+
packed = np.packbits(mm.reshape(-1).astype(np.uint8))
|
|
17162
|
+
b64 = base64.b64encode(packed.tobytes()).decode("ascii")
|
|
17163
|
+
|
|
17164
|
+
out_items.append({
|
|
17165
|
+
"x0": int(x0),
|
|
17166
|
+
"y0": int(y0),
|
|
17167
|
+
"h": int(th),
|
|
17168
|
+
"w": int(tw),
|
|
17169
|
+
"bits": b64
|
|
17170
|
+
})
|
|
17171
|
+
|
|
17172
|
+
payload["files"][os.path.normpath(fpath)] = {
|
|
17173
|
+
"format": "tilemask",
|
|
17174
|
+
"tiles": out_items
|
|
17175
|
+
}
|
|
17176
|
+
|
|
17177
|
+
elif _is_xy_item(first):
|
|
17178
|
+
# old (x,y) list
|
|
17179
|
+
xy = []
|
|
17180
|
+
for it in items:
|
|
17181
|
+
try:
|
|
17182
|
+
x, y = it
|
|
17183
|
+
except Exception:
|
|
17184
|
+
continue
|
|
17185
|
+
xy.append([int(x), int(y)])
|
|
17186
|
+
|
|
17187
|
+
payload["files"][os.path.normpath(fpath)] = {
|
|
17188
|
+
"format": "xy",
|
|
17189
|
+
"coords": xy
|
|
17190
|
+
}
|
|
17191
|
+
|
|
17192
|
+
else:
|
|
17193
|
+
# Unknown / mixed junk — try to salvage
|
|
17194
|
+
xy = []
|
|
17195
|
+
tiles = []
|
|
17196
|
+
for it in items:
|
|
17197
|
+
if _is_xy_item(it):
|
|
17198
|
+
x, y = it
|
|
17199
|
+
xy.append([int(x), int(y)])
|
|
17200
|
+
elif _is_tile_item(it):
|
|
17201
|
+
x0, y0, m = it
|
|
17202
|
+
mm = np.asarray(m, dtype=np.bool_)
|
|
17203
|
+
if mm.ndim != 2:
|
|
17204
|
+
mm = np.squeeze(mm)
|
|
17205
|
+
if mm.ndim != 2:
|
|
17206
|
+
continue
|
|
17207
|
+
th, tw = mm.shape
|
|
17208
|
+
packed = np.packbits(mm.reshape(-1).astype(np.uint8))
|
|
17209
|
+
b64 = base64.b64encode(packed.tobytes()).decode("ascii")
|
|
17210
|
+
tiles.append({"x0": int(x0), "y0": int(y0), "h": int(th), "w": int(tw), "bits": b64})
|
|
17211
|
+
|
|
17212
|
+
if tiles:
|
|
17213
|
+
payload["files"][os.path.normpath(fpath)] = {"format": "tilemask", "tiles": tiles}
|
|
17214
|
+
elif xy:
|
|
17215
|
+
payload["files"][os.path.normpath(fpath)] = {"format": "xy", "coords": xy}
|
|
17216
|
+
|
|
17217
|
+
os.makedirs(os.path.dirname(out_path), exist_ok=True)
|
|
17218
|
+
with open(out_path, "w", encoding="utf-8") as f:
|
|
17219
|
+
json.dump(payload, f, indent=2)
|
|
17220
|
+
|
|
16986
17221
|
|
|
16987
17222
|
def load_rejection_map_sasr(self, in_file):
|
|
16988
17223
|
"""
|
|
16989
|
-
|
|
16990
|
-
|
|
17224
|
+
Load a .sasr rejection map.
|
|
17225
|
+
|
|
17226
|
+
Supports:
|
|
17227
|
+
- V1 legacy text format:
|
|
17228
|
+
FILE: <path>
|
|
17229
|
+
x, y
|
|
17230
|
+
x, y
|
|
17231
|
+
|
|
17232
|
+
Returns: { fpath: [(x, y), ...] }
|
|
17233
|
+
|
|
17234
|
+
- V2 JSON format (tile masks, bit-packed):
|
|
17235
|
+
Returns: { fpath: [(x0, y0, mask_bool_2d), ...] }
|
|
17236
|
+
|
|
17237
|
+
Notes:
|
|
17238
|
+
- This function returns whatever the file contains (xy or tilemask).
|
|
17239
|
+
- Your drizzle path expects tilemask format keyed by *aligned path*.
|
|
16991
17240
|
"""
|
|
17241
|
+
import os, re, json, base64
|
|
17242
|
+
import numpy as np
|
|
17243
|
+
|
|
17244
|
+
# Read file
|
|
17245
|
+
with open(in_file, "r", encoding="utf-8") as f:
|
|
17246
|
+
content = f.read()
|
|
17247
|
+
|
|
17248
|
+
s = content.lstrip()
|
|
17249
|
+
|
|
17250
|
+
# -----------------------
|
|
17251
|
+
# V2: JSON tilemask format
|
|
17252
|
+
# -----------------------
|
|
17253
|
+
if s.startswith("{") or s.startswith("["):
|
|
17254
|
+
try:
|
|
17255
|
+
obj = json.loads(content)
|
|
17256
|
+
except Exception:
|
|
17257
|
+
obj = None
|
|
17258
|
+
|
|
17259
|
+
if isinstance(obj, dict) and "files" in obj:
|
|
17260
|
+
files = obj.get("files") or {}
|
|
17261
|
+
rejections = {}
|
|
17262
|
+
|
|
17263
|
+
for raw_path, entry in files.items():
|
|
17264
|
+
if not isinstance(entry, dict):
|
|
17265
|
+
continue
|
|
17266
|
+
fmt = (entry.get("format") or "").lower()
|
|
17267
|
+
|
|
17268
|
+
if fmt == "xy":
|
|
17269
|
+
coords = []
|
|
17270
|
+
for xy in entry.get("coords") or []:
|
|
17271
|
+
try:
|
|
17272
|
+
x, y = xy
|
|
17273
|
+
coords.append((int(x), int(y)))
|
|
17274
|
+
except Exception:
|
|
17275
|
+
continue
|
|
17276
|
+
if coords:
|
|
17277
|
+
rejections[os.path.normpath(raw_path)] = coords
|
|
17278
|
+
|
|
17279
|
+
elif fmt == "tilemask":
|
|
17280
|
+
tiles_out = []
|
|
17281
|
+
for t in entry.get("tiles") or []:
|
|
17282
|
+
try:
|
|
17283
|
+
x0 = int(t["x0"])
|
|
17284
|
+
y0 = int(t["y0"])
|
|
17285
|
+
h = int(t["h"])
|
|
17286
|
+
w = int(t["w"])
|
|
17287
|
+
b64 = t["bits"]
|
|
17288
|
+
except Exception:
|
|
17289
|
+
continue
|
|
17290
|
+
|
|
17291
|
+
try:
|
|
17292
|
+
packed = np.frombuffer(base64.b64decode(b64.encode("ascii")), dtype=np.uint8)
|
|
17293
|
+
bits = np.unpackbits(packed) # 1D, length >= h*w
|
|
17294
|
+
bits = bits[: (h * w)]
|
|
17295
|
+
mask = (bits.reshape(h, w).astype(bool))
|
|
17296
|
+
except Exception:
|
|
17297
|
+
continue
|
|
17298
|
+
|
|
17299
|
+
tiles_out.append((x0, y0, mask))
|
|
17300
|
+
|
|
17301
|
+
if tiles_out:
|
|
17302
|
+
rejections[os.path.normpath(raw_path)] = tiles_out
|
|
17303
|
+
|
|
17304
|
+
else:
|
|
17305
|
+
# Unknown entry type; ignore
|
|
17306
|
+
continue
|
|
17307
|
+
|
|
17308
|
+
return rejections
|
|
17309
|
+
|
|
17310
|
+
# If JSON but not our format, fall through to legacy parse attempt
|
|
17311
|
+
# (some people may have old text that starts with '{' accidentally)
|
|
17312
|
+
|
|
17313
|
+
# -----------------------
|
|
17314
|
+
# V1: legacy text format
|
|
17315
|
+
# -----------------------
|
|
16992
17316
|
rejections = {}
|
|
16993
|
-
|
|
16994
|
-
|
|
17317
|
+
content = content.strip()
|
|
17318
|
+
if not content:
|
|
17319
|
+
return rejections
|
|
16995
17320
|
|
|
16996
17321
|
# Split on blank lines
|
|
16997
17322
|
blocks = re.split(r"\n\s*\n", content)
|
|
@@ -17000,21 +17325,23 @@ class StackingSuiteDialog(QDialog):
|
|
|
17000
17325
|
if not lines:
|
|
17001
17326
|
continue
|
|
17002
17327
|
|
|
17003
|
-
# First line should be 'FILE: <path>'
|
|
17004
17328
|
if lines[0].startswith("FILE:"):
|
|
17005
17329
|
raw_path = lines[0].replace("FILE:", "").strip()
|
|
17006
17330
|
coords = []
|
|
17007
17331
|
for line in lines[1:]:
|
|
17008
|
-
# Each subsequent line is "x, y"
|
|
17009
17332
|
parts = line.split(",")
|
|
17010
17333
|
if len(parts) == 2:
|
|
17011
17334
|
x_str, y_str = parts
|
|
17012
|
-
|
|
17013
|
-
|
|
17335
|
+
try:
|
|
17336
|
+
x = int(x_str.strip())
|
|
17337
|
+
y = int(y_str.strip())
|
|
17338
|
+
except Exception:
|
|
17339
|
+
continue
|
|
17014
17340
|
coords.append((x, y))
|
|
17015
|
-
|
|
17016
|
-
|
|
17341
|
+
if coords:
|
|
17342
|
+
rejections[os.path.normpath(raw_path)] = coords
|
|
17017
17343
|
|
|
17344
|
+
return rejections
|
|
17018
17345
|
|
|
17019
17346
|
@pyqtSlot(list, dict, result=object) # (files: list[str], initial_xy: dict[str, (x,y)]) -> dict|None
|
|
17020
17347
|
def show_comet_preview(self, files, initial_xy):
|
|
@@ -17532,6 +17859,14 @@ class StackingSuiteDialog(QDialog):
|
|
|
17532
17859
|
|
|
17533
17860
|
# ---- Drizzle bookkeeping for this group ----
|
|
17534
17861
|
if drizzle_enabled_global:
|
|
17862
|
+
|
|
17863
|
+
try:
|
|
17864
|
+
any_key = next(iter(rejection_map))
|
|
17865
|
+
first_item = rejection_map[any_key][0] if rejection_map[any_key] else None
|
|
17866
|
+
log(f"🔎 rej_map sample: key={os.path.basename(any_key)} item_type={type(first_item)} len={len(first_item) if hasattr(first_item,'__len__') else 'NA'}")
|
|
17867
|
+
except Exception:
|
|
17868
|
+
pass
|
|
17869
|
+
|
|
17535
17870
|
sasr_path = os.path.join(self.stacking_directory, f"{group_key}_rejections.sasr")
|
|
17536
17871
|
self.save_rejection_map_sasr(rejection_map, sasr_path)
|
|
17537
17872
|
log(f"✅ Saved rejection map to {sasr_path}")
|
|
@@ -17650,7 +17985,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
17650
17985
|
*,
|
|
17651
17986
|
algo_override: str | None = None
|
|
17652
17987
|
):
|
|
17653
|
-
|
|
17988
|
+
collect_per_file = bool(self._get_drizzle_enabled())
|
|
17989
|
+
per_file_rejections = {f: [] for f in file_list} if collect_per_file else None
|
|
17654
17990
|
debug_starrem = bool(self.settings.value("stacking/comet_starrem/debug_dump", False, type=bool))
|
|
17655
17991
|
debug_dir = os.path.join(self.stacking_directory, "debug_comet_starrem")
|
|
17656
17992
|
os.makedirs(debug_dir, exist_ok=True)
|
|
@@ -17969,10 +18305,13 @@ class StackingSuiteDialog(QDialog):
|
|
|
17969
18305
|
rej_any[y0:y1, x0:x1] |= np.any(trm, axis=0)
|
|
17970
18306
|
rej_count[y0:y1, x0:x1] += trm.sum(axis=0).astype(np.uint16)
|
|
17971
18307
|
|
|
17972
|
-
|
|
17973
|
-
|
|
17974
|
-
|
|
17975
|
-
|
|
18308
|
+
if collect_per_file:
|
|
18309
|
+
for i, fpath in enumerate(file_list):
|
|
18310
|
+
m = trm[i]
|
|
18311
|
+
if np.any(m):
|
|
18312
|
+
# store as a tile mask, not coord list
|
|
18313
|
+
per_file_rejections[fpath].append((x0, y0, m.copy()))
|
|
18314
|
+
|
|
17976
18315
|
|
|
17977
18316
|
# Close readers and clean temp
|
|
17978
18317
|
for s in sources:
|
|
@@ -18849,13 +19188,13 @@ class StackingSuiteDialog(QDialog):
|
|
|
18849
19188
|
frame_weights,
|
|
18850
19189
|
scale_factor,
|
|
18851
19190
|
drop_shrink,
|
|
18852
|
-
rejection_map,
|
|
19191
|
+
rejection_map, # NEW FORMAT: { aligned_path: [(x0,y0,mask_bool), ...] }
|
|
18853
19192
|
autocrop_enabled,
|
|
18854
19193
|
rect_override,
|
|
18855
19194
|
status_cb
|
|
18856
19195
|
):
|
|
18857
19196
|
import os
|
|
18858
|
-
|
|
19197
|
+
import numpy as np
|
|
18859
19198
|
import cv2
|
|
18860
19199
|
from datetime import datetime
|
|
18861
19200
|
from astropy.io import fits
|
|
@@ -18873,7 +19212,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
18873
19212
|
log(f"ℹ️ Using in-memory REF_SHAPE fallback: {ref_H}×{ref_W}")
|
|
18874
19213
|
else:
|
|
18875
19214
|
log("⚠️ Missing REF_SHAPE in SASD; cannot drizzle.")
|
|
18876
|
-
return
|
|
19215
|
+
return False
|
|
18877
19216
|
|
|
18878
19217
|
log(f"✅ SASD v2: loaded {len(xforms)} transform(s).")
|
|
18879
19218
|
|
|
@@ -18907,8 +19246,14 @@ class StackingSuiteDialog(QDialog):
|
|
|
18907
19246
|
else:
|
|
18908
19247
|
_kcode = 0 # square
|
|
18909
19248
|
|
|
18910
|
-
|
|
18911
|
-
|
|
19249
|
+
# Rejection logging: now "tiles", not "pixels"
|
|
19250
|
+
total_tiles = 0
|
|
19251
|
+
if rejection_map:
|
|
19252
|
+
try:
|
|
19253
|
+
total_tiles = sum(len(v) for v in rejection_map.values())
|
|
19254
|
+
except Exception:
|
|
19255
|
+
total_tiles = 0
|
|
19256
|
+
log(f"🔭 Drizzle stacking for group '{group_key}' with {total_tiles} rejection tiles.")
|
|
18912
19257
|
|
|
18913
19258
|
# Need at least 2 frames to be worth it
|
|
18914
19259
|
if len(file_list) < 2:
|
|
@@ -18953,9 +19298,118 @@ class StackingSuiteDialog(QDialog):
|
|
|
18953
19298
|
# ---- allocate buffers ----
|
|
18954
19299
|
out_h = int(canvas_H)
|
|
18955
19300
|
out_w = int(canvas_W)
|
|
18956
|
-
drizzle_buffer
|
|
19301
|
+
drizzle_buffer = np.zeros((out_h, out_w) if is_mono else (out_h, out_w, c), dtype=self._dtype())
|
|
18957
19302
|
coverage_buffer = np.zeros_like(drizzle_buffer, dtype=self._dtype())
|
|
18958
|
-
finalize_func
|
|
19303
|
+
finalize_func = finalize_drizzle_2d if is_mono else finalize_drizzle_3d
|
|
19304
|
+
|
|
19305
|
+
# ---- helpers for rejection masks ----
|
|
19306
|
+
dilate_px = int(self.settings.value("stacking/reject_dilate_px", 0, type=int))
|
|
19307
|
+
dilate_shape = (self.settings.value("stacking/reject_dilate_shape", "square", type=str) or "square").lower()
|
|
19308
|
+
|
|
19309
|
+
def _make_dilate_kernel(r: int, shape_name: str):
|
|
19310
|
+
if r <= 0:
|
|
19311
|
+
return None
|
|
19312
|
+
ksz = 2 * r + 1
|
|
19313
|
+
if shape_name.startswith("dia"):
|
|
19314
|
+
k = np.zeros((ksz, ksz), np.uint8)
|
|
19315
|
+
for yy in range(-r, r + 1):
|
|
19316
|
+
for xx in range(-r, r + 1):
|
|
19317
|
+
if (abs(xx) + abs(yy)) <= r:
|
|
19318
|
+
k[yy + r, xx + r] = 1
|
|
19319
|
+
return k
|
|
19320
|
+
# square default
|
|
19321
|
+
return np.ones((ksz, ksz), np.uint8)
|
|
19322
|
+
|
|
19323
|
+
_dilate_kernel = _make_dilate_kernel(dilate_px, dilate_shape)
|
|
19324
|
+
|
|
19325
|
+
def _build_rej_mask_ref(aligned_key: str):
|
|
19326
|
+
"""
|
|
19327
|
+
Build a full-frame ref-space rejection mask (ref_H, ref_W) from tile masks:
|
|
19328
|
+
rejection_map[aligned_key] = [(x0,y0,mask_bool_thx_tw), ...]
|
|
19329
|
+
Returns None if no tiles.
|
|
19330
|
+
"""
|
|
19331
|
+
if not rejection_map:
|
|
19332
|
+
return None
|
|
19333
|
+
tiles = rejection_map.get(aligned_key, None)
|
|
19334
|
+
if not tiles:
|
|
19335
|
+
return None
|
|
19336
|
+
|
|
19337
|
+
mref = np.zeros((ref_H, ref_W), dtype=np.bool_)
|
|
19338
|
+
for item in tiles:
|
|
19339
|
+
try:
|
|
19340
|
+
x0, y0, m = item
|
|
19341
|
+
except Exception:
|
|
19342
|
+
continue
|
|
19343
|
+
if m is None:
|
|
19344
|
+
continue
|
|
19345
|
+
# ensure bool
|
|
19346
|
+
mm = np.asarray(m, dtype=np.bool_)
|
|
19347
|
+
th, tw = mm.shape[:2]
|
|
19348
|
+
if th <= 0 or tw <= 0:
|
|
19349
|
+
continue
|
|
19350
|
+
x0i = int(x0); y0i = int(y0)
|
|
19351
|
+
if x0i >= ref_W or y0i >= ref_H:
|
|
19352
|
+
continue
|
|
19353
|
+
x1i = min(ref_W, x0i + tw)
|
|
19354
|
+
y1i = min(ref_H, y0i + th)
|
|
19355
|
+
if x1i <= x0i or y1i <= y0i:
|
|
19356
|
+
continue
|
|
19357
|
+
mref[y0i:y1i, x0i:x1i] |= mm[:(y1i - y0i), :(x1i - x0i)]
|
|
19358
|
+
|
|
19359
|
+
if _dilate_kernel is not None:
|
|
19360
|
+
mref = cv2.dilate(mref.astype(np.uint8), _dilate_kernel, iterations=1).astype(bool)
|
|
19361
|
+
return mref
|
|
19362
|
+
|
|
19363
|
+
def _apply_rejections_to_img(img_data: np.ndarray, rej_mask_ref: np.ndarray, pixels_are_registered: bool, A23: np.ndarray):
|
|
19364
|
+
"""
|
|
19365
|
+
Apply ref-space rejection mask to img_data:
|
|
19366
|
+
- if pixels_are_registered: img_data is already in ref space -> direct apply
|
|
19367
|
+
- else: img_data is raw/original and A23 maps raw->ref -> warp mask ref->raw once, then apply
|
|
19368
|
+
"""
|
|
19369
|
+
if rej_mask_ref is None:
|
|
19370
|
+
return img_data
|
|
19371
|
+
|
|
19372
|
+
# after channel extraction, img_data may be 2D
|
|
19373
|
+
if img_data.ndim == 2:
|
|
19374
|
+
Hraw, Wraw = img_data.shape
|
|
19375
|
+
else:
|
|
19376
|
+
Hraw, Wraw = img_data.shape[0], img_data.shape[1]
|
|
19377
|
+
|
|
19378
|
+
if pixels_are_registered:
|
|
19379
|
+
# Clip mask to actual image dims (paranoia)
|
|
19380
|
+
hh = min(rej_mask_ref.shape[0], Hraw)
|
|
19381
|
+
ww = min(rej_mask_ref.shape[1], Wraw)
|
|
19382
|
+
m = rej_mask_ref[:hh, :ww]
|
|
19383
|
+
if img_data.ndim == 2:
|
|
19384
|
+
img_data[:hh, :ww][m] = 0.0
|
|
19385
|
+
else:
|
|
19386
|
+
img_data[:hh, :ww, :][m, :] = 0.0
|
|
19387
|
+
return img_data
|
|
19388
|
+
|
|
19389
|
+
# affine case: A23 maps raw->ref. Need ref-mask in raw space -> warp with inverse affine.
|
|
19390
|
+
try:
|
|
19391
|
+
invA23 = cv2.invertAffineTransform(A23.astype(np.float32))
|
|
19392
|
+
except Exception:
|
|
19393
|
+
# fallback to numpy invert of 3x3, then slice
|
|
19394
|
+
Hc = np.eye(3, dtype=np.float32)
|
|
19395
|
+
Hc[:2, :] = A23.astype(np.float32)
|
|
19396
|
+
Hinv = np.linalg.inv(Hc)
|
|
19397
|
+
invA23 = Hinv[:2, :].astype(np.float32)
|
|
19398
|
+
|
|
19399
|
+
raw_mask = cv2.warpAffine(
|
|
19400
|
+
rej_mask_ref.astype(np.uint8),
|
|
19401
|
+
invA23,
|
|
19402
|
+
(Wraw, Hraw),
|
|
19403
|
+
flags=cv2.INTER_NEAREST,
|
|
19404
|
+
borderMode=cv2.BORDER_CONSTANT,
|
|
19405
|
+
borderValue=0
|
|
19406
|
+
).astype(bool)
|
|
19407
|
+
|
|
19408
|
+
if img_data.ndim == 2:
|
|
19409
|
+
img_data[raw_mask] = 0.0
|
|
19410
|
+
else:
|
|
19411
|
+
img_data[raw_mask, :] = 0.0
|
|
19412
|
+
return img_data
|
|
18959
19413
|
|
|
18960
19414
|
# ---- main loop over ENTRIES ----
|
|
18961
19415
|
# each entry: {"orig": <original path>, "aligned": <aligned path>, "chan": "R"/"G"/None}
|
|
@@ -19052,50 +19506,16 @@ class StackingSuiteDialog(QDialog):
|
|
|
19052
19506
|
f"canvas {int(ref_W * scale_factor)}×{int(ref_H * scale_factor)} @ {scale_factor}×"
|
|
19053
19507
|
)
|
|
19054
19508
|
|
|
19055
|
-
# ---- apply per-file rejections (
|
|
19056
|
-
|
|
19057
|
-
|
|
19058
|
-
|
|
19059
|
-
|
|
19060
|
-
|
|
19061
|
-
|
|
19062
|
-
|
|
19063
|
-
|
|
19064
|
-
|
|
19065
|
-
offsets = [(dx, dy) for dx in range(-r, r + 1) for dy in range(-r, r + 1)]
|
|
19066
|
-
if dilate_shape.startswith("dia"):
|
|
19067
|
-
offsets = [(dx, dy) for (dx, dy) in offsets if (abs(dx) + abs(dy) <= r)]
|
|
19068
|
-
|
|
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]
|
|
19074
|
-
|
|
19075
|
-
if pixels_are_registered:
|
|
19076
|
-
# Directly zero registered pixels
|
|
19077
|
-
for (x_r, y_r) in coords_for_this_file:
|
|
19078
|
-
for (ox, oy) in offsets:
|
|
19079
|
-
xr, yr = x_r + ox, y_r + oy
|
|
19080
|
-
if 0 <= xr < Wraw and 0 <= yr < Hraw:
|
|
19081
|
-
if img_data.ndim == 2:
|
|
19082
|
-
img_data[yr, xr] = 0.0
|
|
19083
|
-
else:
|
|
19084
|
-
img_data[yr, xr, :] = 0.0
|
|
19085
|
-
else:
|
|
19086
|
-
# Back-project via inverse affine (H_canvas)
|
|
19087
|
-
Hinv = np.linalg.inv(H_canvas)
|
|
19088
|
-
for (x_r, y_r) in coords_for_this_file:
|
|
19089
|
-
for (ox, oy) in offsets:
|
|
19090
|
-
xr, yr = x_r + ox, y_r + oy
|
|
19091
|
-
v = Hinv @ np.array([xr, yr, 1.0], np.float32)
|
|
19092
|
-
x_raw = int(round(v[0] / max(v[2], 1e-8)))
|
|
19093
|
-
y_raw = int(round(v[1] / max(v[2], 1e-8)))
|
|
19094
|
-
if 0 <= x_raw < Wraw and 0 <= y_raw < Hraw:
|
|
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
|
|
19509
|
+
# ---- apply per-file rejections (FAST: tile masks -> full mask -> optional warp) ----
|
|
19510
|
+
rej_mask_ref = _build_rej_mask_ref(aligned_file)
|
|
19511
|
+
if rej_mask_ref is not None:
|
|
19512
|
+
A23_for_mask = H_canvas[:2, :] # raw->ref affine (or identity)
|
|
19513
|
+
img_data = _apply_rejections_to_img(
|
|
19514
|
+
img_data=img_data,
|
|
19515
|
+
rej_mask_ref=rej_mask_ref,
|
|
19516
|
+
pixels_are_registered=pixels_are_registered,
|
|
19517
|
+
A23=A23_for_mask
|
|
19518
|
+
)
|
|
19099
19519
|
|
|
19100
19520
|
# ---- deposit ----
|
|
19101
19521
|
A23 = H_canvas[:2, :]
|
|
@@ -19131,24 +19551,24 @@ class StackingSuiteDialog(QDialog):
|
|
|
19131
19551
|
out_path_orig = self._build_out(self._master_light_dir(), base_stem, "fit")
|
|
19132
19552
|
|
|
19133
19553
|
hdr_orig = hdr.copy() if hdr is not None else fits.Header()
|
|
19134
|
-
hdr_orig["IMAGETYP"]
|
|
19554
|
+
hdr_orig["IMAGETYP"] = "MASTER STACK - DRIZZLE"
|
|
19135
19555
|
hdr_orig["DRIZFACTOR"] = (float(scale_factor), "Drizzle scale factor")
|
|
19136
|
-
hdr_orig["DROPFRAC"]
|
|
19137
|
-
hdr_orig["CREATOR"]
|
|
19138
|
-
hdr_orig["DATE-OBS"]
|
|
19556
|
+
hdr_orig["DROPFRAC"] = (float(drop_shrink), "Drizzle drop shrink/pixfrac")
|
|
19557
|
+
hdr_orig["CREATOR"] = "SetiAstroSuite"
|
|
19558
|
+
hdr_orig["DATE-OBS"] = datetime.utcnow().isoformat()
|
|
19139
19559
|
|
|
19140
19560
|
n_frames = int(len(file_list))
|
|
19141
19561
|
hdr_orig["NCOMBINE"] = (n_frames, "Number of frames combined")
|
|
19142
|
-
hdr_orig["NSTACK"]
|
|
19562
|
+
hdr_orig["NSTACK"] = (n_frames, "Alias of NCOMBINE (SetiAstro)")
|
|
19143
19563
|
|
|
19144
19564
|
if final_drizzle.ndim == 2:
|
|
19145
|
-
hdr_orig["NAXIS"]
|
|
19565
|
+
hdr_orig["NAXIS"] = 2
|
|
19146
19566
|
hdr_orig["NAXIS1"] = final_drizzle.shape[1]
|
|
19147
19567
|
hdr_orig["NAXIS2"] = final_drizzle.shape[0]
|
|
19148
19568
|
if "NAXIS3" in hdr_orig:
|
|
19149
19569
|
del hdr_orig["NAXIS3"]
|
|
19150
19570
|
else:
|
|
19151
|
-
hdr_orig["NAXIS"]
|
|
19571
|
+
hdr_orig["NAXIS"] = 3
|
|
19152
19572
|
hdr_orig["NAXIS1"] = final_drizzle.shape[1]
|
|
19153
19573
|
hdr_orig["NAXIS2"] = final_drizzle.shape[0]
|
|
19154
19574
|
hdr_orig["NAXIS3"] = final_drizzle.shape[2]
|
|
@@ -19175,7 +19595,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
19175
19595
|
rect_override=rect_override
|
|
19176
19596
|
)
|
|
19177
19597
|
hdr_crop["NCOMBINE"] = (n_frames, "Number of frames combined")
|
|
19178
|
-
hdr_crop["NSTACK"]
|
|
19598
|
+
hdr_crop["NSTACK"] = (n_frames, "Alias of NCOMBINE (SetiAstro)")
|
|
19179
19599
|
|
|
19180
19600
|
is_mono_crop = (cropped_drizzle.ndim == 2)
|
|
19181
19601
|
display_group_driz_crop = self._label_with_dims(group_key, cropped_drizzle.shape[1], cropped_drizzle.shape[0])
|
|
@@ -19196,6 +19616,7 @@ class StackingSuiteDialog(QDialog):
|
|
|
19196
19616
|
self._autocrop_outputs = []
|
|
19197
19617
|
self._autocrop_outputs.append((group_key, out_path_crop))
|
|
19198
19618
|
log(f"✂️ Drizzle (auto-cropped) saved: {out_path_crop}")
|
|
19619
|
+
|
|
19199
19620
|
return True
|
|
19200
19621
|
|
|
19201
19622
|
def _load_sasd_v2(self, path: str):
|
|
@@ -19310,7 +19731,8 @@ class StackingSuiteDialog(QDialog):
|
|
|
19310
19731
|
algo_override: str | None = None
|
|
19311
19732
|
):
|
|
19312
19733
|
import errno
|
|
19313
|
-
|
|
19734
|
+
collect_per_file = bool(self._get_drizzle_enabled())
|
|
19735
|
+
per_file_rejections = {f: [] for f in file_list} if collect_per_file else None
|
|
19314
19736
|
log = status_cb or (lambda *_: None)
|
|
19315
19737
|
log(f"Starting integration for group '{group_key}' with {len(file_list)} files.")
|
|
19316
19738
|
if not file_list:
|
|
@@ -19622,10 +20044,12 @@ class StackingSuiteDialog(QDialog):
|
|
|
19622
20044
|
rej_count[y0:y1, x0:x1] += trm.sum(axis=0).astype(np.uint16)
|
|
19623
20045
|
|
|
19624
20046
|
# per-file coords (existing behavior)
|
|
19625
|
-
|
|
19626
|
-
|
|
19627
|
-
|
|
19628
|
-
|
|
20047
|
+
if collect_per_file:
|
|
20048
|
+
for i, fpath in enumerate(file_list):
|
|
20049
|
+
m = trm[i]
|
|
20050
|
+
if np.any(m):
|
|
20051
|
+
# store as a tile mask, not coord list
|
|
20052
|
+
per_file_rejections[fpath].append((x0, y0, m.copy()))
|
|
19629
20053
|
|
|
19630
20054
|
# perf log
|
|
19631
20055
|
dt = time.perf_counter() - t0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: setiastrosuitepro
|
|
3
|
-
Version: 1.7.5
|
|
3
|
+
Version: 1.7.5.post1
|
|
4
4
|
Summary: Seti Astro Suite Pro - Advanced astrophotography toolkit for image calibration, stacking, registration, photometry, and visualization
|
|
5
5
|
License: GPL-3.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -183,7 +183,7 @@ setiastro/qml/ResourceMonitor.qml,sha256=k9_qXKAZLi8vj-5BffJTJu_UkRnxunZKn53Hthd
|
|
|
183
183
|
setiastro/saspro/__init__.py,sha256=6o8orhytzBWyJWBdG37xPcT6IVPZOG8d22FBVzn0Kac,902
|
|
184
184
|
setiastro/saspro/__main__.py,sha256=_zAHsw4SNWYzMXlswBuiMGgTUGYrU5p5-dsxhQ3_GWk,40789
|
|
185
185
|
setiastro/saspro/_generated/__init__.py,sha256=HbruQfKNbbVL4kh_t4oVG3UeUieaW8MUaqIcDCmnTvA,197
|
|
186
|
-
setiastro/saspro/_generated/build_info.py,sha256=
|
|
186
|
+
setiastro/saspro/_generated/build_info.py,sha256=05WXuNKXhrTskHfLfuo-I6cQRkffHcmX6SOeqDMBgSU,117
|
|
187
187
|
setiastro/saspro/abe.py,sha256=l6o0klT-TqJppzEjCBb9isyIvtuf_UsuMZV8JvqbgPI,59779
|
|
188
188
|
setiastro/saspro/abe_preset.py,sha256=u9t16yTb9v98tLjhvh496Fsp3Z-dNiBSs5itnAaJwh8,7750
|
|
189
189
|
setiastro/saspro/aberration_ai.py,sha256=8LtDu_KuqvL-W0MTUIJq9KguMjJPiUEQjMUibY16H-4,41673
|
|
@@ -200,7 +200,7 @@ setiastro/saspro/backgroundneutral.py,sha256=MFlBEwR65ASNXIY0S-0ZUaS89FCmNV_qafP
|
|
|
200
200
|
setiastro/saspro/batch_convert.py,sha256=O46KyB-DBZiTHokaWt_op1GDRr3juSaDSeFzP3pv1Zs,12377
|
|
201
201
|
setiastro/saspro/batch_renamer.py,sha256=PZe2MEwjg0n055UETIbwnwdfAux4heTVx5gr3qnNW5g,21900
|
|
202
202
|
setiastro/saspro/blemish_blaster.py,sha256=aiIpr-qpDmfaLZErUGUtwQgSTnaBHz3l14JvDWRfL3w,22983
|
|
203
|
-
setiastro/saspro/blink_comparator_pro.py,sha256=
|
|
203
|
+
setiastro/saspro/blink_comparator_pro.py,sha256=n4-OQWZ42yT1JfsbUXzUwFopA4-CTlp5kicrVvlJiW4,138274
|
|
204
204
|
setiastro/saspro/bundles.py,sha256=jWMYpalOcaMQ1QgikseOOmF7bBzDz9aykhRddaTsReQ,1892
|
|
205
205
|
setiastro/saspro/bundles_dock.py,sha256=IEAJEOEEe7V4NvsmClMygTf55LXN3SkV4e23K1erzkc,4441
|
|
206
206
|
setiastro/saspro/cheat_sheet.py,sha256=0bLmwY3GBWGcb4M7BBYzzrB_03XfqvZWAhQ2cBPavQ8,7852
|
|
@@ -333,7 +333,7 @@ setiastro/saspro/serviewer.py,sha256=QvPtJky2IzrywXaOYjeSZSNY0I64TSrzfgH7vRgGk7M
|
|
|
333
333
|
setiastro/saspro/sfcc.py,sha256=4wgXqH-E0OoSl_Ox4tMoExUHSnjosx2EnjjKeOXyZgo,89133
|
|
334
334
|
setiastro/saspro/shortcuts.py,sha256=QvFBXN_S8jqEwaP9m4pJMLVqzBmxo5HrjWhVCV9etQg,138256
|
|
335
335
|
setiastro/saspro/signature_insert.py,sha256=pWDxUO1Rxm27_fHSo2Y99bdOD2iG9q4AUjGR20x6TiA,70401
|
|
336
|
-
setiastro/saspro/stacking_suite.py,sha256
|
|
336
|
+
setiastro/saspro/stacking_suite.py,sha256=z3aTP10RfXjxilbzLAFBtJ37aPL3ZAKDOrxdVIASmdk,895924
|
|
337
337
|
setiastro/saspro/star_alignment.py,sha256=lRpDFvDKsMluu2P_kotQ9OumlnGtEWwGE7Gm-ZYccm8,326894
|
|
338
338
|
setiastro/saspro/star_alignment_preset.py,sha256=1QLRRUsVGr3-iX4kKvV9s-NuDRJVnRQat8qdTIs1nao,13164
|
|
339
339
|
setiastro/saspro/star_metrics.py,sha256=LRfBdqjwJJ9iz_paY-CkhPIqt2MxoXARLUy73ZKTA_0,1503
|
|
@@ -409,9 +409,9 @@ setiastro/saspro/wimi.py,sha256=CNo833Pur9P-A1DUSntlAaQWekf6gzWIvetOMvLDrOw,3146
|
|
|
409
409
|
setiastro/saspro/wims.py,sha256=HDfVI3Ckf5OJEJLH8NI36pFc2USZnETpb4UDIvweNX4,27450
|
|
410
410
|
setiastro/saspro/window_shelf.py,sha256=hpId8uRPq7-UZ3dWW5YzH_v4TchQokGFoPGM8dyaIZA,9448
|
|
411
411
|
setiastro/saspro/xisf.py,sha256=Ah1CXDAohN__ej1Lq7LPU8vGLnDz8fluLQTGE71aUoc,52669
|
|
412
|
-
setiastrosuitepro-1.7.5.dist-info/entry_points.txt,sha256=g7cHWhUSiIP7mkyByG9JXGWWlHKeVC2vL7zvB9U-vEU,236
|
|
413
|
-
setiastrosuitepro-1.7.5.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
414
|
-
setiastrosuitepro-1.7.5.dist-info/licenses/license.txt,sha256=KCwYZ9VpVwmzjelDq1BzzWqpBvt9nbbapa-woz47hfQ,123930
|
|
415
|
-
setiastrosuitepro-1.7.5.dist-info/METADATA,sha256
|
|
416
|
-
setiastrosuitepro-1.7.5.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
417
|
-
setiastrosuitepro-1.7.5.dist-info/RECORD,,
|
|
412
|
+
setiastrosuitepro-1.7.5.post1.dist-info/entry_points.txt,sha256=g7cHWhUSiIP7mkyByG9JXGWWlHKeVC2vL7zvB9U-vEU,236
|
|
413
|
+
setiastrosuitepro-1.7.5.post1.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
414
|
+
setiastrosuitepro-1.7.5.post1.dist-info/licenses/license.txt,sha256=KCwYZ9VpVwmzjelDq1BzzWqpBvt9nbbapa-woz47hfQ,123930
|
|
415
|
+
setiastrosuitepro-1.7.5.post1.dist-info/METADATA,sha256=-drTlx3V1woZS9yNTvmVFOoJukIVfNd5X_PnRpKlRv8,9925
|
|
416
|
+
setiastrosuitepro-1.7.5.post1.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
417
|
+
setiastrosuitepro-1.7.5.post1.dist-info/RECORD,,
|
|
File without changes
|
{setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/licenses/license.txt
RENAMED
|
File without changes
|