setiastrosuitepro 1.6.0__py3-none-any.whl → 1.6.4.post1__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 (293) hide show
  1. setiastro/data/SASP_data.fits +0 -0
  2. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  3. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  4. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  5. setiastro/data/catalogs/cali2.csv +63 -0
  6. setiastro/data/catalogs/cali2color.csv +65 -0
  7. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  8. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  9. setiastro/data/catalogs/detected_stars.csv +24784 -0
  10. setiastro/data/catalogs/fits_header_data.csv +46 -0
  11. setiastro/data/catalogs/test.csv +8 -0
  12. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  13. setiastro/images/Astro_Spikes.png +0 -0
  14. setiastro/images/Background_startup.jpg +0 -0
  15. setiastro/images/HRDiagram.png +0 -0
  16. setiastro/images/LExtract.png +0 -0
  17. setiastro/images/LInsert.png +0 -0
  18. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  19. setiastro/images/RGB080604.png +0 -0
  20. setiastro/images/abeicon.png +0 -0
  21. setiastro/images/aberration.png +0 -0
  22. setiastro/images/andromedatry.png +0 -0
  23. setiastro/images/andromedatry_satellited.png +0 -0
  24. setiastro/images/annotated.png +0 -0
  25. setiastro/images/aperture.png +0 -0
  26. setiastro/images/astrosuite.ico +0 -0
  27. setiastro/images/astrosuite.png +0 -0
  28. setiastro/images/astrosuitepro.icns +0 -0
  29. setiastro/images/astrosuitepro.ico +0 -0
  30. setiastro/images/astrosuitepro.png +0 -0
  31. setiastro/images/background.png +0 -0
  32. setiastro/images/background2.png +0 -0
  33. setiastro/images/benchmark.png +0 -0
  34. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  36. setiastro/images/blaster.png +0 -0
  37. setiastro/images/blink.png +0 -0
  38. setiastro/images/clahe.png +0 -0
  39. setiastro/images/collage.png +0 -0
  40. setiastro/images/colorwheel.png +0 -0
  41. setiastro/images/contsub.png +0 -0
  42. setiastro/images/convo.png +0 -0
  43. setiastro/images/copyslot.png +0 -0
  44. setiastro/images/cosmic.png +0 -0
  45. setiastro/images/cosmicsat.png +0 -0
  46. setiastro/images/crop1.png +0 -0
  47. setiastro/images/cropicon.png +0 -0
  48. setiastro/images/curves.png +0 -0
  49. setiastro/images/cvs.png +0 -0
  50. setiastro/images/debayer.png +0 -0
  51. setiastro/images/denoise_cnn_custom.png +0 -0
  52. setiastro/images/denoise_cnn_graph.png +0 -0
  53. setiastro/images/disk.png +0 -0
  54. setiastro/images/dse.png +0 -0
  55. setiastro/images/exoicon.png +0 -0
  56. setiastro/images/eye.png +0 -0
  57. setiastro/images/fliphorizontal.png +0 -0
  58. setiastro/images/flipvertical.png +0 -0
  59. setiastro/images/font.png +0 -0
  60. setiastro/images/freqsep.png +0 -0
  61. setiastro/images/functionbundle.png +0 -0
  62. setiastro/images/graxpert.png +0 -0
  63. setiastro/images/green.png +0 -0
  64. setiastro/images/gridicon.png +0 -0
  65. setiastro/images/halo.png +0 -0
  66. setiastro/images/hdr.png +0 -0
  67. setiastro/images/histogram.png +0 -0
  68. setiastro/images/hubble.png +0 -0
  69. setiastro/images/imagecombine.png +0 -0
  70. setiastro/images/invert.png +0 -0
  71. setiastro/images/isophote.png +0 -0
  72. setiastro/images/isophote_demo_figure.png +0 -0
  73. setiastro/images/isophote_demo_image.png +0 -0
  74. setiastro/images/isophote_demo_model.png +0 -0
  75. setiastro/images/isophote_demo_residual.png +0 -0
  76. setiastro/images/jwstpupil.png +0 -0
  77. setiastro/images/linearfit.png +0 -0
  78. setiastro/images/livestacking.png +0 -0
  79. setiastro/images/mask.png +0 -0
  80. setiastro/images/maskapply.png +0 -0
  81. setiastro/images/maskcreate.png +0 -0
  82. setiastro/images/maskremove.png +0 -0
  83. setiastro/images/morpho.png +0 -0
  84. setiastro/images/mosaic.png +0 -0
  85. setiastro/images/multiscale_decomp.png +0 -0
  86. setiastro/images/nbtorgb.png +0 -0
  87. setiastro/images/neutral.png +0 -0
  88. setiastro/images/nuke.png +0 -0
  89. setiastro/images/openfile.png +0 -0
  90. setiastro/images/pedestal.png +0 -0
  91. setiastro/images/pen.png +0 -0
  92. setiastro/images/pixelmath.png +0 -0
  93. setiastro/images/platesolve.png +0 -0
  94. setiastro/images/ppp.png +0 -0
  95. setiastro/images/pro.png +0 -0
  96. setiastro/images/project.png +0 -0
  97. setiastro/images/psf.png +0 -0
  98. setiastro/images/redo.png +0 -0
  99. setiastro/images/redoicon.png +0 -0
  100. setiastro/images/rescale.png +0 -0
  101. setiastro/images/rgbalign.png +0 -0
  102. setiastro/images/rgbcombo.png +0 -0
  103. setiastro/images/rgbextract.png +0 -0
  104. setiastro/images/rotate180.png +0 -0
  105. setiastro/images/rotatearbitrary.png +0 -0
  106. setiastro/images/rotateclockwise.png +0 -0
  107. setiastro/images/rotatecounterclockwise.png +0 -0
  108. setiastro/images/satellite.png +0 -0
  109. setiastro/images/script.png +0 -0
  110. setiastro/images/selectivecolor.png +0 -0
  111. setiastro/images/simbad.png +0 -0
  112. setiastro/images/slot0.png +0 -0
  113. setiastro/images/slot1.png +0 -0
  114. setiastro/images/slot2.png +0 -0
  115. setiastro/images/slot3.png +0 -0
  116. setiastro/images/slot4.png +0 -0
  117. setiastro/images/slot5.png +0 -0
  118. setiastro/images/slot6.png +0 -0
  119. setiastro/images/slot7.png +0 -0
  120. setiastro/images/slot8.png +0 -0
  121. setiastro/images/slot9.png +0 -0
  122. setiastro/images/spcc.png +0 -0
  123. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  124. setiastro/images/spinner.gif +0 -0
  125. setiastro/images/stacking.png +0 -0
  126. setiastro/images/staradd.png +0 -0
  127. setiastro/images/staralign.png +0 -0
  128. setiastro/images/starnet.png +0 -0
  129. setiastro/images/starregistration.png +0 -0
  130. setiastro/images/starspike.png +0 -0
  131. setiastro/images/starstretch.png +0 -0
  132. setiastro/images/statstretch.png +0 -0
  133. setiastro/images/supernova.png +0 -0
  134. setiastro/images/uhs.png +0 -0
  135. setiastro/images/undoicon.png +0 -0
  136. setiastro/images/upscale.png +0 -0
  137. setiastro/images/viewbundle.png +0 -0
  138. setiastro/images/whitebalance.png +0 -0
  139. setiastro/images/wimi_icon_256x256.png +0 -0
  140. setiastro/images/wimilogo.png +0 -0
  141. setiastro/images/wims.png +0 -0
  142. setiastro/images/wrench_icon.png +0 -0
  143. setiastro/images/xisfliberator.png +0 -0
  144. setiastro/qml/ResourceMonitor.qml +126 -0
  145. setiastro/saspro/__main__.py +228 -67
  146. setiastro/saspro/_generated/build_info.py +2 -1
  147. setiastro/saspro/abe.py +76 -25
  148. setiastro/saspro/aberration_ai.py +14 -14
  149. setiastro/saspro/add_stars.py +15 -12
  150. setiastro/saspro/astrobin_exporter.py +61 -58
  151. setiastro/saspro/astrospike_python.py +3 -1
  152. setiastro/saspro/autostretch.py +4 -2
  153. setiastro/saspro/backgroundneutral.py +65 -14
  154. setiastro/saspro/batch_convert.py +8 -5
  155. setiastro/saspro/batch_renamer.py +39 -36
  156. setiastro/saspro/blemish_blaster.py +15 -12
  157. setiastro/saspro/blink_comparator_pro.py +605 -379
  158. setiastro/saspro/cheat_sheet.py +62 -17
  159. setiastro/saspro/clahe.py +34 -8
  160. setiastro/saspro/comet_stacking.py +103 -38
  161. setiastro/saspro/common_tr.py +107 -0
  162. setiastro/saspro/continuum_subtract.py +7 -7
  163. setiastro/saspro/convo.py +12 -9
  164. setiastro/saspro/copyastro.py +3 -0
  165. setiastro/saspro/cosmicclarity.py +77 -52
  166. setiastro/saspro/crop_dialog_pro.py +80 -45
  167. setiastro/saspro/curve_editor_pro.py +51 -33
  168. setiastro/saspro/debayer.py +6 -3
  169. setiastro/saspro/doc_manager.py +49 -19
  170. setiastro/saspro/exoplanet_detector.py +11 -11
  171. setiastro/saspro/fitsmodifier.py +48 -44
  172. setiastro/saspro/fix_bom.py +32 -0
  173. setiastro/saspro/frequency_separation.py +18 -12
  174. setiastro/saspro/function_bundle.py +18 -16
  175. setiastro/saspro/generate_translations.py +3092 -0
  176. setiastro/saspro/ghs_dialog_pro.py +19 -16
  177. setiastro/saspro/graxpert.py +3 -0
  178. setiastro/saspro/gui/main_window.py +471 -126
  179. setiastro/saspro/gui/mixins/dock_mixin.py +123 -11
  180. setiastro/saspro/gui/mixins/file_mixin.py +25 -20
  181. setiastro/saspro/gui/mixins/geometry_mixin.py +115 -15
  182. setiastro/saspro/gui/mixins/header_mixin.py +6 -6
  183. setiastro/saspro/gui/mixins/mask_mixin.py +8 -8
  184. setiastro/saspro/gui/mixins/menu_mixin.py +62 -33
  185. setiastro/saspro/gui/mixins/toolbar_mixin.py +382 -226
  186. setiastro/saspro/gui/mixins/update_mixin.py +26 -26
  187. setiastro/saspro/gui/statistics_dialog.py +47 -0
  188. setiastro/saspro/halobgon.py +29 -3
  189. setiastro/saspro/header_viewer.py +21 -18
  190. setiastro/saspro/histogram.py +29 -26
  191. setiastro/saspro/history_explorer.py +2 -0
  192. setiastro/saspro/i18n.py +168 -0
  193. setiastro/saspro/image_combine.py +3 -0
  194. setiastro/saspro/image_peeker_pro.py +52 -44
  195. setiastro/saspro/imageops/stretch.py +5 -13
  196. setiastro/saspro/isophote.py +3 -0
  197. setiastro/saspro/legacy/numba_utils.py +64 -47
  198. setiastro/saspro/linear_fit.py +3 -0
  199. setiastro/saspro/live_stacking.py +13 -2
  200. setiastro/saspro/mask_creation.py +180 -22
  201. setiastro/saspro/mfdeconv.py +5 -0
  202. setiastro/saspro/morphology.py +38 -13
  203. setiastro/saspro/multiscale_decomp.py +713 -256
  204. setiastro/saspro/nbtorgb_stars.py +12 -2
  205. setiastro/saspro/numba_utils.py +149 -48
  206. setiastro/saspro/ops/scripts.py +77 -17
  207. setiastro/saspro/ops/settings.py +177 -100
  208. setiastro/saspro/perfect_palette_picker.py +25 -7
  209. setiastro/saspro/pixelmath.py +114 -110
  210. setiastro/saspro/plate_solver.py +118 -108
  211. setiastro/saspro/remove_green.py +24 -7
  212. setiastro/saspro/remove_stars.py +136 -162
  213. setiastro/saspro/remove_stars_preset.py +55 -13
  214. setiastro/saspro/resources.py +46 -15
  215. setiastro/saspro/rgb_combination.py +19 -18
  216. setiastro/saspro/rgbalign.py +11 -11
  217. setiastro/saspro/save_options.py +5 -4
  218. setiastro/saspro/selective_color.py +84 -25
  219. setiastro/saspro/sfcc.py +119 -72
  220. setiastro/saspro/shortcuts.py +345 -36
  221. setiastro/saspro/signature_insert.py +4 -1
  222. setiastro/saspro/stacking_suite.py +2066 -1119
  223. setiastro/saspro/star_alignment.py +291 -331
  224. setiastro/saspro/star_spikes.py +137 -53
  225. setiastro/saspro/star_stretch.py +47 -10
  226. setiastro/saspro/stat_stretch.py +52 -16
  227. setiastro/saspro/status_log_dock.py +1 -1
  228. setiastro/saspro/subwindow.py +97 -36
  229. setiastro/saspro/supernovaasteroidhunter.py +68 -61
  230. setiastro/saspro/swap_manager.py +77 -42
  231. setiastro/saspro/translations/all_source_strings.json +4726 -0
  232. setiastro/saspro/translations/ar_translations.py +4096 -0
  233. setiastro/saspro/translations/de_translations.py +3728 -0
  234. setiastro/saspro/translations/es_translations.py +4169 -0
  235. setiastro/saspro/translations/fr_translations.py +4090 -0
  236. setiastro/saspro/translations/hi_translations.py +3803 -0
  237. setiastro/saspro/translations/integrate_translations.py +271 -0
  238. setiastro/saspro/translations/it_translations.py +4728 -0
  239. setiastro/saspro/translations/ja_translations.py +3834 -0
  240. setiastro/saspro/translations/pt_translations.py +3847 -0
  241. setiastro/saspro/translations/ru_translations.py +3082 -0
  242. setiastro/saspro/translations/saspro_ar.qm +0 -0
  243. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  244. setiastro/saspro/translations/saspro_de.qm +0 -0
  245. setiastro/saspro/translations/saspro_de.ts +14548 -0
  246. setiastro/saspro/translations/saspro_es.qm +0 -0
  247. setiastro/saspro/translations/saspro_es.ts +16202 -0
  248. setiastro/saspro/translations/saspro_fr.qm +0 -0
  249. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  250. setiastro/saspro/translations/saspro_hi.qm +0 -0
  251. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  252. setiastro/saspro/translations/saspro_it.qm +0 -0
  253. setiastro/saspro/translations/saspro_it.ts +19046 -0
  254. setiastro/saspro/translations/saspro_ja.qm +0 -0
  255. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  256. setiastro/saspro/translations/saspro_pt.qm +0 -0
  257. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  258. setiastro/saspro/translations/saspro_ru.qm +0 -0
  259. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  260. setiastro/saspro/translations/saspro_sw.qm +0 -0
  261. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  262. setiastro/saspro/translations/saspro_uk.qm +0 -0
  263. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  264. setiastro/saspro/translations/saspro_zh.qm +0 -0
  265. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  266. setiastro/saspro/translations/sw_translations.py +3897 -0
  267. setiastro/saspro/translations/uk_translations.py +3929 -0
  268. setiastro/saspro/translations/zh_translations.py +3910 -0
  269. setiastro/saspro/versioning.py +77 -0
  270. setiastro/saspro/view_bundle.py +20 -17
  271. setiastro/saspro/wavescale_hdr.py +54 -33
  272. setiastro/saspro/wavescale_hdr_preset.py +6 -5
  273. setiastro/saspro/wavescalede.py +54 -31
  274. setiastro/saspro/wavescalede_preset.py +9 -7
  275. setiastro/saspro/whitebalance.py +58 -22
  276. setiastro/saspro/widgets/common_utilities.py +12 -11
  277. setiastro/saspro/widgets/minigame/game.js +991 -0
  278. setiastro/saspro/widgets/minigame/index.html +53 -0
  279. setiastro/saspro/widgets/minigame/style.css +241 -0
  280. setiastro/saspro/widgets/preview_dialogs.py +8 -8
  281. setiastro/saspro/widgets/resource_monitor.py +263 -0
  282. setiastro/saspro/widgets/spinboxes.py +18 -0
  283. setiastro/saspro/widgets/wavelet_utils.py +52 -20
  284. setiastro/saspro/wimi.py +7996 -0
  285. setiastro/saspro/wims.py +578 -0
  286. setiastro/saspro/window_shelf.py +2 -2
  287. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/METADATA +15 -3
  288. setiastrosuitepro-1.6.4.post1.dist-info/RECORD +368 -0
  289. setiastrosuitepro-1.6.0.dist-info/RECORD +0 -174
  290. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/WHEEL +0 -0
  291. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/entry_points.txt +0 -0
  292. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/LICENSE +0 -0
  293. {setiastrosuitepro-1.6.0.dist-info → setiastrosuitepro-1.6.4.post1.dist-info}/licenses/license.txt +0 -0
