setiastrosuitepro 1.6.4__py3-none-any.whl → 1.7.1.post2__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/TextureClarity.svg +56 -0
- setiastro/images/abeicon.svg +16 -0
- setiastro/images/acv_icon.png +0 -0
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/cosmic.svg +40 -0
- setiastro/images/cosmicsat.svg +24 -0
- setiastro/images/first_quarter.png +0 -0
- setiastro/images/full_moon.png +0 -0
- setiastro/images/graxpert.svg +19 -0
- setiastro/images/last_quarter.png +0 -0
- setiastro/images/linearfit.svg +32 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/new_moon.png +0 -0
- setiastro/images/pixelmath.svg +42 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/images/waning_crescent_1.png +0 -0
- setiastro/images/waning_crescent_2.png +0 -0
- setiastro/images/waning_crescent_3.png +0 -0
- setiastro/images/waning_crescent_4.png +0 -0
- setiastro/images/waning_crescent_5.png +0 -0
- setiastro/images/waning_gibbous_1.png +0 -0
- setiastro/images/waning_gibbous_2.png +0 -0
- setiastro/images/waning_gibbous_3.png +0 -0
- setiastro/images/waning_gibbous_4.png +0 -0
- setiastro/images/waning_gibbous_5.png +0 -0
- setiastro/images/waxing_crescent_1.png +0 -0
- setiastro/images/waxing_crescent_2.png +0 -0
- setiastro/images/waxing_crescent_3.png +0 -0
- setiastro/images/waxing_crescent_4.png +0 -0
- setiastro/images/waxing_crescent_5.png +0 -0
- setiastro/images/waxing_gibbous_1.png +0 -0
- setiastro/images/waxing_gibbous_2.png +0 -0
- setiastro/images/waxing_gibbous_3.png +0 -0
- setiastro/images/waxing_gibbous_4.png +0 -0
- setiastro/images/waxing_gibbous_5.png +0 -0
- setiastro/qml/ResourceMonitor.qml +84 -82
- setiastro/saspro/__main__.py +20 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/abe.py +37 -4
- setiastro/saspro/aberration_ai.py +364 -33
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/acv_exporter.py +379 -0
- setiastro/saspro/add_stars.py +33 -6
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/backgroundneutral.py +108 -40
- setiastro/saspro/blemish_blaster.py +4 -1
- setiastro/saspro/blink_comparator_pro.py +150 -55
- setiastro/saspro/clahe.py +4 -1
- setiastro/saspro/continuum_subtract.py +4 -1
- setiastro/saspro/convo.py +13 -7
- setiastro/saspro/cosmicclarity.py +129 -18
- setiastro/saspro/crop_dialog_pro.py +123 -7
- setiastro/saspro/curve_editor_pro.py +181 -64
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +245 -15
- setiastro/saspro/exoplanet_detector.py +120 -28
- setiastro/saspro/frequency_separation.py +1158 -204
- setiastro/saspro/ghs_dialog_pro.py +81 -16
- setiastro/saspro/graxpert.py +1 -0
- setiastro/saspro/gui/main_window.py +706 -264
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +35 -1
- setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
- setiastro/saspro/gui/mixins/toolbar_mixin.py +499 -24
- setiastro/saspro/gui/mixins/update_mixin.py +138 -36
- setiastro/saspro/gui/mixins/view_mixin.py +42 -0
- setiastro/saspro/halobgon.py +4 -0
- setiastro/saspro/histogram.py +184 -8
- setiastro/saspro/image_combine.py +4 -0
- setiastro/saspro/image_peeker_pro.py +4 -0
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +582 -62
- setiastro/saspro/isophote.py +4 -0
- setiastro/saspro/layers.py +13 -9
- setiastro/saspro/layers_dock.py +183 -3
- setiastro/saspro/legacy/image_manager.py +154 -20
- setiastro/saspro/legacy/numba_utils.py +68 -48
- setiastro/saspro/legacy/xisf.py +240 -98
- setiastro/saspro/live_stacking.py +203 -82
- setiastro/saspro/luminancerecombine.py +228 -27
- setiastro/saspro/mask_creation.py +174 -15
- setiastro/saspro/mfdeconv.py +113 -35
- setiastro/saspro/mfdeconvcudnn.py +119 -70
- setiastro/saspro/mfdeconvsport.py +112 -35
- setiastro/saspro/morphology.py +4 -0
- setiastro/saspro/multiscale_decomp.py +81 -29
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +72 -57
- setiastro/saspro/ops/commands.py +18 -18
- setiastro/saspro/ops/script_editor.py +10 -2
- setiastro/saspro/ops/scripts.py +122 -0
- setiastro/saspro/perfect_palette_picker.py +37 -3
- setiastro/saspro/plate_solver.py +84 -49
- setiastro/saspro/psf_viewer.py +119 -37
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +73 -0
- setiastro/saspro/rgbalign.py +460 -12
- setiastro/saspro/selective_color.py +4 -1
- setiastro/saspro/ser_stack_config.py +82 -0
- setiastro/saspro/ser_stacker.py +2321 -0
- setiastro/saspro/ser_stacker_dialog.py +1838 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1625 -0
- setiastro/saspro/sfcc.py +662 -216
- setiastro/saspro/shortcuts.py +171 -33
- setiastro/saspro/signature_insert.py +692 -33
- setiastro/saspro/stacking_suite.py +1347 -485
- setiastro/saspro/star_alignment.py +247 -123
- setiastro/saspro/star_spikes.py +4 -0
- setiastro/saspro/star_stretch.py +38 -3
- setiastro/saspro/stat_stretch.py +892 -129
- setiastro/saspro/subwindow.py +787 -363
- setiastro/saspro/supernovaasteroidhunter.py +1 -1
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/wavescale_hdr.py +4 -1
- setiastro/saspro/wavescalede.py +4 -1
- setiastro/saspro/whitebalance.py +84 -12
- setiastro/saspro/widgets/common_utilities.py +28 -21
- setiastro/saspro/widgets/resource_monitor.py +209 -111
- setiastro/saspro/widgets/spinboxes.py +10 -13
- setiastro/saspro/wimi.py +27 -656
- setiastro/saspro/wims.py +13 -3
- setiastro/saspro/xisf.py +101 -11
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +4 -2
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +132 -87
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.4.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -7,13 +7,37 @@ import numpy as np
|
|
|
7
7
|
import sys
|
|
8
8
|
import platform # add
|
|
9
9
|
import time
|
|
10
|
+
import subprocess
|
|
10
11
|
|
|
11
12
|
IS_APPLE_ARM = (sys.platform == "darwin" and platform.machine() == "arm64")
|
|
12
13
|
|
|
14
|
+
def _has_nvidia_gpu() -> bool:
|
|
15
|
+
"""Check if system has an NVIDIA GPU (Linux/Windows)."""
|
|
16
|
+
try:
|
|
17
|
+
if platform.system() == "Linux":
|
|
18
|
+
r = subprocess.run(["nvidia-smi", "-L"], capture_output=True, timeout=2)
|
|
19
|
+
return "GPU" in (r.stdout.decode("utf-8", errors="ignore") or "")
|
|
20
|
+
elif platform.system() == "Windows":
|
|
21
|
+
try:
|
|
22
|
+
ps = subprocess.run(
|
|
23
|
+
["powershell", "-NoProfile", "-Command",
|
|
24
|
+
"(Get-CimInstance Win32_VideoController | Select-Object -ExpandProperty Name) -join ';'"],
|
|
25
|
+
capture_output=True, timeout=2
|
|
26
|
+
)
|
|
27
|
+
out = (ps.stdout.decode("utf-8", errors="ignore") or "").lower()
|
|
28
|
+
return "nvidia" in out
|
|
29
|
+
except Exception:
|
|
30
|
+
w = subprocess.run(["wmic", "path", "win32_VideoController", "get", "name"],
|
|
31
|
+
capture_output=True, timeout=2)
|
|
32
|
+
return "nvidia" in (w.stdout.decode("utf-8", errors="ignore") or "").lower()
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
return False
|
|
36
|
+
|
|
13
37
|
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QStandardPaths, QSettings
|
|
14
38
|
from PyQt6.QtWidgets import (
|
|
15
39
|
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog,
|
|
16
|
-
QComboBox, QSpinBox, QProgressBar, QMessageBox, QCheckBox
|
|
40
|
+
QComboBox, QSpinBox, QProgressBar, QMessageBox, QCheckBox, QLineEdit, QApplication
|
|
17
41
|
)
|
|
18
42
|
from PyQt6.QtGui import QIcon
|
|
19
43
|
from setiastro.saspro.config import Config
|
|
@@ -145,10 +169,13 @@ def _restore_output(arr: np.ndarray, channels_last: bool, was_uint16: bool, H: i
|
|
|
145
169
|
arr = arr[0] # (H,W)
|
|
146
170
|
return arr
|
|
147
171
|
|
|
148
|
-
def run_onnx_tiled(session, img: np.ndarray, patch_size=512, overlap=64,
|
|
172
|
+
def run_onnx_tiled(session, img: np.ndarray, patch_size=512, overlap=64,
|
|
173
|
+
progress_cb=None, cancel_cb=None) -> np.ndarray:
|
|
149
174
|
"""
|
|
150
175
|
session: onnxruntime.InferenceSession
|
|
151
176
|
img: mono (H,W) or RGB (H,W,3) numpy array
|
|
177
|
+
|
|
178
|
+
cancel_cb: callable -> bool, return True to cancel
|
|
152
179
|
"""
|
|
153
180
|
arr, channels_last, was_uint16 = _prepare_input(img) # (C,H,W)
|
|
154
181
|
arr, H0, W0 = _pad_C_HW(arr, patch_size)
|
|
@@ -168,11 +195,15 @@ def run_onnx_tiled(session, img: np.ndarray, patch_size=512, overlap=64, progres
|
|
|
168
195
|
for c in range(C):
|
|
169
196
|
for i in hs:
|
|
170
197
|
for j in ws:
|
|
171
|
-
|
|
198
|
+
if cancel_cb and cancel_cb():
|
|
199
|
+
raise RuntimeError("Canceled")
|
|
200
|
+
|
|
201
|
+
patch = arr[c:c+1, i:i+patch_size, j:j+patch_size] # (1,P,P)
|
|
172
202
|
inp = np.ascontiguousarray(patch[np.newaxis, ...], dtype=np.float32) # (1,1,P,P)
|
|
173
203
|
|
|
174
204
|
out_patch = session.run(None, {inp_name: inp})[0] # (1,1,P,P)
|
|
175
205
|
out_patch = np.squeeze(out_patch, axis=0) # (1,P,P)
|
|
206
|
+
|
|
176
207
|
out[c:c+1, i:i+patch_size, j:j+patch_size] += out_patch * win
|
|
177
208
|
wgt[c:c+1, i:i+patch_size, j:j+patch_size] += win
|
|
178
209
|
|
|
@@ -184,7 +215,6 @@ def run_onnx_tiled(session, img: np.ndarray, patch_size=512, overlap=64, progres
|
|
|
184
215
|
arr = out / wgt
|
|
185
216
|
return _restore_output(arr, channels_last, was_uint16, H0, W0)
|
|
186
217
|
|
|
187
|
-
|
|
188
218
|
# ---------- providers ----------
|
|
189
219
|
def pick_providers(auto_gpu=True) -> list[str]:
|
|
190
220
|
"""
|
|
@@ -248,9 +278,11 @@ def _preserve_border(dst: np.ndarray, src: np.ndarray, px: int = 10) -> np.ndarr
|
|
|
248
278
|
|
|
249
279
|
# ---------- worker ----------
|
|
250
280
|
class _ONNXWorker(QThread):
|
|
251
|
-
progressed
|
|
252
|
-
failed
|
|
253
|
-
finished_ok= pyqtSignal(np.ndarray)
|
|
281
|
+
progressed = pyqtSignal(int) # 0..100
|
|
282
|
+
failed = pyqtSignal(str)
|
|
283
|
+
finished_ok = pyqtSignal(np.ndarray)
|
|
284
|
+
canceled = pyqtSignal()
|
|
285
|
+
log_message = pyqtSignal(str) # for console logging
|
|
254
286
|
|
|
255
287
|
def __init__(self, model_path: str, image: np.ndarray, patch: int, overlap: int, providers: list[str]):
|
|
256
288
|
super().__init__()
|
|
@@ -260,33 +292,115 @@ class _ONNXWorker(QThread):
|
|
|
260
292
|
self.overlap = overlap
|
|
261
293
|
self.providers = providers
|
|
262
294
|
self.used_provider = None
|
|
295
|
+
self._cancel = False # cooperative flag
|
|
296
|
+
|
|
297
|
+
def cancel(self):
|
|
298
|
+
# Safe to call from UI thread
|
|
299
|
+
self._cancel = True
|
|
300
|
+
self.requestInterruption()
|
|
301
|
+
|
|
302
|
+
def _is_canceled(self) -> bool:
|
|
303
|
+
return self._cancel or self.isInterruptionRequested()
|
|
263
304
|
|
|
264
305
|
def run(self):
|
|
265
306
|
if ort is None:
|
|
266
307
|
self.failed.emit("onnxruntime is not installed.")
|
|
267
308
|
return
|
|
309
|
+
|
|
310
|
+
# If canceled before start, exit cleanly
|
|
311
|
+
if self._is_canceled():
|
|
312
|
+
self.canceled.emit()
|
|
313
|
+
return
|
|
314
|
+
|
|
315
|
+
# Log available providers for debugging
|
|
316
|
+
avail_providers = ort.get_available_providers()
|
|
317
|
+
gpu_providers = [p for p in self.providers if p != "CPUExecutionProvider"]
|
|
318
|
+
has_nvidia = _has_nvidia_gpu()
|
|
319
|
+
|
|
320
|
+
self.log_message.emit(f"🔍 Available ONNX providers: {', '.join(avail_providers)}")
|
|
321
|
+
self.log_message.emit(f"🔍 Attempting providers: {', '.join(self.providers)}")
|
|
322
|
+
print(f"🔍 Available ONNX providers: {', '.join(avail_providers)}")
|
|
323
|
+
print(f"🔍 Attempting providers: {', '.join(self.providers)}")
|
|
324
|
+
|
|
325
|
+
# Check if NVIDIA GPU is present but CUDA provider is missing
|
|
326
|
+
if has_nvidia and "CUDAExecutionProvider" not in avail_providers:
|
|
327
|
+
msg = ("⚠️ GPU NVIDIA détecté mais CUDAExecutionProvider n'est pas disponible.\n"
|
|
328
|
+
" Vous devez installer 'onnxruntime-gpu' au lieu de 'onnxruntime'.\n"
|
|
329
|
+
" Commande: pip uninstall onnxruntime && pip install onnxruntime-gpu")
|
|
330
|
+
self.log_message.emit(msg)
|
|
331
|
+
print(msg)
|
|
332
|
+
|
|
268
333
|
try:
|
|
269
334
|
sess = ort.InferenceSession(self.model_path, providers=self.providers)
|
|
270
335
|
self.used_provider = (sess.get_providers()[0] if sess.get_providers() else None)
|
|
271
|
-
|
|
336
|
+
# Log successful GPU usage
|
|
337
|
+
if self.used_provider != "CPUExecutionProvider" and gpu_providers:
|
|
338
|
+
msg = f"✅ Aberration AI: Using GPU provider {self.used_provider}"
|
|
339
|
+
self.log_message.emit(msg)
|
|
340
|
+
print(msg)
|
|
341
|
+
elif has_nvidia and self.used_provider == "CPUExecutionProvider":
|
|
342
|
+
msg = ("⚠️ GPU NVIDIA détecté mais utilisation du CPU.\n"
|
|
343
|
+
" Installez 'onnxruntime-gpu' pour utiliser le GPU.")
|
|
344
|
+
self.log_message.emit(msg)
|
|
345
|
+
print(msg)
|
|
346
|
+
except Exception as e:
|
|
347
|
+
# Log the actual error for debugging
|
|
348
|
+
error_msg = str(e)
|
|
349
|
+
msg = f"⚠️ Aberration AI: GPU provider failed: {error_msg}"
|
|
350
|
+
self.log_message.emit(msg)
|
|
351
|
+
print(msg)
|
|
352
|
+
self.log_message.emit(f"Available providers: {', '.join(avail_providers)}")
|
|
353
|
+
print(f"Available providers: {', '.join(avail_providers)}")
|
|
354
|
+
self.log_message.emit(f"Attempted providers: {', '.join(self.providers)}")
|
|
355
|
+
print(f"Attempted providers: {', '.join(self.providers)}")
|
|
356
|
+
|
|
357
|
+
# Check if onnxruntime-gpu is installed (CUDA provider should be available if it is)
|
|
358
|
+
if "CUDAExecutionProvider" in self.providers and "CUDAExecutionProvider" not in avail_providers:
|
|
359
|
+
if has_nvidia:
|
|
360
|
+
msg = ("❌ CUDAExecutionProvider non disponible alors qu'un GPU NVIDIA est présent.\n"
|
|
361
|
+
" Installez 'onnxruntime-gpu': pip uninstall onnxruntime && pip install onnxruntime-gpu")
|
|
362
|
+
else:
|
|
363
|
+
msg = "⚠️ CUDAExecutionProvider not available. You may need to install onnxruntime-gpu instead of onnxruntime."
|
|
364
|
+
self.log_message.emit(msg)
|
|
365
|
+
print(msg)
|
|
366
|
+
|
|
272
367
|
# fallback CPU if GPU fails
|
|
273
368
|
try:
|
|
274
369
|
sess = ort.InferenceSession(self.model_path, providers=["CPUExecutionProvider"])
|
|
275
|
-
self.used_provider = "CPUExecutionProvider"
|
|
370
|
+
self.used_provider = "CPUExecutionProvider"
|
|
371
|
+
msg = f"⚠️ Aberration AI: Falling back to CPU (GPU initialization failed: {error_msg})"
|
|
372
|
+
self.log_message.emit(msg)
|
|
373
|
+
print(msg)
|
|
276
374
|
except Exception as e2:
|
|
277
|
-
self.failed.emit(f"Failed to init ONNX session:\
|
|
375
|
+
self.failed.emit(f"Failed to init ONNX session:\nGPU error: {error_msg}\nCPU error: {e2}")
|
|
278
376
|
return
|
|
279
377
|
|
|
280
378
|
def cb(frac):
|
|
281
379
|
self.progressed.emit(int(frac * 100))
|
|
282
380
|
|
|
283
381
|
try:
|
|
284
|
-
out = run_onnx_tiled(
|
|
382
|
+
out = run_onnx_tiled(
|
|
383
|
+
sess,
|
|
384
|
+
self.image,
|
|
385
|
+
self.patch,
|
|
386
|
+
self.overlap,
|
|
387
|
+
progress_cb=cb,
|
|
388
|
+
cancel_cb=self._is_canceled,
|
|
389
|
+
)
|
|
285
390
|
except Exception as e:
|
|
286
|
-
|
|
391
|
+
# Normalize cancel
|
|
392
|
+
msg = str(e) or "Error"
|
|
393
|
+
if "Canceled" in msg:
|
|
394
|
+
self.canceled.emit()
|
|
395
|
+
else:
|
|
396
|
+
self.failed.emit(msg)
|
|
397
|
+
return
|
|
287
398
|
|
|
288
|
-
self.
|
|
399
|
+
if self._is_canceled():
|
|
400
|
+
self.canceled.emit()
|
|
401
|
+
return
|
|
289
402
|
|
|
403
|
+
self.finished_ok.emit(out)
|
|
290
404
|
|
|
291
405
|
# ---------- dialog ----------
|
|
292
406
|
class AberrationAIDialog(QDialog):
|
|
@@ -315,11 +429,29 @@ class AberrationAIDialog(QDialog):
|
|
|
315
429
|
row.addWidget(QLabel(self.tr("Model:")))
|
|
316
430
|
self.model_label = QLabel("—")
|
|
317
431
|
self.model_label.setToolTip("")
|
|
318
|
-
btn_browse = QPushButton(self.tr("Browse…")); btn_browse.clicked.connect(self.
|
|
432
|
+
btn_browse = QPushButton(self.tr("Browse…")); btn_browse.clicked.connect(self._browse_active_model)
|
|
319
433
|
row.addWidget(self.model_label, 1)
|
|
320
434
|
row.addWidget(btn_browse)
|
|
321
435
|
v.addLayout(row)
|
|
436
|
+
# Custom model row (NEW)
|
|
437
|
+
row_custom = QHBoxLayout()
|
|
438
|
+
self.chk_use_custom = QCheckBox(self.tr("Use custom model file"))
|
|
439
|
+
self.chk_use_custom.setChecked(False)
|
|
440
|
+
self.chk_use_custom.toggled.connect(self._on_use_custom_toggled)
|
|
441
|
+
|
|
442
|
+
self.le_custom_model = QLineEdit()
|
|
443
|
+
self.le_custom_model.setReadOnly(True)
|
|
444
|
+
self.le_custom_model.setPlaceholderText(self.tr("No custom model selected"))
|
|
445
|
+
self.le_custom_model.setToolTip("")
|
|
446
|
+
|
|
447
|
+
btn_custom_clear = QPushButton(self.tr("Clear"))
|
|
448
|
+
btn_custom_clear.clicked.connect(self._clear_custom_model)
|
|
449
|
+
|
|
450
|
+
row_custom.addWidget(self.chk_use_custom)
|
|
451
|
+
row_custom.addWidget(self.le_custom_model, 1)
|
|
322
452
|
|
|
453
|
+
row_custom.addWidget(btn_custom_clear)
|
|
454
|
+
v.addLayout(row_custom)
|
|
323
455
|
# Providers row
|
|
324
456
|
row2 = QHBoxLayout()
|
|
325
457
|
self.chk_auto = QCheckBox(self.tr("Auto GPU (if available)"))
|
|
@@ -373,7 +505,9 @@ class AberrationAIDialog(QDialog):
|
|
|
373
505
|
self._model_path = None
|
|
374
506
|
self._refresh_providers()
|
|
375
507
|
self._load_last_model_from_settings()
|
|
376
|
-
|
|
508
|
+
self._load_last_custom_model_from_settings()
|
|
509
|
+
use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
|
|
510
|
+
self.chk_use_custom.setChecked(bool(use_custom))
|
|
377
511
|
if IS_APPLE_ARM:
|
|
378
512
|
self.chk_auto.setChecked(False)
|
|
379
513
|
self.chk_auto.setEnabled(False)
|
|
@@ -395,11 +529,73 @@ class AberrationAIDialog(QDialog):
|
|
|
395
529
|
if p and os.path.isfile(p):
|
|
396
530
|
self._set_model_path(p)
|
|
397
531
|
|
|
398
|
-
def
|
|
399
|
-
|
|
532
|
+
def _browse_active_model(self):
|
|
533
|
+
"""
|
|
534
|
+
Single Browse button.
|
|
535
|
+
- If user picks a file inside the app model folder -> treat as "downloaded" selection (use_custom_model=False)
|
|
536
|
+
- If user picks a file outside -> treat as "custom" (use_custom_model=True)
|
|
537
|
+
"""
|
|
538
|
+
app_dir = os.path.abspath(_app_model_dir())
|
|
539
|
+
|
|
540
|
+
# Start in last-used folder if possible
|
|
541
|
+
last_custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
542
|
+
last_downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
|
|
543
|
+
start_dir = None
|
|
544
|
+
for candidate in (last_custom, last_downloaded):
|
|
545
|
+
if candidate and os.path.isfile(candidate):
|
|
546
|
+
d = os.path.dirname(candidate)
|
|
547
|
+
if os.path.isdir(d):
|
|
548
|
+
start_dir = d
|
|
549
|
+
break
|
|
550
|
+
if start_dir is None:
|
|
551
|
+
start_dir = app_dir
|
|
552
|
+
|
|
400
553
|
p, _ = QFileDialog.getOpenFileName(self, "Select ONNX model", start_dir, "ONNX (*.onnx)")
|
|
401
|
-
if p:
|
|
402
|
-
|
|
554
|
+
if not p:
|
|
555
|
+
return
|
|
556
|
+
|
|
557
|
+
p_abs = os.path.abspath(p)
|
|
558
|
+
# Determine if picked file is inside app model folder
|
|
559
|
+
in_app_dir = False
|
|
560
|
+
try:
|
|
561
|
+
in_app_dir = os.path.commonpath([app_dir, p_abs]) == app_dir
|
|
562
|
+
except Exception:
|
|
563
|
+
in_app_dir = p_abs.startswith(app_dir)
|
|
564
|
+
|
|
565
|
+
if in_app_dir:
|
|
566
|
+
# "Downloaded" selection
|
|
567
|
+
self._set_model_path(p_abs)
|
|
568
|
+
self._set_custom_model_path(None)
|
|
569
|
+
QSettings().setValue("AberrationAI/use_custom_model", False)
|
|
570
|
+
if hasattr(self, "chk_use_custom"):
|
|
571
|
+
self.chk_use_custom.setChecked(False)
|
|
572
|
+
else:
|
|
573
|
+
# "Custom" selection
|
|
574
|
+
self._set_custom_model_path(p_abs)
|
|
575
|
+
QSettings().setValue("AberrationAI/use_custom_model", True)
|
|
576
|
+
if hasattr(self, "chk_use_custom"):
|
|
577
|
+
self.chk_use_custom.setChecked(True)
|
|
578
|
+
|
|
579
|
+
# Keep visuals in sync
|
|
580
|
+
self._refresh_model_label()
|
|
581
|
+
self._refresh_custom_row_visibility()
|
|
582
|
+
|
|
583
|
+
|
|
584
|
+
def _refresh_model_label(self):
|
|
585
|
+
downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
|
|
586
|
+
custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
587
|
+
use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
|
|
588
|
+
|
|
589
|
+
if use_custom and custom:
|
|
590
|
+
self.model_label.setText(f"Custom: {os.path.basename(custom)}")
|
|
591
|
+
self.model_label.setToolTip(custom)
|
|
592
|
+
elif downloaded:
|
|
593
|
+
self.model_label.setText(f"Downloaded: {os.path.basename(downloaded)}")
|
|
594
|
+
self.model_label.setToolTip(downloaded)
|
|
595
|
+
else:
|
|
596
|
+
self.model_label.setText("—")
|
|
597
|
+
self.model_label.setToolTip("")
|
|
598
|
+
|
|
403
599
|
|
|
404
600
|
def _open_model_folder(self):
|
|
405
601
|
d = _app_model_dir()
|
|
@@ -412,6 +608,108 @@ class AberrationAIDialog(QDialog):
|
|
|
412
608
|
import subprocess; subprocess.Popen(["xdg-open", d])
|
|
413
609
|
except Exception:
|
|
414
610
|
webbrowser.open(f"file://{d}")
|
|
611
|
+
# ----- custom model helpers (NEW) -----
|
|
612
|
+
def _set_custom_model_path(self, p: str | None):
|
|
613
|
+
if p:
|
|
614
|
+
self.le_custom_model.setText(os.path.basename(p))
|
|
615
|
+
self.le_custom_model.setToolTip(p)
|
|
616
|
+
QSettings().setValue("AberrationAI/custom_model_path", p)
|
|
617
|
+
else:
|
|
618
|
+
self.le_custom_model.clear()
|
|
619
|
+
self.le_custom_model.setToolTip("")
|
|
620
|
+
QSettings().remove("AberrationAI/custom_model_path")
|
|
621
|
+
|
|
622
|
+
def _load_last_custom_model_from_settings(self):
|
|
623
|
+
p = QSettings().value("AberrationAI/custom_model_path", type=str)
|
|
624
|
+
if p:
|
|
625
|
+
if os.path.isfile(p):
|
|
626
|
+
self._set_custom_model_path(p)
|
|
627
|
+
else:
|
|
628
|
+
# Keep the broken path visible in tooltip for debugging
|
|
629
|
+
if hasattr(self, "le_custom_model"):
|
|
630
|
+
self.le_custom_model.setText(os.path.basename(p) + " (missing)")
|
|
631
|
+
self.le_custom_model.setToolTip(p)
|
|
632
|
+
|
|
633
|
+
# After both loads, sync labels/visibility
|
|
634
|
+
self._refresh_model_label()
|
|
635
|
+
self._refresh_custom_row_visibility()
|
|
636
|
+
|
|
637
|
+
def _refresh_custom_row_visibility(self):
|
|
638
|
+
"""
|
|
639
|
+
If you keep the custom row in the UI, hide the path field unless custom is enabled.
|
|
640
|
+
"""
|
|
641
|
+
if not hasattr(self, "le_custom_model"):
|
|
642
|
+
return
|
|
643
|
+
use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
|
|
644
|
+
self.le_custom_model.setVisible(bool(use_custom))
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _refresh_model_label(self):
|
|
648
|
+
downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
|
|
649
|
+
custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
650
|
+
use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
|
|
651
|
+
|
|
652
|
+
# Prefer custom only if enabled AND the file exists
|
|
653
|
+
if use_custom and custom:
|
|
654
|
+
if os.path.isfile(custom):
|
|
655
|
+
self.model_label.setText(f"Custom: {os.path.basename(custom)}")
|
|
656
|
+
self.model_label.setToolTip(custom)
|
|
657
|
+
return
|
|
658
|
+
else:
|
|
659
|
+
self.model_label.setText(f"Custom: {os.path.basename(custom)} (missing)")
|
|
660
|
+
self.model_label.setToolTip(custom)
|
|
661
|
+
return
|
|
662
|
+
|
|
663
|
+
# Otherwise show downloaded if valid
|
|
664
|
+
if downloaded and os.path.isfile(downloaded):
|
|
665
|
+
self.model_label.setText(f"Downloaded: {os.path.basename(downloaded)}")
|
|
666
|
+
self.model_label.setToolTip(downloaded)
|
|
667
|
+
else:
|
|
668
|
+
self.model_label.setText("—")
|
|
669
|
+
self.model_label.setToolTip("")
|
|
670
|
+
|
|
671
|
+
|
|
672
|
+
def _browse_custom_model(self):
|
|
673
|
+
# Start at last dir if possible, else app model dir
|
|
674
|
+
last = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
675
|
+
start_dir = os.path.dirname(last) if last and os.path.isdir(os.path.dirname(last)) else _app_model_dir()
|
|
676
|
+
p, _ = QFileDialog.getOpenFileName(self, "Select custom ONNX model", start_dir, "ONNX (*.onnx)")
|
|
677
|
+
if p:
|
|
678
|
+
self._set_custom_model_path(p)
|
|
679
|
+
QSettings().setValue("AberrationAI/use_custom_model", True)
|
|
680
|
+
if not self.chk_use_custom.isChecked():
|
|
681
|
+
self.chk_use_custom.setChecked(True)
|
|
682
|
+
|
|
683
|
+
def _clear_custom_model(self):
|
|
684
|
+
self._set_custom_model_path(None)
|
|
685
|
+
QSettings().setValue("AberrationAI/use_custom_model", False)
|
|
686
|
+
if hasattr(self, "chk_use_custom"):
|
|
687
|
+
self.chk_use_custom.setChecked(False)
|
|
688
|
+
|
|
689
|
+
self._refresh_model_label()
|
|
690
|
+
self._refresh_custom_row_visibility()
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def _on_use_custom_toggled(self, on: bool):
|
|
694
|
+
QSettings().setValue("AberrationAI/use_custom_model", bool(on))
|
|
695
|
+
|
|
696
|
+
if on:
|
|
697
|
+
p = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
698
|
+
if not (p and os.path.isfile(p)):
|
|
699
|
+
# Don’t spawn another browse button path; use the ONE browse if they want
|
|
700
|
+
QMessageBox.information(
|
|
701
|
+
self,
|
|
702
|
+
self.tr("Custom model"),
|
|
703
|
+
self.tr("Custom model is enabled, but no custom file is selected.\n"
|
|
704
|
+
"Click Browse… to choose a model file.")
|
|
705
|
+
)
|
|
706
|
+
# Optional: auto-open the single browse:
|
|
707
|
+
# self._browse_active_model()
|
|
708
|
+
# return
|
|
709
|
+
|
|
710
|
+
self._refresh_model_label()
|
|
711
|
+
self._refresh_custom_row_visibility()
|
|
712
|
+
|
|
415
713
|
|
|
416
714
|
# ----- provider UI -----
|
|
417
715
|
def _log(self, msg: str): # NEW
|
|
@@ -477,8 +775,16 @@ class AberrationAIDialog(QDialog):
|
|
|
477
775
|
def _on_download_ok(self, path: str):
|
|
478
776
|
self.progress.setValue(100)
|
|
479
777
|
self._set_model_path(path)
|
|
778
|
+
|
|
779
|
+
# Download becomes the active model unless custom is explicitly enabled
|
|
780
|
+
if not QSettings().value("AberrationAI/use_custom_model", False, type=bool):
|
|
781
|
+
self._set_custom_model_path(None)
|
|
782
|
+
|
|
480
783
|
QMessageBox.information(self, "Model", f"Downloaded: {os.path.basename(path)}")
|
|
481
784
|
|
|
785
|
+
self._refresh_model_label()
|
|
786
|
+
self._refresh_custom_row_visibility()
|
|
787
|
+
|
|
482
788
|
# ----- run -----
|
|
483
789
|
def _run(self):
|
|
484
790
|
if ort is None:
|
|
@@ -489,7 +795,22 @@ class AberrationAIDialog(QDialog):
|
|
|
489
795
|
"Please try installing an earlier version (for example 1.19.x) and try again."
|
|
490
796
|
)
|
|
491
797
|
return
|
|
492
|
-
|
|
798
|
+
|
|
799
|
+
# Choose model path (normal vs custom)
|
|
800
|
+
use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
|
|
801
|
+
downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
|
|
802
|
+
custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
|
|
803
|
+
|
|
804
|
+
model_path = custom if use_custom else downloaded
|
|
805
|
+
if self.chk_use_custom.isChecked():
|
|
806
|
+
cp = QSettings().value("AberrationAI/custom_model_path", type=str)
|
|
807
|
+
if cp and os.path.isfile(cp):
|
|
808
|
+
model_path = cp
|
|
809
|
+
else:
|
|
810
|
+
QMessageBox.warning(self, "Model", "Custom model is enabled but the file is missing. Please browse to a valid .onnx.")
|
|
811
|
+
return
|
|
812
|
+
|
|
813
|
+
if not model_path or not os.path.isfile(model_path):
|
|
493
814
|
QMessageBox.warning(self, "Model", "Please select or download a valid .onnx model first.")
|
|
494
815
|
return
|
|
495
816
|
|
|
@@ -516,7 +837,7 @@ class AberrationAIDialog(QDialog):
|
|
|
516
837
|
providers = [sel] if sel else ["CPUExecutionProvider"]
|
|
517
838
|
|
|
518
839
|
# --- make patch match the model's requirement (if fixed) ---
|
|
519
|
-
req = _model_required_patch(
|
|
840
|
+
req = _model_required_patch(model_path)
|
|
520
841
|
if req and req > 0:
|
|
521
842
|
patch = req
|
|
522
843
|
try:
|
|
@@ -537,26 +858,34 @@ class AberrationAIDialog(QDialog):
|
|
|
537
858
|
|
|
538
859
|
self._t_start = time.perf_counter()
|
|
539
860
|
prov_txt = ("auto" if self.chk_auto.isChecked() else self.cmb_provider.currentText() or "CPU")
|
|
540
|
-
self._log(f"🚀 Aberration AI: model={os.path.basename(
|
|
861
|
+
self._log(f"🚀 Aberration AI: model={os.path.basename(model_path)}, "
|
|
541
862
|
f"provider={prov_txt}, patch={patch}, overlap={overlap}")
|
|
863
|
+
|
|
864
|
+
self._effective_model_path = model_path
|
|
542
865
|
|
|
543
866
|
# -------- run worker --------
|
|
544
867
|
self.progress.setValue(0)
|
|
545
868
|
self.btn_run.setEnabled(False)
|
|
546
869
|
|
|
547
|
-
self._worker = _ONNXWorker(
|
|
870
|
+
self._worker = _ONNXWorker(model_path, img, patch, overlap, providers)
|
|
548
871
|
self._worker.progressed.connect(self.progress.setValue)
|
|
549
872
|
self._worker.failed.connect(self._on_failed)
|
|
550
873
|
self._worker.finished_ok.connect(self._on_ok)
|
|
551
874
|
self._worker.finished.connect(self._on_worker_finished)
|
|
875
|
+
self._worker.log_message.connect(self._log) # Connect log messages to console
|
|
552
876
|
self._worker.start()
|
|
553
877
|
|
|
554
878
|
|
|
555
879
|
def _on_failed(self, msg: str):
|
|
556
|
-
|
|
880
|
+
model_path = getattr(self, "_effective_model_path", self._model_path)
|
|
881
|
+
self._log(f"❌ Aberration AI failed: {msg}")
|
|
557
882
|
QMessageBox.critical(self, "ONNX Error", msg)
|
|
883
|
+
self.reject() # closes the dialog
|
|
558
884
|
|
|
559
885
|
def _on_ok(self, out: np.ndarray):
|
|
886
|
+
used = getattr(self._worker, "used_provider", None) or \
|
|
887
|
+
(self.cmb_provider.currentText() if not self.chk_auto.isChecked() else "auto")
|
|
888
|
+
model_path = getattr(self, "_effective_model_path", self._model_path)
|
|
560
889
|
doc = self.get_active_doc()
|
|
561
890
|
if doc is None or getattr(doc, "image", None) is None:
|
|
562
891
|
QMessageBox.warning(self, "Image", "No active image.")
|
|
@@ -578,11 +907,10 @@ class AberrationAIDialog(QDialog):
|
|
|
578
907
|
"processing_parameters": {
|
|
579
908
|
**(getattr(doc, "metadata", {}) or {}).get("processing_parameters", {}),
|
|
580
909
|
"AberrationAI": {
|
|
581
|
-
"model_path":
|
|
910
|
+
"model_path": model_path,
|
|
582
911
|
"patch_size": int(self.spin_patch.value()),
|
|
583
912
|
"overlap": int(self.spin_overlap.value()),
|
|
584
|
-
"provider":
|
|
585
|
-
if not self.chk_auto.isChecked() else "auto"),
|
|
913
|
+
"provider": used,
|
|
586
914
|
"border_px": BORDER_PX,
|
|
587
915
|
}
|
|
588
916
|
}
|
|
@@ -615,7 +943,7 @@ class AberrationAIDialog(QDialog):
|
|
|
615
943
|
if main is not None:
|
|
616
944
|
auto_gpu = bool(self.chk_auto.isChecked())
|
|
617
945
|
preset = {
|
|
618
|
-
"model":
|
|
946
|
+
"model": model_path,
|
|
619
947
|
"patch": int(self.spin_patch.value()),
|
|
620
948
|
"overlap": int(self.spin_overlap.value()),
|
|
621
949
|
"border_px": int(BORDER_PX),
|
|
@@ -674,21 +1002,24 @@ class AberrationAIDialog(QDialog):
|
|
|
674
1002
|
BORDER_PX = 10 # same value used above
|
|
675
1003
|
self._log(
|
|
676
1004
|
f"✅ Aberration AI applied "
|
|
677
|
-
f"(model={os.path.basename(
|
|
1005
|
+
f"(model={os.path.basename(model_path)}, provider={used}, "
|
|
678
1006
|
f"patch={int(self.spin_patch.value())}, overlap={int(self.spin_overlap.value())}, "
|
|
679
1007
|
f"border={BORDER_PX}px, time={dt:.2f}s)"
|
|
680
1008
|
)
|
|
681
1009
|
|
|
682
1010
|
self.progress.setValue(100)
|
|
683
|
-
#
|
|
1011
|
+
# NEW: close this UI after a successful run
|
|
1012
|
+
self.accept() # or self.close()
|
|
1013
|
+
return
|
|
684
1014
|
|
|
685
1015
|
def _on_worker_finished(self):
|
|
686
|
-
#
|
|
687
|
-
|
|
1016
|
+
# Dialog might have been closed by _on_ok()
|
|
1017
|
+
if not self.isVisible():
|
|
1018
|
+
return
|
|
1019
|
+
|
|
688
1020
|
if hasattr(self, "btn_run"):
|
|
689
1021
|
try:
|
|
690
1022
|
self.btn_run.setEnabled(True)
|
|
691
1023
|
except RuntimeError:
|
|
692
|
-
# Button already deleted; ignore
|
|
693
1024
|
pass
|
|
694
1025
|
self._worker = None
|
|
@@ -4,7 +4,7 @@ import os
|
|
|
4
4
|
import time
|
|
5
5
|
import numpy as np
|
|
6
6
|
from PyQt6.QtCore import QTimer
|
|
7
|
-
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QProgressBar, QPushButton, QMessageBox, QFormLayout, QDialogButtonBox, QSpinBox, QCheckBox, QComboBox, QLabel
|
|
7
|
+
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QProgressBar, QPushButton, QMessageBox, QFormLayout, QDialogButtonBox, QSpinBox, QCheckBox, QComboBox, QLabel, QApplication
|
|
8
8
|
|
|
9
9
|
from PyQt6.QtCore import QSettings
|
|
10
10
|
# reuse everything from the UI module
|
|
@@ -91,13 +91,29 @@ def run_aberration_ai_via_preset(main, preset: dict | None = None, doc=None):
|
|
|
91
91
|
worker = _ONNXWorker(model, img, patch, overlap, providers)
|
|
92
92
|
worker.progressed.connect(bar.setValue)
|
|
93
93
|
|
|
94
|
+
def _cancel_clicked():
|
|
95
|
+
btn.setEnabled(False)
|
|
96
|
+
btn.setText("Canceling…")
|
|
97
|
+
worker.cancel() # <-- SAFE
|
|
98
|
+
QApplication.processEvents()
|
|
99
|
+
|
|
94
100
|
def _fail(msg: str):
|
|
95
101
|
try:
|
|
96
102
|
if hasattr(main, "_log"):
|
|
97
103
|
main._log(f"❌ Aberration AI failed: {msg}")
|
|
98
104
|
except Exception:
|
|
99
105
|
pass
|
|
100
|
-
|
|
106
|
+
# If canceled, don't pop an error box
|
|
107
|
+
if "Canceled" not in (msg or ""):
|
|
108
|
+
QMessageBox.critical(main, "Aberration AI", msg)
|
|
109
|
+
dlg.close()
|
|
110
|
+
|
|
111
|
+
def _canceled():
|
|
112
|
+
try:
|
|
113
|
+
if hasattr(main, "_log"):
|
|
114
|
+
main._log("⛔ Aberration AI canceled.")
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
101
117
|
dlg.close()
|
|
102
118
|
|
|
103
119
|
def _ok(out: np.ndarray):
|
|
@@ -157,13 +173,23 @@ def run_aberration_ai_via_preset(main, preset: dict | None = None, doc=None):
|
|
|
157
173
|
dlg.close()
|
|
158
174
|
|
|
159
175
|
worker.failed.connect(_fail)
|
|
176
|
+
worker.canceled.connect(_canceled) # <-- NEW
|
|
160
177
|
worker.finished_ok.connect(_ok)
|
|
161
178
|
worker.finished.connect(lambda: btn.setEnabled(False))
|
|
162
|
-
|
|
179
|
+
|
|
180
|
+
btn.clicked.connect(_cancel_clicked)
|
|
181
|
+
|
|
182
|
+
# If user closes dialog via window X, also cancel
|
|
183
|
+
dlg.rejected.connect(_cancel_clicked)
|
|
163
184
|
|
|
164
185
|
worker.start()
|
|
165
186
|
dlg.exec()
|
|
166
187
|
|
|
188
|
+
# Ensure the worker is not left running after the modal closes
|
|
189
|
+
if worker.isRunning():
|
|
190
|
+
worker.cancel()
|
|
191
|
+
worker.wait(2000) # don't hang forever; just give it a moment
|
|
192
|
+
|
|
167
193
|
# clear the guard after a brief tick so downstream signals don’t re-open UI
|
|
168
194
|
def _clear():
|
|
169
195
|
for k in ("_aberration_ai_headless_running", "_aberration_ai_guard"):
|