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.

Binary file
@@ -1,3 +1,3 @@
1
1
  # Auto-generated at build time. Do not edit.
2
- BUILD_TIMESTAMP = "2026-01-21T15:42:11Z"
3
- APP_VERSION = "1.7.4"
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
- main = QVBoxLayout(self)
277
- main.addWidget(self.scroll)
278
- zoom_bar = QHBoxLayout()
279
- zoom_bar.addStretch()
280
- zoom_bar.addWidget(self.btn_zoom_out)
281
- zoom_bar.addWidget(self.btn_zoom_in)
282
- zoom_bar.addWidget(self.btn_zoom_fit)
283
- zoom_bar.addStretch()
284
- main.addLayout(zoom_bar)
285
- main.addWidget(ctrls)
286
- main.addLayout(bb)
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):