setiastrosuitepro 1.6.1__py3-none-any.whl → 1.6.2__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.
Files changed (128) hide show
  1. setiastro/images/Background_startup.jpg +0 -0
  2. setiastro/qml/ResourceMonitor.qml +126 -0
  3. setiastro/saspro/__main__.py +159 -23
  4. setiastro/saspro/_generated/build_info.py +2 -1
  5. setiastro/saspro/abe.py +62 -11
  6. setiastro/saspro/aberration_ai.py +3 -3
  7. setiastro/saspro/add_stars.py +5 -2
  8. setiastro/saspro/astrobin_exporter.py +3 -0
  9. setiastro/saspro/astrospike_python.py +3 -1
  10. setiastro/saspro/autostretch.py +4 -2
  11. setiastro/saspro/backgroundneutral.py +52 -10
  12. setiastro/saspro/batch_convert.py +3 -0
  13. setiastro/saspro/batch_renamer.py +3 -0
  14. setiastro/saspro/blemish_blaster.py +3 -0
  15. setiastro/saspro/cheat_sheet.py +50 -15
  16. setiastro/saspro/clahe.py +27 -1
  17. setiastro/saspro/comet_stacking.py +103 -38
  18. setiastro/saspro/convo.py +3 -0
  19. setiastro/saspro/copyastro.py +3 -0
  20. setiastro/saspro/cosmicclarity.py +70 -45
  21. setiastro/saspro/crop_dialog_pro.py +17 -0
  22. setiastro/saspro/curve_editor_pro.py +18 -0
  23. setiastro/saspro/debayer.py +3 -0
  24. setiastro/saspro/doc_manager.py +39 -16
  25. setiastro/saspro/fitsmodifier.py +3 -0
  26. setiastro/saspro/frequency_separation.py +8 -2
  27. setiastro/saspro/function_bundle.py +2 -0
  28. setiastro/saspro/generate_translations.py +715 -1
  29. setiastro/saspro/ghs_dialog_pro.py +3 -0
  30. setiastro/saspro/graxpert.py +3 -0
  31. setiastro/saspro/gui/main_window.py +275 -32
  32. setiastro/saspro/gui/mixins/dock_mixin.py +100 -1
  33. setiastro/saspro/gui/mixins/file_mixin.py +7 -0
  34. setiastro/saspro/gui/mixins/menu_mixin.py +28 -0
  35. setiastro/saspro/gui/statistics_dialog.py +47 -0
  36. setiastro/saspro/halobgon.py +29 -3
  37. setiastro/saspro/histogram.py +3 -0
  38. setiastro/saspro/history_explorer.py +2 -0
  39. setiastro/saspro/i18n.py +22 -10
  40. setiastro/saspro/image_combine.py +3 -0
  41. setiastro/saspro/image_peeker_pro.py +3 -0
  42. setiastro/saspro/imageops/stretch.py +5 -13
  43. setiastro/saspro/isophote.py +3 -0
  44. setiastro/saspro/legacy/numba_utils.py +64 -47
  45. setiastro/saspro/linear_fit.py +3 -0
  46. setiastro/saspro/live_stacking.py +13 -2
  47. setiastro/saspro/mask_creation.py +3 -0
  48. setiastro/saspro/mfdeconv.py +5 -0
  49. setiastro/saspro/morphology.py +30 -5
  50. setiastro/saspro/multiscale_decomp.py +3 -0
  51. setiastro/saspro/nbtorgb_stars.py +12 -2
  52. setiastro/saspro/numba_utils.py +148 -47
  53. setiastro/saspro/ops/scripts.py +77 -17
  54. setiastro/saspro/ops/settings.py +1 -43
  55. setiastro/saspro/perfect_palette_picker.py +1 -0
  56. setiastro/saspro/pixelmath.py +6 -2
  57. setiastro/saspro/plate_solver.py +2 -1
  58. setiastro/saspro/remove_green.py +18 -1
  59. setiastro/saspro/remove_stars.py +136 -162
  60. setiastro/saspro/resources.py +7 -0
  61. setiastro/saspro/rgb_combination.py +1 -0
  62. setiastro/saspro/rgbalign.py +4 -4
  63. setiastro/saspro/save_options.py +1 -0
  64. setiastro/saspro/sfcc.py +50 -8
  65. setiastro/saspro/signature_insert.py +3 -0
  66. setiastro/saspro/stacking_suite.py +630 -341
  67. setiastro/saspro/star_alignment.py +16 -1
  68. setiastro/saspro/star_spikes.py +116 -32
  69. setiastro/saspro/star_stretch.py +38 -1
  70. setiastro/saspro/stat_stretch.py +35 -3
  71. setiastro/saspro/subwindow.py +63 -2
  72. setiastro/saspro/supernovaasteroidhunter.py +3 -0
  73. setiastro/saspro/translations/all_source_strings.json +3654 -0
  74. setiastro/saspro/translations/ar_translations.py +3865 -0
  75. setiastro/saspro/translations/de_translations.py +16 -0
  76. setiastro/saspro/translations/es_translations.py +16 -0
  77. setiastro/saspro/translations/fr_translations.py +16 -0
  78. setiastro/saspro/translations/hi_translations.py +3571 -0
  79. setiastro/saspro/translations/integrate_translations.py +36 -0
  80. setiastro/saspro/translations/it_translations.py +16 -0
  81. setiastro/saspro/translations/ja_translations.py +16 -0
  82. setiastro/saspro/translations/pt_translations.py +16 -0
  83. setiastro/saspro/translations/ru_translations.py +2848 -0
  84. setiastro/saspro/translations/saspro_ar.qm +0 -0
  85. setiastro/saspro/translations/saspro_ar.ts +255 -0
  86. setiastro/saspro/translations/saspro_de.qm +0 -0
  87. setiastro/saspro/translations/saspro_de.ts +3 -3
  88. setiastro/saspro/translations/saspro_es.qm +0 -0
  89. setiastro/saspro/translations/saspro_es.ts +3 -3
  90. setiastro/saspro/translations/saspro_fr.qm +0 -0
  91. setiastro/saspro/translations/saspro_fr.ts +3 -3
  92. setiastro/saspro/translations/saspro_hi.qm +0 -0
  93. setiastro/saspro/translations/saspro_hi.ts +257 -0
  94. setiastro/saspro/translations/saspro_it.qm +0 -0
  95. setiastro/saspro/translations/saspro_it.ts +3 -3
  96. setiastro/saspro/translations/saspro_ja.qm +0 -0
  97. setiastro/saspro/translations/saspro_ja.ts +4 -4
  98. setiastro/saspro/translations/saspro_pt.qm +0 -0
  99. setiastro/saspro/translations/saspro_pt.ts +3 -3
  100. setiastro/saspro/translations/saspro_ru.qm +0 -0
  101. setiastro/saspro/translations/saspro_ru.ts +237 -0
  102. setiastro/saspro/translations/saspro_sw.qm +0 -0
  103. setiastro/saspro/translations/saspro_sw.ts +257 -0
  104. setiastro/saspro/translations/saspro_uk.qm +0 -0
  105. setiastro/saspro/translations/saspro_uk.ts +10771 -0
  106. setiastro/saspro/translations/saspro_zh.qm +0 -0
  107. setiastro/saspro/translations/saspro_zh.ts +3 -3
  108. setiastro/saspro/translations/sw_translations.py +3671 -0
  109. setiastro/saspro/translations/uk_translations.py +3700 -0
  110. setiastro/saspro/translations/zh_translations.py +16 -0
  111. setiastro/saspro/versioning.py +12 -6
  112. setiastro/saspro/view_bundle.py +3 -0
  113. setiastro/saspro/wavescale_hdr.py +22 -1
  114. setiastro/saspro/wavescalede.py +23 -1
  115. setiastro/saspro/whitebalance.py +39 -3
  116. setiastro/saspro/widgets/minigame/game.js +986 -0
  117. setiastro/saspro/widgets/minigame/index.html +53 -0
  118. setiastro/saspro/widgets/minigame/style.css +241 -0
  119. setiastro/saspro/widgets/resource_monitor.py +237 -0
  120. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  121. setiastro/saspro/wimi.py +7996 -0
  122. setiastro/saspro/wims.py +578 -0
  123. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/METADATA +15 -4
  124. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/RECORD +128 -103
  125. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/WHEEL +0 -0
  126. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/entry_points.txt +0 -0
  127. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/LICENSE +0 -0
  128. {setiastrosuitepro-1.6.1.dist-info → setiastrosuitepro-1.6.2.dist-info}/licenses/license.txt +0 -0