@@ -156,12 +156,15 @@ class RemoveGreenDialog(QDialog):
156
156
  super().__init__(parent)
157
157
  self.main = main
158
158
  self.doc = doc
159
- self.setWindowTitle("Remove Green (SCNR)")
159
+ self.setWindowTitle(self.tr("Remove Green (SCNR)"))
160
+ self.setWindowFlag(Qt.WindowType.Window, True)
161
+ self.setWindowModality(Qt.WindowModality.NonModal)
162
+ self.setModal(False)
160
163
  self._build_ui()
161
164
 
162
165
  def _build_ui(self):
163
166
  lay = QVBoxLayout(self)
164
- lay.addWidget(QLabel("Select the amount to remove green noise:"))
167
+ lay.addWidget(QLabel(self.tr("Select the amount to remove green noise:")))
165
168
 
166
169
  # amount
167
170
  self.slider = QSlider(Qt.Orientation.Horizontal)
@@ -176,7 +179,7 @@ class RemoveGreenDialog(QDialog):
176
179
 
177
180
  # mode dropdown
178
181
  row_mode = QHBoxLayout()
179
- row_mode.addWidget(QLabel("Neutral mode:"))
182
+ row_mode.addWidget(QLabel(self.tr("Neutral mode:")))
180
183
  self.mode_box = QComboBox()
