setiastrosuitepro 1.6.7__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 (394) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/data/SASP_data.fits +0 -0
  3. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  4. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  5. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  6. setiastro/data/catalogs/cali2.csv +63 -0
  7. setiastro/data/catalogs/cali2color.csv +65 -0
  8. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  9. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  10. setiastro/data/catalogs/detected_stars.csv +24784 -0
  11. setiastro/data/catalogs/fits_header_data.csv +46 -0
  12. setiastro/data/catalogs/test.csv +8 -0
  13. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  14. setiastro/images/Astro_Spikes.png +0 -0
  15. setiastro/images/Background_startup.jpg +0 -0
  16. setiastro/images/HRDiagram.png +0 -0
  17. setiastro/images/LExtract.png +0 -0
  18. setiastro/images/LInsert.png +0 -0
  19. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  20. setiastro/images/RGB080604.png +0 -0
  21. setiastro/images/abeicon.png +0 -0
  22. setiastro/images/aberration.png +0 -0
  23. setiastro/images/acv_icon.png +0 -0
  24. setiastro/images/andromedatry.png +0 -0
  25. setiastro/images/andromedatry_satellited.png +0 -0
  26. setiastro/images/annotated.png +0 -0
  27. setiastro/images/aperture.png +0 -0
  28. setiastro/images/astrosuite.ico +0 -0
  29. setiastro/images/astrosuite.png +0 -0
  30. setiastro/images/astrosuitepro.icns +0 -0
  31. setiastro/images/astrosuitepro.ico +0 -0
  32. setiastro/images/astrosuitepro.png +0 -0
  33. setiastro/images/background.png +0 -0
  34. setiastro/images/background2.png +0 -0
  35. setiastro/images/benchmark.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  37. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  38. setiastro/images/blaster.png +0 -0
  39. setiastro/images/blink.png +0 -0
  40. setiastro/images/clahe.png +0 -0
  41. setiastro/images/collage.png +0 -0
  42. setiastro/images/colorwheel.png +0 -0
  43. setiastro/images/contsub.png +0 -0
  44. setiastro/images/convo.png +0 -0
  45. setiastro/images/copyslot.png +0 -0
  46. setiastro/images/cosmic.png +0 -0
  47. setiastro/images/cosmicsat.png +0 -0
  48. setiastro/images/crop1.png +0 -0
  49. setiastro/images/cropicon.png +0 -0
  50. setiastro/images/curves.png +0 -0
  51. setiastro/images/cvs.png +0 -0
  52. setiastro/images/debayer.png +0 -0
  53. setiastro/images/denoise_cnn_custom.png +0 -0
  54. setiastro/images/denoise_cnn_graph.png +0 -0
  55. setiastro/images/disk.png +0 -0
  56. setiastro/images/dse.png +0 -0
  57. setiastro/images/exoicon.png +0 -0
  58. setiastro/images/eye.png +0 -0
  59. setiastro/images/first_quarter.png +0 -0
  60. setiastro/images/fliphorizontal.png +0 -0
  61. setiastro/images/flipvertical.png +0 -0
  62. setiastro/images/font.png +0 -0
  63. setiastro/images/freqsep.png +0 -0
  64. setiastro/images/full_moon.png +0 -0
  65. setiastro/images/functionbundle.png +0 -0
  66. setiastro/images/graxpert.png +0 -0
  67. setiastro/images/green.png +0 -0
  68. setiastro/images/gridicon.png +0 -0
  69. setiastro/images/halo.png +0 -0
  70. setiastro/images/hdr.png +0 -0
  71. setiastro/images/histogram.png +0 -0
  72. setiastro/images/hubble.png +0 -0
  73. setiastro/images/imagecombine.png +0 -0
  74. setiastro/images/invert.png +0 -0
  75. setiastro/images/isophote.png +0 -0
  76. setiastro/images/isophote_demo_figure.png +0 -0
  77. setiastro/images/isophote_demo_image.png +0 -0
  78. setiastro/images/isophote_demo_model.png +0 -0
  79. setiastro/images/isophote_demo_residual.png +0 -0
  80. setiastro/images/jwstpupil.png +0 -0
  81. setiastro/images/last_quarter.png +0 -0
  82. setiastro/images/linearfit.png +0 -0
  83. setiastro/images/livestacking.png +0 -0
  84. setiastro/images/mask.png +0 -0
  85. setiastro/images/maskapply.png +0 -0
  86. setiastro/images/maskcreate.png +0 -0
  87. setiastro/images/maskremove.png +0 -0
  88. setiastro/images/morpho.png +0 -0
  89. setiastro/images/mosaic.png +0 -0
  90. setiastro/images/multiscale_decomp.png +0 -0
  91. setiastro/images/nbtorgb.png +0 -0
  92. setiastro/images/neutral.png +0 -0
  93. setiastro/images/new_moon.png +0 -0
  94. setiastro/images/nuke.png +0 -0
  95. setiastro/images/openfile.png +0 -0
  96. setiastro/images/pedestal.png +0 -0
  97. setiastro/images/pen.png +0 -0
  98. setiastro/images/pixelmath.png +0 -0
  99. setiastro/images/platesolve.png +0 -0
  100. setiastro/images/ppp.png +0 -0
  101. setiastro/images/pro.png +0 -0
  102. setiastro/images/project.png +0 -0
  103. setiastro/images/psf.png +0 -0
  104. setiastro/images/redo.png +0 -0
  105. setiastro/images/redoicon.png +0 -0
  106. setiastro/images/rescale.png +0 -0
  107. setiastro/images/rgbalign.png +0 -0
  108. setiastro/images/rgbcombo.png +0 -0
  109. setiastro/images/rgbextract.png +0 -0
  110. setiastro/images/rotate180.png +0 -0
  111. setiastro/images/rotatearbitrary.png +0 -0
  112. setiastro/images/rotateclockwise.png +0 -0
  113. setiastro/images/rotatecounterclockwise.png +0 -0
  114. setiastro/images/satellite.png +0 -0
  115. setiastro/images/script.png +0 -0
  116. setiastro/images/selectivecolor.png +0 -0
  117. setiastro/images/simbad.png +0 -0
  118. setiastro/images/slot0.png +0 -0
  119. setiastro/images/slot1.png +0 -0
  120. setiastro/images/slot2.png +0 -0
  121. setiastro/images/slot3.png +0 -0
  122. setiastro/images/slot4.png +0 -0
  123. setiastro/images/slot5.png +0 -0
  124. setiastro/images/slot6.png +0 -0
  125. setiastro/images/slot7.png +0 -0
  126. setiastro/images/slot8.png +0 -0
  127. setiastro/images/slot9.png +0 -0
  128. setiastro/images/spcc.png +0 -0
  129. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  130. setiastro/images/spinner.gif +0 -0
  131. setiastro/images/stacking.png +0 -0
  132. setiastro/images/staradd.png +0 -0
  133. setiastro/images/staralign.png +0 -0
  134. setiastro/images/starnet.png +0 -0
  135. setiastro/images/starregistration.png +0 -0
  136. setiastro/images/starspike.png +0 -0
  137. setiastro/images/starstretch.png +0 -0
  138. setiastro/images/statstretch.png +0 -0
  139. setiastro/images/supernova.png +0 -0
  140. setiastro/images/uhs.png +0 -0
  141. setiastro/images/undoicon.png +0 -0
  142. setiastro/images/upscale.png +0 -0
  143. setiastro/images/viewbundle.png +0 -0
  144. setiastro/images/waning_crescent_1.png +0 -0
  145. setiastro/images/waning_crescent_2.png +0 -0
  146. setiastro/images/waning_crescent_3.png +0 -0
  147. setiastro/images/waning_crescent_4.png +0 -0
  148. setiastro/images/waning_crescent_5.png +0 -0
  149. setiastro/images/waning_gibbous_1.png +0 -0
  150. setiastro/images/waning_gibbous_2.png +0 -0
  151. setiastro/images/waning_gibbous_3.png +0 -0
  152. setiastro/images/waning_gibbous_4.png +0 -0
  153. setiastro/images/waning_gibbous_5.png +0 -0
  154. setiastro/images/waxing_crescent_1.png +0 -0
  155. setiastro/images/waxing_crescent_2.png +0 -0
  156. setiastro/images/waxing_crescent_3.png +0 -0
  157. setiastro/images/waxing_crescent_4.png +0 -0
  158. setiastro/images/waxing_crescent_5.png +0 -0
  159. setiastro/images/waxing_gibbous_1.png +0 -0
  160. setiastro/images/waxing_gibbous_2.png +0 -0
  161. setiastro/images/waxing_gibbous_3.png +0 -0
  162. setiastro/images/waxing_gibbous_4.png +0 -0
  163. setiastro/images/waxing_gibbous_5.png +0 -0
  164. setiastro/images/whitebalance.png +0 -0
  165. setiastro/images/wimi_icon_256x256.png +0 -0
  166. setiastro/images/wimilogo.png +0 -0
  167. setiastro/images/wims.png +0 -0
  168. setiastro/images/wrench_icon.png +0 -0
  169. setiastro/images/xisfliberator.png +0 -0
  170. setiastro/qml/ResourceMonitor.qml +128 -0
  171. setiastro/saspro/__init__.py +20 -0
  172. setiastro/saspro/__main__.py +964 -0
  173. setiastro/saspro/_generated/__init__.py +7 -0
  174. setiastro/saspro/_generated/build_info.py +3 -0
  175. setiastro/saspro/abe.py +1379 -0
  176. setiastro/saspro/abe_preset.py +196 -0
  177. setiastro/saspro/aberration_ai.py +910 -0
  178. setiastro/saspro/aberration_ai_preset.py +224 -0
  179. setiastro/saspro/accel_installer.py +218 -0
  180. setiastro/saspro/accel_workers.py +30 -0
  181. setiastro/saspro/acv_exporter.py +379 -0
  182. setiastro/saspro/add_stars.py +627 -0
  183. setiastro/saspro/astrobin_exporter.py +1010 -0
  184. setiastro/saspro/astrospike.py +153 -0
  185. setiastro/saspro/astrospike_python.py +1841 -0
  186. setiastro/saspro/autostretch.py +198 -0
  187. setiastro/saspro/backgroundneutral.py +639 -0
  188. setiastro/saspro/batch_convert.py +328 -0
  189. setiastro/saspro/batch_renamer.py +522 -0
  190. setiastro/saspro/blemish_blaster.py +494 -0
  191. setiastro/saspro/blink_comparator_pro.py +3149 -0
  192. setiastro/saspro/bundles.py +61 -0
  193. setiastro/saspro/bundles_dock.py +114 -0
  194. setiastro/saspro/cheat_sheet.py +213 -0
  195. setiastro/saspro/clahe.py +371 -0
  196. setiastro/saspro/comet_stacking.py +1442 -0
  197. setiastro/saspro/common_tr.py +107 -0
  198. setiastro/saspro/config.py +38 -0
  199. setiastro/saspro/config_bootstrap.py +40 -0
  200. setiastro/saspro/config_manager.py +316 -0
  201. setiastro/saspro/continuum_subtract.py +1620 -0
  202. setiastro/saspro/convo.py +1403 -0
  203. setiastro/saspro/convo_preset.py +414 -0
  204. setiastro/saspro/copyastro.py +190 -0
  205. setiastro/saspro/cosmicclarity.py +1593 -0
  206. setiastro/saspro/cosmicclarity_preset.py +407 -0
  207. setiastro/saspro/crop_dialog_pro.py +1005 -0
  208. setiastro/saspro/crop_preset.py +189 -0
  209. setiastro/saspro/curve_editor_pro.py +2608 -0
  210. setiastro/saspro/curves_preset.py +375 -0
  211. setiastro/saspro/debayer.py +673 -0
  212. setiastro/saspro/debug_utils.py +29 -0
  213. setiastro/saspro/dnd_mime.py +35 -0
  214. setiastro/saspro/doc_manager.py +2727 -0
  215. setiastro/saspro/exoplanet_detector.py +2258 -0
  216. setiastro/saspro/file_utils.py +284 -0
  217. setiastro/saspro/fitsmodifier.py +748 -0
  218. setiastro/saspro/fix_bom.py +32 -0
  219. setiastro/saspro/free_torch_memory.py +48 -0
  220. setiastro/saspro/frequency_separation.py +1352 -0
  221. setiastro/saspro/function_bundle.py +1596 -0
  222. setiastro/saspro/generate_translations.py +3092 -0
  223. setiastro/saspro/ghs_dialog_pro.py +728 -0
  224. setiastro/saspro/ghs_preset.py +284 -0
  225. setiastro/saspro/graxpert.py +638 -0
  226. setiastro/saspro/graxpert_preset.py +287 -0
  227. setiastro/saspro/gui/__init__.py +0 -0
  228. setiastro/saspro/gui/main_window.py +8928 -0
  229. setiastro/saspro/gui/mixins/__init__.py +33 -0
  230. setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
  231. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  232. setiastro/saspro/gui/mixins/geometry_mixin.py +503 -0
  233. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  234. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  235. setiastro/saspro/gui/mixins/menu_mixin.py +391 -0
  236. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  237. setiastro/saspro/gui/mixins/toolbar_mixin.py +1824 -0
  238. setiastro/saspro/gui/mixins/update_mixin.py +323 -0
  239. setiastro/saspro/gui/mixins/view_mixin.py +477 -0
  240. setiastro/saspro/gui/statistics_dialog.py +47 -0
  241. setiastro/saspro/halobgon.py +492 -0
  242. setiastro/saspro/header_viewer.py +448 -0
  243. setiastro/saspro/headless_utils.py +88 -0
  244. setiastro/saspro/histogram.py +760 -0
  245. setiastro/saspro/history_explorer.py +941 -0
  246. setiastro/saspro/i18n.py +168 -0
  247. setiastro/saspro/image_combine.py +421 -0
  248. setiastro/saspro/image_peeker_pro.py +1608 -0
  249. setiastro/saspro/imageops/__init__.py +37 -0
  250. setiastro/saspro/imageops/mdi_snap.py +292 -0
  251. setiastro/saspro/imageops/scnr.py +36 -0
  252. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  253. setiastro/saspro/imageops/stretch.py +236 -0
  254. setiastro/saspro/isophote.py +1186 -0
  255. setiastro/saspro/layers.py +208 -0
  256. setiastro/saspro/layers_dock.py +714 -0
  257. setiastro/saspro/lazy_imports.py +193 -0
  258. setiastro/saspro/legacy/__init__.py +2 -0
  259. setiastro/saspro/legacy/image_manager.py +2360 -0
  260. setiastro/saspro/legacy/numba_utils.py +3676 -0
  261. setiastro/saspro/legacy/xisf.py +1213 -0
  262. setiastro/saspro/linear_fit.py +537 -0
  263. setiastro/saspro/live_stacking.py +1854 -0
  264. setiastro/saspro/log_bus.py +5 -0
  265. setiastro/saspro/logging_config.py +460 -0
  266. setiastro/saspro/luminancerecombine.py +510 -0
  267. setiastro/saspro/main_helpers.py +201 -0
  268. setiastro/saspro/mask_creation.py +1090 -0
  269. setiastro/saspro/masks_core.py +56 -0
  270. setiastro/saspro/mdi_widgets.py +353 -0
  271. setiastro/saspro/memory_utils.py +666 -0
  272. setiastro/saspro/metadata_patcher.py +75 -0
  273. setiastro/saspro/mfdeconv.py +3909 -0
  274. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  275. setiastro/saspro/mfdeconvcudnn.py +3312 -0
  276. setiastro/saspro/mfdeconvsport.py +2459 -0
  277. setiastro/saspro/minorbodycatalog.py +567 -0
  278. setiastro/saspro/morphology.py +411 -0
  279. setiastro/saspro/multiscale_decomp.py +1751 -0
  280. setiastro/saspro/nbtorgb_stars.py +541 -0
  281. setiastro/saspro/numba_utils.py +3145 -0
  282. setiastro/saspro/numba_warmup.py +141 -0
  283. setiastro/saspro/ops/__init__.py +9 -0
  284. setiastro/saspro/ops/command_help_dialog.py +623 -0
  285. setiastro/saspro/ops/command_runner.py +217 -0
  286. setiastro/saspro/ops/commands.py +1594 -0
  287. setiastro/saspro/ops/script_editor.py +1105 -0
  288. setiastro/saspro/ops/scripts.py +1476 -0
  289. setiastro/saspro/ops/settings.py +637 -0
  290. setiastro/saspro/parallel_utils.py +554 -0
  291. setiastro/saspro/pedestal.py +121 -0
  292. setiastro/saspro/perfect_palette_picker.py +1105 -0
  293. setiastro/saspro/pipeline.py +110 -0
  294. setiastro/saspro/pixelmath.py +1604 -0
  295. setiastro/saspro/plate_solver.py +2480 -0
  296. setiastro/saspro/project_io.py +797 -0
  297. setiastro/saspro/psf_utils.py +136 -0
  298. setiastro/saspro/psf_viewer.py +631 -0
  299. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  300. setiastro/saspro/remove_green.py +331 -0
  301. setiastro/saspro/remove_stars.py +1599 -0
  302. setiastro/saspro/remove_stars_preset.py +446 -0
  303. setiastro/saspro/resources.py +570 -0
  304. setiastro/saspro/rgb_combination.py +208 -0
  305. setiastro/saspro/rgb_extract.py +19 -0
  306. setiastro/saspro/rgbalign.py +727 -0
  307. setiastro/saspro/runtime_imports.py +7 -0
  308. setiastro/saspro/runtime_torch.py +754 -0
  309. setiastro/saspro/save_options.py +73 -0
  310. setiastro/saspro/selective_color.py +1614 -0
  311. setiastro/saspro/sfcc.py +1530 -0
  312. setiastro/saspro/shortcuts.py +3125 -0
  313. setiastro/saspro/signature_insert.py +1106 -0
  314. setiastro/saspro/stacking_suite.py +19069 -0
  315. setiastro/saspro/star_alignment.py +7383 -0
  316. setiastro/saspro/star_alignment_preset.py +329 -0
  317. setiastro/saspro/star_metrics.py +49 -0
  318. setiastro/saspro/star_spikes.py +769 -0
  319. setiastro/saspro/star_stretch.py +542 -0
  320. setiastro/saspro/stat_stretch.py +554 -0
  321. setiastro/saspro/status_log_dock.py +78 -0
  322. setiastro/saspro/subwindow.py +3523 -0
  323. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  324. setiastro/saspro/swap_manager.py +134 -0
  325. setiastro/saspro/torch_backend.py +89 -0
  326. setiastro/saspro/torch_rejection.py +434 -0
  327. setiastro/saspro/translations/all_source_strings.json +4726 -0
  328. setiastro/saspro/translations/ar_translations.py +4096 -0
  329. setiastro/saspro/translations/de_translations.py +3728 -0
  330. setiastro/saspro/translations/es_translations.py +4169 -0
  331. setiastro/saspro/translations/fr_translations.py +4090 -0
  332. setiastro/saspro/translations/hi_translations.py +3803 -0
  333. setiastro/saspro/translations/integrate_translations.py +271 -0
  334. setiastro/saspro/translations/it_translations.py +4728 -0
  335. setiastro/saspro/translations/ja_translations.py +3834 -0
  336. setiastro/saspro/translations/pt_translations.py +3847 -0
  337. setiastro/saspro/translations/ru_translations.py +3082 -0
  338. setiastro/saspro/translations/saspro_ar.qm +0 -0
  339. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  340. setiastro/saspro/translations/saspro_de.qm +0 -0
  341. setiastro/saspro/translations/saspro_de.ts +14548 -0
  342. setiastro/saspro/translations/saspro_es.qm +0 -0
  343. setiastro/saspro/translations/saspro_es.ts +16202 -0
  344. setiastro/saspro/translations/saspro_fr.qm +0 -0
  345. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  346. setiastro/saspro/translations/saspro_hi.qm +0 -0
  347. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  348. setiastro/saspro/translations/saspro_it.qm +0 -0
  349. setiastro/saspro/translations/saspro_it.ts +19046 -0
  350. setiastro/saspro/translations/saspro_ja.qm +0 -0
  351. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  352. setiastro/saspro/translations/saspro_pt.qm +0 -0
  353. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  354. setiastro/saspro/translations/saspro_ru.qm +0 -0
  355. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  356. setiastro/saspro/translations/saspro_sw.qm +0 -0
  357. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  358. setiastro/saspro/translations/saspro_uk.qm +0 -0
  359. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  360. setiastro/saspro/translations/saspro_zh.qm +0 -0
  361. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  362. setiastro/saspro/translations/sw_translations.py +3897 -0
  363. setiastro/saspro/translations/uk_translations.py +3929 -0
  364. setiastro/saspro/translations/zh_translations.py +3910 -0
  365. setiastro/saspro/versioning.py +77 -0
  366. setiastro/saspro/view_bundle.py +1558 -0
  367. setiastro/saspro/wavescale_hdr.py +648 -0
  368. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  369. setiastro/saspro/wavescalede.py +683 -0
  370. setiastro/saspro/wavescalede_preset.py +230 -0
  371. setiastro/saspro/wcs_update.py +374 -0
  372. setiastro/saspro/whitebalance.py +540 -0
  373. setiastro/saspro/widgets/__init__.py +48 -0
  374. setiastro/saspro/widgets/common_utilities.py +306 -0
  375. setiastro/saspro/widgets/graphics_views.py +122 -0
  376. setiastro/saspro/widgets/image_utils.py +518 -0
  377. setiastro/saspro/widgets/minigame/game.js +991 -0
  378. setiastro/saspro/widgets/minigame/index.html +53 -0
  379. setiastro/saspro/widgets/minigame/style.css +241 -0
  380. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  381. setiastro/saspro/widgets/resource_monitor.py +313 -0
  382. setiastro/saspro/widgets/spinboxes.py +290 -0
  383. setiastro/saspro/widgets/themed_buttons.py +13 -0
  384. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  385. setiastro/saspro/wimi.py +7367 -0
  386. setiastro/saspro/wims.py +588 -0
  387. setiastro/saspro/window_shelf.py +185 -0
  388. setiastro/saspro/xisf.py +1213 -0
  389. setiastrosuitepro-1.6.7.dist-info/METADATA +279 -0
  390. setiastrosuitepro-1.6.7.dist-info/RECORD +394 -0
  391. setiastrosuitepro-1.6.7.dist-info/WHEEL +4 -0
  392. setiastrosuitepro-1.6.7.dist-info/entry_points.txt +6 -0
  393. setiastrosuitepro-1.6.7.dist-info/licenses/LICENSE +674 -0
  394. setiastrosuitepro-1.6.7.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,627 @@