Binary file
@@ -0,0 +1,126 @@
1
+ import QtQuick 2.15
2
+ import QtQuick.Controls 2.15
3
+ import QtQuick.Layouts 1.15
4
+
5
+ Rectangle {
6
+ id: root
7
+ // Wider to fit 3 gauges + spacing
8
+ width: 200
9
+ height: 60
10
+ // 50% opacity black
11
+ color: "#80000000"
12
+ radius: 30
13
+ border.color: "#555"
14
+ border.width: 1
15
+
16
+ property double cpuUsage: 0.0
17
+ property double ramUsage: 0.0
18
+ property double gpuUsage: 0.0
19
+ property double appRamUsage: 0.0
20
+ property string appRamString: "0 MB"
21
+
22
+ // Helper component for gauges
23
+ component MiniGauge: Item {
24
+ Layout.preferredWidth: 40
25
+ Layout.preferredHeight: 40
26
+ property string label: ""
27
+ property color barColor: "#0f0"
28
+ property double value: 0
29
+
30
+ // Trigger repaint when value changes
31
+ onValueChanged: if (gaugeCanvas) gaugeCanvas.requestPaint()
32
+
33
+ Canvas {
34
+ id: gaugeCanvas
35
+ anchors.fill: parent
36
+ antialiasing: true
37
+ onPaint: {
38
+ var ctx = getContext("2d");
39
+ var cx = width / 2;
40
+ var cy = height / 2;
41
+ var r = (width / 2) - 3;
42
+
43
+ ctx.reset();
44
+
45
+ // Track
46
+ ctx.beginPath();
47
+ ctx.arc(cx, cy, r, 0, 2*Math.PI);
48
+ ctx.lineWidth = 4;
49
+ ctx.strokeStyle = "#444";
50
+ ctx.stroke();
51
+
52
+ // Value Arc
53
+ // start at -90 deg (top)
54
+ var start = -Math.PI/2;
55
+ var end = start + (value/100 * 2*Math.PI);
56
+
57
+ ctx.beginPath();
58
+ ctx.arc(cx, cy, r, start, end);
59
+ ctx.lineWidth = 4;
60
+ ctx.lineCap = "round";
61
+ ctx.strokeStyle = barColor;
62
+ ctx.stroke();
63
+ }
64
+ }
65
+
66
+ // Numeric Percent in center
67
+ Text {
68
+ anchors.centerIn: parent
69
+ text: Math.round(value) + "%"
70
+ font.pixelSize: 10
71
+ font.bold: true
72
+ color: "#fff"
73
+ }
74
+ }
75
+
76
+ RowLayout {
77
+ anchors.centerIn: parent
78
+ spacing: 15
79
+
80
+ // --- CPU ---
81
+ ColumnLayout {
82
+ spacing: 2
83
+ MiniGauge {
84
+ value: root.cpuUsage
85
+ // Dynamic color
86
+ barColor: root.cpuUsage > 80 ? "#ff4444" : (root.cpuUsage > 50 ? "#ffbb33" : "#00C851")
87
+ }
88
+ Text {
89
+ Layout.alignment: Qt.AlignHCenter
90
+ text: "CPU"
91
+ color: "#aaaaaa"
92
+ font.pixelSize: 9
93
+ }
94
+ }
95
+
96
+ // --- RAM ---
97
+ ColumnLayout {
98
+ spacing: 2
99
+ MiniGauge {
100
+ value: root.ramUsage
101
+ barColor: "#33b5e5"
102
+ }
103
+ Text {
104
+ Layout.alignment: Qt.AlignHCenter
105
+ text: "RAM"
106
+ color: "#aaaaaa"
107
+ font.pixelSize: 9
108
+ }
109
+ }
110
+
111
+ // --- GPU ---
112
+ ColumnLayout {
113
+ spacing: 2
114
+ MiniGauge {
115
+ value: root.gpuUsage
116
+ barColor: "#aa66cc"
117
+ }
118
+ Text {
119
+ Layout.alignment: Qt.AlignHCenter
120
+ text: "GPU"
121
+ color: "#aaaaaa"
122
+ font.pixelSize: 9
123
+ }
124
+ }
125
+ }
126
+ }
@@ -38,6 +38,8 @@ _splash_initialized = False
38
38
  from setiastro.saspro.versioning import get_app_version
