setiastrosuitepro 1.6.2__py3-none-any.whl → 1.6.12__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 (162) hide show
  1. setiastro/images/abeicon.svg +16 -0
  2. setiastro/images/acv_icon.png +0 -0
  3. setiastro/images/colorwheel.svg +97 -0
  4. setiastro/images/cosmic.svg +40 -0
  5. setiastro/images/cosmicsat.svg +24 -0
  6. setiastro/images/first_quarter.png +0 -0
  7. setiastro/images/full_moon.png +0 -0
  8. setiastro/images/graxpert.svg +19 -0
  9. setiastro/images/last_quarter.png +0 -0
  10. setiastro/images/linearfit.svg +32 -0
  11. setiastro/images/new_moon.png +0 -0
  12. setiastro/images/pixelmath.svg +42 -0
  13. setiastro/images/rotatearbitrary.png +0 -0
  14. setiastro/images/waning_crescent_1.png +0 -0
  15. setiastro/images/waning_crescent_2.png +0 -0
  16. setiastro/images/waning_crescent_3.png +0 -0
  17. setiastro/images/waning_crescent_4.png +0 -0
  18. setiastro/images/waning_crescent_5.png +0 -0
  19. setiastro/images/waning_gibbous_1.png +0 -0
  20. setiastro/images/waning_gibbous_2.png +0 -0
  21. setiastro/images/waning_gibbous_3.png +0 -0
  22. setiastro/images/waning_gibbous_4.png +0 -0
  23. setiastro/images/waning_gibbous_5.png +0 -0
  24. setiastro/images/waxing_crescent_1.png +0 -0
  25. setiastro/images/waxing_crescent_2.png +0 -0
  26. setiastro/images/waxing_crescent_3.png +0 -0
  27. setiastro/images/waxing_crescent_4.png +0 -0
  28. setiastro/images/waxing_crescent_5.png +0 -0
  29. setiastro/images/waxing_gibbous_1.png +0 -0
  30. setiastro/images/waxing_gibbous_2.png +0 -0
  31. setiastro/images/waxing_gibbous_3.png +0 -0
  32. setiastro/images/waxing_gibbous_4.png +0 -0
  33. setiastro/images/waxing_gibbous_5.png +0 -0
  34. setiastro/qml/ResourceMonitor.qml +84 -82
  35. setiastro/saspro/__main__.py +20 -1
  36. setiastro/saspro/_generated/build_info.py +2 -2
  37. setiastro/saspro/abe.py +37 -4
  38. setiastro/saspro/aberration_ai.py +237 -21
  39. setiastro/saspro/acv_exporter.py +379 -0
  40. setiastro/saspro/add_stars.py +33 -6
  41. setiastro/saspro/backgroundneutral.py +114 -37
  42. setiastro/saspro/blemish_blaster.py +4 -1
  43. setiastro/saspro/blink_comparator_pro.py +548 -275
  44. setiastro/saspro/clahe.py +4 -1
  45. setiastro/saspro/continuum_subtract.py +4 -1
  46. setiastro/saspro/convo.py +13 -7
  47. setiastro/saspro/cosmicclarity.py +129 -18
  48. setiastro/saspro/crop_dialog_pro.py +134 -8
  49. setiastro/saspro/curve_editor_pro.py +109 -42
  50. setiastro/saspro/doc_manager.py +246 -16
  51. setiastro/saspro/exoplanet_detector.py +120 -28
  52. setiastro/saspro/frequency_separation.py +1158 -204
  53. setiastro/saspro/function_bundle.py +16 -16
  54. setiastro/saspro/ghs_dialog_pro.py +81 -16
  55. setiastro/saspro/graxpert.py +1 -0
  56. setiastro/saspro/gui/main_window.py +519 -289
  57. setiastro/saspro/gui/mixins/dock_mixin.py +276 -42
  58. setiastro/saspro/gui/mixins/geometry_mixin.py +105 -5
  59. setiastro/saspro/gui/mixins/menu_mixin.py +28 -1
  60. setiastro/saspro/gui/mixins/theme_mixin.py +160 -14
  61. setiastro/saspro/gui/mixins/toolbar_mixin.py +416 -27
  62. setiastro/saspro/gui/mixins/update_mixin.py +138 -36
  63. setiastro/saspro/gui/mixins/view_mixin.py +42 -0
  64. setiastro/saspro/halobgon.py +4 -0
  65. setiastro/saspro/histogram.py +5 -1
  66. setiastro/saspro/image_combine.py +4 -0
  67. setiastro/saspro/image_peeker_pro.py +4 -0
  68. setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
  69. setiastro/saspro/imageops/stretch.py +582 -62
  70. setiastro/saspro/isophote.py +4 -0
  71. setiastro/saspro/layers.py +13 -9
  72. setiastro/saspro/layers_dock.py +183 -3
  73. setiastro/saspro/legacy/image_manager.py +154 -20
  74. setiastro/saspro/legacy/numba_utils.py +67 -47
  75. setiastro/saspro/legacy/xisf.py +240 -98
  76. setiastro/saspro/live_stacking.py +180 -79
  77. setiastro/saspro/luminancerecombine.py +228 -27
  78. setiastro/saspro/mask_creation.py +174 -15
  79. setiastro/saspro/mfdeconv.py +113 -35
  80. setiastro/saspro/mfdeconvcudnn.py +119 -70
  81. setiastro/saspro/mfdeconvsport.py +112 -35
  82. setiastro/saspro/morphology.py +4 -0
  83. setiastro/saspro/multiscale_decomp.py +748 -255
  84. setiastro/saspro/numba_utils.py +72 -57
  85. setiastro/saspro/ops/commands.py +18 -18
  86. setiastro/saspro/ops/script_editor.py +10 -2
  87. setiastro/saspro/ops/scripts.py +122 -0
  88. setiastro/saspro/perfect_palette_picker.py +37 -3
  89. setiastro/saspro/plate_solver.py +84 -49
  90. setiastro/saspro/psf_viewer.py +119 -37
  91. setiastro/saspro/remove_stars_preset.py +55 -13
  92. setiastro/saspro/resources.py +97 -11
  93. setiastro/saspro/rgbalign.py +4 -0
  94. setiastro/saspro/selective_color.py +83 -21
  95. setiastro/saspro/sfcc.py +364 -152
  96. setiastro/saspro/shortcuts.py +253 -49
  97. setiastro/saspro/signature_insert.py +692 -33
  98. setiastro/saspro/stacking_suite.py +1610 -574
  99. setiastro/saspro/star_alignment.py +522 -453
  100. setiastro/saspro/star_spikes.py +4 -0
  101. setiastro/saspro/star_stretch.py +38 -3
  102. setiastro/saspro/stat_stretch.py +743 -128
  103. setiastro/saspro/status_log_dock.py +1 -1
  104. setiastro/saspro/subwindow.py +786 -360
  105. setiastro/saspro/supernovaasteroidhunter.py +1 -1
  106. setiastro/saspro/swap_manager.py +77 -42
  107. setiastro/saspro/translations/all_source_strings.json +1588 -516
  108. setiastro/saspro/translations/ar_translations.py +915 -684
  109. setiastro/saspro/translations/de_translations.py +442 -463
  110. setiastro/saspro/translations/es_translations.py +277 -47
  111. setiastro/saspro/translations/fr_translations.py +279 -47
  112. setiastro/saspro/translations/hi_translations.py +253 -21
  113. setiastro/saspro/translations/integrate_translations.py +3 -2
  114. setiastro/saspro/translations/it_translations.py +1211 -161
  115. setiastro/saspro/translations/ja_translations.py +3340 -3107
  116. setiastro/saspro/translations/pt_translations.py +3315 -3337
  117. setiastro/saspro/translations/ru_translations.py +351 -117
  118. setiastro/saspro/translations/saspro_ar.qm +0 -0
  119. setiastro/saspro/translations/saspro_ar.ts +15902 -138
  120. setiastro/saspro/translations/saspro_de.qm +0 -0
  121. setiastro/saspro/translations/saspro_de.ts +14428 -133
  122. setiastro/saspro/translations/saspro_es.qm +0 -0
  123. setiastro/saspro/translations/saspro_es.ts +11503 -7821
  124. setiastro/saspro/translations/saspro_fr.qm +0 -0
  125. setiastro/saspro/translations/saspro_fr.ts +11168 -7812
  126. setiastro/saspro/translations/saspro_hi.qm +0 -0
  127. setiastro/saspro/translations/saspro_hi.ts +14733 -135
  128. setiastro/saspro/translations/saspro_it.qm +0 -0
  129. setiastro/saspro/translations/saspro_it.ts +14347 -7821
  130. setiastro/saspro/translations/saspro_ja.qm +0 -0
  131. setiastro/saspro/translations/saspro_ja.ts +14860 -137
  132. setiastro/saspro/translations/saspro_pt.qm +0 -0
  133. setiastro/saspro/translations/saspro_pt.ts +14904 -137
  134. setiastro/saspro/translations/saspro_ru.qm +0 -0
  135. setiastro/saspro/translations/saspro_ru.ts +11766 -168
  136. setiastro/saspro/translations/saspro_sw.qm +0 -0
  137. setiastro/saspro/translations/saspro_sw.ts +15115 -135
  138. setiastro/saspro/translations/saspro_uk.qm +0 -0
  139. setiastro/saspro/translations/saspro_uk.ts +11206 -6729
  140. setiastro/saspro/translations/saspro_zh.qm +0 -0
  141. setiastro/saspro/translations/saspro_zh.ts +10581 -7812
  142. setiastro/saspro/translations/sw_translations.py +282 -56
  143. setiastro/saspro/translations/uk_translations.py +264 -35
  144. setiastro/saspro/translations/zh_translations.py +282 -47
  145. setiastro/saspro/view_bundle.py +17 -17
  146. setiastro/saspro/wavescale_hdr.py +4 -1
  147. setiastro/saspro/wavescalede.py +4 -1
  148. setiastro/saspro/whitebalance.py +84 -12
  149. setiastro/saspro/widgets/common_utilities.py +28 -21
  150. setiastro/saspro/widgets/minigame/game.js +11 -6
  151. setiastro/saspro/widgets/resource_monitor.py +133 -57
  152. setiastro/saspro/widgets/spinboxes.py +28 -13
  153. setiastro/saspro/wimi.py +92 -721
  154. setiastro/saspro/wims.py +46 -36
  155. setiastro/saspro/window_shelf.py +2 -2
  156. setiastro/saspro/xisf.py +101 -11
  157. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/METADATA +8 -7
  158. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/RECORD +162 -128
  159. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/WHEEL +0 -0
  160. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/entry_points.txt +0 -0
  161. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/LICENSE +0 -0
  162. {setiastrosuitepro-1.6.2.dist-info → setiastrosuitepro-1.6.12.dist-info}/licenses/license.txt +0 -0