181
184
  # order: avg (default), max, min
182
185
  self.mode_box.addItem(_SCNR_MODE_LABELS["avg"], userData="avg")
@@ -188,14 +191,14 @@ class RemoveGreenDialog(QDialog):
188
191
  lay.addLayout(row_mode)
189
192
 
190
193
  # preserve lightness
191
- self.cb_preserve = QCheckBox("Preserve lightness")
194
+ self.cb_preserve = QCheckBox(self.tr("Preserve lightness"))
192
195
  self.cb_preserve.setChecked(True)
193
196
  lay.addWidget(self.cb_preserve)
194
197
 
195
198
  # buttons
196
199
  row = QHBoxLayout()
197
- btn_apply = QPushButton("Apply"); btn_apply.clicked.connect(self._apply)
198
- btn_cancel= QPushButton("Cancel"); btn_cancel.clicked.connect(self.reject)
200
+ btn_apply = QPushButton(self.tr("Apply")); btn_apply.clicked.connect(self._apply)
201
+ btn_cancel= QPushButton(self.tr("Cancel")); btn_cancel.clicked.connect(self.reject)
199
202
  row.addStretch(1); row.addWidget(btn_apply); row.addWidget(btn_cancel)
200
203
  lay.addLayout(row)
201
204
 
@@ -263,8 +266,22 @@ class RemoveGreenDialog(QDialog):
263
266
  # Never let replay bookkeeping kill the dialog
