setiastrosuitepro 1.6.1.post1__py3-none-any.whl → 1.6.4__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (139) hide show
  1. setiastro/images/Background_startup.jpg +0 -0
  2. setiastro/images/rotatearbitrary.png +0 -0
  3. setiastro/qml/ResourceMonitor.qml +126 -0
  4. setiastro/saspro/__main__.py +162 -25
  5. setiastro/saspro/_generated/build_info.py +2 -1
  6. setiastro/saspro/abe.py +62 -11
  7. setiastro/saspro/aberration_ai.py +3 -3
  8. setiastro/saspro/add_stars.py +5 -2
  9. setiastro/saspro/astrobin_exporter.py +3 -0
  10. setiastro/saspro/astrospike_python.py +3 -1
  11. setiastro/saspro/autostretch.py +4 -2
  12. setiastro/saspro/backgroundneutral.py +60 -9
  13. setiastro/saspro/batch_convert.py +3 -0
  14. setiastro/saspro/batch_renamer.py +3 -0
  15. setiastro/saspro/blemish_blaster.py +3 -0
  16. setiastro/saspro/blink_comparator_pro.py +474 -251
  17. setiastro/saspro/cheat_sheet.py +50 -15
  18. setiastro/saspro/clahe.py +27 -1
  19. setiastro/saspro/comet_stacking.py +103 -38
  20. setiastro/saspro/convo.py +3 -0
  21. setiastro/saspro/copyastro.py +3 -0
  22. setiastro/saspro/cosmicclarity.py +70 -45
  23. setiastro/saspro/crop_dialog_pro.py +28 -1
  24. setiastro/saspro/curve_editor_pro.py +18 -0
  25. setiastro/saspro/debayer.py +3 -0
  26. setiastro/saspro/doc_manager.py +40 -17
  27. setiastro/saspro/fitsmodifier.py +3 -0
  28. setiastro/saspro/frequency_separation.py +8 -2
  29. setiastro/saspro/function_bundle.py +18 -16
  30. setiastro/saspro/generate_translations.py +715 -1
  31. setiastro/saspro/ghs_dialog_pro.py +3 -0
  32. setiastro/saspro/graxpert.py +3 -0
  33. setiastro/saspro/gui/main_window.py +364 -92
  34. setiastro/saspro/gui/mixins/dock_mixin.py +119 -7
  35. setiastro/saspro/gui/mixins/file_mixin.py +7 -0
  36. setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
  37. setiastro/saspro/gui/mixins/menu_mixin.py +29 -0
  38. setiastro/saspro/gui/mixins/toolbar_mixin.py +33 -10
  39. setiastro/saspro/gui/statistics_dialog.py +47 -0
  40. setiastro/saspro/halobgon.py +29 -3
  41. setiastro/saspro/histogram.py +3 -0
  42. setiastro/saspro/history_explorer.py +2 -0
  43. setiastro/saspro/i18n.py +22 -10
  44. setiastro/saspro/image_combine.py +3 -0
  45. setiastro/saspro/image_peeker_pro.py +3 -0
  46. setiastro/saspro/imageops/stretch.py +5 -13
  47. setiastro/saspro/isophote.py +3 -0
  48. setiastro/saspro/legacy/numba_utils.py +64 -47
  49. setiastro/saspro/linear_fit.py +3 -0
  50. setiastro/saspro/live_stacking.py +13 -2
  51. setiastro/saspro/mask_creation.py +3 -0
  52. setiastro/saspro/mfdeconv.py +5 -0
  53. setiastro/saspro/morphology.py +30 -5
  54. setiastro/saspro/multiscale_decomp.py +713 -256
  55. setiastro/saspro/nbtorgb_stars.py +12 -2
  56. setiastro/saspro/numba_utils.py +148 -47
  57. setiastro/saspro/ops/scripts.py +77 -17
  58. setiastro/saspro/ops/settings.py +1 -43
  59. setiastro/saspro/perfect_palette_picker.py +1 -0
  60. setiastro/saspro/pixelmath.py +6 -2
  61. setiastro/saspro/plate_solver.py +1 -0
  62. setiastro/saspro/remove_green.py +18 -1
  63. setiastro/saspro/remove_stars.py +136 -162
  64. setiastro/saspro/remove_stars_preset.py +55 -13
  65. setiastro/saspro/resources.py +36 -10
  66. setiastro/saspro/rgb_combination.py +1 -0
  67. setiastro/saspro/rgbalign.py +4 -4
  68. setiastro/saspro/save_options.py +1 -0
  69. setiastro/saspro/selective_color.py +79 -20
  70. setiastro/saspro/sfcc.py +50 -8
  71. setiastro/saspro/shortcuts.py +94 -21
  72. setiastro/saspro/signature_insert.py +3 -0
  73. setiastro/saspro/stacking_suite.py +924 -446
  74. setiastro/saspro/star_alignment.py +291 -331
  75. setiastro/saspro/star_spikes.py +116 -32
  76. setiastro/saspro/star_stretch.py +38 -1
  77. setiastro/saspro/stat_stretch.py +35 -3
  78. setiastro/saspro/status_log_dock.py +1 -1
  79. setiastro/saspro/subwindow.py +63 -2
  80. setiastro/saspro/supernovaasteroidhunter.py +3 -0
  81. setiastro/saspro/swap_manager.py +77 -42
  82. setiastro/saspro/translations/all_source_strings.json +4726 -0
  83. setiastro/saspro/translations/ar_translations.py +4096 -0
  84. setiastro/saspro/translations/de_translations.py +441 -446
  85. setiastro/saspro/translations/es_translations.py +278 -32
  86. setiastro/saspro/translations/fr_translations.py +280 -32
  87. setiastro/saspro/translations/hi_translations.py +3803 -0
  88. setiastro/saspro/translations/integrate_translations.py +38 -1
  89. setiastro/saspro/translations/it_translations.py +1211 -145
  90. setiastro/saspro/translations/ja_translations.py +556 -307
  91. setiastro/saspro/translations/pt_translations.py +3316 -3322
  92. setiastro/saspro/translations/ru_translations.py +3082 -0
  93. setiastro/saspro/translations/saspro_ar.qm +0 -0
  94. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  95. setiastro/saspro/translations/saspro_de.qm +0 -0
  96. setiastro/saspro/translations/saspro_de.ts +14428 -133
  97. setiastro/saspro/translations/saspro_es.qm +0 -0
  98. setiastro/saspro/translations/saspro_es.ts +11503 -7821
  99. setiastro/saspro/translations/saspro_fr.qm +0 -0
  100. setiastro/saspro/translations/saspro_fr.ts +11168 -7812
  101. setiastro/saspro/translations/saspro_hi.qm +0 -0
  102. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  103. setiastro/saspro/translations/saspro_it.qm +0 -0
  104. setiastro/saspro/translations/saspro_it.ts +14347 -7821
  105. setiastro/saspro/translations/saspro_ja.qm +0 -0
  106. setiastro/saspro/translations/saspro_ja.ts +14860 -137
  107. setiastro/saspro/translations/saspro_pt.qm +0 -0
  108. setiastro/saspro/translations/saspro_pt.ts +14904 -137
  109. setiastro/saspro/translations/saspro_ru.qm +0 -0
  110. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  111. setiastro/saspro/translations/saspro_sw.qm +0 -0
  112. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  113. setiastro/saspro/translations/saspro_uk.qm +0 -0
  114. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  115. setiastro/saspro/translations/saspro_zh.qm +0 -0
  116. setiastro/saspro/translations/saspro_zh.ts +10581 -7812
  117. setiastro/saspro/translations/sw_translations.py +3897 -0
  118. setiastro/saspro/translations/uk_translations.py +3929 -0
  119. setiastro/saspro/translations/zh_translations.py +283 -32
  120. setiastro/saspro/versioning.py +36 -5
  121. setiastro/saspro/view_bundle.py +20 -17
  122. setiastro/saspro/wavescale_hdr.py +22 -1
  123. setiastro/saspro/wavescalede.py +23 -1
  124. setiastro/saspro/whitebalance.py +39 -3
  125. setiastro/saspro/widgets/minigame/game.js +991 -0
  126. setiastro/saspro/widgets/minigame/index.html +53 -0
  127. setiastro/saspro/widgets/minigame/style.css +241 -0
  128. setiastro/saspro/widgets/resource_monitor.py +263 -0
  129. setiastro/saspro/widgets/spinboxes.py +18 -0
  130. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  131. setiastro/saspro/wimi.py +100 -80
  132. setiastro/saspro/wims.py +33 -33
  133. setiastro/saspro/window_shelf.py +2 -2
  134. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/METADATA +15 -4
  135. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/RECORD +139 -115
  136. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/WHEEL +0 -0
  137. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/entry_points.txt +0 -0
  138. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/LICENSE +0 -0
  139. {setiastrosuitepro-1.6.1.post1.dist-info → setiastrosuitepro-1.6.4.dist-info}/licenses/license.txt +0 -0