@@ -116,16 +116,15 @@ def _status_popup_update(text: str):
116
116
  _STATUS_POPUP.update_text(text)
117
117
 
118
118
  def _status_popup_close():
119
- """Hide (but do not destroy) the singleton status popup if it exists."""
120
119
  global _STATUS_POPUP
120
+ if _STATUS_POPUP is None:
121
+ return
121
122
  try:
122
- if _STATUS_POPUP is not None:
123
- _STATUS_POPUP.hide()
124
- # keep instance for reuse (fast re-open)
123
+ _STATUS_POPUP.hide()
125
124
  except Exception:
126
- # Completely safe to ignore; worst case the popup was already gone.
127
125
  pass
128
126
 
127
+
129
128
  def _sleep_ui(ms: int):
130
129
  """Non-blocking sleep that keeps the UI responsive."""
131
130
  loop = QEventLoop()
@@ -137,54 +136,38 @@ def _with_events():
137
136
  QApplication.processEvents()
138
137
 
139
138
  def _set_status_ui(parent, text: str):
140
- """
141
- Update dialog/main-window status or batch log; if neither exists (headless),
142
- show/update a small modeless popup. Always pumps events for responsiveness.
143
- """
144
139
  try:
145
140
  updated_any = False
146
141
 
147
- def _do():
148
- nonlocal updated_any
149
- target = None
150
- # Dialog status label?
151
- if hasattr(parent, "status") and isinstance(getattr(parent, "status"), QLabel):
152
- target = parent.status
153
- # Named child fallback
154
- if target is None and hasattr(parent, "findChild"):
155
- target = parent.findChild(QLabel, "status_label")
156
- if target is not None:
157
- target.setText(text)
142
+ target = None
143
+ if hasattr(parent, "status") and isinstance(getattr(parent, "status"), QLabel):
144
+ target = parent.status
145
+ if target is None and hasattr(parent, "findChild"):
146
+ target = parent.findChild(QLabel, "status_label")
147
+ if target is not None:
148
+ target.setText(text)
149
+ updated_any = True
150
+
151
+ logw = getattr(parent, "log", None)
152
+ if logw and hasattr(logw, "append"):
153
+ tr_status = QCoreApplication.translate("PlateSolver", "Status:")
154
+ if text and (text.startswith("Status:") or text.startswith(tr_status) or text.startswith("▶") or text.startswith("✔") or text.startswith("❌")):
155
+ logw.append(text)
158
156
  updated_any = True