39
39
  _EARLY_VERSION = get_app_version("setiastrosuitepro")
40
40
 
41
+ VERSION = _EARLY_VERSION
42
+
41
43
  def _init_splash():
42
44
  """Initialize the splash screen. Safe to call multiple times."""
43
45
  global _splash, _app, _splash_initialized
@@ -47,7 +49,8 @@ def _init_splash():
47
49
 
48
50
  # Minimal imports for splash screen
49
51
  from PyQt6.QtWidgets import QApplication, QWidget
50
- from PyQt6.QtCore import Qt, QCoreApplication, QRect
52
+ from PyQt6.QtCore import Qt, QCoreApplication, QRect, QPropertyAnimation, QEasingCurve
53
+ import time
51
54
  from PyQt6.QtGui import QGuiApplication, QIcon, QPixmap, QColor, QPainter, QFont, QLinearGradient
52
55
 
53
56
 
@@ -129,13 +132,20 @@ def _init_splash():
129
132
 
130
133
  # NEW: Prefer centralized resources resolver
131
134
  try:
132
- from setiastro.saspro.resources import icon_path
135
+ from setiastro.saspro.resources import icon_path, background_startup_path
133
136
  _early_icon_path = icon_path
134
137
  if not os.path.exists(_early_icon_path):
135
138
  # fall back to legacy search if for some reason this is missing