264
267
  pass
265
268
 
266
- self.accept()
269
+ # Dialog stays open so user can apply to other images
270
+ # Refresh document reference for next operation
271
+ self._refresh_document_from_active()
267
272
 
273
+ def _refresh_document_from_active(self):
274
+ """
275
+ Refresh the dialog's document reference to the currently active document.
276
+ This allows reusing the same dialog on different images.
277
+ """
278
+ try:
279
+ if self.main and hasattr(self.main, "_active_doc"):
280
+ new_doc = self.main._active_doc()
281
+ if new_doc is not None and new_doc is not self.doc:
282
+ self.doc = new_doc
283
+ except Exception:
284
+ pass
268
285
 
269
286
 
270
287
  # ---------- entry points used by main ----------
@@ -136,154 +136,82 @@ def _mtf_params_unlinked(img_rgb01: np.ndarray,
136
136
  Works on float32 data assumed in [0,1].
137
137
  Returns dict with arrays: {'s': (C,), 'm': (C,), 'h': (C,)}.
138
138
  """
139
- x = np.asarray(img_rgb01, dtype=np.float32)
140
- # Force 3 channels internally (Siril expects 1 or 3; we always give it 3 here)
141
- if x.ndim == 2:
142
- x = np.stack([x] * 3, axis=-1)
143
- elif x.ndim == 3 and x.shape[2] == 1:
144
- x = np.repeat(x, 3, axis=2)
145
-
146
- C = x.shape[2]
147
- s = np.zeros(C, dtype=np.float32)
148
- m = np.zeros(C, dtype=np.float32)
149
- h = np.zeros(C, dtype=np.float32)
150
-
151
- med = np.zeros(C, dtype=np.float32)
152
- mad = np.zeros(C, dtype=np.float32)
153
- inverted_flags = np.zeros(C, dtype=bool)
154
-
155
- # --- stats per channel (Siril: median / normValue, mad / normValue * MAD_NORM) ---
156
- # Here normValue == 1.0 because we're already in [0,1]
157
- for c in range(C):
158
- ch = x[..., c].astype(np.float32, copy=False)
159
- med_c = float(np.median(ch))
160
- mad_raw = float(np.median(np.abs(ch - med_c)))
161
- mad_c = mad_raw * _MAD_NORM
162
- if mad_c == 0.0:
163
- mad_c = 0.001
164
-
165
- med[c] = med_c
166
- mad[c] = mad_c
167
- if med_c > 0.5:
168
- inverted_flags[c] = True
169
-
170
- inverted_channels = int(inverted_flags.sum())
171
-
172
- # --- Main branch (non-inverted dominant) ---
173
- if inverted_channels < C:
174
- for c in range(C):
175
- median = float(med[c])
176
- mad_c = float(mad[c])
177
-
178
- c0 = median + shadows_clipping * mad_c
179
- if c0 < 0.0:
180
- c0 = 0.0
181
- # Siril: m2 = median - c0; midtones = MTF(m2, target_bg, 0,1)
182
- m2 = median - c0
183
- mid = float(_mtf_scalar(m2, targetbg, 0.0, 1.0))
184
-
185
- s[c] = c0
186
- m[c] = mid
187
- h[c] = 1.0
188
-
189
- # --- Inverted channel branch ---
190
- else:
191
- for c in range(C):
192
- median = float(med[c])
193
- mad_c = float(mad[c])
194
-
195
- c1 = median - shadows_clipping * mad_c
196
- if c1 > 1.0:
197
- c1 = 1.0
198
- m2 = c1 - median
199
- mid = 1.0 - float(_mtf_scalar(m2, targetbg, 0.0, 1.0))
200
-
201
- s[c] = 0.0
202
- m[c] = mid
203
- h[c] = c1
204
-
205
- return {"s": s, "m": m, "h": h}
206
-
207
-
208
- def _apply_mtf_unlinked_rgb(img_rgb01: np.ndarray, p: dict) -> np.ndarray:
209
139
  """
210
- Apply per-channel MTF exactly. p from _mtf_params_unlinked.
140
+ Siril-style per-channel MTF parameter estimation, matching
141
+ find_unlinked_midtones_balance_default() / find_unlinked_midtones_balance().
142
+
143
+ Works on float32 data assumed in [0,1].
144
+ Returns dict with arrays: {'s': (C,), 'm': (C,), 'h': (C,)}.
211
145
  """
212
146
  x = np.asarray(img_rgb01, dtype=np.float32)
147
+
148
+ # Analyze input shape to handle mono efficiently
213
149
  if x.ndim == 2:
214
- x = np.stack([x]*3, axis=-1)
150
+ # (H, W) -> treat as single channel
151
+ x_in = x[..., None] # Virtual 3D (H,W,1)
152
+ C_in = 1
215
153
  elif x.ndim == 3 and x.shape[2] == 1:
216
- x = np.repeat(x, 3, axis=2)
217
-
218
- out = np.empty_like(x, dtype=np.float32)
219
- for c in range(x.shape[2]):
220
- out[..., c] = _mtf_apply(x[..., c], float(p["s"][c]), float(p["m"][c]), float(p["h"][c]))
221
- return np.clip(out, 0.0, 1.0)
222
-
223
-
224
- def _invert_mtf_unlinked_rgb(img_rgb01: np.ndarray, p: dict) -> np.ndarray:
225
- """
226
- Exact analytic inverse per channel (uses same s/m/h arrays).
227
- """
228
- y = np.asarray(img_rgb01, dtype=np.float32)
229
- if y.ndim == 2:
230
- y = np.stack([y]*3, axis=-1)
231
- elif y.ndim == 3 and y.shape[2] == 1:
232
- y = np.repeat(y, 3, axis=2)
233
-
234
- out = np.empty_like(y, dtype=np.float32)
235
- for c in range(y.shape[2]):
236
- out[..., c] = _mtf_inverse(y[..., c], float(p["s"][c]), float(p["m"][c]), float(p["h"][c]))
237
- return np.clip(out, 0.0, 1.0)
238
-
239
- def _stat_stretch_rgb(img: np.ndarray,
240
- lo_pct: float = 0.25,
241
- hi_pct: float = 99.75) -> tuple[np.ndarray, dict]:
242
- """
243
- Make sure img is RGB float32 in [0,1], stretch each channel to [0,1]
244
- using percentiles. Returns (stretched_img, params) where params can be
245
- fed to _stat_unstretch_rgb() to invert exactly.
246
- """
247
- was_single = (img.ndim == 2) or (img.ndim == 3 and img.shape[2] == 1)
248
- if was_single:
249
- img = np.stack([img] * 3, axis=-1)
250
-
251
- x = img.astype(np.float32, copy=False)
252
- out = np.empty_like(x, dtype=np.float32)
253
- lo_vals, hi_vals = [], []
154
+ x_in = x
155
+ C_in = 1
156
+ else:
157
+ x_in = x
158
+ C_in = x.shape[2]
159
+
160
+ # Vectorized stats calculation on actual data only
161
+ med = np.median(x_in, axis=(0, 1)).astype(np.float32) # shape (C_in,)
162
+
163
+ # MAD requires centered abs diff
164
+ diff = np.abs(x_in - med.reshape(1, 1, C_in))
165
+ mad_raw = np.median(diff, axis=(0, 1)).astype(np.float32) # shape (C_in,)
166
+
167
+ mad = mad_raw * _MAD_NORM
168
+ mad[mad == 0] = 0.001
169
+
170
+ inverted_flags = (med > 0.5)
171
+ # If mono, we just check the one channel. If RGB, we check all.
172
+ # Logic below assumes we return 3-channel params s,m,h even for mono input (broadcasted).
173
+
174
+ # To match original behavior which always returned 3-element arrays for s,m,h:
175
+ # We will compute s_in, m_in, h_in for the input channels, then broadcast to 3.
176
+
177
+ s_in = np.zeros(C_in, dtype=np.float32)
178
+ m_in = np.zeros(C_in, dtype=np.float32)
179
+ h_in = np.zeros(C_in, dtype=np.float32)
180
+
181
+ # We iterate C_in times (1 or 3)
182
+ for c in range(C_in):
183
+ is_inv = inverted_flags[c]
184
+ md = med[c]
185
+ md_dev = mad[c]
186
+
187
+ if not is_inv:
188
+ # Normal
189
+ c0 = max(md + shadows_clipping * md_dev, 0.0)
190
+ m2 = md - c0
191
+
192
+ s_in[c] = c0
193
+ m_in[c] = float(_mtf_scalar(m2, targetbg, 0.0, 1.0))
194
+ h_in[c] = 1.0
195
+ else:
196
+ # Inverted
197
+ c1 = min(md - shadows_clipping * md_dev, 1.0)
198
+ m2 = c1 - md
199
+
200
+ s_in[c] = 0.0
201
+ m_in[c] = 1.0 - float(_mtf_scalar(m2, targetbg, 0.0, 1.0))
202
+ h_in[c] = c1
203
+
204
+ # Broadcast to 3 channels if needed
205
+ if C_in == 1:
206
+ s = np.repeat(s_in, 3)
207
+ m = np.repeat(m_in, 3)
208
+ h = np.repeat(h_in, 3)
209
+ else:
210
+ s = s_in
211
+ m = m_in
212
+ h = h_in
254
213
 
255
- for c in range(3):
256
- ch = x[..., c]
257
- lo = float(np.percentile(ch, lo_pct))
258
- hi = float(np.percentile(ch, hi_pct))
259
- if not np.isfinite(lo): lo = 0.0
260
- if not np.isfinite(hi): hi = 1.0
261
- if hi - lo < 1e-6:
262
- hi = lo + 1e-6
263
- lo_vals.append(lo); hi_vals.append(hi)
264
- out[..., c] = (ch - lo) / (hi - lo)
265
-
266
- out = np.clip(out, 0.0, 1.0)
267
- params = {"lo": lo_vals, "hi": hi_vals, "was_single": was_single}
268
- return out, params
269
-
270
-
271
- def _stat_unstretch_rgb(img: np.ndarray, params: dict) -> np.ndarray:
272
- """
273
- Inverse of _stat_stretch_rgb. Expects img RGB float32 [0,1].
274
- """
275
- lo = np.asarray(params["lo"], dtype=np.float32)
276
- hi = np.asarray(params["hi"], dtype=np.float32)
277
- out = img.astype(np.float32, copy=True)
278
- for c in range(3):
279
- out[..., c] = out[..., c] * (hi[c] - lo[c]) + lo[c]
280
- out = np.clip(out, 0.0, 1.0)
281
- if params.get("was_single", False):
282
- out = out.mean(axis=2, keepdims=False) # back to single channel if needed
283
- # StarNet needs RGB during processing; we keep RGB after removal for consistency.
284
- # If you want to return mono to the doc when the source was mono, do it at the very end.
285
- out = np.stack([out] * 3, axis=-1)
286
- return out
214
+ return {"s": s, "m": m, "h": h}
287
215
 
288
216
  def _mtf_scalar(x: float, m: float, lo: float = 0.0, hi: float = 1.0) -> float:
289
217
  """
@@ -324,6 +252,36 @@ def _mtf_scalar(x: float, m: float, lo: float = 0.0, hi: float = 1.0) -> float:
324
252
  return float(y)
325
253
 
326
254
 
255
+ def _apply_mtf_unlinked_rgb(img_rgb01: np.ndarray, p: dict) -> np.ndarray:
256
+ """
257
+ Apply per-channel MTF exactly. p from _mtf_params_unlinked.
258
+ """
259
+ x = np.asarray(img_rgb01, dtype=np.float32)
260
+ if x.ndim == 2:
261
+ x = np.stack([x]*3, axis=-1)
262
+ elif x.ndim == 3 and x.shape[2] == 1:
263
+ x = np.repeat(x, 3, axis=2)
264
+
265
+ out = np.empty_like(x, dtype=np.float32)
266
+ for c in range(x.shape[2]):
267
+ out[..., c] = _mtf_apply(x[..., c], float(p["s"][c]), float(p["m"][c]), float(p["h"][c]))
268
+ return np.clip(out, 0.0, 1.0)
269
+
270
+
271
+ def _invert_mtf_unlinked_rgb(img_rgb01: np.ndarray, p: dict) -> np.ndarray:
272
+ """
273
+ Exact analytic inverse per channel (uses same s/m/h arrays).
274
+ """
275
+ y = np.asarray(img_rgb01, dtype=np.float32)
276
+ if y.ndim == 2:
277
+ y = np.stack([y]*3, axis=-1)
278
+ elif y.ndim == 3 and y.shape[2] == 1:
279
+ y = np.repeat(y, 3, axis=2)
280
+
281
+ out = np.empty_like(y, dtype=np.float32)
282
+ for c in range(y.shape[2]):
283
+ out[..., c] = _mtf_inverse(y[..., c], float(p["s"][c]), float(p["m"][c]), float(p["h"][c]))
284
+ return np.clip(out, 0.0, 1.0)
327
285
  # ------------------------------------------------------------
328
286
  # Settings helper
329
287
  # ------------------------------------------------------------
@@ -370,24 +328,28 @@ def starnet_starless_from_array(arr_rgb01: np.ndarray, settings, *, tmp_prefix="
370
328
  in_path = os.path.join(workdir, f"{tmp_prefix}_in.tif")
371
329
  out_path = os.path.join(workdir, f"{tmp_prefix}_out.tif")
372
330
 
373
- # --- Normalize input shape and safe values ---
374
- x = arr
375
- if x.ndim == 2:
376
- x = np.stack([x] * 3, axis=-1)
377
- elif x.ndim == 3 and x.shape[2] == 1:
378
- x = np.repeat(x, 3, axis=2)
379
- x = np.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
331
+ # --- Normalize input shape (virtual) and safe values ---
332
+ x_in = np.asarray(arr, dtype=np.float32)
333
+
334
+ # If (H,W,1), collapse to (H,W) so mono flows cleanly
335
+ if x_in.ndim == 3 and x_in.shape[2] == 1:
336
+ x_in = x_in[..., 0]
337
+
338
+ # sanitize
339
+ x_in = np.nan_to_num(x_in, nan=0.0, posinf=0.0, neginf=0.0).astype(np.float32, copy=False)
380
340
 
381
341
  # Preserve original numeric scale if users pass >1.0
382
- xmax = float(np.max(x)) if x.size else 1.0
342
+ xmax = float(np.max(x_in)) if x_in.size else 1.0
383
343
  scale_factor = xmax if xmax > 1.01 else 1.0
384
- xin = (x / scale_factor) if scale_factor > 1.0 else x
344
+
345
+ xin = (x_in / scale_factor) if scale_factor > 1.0 else x_in
385
346
  xin = np.clip(xin, 0.0, 1.0)
386
347
 
387
348
  # --- Siril-style unlinked MTF params + pre-stretch ---
388
349
  mtf_params = _mtf_params_unlinked(xin, shadows_clipping=-2.8, targetbg=0.25)
389
350
  x_for_starnet = _apply_mtf_unlinked_rgb(xin, mtf_params).astype(np.float32, copy=False)
390
351
 
352
+
391
353
  # --- Write 16-bit TIFF for StarNet ---
392
354
  save_image(
393
355
  x_for_starnet, in_path,
@@ -662,18 +624,19 @@ def _run_starnet(main, doc):
662
624
  )
663
625
  except Exception:
664
626
  pass
665
- # --- Ensure RGB float32 in safe range
627
+ # --- Ensure RGB float32 in safe range (without expanding yet)
628
+ # Starnet needs RGB eventually, but we can compute stats/normalization on mono
666
629
  src = np.asarray(doc.image)
667
- if src.ndim == 2:
668
- processing_image = np.stack([src]*3, axis=-1)
669
- elif src.ndim == 3 and src.shape[2] == 1:
670
- processing_image = np.repeat(src, 3, axis=2)
630
+ if src.ndim == 3 and src.shape[2] == 1:
631
+ # standardizing shape is cheap
632
+ processing_image = src[..., 0]
671
633
  else:
672
634
  processing_image = src
635
+
673
636
  processing_image = np.nan_to_num(processing_image.astype(np.float32, copy=False),
674
637
  nan=0.0, posinf=0.0, neginf=0.0)
675
638
 
676
- # --- Scale normalization if >1.0 (same reason as before: 16-bit export safety)
639
+ # --- Scale normalization if >1.0
677
640
  scale_factor = float(np.max(processing_image))
678
641
  if scale_factor > 1.0:
679
642
  processing_norm = processing_image / scale_factor
@@ -1027,11 +990,10 @@ def _run_darkstar(main, doc):
1027
990
  pass
1028
991
 
1029
992
  # --- Build processing image (RGB float32, normalized) ---
993
+ # DarkStar needs RGB, but we can delay expansion until save
1030
994
  src = np.asarray(doc.image)
1031
- if src.ndim == 2:
1032
- processing_image = np.stack([src] * 3, axis=-1)
1033
- elif src.ndim == 3 and src.shape[2] == 1:
1034
- processing_image = np.repeat(src, 3, axis=2)
995
+ if src.ndim == 3 and src.shape[2] == 1:
996
+ processing_image = src[..., 0]
1035
997
  else:
1036
998
  processing_image = src
1037
999
 
@@ -1088,8 +1050,20 @@ def _run_darkstar(main, doc):
1088
1050
  # --- Save pre-stretched image as 32-bit float TIFF for DarkStar ---
1089
1051
  in_path = os.path.join(input_dir, "imagetoremovestars.tif")
1090
1052
  try:
1053
+ # Check if we need to expand on-the-fly for DarkStar (it expects RGB input)
1054
+ # If img_for_darkstar is mono, save_image might save mono.
1055
+ # "is_mono=False" flag to save_image hints we want RGB.
1056
+ # If the array is 2D, save_image might still save mono unless we feed it 3D.
1057
+ # For safety with DarkStar, we create the 3D view now if needed.
1058
+
1059
+ to_save = img_for_darkstar
1060
+ if to_save.ndim == 2:
1061
+ to_save = np.stack([to_save]*3, axis=-1)
1062
+ elif to_save.ndim == 3 and to_save.shape[2] == 1:
1063
+ to_save = np.repeat(to_save, 3, axis=2)
1064
+
1091
1065
  save_image(
1092
- img_for_darkstar,
1066
+ to_save,
1093
1067
  in_path,
1094
1068
  original_format="tif",
1095
1069
  bit_depth="32-bit floating point",
@@ -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)
@@ -77,14 +77,19 @@ def _get_base_path() -> str:
77
77
  except Exception:
78
78
  pass
79
79
 
80
- # Development: resources are in project root
80
+ # Development: resources are in package directory (src/setiastro/images/)
81
81
  # File is at: src/setiastro/saspro/resources.py
82
- # Need to go up 4 levels to reach project root
82
+ # Check if images/ exists in the setiastro package directory
83
83
  current_file = os.path.abspath(__file__)
84
- # Go up from resources.py -> saspro -> setiastro -> src -> project root
85
- base = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(current_file))))
84
+ # Go up from resources.py -> saspro -> setiastro (package directory)
85
+ package_dir = os.path.dirname(os.path.dirname(current_file))
86
+ images_dir = os.path.join(package_dir, 'images')
87
+ if os.path.exists(images_dir):
88
+ return package_dir
86
89
 