159
157
 
160
- # Batch log?
161
- logw = getattr(parent, "log", None)
162
- if logw and hasattr(logw, "append"):
163
- tr_status = QCoreApplication.translate("PlateSolver", "Status:")
164
- if text and (text.startswith("Status:") or text.startswith(tr_status) or text.startswith("▶") or text.startswith("✔") or text.startswith("❌")):
165
- logw.append(text)
166
- updated_any = True
167
-
168
- # If we couldn't update any inline widget, use the headless popup.
169
- if not updated_any:
170
- _status_popup_open(parent, text)
171
- else:
172
- # If inline widgets exist and popup is visible, keep it quiet.
173
- _status_popup_update(text)
174
-
175
- QApplication.processEvents()
176
-
177
- if isinstance(parent, QWidget):
178
- QTimer.singleShot(0, _do)
158
+ if not updated_any:
159
+ _status_popup_open(parent, text)
179
160
  else:
180
- _do()
161
+ _status_popup_update(text)
162
+
163
+ QApplication.processEvents()
181
164
  except Exception:
182
- # Last-resort popup if even the above failed
183
165
  try:
184
166
  _status_popup_open(parent, text)
185
167
  except Exception:
186
168
  pass
187
169
 
170
+
188
171
  def _wait_process(proc: QProcess, timeout_ms: int, parent=None) -> bool:
189
172
  """
190
173
  Incrementally wait for a QProcess while pumping UI events so the dialog stays responsive.
@@ -234,10 +217,53 @@ def _get_solvefield_exe(settings) -> str:
234
217
  return cand[0] # may be empty (used to decide web vs. local)
235
218
 
236
219
  def _get_astrometry_api_key(settings) -> str:
237
- return settings.value("astrometry/api_key", "", type=str) or ""
220
+ """
221
+ Canonical key: 'api/astrometry_key' (matches SettingsDialog).
222
+ Also check older legacy keys for backward compatibility.
223
+ """
224
+ if settings is None:
225
+ return ""
226
+
227
+ # ✅ canonical
228
+ key = settings.value("api/astrometry_key", "", type=str) or ""
229
+ key = key.strip()
230
+ if key:
231
+ return key
232
+
233
+ # 🔁 legacy fallbacks (if you ever stored them differently)
234
+ for k in (
235
+ "api/astrometry", # old guess
236
+ "astrometry/api_key",
237
+ "astrometry/key",
238
+ "astrometry_key",
239
+ "plate_solver/astrometry_key",
240
+ ):
241
+ v = settings.value(k, "", type=str) or ""
242
+ v = v.strip()
243
+ if v:
244
+ # migrate forward so it works next time
245
+ settings.setValue("api/astrometry_key", v)
246
+ try:
247
+ settings.remove(k)
248
+ except Exception:
249
+ pass
250
+ try:
251
+ settings.sync()
252
+ except Exception:
253
+ pass
254
+ return v
255
+
256
+ return ""
238
257
 
239
- def _set_astrometry_api_key(settings, key: str):
240
- settings.setValue("astrometry/api_key", key or "")
258
+
259
+ def _set_astrometry_api_key(settings, key: str) -> None:
260
+ if settings is None:
261
+ return
262
+ settings.setValue("api/astrometry_key", (key or "").strip())
263
+ try:
264
+ settings.sync()
265
+ except Exception:
266
+ pass
241
267
 
242
268
  def _wcs_header_from_astrometry_calib(calib: dict, image_shape: tuple[int, ...]) -> Header:
243
269
  """
