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.
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/accel_installer.py +21 -8
- setiastro/saspro/accel_workers.py +11 -12
- setiastro/saspro/blink_comparator_pro.py +146 -2
- setiastro/saspro/comet_stacking.py +113 -85
- setiastro/saspro/cosmicclarity.py +604 -826
- setiastro/saspro/cosmicclarity_engines/benchmark_engine.py +715 -0
- setiastro/saspro/cosmicclarity_engines/darkstar_engine.py +576 -0
- setiastro/saspro/cosmicclarity_engines/denoise_engine.py +567 -0
- setiastro/saspro/cosmicclarity_engines/satellite_engine.py +620 -0
- setiastro/saspro/cosmicclarity_engines/sharpen_engine.py +587 -0
- setiastro/saspro/cosmicclarity_engines/superres_engine.py +412 -0
- setiastro/saspro/gui/main_window.py +14 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +2 -0
- setiastro/saspro/model_manager.py +306 -0
- setiastro/saspro/model_workers.py +65 -0
- setiastro/saspro/ops/benchmark.py +320 -0
- setiastro/saspro/ops/settings.py +308 -9
- setiastro/saspro/remove_stars.py +424 -442
- setiastro/saspro/resources.py +73 -10
- setiastro/saspro/runtime_torch.py +107 -22
- setiastro/saspro/signature_insert.py +14 -3
- setiastro/saspro/stacking_suite.py +539 -115
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.dist-info}/METADATA +2 -1
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.dist-info}/RECORD +29 -20
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.7.5.dist-info → setiastrosuitepro-1.8.0.dist-info}/licenses/LICENSE +0 -0
- {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-
|
|
3
|
-
APP_VERSION = "1.
|
|
2
|
+
BUILD_TIMESTAMP = "2026-01-26T16:03:43Z"
|
|
3
|
+
APP_VERSION = "1.8.0"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
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.
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
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
|
-
|
|
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()
|