136
139
  _early_icon_path = _find_icon_path()
140
+
141
+ # Load startup background path
142
+ _startup_bg_path = background_startup_path
143
+ if not os.path.exists(_startup_bg_path):
144
+ _startup_bg_path = None
145
+
137
146
  except Exception:
138
147
  _early_icon_path = _find_icon_path()
148
+ _startup_bg_path = None
139
149
 
140
150
 
141
151
  # =========================================================================
@@ -177,6 +187,11 @@ def _init_splash():
177
187
  # Load and scale logo
178
188
  self.logo_pixmap = self._load_logo(logo_path)
179
189
 
190
+ # Load background image
191
+ self.bg_image_pixmap = QPixmap()
192
+ if _startup_bg_path:
193
+ self.bg_image_pixmap = QPixmap(_startup_bg_path)
194
+
180
195
  # Fonts
181
196
  self.title_font = QFont("Segoe UI", 28, QFont.Weight.Bold)
182
197
  self.subtitle_font = QFont("Segoe UI", 11)
@@ -215,15 +230,42 @@ def _init_splash():
215
230
  _app.processEvents()
216
231
 
217
232
  def setProgress(self, value: int):
218
- """Update progress (0-100)."""
219
- self.progress_value = max(0, min(100, value))
233
+ """Update progress (0-100) with smooth animation."""
234
+ target = max(0, min(100, value))
235
+ start = self.progress_value
236
+
237
+ # If jumping backwards or small change, just set it
238
+ if target <= start or (target - start) < 1:
239
+ self.progress_value = target
240
+ self.repaint()
241
+ if _app: _app.processEvents()
242
+ return
243
+
244
+ # Animate forward
245
+ steps = 15 # number of frames for the slide
246
+ # We want the total slide to take ~100-150ms max to feel responsive but smooth
247
+ dt = 0.005 # 5ms per frame
248
+
249
+ for i in range(1, steps + 1):
250
+ # Ease out interpolator
251
+ t = i / steps
252
+ # Quadratic ease out: f(t) = -t*(t-2)
253
+ factor = -t * (t - 2)
254
+
255
+ cur = start + (target - start) * factor
256
+ self.progress_value = cur
257
+ self.repaint()
258
+ if _app: _app.processEvents()
259
+ time.sleep(dt)
260
+
261
+ self.progress_value = target
220
262
  self.repaint()
221
263
  if _app:
222
264
  _app.processEvents()
223
265
 
224
266
  def setBuildInfo(self, version: str, build: str):
225
267
  """Update version and build info once available."""
226
- self._version = version
268
+ self._version = _EARLY_VERSION
227
269
  self._build = build
228
270
  self.repaint()
229
271
 
@@ -242,6 +284,45 @@ def _init_splash():
242
284
  gradient.setColorAt(1.0, QColor(10, 10, 20))
243
285
  painter.fillRect(0, 0, w, h, gradient)
244
286
 
