setiastrosuitepro 1.7.5__py3-none-any.whl → 1.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (29) hide show
  1. setiastro/saspro/_generated/build_info.py +2 -2
  2. setiastro/saspro/accel_installer.py +21 -8
  3. setiastro/saspro/accel_workers.py +11 -12
  4. setiastro/saspro/blink_comparator_pro.py +146 -2
  5. setiastro/saspro/comet_stacking.py +113 -85
  6. setiastro/saspro/cosmicclarity.py +604 -826
  7. setiastro/saspro/cosmicclarity_engines/benchmark_engine.py +715 -0
  8. setiastro/saspro/cosmicclarity_engines/darkstar_engine.py +576 -0
  9. setiastro/saspro/cosmicclarity_engines/denoise_engine.py +567 -0
  10. setiastro/saspro/cosmicclarity_engines/satellite_engine.py +620 -0
  11. setiastro/saspro/cosmicclarity_engines/sharpen_engine.py +587 -0
  12. setiastro/saspro/cosmicclarity_engines/superres_engine.py +412 -0
  13. setiastro/saspro/gui/main_window.py +14 -0
  14. setiastro/saspro/gui/mixins/menu_mixin.py +2 -0
  15. setiastro/saspro/model_manager.py +306 -0
  16. setiastro/saspro/model_workers.py +65 -0
  17. setiastro/saspro/ops/benchmark.py +320 -0
  18. setiastro/saspro/ops/settings.py +308 -9
  19. setiastro/saspro/remove_stars.py +424 -442
  20. setiastro/saspro/resources.py +73 -10
  21. setiastro/saspro/runtime_torch.py +107 -22
  22. setiastro/saspro/signature_insert.py +14 -3
  23. setiastro/saspro/stacking_suite.py +539 -115
  24. {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.dist-info}/METADATA +2 -1
  25. {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.dist-info}/RECORD +29 -20
  26. {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.dist-info}/WHEEL +0 -0
  27. {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.dist-info}/entry_points.txt +0 -0
  28. {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.dist-info}/licenses/LICENSE +0 -0
  29. {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.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-22T14:55:21Z"
3
- APP_VERSION = "1.7.5"
2
+ BUILD_TIMESTAMP = "2026-01-26T16:03:43Z"
3
+ APP_VERSION = "1.8.0"
@@ -1,4 +1,4 @@
1
- # pro/accel_installer.py
1
+ # saspro/accel_installer.py
2
2
  from __future__ import annotations
3
3
  import platform
4
4
  import subprocess
@@ -77,21 +77,34 @@ def _nvidia_driver_ok(log_cb: LogCB) -> bool:
77
77
  return False
78
78
 
79
79
 
80
- def ensure_torch_installed(prefer_gpu: bool, log_cb: LogCB) -> tuple[bool, Optional[str]]:
80
+ def ensure_torch_installed(prefer_gpu: bool, log_cb: LogCB, preferred_backend: str = "auto") -> tuple[bool, Optional[str]]:
81
+
81
82
  try:
83
+ preferred_backend = (preferred_backend or "auto").lower()
82
84
  is_windows = platform.system() == "Windows"
85
+
83
86
  has_nv = _has_nvidia() and platform.system() in ("Windows", "Linux")
84
87
  has_intel = (not has_nv) and _has_intel_arc() and platform.system() in ("Windows", "Linux")
85
88
 
86
- prefer_cuda = prefer_gpu and has_nv
87
- prefer_xpu = prefer_gpu and (not has_nv) and has_intel
89
+ prefer_cuda = prefer_gpu and (preferred_backend in ("auto", "cuda")) and has_nv
90
+ prefer_xpu = prefer_gpu and (preferred_backend in ("auto", "xpu")) and (not has_nv) and has_intel
88
91
 
89
- if prefer_cuda and not _nvidia_driver_ok(log_cb):
90
- log_cb("CUDA requested but NVIDIA driver not detected/working; CUDA wheels may not initialize.")
91
- log_cb(f"PyTorch install preference: prefer_cuda={prefer_cuda}, prefer_xpu={prefer_xpu} (OS={platform.system()})")
92
+ # NEW: DirectML preference is ONLY meaningful on Windows with no NVIDIA
93
+ prefer_dml = (
94
+ is_windows
95
+ and (not has_nv)
96
+ and preferred_backend in ("directml", "auto")
97
+ and prefer_gpu
98
+ and (not prefer_xpu) # if Intel XPU actually works, prefer that over DML
99
+ )
100
+
101
+ # If user forced CPU, disable everything
102
+ if preferred_backend == "cpu":
103
+ prefer_cuda = prefer_xpu = prefer_dml = False
92
104
 
93
105
  # Install torch (tries CUDA → XPU → CPU)
94
- torch = import_torch(prefer_cuda=prefer_cuda, prefer_xpu=prefer_xpu, status_cb=log_cb)
106
+ torch = import_torch(prefer_cuda=prefer_cuda, prefer_xpu=prefer_xpu, prefer_dml=prefer_dml, status_cb=log_cb)
107
+
95
108
 
96
109
  cuda_ok = bool(getattr(torch, "cuda", None) and torch.cuda.is_available())
97
110
  xpu_ok = bool(hasattr(torch, "xpu") and torch.xpu.is_available())
@@ -1,25 +1,24 @@
1
- # pro/accel_workers.py
1
+ # saspro/accel_workers.py
2
2
  from __future__ import annotations
3
3
  from PyQt6.QtCore import QObject, pyqtSignal, QThread
4
4
  from setiastro.saspro.accel_installer import ensure_torch_installed
5
5
 
6
6
  class AccelInstallWorker(QObject):
7
- progress = pyqtSignal(str) # emitted from worker thread; GUI updates must connect with QueuedConnection
7
+ progress = pyqtSignal(str) # emitted from worker thread
8
8
  finished = pyqtSignal(bool, str) # (ok, message)
9
9
 
10
- def __init__(self, prefer_gpu: bool = True):
10
+ def __init__(self, prefer_gpu: bool = True, preferred_backend: str = "auto"):
11
11
  super().__init__()
12
- self.prefer_gpu = prefer_gpu
13
-
14
- def _log(self, s: str):
15
- # Never touch widgets here; just emit text
16
- self.progress.emit(s)
12
+ self.prefer_gpu = bool(prefer_gpu)
13
+ self.preferred_backend = (preferred_backend or "auto").lower()
17
14
 
18
15
  def run(self):
19
- # pure backend work; no QWidget/QMessageBox etc. in this method
20
- ok, msg = ensure_torch_installed(self.prefer_gpu, self._log)
16
+ ok, msg = ensure_torch_installed(
17
+ prefer_gpu=self.prefer_gpu,
18
+ preferred_backend=self.preferred_backend,
19
+ log_cb=self.progress.emit
20
+ )
21
21
 
22
- # honor cancellation if requested
23
22
  if QThread.currentThread().isInterruptionRequested():
24
23
  self.finished.emit(False, "Canceled.")
25
24
  return
@@ -27,4 +26,4 @@ class AccelInstallWorker(QObject):
27
26
  if ok:
28
27
  self.finished.emit(True, "PyTorch installed and ready.")
29
28
  else:
30
- self.finished.emit(False, msg or "Installation failed.")
29
+ self.finished.emit(False, msg or "Installation failed.")
@@ -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):
@@ -13,15 +13,52 @@ from functools import lru_cache
13
13
  from astropy.io import fits