@@ -1774,7 +1800,8 @@ def _debug_dump_meta(label: str, meta: dict):
1774
1800
  print(f" {k}: {type(v).__name__}")
1775
1801
  print("================================\n")
1776
1802
 
1777
-
1803
+ def tr(s: str) -> str:
1804
+ return QCoreApplication.translate("PlateSolver", s)
1778
1805
 
1779
1806
  def plate_solve_doc_inplace(parent, doc, settings) -> Tuple[bool, Header | str]:
1780
1807
  img = getattr(doc, "image", None)
@@ -1831,8 +1858,9 @@ def plate_solve_doc_inplace(parent, doc, settings) -> Tuple[bool, Header | str]:
1831
1858
  (hasattr(parent, "findChild") and parent.findChild(QLabel, "status_label") is not None)
1832
1859
  )
1833
1860
  if headless:
1834
- _status_popup_open(parent, QCoreApplication.translate("PlateSolver", "Status: Preparing plate solve…"))
1861
+ _status_popup_open(parent, tr("Status: Preparing plate solve…"))
1835
1862
 
1863
+ ok_solve = False
1836
1864
  try:
1837
1865
  ok, res = _solve_numpy_with_fallback(parent, settings, img, seed_h)
1838
1866
  if not ok:
@@ -1886,11 +1914,18 @@ def plate_solve_doc_inplace(parent, doc, settings) -> Tuple[bool, Header | str]:
1886
1914
  if hasattr(parent, "currentDocumentChanged"):
1887
1915
  QTimer.singleShot(0, lambda: parent.currentDocumentChanged.emit(doc))
1888
1916
 
1889
- _set_status_ui(parent, QCoreApplication.translate("PlateSolver", "Status: Plate solve completed."))
1890
- _status_popup_close()
1917
+ _set_status_ui(parent, tr("Status: Plate solve completed."))
1918
+
1919
+
1920
+ ok_solve = True
1921
+ if headless:
1922
+ QTimer.singleShot(1200, _status_popup_close)
1923
+ else:
1924
+ _status_popup_close()
1891
1925
  return True, hdr
1892
1926
  finally:
1893
- _status_popup_close()
1927
+ if not ok_solve:
1928
+ _status_popup_close()
1894
1929
 
1895
1930
 
1896
1931
 
@@ -13,7 +13,7 @@ from PyQt6.QtWidgets import (
13
13
  )
14
14
  from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
15
15
 
16
- from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject
16
+ from PyQt6.QtCore import QThread, pyqtSignal, QObject
17
17
  from PyQt6.QtWidgets import QWidget
18
18
 
19
19
  class _ProcessingOverlay(QWidget):
@@ -108,7 +108,10 @@ class PSFViewer(QDialog):
108
108
  # Accept either a view (with .document) or a doc directly
109
109
  doc = getattr(view_or_doc, "document", None)
110
110
  self.doc = doc if doc is not None else view_or_doc
111
-
111
+ try:
112
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
113
+ except Exception:
114
+ pass # older PyQt6 versions
112
115
  # Image + state
113
116
  self.image = self._grab_image()
114
117
  self.zoom_factor = 1.0
@@ -124,12 +127,19 @@ class PSFViewer(QDialog):
124
127
  self.threshold_timer.timeout.connect(self._applyThreshold)
125
128
 
126
129
  # Auto-update when the document changes
130
+
131
+ self._psf_thread = None
132
+ self._psf_worker = None
133
+ self._doc_conn = False
127
134
  if hasattr(self.doc, "changed"):
128
135
  try:
129
136
  self.doc.changed.connect(self._on_doc_changed)
137
+ self._doc_conn = True
130
138
  except Exception:
131
- pass
139
+ self._doc_conn = False
132
140
 
141
+ # cleanup no matter how the dialog is dismissed (accept/reject/done)
142
+ self.finished.connect(self._cleanup)
133
143
  self._build_ui()
134
144
  # Defer first compute until after the dialog is shown/layouted
135
145
  QTimer.singleShot(0, self._applyThreshold)
@@ -235,7 +245,7 @@ class PSFViewer(QDialog):
235
245
 
236
246
  # Close
237
247
  close_btn = QPushButton("Close", self)
238
- close_btn.clicked.connect(self.accept)
248
+ close_btn.clicked.connect(self.close)
239
249
  main_layout.addWidget(close_btn)
240
250
 
241
251
  self.setLayout(main_layout)
@@ -279,7 +289,6 @@ class PSFViewer(QDialog):
279
289
  self.hist_label.resize(scaled.size())