287
+ # --- Background Image (Centered with Fade Out) ---
288
+ if not self.bg_image_pixmap.isNull():
289
+ # Create a temporary pixmap to handle the masking
290
+ temp = QPixmap(w, h)
291
+ temp.fill(Qt.GlobalColor.transparent)
292
+
293
+ ptmp = QPainter(temp)
294
+ ptmp.setRenderHint(QPainter.RenderHint.Antialiasing)
295
+ ptmp.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
296
+
297
+ # Scale image to cover the entire splash screen
298
+ scaled = self.bg_image_pixmap.scaled(
299
+ w, h,
300
+ Qt.AspectRatioMode.KeepAspectRatioByExpanding,
301
+ Qt.TransformationMode.SmoothTransformation
302
+ )
303
+
304
+ # Center the image
305
+ sx = (w - scaled.width()) // 2
306
+ sy = (h - scaled.height()) // 2
307
+ ptmp.drawPixmap(sx, sy, scaled)
308
+
309
+ # Apply Fade Out Mask (Gradient Alpha)
310
+ ptmp.setCompositionMode(QPainter.CompositionMode.CompositionMode_DestinationIn)
311
+ fade_gradient = QLinearGradient(0, 0, 0, h)
312
+ # Keep top half fully visible (subject to global opacity)
313
+ fade_gradient.setColorAt(0.0, QColor(0, 0, 0, 255))
314
+ fade_gradient.setColorAt(0.5, QColor(0, 0, 0, 255))
315
+ # Fade out completely at the bottom
316
+ fade_gradient.setColorAt(1.0, QColor(0, 0, 0, 0))
317
+ ptmp.fillRect(0, 0, w, h, fade_gradient)
318
+ ptmp.end()
319
+
320
+ # Draw combined result with 50% opacity
321
+ painter.save()
322
+ painter.setOpacity(0.25)
323
+ painter.drawPixmap(0, 0, temp)
324
+ painter.restore()
325
+
245
326
  # --- Subtle border ---
246
327
  painter.setPen(QColor(60, 60, 80))
247
328
  painter.drawRect(0, 0, w - 1, h - 1)
@@ -312,20 +393,52 @@ def _init_splash():
312
393
  self.hide()
313
394
  self.close()
314
395
  self.deleteLater()
396
+
397
+ def start_fade_out(self):
398
+ """Smoothly fade out the splash screen."""
399
+ self._anim = QPropertyAnimation(self, b"windowOpacity")
400
+ self._anim.setDuration(1000)
401
+ self._anim.setStartValue(1.0)
402
+ self._anim.setEndValue(0.0)
403
+ self._anim.setEasingCurve(QEasingCurve.Type.OutQuad)
404
+ self._anim.finished.connect(self.finish)
405
+ self._anim.start()
315
406
 
407
+ def start_fade_in(self):
408
+ """Smoothly fade in the splash screen."""
409
+ self.setWindowOpacity(0.0)
410
+ self._anim = QPropertyAnimation(self, b"windowOpacity")
411
+ self._anim.setDuration(800)
412
+ self._anim.setStartValue(0.0)
413
+ self._anim.setEndValue(1.0)
414
+ self._anim.setEasingCurve(QEasingCurve.Type.InQuad)
415
+ self._anim.start()
416
+
316
417
  # --- Show splash IMMEDIATELY ---
317
418
  _splash = _EarlySplash(_early_icon_path)
419
+ _splash.start_fade_in()
318
420
  _splash.show()
421
+
422
+ # Block briefly to allow fade-in to progress smoothly before heavy imports start
423
+ # We use a busy loop with processEvents to keep the UI responsive during fade
424
+ t_start = time.time()
425
+ while time.time() - t_start < 0.85: # slightly longer than animation
426
+ _app.processEvents()
427
+ if _splash.windowOpacity() >= 0.99:
428
+ break
429
+ time.sleep(0.01)
430
+
319
431
  _splash.setMessage(QCoreApplication.translate("Splash", "Initializing Python runtime..."))
320
432
  _splash.setProgress(2)
321
433
  _app.processEvents()
322
434
 
323
435
  # Load translation BEFORE any other widgets are created
324
436
  try:
325
- from setiastro.saspro.i18n import load_language
326
- load_language(app=_app)
327
- except Exception:
328
- pass # Translations not critical - continue without them
437
+ from setiastro.saspro.i18n import load_language, get_translations_dir
438
+ ok = load_language(app=_app)
439
+ except Exception as e:
440
+ print("i18n load failed:", repr(e))
441
+
329
442
 
330
443
  _splash_initialized = True
331
444
 
@@ -516,7 +629,8 @@ from PyQt6.QtGui import (QPixmap, QColor, QIcon, QKeySequence, QShortcut, QGuiAp
516
629
  )