1
+ # pro/add_stars.py
2
+ from __future__ import annotations
3
+ import os
4
+ import numpy as np
5
+
6
+ # Qt
7
+ from PyQt6.QtCore import Qt, pyqtSignal, QTimer
8
+ from PyQt6.QtGui import QImage, QPixmap, QWheelEvent
9
+ from PyQt6.QtWidgets import (
10
+ QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
11
+ QLabel, QPushButton, QScrollArea, QSizePolicy,
12
+ QComboBox, QSlider, QMessageBox, QFileDialog, QFormLayout
13
+ )
14
+
15
+ # I/O (use your legacy functions)
16
+ from setiastro.saspro.legacy.image_manager import load_image
17
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
18
+
19
+
20
+ try:
21
+ import cv2
22
+ except Exception:
23
+ cv2 = None
24
+
25
+
26
+ # ──────────────────────────────────────────────────────────────────────────────
27
+ # Helpers to enumerate docs and masks from the Pro app
28
+ # ──────────────────────────────────────────────────────────────────────────────
29
+ # REPLACE OLD _iter_open_docs WITH THIS
30
+ def _iter_open_docs(main):
31
+ """
32
+ Find open views/docs by:
33
+ 1) docman.{documents|docs|open_docs|views|iter_docs|all_docs}
34
+ 2) Any attribute on main that has subWindowList() (QMdiArea), pulling docs
35
+ from subwindow.widget().{doc,_doc,document} or the widget itself if it
36
+ exposes an image.
37
+ Returns a list of (label, provider) where provider can be a doc or widget.
38
+ """
39
+ def _label_for(obj, fallback):
40
+ name = ""
41
+ try:
42
+ if hasattr(obj, "display_name") and callable(obj.display_name):
43
+ name = obj.display_name()
44
+ else:
45
+ name = getattr(obj, "name", "") or ""
46
+ except Exception:
47
+ name = ""
48
+ return name or fallback or f"View {len(items)}"
49
+
50
+ def _image_from_any(x):
51
+ """Robustly get a numpy-ish image from doc/widget."""
52
+ if x is None:
53
+ return None
54
+ chain = [x, getattr(x, "doc", None), getattr(x, "_doc", None), getattr(x, "document", None)]
55
+ for c in chain:
56
+ if c is None:
57
+ continue
58
+ img = getattr(c, "image", None)
59
+ if img is not None:
60
+ try:
61
+ a = np.asarray(img)
62
+ if a is not None and a.size:
63
+ return a
64
+ except Exception:
65
+ pass
66
+ # method fallbacks
67
+ for m in ("get_image", "current_image", "image_array"):
68
+ f = getattr(c, m, None)
69
+ if callable(f):
70
+ try:
71
+ a = f()
72
+ a = np.asarray(a) if a is not None else None
73
+ if a is not None and a.size:
74
+ return a
75
+ except Exception:
76
+ pass
77
+ return None
78
+
79
+ def _add_item(obj, label_hint=None):
80
+ img = _image_from_any(obj)
81
+ if img is None:
82
+ return
83
+ key = id(getattr(obj, "image", obj)) # stable-ish identity
84
+ if key in seen:
85
+ return
86
+ seen.add(key)
87
+ items.append((_label_for(obj, label_hint), obj))
88
+
89
+ items, seen = [], set()
90
+
91
+ # 1) docman sources
92
+ dm = getattr(main, "docman", None)
93
+ if dm is not None:
94
+ for attr in ("documents", "docs", "open_docs", "views"):
95
+ coll = getattr(dm, attr, None)
96
+ if isinstance(coll, dict):
97
+ for d in coll.values():
98
+ _add_item(d)
99
+ elif isinstance(coll, (list, tuple, set)):
100
+ for d in coll:
101
+ _add_item(d)
102
+ for meth in ("iter_docs", "all_docs", "iter"):
103
+ fn = getattr(dm, meth, None)
104
+ if callable(fn):
105
+ try:
106
+ for d in fn():
107
+ _add_item(d)
108
+ except Exception:
109
+ pass
110
+
111
+ # 2) any QMdiArea on main
112
+ for attr in dir(main):
113
+ try:
114
+ val = getattr(main, attr)
115
+ except Exception:
116
+ continue
117
+ if hasattr(val, "subWindowList"):
118
+ try:
119
+ for sw in val.subWindowList():
120
+ title = ""
121
+ try:
122
+ title = sw.windowTitle()
123
+ except Exception:
124
+ pass
125
+ w = None
126
+ try:
127
+ w = sw.widget()
128
+ except Exception:
129
+ pass
130
+ # prefer an actual doc if present; fallback to widget
131
+ for candidate in (
132
+ getattr(w, "doc", None),
133
+ getattr(w, "_doc", None),
134
+ getattr(w, "document", None),
135
+ w,
136
+ ):
137
+ if candidate is None:
138
+ continue
139
+ if _image_from_any(candidate) is not None:
140
+ _add_item(candidate, label_hint=title)
141
+ break
142
+ except Exception:
143
+ continue
144
+
145
+ return items
146
+
147
+
148
+
149
+ def _doc_image(doc_like) -> np.ndarray | None:
150
+ """
151
+ Accepts a doc or a view widget and returns a float32 image array
152
+ (mono 2D or RGB 3D). No boolean ops on arrays to avoid ambiguity.
153
+ """
154
+ def _grab(x):
155
+ if x is None:
156
+ return None
157
+ # direct attribute
158
+ img = getattr(x, "image", None)
159
+ if img is not None:
160
+ return img
161
+ # method fallbacks
162
+ for m in ("get_image", "current_image", "image_array"):
163
+ fn = getattr(x, m, None)
164
+ if callable(fn):
165
+ try:
166
+ return fn()
167
+ except Exception:
168
+ pass
169
+ return None
170
+
171
+ img = _grab(doc_like)
172
+ if img is None:
173
+ img = _grab(getattr(doc_like, "doc", None))
174
+ if img is None:
175
+ img = _grab(getattr(doc_like, "_doc", None))
176
+ if img is None:
177
+ img = _grab(getattr(doc_like, "document", None))
178
+ if img is None:
179
+ return None
180
+
181
+ a = np.asarray(img).astype(np.float32, copy=False)
182
+ if a.ndim == 3 and a.shape[2] == 1:
183
+ a = a[..., 0]
184
+ # Defensive normalization for big float ranges
185
+ if a.dtype.kind == "f" and a.size:
186
+ mx = float(a.max())
187
+ if mx > 5.0:
188
+ a = a / mx
189
+ return a
190
+
191
+
192
+
193
+
194
+ def _active_mask_array_from_doc(doc) -> np.ndarray | None:
195
+ """
196
+ Return active mask (H,W) float32 in [0,1] from the document, if present.
197
+ """
198
+ try:
199
+ mid = getattr(doc, "active_mask_id", None)
200
+ if not mid:
201
+ return None
202
+ masks = getattr(doc, "masks", {}) or {}
203
+ layer = masks.get(mid)
204
+ data = getattr(layer, "data", None) if layer is not None else None
205
+ if data is None:
206
+ return None
207
+ a = np.asarray(data)
208
+ if a.ndim == 3:
209
+ if cv2 is not None:
210
+ a = cv2.cvtColor(a, cv2.COLOR_BGR2GRAY)
211
+ else:
212
+ a = a.mean(axis=2)
213
+ a = a.astype(np.float32, copy=False)
214
+ a = np.clip(a, 0.0, 1.0)
215
+ return a
216
+ except Exception:
217
+ return None
218
+
219
+
220
+ # ──────────────────────────────────────────────────────────────────────────────
221
+ # Dialog
222
+ # ──────────────────────────────────────────────────────────────────────────────
223
+ class AddStarsDialog(QDialog):
224
+ stars_added = pyqtSignal(object, object)
225
+ def __init__(self, main, parent=None):
226
+ super().__init__(parent)
227
+ self.setWindowTitle(self.tr("Add Stars to Image"))
228
+
229
+ self.setWindowFlag(Qt.WindowType.Window, True)
230
+ # Non-modal: allow user to switch between images while dialog is open
231
+ self.setWindowModality(Qt.WindowModality.NonModal)
232
+ self.setModal(False)
233
+ #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
234
+ try:
235
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
236
+ except Exception:
237
+ pass # older PyQt6 versions
238
+ self.main = main
239
+ self.starless = None
240
+ self.stars_only = None
241
+ self.blended_image = None
242
+ self.scale_factor = 1.0
243
+ self._fit_once = False
244
+
245
+ self._build_ui()
246
+ self._populate_doc_combos()
247
+
248
+ # UI -----------------------------------------------------------------------
249
+ def _build_ui(self):
250
+ layout = QVBoxLayout(self)
251
+
252
+ # Preview
253
+ self.preview_label = QLabel()
254
+ self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
255
+ self.preview_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
256
+ self.preview_label.setScaledContents(False)
257
+
258
+ self.scroll_area = QScrollArea(self)
259
+ self.scroll_area.setWidgetResizable(False)
260
+ self.scroll_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
261
+ self.scroll_area.setWidget(self.preview_label)
262
+ layout.addWidget(self.scroll_area)
263
+
264
+ # Zoom row (standardized themed toolbuttons)
265
+ zrow = QHBoxLayout()
266
+ self.btn_zoom_in = themed_toolbtn("zoom-in", "Zoom In")
267
+ self.btn_zoom_out = themed_toolbtn("zoom-out", "Zoom Out")
268
+ self.btn_fit = themed_toolbtn("zoom-fit-best", "Fit to Preview")
269
+
270
+ self.btn_zoom_in.clicked.connect(self.zoom_in)
271
+ self.btn_zoom_out.clicked.connect(self.zoom_out)
272
+ self.btn_fit.clicked.connect(self.fit_to_preview)
273
+
274
+ zrow.addWidget(self.btn_zoom_in)
275
+ zrow.addWidget(self.btn_zoom_out)
276
+ zrow.addWidget(self.btn_fit)
277
+ zrow.addStretch(1)
278
+ layout.addLayout(zrow)
279
+
280
+ # Selection + blend
281
+ grid = QGridLayout()
282
+
283
+ # Blend type
284
+ grid.addWidget(QLabel(self.tr("Blend Type:")), 0, 0)
285
+ self.cmb_blend = QComboBox(); self.cmb_blend.addItems(["Screen", "Add"])
286
+ self.cmb_blend.currentIndexChanged.connect(self.update_preview)
287
+ grid.addWidget(self.cmb_blend, 0, 1)
288
+
289
+ # Starless source
290
+ grid.addWidget(QLabel(self.tr("Starless View:")), 1, 0)
291
+ self.cmb_starless = QComboBox(); grid.addWidget(self.cmb_starless, 1, 1)
292
+ btn_sless_file = QPushButton(self.tr("Load from File")); btn_sless_file.clicked.connect(lambda: self._load_from_file('starless'))
293
+ grid.addWidget(btn_sless_file, 1, 2)
294
+
295
+ # Stars-only source
296
+ grid.addWidget(QLabel(self.tr("Stars-Only View:")), 2, 0)
297
+ self.cmb_stars = QComboBox(); grid.addWidget(self.cmb_stars, 2, 1)
298
+ btn_stars_file = QPushButton(self.tr("Load from File")); btn_stars_file.clicked.connect(lambda: self._load_from_file('stars'))
299
+ grid.addWidget(btn_stars_file, 2, 2)
300
+
301
+ layout.addLayout(grid)
302
+
303
+ refresh_row = QHBoxLayout()
304
+ btn_refresh = QPushButton(self.tr("Refresh Views"))
305
+ btn_refresh.clicked.connect(self._populate_doc_combos)
306
+ refresh_row.addStretch(1)
307
+ refresh_row.addWidget(btn_refresh)
308
+ layout.addLayout(refresh_row)
309
+
310
+ # Ratio slider
311
+ row = QHBoxLayout()
312
+ row.addWidget(QLabel(self.tr("Blend Ratio (Screen/Add Intensity):")))
313
+ self.slider_ratio = QSlider(Qt.Orientation.Horizontal)
314
+ self.slider_ratio.setRange(0, 100); self.slider_ratio.setValue(100)
315
+ self.slider_ratio.setTickInterval(10); self.slider_ratio.setTickPosition(QSlider.TickPosition.TicksBelow)
316
+ self.slider_ratio.valueChanged.connect(self.update_preview)
317
+ row.addWidget(self.slider_ratio)
318
+ layout.addLayout(row)
319
+
320
+ # Buttons
321
+ brow = QHBoxLayout(); brow.addStretch(1)
322
+ btn_apply = QPushButton(self.tr("Apply")); btn_apply.clicked.connect(self._apply)
323
+ btn_cancel= QPushButton(self.tr("Cancel")); btn_cancel.clicked.connect(self.reject)
324
+ brow.addWidget(btn_apply); brow.addWidget(btn_cancel)
325
+ layout.addLayout(brow)
326
+
327
+ self.setMinimumSize(900, 650)
328
+
329
+ # signals for combos
330
+ self.cmb_starless.currentIndexChanged.connect(self._pick_starless_from_combo)
331
+ self.cmb_stars.currentIndexChanged.connect(self._pick_stars_from_combo)
332
+
333
+ # Populate combos with open docs (+ sentinel for file)
334
+ def _populate_doc_combos(self):
335
+ items = [("Select View", None)]
336
+ for name, d in _iter_open_docs(self.main):
337
+ items.append((name, d))
338
+ items.append(("Load from File", "file"))
339
+
340
+ self.cmb_starless.clear()
341
+ self.cmb_stars.clear()
342
+ for label, data in items:
343
+ self.cmb_starless.addItem(label, data)
344
+ self.cmb_stars.addItem(label, data)
345
+
346
+ # File load ----------------------------------------------------------------
347
+ def _load_from_file(self, which: str):
348
+ fn, _ = QFileDialog.getOpenFileName(
349
+ self, f"Select {'Starless' if which=='starless' else 'Stars-Only'} Image", "",
350
+ "Image Files (*.png *.tif *.tiff *.fits *.fit *.xisf *.jpg *.jpeg)"
351
+ )
352
+ if not fn:
353
+ return
354
+ img, _, _, _ = load_image(fn)
355
+ if img is None:
356
+ QMessageBox.critical(self, "Load Error", f"Failed to load: {os.path.basename(fn)}")
357
+ return
358
+ if which == 'starless':
359
+ self.starless = self._to_rgb01(img)
360
+ self.cmb_starless.setCurrentIndex(self.cmb_starless.count()-1) # "Load from File"
361
+ else:
362
+ self.stars_only = self._to_rgb01(img)
363
+ self.cmb_stars.setCurrentIndex(self.cmb_stars.count()-1)
364
+ self.update_preview()
365
+
366
+ @staticmethod
367
+ def _resolve_doc_object(doc_like):
368
+ if doc_like is None:
369
+ return None
370
+ for c in (doc_like,
371
+ getattr(doc_like, "doc", None),
372
+ getattr(doc_like, "_doc", None),
373
+ getattr(doc_like, "document", None)):
374
+ if c is None:
375
+ continue
376
+ if hasattr(c, "apply_edit") and any(
377
+ hasattr(c, a) for a in ("image", "get_image", "current_image", "image_array")
378
+ ):
379
+ return c
380
+ return None
381
+
382
+ def _target_doc_for_mask(self):
383
+ """Use the selected Starless View's doc (fallback to active doc)."""
384
+ sel = self.cmb_starless.currentData()
385
+ if sel is None or sel == "file":
386
+ doc = getattr(self.main, "_active_doc", None)
387
+ if callable(doc): doc = doc()
388
+ return self._resolve_doc_object(doc)
389
+ return self._resolve_doc_object(sel)
390
+
391
+ # Combo selects ------------------------------------------------------------
392
+ def _pick_starless_from_combo(self):
393
+ data = self.cmb_starless.currentData()
394
+ if data is None or data == "file":
395
+ # None or "Load from File" (the button sets image)
396
+ self.update_preview()
397
+ return
398
+ img = _doc_image(data)
399
+ if img is None:
400
+ QMessageBox.warning(self, "Empty View", "Selected starless view has no image.")
401
+ return
402
+ self.starless = self._to_rgb01(img)
403
+ self.update_preview()
404
+
405
+ def _pick_stars_from_combo(self):
406
+ data = self.cmb_stars.currentData()
407
+ if data is None or data == "file":
408
+ self.update_preview()
409
+ return
410
+ img = _doc_image(data)
411
+ if img is None:
412
+ QMessageBox.warning(self, "Empty View", "Selected stars-only view has no image.")
413
+ return
414
+ self.stars_only = self._to_rgb01(img)
415
+ self.update_preview()
416
+
417
+ # Math ---------------------------------------------------------------------
418
+ @staticmethod
419
+ def _to_rgb01(a: np.ndarray) -> np.ndarray:
420
+ a = np.asarray(a).astype(np.float32, copy=False)
421
+ if a.ndim == 2:
422
+ a = np.stack([a]*3, axis=-1)
423
+ elif a.ndim == 3 and a.shape[2] == 1:
424
+ a = np.repeat(a, 3, axis=2)
425
+ a = np.clip(a, 0.0, 1.0)
426
+ return a
427
+
428
+ def _blend_images(self) -> np.ndarray | None:
429
+ if self.starless is None or self.stars_only is None:
430
+ return None
431
+
432
+ # same size?
433
+ if self.starless.shape != self.stars_only.shape:
434
+ QMessageBox.critical(self, "Size Mismatch", "Starless and Stars-Only views are different sizes.")
435
+ return None
436
+
437
+ mode = self.cmb_blend.currentText()
438
+ r = self.slider_ratio.value() / 100.0
439
+
440
+ if mode == "Screen":
441
+ base = self.starless + self.stars_only - (self.starless * self.stars_only)
442
+ else:
443
+ base = self.starless + self.stars_only
444
+
445
+ blended = (1.0 - r) * self.starless + r * base
446
+ blended = np.clip(blended, 0.0, 1.0)
447
+
448
+ # mask from the *destination* doc (selected Starless View)
449
+ tgt = self._target_doc_for_mask()
450
+ if tgt is not None:
451
+ m = _active_mask_array_from_doc(tgt)
452
+ if m is not None:
453
+ h, w = blended.shape[:2]
454
+ if m.shape != (h, w):
455
+ if cv2 is not None:
456
+ m = cv2.resize(m, (w, h), interpolation=cv2.INTER_NEAREST)
457
+ else:
458
+ yi = (np.linspace(0, m.shape[0]-1, h)).astype(np.int32)
459
+ xi = (np.linspace(0, m.shape[1]-1, w)).astype(np.int32)
460
+ m = m[yi][:, xi]
461
+ m3 = np.repeat(m[:, :, None], 3, axis=2)
462
+ # only replace where mask==1; keep original starless elsewhere
463
+ blended = np.clip(self.starless * (1.0 - m3) + blended * m3, 0.0, 1.0).astype(np.float32, copy=False)
464
+
465
+ return blended
466
+
467
+ # Preview ------------------------------------------------------------------
468
+ def update_preview(self):
469
+ out = self._blend_images()
470
+ self.blended_image = out
471
+ if out is None:
472
+ self.preview_label.clear()
473
+ return
474
+
475
+ pix = self._to_pixmap(out)
476
+ # keep scroll position
477
+ hs = self.scroll_area.horizontalScrollBar().value()
478
+ vs = self.scroll_area.verticalScrollBar().value()
479
+
480
+ scaled = pix.scaled(
481
+ pix.size() * self.scale_factor,
482
+ Qt.AspectRatioMode.KeepAspectRatio,
483
+ Qt.TransformationMode.SmoothTransformation
484
+ )
485
+ self.preview_label.setPixmap(scaled)
486
+ self.preview_label.adjustSize()
487
+ self.scroll_area.horizontalScrollBar().setValue(hs)
488
+ self.scroll_area.verticalScrollBar().setValue(vs)
489
+
490
+ def _to_pixmap(self, img: np.ndarray) -> QPixmap:
491
+ im = np.clip(img, 0.0, 1.0)
492
+ u8 = (im * 255.0 + 0.5).astype(np.uint8)
493
+ if u8.ndim == 2:
494
+ q = QImage(u8.data, u8.shape[1], u8.shape[0], u8.strides[0], QImage.Format.Format_Grayscale8)
495
+ else:
496
+ # RGB888
497
+ q = QImage(u8.data, u8.shape[1], u8.shape[0], u8.strides[0], QImage.Format.Format_RGB888)
498
+ return QPixmap.fromImage(q)
499
+
500
+ # Zoom/fit -----------------------------------------------------------------
501
+ def wheelEvent(self, ev: QWheelEvent):
502
+ if ev.angleDelta().y() > 0:
503
+ self.zoom_in()
504
+ else:
505
+ self.zoom_out()
506
+ ev.accept()
507
+
508
+ def zoom_in(self):
509
+ self.scale_factor *= 1.25
510
+ self._refresh_scaled()
511
+
512
+ def zoom_out(self):
513
+ self.scale_factor /= 1.25
514
+ self._refresh_scaled()
515
+
516
+ def fit_to_preview(self):
517
+ if self.blended_image is None:
518
+ return
519
+ QTimer.singleShot(0, self._do_fit)
520
+
521
+ def _do_fit(self):
522
+ if self.blended_image is None:
523
+ return
524
+ pix = self._to_pixmap(self.blended_image)
525
+ vsz = self.scroll_area.viewport().size()
526
+ if pix.isNull() or pix.width() == 0 or pix.height() == 0:
527
+ return
528
+ sw = vsz.width() / pix.width()
529
+ sh = vsz.height() / pix.height()
530
+ self.scale_factor = min(sw, sh)
531
+ self.update_preview()
532
+
533
+ def _refresh_scaled(self):
534
+ if self.blended_image is None:
535
+ return
536
+ pix = self._to_pixmap(self.blended_image)
537
+ scaled = pix.scaled(
538
+ pix.size() * self.scale_factor,
539
+ Qt.AspectRatioMode.KeepAspectRatio,
540
+ Qt.TransformationMode.SmoothTransformation
541
+ )
542
+ self.preview_label.setPixmap(scaled)
543
+ self.preview_label.adjustSize()
544
+
545
+ # Apply --------------------------------------------------------------------
546
+ def _apply(self):
547
+ """
548
+ Applies the blended image to the selected *Starless View* (or, if the starless
549
+ source is "Load from File", falls back to the active doc).
550
+ """
551
+ if self.blended_image is None:
552
+ QMessageBox.warning(self, "No Blend", "No blended image to apply.")
553
+ return
554
+
555
+ sel = self.cmb_starless.currentData()
556
+ target_doc = None
557
+
558
+ if sel is None: # "Select View"
559
+ # Fallback: active doc
560
+ doc = getattr(self.main, "_active_doc", None)
561
+ if callable(doc):
562
+ doc = doc()
563
+ target_doc = self._resolve_doc_object(doc)
564
+ elif sel == "file":
565
+ # Starless came from a file; no view to overwrite → fallback to active
566
+ doc = getattr(self.main, "_active_doc", None)
567
+ if callable(doc):
568
+ doc = doc()
569
+ target_doc = self._resolve_doc_object(doc)
570
+ else:
571
+ # A real view/doc was chosen
572
+ target_doc = self._resolve_doc_object(sel)
573
+
574
+ if target_doc is None:
575
+ QMessageBox.warning(self, "No Target",
576
+ "Pick a starless view to overwrite (or activate a destination window).")
577
+ return
578
+
579
+ # Emit (target_doc, blended_image)
580
+ self.stars_added.emit(target_doc, self.blended_image.astype(np.float32, copy=False))
581
+ # Dialog stays open so user can apply to other images
582
+ # Refresh combo boxes for next operation
583
+ self._populate_doc_combos()
584
+
585
+
586
+ # Ensure initial fit once shown
587
+ def showEvent(self, ev):
588
+ super().showEvent(ev)
589
+ # repopulate in case windows opened after dialog construction
590
+ self._populate_doc_combos()
591
+ if not self._fit_once:
592
+ self._fit_once = True
593
+ QTimer.singleShot(0, self.fit_to_preview)
594
+
595
+
596
+ # ──────────────────────────────────────────────────────────────────────────────
597
+ # Public entry point: open dialog, then apply to active doc
598
+ # ──────────────────────────────────────────────────────────────────────────────
599
+ def add_stars(main):
600
+ doc = getattr(main, "_active_doc", None)
601
+ if callable(doc):
602
+ doc = doc()
603
+ if doc is None or getattr(doc, "image", None) is None:
604
+ QMessageBox.warning(main, "No Image", "Please activate a destination image window first.")
605
+ return
606
+
607
+ dlg = AddStarsDialog(main, parent=main)
608
+ dlg.stars_added.connect(lambda target, arr: _apply_to_doc(main, target, arr))
609
+ dlg.exec()
610
+
611
+
612
+ def _apply_to_doc(main, doc, arr: np.ndarray):
613
+ """Overwrite the given document with the blended (stars added) result."""
614
+ if doc is None:
615
+ QMessageBox.warning(main, "No Target Document", "No document to apply to.")
616
+ return
617
+ try:
618
+ meta = {
619
+ "step_name": "Stars Added",
620
+ "bit_depth": "32-bit floating point",
621
+ "is_mono": (arr.ndim == 2),
622
+ }
623
+ doc.apply_edit(arr.astype(np.float32, copy=False), metadata=meta, step_name="Stars Added")
624
+ if hasattr(main, "_log"):
625
+ main._log("Stars Added")
626
+ except Exception as e:
627
+ QMessageBox.critical(main, "Add Stars", f"Failed to apply result:\n{e}")