setiastrosuitepro 1.7.4__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/images/clonestamp.png +0 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/blemish_blaster.py +54 -14
- setiastro/saspro/blink_comparator_pro.py +146 -2
- setiastro/saspro/clone_stamp.py +753 -0
- setiastro/saspro/gui/main_window.py +22 -1
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +8 -13
- setiastro/saspro/resources.py +2 -0
- setiastro/saspro/stacking_suite.py +646 -164
- {setiastrosuitepro-1.7.4.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.7.4.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/RECORD +16 -14
- {setiastrosuitepro-1.7.4.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.4.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.7.4.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.7.4.dist-info → setiastrosuitepro-1.7.5.post1.dist-info}/licenses/license.txt +0 -0
|
Binary file
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Auto-generated at build time. Do not edit.
|
|
2
|
-
BUILD_TIMESTAMP = "2026-01-
|
|
3
|
-
APP_VERSION = "1.7.
|
|
2
|
+
BUILD_TIMESTAMP = "2026-01-24T17:02:32Z"
|
|
3
|
+
APP_VERSION = "1.7.5.post1"
|
|
@@ -6,8 +6,8 @@ from typing import Optional
|
|
|
6
6
|
from PyQt6.QtCore import Qt, QEvent, QPointF, QRunnable, QThreadPool, pyqtSlot, QObject, pyqtSignal
|
|
7
7
|
from PyQt6.QtGui import QImage, QPixmap, QPen, QBrush, QAction, QKeySequence, QColor, QWheelEvent, QIcon
|
|
8
8
|
from PyQt6.QtWidgets import (
|
|
9
|
-
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, QLabel, QPushButton, QSlider,
|
|
10
|
-
QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsEllipseItem, QMessageBox, QScrollArea, QCheckBox, QDoubleSpinBox
|
|
9
|
+
QDialog, QVBoxLayout, QHBoxLayout, QFormLayout, QGroupBox, QLabel, QPushButton, QSlider, QSizePolicy,
|
|
10
|
+
QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QGraphicsEllipseItem, QMessageBox, QScrollArea, QCheckBox, QDoubleSpinBox, QWidget, QFrame
|
|
11
11
|
)
|
|
12
12
|
from setiastro.saspro.imageops.stretch import stretch_color_image, stretch_mono_image
|
|
13
13
|
|
|
@@ -209,7 +209,7 @@ class BlemishBlasterDialogPro(QDialog):
|
|
|
209
209
|
self.scroll = QScrollArea(self)
|
|
210
210
|
self.scroll.setWidgetResizable(True)
|
|
211
211
|
self.scroll.setWidget(self.view)
|
|
212
|
-
|
|
212
|
+
self.scroll.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
|
|
213
213
|
# --- Zoom controls (buttons) ---------------------------------
|
|
214
214
|
# --- Zoom controls (standard themed toolbuttons) ---------------
|
|
215
215
|
self._zoom = 1.0 # initial zoom factor
|
|
@@ -273,17 +273,57 @@ class BlemishBlasterDialogPro(QDialog):
|
|
|
273
273
|
bb.addWidget(self.btn_apply)
|
|
274
274
|
bb.addWidget(self.btn_close)
|
|
275
275
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
276
|
+
# ─────────────────────────────────────────────────────────────
|
|
277
|
+
# Layout: Left = Controls, Right = Preview (so preview gets height)
|
|
278
|
+
# ─────────────────────────────────────────────────────────────
|
|
279
|
+
root = QHBoxLayout(self)
|
|
280
|
+
root.setContentsMargins(8, 8, 8, 8)
|
|
281
|
+
root.setSpacing(10)
|
|
282
|
+
|
|
283
|
+
# ---- LEFT: Zoom + Controls + Buttons (scrollable on small screens) ----
|
|
284
|
+
left = QVBoxLayout()
|
|
285
|
+
left.setSpacing(10)
|
|
286
|
+
|
|
287
|
+
zoom_box = QGroupBox(self.tr("Zoom"))
|
|
288
|
+
zoom_lay = QHBoxLayout(zoom_box)
|
|
289
|
+
zoom_lay.addStretch(1)
|
|
290
|
+
zoom_lay.addWidget(self.btn_zoom_out)
|
|
291
|
+
zoom_lay.addWidget(self.btn_zoom_in)
|
|
292
|
+
zoom_lay.addWidget(self.btn_zoom_fit)
|
|
293
|
+
zoom_lay.addStretch(1)
|
|
294
|
+
|
|
295
|
+
left.addWidget(zoom_box)
|
|
296
|
+
left.addWidget(ctrls, 0)
|
|
297
|
+
|
|
298
|
+
btn_row = QWidget(self)
|
|
299
|
+
btn_row.setLayout(bb)
|
|
300
|
+
left.addWidget(btn_row, 0)
|
|
301
|
+
|
|
302
|
+
left.addStretch(1)
|
|
303
|
+
|
|
304
|
+
left_widget = QWidget(self)
|
|
305
|
+
left_widget.setLayout(left)
|
|
306
|
+
|
|
307
|
+
left_scroll = QScrollArea(self)
|
|
308
|
+
left_scroll.setWidgetResizable(True)
|
|
309
|
+
left_scroll.setMinimumWidth(320) # tweak 300–360
|
|
310
|
+
left_scroll.setWidget(left_widget)
|
|
311
|
+
|
|
312
|
+
# ---- vertical separator ----
|
|
313
|
+
sep = QFrame(self)
|
|
314
|
+
sep.setFrameShape(QFrame.Shape.VLine)
|
|
315
|
+
sep.setFrameShadow(QFrame.Shadow.Sunken)
|
|
316
|
+
|
|
317
|
+
# ---- RIGHT: Preview ----
|
|
318
|
+
right = QVBoxLayout()
|
|
319
|
+
right.setSpacing(8)
|
|
320
|
+
right.addWidget(self.scroll, 1)
|
|
321
|
+
|
|
322
|
+
# Add in order: controls left, preview right
|
|
323
|
+
root.addWidget(left_scroll, 0)
|
|
324
|
+
root.addWidget(sep)
|
|
325
|
+
root.addLayout(right, 1)
|
|
326
|
+
|
|
287
327
|
|
|
288
328
|
# behavior
|
|
289
329
|
self._threadpool = QThreadPool.globalInstance()
|
|
@@ -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):
|