517
630
 
518
631
  # ----- QtCore -----
519
- from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QTimer, QSize, QSignalBlocker, QModelIndex, QThread, QUrl, QSettings, QEvent, QByteArray, QObject
632
+ from PyQt6.QtCore import (Qt, pyqtSignal, QCoreApplication, QTimer, QSize, QSignalBlocker, QModelIndex, QThread, QUrl, QSettings, QEvent, QByteArray, QObject,
633
+ QPropertyAnimation, QEasingCurve
520
634
  )
521
635
 
522
636
  from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
@@ -529,8 +643,6 @@ except Exception:
529
643
  BUILD_TIMESTAMP = "dev"
530
644
 
531
645
 
532
- from setiastro.saspro.versioning import get_app_version
533
- VERSION = get_app_version("setiastrosuitepro")
534
646
 
535
647
  _update_splash(QCoreApplication.translate("Splash", "Loading resources..."), 50)
536
648
 
@@ -747,7 +859,33 @@ def main():
747
859
  if _splash:
748
860
  _splash.setMessage(QCoreApplication.translate("Splash", "Showing main window..."))
749
861
  _splash.setProgress(95)
862
+
863
+ # --- Smooth Transition: App Fade In + Splash Fade Out ---
864
+ # MITIGATION: Prevent "White Flash" on startup
865
+ # 1. Force a dark background immediately so if opacity lags, it's dark not white
866
+ win.setStyleSheet("QMainWindow { background-color: #0F0F19; }")
867
+ # 2. Ensure native window handle exists so setWindowOpacity works immediately
868
+ win.winId()
869
+ # 3. Set opacity to 0
870
+ win.setWindowOpacity(0.0)
871
+
750
872
  win.show()
873
+
874
+ # 1. Animate Main Window Fade In
875
+ anim_app = QPropertyAnimation(win, b"windowOpacity")
876
+ anim_app.setDuration(1200)
877
+ anim_app.setStartValue(0.0)
878
+ anim_app.setEndValue(1.0)
879
+ anim_app.setEasingCurve(QEasingCurve.Type.OutQuad)
880
+
881
+ # Cleanup temp stylesheet upon completion to avoid interfering with ThemeMixin
882
+ def _on_fade_in_finished():
883
+ win.setStyleSheet("")
884
+ if hasattr(win, "on_fade_in_complete"):
885
+ win.on_fade_in_complete()
886
+
887
+ anim_app.finished.connect(_on_fade_in_finished)
888
+ anim_app.start()
751
889
 
752
890
  # Start background Numba warmup after UI is visible
753
891
  try:
@@ -761,18 +899,16 @@ def main():
761
899
  _splash.setProgress(100)
762
900
  _app.processEvents()
763
901
 
764
- # Small delay to show "Ready!" before closing
902
+ # Small delay to ensure "Ready!" is seen briefly before fade starts
765
903
  import time
766
- time.sleep(0.3)
767
- _app.processEvents()
768
-
769
- # Ensure the splash cannot resurrect later:
770
- try:
771
- _splash.finish()
772
- finally:
773
- _splash.hide()
774
- _splash.close()
775
- _splash.deleteLater()
904
+ time.sleep(0.1)
905
+
906
+ # 2. Animate Splash Fade Out
907
+ # Note: We do NOT use finish() directly here. The animation calls it when done.
908
+ _splash.start_fade_out()
909
+
910
+ # NOTE: We keep a reference to _splash (global) so it doesn't get GC'd during animation.
911
+ # It will deleteLater() itself.
776
912
 
777
913
  if BUILD_TIMESTAMP == "dev":
778
914
  build_label = "running from local source code"
@@ -1,2 +1,3 @@
1
1
  # Auto-generated at build time. Do not edit.