Binary file
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
+ }
@@ -36,7 +36,10 @@ _app = None
36
36
  _splash_initialized = False
37
37
 
38
38
  from setiastro.saspro.versioning import get_app_version
39
- _EARLY_VERSION = "1.6.1"
39
+ _EARLY_VERSION = get_app_version("setiastrosuitepro")
40
+
41
+ VERSION = _EARLY_VERSION
42
+
40
43
  def _init_splash():
41
44
  """Initialize the splash screen. Safe to call multiple times."""
42
45
  global _splash, _app, _splash_initialized
@@ -46,7 +49,8 @@ def _init_splash():
46
49
 
47
50
  # Minimal imports for splash screen
48
51
  from PyQt6.QtWidgets import QApplication, QWidget
49
- from PyQt6.QtCore import Qt, QCoreApplication, QRect
52
+ from PyQt6.QtCore import Qt, QCoreApplication, QRect, QPropertyAnimation, QEasingCurve
53
+ import time
50
54
  from PyQt6.QtGui import QGuiApplication, QIcon, QPixmap, QColor, QPainter, QFont, QLinearGradient
51
55
 
52
56
 
@@ -128,13 +132,20 @@ def _init_splash():
128
132
 
129
133
  # NEW: Prefer centralized resources resolver
130
134
  try:
131
- from setiastro.saspro.resources import icon_path
135
+ from setiastro.saspro.resources import icon_path, background_startup_path
132
136
  _early_icon_path = icon_path
133
137
  if not os.path.exists(_early_icon_path):
134
138
  # fall back to legacy search if for some reason this is missing
135
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
+
136
146
  except Exception:
137
147
  _early_icon_path = _find_icon_path()
148
+ _startup_bg_path = None
138
149
 
139
150
 
140
151
  # =========================================================================
@@ -146,7 +157,7 @@ def _init_splash():
146
157
  """
147
158
  def __init__(self, logo_path: str):
148
159
  super().__init__()
149
- self._version = "1.6.1"
160
+ self._version = _EARLY_VERSION
150
161
  self._build = ""
151
162
  self.current_message = QCoreApplication.translate("Splash", "Starting...")
152
163
  self.progress_value = 0
@@ -176,6 +187,11 @@ def _init_splash():
176
187
  # Load and scale logo
177
188
  self.logo_pixmap = self._load_logo(logo_path)
178
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
+
179
195
  # Fonts
180
196
  self.title_font = QFont("Segoe UI", 28, QFont.Weight.Bold)
181
197
  self.subtitle_font = QFont("Segoe UI", 11)
@@ -214,15 +230,42 @@ def _init_splash():
214
230
  _app.processEvents()
215
231
 
216
232
  def setProgress(self, value: int):
217
- """Update progress (0-100)."""
218
- 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
219
262
  self.repaint()
220
263
  if _app:
221
264
  _app.processEvents()
222
265
 
223
266
  def setBuildInfo(self, version: str, build: str):
224
267
  """Update version and build info once available."""
225
- self._version = "1.6.1"
268
+ self._version = _EARLY_VERSION
226
269
  self._build = build
227
270
  self.repaint()
228
271
 
@@ -241,6 +284,45 @@ def _init_splash():
241
284
  gradient.setColorAt(1.0, QColor(10, 10, 20))
242
285
  painter.fillRect(0, 0, w, h, gradient)
243
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
+
244
326
  # --- Subtle border ---
245
327
  painter.setPen(QColor(60, 60, 80))
246
328
  painter.drawRect(0, 0, w - 1, h - 1)
@@ -311,20 +393,52 @@ def _init_splash():
311
393
  self.hide()
312
394
  self.close()
313
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()
314
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
+
315
417
  # --- Show splash IMMEDIATELY ---
316
418
  _splash = _EarlySplash(_early_icon_path)
419
+ _splash.start_fade_in()
317
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
+
318
431
  _splash.setMessage(QCoreApplication.translate("Splash", "Initializing Python runtime..."))
319
432
  _splash.setProgress(2)
320
433
  _app.processEvents()
321
434
 
322
435
  # Load translation BEFORE any other widgets are created
323
436
  try:
324
- from setiastro.saspro.i18n import load_language
325
- load_language(app=_app)
326
- except Exception:
327
- 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
+
328
442
 
329
443
  _splash_initialized = True
330
444
 
@@ -515,7 +629,8 @@ from PyQt6.QtGui import (QPixmap, QColor, QIcon, QKeySequence, QShortcut, QGuiAp
515
629
  )
516
630
 
517
631
  # ----- QtCore -----
518
- 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
519
634
  )
520
635
 
521
636
  from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
@@ -528,8 +643,6 @@ except Exception:
528
643
  BUILD_TIMESTAMP = "dev"
529
644
 
530
645
 
531
- from setiastro.saspro.versioning import get_app_version
532
- VERSION = "1.6.1"
533
646
 
534
647
  _update_splash(QCoreApplication.translate("Splash", "Loading resources..."), 50)
535
648
 
@@ -746,7 +859,33 @@ def main():
746
859
  if _splash:
747
860
  _splash.setMessage(QCoreApplication.translate("Splash", "Showing main window..."))
748
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
+
749
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()
750
889
 
751
890
  # Start background Numba warmup after UI is visible
752
891
  try:
@@ -760,18 +899,16 @@ def main():
760
899
  _splash.setProgress(100)
761
900
  _app.processEvents()
762
901
 
763
- # Small delay to show "Ready!" before closing
902
+ # Small delay to ensure "Ready!" is seen briefly before fade starts
764
903
  import time
765
- time.sleep(0.3)
766
- _app.processEvents()
767
-
768
- # Ensure the splash cannot resurrect later:
769
- try:
770
- _splash.finish()
771
- finally:
772
- _splash.hide()
773
- _splash.close()
774
- _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.
775
912
 
776
913
  if BUILD_TIMESTAMP == "dev":
777
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-22T03:49:30Z"
2
+ BUILD_TIMESTAMP = "2025-12-27T00:14:44Z"
3
+ APP_VERSION = "1.6.4"
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