14
14
  from astropy.stats import sigma_clipped_stats
15
15
  import sep
16
- from setiastro.saspro.remove_stars import (
17
- _get_setting_any,
18
- _mtf_params_linked, _apply_mtf_linked_rgb, _invert_mtf_linked_rgb,
19
- _resolve_darkstar_exe, _ensure_exec_bit, _purge_darkstar_io
20
-
16
+ from setiastro.saspro.cosmicclarity_engines.darkstar_engine import (
17
+ darkstar_starremoval_rgb01,
18
+ DarkStarParams,
21
19
  )
20
+
21
+
22
22
  from setiastro.saspro.legacy.image_manager import (load_image, save_image)
23
23
  from setiastro.saspro.imageops.stretch import stretch_color_image, stretch_mono_image
24
24
 
25
+ def _get_setting_any(settings, keys, default=None):
26
+ """
27
+ Try multiple keys against QSettings-like or dict-like settings.
28
+ keys: iterable of strings
29
+ """
30
+ if settings is None:
31
+ return default
32
+
33
+ # QSettings / object with .value()
34
+ if hasattr(settings, "value") and callable(getattr(settings, "value")):
35
+ for k in keys:
36
+ try:
37
+ v = settings.value(k, None)
38
+ except Exception:
39
+ v = None
40
+ if v not in (None, "", "None"):
41
+ return v
42
+ return default
43
+
44
+ # dict-like
45
+ if isinstance(settings, dict):
46
+ for k in keys:
47
+ if k in settings and settings[k] not in (None, "", "None"):
48
+ return settings[k]
49
+ return default
50
+
51
+ # fallback: attribute lookup
52
+ for k in keys:
53
+ try:
54
+ v = getattr(settings, k)
55
+ except Exception:
56
+ v = None
57
+ if v not in (None, "", "None"):
58
+ return v
59
+ return default
60
+
61
+
25
62
  def _blackpoint_nonzero(img_norm: np.ndarray, p: float = 0.1) -> float:
26
63
  """Scalar blackpoint from non-zero pixels across all channels (linked).
27
64
  p in [0..100]: small percentile to resist outliers; use 0 for strict min."""
@@ -66,6 +103,18 @@ def _u16_to_float01(x: np.ndarray) -> np.ndarray:
66
103
 
67
104
  # comet_stacking.py (or wherever this lives)
68
105
 
106
+ def _ensure_exec_bit(path: str) -> None:
107
+ try:
108
+ if os.name == "nt":
109
+ return
110
+ p = os.path.realpath(path)
111
+ st = os.stat(p)
112
+ if not (st.st_mode & 0o111):
113
+ os.chmod(p, st.st_mode | 0o111)
114
+ except Exception:
115
+ return
116
+
117
+
69
118
  def starnet_starless_pair_from_array(
70
119
  src_rgb01,
71
120
  settings,
@@ -211,78 +260,42 @@ def starnet_starless_pair_from_array(
211
260
  return np.clip(protected_unstretch, 0.0, 1.0), np.clip(protected_unstretch, 0.0, 1.0)
212
261
 
213
262
 
214
-
215
- def darkstar_starless_from_array(src_rgb01: np.ndarray, settings, **_ignored) -> np.ndarray:
263
+ def darkstar_starless_from_array(
264
+ src_rgb01: np.ndarray,
265
+ settings,
266
+ *,
267
+ use_gpu: bool = True,
268
+ chunk_size: int = 512,
269
+ overlap_frac: float = 0.125,
270
+ mode: str = "unscreen",
271
+ progress_cb=None,
272
+ status_cb=None,
273
+ ) -> np.ndarray:
216
274
  """
217
- Headless CosmicClarity DarkStar run for a single RGB frame.
218
- Returns starless RGB in [0..1]. Uses CC’s input/output folders.
275
+ In-process Dark Star removal via darkstar_engine.py
276
+ Input: float32 [0..1], HxW or HxWx1 or HxWx3
277
+ Output: float32 [0..1], same "mono-ness" behavior as engine (HxWx1 if mono input)
219
278
  """
220
- # normalize channels
221
- img = src_rgb01.astype(np.float32, copy=False)
222
- # Delay expansion: if it's 2D/Mono, send it as-is if DarkStar supports it,
223
- # but DarkStar expects 3-channel TIF usually.
224
- # We'll just expand for the save call, not "in place" if possible.
225
- # Actually DarkStar runner saves `img` directly.
226
- # So we'll expand just for that save to avoid holding 2 copies in memory.
227
- if img.ndim == 2:
228
- img_to_save = np.stack([img]*3, axis=-1)
229
- elif img.ndim == 3 and img.shape[2] == 1:
230
- img_to_save = np.repeat(img, 3, axis=2)
231
- else:
232
- img_to_save = img
233
-
234
- # resolve exe and base folder
235
- exe, base = _resolve_darkstar_exe(type("Dummy", (), {"settings": settings})())
236
- if not exe or not base:
237
- raise RuntimeError("Cosmic Clarity DarkStar executable path is not set.")
238
-
239
- _ensure_exec_bit(exe)
240
-
241
- input_dir = os.path.join(base, "input")
242
- output_dir = os.path.join(base, "output")
243
- os.makedirs(input_dir, exist_ok=True)
244
- os.makedirs(output_dir, exist_ok=True)
245
-
246
- # purge any prior files (safe; scoped to imagetoremovestars*)
247
- _purge_darkstar_io(base, prefix="imagetoremovestars", clear_input=True, clear_output=True)
248
-
249
- in_path = os.path.join(input_dir, "imagetoremovestars.tif")
250
- out_path = os.path.join(output_dir, "imagetoremovestars_starless.tif")
251
-
252
- # save input as float32 TIFF
253
- # save input as float32 TIFF
254
- save_image(img_to_save, in_path, original_format="tif", bit_depth="32-bit floating point",
255
- original_header=None, is_mono=False, image_meta=None, file_meta=None)
256
-
257
- # build command (SASv2 parity): default unscreen, show extracted stars off, stride 512
258
- cmd = [exe, "--star_removal_mode", "unscreen", "--chunk_size", "512"]
259
-
260
- rc = subprocess.call(cmd, cwd=output_dir)
261
- if rc != 0 or not os.path.exists(out_path):
262
- try: os.remove(in_path)
263
- except Exception as e:
264
- import logging
265
- logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
266
- raise RuntimeError(f"DarkStar failed (rc={rc}).")
267
-
268
- starless, _, _, _ = load_image(out_path)
269
- # cleanup
270
- try:
271
- os.remove(in_path)
272
- os.remove(out_path)
273
- _purge_darkstar_io(base, prefix="imagetoremovestars", clear_input=True, clear_output=True)
274
- except Exception:
275
- pass
276
-
277
- if starless is None:
278
- raise RuntimeError("DarkStar produced no output.")
279
+ if progress_cb is None:
280
+ progress_cb = lambda done, total, stage: None
281
+ if status_cb is None:
282
+ status_cb = lambda msg: None
283
+
284
+ img = np.asarray(src_rgb01, np.float32)
285
+ params = DarkStarParams(
286
+ use_gpu=bool(use_gpu),
287
+ chunk_size=int(chunk_size),
288
+ overlap_frac=float(overlap_frac),
289
+ mode=str(mode),
290
+ output_stars_only=False,
291
+ )
279
292
 
280
- # Delayed expansion
281
- if starless.ndim == 2:
282
- starless = np.stack([starless]*3, axis=-1)
283
- elif starless.ndim == 3 and starless.shape[2] == 1:
284
- starless = np.repeat(starless, 3, axis=2)
285
-
293
+ starless, _stars_only, _was_mono = darkstar_starremoval_rgb01(
294
+ img,
295
+ params=params,
296
+ progress_cb=progress_cb,
297
+ status_cb=status_cb,
298
+ )
286
299
  return np.clip(starless.astype(np.float32, copy=False), 0.0, 1.0)
287
300
 
288
301
  # ---------- small helpers ----------
@@ -1106,16 +1119,37 @@ def _starless_frame_for_comet(img: np.ndarray,
1106
1119
 
1107
1120
  # run
1108
1121
  if tool == "CosmicClarityDarkStar":
1109
- # DarkStar returns in the same domain we fed in.
1110
- base_for_mask = src
1111
- starless = darkstar_starless_from_array(src, settings)
1122
+ base_for_mask = src # RGB [0..1]
1123
+
1124
+ # optional: forward progress from comet stacking if you want
1125
+ def _ds_progress(done, total, stage):
1126
+ # stage is like "Dark Star removal"
1127
+ # you can route to log or status_cb; keep lightweight
1128
+ pass
1129
+
1130
+ starless = darkstar_starless_from_array(
1131
+ src,
1132
+ settings,
1133
+ use_gpu=True,
1134
+ chunk_size=512,
1135
+ overlap_frac=0.125,
1136
+ mode="unscreen",
1137
+ progress_cb=_ds_progress,
1138
+ status_cb=None, # or log
1139
+ )
1140
+
1141
+ # DarkStar may return HxWx1 for mono-ish inputs — expand to RGB for nucleus protection
1142
+ if starless.ndim == 2:
1143
+ starless = np.repeat(starless[..., None], 3, axis=2)
1144
+ elif starless.ndim == 3 and starless.shape[2] == 1:
1145
+ starless = np.repeat(starless, 3, axis=2)
1112
1146
 
1113
- # protect nucleus (blend original back where mask=1), in *current* domain
1114
1147
  m = core_mask.astype(np.float32)
1115
1148
  m3 = np.repeat(m[..., None], 3, axis=2)
1116
1149
  protected = starless * (1.0 - m3) + base_for_mask * m3
1117
1150
  return np.clip(protected, 0.0, 1.0)
1118
1151
 
1152
+
1119
1153
  else:
1120
1154
  # StarNet path: do mask-blend inside the function (in its stretched domain)
1121
1155
  protected, _ = starnet_starless_pair_from_array(
@@ -1302,13 +1336,7 @@ class CometCentroidPreview(QDialog):
1302
1336
  def _prep_slider(self, s, lo, hi, val):
1303
1337
  s.setRange(lo, hi); s.setValue(val); s.setSingleStep(1); s.setPageStep(5)
1304
1338
 
1305
- def eventFilter(self, obj, ev):
1306
- if obj is self.view.viewport() and ev.type() == QEvent.Type.MouseButtonPress:
1307
- if ev.button() == Qt.MouseButton.LeftButton:
1308
- pos = self.view.mapToScene(ev.position().toPoint())
1309
- self._set_xy_current(pos.x(), pos.y())
1310
- return True
1311
- return super().eventFilter(obj, ev)
1339
+
1312
1340
 
1313
1341
  def keyPressEvent(self, ev):
1314
1342
  k = ev.key()