2
- BUILD_TIMESTAMP = "2025-12-18T23:29:54Z"
2
+ BUILD_TIMESTAMP = "2025-12-22T00:35:11Z"
3
+ APP_VERSION = "1.6.2"
setiastro/saspro/abe.py CHANGED
@@ -75,15 +75,28 @@ def _fit_poly_on_small(small: np.ndarray, points: np.ndarray, degree: int, patch
75
75
 
76
76
  if small.ndim == 3 and small.shape[2] == 3:
77
77
  bg_small = np.zeros_like(small, dtype=np.float32)
78
+
79
+ # Batch collect samples: (num_samples, 3)
80
+ # We need N samples. z will be list of (3,) arrays
81
+
82
+ # Pre-allocate Z: (N, 3)
83
+ Z = np.zeros((len(xs), 3), dtype=np.float32)
84
+
85
+ for k, (x, y) in enumerate(zip(xs, ys)):
86
+ x0, x1 = max(0, x - half), min(W, x + half + 1)
87
+ y0, y1 = max(0, y - half), min(H, y + half + 1)
88
+ # Efficiently compute median for all channels in this patch
89
+ patch = small[y0:y1, x0:x1, :]
90
+ Z[k] = np.median(patch, axis=(0, 1))
91
+
92
+ # Solve once: A is (N, terms), Z is (N, 3) -> coeffs is (terms, 3)
93
+ coeffs_all, *_ = np.linalg.lstsq(A, Z, rcond=None)
94
+
95
+ # Evaluate per channel
78
96
  for c in range(3):
79
- z = []
80
- for x, y in zip(xs, ys):
81
- x0, x1 = max(0, x - half), min(W, x + half + 1)
82
- y0, y1 = max(0, y - half), min(H, y + half + 1)
83
- z.append(np.median(small[y0:y1, x0:x1, c]))
84
- z = np.asarray(z, dtype=np.float32)
85
- coeffs, *_ = np.linalg.lstsq(A, z, rcond=None)
86
- bg_small[..., c] = evaluate_polynomial(H, W, coeffs.astype(np.float32), degree)
97
+ # coeffs_all[:, c] gives the terms for channel c
98
+ bg_small[..., c] = evaluate_polynomial(H, W, coeffs_all[:, c].astype(np.float32), degree)
99
+
87
100
  return bg_small
88
101
  else:
89
102
  z = []
@@ -475,13 +488,18 @@ class ABEDialog(QDialog):
475
488
 
476
489
  # IMPORTANT: avoid “attached modal sheet” behavior on some Linux WMs
477
490
  self.setWindowFlag(Qt.WindowType.Window, True)
478
- # keep it blocking if you want, but as a top-level window
479
- self.setWindowModality(Qt.WindowModality.ApplicationModal)
491
+ # Non-modal: allow user to switch between images while dialog is open
492
+ self.setWindowModality(Qt.WindowModality.NonModal)
480
493
  self.setModal(False)
481
494
  #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
482
495
 
496
+ self._main = parent
483
497
  self.doc = document
484
498
 
499
+ # Connect to active document change signal
500
+ if hasattr(self._main, "currentDocumentChanged"):
501
+ self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
502
+
485
503
  self._preview_scale = 1.0
486
504
  self._preview_qimg = None
487
505
  self._last_preview = None # backing ndarray for QImage lifetime
@@ -612,6 +630,17 @@ class ABEDialog(QDialog):
612
630
  bar.addWidget(self.btn_autostr)
613
631
  return bar
614
632
 
633
+ # ----- active document change -----
634
+ def _on_active_doc_changed(self, doc):
635
+ """Called when user clicks a different image window."""
636
+ if doc is None or getattr(doc, "image", None) is None:
637
+ return
638
+ self.doc = doc
639
+ self._polygons.clear()
640
+ self._drawing_poly = None
641
+ self._preview_source_f01 = None
642
+ self._populate_initial_preview()
643
+
615
644
  # ----- data helpers -----
616
645
  def _get_source_float(self) -> np.ndarray | None:
617
646
  src = np.asarray(self.doc.image)
@@ -813,12 +842,34 @@ class ABEDialog(QDialog):
813
842
  pass
814
843
 
815
844
  self._set_status("Done")
816
- self.accept()
845
+ # Dialog stays open so user can apply to other images
846
+ # Refresh to use the now-active document for next operation
847
+ self._refresh_document_from_active()
817
848
 
818
849
  except Exception as e:
819
850
  self._set_status("Error")
820
851
  QMessageBox.critical(self, "Apply failed", str(e))
821
852
 
853
+ def _refresh_document_from_active(self):
854
+ """
855
+ Refresh the dialog's document reference to the currently active document.
856
+ This allows reusing the same dialog on different images.
857
+ """
858
+ try:
859
+ main = self.parent()
860
+ if main and hasattr(main, "_active_doc"):
861
+ new_doc = main._active_doc()
862
+ if new_doc is not None and new_doc is not self.doc:
863
+ self.doc = new_doc
864
+ # Reset preview state for new document
865
+ self._preview_source_f01 = None
866
+ self._last_preview = None
867
+ self._preview_qimg = None
868
+ # Clear polygons since they were for old image
869
+ self._clear_polys()
870
+ except Exception:
871
+ pass
872
+
822
873
 
823
874
  # ----- exclusion polygons & mask -----
824
875
  def _clear_polys(self):
@@ -298,8 +298,8 @@ class AberrationAIDialog(QDialog):
298
298
 
299
299
  # Normalize window behavior across platforms
300
300
  self.setWindowFlag(Qt.WindowType.Window, True)
301
- # This is a “big operation” tool; app-modal is usually fine here
302
- self.setWindowModality(Qt.WindowModality.ApplicationModal)
301
+ # Non-modal: allow user to switch between images while dialog is open
302
+ self.setWindowModality(Qt.WindowModality.NonModal)
303
303
  self.setModal(False)
304
304
  #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
305
305
 
@@ -680,7 +680,7 @@ class AberrationAIDialog(QDialog):
680
680
  )