87
- # Verify we found the right directory by checking for images/ folder
90
+ # Fallback: try project root (for backward compatibility)
91
+ # Go up from resources.py -> saspro -> setiastro -> src -> project root
92
+ base = os.path.dirname(os.path.dirname(package_dir))
88
93
  images_dir = os.path.join(base, 'images')
89
94
  if os.path.exists(images_dir):
90
95
  return base
@@ -118,17 +123,31 @@ def _get_base_path() -> str:
118
123
 
119
124
 
120
125
  def _resource_path(filename: str) -> str:
121
- """Get full path to a resource file."""
122
126
  base = _get_base_path()
123
-
124
- # Check if it's an image file - look in images/ subdirectory
125
- if filename.endswith(('.png', '.ico', '.gif', '.icns', '.svg')):
126
- images_path = os.path.join(base, 'images', filename)
127
- if os.path.exists(images_path):
128
- return images_path
129
-
130
- # Fallback to root directory (for data files like .csv, .fits, etc.)
131
- 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)
132
151
 
133
152
 
134
153
  class Icons:
@@ -205,6 +224,7 @@ class Icons:
205
224
  ROTATE_CW = property(lambda self: _resource_path('rotateclockwise.png'))
206
225
  ROTATE_CCW = property(lambda self: _resource_path('rotatecounterclockwise.png'))