280
290
 
281
291
  def _applyThreshold(self):
282
- # kick off worker
283
292
  if self.image is None:
284
293
  self.star_list = None
285
294
  self.status_label.setText("Status: No image.")
@@ -288,46 +297,72 @@ class PSFViewer(QDialog):
288
297
 
289
298
  self._show_processing("Processing… extracting stars / PSFs")
290
299
 
291
- # kill previous run if any
292
- if hasattr(self, "_psf_thread") and self._psf_thread is not None:
293
- try:
294
- self._psf_thread.quit()
295
- self._psf_thread.wait(50)
296
- except Exception:
297
- pass
300
+ # stop any previous run cleanly
301
+ self._stop_psf_worker()
298
302
 
299
303
  self._psf_thread = QThread(self)
300
304
  self._psf_worker = _PSFWorker(self.image, self.detection_threshold)
301
305
  self._psf_worker.moveToThread(self._psf_thread)
302
306
 
303
307
  self._psf_thread.started.connect(self._psf_worker.run)
308
+ self._psf_worker.finished.connect(self._on_psf_done)
309
+ self._psf_worker.failed.connect(self._on_psf_fail)
304
310
 
305
- def _done(tbl, status):
306
- self.star_list = tbl
307
- self.status_label.setText(status)
308
- self._hide_processing()
309
- self.drawHistogram()
310
- self._psf_thread.quit()
311
- self._psf_thread.wait(100)
312
-
313
- def _fail(msg):
314
- self.star_list = None
315
- self.status_label.setText(f"Status: {msg}")
316
- self._hide_processing()
317
- self.drawHistogram()
318
- self._psf_thread.quit()
319
- self._psf_thread.wait(100)
311
+ # ensure thread quits once worker reports anything
312
+ self._psf_worker.finished.connect(lambda *_: self._stop_psf_worker(quit_only=False))
313
+ self._psf_worker.failed.connect(lambda *_: self._stop_psf_worker(quit_only=False))
320
314
 
321
- self._psf_worker.finished.connect(_done)
322
- self._psf_worker.failed.connect(_fail)
323
315
 
324
316
  self._psf_thread.start()
325
317
 
318
+ def _stop_psf_worker(self, quit_only: bool = False):
319
+ thr = getattr(self, "_psf_thread", None)
320
+ wkr = getattr(self, "_psf_worker", None)
321
+
322
+ if thr is None:
323
+ return
324
+
325
+ try:
326
+ thr.quit()
327
+ except Exception:
328
+ pass
329
+ try:
330
+ thr.wait(250)
331
+ except Exception:
332
+ pass
333
+
334
+ if not quit_only:
335
+ try:
336
+ if wkr is not None:
337
+ wkr.deleteLater()
338
+ except Exception:
339
+ pass
340
+ try:
341
+ thr.deleteLater()
342
+ except Exception:
343
+ pass
344
+ self._psf_worker = None
345
+ self._psf_thread = None
346
+
347
+ def _on_psf_done(self, tbl, status: str):
348
+ # tbl is an astropy Table or None
349
+ self.star_list = tbl
350
+ self.status_label.setText(status)
351
+ self._hide_processing()
352
+ self.drawHistogram()
353
+
354
+ def _on_psf_fail(self, msg: str):
355
+ self.star_list = None
356
+ self.status_label.setText(f"Status: {msg}")
357
+ self._hide_processing()
358
+ self.drawHistogram()
359
+
326
360
 
327
361
  def updateImage(self, new_image):
328
362
  self.image = np.asarray(new_image) if new_image is not None else None
329
- self.compute_star_list()
330
- self.drawHistogram()
363
+ if self.threshold_timer.isActive():
364
+ self.threshold_timer.stop()
365
+ self.threshold_timer.start()
331
366
 
332
367
  def updateZoom(self, _=None):
333
368
  self._apply_hist_zoom()
@@ -538,12 +573,59 @@ class PSFViewer(QDialog):
538
573
  it.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
539
574
  self.stats_table.setItem(ri, ci, it)
540
575
 
576
+
577
+ def _cleanup(self):
578
+ # stop debounce timer
579
+ try:
580
+ if getattr(self, "threshold_timer", None) is not None:
581
+ self.threshold_timer.stop()
582
+ except Exception:
583
+ pass
584
+
585
+ # disconnect doc listener
586
+ try:
587
+ if self._doc_conn and hasattr(self.doc, "changed"):
588
+ self.doc.changed.disconnect(self._on_doc_changed)
589
+ except Exception:
590
+ pass
591
+ self._doc_conn = False
592
+
593
+ # stop worker/thread
594
+ try:
595
+ thr = getattr(self, "_psf_thread", None)
596
+ wkr = getattr(self, "_psf_worker", None)
597
+
598
+ if wkr is not None:
599
+ try:
600
+ wkr.deleteLater()
601
+ except Exception:
602
+ pass
603
+
604
+ if thr is not None:
605
+ try:
606
+ thr.requestInterruption()
607
+ except Exception:
608
+ pass
609
+ try:
610
+ thr.quit()
611
+ except Exception:
612
+ pass
613
+ try:
614
+ thr.wait(250)
615
+ except Exception:
616
+ pass
617
+ try:
618
+ thr.deleteLater()
619
+ except Exception:
620
+ pass
621
+ except Exception:
622
+ pass
623
+
624
+ self._psf_worker = None
625
+ self._psf_thread = None
626
+
541
627
  # ---------- lifecycle ----------