681
681
 
682
682
  self.progress.setValue(100)
683
- self.accept()
683
+ # Dialog stays open so user can apply to other images
684
684
 
685
685
  def _on_worker_finished(self):
686
686
  # If dialog is already gone, this method is never called because the receiver (self)
@@ -227,7 +227,8 @@ class AddStarsDialog(QDialog):
227
227
  self.setWindowTitle(self.tr("Add Stars to Image"))
228
228
 
229
229
  self.setWindowFlag(Qt.WindowType.Window, True)
230
- self.setWindowModality(Qt.WindowModality.ApplicationModal)
230
+ # Non-modal: allow user to switch between images while dialog is open
231
+ self.setWindowModality(Qt.WindowModality.NonModal)
231
232
  self.setModal(False)
232
233
  #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
233
234
 
@@ -574,7 +575,9 @@ class AddStarsDialog(QDialog):
574
575
 
575
576
  # Emit (target_doc, blended_image)
576
577
  self.stars_added.emit(target_doc, self.blended_image.astype(np.float32, copy=False))
577
- self.accept()
578
+ # Dialog stays open so user can apply to other images
579
+ # Refresh combo boxes for next operation
580
+ self._populate_doc_combos()
578
581
 
579
582
 
580
583
  # Ensure initial fit once shown
@@ -997,6 +997,9 @@ class AstrobinExporterDialog(QDialog):
997
997
  def __init__(self, parent=None, offline_filters_csv: Optional[str] = None):
998
998
  super().__init__(parent)
999
999
  self.setWindowTitle(self.tr("AstroBin Exporter"))
1000
+ self.setWindowFlag(Qt.WindowType.Window, True)
1001
+ self.setWindowModality(Qt.WindowModality.NonModal)
1002
+ self.setModal(False)
1000
1003
  self.resize(980, 640)
1001
1004
  v = QVBoxLayout(self)
1002
1005
  self.tab = AstrobinExportTab(self, offline_filters_csv=offline_filters_csv)
@@ -1040,7 +1040,9 @@ class AstroSpikeWindow(QDialog):
1040
1040
  def __init__(self, image_data_255: np.ndarray, image_data_float: np.ndarray, ctx):
1041
1041
  super().__init__()
1042
1042
  self.setWindowTitle("AstroSpike - Star Diffraction Spikes")
1043
- self.setModal(True)
1043
+ self.setWindowFlag(Qt.WindowType.Window, True)
1044
+ self.setWindowModality(Qt.WindowModality.NonModal)
1045
+ self.setModal(False)
1044
1046
 
1045
1047
  self.ctx = ctx
1046
1048
  self.image_data = image_data_255 # uint8 for detection
@@ -180,8 +180,10 @@ def autostretch(
180
180
  lut = _compute_lut_from_sample(lum, target_median, sigma, maxv)
181
181
 
182
182
  out = np.empty_like(u, dtype=np.float32)
183
- for c in range(min(3, C)):
184
- out[..., c] = lut[u[..., c]]
183
+ # Vectorized LUT application: apply to all RGB channels at once
184
+ # lut is 1D array of float32; u[..., :3] selects RGB indices
185
+ out[..., :3] = lut[u[..., :3]]
186
+
185
187
  if C > 3: # pass-through non-RGB channels
186
188
  out[..., 3:] = u[..., 3:] / float(maxv)
187
189
  return out