207
226
  ROTATE_180 = property(lambda self: _resource_path('rotate180.png'))
227
+ ROTATE_ANY = property(lambda self: _resource_path('rotatearbitrary.png'))
208
228
  RESCALE = property(lambda self: _resource_path('rescale.png'))
209
229
 
210
230
  # Masks
@@ -390,6 +410,7 @@ def _init_legacy_paths():
390
410
  'rotateclockwise_path': get_icon_path('rotateclockwise.png'),
391
411
  'rotatecounterclockwise_path': get_icon_path('rotatecounterclockwise.png'),
392
412
  'rotate180_path': get_icon_path('rotate180.png'),
413
+ 'rotatearbitrary_path': get_icon_path('rotatearbitrary.png'),
393
414
  'maskcreate_path': get_icon_path('maskcreate.png'),
394
415
  'maskapply_path': get_icon_path('maskapply.png'),
395
416
  'maskremove_path': get_icon_path('maskremove.png'),
@@ -464,9 +485,19 @@ def _init_legacy_paths():
464
485
  _legacy = _init_legacy_paths()
465
486
  globals().update(_legacy)
466
487
 
488
+
489
+ # Background for startup
490
+ background_startup_path = _resource_path('Background_startup.jpg')
491
+ _legacy['background_startup_path'] = background_startup_path
492
+
493
+ # QML helper
494
+ resource_monitor_qml = _resource_path(os.path.join("qml", "ResourceMonitor.qml"))
495
+
467
496
  # Export list for `from setiastro.saspro.resources import *`
468
497
  __all__ = [
469
498
  'Icons', 'Resources',
470
499
  'get_icons', 'get_resources',
471
500
  'get_icon_path', 'get_data_path',
501
+ 'background_startup_path',
472
502
  ] + list(_legacy.keys())
503
+