542
628
  def closeEvent(self, e):
543
- # Best-effort disconnect
544
- if hasattr(self.doc, "changed"):
545
- try:
546
- self.doc.changed.disconnect(self._on_doc_changed)
547
- except Exception:
548
- pass
629
+ self._cleanup()
549
630
  super().closeEvent(e)
631
+
@@ -12,7 +12,7 @@ from setiastro.saspro.legacy.image_manager import save_image, load_image
12
12
  # Reuse helpers & plumbing from the interactive module
13
13
  from .remove_stars import (
14
14
  _ProcThread, _ProcDialog,
15
- _stat_stretch_rgb, _stat_unstretch_rgb,
15
+ _mtf_params_unlinked, _apply_mtf_unlinked_rgb, _invert_mtf_unlinked_rgb,
16
16
  _active_mask3_from_doc, _mask_blend_with_doc_mask, _push_as_new_doc,
17
17
  _ensure_exec_bit,
18
18
  )
@@ -125,24 +125,48 @@ def _run_starnet_headless(main, doc, p):
125
125
  processing_image = processing_image.astype(np.float32, copy=False)
126
126
 
127
127
  is_linear = bool(p.get("linear", True))
128
- did_stretch = False
129
- stretch_params = None
128
+ did_stretch = is_linear
129
+
130
+ # sanitize + normalize if needed (keep exactly like interactive)
131
+ processing_image = np.nan_to_num(processing_image, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
132
+
133
+ scale_factor = float(np.max(processing_image)) if processing_image.size else 1.0
134
+ processing_norm = (processing_image / scale_factor) if scale_factor > 1.0 else processing_image
135
+ processing_norm = np.clip(processing_norm, 0.0, 1.0)
136
+
137
+ img_for_starnet = processing_norm
138
+
130
139
  if is_linear:
131
- processing_image, stretch_params = _stat_stretch_rgb(processing_image)
132
- did_stretch = True
133
- setattr(main, "_starnet_last_stretch_params", stretch_params)
140
+ mtf_params = _mtf_params_unlinked(processing_norm, shadows_clipping=-2.8, targetbg=0.25)
141
+ img_for_starnet = _apply_mtf_unlinked_rgb(processing_norm, mtf_params)
142
+
143
+ # stash for inverse step (same keys as interactive)
144
+ try:
145
+ setattr(main, "_starnet_stat_meta", {
146
+ "scheme": "siril_mtf",
147
+ "s": np.asarray(mtf_params["s"], dtype=np.float32),
148
+ "m": np.asarray(mtf_params["m"], dtype=np.float32),
149
+ "h": np.asarray(mtf_params["h"], dtype=np.float32),
150
+ "scale": float(scale_factor),
151
+ })
152
+ except Exception:
153
+ pass
134
154
  else:
135
- if hasattr(main, "_starnet_last_stretch_params"):
136
- delattr(main, "_starnet_last_stretch_params")
155
+ try:
156
+ if hasattr(main, "_starnet_stat_meta"):
157
+ delattr(main, "_starnet_stat_meta")
158
+ except Exception:
159
+ pass
160
+
137
161
 
138
162
  starnet_dir = os.path.dirname(exe) or os.getcwd()
139
163
  in_path = os.path.join(starnet_dir, "imagetoremovestars.tif")
140
164
  out_path = os.path.join(starnet_dir, "starless.tif")
141
165
 
142
166
  try:
143
- save_image(processing_image, in_path, original_format="tif",
144
- bit_depth="16-bit", original_header=None, is_mono=False,
145
- image_meta=None, file_meta=None)
167
+ save_image(img_for_starnet, in_path, original_format="tif",
168
+ bit_depth="16-bit", original_header=None, is_mono=False,
169
+ image_meta=None, file_meta=None)
146
170
  except Exception as e:
147
171
  QMessageBox.critical(main, "StarNet", f"Failed to write input TIFF:\n{e}")
148
172
  return
@@ -179,12 +203,30 @@ def _finish_starnet(main, doc, rc, dlg, in_path, out_path, did_stretch):
179
203
  starless_rgb = starless_rgb.astype(np.float32, copy=False)
180
204
 
181
205
  if did_stretch:
206
+ meta = getattr(main, "_starnet_stat_meta", None)
207
+ if isinstance(meta, dict) and meta.get("scheme") == "siril_mtf":
208
+ try:
209
+ p = {
210
+ "s": np.asarray(meta.get("s"), dtype=np.float32),
211
+ "m": np.asarray(meta.get("m"), dtype=np.float32),
212
+ "h": np.asarray(meta.get("h"), dtype=np.float32),
213
+ }
214
+ inv = _invert_mtf_unlinked_rgb(starless_rgb, p)
215
+ sc = float(meta.get("scale", 1.0))
216
+ if sc > 1.0:
217
+ inv *= sc
218
+ starless_rgb = np.clip(inv, 0.0, 1.0).astype(np.float32, copy=False)
219
+ except Exception:
220
+ pass
221
+
222
+ # cleanup so it can't leak
182
223
  try:
183
- params = getattr(main, "_starnet_last_stretch_params", None)
184
- if params: starless_rgb = _stat_unstretch_rgb(starless_rgb, params)
224
+ if hasattr(main, "_starnet_stat_meta"):
225
+ delattr(main, "_starnet_stat_meta")
185
226
  except Exception:
186
227
  pass
187
228
 
229
+
188
230
  # original as RGB
189
231
  orig = np.asarray(doc.image)
190
232
  if orig.ndim == 2: original_rgb = np.stack([orig]*3, axis=-1)
@@ -123,17 +123,31 @@ def _get_base_path() -> str:
123
123
 
124
124
 
125
125
  def _resource_path(filename: str) -> str:
126
- """Get full path to a resource file."""
127
126
  base = _get_base_path()
128
-
129
- # Check if it's an image file - look in images/ subdirectory
130
- if filename.endswith(('.png', '.ico', '.gif', '.icns', '.svg')):
131
- images_path = os.path.join(base, 'images', filename)
132
- if os.path.exists(images_path):
133
- return images_path
134
-
135
- # Fallback to root directory (for data files like .csv, .fits, etc.)
136
- return os.path.join(base, filename)
127
+ fn = filename
128
+
129
+ is_img = fn.lower().endswith(('.png','.ico','.gif','.icns','.svg','.jpg','.jpeg','.bmp'))
130
+ if is_img:
131
+ candidates = [
132
+ os.path.join(base, 'images', fn),
133
+ os.path.join(base, 'setiastro', 'images', fn),
134
+ os.path.join(base, 'setiastro', 'saspro', 'images', fn),
135
+ ]
136
+ for p in candidates:
137
+ if os.path.exists(p):
138
+ return p
139
+
140
+ # data / other files
141
+ candidates = [
142
+ os.path.join(base, fn),
143
+ os.path.join(base, 'setiastro', fn),
144
+ os.path.join(base, 'setiastro', 'saspro', fn),
145
+ ]
146
+ for p in candidates:
147
+ if os.path.exists(p):
148
+ return p
149
+
150
+ return os.path.join(base, fn)
137
151
 
138
152
 
139
153
  class Icons:
@@ -210,6 +224,7 @@ class Icons:
210
224
  ROTATE_CW = property(lambda self: _resource_path('rotateclockwise.png'))
211
225
  ROTATE_CCW = property(lambda self: _resource_path('rotatecounterclockwise.png'))
212
226
  ROTATE_180 = property(lambda self: _resource_path('rotate180.png'))
227
+ ROTATE_ANY = property(lambda self: _resource_path('rotatearbitrary.png'))
213
228
  RESCALE = property(lambda self: _resource_path('rescale.png'))
214
229
 
215
230
  # Masks
@@ -230,6 +245,39 @@ class Icons:
230
245
  LIVE_STACKING = property(lambda self: _resource_path('livestacking.png'))
231
246
  IMAGE_COMBINE = property(lambda self: _resource_path('imagecombine.png'))
232
247
 
248
+ # Moon phase (WIMS)
249
+ MOON_NEW = property(lambda self: _resource_path('new_moon.png'))
250
+ MOON_WAXING_CRES_1 = property(lambda self: _resource_path('waxing_crescent_1.png'))
251
+ MOON_WAXING_CRES_2 = property(lambda self: _resource_path('waxing_crescent_2.png'))
252
+ MOON_WAXING_CRES_3 = property(lambda self: _resource_path('waxing_crescent_3.png'))
253
+ MOON_WAXING_CRES_4 = property(lambda self: _resource_path('waxing_crescent_4.png'))
254
+ MOON_WAXING_CRES_5 = property(lambda self: _resource_path('waxing_crescent_5.png'))
255
+
256
+ MOON_FIRST_QUARTER = property(lambda self: _resource_path('first_quarter.png'))
257
+
258
+ MOON_WAXING_GIB_1 = property(lambda self: _resource_path('waxing_gibbous_1.png'))
259
+ MOON_WAXING_GIB_2 = property(lambda self: _resource_path('waxing_gibbous_2.png'))
260
+ MOON_WAXING_GIB_3 = property(lambda self: _resource_path('waxing_gibbous_3.png'))
261
+ MOON_WAXING_GIB_4 = property(lambda self: _resource_path('waxing_gibbous_4.png'))
262
+ MOON_WAXING_GIB_5 = property(lambda self: _resource_path('waxing_gibbous_5.png'))
263
+
264
+ MOON_FULL = property(lambda self: _resource_path('full_moon.png'))
265
+
266
+ MOON_WANING_GIB_1 = property(lambda self: _resource_path('waning_gibbous_1.png'))
267
+ MOON_WANING_GIB_2 = property(lambda self: _resource_path('waning_gibbous_2.png'))
268
+ MOON_WANING_GIB_3 = property(lambda self: _resource_path('waning_gibbous_3.png'))
269
+ MOON_WANING_GIB_4 = property(lambda self: _resource_path('waning_gibbous_4.png'))
270
+ MOON_WANING_GIB_5 = property(lambda self: _resource_path('waning_gibbous_5.png'))
271
+
272
+ MOON_LAST_QUARTER = property(lambda self: _resource_path('last_quarter.png'))
273
+
274
+ MOON_WANING_CRES_1 = property(lambda self: _resource_path('waning_crescent_1.png'))
275
+ MOON_WANING_CRES_2 = property(lambda self: _resource_path('waning_crescent_2.png'))
276
+ MOON_WANING_CRES_3 = property(lambda self: _resource_path('waning_crescent_3.png'))
277
+ MOON_WANING_CRES_4 = property(lambda self: _resource_path('waning_crescent_4.png'))
278
+ MOON_WANING_CRES_5 = property(lambda self: _resource_path('waning_crescent_5.png'))
279
+
280
+
233
281
  # Special features
234
282
  SUPERNOVA = property(lambda self: _resource_path('supernova.png'))
235
283
  PEDESTAL = property(lambda self: _resource_path('pedestal.png'))
@@ -269,6 +317,7 @@ class Icons:
269
317
  CSV = property(lambda self: _resource_path('cvs.png'))
270
318
  PPP = property(lambda self: _resource_path('ppp.png'))
271
319
  SCRIPT = property(lambda self: _resource_path('script.png'))
320
+ ACV = property(lambda self: _resource_path('acv_icon.png'))
272
321
 
273
322
  # Blink & comparison
274
323
  BLINK = property(lambda self: _resource_path('blink.png'))
@@ -378,6 +427,39 @@ def _init_legacy_paths():
378
427
  'slot7_path': get_icon_path('slot7.png'),
379
428
  'slot8_path': get_icon_path('slot8.png'),
380
429
  'slot9_path': get_icon_path('slot9.png'),
430
+ 'acv_icon_path': get_icon_path('acv_icon.png'),
431
+
432
+ 'moon_new_path': get_icon_path('new_moon.png'),
433
+ 'moon_waxing_crescent_1_path': get_icon_path('waxing_crescent_1.png'),
434
+ 'moon_waxing_crescent_2_path': get_icon_path('waxing_crescent_2.png'),
435
+ 'moon_waxing_crescent_3_path': get_icon_path('waxing_crescent_3.png'),
436
+ 'moon_waxing_crescent_4_path': get_icon_path('waxing_crescent_4.png'),
437
+ 'moon_waxing_crescent_5_path': get_icon_path('waxing_crescent_5.png'),
438
+
439
+ 'moon_first_quarter_path': get_icon_path('first_quarter.png'),
440
+
441
+ 'moon_waxing_gibbous_1_path': get_icon_path('waxing_gibbous_1.png'),
442
+ 'moon_waxing_gibbous_2_path': get_icon_path('waxing_gibbous_2.png'),
443
+ 'moon_waxing_gibbous_3_path': get_icon_path('waxing_gibbous_3.png'),
444
+ 'moon_waxing_gibbous_4_path': get_icon_path('waxing_gibbous_4.png'),
445
+ 'moon_waxing_gibbous_5_path': get_icon_path('waxing_gibbous_5.png'),
446
+
447
+ 'moon_full_path': get_icon_path('full_moon.png'),
448
+
449
+ 'moon_waning_gibbous_1_path': get_icon_path('waning_gibbous_1.png'),
450
+ 'moon_waning_gibbous_2_path': get_icon_path('waning_gibbous_2.png'),
451
+ 'moon_waning_gibbous_3_path': get_icon_path('waning_gibbous_3.png'),
452
+ 'moon_waning_gibbous_4_path': get_icon_path('waning_gibbous_4.png'),
453
+ 'moon_waning_gibbous_5_path': get_icon_path('waning_gibbous_5.png'),
454
+
455
+ 'moon_last_quarter_path': get_icon_path('last_quarter.png'),
456
+
457
+ 'moon_waning_crescent_1_path': get_icon_path('waning_crescent_1.png'),
458
+ 'moon_waning_crescent_2_path': get_icon_path('waning_crescent_2.png'),
459
+ 'moon_waning_crescent_3_path': get_icon_path('waning_crescent_3.png'),
460
+ 'moon_waning_crescent_4_path': get_icon_path('waning_crescent_4.png'),
461
+ 'moon_waning_crescent_5_path': get_icon_path('waning_crescent_5.png'),
462
+
381
463
  'rgbcombo_path': get_icon_path('rgbcombo.png'),
382
464
  'rgbextract_path': get_icon_path('rgbextract.png'),
383
465
  'copyslot_path': get_icon_path('copyslot.png'),
@@ -395,6 +477,7 @@ def _init_legacy_paths():
395
477
  'rotateclockwise_path': get_icon_path('rotateclockwise.png'),
396
478
  'rotatecounterclockwise_path': get_icon_path('rotatecounterclockwise.png'),
397
479
  'rotate180_path': get_icon_path('rotate180.png'),
480
+ 'rotatearbitrary_path': get_icon_path('rotatearbitrary.png'),
398
481
  'maskcreate_path': get_icon_path('maskcreate.png'),
399
482
  'maskapply_path': get_icon_path('maskapply.png'),
400
483
  'maskremove_path': get_icon_path('maskremove.png'),
@@ -471,9 +554,12 @@ globals().update(_legacy)
471
554
 
472
555
 
473
556
  # Background for startup
474
- background_startup_path = os.path.join(_get_base_path(), 'images', 'Background_startup.jpg')
557
+ background_startup_path = _resource_path('Background_startup.jpg')
475
558
  _legacy['background_startup_path'] = background_startup_path
476
559
 
560
+ # QML helper
561
+ resource_monitor_qml = _resource_path(os.path.join("qml", "ResourceMonitor.qml"))
562
+
477
563
  # Export list for `from setiastro.saspro.resources import *`
478
564
  __all__ = [
479
565
  'Icons', 'Resources',