setiastrosuitepro 1.6.12__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.
- setiastro/images/TextureClarity.svg +56 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +128 -13
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/gui/main_window.py +285 -44
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +8 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +115 -6
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -0
- setiastro/saspro/legacy/numba_utils.py +1 -1
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +6 -0
- setiastro/saspro/rgbalign.py +456 -12
- 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 +298 -64
- setiastro/saspro/shortcuts.py +14 -7
- setiastro/saspro/stacking_suite.py +21 -6
- setiastro/saspro/stat_stretch.py +179 -31
- setiastro/saspro/subwindow.py +2 -4
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +3 -2
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +42 -30
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
<svg width="500" height="500" viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="skyGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#0d0221;stop-opacity:1" />
|
|
5
|
+
<stop offset="100%" style="stop-color:#2a0e66;stop-opacity:1" />
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="sunGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
8
|
+
<stop offset="0%" style="stop-color:#ff9a00;stop-opacity:1" />
|
|
9
|
+
<stop offset="40%" style="stop-color:#ff0055;stop-opacity:1" />
|
|
10
|
+
<stop offset="100%" style="stop-color:#9900ff;stop-opacity:1" />
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<linearGradient id="textGradient" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
13
|
+
<stop offset="0%" style="stop-color:#00ffff;stop-opacity:1" />
|
|
14
|
+
<stop offset="100%" style="stop-color:#ff00ff;stop-opacity:1" />
|
|
15
|
+
</linearGradient>
|
|
16
|
+
<filter id="neonGlow" x="-50%" y="-50%" width="200%" height="200%">
|
|
17
|
+
<feGaussianBlur stdDeviation="3.0" result="coloredBlur"/>
|
|
18
|
+
<feMerge>
|
|
19
|
+
<feMergeNode in="coloredBlur"/>
|
|
20
|
+
<feMergeNode in="SourceGraphic"/>
|
|
21
|
+
</feMerge>
|
|
22
|
+
</filter>
|
|
23
|
+
</defs>
|
|
24
|
+
<rect width="500" height="500" fill="url(#skyGradient)" />
|
|
25
|
+
<g transform="translate(250, 220)">
|
|
26
|
+
<circle r="120" fill="url(#sunGradient)" />
|
|
27
|
+
<rect x="-120" y="20" width="240" height="4" fill="#2a0e66" opacity="0.8" />
|
|
28
|
+
<rect x="-120" y="35" width="240" height="6" fill="#2a0e66" opacity="0.8" />
|
|
29
|
+
<rect x="-120" y="52" width="240" height="8" fill="#2a0e66" opacity="0.8" />
|
|
30
|
+
<rect x="-120" y="72" width="240" height="12" fill="#2a0e66" opacity="0.8" />
|
|
31
|
+
<rect x="-120" y="96" width="240" height="16" fill="#2a0e66" opacity="0.8" />
|
|
32
|
+
</g>
|
|
33
|
+
<g stroke="#ff00ff" stroke-width="2" opacity="0.4">
|
|
34
|
+
<line x1="0" y1="350" x2="500" y2="350" />
|
|
35
|
+
<line x1="0" y1="380" x2="500" y2="380" />
|
|
36
|
+
<line x1="0" y1="420" x2="500" y2="420" />
|
|
37
|
+
<line x1="0" y1="470" x2="500" y2="470" />
|
|
38
|
+
<line x1="250" y1="350" x2="250" y2="500" />
|
|
39
|
+
<line x1="250" y1="350" x2="50" y2="500" />
|
|
40
|
+
<line x1="250" y1="350" x2="-150" y2="500" />
|
|
41
|
+
<line x1="250" y1="350" x2="450" y2="500" />
|
|
42
|
+
<line x1="250" y1="350" x2="650" y2="500" />
|
|
43
|
+
</g>
|
|
44
|
+
<g font-family="'Times New Roman', Times, serif" font-weight="bold" font-style="italic" font-size="360" text-anchor="middle">
|
|
45
|
+
<text x="217" y="390" fill="#00ffff" opacity="0.7">TC</text>
|
|
46
|
+
<text x="233" y="390" fill="#ff0055" opacity="0.7">TC</text>
|
|
47
|
+
<text x="225" y="390" fill="url(#textGradient)" stroke="#ffffff" stroke-width="3" filter="url(#neonGlow)">TC</text>
|
|
48
|
+
</g>
|
|
49
|
+
<g fill="#ffffff" opacity="0.8">
|
|
50
|
+
<circle cx="50" cy="50" r="2" />
|
|
51
|
+
<circle cx="450" cy="80" r="2" />
|
|
52
|
+
<circle cx="100" cy="150" r="1.5" />
|
|
53
|
+
<circle cx="400" cy="20" r="1.5" />
|
|
54
|
+
<path d="M420 120 L422 130 L432 132 L422 134 L420 144 L418 134 L408 132 L418 130 Z" fill="#00ffff" />
|
|
55
|
+
</g>
|
|
56
|
+
</svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Auto-generated at build time. Do not edit.
|
|
2
|
-
BUILD_TIMESTAMP = "2026-01-
|
|
3
|
-
APP_VERSION = "1.
|
|
2
|
+
BUILD_TIMESTAMP = "2026-01-15T16:55:53Z"
|
|
3
|
+
APP_VERSION = "1.7.1.post2"
|
|
@@ -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, QLineEdit
|
|
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):
|
|
@@ -758,6 +872,7 @@ class AberrationAIDialog(QDialog):
|
|
|
758
872
|
self._worker.failed.connect(self._on_failed)
|
|
759
873
|
self._worker.finished_ok.connect(self._on_ok)
|
|
760
874
|
self._worker.finished.connect(self._on_worker_finished)
|
|
875
|
+
self._worker.log_message.connect(self._log) # Connect log messages to console
|
|
761
876
|
self._worker.start()
|
|
762
877
|
|
|
763
878
|
|
|
@@ -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"):
|
|
@@ -685,9 +685,51 @@ def render_spikes(output: np.ndarray, stars: List[Star], config: SpikeConfig, ct
|
|
|
685
685
|
|
|
686
686
|
# Main spikes
|
|
687
687
|
if config.intensity > 0:
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
688
|
+
rainbow_str = config.rainbow_spike_intensity if (config.enable_rainbow and config.rainbow_spikes) else 0
|
|
689
|
+
for i in range(int(config.quantity)):
|
|
690
|
+
theta = main_angle_rad + (i * (math.pi * 2) / float(config.quantity))
|
|
691
|
+
cos_t = math.cos(theta)
|
|
692
|
+
sin_t = math.sin(theta)
|
|
693
|
+
|
|
694
|
+
start_x = star.x + cos_t * 0.5
|
|
695
|
+
start_y = star.y + sin_t * 0.5
|
|
696
|
+
end_x = star.x + cos_t * base_length
|
|
697
|
+
end_y = star.y + sin_t * base_length
|
|
698
|
+
|
|
699
|
+
# Standard Spike
|
|
700
|
+
# Base star color, fading to zero alpha
|
|
701
|
+
c_end = (star.color.r/255.0, star.color.g/255.0, star.color.b/255.0, 0.0)
|
|
702
|
+
|
|
703
|
+
# If rainbow enabled, standard spike is dimmed (matches preview logic)
|
|
704
|
+
opacity_mult = 0.4 if rainbow_str > 0 else 1.0
|
|
705
|
+
c_start = (color[0], color[1], color[2], color[3] * opacity_mult)
|
|
706
|
+
|
|
707
|
+
draw_line_gradient(output, start_x, start_y, end_x, end_y,
|
|
708
|
+
c_start, c_end, thickness, config.sharpness)
|
|
709
|
+
|
|
710
|
+
# Rainbow Overlay
|
|
711
|
+
if rainbow_str > 0:
|
|
712
|
+
stops = 10
|
|
713
|
+
for s in range(stops):
|
|
714
|
+
p1 = s / stops
|
|
715
|
+
p2 = (s + 1) / stops
|
|
716
|
+
if p1 > config.rainbow_spike_length:
|
|
717
|
+
break
|
|
718
|
+
|
|
719
|
+
hue = (p1 * 360.0 * config.rainbow_spike_frequency) % 360.0
|
|
720
|
+
a_rainbow = min(1.0, config.intensity * rainbow_str * 2.0) * (1.0 - p1)
|
|
721
|
+
r_seg, g_seg, b_seg = hsl_to_rgb(hue / 360.0, 0.8, 0.6)
|
|
722
|
+
c_seg = (r_seg, g_seg, b_seg, a_rainbow)
|
|
723
|
+
|
|
724
|
+
# Calculate segment positions
|
|
725
|
+
sx = start_x + (end_x - start_x) * p1
|
|
726
|
+
sy = start_y + (end_y - start_y) * p1
|
|
727
|
+
ex = start_x + (end_x - start_x) * p2
|
|
728
|
+
ey = start_y + (end_y - start_y) * p2
|
|
729
|
+
|
|
730
|
+
# Draw rainbow segment with constant color
|
|
731
|
+
draw_line_gradient(output, sx, sy, ex, ey,
|
|
732
|
+
c_seg, c_seg, thickness, 1.0)
|
|
691
733
|
|
|
692
734
|
# Secondary spikes
|
|
693
735
|
if config.secondary_intensity > 0:
|