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,769 @@
1
+ # pro/tools/star_spikes.py
2
+ from __future__ import annotations
3
+ import numpy as np
4
+ import math
5
+ from PyQt6.QtCore import Qt
6
+ from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QSplitter, QSizePolicy, QWidget, QApplication,
7
+ QFormLayout, QGroupBox, QDoubleSpinBox, QSpinBox,
8
+ QMessageBox, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem)
9
+
10
+ from PyQt6.QtGui import QPixmap, QImage, QPainter
11
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
12
+
13
+ # deps
14
+ try:
15
+ import sep
16
+ except Exception as _e_sep:
17
+ sep = None
18
+ try:
19
+ import cv2
20
+ except Exception as _e_cv2:
21
+ cv2 = None
22
+ try:
23
+ from scipy.ndimage import gaussian_filter
24
+ import scipy.ndimage as ndi
25
+ except Exception as _e_scipy:
26
+ gaussian_filter = None
27
+ ndi = None
28
+
29
+ class PreviewView(QGraphicsView):
30
+ def __init__(self, *a, **k):
31
+ super().__init__(*a, **k)
32
+ # drag to pan
33
+ self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
34
+ # zoom toward mouse
35
+ self.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
36
+ # when the view resizes, keep the scene centered
37
+ self.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
38
+ # nicer defaults
39
+ self.setRenderHints(self.renderHints() | QPainter.RenderHint.SmoothPixmapTransform)
40
+ self.setViewportUpdateMode(QGraphicsView.ViewportUpdateMode.SmartViewportUpdate)
41
+
42
+ class StarSpikesDialogPro(QDialog):
43
+ WARN_LIMIT = 1000
44
+ MAX_AUTO_RETRIES = 2
45
+
46
+ def __init__(self, parent=None, doc_manager=None,
47
+ initial_doc=None,
48
+ jwstpupil_path: str | None = None,
49
+ aperture_help_path: str | None = None,
50
+ spinner_path: str | None = None):
51
+ super().__init__(parent)
52
+ self.setWindowTitle(self.tr("Diffraction Spikes"))
53
+ self.setWindowFlag(Qt.WindowType.Window, True)
54
+ self.setWindowModality(Qt.WindowModality.NonModal)
55
+ self.setModal(False)
56
+ try:
57
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
58
+ except Exception:
59
+ pass # older PyQt6 versions
60
+ self.docman = doc_manager
61
+ self.doc = initial_doc or (self.docman.get_active_document() if self.docman else None)
62
+ self.jwstpupil_path = jwstpupil_path
63
+ self.aperture_help_path = aperture_help_path
64
+
65
+ self.final_image = None
66
+ self._img_src = None # float32, 2D or 3D, [0..1]
67
+
68
+ # defaults (aligned to your SASv2 tool)
69
+ self.advanced = {
70
+ "flux_max": 300.0, "bscale_min": 10.0, "bscale_max": 30.0,
71
+ "shrink_min": 1.0, "shrink_max": 5.0, "detect_thresh": 5.0,
72
+ }
73
+
74
+ self._build_ui()
75
+ self._load_active_image()
76
+
77
+ # ---------- UI ----------
78
+ def _build_ui(self):
79
+ # top-level splitter: controls (left) | preview (right)
80
+ splitter = QSplitter(Qt.Orientation.Horizontal, self)
81
+ splitter.setChildrenCollapsible(False)
82
+
83
+ # ----- LEFT: controls panel (stacked groups) -----
84
+ left = QWidget()
85
+ left_v = QVBoxLayout(left)
86
+ left_v.setContentsMargins(10, 10, 10, 10)
87
+ left_v.setSpacing(10)
88
+
89
+ def dspin(lo, hi, step, val, decimals=2):
90
+ sp = QDoubleSpinBox()
91
+ sp.setRange(lo, hi)
92
+ sp.setSingleStep(step)
93
+ sp.setDecimals(decimals)
94
+ sp.setValue(val)
95
+ sp.setMaximumWidth(140)
96
+ return sp
97
+
98
+ def ispin(lo, hi, step, val):
99
+ sp = QSpinBox()
100
+ sp.setRange(lo, hi)
101
+ sp.setSingleStep(step)
102
+ sp.setValue(val)
103
+ sp.setMaximumWidth(140)
104
+ return sp
105
+
106
+ # --- Group: Star Detection ---
107
+ grp_detect = QGroupBox(self.tr("Star Detection"))
108
+ f_detect = QFormLayout(grp_detect)
109
+ f_detect.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
110
+ f_detect.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
111
+
112
+ self.flux_min = dspin(0.0, 999999.0, 10.0, 30.0, decimals=1)
113
+ self.detect_thresh = dspin(0.5, 50.0, 0.5, float(self.advanced.get("detect_thresh", 5.0)), decimals=2)
114
+ self.detect_thresh.setToolTip("σ threshold for SEP detection (higher = fewer stars).")
115
+ # keep self.advanced in sync if user edits
116
+ self.detect_thresh.valueChanged.connect(lambda v: self.advanced.__setitem__("detect_thresh", float(v)))
117
+
118
+ f_detect.addRow(self.tr("Flux Min:"), self.flux_min)
119
+ f_detect.addRow(self.tr("Detection Threshold (σ):"), self.detect_thresh)
120
+
121
+ # --- Group: Aperture (Geometry) ---
122
+ grp_ap = QGroupBox(self.tr("Aperture"))
123
+ f_ap = QFormLayout(grp_ap)
124
+ f_ap.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
125
+ f_ap.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
126
+
127
+ self.pupil_jwst = QPushButton("Circular")
128
+ self.pupil_jwst.setCheckable(True)
129
+ self.pupil_jwst.setChecked(False)
130
+ self.pupil_jwst.toggled.connect(lambda on: self._toggle_pupil(on))
131
+ self.pupil_jwst.setToolTip("Toggle between circular aperture and JWST pupil image.")
132
+ self.pupil_jwst.setStyleSheet("""
133
+ QPushButton { min-width: 72px; max-width: 72px; min-height: 28px; max-height: 28px;
134
+ border-radius: 14px; background:#ccc; border:1px solid #999;}
135
+ QPushButton:checked { background:#66bb6a; }
136
+ """)
137
+ f_ap.addRow(self.tr("Aperture Type:"), self.pupil_jwst)
138
+
139
+ self.radius = dspin(1.0, 512.0, 1.0, 128.0, decimals=1)
140
+ self.obstruction = dspin(0.0, 0.99, 0.01, 0.2, decimals=2)
141
+ self.num_vanes = ispin(2, 8, 1, 4)
142
+ self.vane_width = dspin(0.0, 50.0, 0.5, 4.0, decimals=2)
143
+ self.rotation = dspin(0.0, 360.0, 1.0, 0.0, decimals=1)
144
+
145
+ f_ap.addRow(self.tr("Pupil Radius:"), self.radius)
146
+ f_ap.addRow(self.tr("Obstruction:"), self.obstruction)
147
+ f_ap.addRow(self.tr("Number of Vanes:"), self.num_vanes)
148
+ f_ap.addRow(self.tr("Vane Width:"), self.vane_width)
149
+ f_ap.addRow(self.tr("Rotation (deg):"), self.rotation)
150
+
151
+ # --- Group: PSF & Synthesis ---
152
+ grp_psf = QGroupBox(self.tr("PSF & Synthesis"))
153
+ f_psf = QFormLayout(grp_psf)
154
+ f_psf.setLabelAlignment(Qt.AlignmentFlag.AlignRight)
155
+ f_psf.setFormAlignment(Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter)
156
+
157
+ self.color_boost = dspin(0.1, 10.0, 0.1, 1.5, decimals=2)
158
+ self.blur_sigma = dspin(0.1, 10.0, 0.1, 2.0, decimals=2)
159
+
160
+ f_psf.addRow(self.tr("Spike Boost:"), self.color_boost)
161
+ f_psf.addRow(self.tr("PSF Blur Sigma:"), self.blur_sigma)
162
+
163
+ # --- Actions ---
164
+ row_actions = QHBoxLayout()
165
+ row_actions.setSpacing(8)
166
+ self.btn_run = QPushButton(self.tr("Generate Spikes"))
167
+ self.btn_run.clicked.connect(self._run)
168
+ self.btn_apply = QPushButton(self.tr("Apply to Active Document"))
169
+ self.btn_apply.clicked.connect(self._apply_to_doc)
170
+ self.btn_apply.setEnabled(False)
171
+ self.btn_help = QPushButton(self.tr("Aperture Help"))
172
+ self.btn_help.clicked.connect(self._show_help)
173
+ row_actions.addWidget(self.btn_run)
174
+ row_actions.addWidget(self.btn_apply)
175
+ row_actions.addWidget(self.btn_help)
176
+ row_actions.addStretch(1)
177
+
178
+ # --- Status ---
179
+ self.status = QLabel(self.tr("Ready"))
180
+ self.status.setAlignment(Qt.AlignmentFlag.AlignCenter)
181
+ self.status.setWordWrap(True)
182
+
183
+ # assemble left panel
184
+ left_v.addWidget(grp_detect)
185
+ left_v.addWidget(grp_ap)
186
+ left_v.addWidget(grp_psf)
187
+ left_v.addLayout(row_actions)
188
+ left_v.addWidget(self.status)
189
+ left_v.addStretch(1)
190
+
191
+ splitter.addWidget(left)
192
+
193
+ # ----- RIGHT: preview panel -----
194
+ right = QWidget()
195
+ right_v = QVBoxLayout(right)
196
+
197
+ # zoom toolbar
198
+ zrow = QHBoxLayout()
199
+ self.btn_zoom_in = QPushButton(self.tr("Zoom In"))
200
+ self.btn_zoom_out = QPushButton(self.tr("Zoom Out"))
201
+ self.btn_fit = QPushButton(self.tr("Fit to Preview"))
202
+ self.btn_zoom_in.clicked.connect(self._zoom_in)
203
+ self.btn_zoom_out.clicked.connect(self._zoom_out)
204
+ self.btn_fit.clicked.connect(self._fit_to_preview)
205
+ zrow.addWidget(self.btn_zoom_in)
206
+ zrow.addWidget(self.btn_zoom_out)
207
+ zrow.addWidget(self.btn_fit)
208
+ zrow.addStretch(1)
209
+ right_v.addLayout(zrow)
210
+
211
+ # graphics scene/view
212
+ self.scene = QGraphicsScene()
213
+ self.view = PreviewView()
214
+ self.view.setScene(self.scene)
215
+ self.view.setAlignment(Qt.AlignmentFlag.AlignCenter)
216
+ self.view.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
217
+ self.view.setMinimumSize(600, 450)
218
+ self.pix = QGraphicsPixmapItem()
219
+ self.scene.addItem(self.pix)
220
+ right_v.addWidget(self.view, 1)
221
+
222
+ splitter.addWidget(right)
223
+
224
+ # make preview side bigger by default
225
+ splitter.setStretchFactor(0, 0) # left
226
+ splitter.setStretchFactor(1, 1) # right
227
+ splitter.setSizes([360, 900])
228
+
229
+ # top-level layout contains just the splitter
230
+ root = QVBoxLayout(self)
231
+ root.addWidget(splitter)
232
+
233
+ # init pupil visibility
234
+ self._toggle_pupil(False)
235
+
236
+ # zoom state
237
+ self._zoom = 1.0
238
+ self._fit_mode = True # start fitted
239
+
240
+ def _toggle_pupil(self, jwst: bool):
241
+ self.pupil_jwst.setText("JWST" if jwst else "Circular")
242
+ # hide circular-only params when JWST pupil is used
243
+ for w in (self.num_vanes, self.vane_width, self.obstruction, self.radius):
244
+ w.setVisible(not jwst)
245
+
246
+ # ---------- data/preset ----------
247
+ def _load_active_image(self):
248
+ if not self.doc or getattr(self.doc, "image", None) is None:
249
+ self.status.setText("No active image.")
250
+ return
251
+ arr = np.asarray(self.doc.image)
252
+ if arr.dtype != np.float32:
253
+ arr = arr.astype(np.float32, copy=False)
254
+ # strip alpha
255
+ if arr.ndim == 3 and arr.shape[2] == 4:
256
+ arr = arr[..., :3]
257
+ # keep within [0..1] for the math we use
258
+ if np.issubdtype(arr.dtype, np.floating):
259
+ mx = float(arr.max()) if arr.size else 1.0
260
+ if mx > 1.0:
261
+ arr = arr / (65535.0 if mx > 5.0 else mx)
262
+ self._img_src = np.clip(arr, 0.0, 1.0)
263
+
264
+ def apply_preset(self, p: dict):
265
+ if not p:
266
+ return
267
+ self.flux_min.setValue(float(p.get("flux_min", self.flux_min.value())))
268
+ self.advanced["flux_max"] = float(p.get("flux_max", self.advanced["flux_max"]))
269
+ self.advanced["bscale_min"] = float(p.get("bscale_min", self.advanced["bscale_min"]))
270
+ self.advanced["bscale_max"] = float(p.get("bscale_max", self.advanced["bscale_max"]))
271
+ self.advanced["shrink_min"] = float(p.get("shrink_min", self.advanced["shrink_min"]))
272
+ self.advanced["shrink_max"] = float(p.get("shrink_max", self.advanced["shrink_max"]))
273
+ self.advanced["detect_thresh"] = float(p.get("detect_thresh", self.advanced["detect_thresh"]))
274
+ self.detect_thresh.setValue(float(self.advanced["detect_thresh"])) # reflect in UI
275
+ self.radius.setValue(float(p.get("radius", self.radius.value())))
276
+ self.obstruction.setValue(float(p.get("obstruction", self.obstruction.value())))
277
+ self.num_vanes.setValue(int(p.get("num_vanes", self.num_vanes.value())))
278
+ self.vane_width.setValue(float(p.get("vane_width", self.vane_width.value())))
279
+ self.rotation.setValue(float(p.get("rotation", self.rotation.value())))
280
+ self.color_boost.setValue(float(p.get("color_boost", self.color_boost.value())))
281
+ self.blur_sigma.setValue(float(p.get("blur_sigma", self.blur_sigma.value())))
282
+ self.pupil_jwst.setChecked(bool(p.get("jwst", self.pupil_jwst.isChecked())))
283
+
284
+ # ---------- core ----------
285
+ def _run(self):
286
+ if self._img_src is None:
287
+ self._load_active_image()
288
+ if self._img_src is None:
289
+ QMessageBox.information(self, "Diffraction Spikes", "No active image.")
290
+ return
291
+
292
+ # deps check
293
+ if sep is None:
294
+ QMessageBox.critical(self, "Missing Dependency", "python-sep is required for star detection.")
295
+ return
296
+ if gaussian_filter is None or ndi is None:
297
+ QMessageBox.critical(self, "Missing Dependency", "scipy.ndimage is required.")
298
+ return
299
+
300
+ self.status.setText("Detecting stars…")
301
+ QApplication.processEvents()
302
+ img = self._img_src
303
+ # un-stretch via midtones(0.95) for detection
304
+ if img.ndim == 3:
305
+ lin = img.copy()
306
+ for c in range(3):
307
+ lin[..., c] = self._midtones_m(lin[..., c], 0.95)
308
+ base = 0.2126*lin[...,0] + 0.7152*lin[...,1] + 0.0722*lin[...,2]
309
+ else:
310
+ lin = self._midtones_m(img, 0.95)
311
+ base = lin
312
+
313
+ # initial detection
314
+ thresh = float(self.detect_thresh.value())
315
+ stars = self._detect_stars(base,
316
+ threshold=thresh,
317
+ flux_min=self.flux_min.value(),
318
+ size_min=1.0)
319
+
320
+ # interactive guardrail for dense fields
321
+ tries = 0
322
+ while len(stars) > self.WARN_LIMIT and tries < self.MAX_AUTO_RETRIES:
323
+ suggested = min(50.0, max(thresh + 1.0,
324
+ thresh * (len(stars) / float(self.WARN_LIMIT))**0.5))
325
+ msg = QMessageBox(self)
326
+ msg.setWindowTitle("Too Many Stars Detected")
327
+ msg.setIcon(QMessageBox.Icon.Warning)
328
+ msg.setText(f"{len(stars)} stars detected (limit {self.WARN_LIMIT}).\n"
329
+ "Increase the detection threshold to reduce clutter?")
330
+ raise_btn = msg.addButton(f"Raise to σ={suggested:.2f}", QMessageBox.ButtonRole.AcceptRole)
331
+ cont_btn = msg.addButton("Continue Anyway", QMessageBox.ButtonRole.DestructiveRole)
332
+ cancel_btn= msg.addButton("Cancel", QMessageBox.ButtonRole.RejectRole)
333
+ msg.setDefaultButton(raise_btn)
334
+ msg.exec()
335
+
336
+ clicked = msg.clickedButton()
337
+ if clicked is raise_btn:
338
+ thresh = suggested
339
+ self.detect_thresh.setValue(thresh) # reflect in UI
340
+ self.status.setText(f"Re-detecting stars at σ={thresh:.2f}…")
341
+ QApplication.processEvents()
342
+ stars = self._detect_stars(base,
343
+ threshold=thresh,
344
+ flux_min=self.flux_min.value(),
345
+ size_min=1.0)
346
+ tries += 1
347
+ continue
348
+ elif clicked is cont_btn:
349
+ break
350
+ else: # cancel
351
+ self.status.setText("Cancelled.")
352
+ return
353
+
354
+ if len(stars) == 0:
355
+ self.status.setText("No stars found.")
356
+ QMessageBox.information(self, "Diffraction Spikes", "No stars found above flux_min.")
357
+ return
358
+
359
+ self.status.setText(f"Building pupil/PSFs… ({len(stars)} stars)")
360
+ QApplication.processEvents()
361
+ if self.pupil_jwst.isChecked():
362
+ if cv2 is None or not self.jwstpupil_path:
363
+ QMessageBox.critical(self, "Missing JWST Pupil",
364
+ "OpenCV not available or JWST pupil image path missing.")
365
+ return
366
+ pupil = self._load_pupil_from_png(self.jwstpupil_path, size=1024, rotation=self.rotation.value())
367
+ else:
368
+ pupil = self._make_pupil(size=1024,
369
+ radius=self.radius.value(),
370
+ obstruction=self.obstruction.value(),
371
+ vane_width=self.vane_width.value(),
372
+ num_vanes=self.num_vanes.value(),
373
+ rotation=self.rotation.value())
374
+
375
+ psf_r = self._simulate_psf(pupil, wavelength_scale=1.15, blur_sigma=self.blur_sigma.value())
376
+ psf_g = self._simulate_psf(pupil, wavelength_scale=1.00, blur_sigma=self.blur_sigma.value())
377
+ psf_b = self._simulate_psf(pupil, wavelength_scale=0.85, blur_sigma=self.blur_sigma.value())
378
+
379
+ self.status.setText("Synthesizing spikes…")
380
+ QApplication.processEvents()
381
+ H, W = img.shape[:2]
382
+ canvas = np.zeros((H, W, 3), dtype=np.float32)
383
+
384
+ flux_max = self.advanced["flux_max"]
385
+ bscale_min = self.advanced["bscale_min"]
386
+ bscale_max = self.advanced["bscale_max"]
387
+ shrink_min = self.advanced["shrink_min"]
388
+ shrink_max = self.advanced["shrink_max"]
389
+ color_boost = self.color_boost.value()
390
+
391
+ # Try OpenCV for faster zoom/blur
392
+ try:
393
+ import cv2
394
+ _HAS_CV2 = True
395
+ except ImportError:
396
+ _HAS_CV2 = False
397
+
398
+ from concurrent.futures import ThreadPoolExecutor, as_completed
399
+ def star_runner(x, y, flux, a, b):
400
+ brightness = np.clip(np.log1p(flux)/8.0, 0.1, 3.0)
401
+ tile_size = int(256 + brightness*20)
402
+ tile_size = min(tile_size, 768)
403
+ tile_size += tile_size % 2
404
+ pad = tile_size // 2
405
+
406
+ # Guard against fully out-of-bounds, but allow partial overlaps
407
+ if not (0 <= x < W and 0 <= y < H):
408
+ return None
409
+
410
+ # Measure star color
411
+ r_ratio, g_ratio, b_ratio = self._measure_star_color(img, x, y, sampling_radius=3)
412
+
413
+ # Extract PSF tiles
414
+ tile_r = self._extract_center_tile(psf_r, tile_size) * brightness * r_ratio * color_boost
415
+ tile_g = self._extract_center_tile(psf_g, tile_size) * brightness * g_ratio * color_boost
416
+ tile_b = self._extract_center_tile(psf_b, tile_size) * brightness * b_ratio * color_boost
417
+
418
+ # Boost/Shrink
419
+ b_scale, s_factor = self._boost_shrink_from_flux(flux, self.flux_min.value(), flux_max,
420
+ bscale_min, bscale_max, shrink_min, shrink_max)
421
+
422
+ # --- Fast Resize (Zoom) ---
423
+ def _fast_zoom(arr, z):
424
+ if z == 1.0: return arr
425
+ if _HAS_CV2:
426
+ h, w = arr.shape
427
+ nw, nh = int(round(w * z)), int(round(h * z))
428
+ if nw <= 0 or nh <= 0: return np.zeros((2,2), dtype=np.float32)
429
+ return cv2.resize(arr, (nw, nh), interpolation=cv2.INTER_LINEAR)
430
+ else:
431
+ return ndi.zoom(arr, z, order=1)
432
+
433
+ final_r = np.clip(_fast_zoom(tile_r * b_scale, 1.0/s_factor), 0.0, 1.0)
434
+ final_g = np.clip(_fast_zoom(tile_g * b_scale, 1.0/s_factor), 0.0, 1.0)
435
+ final_b = np.clip(_fast_zoom(tile_b * b_scale, 1.0/s_factor), 0.0, 1.0)
436
+
437
+ # --- Return Patch Data (y, x, patch) ---
438
+ new_h, new_w = final_r.shape
439
+
440
+ # Coords of the *patch top-left* relative to the image
441
+ # The star is at (x,y), and the patch center is approx (new_w//2, new_h//2)
442
+ # We want to center the patch on the star.
443
+ py0 = y - (new_h // 2)
444
+ px0 = x - (new_w // 2)
445
+
446
+ # Combine channels
447
+ patch = np.dstack((final_r, final_g, final_b)).astype(np.float32)
448
+ return (int(py0), int(px0), patch)
449
+
450
+ with ThreadPoolExecutor() as ex:
451
+ futs = [ex.submit(star_runner, *s) for s in stars]
452
+ for f in as_completed(futs):
453
+ res = f.result()
454
+ if res is None:
455
+ continue
456
+
457
+ py0, px0, patch = res
458
+ ph, pw, _ = patch.shape
459
+
460
+ # Calculate intersection with canvas
461
+ y_start = max(0, py0)
462
+ y_end = min(H, py0 + ph)
463
+ x_start = max(0, px0)
464
+ x_end = min(W, px0 + pw)
465
+
466
+ # If no overlap, skip
467
+ if y_start >= y_end or x_start >= x_end:
468
+ continue
469
+
470
+ # Offsets into the patch
471
+ patch_y_start = y_start - py0
472
+ patch_y_end = patch_y_start + (y_end - y_start)
473
+ patch_x_start = x_start - px0
474
+ patch_x_end = patch_x_start + (x_end - x_start)
475
+
476
+ # Add to canvas
477
+ canvas[y_start:y_end, x_start:x_end] += patch[patch_y_start:patch_y_end, patch_x_start:patch_x_end]
478
+
479
+ self.status.setText("Compositing…")
480
+ QApplication.processEvents()
481
+ if lin.ndim == 3:
482
+ spiked_lin = np.clip(lin + canvas, 0, 1)
483
+ else:
484
+ spikes_mono = 0.2126*canvas[...,0] + 0.7152*canvas[...,1] + 0.0722*canvas[...,2]
485
+ spiked_lin = np.clip(lin + spikes_mono, 0, 1)
486
+
487
+ # protect by active mask (document-level)
488
+ if spiked_lin.ndim == 3:
489
+ spiked_final = np.empty_like(spiked_lin)
490
+ for c in range(3):
491
+ spiked_final[..., c] = self._midtones_m(spiked_lin[..., c], 0.05)
492
+ else:
493
+ spiked_final = self._midtones_m(spiked_lin, 0.05)
494
+
495
+ # ---- apply mask AFTER full processing ----
496
+ m = self._active_mask_array(self.doc)
497
+ if m is not None:
498
+ if spiked_final.ndim == 3 and m.ndim == 2:
499
+ m = m[..., None]
500
+
501
+ # white = apply effect, black = protect original
502
+ final = np.clip(spiked_final * m + img * (1.0 - m), 0.0, 1.0)
503
+ else:
504
+ final = spiked_final
505
+
506
+ self.final_image = final
507
+ self._update_preview(final)
508
+ self.btn_apply.setEnabled(True)
509
+ self.status.setText("Done.")
510
+
511
+ def _apply_to_doc(self):
512
+ if self.final_image is None:
513
+ QMessageBox.information(self, "Diffraction Spikes", "Nothing to apply yet.")
514
+ return
515
+ if not self.docman:
516
+ QMessageBox.warning(self, "No DocManager", "DocManager not available.")
517
+ return
518
+ self.docman.apply_edit_to_active(self.final_image, step_name="Diffraction Spikes")
519
+ self.status.setText("Applied to active document.")
520
+ # keep dialog open so user can tweak more if desired
521
+
522
+ # ---------- helpers ----------
523
+ def _update_preview(self, arr):
524
+ arr8 = np.clip(arr, 0, 1)
525
+ arr8 = (arr8 * 255.0).astype(np.uint8)
526
+ if arr8.ndim == 2:
527
+ h, w = arr8.shape
528
+ qimg = QImage(arr8.data, w, h, w, QImage.Format.Format_Grayscale8)
529
+ else:
530
+ h, w, _ = arr8.shape
531
+ qimg = QImage(arr8.data, w, h, 3*w, QImage.Format.Format_RGB888)
532
+ self.pix.setPixmap(QPixmap.fromImage(qimg))
533
+ self.scene.setSceneRect(self.pix.boundingRect())
534
+ # keep current zoom mode
535
+ self._apply_zoom()
536
+
537
+ def _show_help(self):
538
+ if not self.aperture_help_path:
539
+ QMessageBox.information(self, "Aperture Help", "No help image configured.")
540
+ return
541
+ pm = QPixmap(self.aperture_help_path)
542
+ if pm.isNull():
543
+ QMessageBox.critical(self, "Aperture Help", "Failed to load help image.")
544
+ return
545
+ dlg = QDialog(self)
546
+ dlg.setWindowTitle("Aperture Help")
547
+ v = QVBoxLayout(dlg)
548
+ lab = QLabel()
549
+ lab.setPixmap(pm)
550
+ lab.setAlignment(Qt.AlignmentFlag.AlignCenter)
551
+ v.addWidget(lab)
552
+ dlg.setWindowFlag(Qt.WindowType.Window, True)
553
+ dlg.resize(480, 480)
554
+ dlg.show()
555
+
556
+ # ----- math from SASv2, adapted -----
557
+ @staticmethod
558
+ def _midtones_m(x, m):
559
+ x = np.clip(x, 0.0, 1.0).astype(np.float32)
560
+ out = np.zeros_like(x, dtype=np.float32)
561
+ mask0 = (x == 0); out[mask0] = 0.0
562
+ mask1 = (x == 1); out[mask1] = 1.0
563
+ eps = 1e-7
564
+ maskm = (np.abs(x - m) < eps); out[maskm] = 0.5
565
+ mask_oth = ~(mask0 | mask1 | maskm)
566
+ xm = x[mask_oth]
567
+ num = (m - 1.0)*xm
568
+ den = (2.0*m - 1.0)*xm - m
569
+ out[mask_oth] = np.clip(num/(den+1e-12),0,1)
570
+ return out
571
+
572
+ def _make_pupil(self, size=512, radius=100, obstruction=0.3, vane_width=2, num_vanes=4, rotation=0):
573
+ y, x = np.indices((size, size)) - size // 2
574
+ r = np.sqrt(x**2 + y**2)
575
+ pupil = (r <= radius).astype(np.float32)
576
+ pupil[r < radius * obstruction] = 0.0
577
+ if num_vanes >= 2:
578
+ rot = np.deg2rad(rotation)
579
+ for angle in np.linspace(0, np.pi, num_vanes, endpoint=False) + rot:
580
+ xp = x * np.cos(angle) + y * np.sin(angle)
581
+ vane = np.abs(xp) < vane_width
582
+ pupil[vane] = 0.0
583
+ return pupil
584
+
585
+ def _load_pupil_from_png(self, filepath, size=1024, rotation=0.0):
586
+ img = cv2.imread(filepath, cv2.IMREAD_GRAYSCALE)
587
+ if img is None:
588
+ raise ValueError(f"Failed to load image from {filepath}")
589
+ img = img.astype(np.float32) / 255.0
590
+ if img.shape != (size, size):
591
+ img = cv2.resize(img, (size, size), interpolation=cv2.INTER_AREA)
592
+ if abs(rotation) > 1e-3:
593
+ center = (size // 2, size // 2)
594
+ M = cv2.getRotationMatrix2D(center, rotation, 1.0)
595
+ img = cv2.warpAffine(img, M, (size, size), flags=cv2.INTER_LINEAR,
596
+ borderMode=cv2.BORDER_CONSTANT, borderValue=0)
597
+ return img
598
+
599
+ def _simulate_psf(self, pupil, wavelength_scale=1.0, blur_sigma=1.0):
600
+ # Try to use OpenCV for speed
601
+ if getattr(self, "_cv2_checked", False):
602
+ has_cv2 = True
603
+ import cv2
604
+ else:
605
+ try:
606
+ import cv2
607
+ has_cv2 = True
608
+ except ImportError:
609
+ has_cv2 = False
610
+ self._cv2_checked = has_cv2
611
+
612
+ if has_cv2:
613
+ # Gaussian blur on pupil
614
+ # kernel size usually ~6*sigma, must be odd
615
+ k_pupil = int(math.ceil(6 * (0.1 * wavelength_scale))) | 1
616
+ sp = cv2.GaussianBlur(pupil, (k_pupil, k_pupil), 0.1 * wavelength_scale)
617
+ else:
618
+ sp = gaussian_filter(pupil, sigma=0.1 * wavelength_scale)
619
+
620
+ fft = np.fft.fftshift(np.fft.fft2(np.fft.ifftshift(sp)))
621
+ intensity = np.abs(fft)**2
622
+ intensity /= (intensity.max() + 1e-8)
623
+
624
+ if has_cv2 and blur_sigma > 0:
625
+ k_blur = int(math.ceil(6 * blur_sigma)) | 1
626
+ blurred = cv2.GaussianBlur(intensity, (k_blur, k_blur), blur_sigma)
627
+ else:
628
+ blurred = gaussian_filter(intensity, sigma=blur_sigma)
629
+
630
+ psf = blurred / max(blurred.max(), 1e-8)
631
+
632
+ if wavelength_scale != 1.0:
633
+ if has_cv2:
634
+ h, w = psf.shape
635
+ # Zoom uses size, NOT scale factor in resize(..., dsize=(w,h))
636
+ # wavelength_scale > 1 => zoom in => crop middle? or simply scale?
637
+ # The original used ndi.zoom(psf, zoom=wavelength_scale).
638
+ # New size:
639
+ nw, nh = int(round(w * wavelength_scale)), int(round(h * wavelength_scale))
640
+ if nw > 0 and nh > 0:
641
+ scaled = cv2.resize(psf, (nw, nh), interpolation=cv2.INTER_LINEAR)
642
+ # We might need to crop back to original size or pad?
643
+ # ndi.zoom changes the array size.
644
+ # The simulator seems to assume we handle whatever size comes out?
645
+ # Let's check _extract_center_tile usage.
646
+ psf = scaled
647
+ else:
648
+ psf = ndi.zoom(psf, zoom=wavelength_scale, order=1)
649
+
650
+ psf /= psf.max() + 1e-12
651
+ return psf
652
+
653
+ @staticmethod
654
+ def _extract_center_tile(psf, tile_size):
655
+ c = psf.shape[0]//2
656
+ h = tile_size//2
657
+ y0 = max(0, c-h); x0 = max(0, c-h)
658
+ y1 = y0 + tile_size; x1 = x0 + tile_size
659
+ cropped = psf[y0:y1, x0:x1]
660
+ if cropped.shape != (tile_size, tile_size):
661
+ out = np.zeros((tile_size, tile_size), dtype=np.float32)
662
+ ph, pw = cropped.shape
663
+ out[:ph, :pw] = cropped
664
+ return out
665
+ return cropped
666
+
667
+ @staticmethod
668
+ def _detect_stars(image, threshold=5.0, flux_min=30.0, size_min=1.0):
669
+ data = image.astype(np.float32)
670
+ bkg = sep.Background(data)
671
+ data_sub = data - bkg.back()
672
+ err_val = bkg.globalrms
673
+ try:
674
+ objects = sep.extract(data_sub, threshold, err=err_val)
675
+ except Exception as e:
676
+ if "internal pixel buffer full" in str(e):
677
+ QMessageBox.warning(None, "Star Detection Failed",
678
+ "Star detection failed: internal pixel buffer full.\n"
679
+ "Increase detection threshold or minimum flux.")
680
+ else:
681
+ QMessageBox.critical(None, "Star Detection Failed", str(e))
682
+ return []
683
+ stars = []
684
+ for obj in objects:
685
+ flux = obj['flux']; a = obj['a']; b = obj['b']
686
+ if flux >= flux_min and max(a,b) >= size_min:
687
+ stars.append((int(obj['x']), int(obj['y']), float(flux), float(a), float(b)))
688
+ return stars
689
+
690
+ # _shrink_and_boost removed (replaced by inline _fast_zoom for performance)
691
+
692
+ @staticmethod
693
+ def _boost_shrink_from_flux(flux, flux_min, flux_max, bmin, bmax, smin, smax):
694
+ f = np.clip(flux, flux_min, flux_max)
695
+ alpha = 0.0 if flux_max <= flux_min else (f - flux_min) / (flux_max - flux_min)
696
+ bscale = bmin + alpha * (bmax - bmin)
697
+ shrink = smax - alpha * (smax - smin)
698
+ return float(bscale), float(shrink)
699
+
700
+ @staticmethod
701
+ def _measure_star_color(img_color, x, y, sampling_radius=20):
702
+ if img_color.ndim == 2:
703
+ return (1.0, 1.0, 1.0)
704
+ H, W, C = img_color.shape
705
+ if C != 3:
706
+ return (1.0, 1.0, 1.0)
707
+ x0 = max(0, int(x - sampling_radius)); x1 = min(W, int(x + sampling_radius + 1))
708
+ y0 = max(0, int(y - sampling_radius)); y1 = min(H, int(y + sampling_radius + 1))
709
+ if x1 <= x0 or y1 <= y0:
710
+ return (1.0, 1.0, 1.0)
711
+ patch = img_color[y0:y1, x0:x1, :]
712
+ mean_col = np.mean(patch, axis=(0, 1))
713
+ mx = float(np.max(mean_col))
714
+ if mx < 1e-9:
715
+ return (1.0, 1.0, 1.0)
716
+ return (float(mean_col[0]/mx), float(mean_col[1]/mx), float(mean_col[2]/mx))
717
+
718
+ @staticmethod
719
+ def _active_mask_array(doc) -> np.ndarray | None:
720
+ if not doc:
721
+ return None
722
+ mid = getattr(doc, "active_mask_id", None)
723
+ if not mid:
724
+ return None
725
+ masks = getattr(doc, "masks", {}) or {}
726
+ layer = masks.get(mid)
727
+ data = getattr(layer, "data", None) if layer is not None else None
728
+ if data is None:
729
+ return None
730
+ a = np.asarray(data)
731
+ if a.ndim == 3 and a.shape[2] == 1:
732
+ a = a[..., 0]
733
+ if a.ndim != 2:
734
+ return None
735
+ a = a.astype(np.float32, copy=False)
736
+ a = np.clip(a, 0.0, 1.0)
737
+ # keep original where mask == 1.0 (protection mask semantics)
738
+ return a
739
+
740
+ def _apply_zoom(self):
741
+ if self._fit_mode:
742
+ self.view.fitInView(self.pix, Qt.AspectRatioMode.KeepAspectRatio)
743
+ return
744
+ self.view.resetTransform()
745
+ self.view.scale(self._zoom, self._zoom)
746
+
747
+ def _zoom_in(self):
748
+ if self.pix.pixmap().isNull():
749
+ return
750
+ if self._fit_mode:
751
+ self._fit_mode = False
752
+ self._zoom = 1.0
753
+ self._zoom = min(self._zoom * 1.25, 20.0)
754
+ self._apply_zoom()
755
+
756
+ def _zoom_out(self):
757
+ if self.pix.pixmap().isNull():
758
+ return
759
+ if self._fit_mode:
760
+ self._fit_mode = False
761
+ self._zoom = 1.0
762
+ self._zoom = max(self._zoom / 1.25, 0.05)
763
+ self._apply_zoom()
764
+
765
+ def _fit_to_preview(self):
766
+ if self.pix.pixmap().isNull():
767
+ return
768
+ self._fit_mode = True
769
+ self._apply_zoom()