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,910 @@
1
+ # pro/aberration_ai.py
2
+ from __future__ import annotations
3
+ import os
4
+ import webbrowser
5
+ import requests
6
+ import numpy as np
7
+ import sys
8
+ import platform # add
9
+ import time
10
+
11
+ IS_APPLE_ARM = (sys.platform == "darwin" and platform.machine() == "arm64")
12
+
13
+ from PyQt6.QtCore import Qt, QThread, pyqtSignal, QStandardPaths, QSettings
14
+ from PyQt6.QtWidgets import (
15
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QFileDialog,
16
+ QComboBox, QSpinBox, QProgressBar, QMessageBox, QCheckBox, QLineEdit
17
+ )
18
+ from PyQt6.QtGui import QIcon
19
+ from setiastro.saspro.config import Config
20
+
21
+ # Optional import (soft dep)
22
+ try:
23
+ import onnxruntime as ort
24
+ except Exception:
25
+ ort = None
26
+
27
+
28
+ # ---------- GitHub model fetching ----------
29
+ GITHUB_REPO = Config.GITHUB_ABERRATION_REPO
30
+ LATEST_API = f"https://api.github.com/repos/{GITHUB_REPO}/releases/latest"
31
+
32
+ def _model_required_patch(model_path: str) -> int | None:
33
+ """
34
+ Returns the fixed spatial size the model expects (e.g. 512), or None if dynamic.
35
+ """
36
+ if ort is None or not os.path.isfile(model_path):
37
+ return None
38
+ try:
39
+ sess = ort.InferenceSession(model_path, providers=["CPUExecutionProvider"])
40
+ shp = sess.get_inputs()[0].shape # e.g. [1, 1, 512, 512] or ['N','C',512,512]
41
+ h = shp[-2]; w = shp[-1]
42
+ if isinstance(h, int) and isinstance(w, int) and h == w:
43
+ return int(h)
44
+ except Exception:
45
+ pass
46
+ return None
47
+
48
+
49
+ def _app_model_dir() -> str:
50
+ d = Config.get_aberration_models_dir()
51
+ os.makedirs(d, exist_ok=True)
52
+ return d
53
+
54
+
55
+ class _DownloadWorker(QThread):
56
+ progressed = pyqtSignal(int) # 0..100 (downloaded)
57
+ failed = pyqtSignal(str)
58
+ finished_ok= pyqtSignal(str) # path
59
+
60
+ def __init__(self, dst_dir: str):
61
+ super().__init__()
62
+ self.dst_dir = dst_dir
63
+
64
+ def run(self):
65
+ try:
66
+ r = requests.get(LATEST_API, timeout=10)
67
+ if r.status_code != 200:
68
+ raise RuntimeError(f"GitHub API error: {r.status_code}")
69
+ js = r.json()
70
+ assets = js.get("assets", [])
71
+ onnx_assets = [a for a in assets if a.get("name","").lower().endswith(".onnx")]
72
+ if not onnx_assets:
73
+ raise RuntimeError("No .onnx asset found in latest release.")
74
+ asset = onnx_assets[0]
75
+ url = asset["browser_download_url"]
76
+ name = asset["name"]
77
+ out_path = os.path.join(self.dst_dir, name)
78
+
79
+ with requests.get(url, stream=True, timeout=60) as rr:
80
+ rr.raise_for_status()
81
+ total = int(rr.headers.get("Content-Length", "0") or 0)
82
+ got = 0
83
+ chunk = 1 << 20
84
+ with open(out_path, "wb") as f:
85
+ for blk in rr.iter_content(chunk):
86
+ if blk:
87
+ f.write(blk)
88
+ got += len(blk)
89
+ if total > 0:
90
+ self.progressed.emit(int(got * 100 / total))
91
+ self.finished_ok.emit(out_path)
92
+ except Exception as e:
93
+ self.failed.emit(str(e))
94
+
95
+
96
+ # ---------- core: tiling + hann blend ----------
97
+ def _hann2d(n: int) -> np.ndarray:
98
+ w = np.hanning(n).astype(np.float32)
99
+ return (w[:, None] * w[None, :])
100
+
101
+ def _tile_indices(n: int, patch: int, overlap: int) -> list[int]:
102
+ stride = patch - overlap
103
+ if patch >= n:
104
+ return [0]
105
+ idx, pos = [], 0
106
+ while True:
107
+ if pos + patch >= n:
108
+ idx.append(n - patch)
109
+ break
110
+ idx.append(pos); pos += stride
111
+ return sorted(set(idx))
112
+
113
+ def _pad_C_HW(arr: np.ndarray, patch: int) -> tuple[np.ndarray, int, int]:
114
+ C, H, W = arr.shape
115
+ pad_h = max(0, patch - H)
116
+ pad_w = max(0, patch - W)
117
+ if pad_h or pad_w:
118
+ arr = np.pad(arr, ((0,0),(0,pad_h),(0,pad_w)), mode="edge")
119
+ return arr, H, W
120
+
121
+ def _prepare_input(img: np.ndarray) -> tuple[np.ndarray, bool, bool]:
122
+ """
123
+ Returns (C,H,W) float32 in [0..1]; also returns (channels_last, was_uint16)
124
+ """
125
+ channels_last = (img.ndim == 3)
126
+ if channels_last:
127
+ arr = img.transpose(2,0,1) # (C,H,W)
128
+ else:
129
+ arr = img[np.newaxis, ...] # (1,H,W)
130
+ was_uint16 = (arr.dtype == np.uint16)
131
+ if was_uint16:
132
+ arr = arr.astype(np.float32) / 65535.0
133
+ else:
134
+ arr = arr.astype(np.float32)
135
+ return arr, channels_last, was_uint16
136
+
137
+ def _restore_output(arr: np.ndarray, channels_last: bool, was_uint16: bool, H: int, W: int) -> np.ndarray:
138
+ arr = arr[:, :H, :W]
139
+ arr = np.clip(np.nan_to_num(arr), 0.0, 1.0)
140
+ if was_uint16:
141
+ arr = (arr * 65535.0).astype(np.uint16)
142
+ if channels_last:
143
+ arr = arr.transpose(1,2,0) # (H,W,C)
144
+ else:
145
+ arr = arr[0] # (H,W)
146
+ return arr
147
+
148
+ def run_onnx_tiled(session, img: np.ndarray, patch_size=512, overlap=64, progress_cb=None) -> np.ndarray:
149
+ """
150
+ session: onnxruntime.InferenceSession
151
+ img: mono (H,W) or RGB (H,W,3) numpy array
152
+ """
153
+ arr, channels_last, was_uint16 = _prepare_input(img) # (C,H,W)
154
+ arr, H0, W0 = _pad_C_HW(arr, patch_size)
155
+ C, H, W = arr.shape
156
+
157
+ win = _hann2d(patch_size)
158
+ out = np.zeros_like(arr, dtype=np.float32)
159
+ wgt = np.zeros_like(arr, dtype=np.float32)
160
+
161
+ hs = _tile_indices(H, patch_size, overlap)
162
+ ws = _tile_indices(W, patch_size, overlap)
163
+
164
+ inp_name = session.get_inputs()[0].name
165
+ total = len(hs) * len(ws) * C
166
+ done = 0
167
+
168
+ for c in range(C):
169
+ for i in hs:
170
+ for j in ws:
171
+ patch = arr[c:c+1, i:i+patch_size, j:j+patch_size] # (1, P, P)
172
+ inp = np.ascontiguousarray(patch[np.newaxis, ...], dtype=np.float32) # (1,1,P,P)
173
+
174
+ out_patch = session.run(None, {inp_name: inp})[0] # (1,1,P,P)
175
+ out_patch = np.squeeze(out_patch, axis=0) # (1,P,P)
176
+ out[c:c+1, i:i+patch_size, j:j+patch_size] += out_patch * win
177
+ wgt[c:c+1, i:i+patch_size, j:j+patch_size] += win
178
+
179
+ done += 1
180
+ if progress_cb:
181
+ progress_cb(done / max(1, total))
182
+
183
+ wgt[wgt == 0] = 1.0
184
+ arr = out / wgt
185
+ return _restore_output(arr, channels_last, was_uint16, H0, W0)
186
+
187
+
188
+ # ---------- providers ----------
189
+ def pick_providers(auto_gpu=True) -> list[str]:
190
+ """
191
+ Windows: DirectML → CUDA → CPU
192
+ mac(Intel): CPU → CoreML (optional)
193
+ mac(Apple Silicon): **CPU only** (avoid CoreML artifact path)
194
+ """
195
+ if ort is None:
196
+ return []
197
+
198
+ avail = set(ort.get_available_providers())
199
+
200
+ # Apple Silicon: always CPU ( CoreML has 16,384-dim constraint and can artifact )
201
+ if IS_APPLE_ARM:
202
+ return ["CPUExecutionProvider"] if "CPUExecutionProvider" in avail else []
203
+
204
+ # Non-Apple ARM
205
+ if not auto_gpu:
206
+ return ["CPUExecutionProvider"] if "CPUExecutionProvider" in avail else []
207
+
208
+ order = []
209
+ if "DmlExecutionProvider" in avail:
210
+ order.append("DmlExecutionProvider")
211
+ if "CUDAExecutionProvider" in avail:
212
+ order.append("CUDAExecutionProvider")
213
+
214
+ # mac(Intel) can still use CoreML if someone insists, but we won't put it first.
215
+ if "CPUExecutionProvider" in avail:
216
+ order.append("CPUExecutionProvider")
217
+ if "CoreMLExecutionProvider" in avail:
218
+ order.append("CoreMLExecutionProvider")
219
+
220
+ return order
221
+
222
+
223
+ def _preserve_border(dst: np.ndarray, src: np.ndarray, px: int = 10) -> np.ndarray:
224
+ """
225
+ Copy a px-wide ring from src → dst, in-place. Handles mono/RGB.
226
+ Expects same shape for src and dst. Clamps px to image size.
227
+ """
228
+ if px <= 0 or dst is None or src is None:
229
+ return dst
230
+ if dst.shape != src.shape:
231
+ return dst # shapes differ; skip quietly
232
+
233
+ h, w = dst.shape[:2]
234
+ px = int(max(0, min(px, h // 2, w // 2)))
235
+ if px == 0:
236
+ return dst
237
+
238
+ s = src.astype(dst.dtype, copy=False)
239
+
240
+ # top & bottom
241
+ dst[:px, ...] = s[:px, ...]
242
+ dst[-px:, ...] = s[-px:, ...]
243
+ # left & right
244
+ dst[:, :px, ...] = s[:, :px, ...]
245
+ dst[:, -px:, ...] = s[:, -px:, ...]
246
+
247
+ return dst
248
+
249
+ # ---------- worker ----------
250
+ class _ONNXWorker(QThread):
251
+ progressed = pyqtSignal(int) # 0..100
252
+ failed = pyqtSignal(str)
253
+ finished_ok= pyqtSignal(np.ndarray)
254
+
255
+ def __init__(self, model_path: str, image: np.ndarray, patch: int, overlap: int, providers: list[str]):
256
+ super().__init__()
257
+ self.model_path = model_path
258
+ self.image = image
259
+ self.patch = patch
260
+ self.overlap = overlap
261
+ self.providers = providers
262
+ self.used_provider = None
263
+
264
+ def run(self):
265
+ if ort is None:
266
+ self.failed.emit("onnxruntime is not installed.")
267
+ return
268
+ try:
269
+ sess = ort.InferenceSession(self.model_path, providers=self.providers)
270
+ self.used_provider = (sess.get_providers()[0] if sess.get_providers() else None)
271
+ except Exception:
272
+ # fallback CPU if GPU fails
273
+ try:
274
+ sess = ort.InferenceSession(self.model_path, providers=["CPUExecutionProvider"])
275
+ self.used_provider = "CPUExecutionProvider" # NEW
276
+ except Exception as e2:
277
+ self.failed.emit(f"Failed to init ONNX session:\n{e2}")
278
+ return
279
+
280
+ def cb(frac):
281
+ self.progressed.emit(int(frac * 100))
282
+
283
+ try:
284
+ out = run_onnx_tiled(sess, self.image, self.patch, self.overlap, cb)
285
+ except Exception as e:
286
+ self.failed.emit(str(e)); return
287
+
288
+ self.finished_ok.emit(out)
289
+
290
+
291
+ # ---------- dialog ----------
292
+ class AberrationAIDialog(QDialog):
293
+ def __init__(self, parent, docman, get_active_doc_callable, icon: QIcon | None = None):
294
+ super().__init__(parent)
295
+ self.setWindowTitle(self.tr("R.A.'s Aberration Correction (AI)"))
296
+ if icon is not None:
297
+ self.setWindowIcon(icon)
298
+
299
+ # Normalize window behavior across platforms
300
+ self.setWindowFlag(Qt.WindowType.Window, True)
301
+ # Non-modal: allow user to switch between images while dialog is open
302
+ self.setWindowModality(Qt.WindowModality.NonModal)
303
+ self.setModal(False)
304
+ #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
305
+
306
+ self.docman = docman
307
+ self.get_active_doc = get_active_doc_callable
308
+ self._t_start = None
309
+ self._last_used_provider = None
310
+
311
+ v = QVBoxLayout(self)
312
+
313
+ # Model row
314
+ row = QHBoxLayout()
315
+ row.addWidget(QLabel(self.tr("Model:")))
316
+ self.model_label = QLabel("—")
317
+ self.model_label.setToolTip("")
318
+ btn_browse = QPushButton(self.tr("Browse…")); btn_browse.clicked.connect(self._browse_active_model)
319
+ row.addWidget(self.model_label, 1)
320
+ row.addWidget(btn_browse)
321
+ v.addLayout(row)
322
+ # Custom model row (NEW)
323
+ row_custom = QHBoxLayout()
324
+ self.chk_use_custom = QCheckBox(self.tr("Use custom model file"))
325
+ self.chk_use_custom.setChecked(False)
326
+ self.chk_use_custom.toggled.connect(self._on_use_custom_toggled)
327
+
328
+ self.le_custom_model = QLineEdit()
329
+ self.le_custom_model.setReadOnly(True)
330
+ self.le_custom_model.setPlaceholderText(self.tr("No custom model selected"))
331
+ self.le_custom_model.setToolTip("")
332
+
333
+ btn_custom_clear = QPushButton(self.tr("Clear"))
334
+ btn_custom_clear.clicked.connect(self._clear_custom_model)
335
+
336
+ row_custom.addWidget(self.chk_use_custom)
337
+ row_custom.addWidget(self.le_custom_model, 1)
338
+
339
+ row_custom.addWidget(btn_custom_clear)
340
+ v.addLayout(row_custom)
341
+ # Providers row
342
+ row2 = QHBoxLayout()
343
+ self.chk_auto = QCheckBox(self.tr("Auto GPU (if available)"))
344
+ self.chk_auto.setChecked(True)
345
+ row2.addWidget(self.chk_auto)
346
+ self.cmb_provider = QComboBox()
347
+ row2.addWidget(QLabel(self.tr("Provider:")))
348
+ row2.addWidget(self.cmb_provider, 1)
349
+ v.addLayout(row2)
350
+
351
+ # Params row
352
+ row3 = QHBoxLayout()
353
+ row3.addWidget(QLabel(self.tr("Patch")))
354
+ self.spin_patch = QSpinBox(minimum=128, maximum=2048); self.spin_patch.setValue(512)
355
+ row3.addWidget(self.spin_patch)
356
+ row3.addWidget(QLabel(self.tr("Overlap")))
357
+ self.spin_overlap = QSpinBox(minimum=16, maximum=512); self.spin_overlap.setValue(64)
358
+ row3.addWidget(self.spin_overlap)
359
+ v.addLayout(row3)
360
+
361
+ # Download / Open folder
362
+ row4 = QHBoxLayout()
363
+ btn_latest = QPushButton(self.tr("Download latest model…"))
364
+ btn_latest.clicked.connect(self._download_latest_model)
365
+ row4.addWidget(btn_latest)
366
+ btn_openfolder = QPushButton(self.tr("Open model folder"))
367
+ btn_openfolder.clicked.connect(self._open_model_folder)
368
+ row4.addWidget(btn_openfolder)
369
+ row4.addStretch(1)
370
+ v.addLayout(row4)
371
+
372
+ # Progress + actions
373
+ self.progress = QProgressBar(); self.progress.setRange(0, 100); v.addWidget(self.progress)
374
+ row5 = QHBoxLayout()
375
+ self.btn_run = QPushButton(self.tr("Run")); self.btn_run.clicked.connect(self._run)
376
+ btn_close = QPushButton(self.tr("Close")); btn_close.clicked.connect(self.reject)
377
+ row5.addStretch(1); row5.addWidget(self.btn_run); row5.addWidget(btn_close)
378
+ v.addLayout(row5)
379
+
380
+ info = QLabel(
381
+ "Model and weights © Riccardo Alberghi — "
382
+ "<a href='https://github.com/riccardoalberghi'>more information</a>."
383
+ )
384
+ info.setTextFormat(Qt.TextFormat.RichText)
385
+ info.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
386
+ info.setOpenExternalLinks(True)
387
+ info.setWordWrap(True)
388
+ info.setStyleSheet("color:#888; font-size:11px; margin-top:4px;")
389
+ v.addWidget(info)
390
+
391
+ self._model_path = None
392
+ self._refresh_providers()
393
+ self._load_last_model_from_settings()
394
+ self._load_last_custom_model_from_settings()
395
+ use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
396
+ self.chk_use_custom.setChecked(bool(use_custom))
397
+ if IS_APPLE_ARM:
398
+ self.chk_auto.setChecked(False)
399
+ self.chk_auto.setEnabled(False)
400
+
401
+ # ----- model helpers -----
402
+ def _set_model_path(self, p: str | None):
403
+ self._model_path = p
404
+ if p:
405
+ self.model_label.setText(os.path.basename(p))
406
+ self.model_label.setToolTip(p)
407
+ QSettings().setValue("AberrationAI/model_path", p)
408
+ else:
409
+ self.model_label.setText("—")
410
+ self.model_label.setToolTip("")
411
+ QSettings().remove("AberrationAI/model_path")
412
+
413
+ def _load_last_model_from_settings(self):
414
+ p = QSettings().value("AberrationAI/model_path", type=str)
415
+ if p and os.path.isfile(p):
416
+ self._set_model_path(p)
417
+
418
+ def _browse_active_model(self):
419
+ """
420
+ Single Browse button.
421
+ - If user picks a file inside the app model folder -> treat as "downloaded" selection (use_custom_model=False)
422
+ - If user picks a file outside -> treat as "custom" (use_custom_model=True)
423
+ """
424
+ app_dir = os.path.abspath(_app_model_dir())
425
+
426
+ # Start in last-used folder if possible
427
+ last_custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
428
+ last_downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
429
+ start_dir = None
430
+ for candidate in (last_custom, last_downloaded):
431
+ if candidate and os.path.isfile(candidate):
432
+ d = os.path.dirname(candidate)
433
+ if os.path.isdir(d):
434
+ start_dir = d
435
+ break
436
+ if start_dir is None:
437
+ start_dir = app_dir
438
+
439
+ p, _ = QFileDialog.getOpenFileName(self, "Select ONNX model", start_dir, "ONNX (*.onnx)")
440
+ if not p:
441
+ return
442
+
443
+ p_abs = os.path.abspath(p)
444
+ # Determine if picked file is inside app model folder
445
+ in_app_dir = False
446
+ try:
447
+ in_app_dir = os.path.commonpath([app_dir, p_abs]) == app_dir
448
+ except Exception:
449
+ in_app_dir = p_abs.startswith(app_dir)
450
+
451
+ if in_app_dir:
452
+ # "Downloaded" selection
453
+ self._set_model_path(p_abs)
454
+ self._set_custom_model_path(None)
455
+ QSettings().setValue("AberrationAI/use_custom_model", False)
456
+ if hasattr(self, "chk_use_custom"):
457
+ self.chk_use_custom.setChecked(False)
458
+ else:
459
+ # "Custom" selection
460
+ self._set_custom_model_path(p_abs)
461
+ QSettings().setValue("AberrationAI/use_custom_model", True)
462
+ if hasattr(self, "chk_use_custom"):
463
+ self.chk_use_custom.setChecked(True)
464
+
465
+ # Keep visuals in sync
466
+ self._refresh_model_label()
467
+ self._refresh_custom_row_visibility()
468
+
469
+
470
+ def _refresh_model_label(self):
471
+ downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
472
+ custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
473
+ use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
474
+
475
+ if use_custom and custom:
476
+ self.model_label.setText(f"Custom: {os.path.basename(custom)}")
477
+ self.model_label.setToolTip(custom)
478
+ elif downloaded:
479
+ self.model_label.setText(f"Downloaded: {os.path.basename(downloaded)}")
480
+ self.model_label.setToolTip(downloaded)
481
+ else:
482
+ self.model_label.setText("—")
483
+ self.model_label.setToolTip("")
484
+
485
+
486
+ def _open_model_folder(self):
487
+ d = _app_model_dir()
488
+ try:
489
+ if os.name == "nt":
490
+ os.startfile(d) # type: ignore
491
+ elif sys.platform == "darwin":
492
+ import subprocess; subprocess.Popen(["open", d])
493
+ else:
494
+ import subprocess; subprocess.Popen(["xdg-open", d])
495
+ except Exception:
496
+ webbrowser.open(f"file://{d}")
497
+ # ----- custom model helpers (NEW) -----
498
+ def _set_custom_model_path(self, p: str | None):
499
+ if p:
500
+ self.le_custom_model.setText(os.path.basename(p))
501
+ self.le_custom_model.setToolTip(p)
502
+ QSettings().setValue("AberrationAI/custom_model_path", p)
503
+ else:
504
+ self.le_custom_model.clear()
505
+ self.le_custom_model.setToolTip("")
506
+ QSettings().remove("AberrationAI/custom_model_path")
507
+
508
+ def _load_last_custom_model_from_settings(self):
509
+ p = QSettings().value("AberrationAI/custom_model_path", type=str)
510
+ if p:
511
+ if os.path.isfile(p):
512
+ self._set_custom_model_path(p)
513
+ else:
514
+ # Keep the broken path visible in tooltip for debugging
515
+ if hasattr(self, "le_custom_model"):
516
+ self.le_custom_model.setText(os.path.basename(p) + " (missing)")
517
+ self.le_custom_model.setToolTip(p)
518
+
519
+ # After both loads, sync labels/visibility
520
+ self._refresh_model_label()
521
+ self._refresh_custom_row_visibility()
522
+
523
+ def _refresh_custom_row_visibility(self):
524
+ """
525
+ If you keep the custom row in the UI, hide the path field unless custom is enabled.
526
+ """
527
+ if not hasattr(self, "le_custom_model"):
528
+ return
529
+ use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
530
+ self.le_custom_model.setVisible(bool(use_custom))
531
+
532
+
533
+ def _refresh_model_label(self):
534
+ downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
535
+ custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
536
+ use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
537
+
538
+ # Prefer custom only if enabled AND the file exists
539
+ if use_custom and custom:
540
+ if os.path.isfile(custom):
541
+ self.model_label.setText(f"Custom: {os.path.basename(custom)}")
542
+ self.model_label.setToolTip(custom)
543
+ return
544
+ else:
545
+ self.model_label.setText(f"Custom: {os.path.basename(custom)} (missing)")
546
+ self.model_label.setToolTip(custom)
547
+ return
548
+
549
+ # Otherwise show downloaded if valid
550
+ if downloaded and os.path.isfile(downloaded):
551
+ self.model_label.setText(f"Downloaded: {os.path.basename(downloaded)}")
552
+ self.model_label.setToolTip(downloaded)
553
+ else:
554
+ self.model_label.setText("—")
555
+ self.model_label.setToolTip("")
556
+
557
+
558
+ def _browse_custom_model(self):
559
+ # Start at last dir if possible, else app model dir
560
+ last = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
561
+ start_dir = os.path.dirname(last) if last and os.path.isdir(os.path.dirname(last)) else _app_model_dir()
562
+ p, _ = QFileDialog.getOpenFileName(self, "Select custom ONNX model", start_dir, "ONNX (*.onnx)")
563
+ if p:
564
+ self._set_custom_model_path(p)
565
+ QSettings().setValue("AberrationAI/use_custom_model", True)
566
+ if not self.chk_use_custom.isChecked():
567
+ self.chk_use_custom.setChecked(True)
568
+
569
+ def _clear_custom_model(self):
570
+ self._set_custom_model_path(None)
571
+ QSettings().setValue("AberrationAI/use_custom_model", False)
572
+ if hasattr(self, "chk_use_custom"):
573
+ self.chk_use_custom.setChecked(False)
574
+
575
+ self._refresh_model_label()
576
+ self._refresh_custom_row_visibility()
577
+
578
+
579
+ def _on_use_custom_toggled(self, on: bool):
580
+ QSettings().setValue("AberrationAI/use_custom_model", bool(on))
581
+
582
+ if on:
583
+ p = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
584
+ if not (p and os.path.isfile(p)):
585
+ # Don’t spawn another browse button path; use the ONE browse if they want
586
+ QMessageBox.information(
587
+ self,
588
+ self.tr("Custom model"),
589
+ self.tr("Custom model is enabled, but no custom file is selected.\n"
590
+ "Click Browse… to choose a model file.")
591
+ )
592
+ # Optional: auto-open the single browse:
593
+ # self._browse_active_model()
594
+ # return
595
+
596
+ self._refresh_model_label()
597
+ self._refresh_custom_row_visibility()
598
+
599
+
600
+ # ----- provider UI -----
601
+ def _log(self, msg: str): # NEW
602
+ mw = self.parent()
603
+ try:
604
+ if hasattr(mw, "_log"):
605
+ mw._log(msg)
606
+ elif hasattr(mw, "update_status"):
607
+ mw.update_status(msg)
608
+ except Exception:
609
+ pass
610
+
611
+ def _refresh_providers(self):
612
+ if ort is None:
613
+ self.cmb_provider.clear()
614
+ self.cmb_provider.addItem("onnxruntime not installed")
615
+ self.cmb_provider.setEnabled(False)
616
+ return
617
+
618
+ avail = ort.get_available_providers()
619
+ self.cmb_provider.clear()
620
+
621
+ if IS_APPLE_ARM:
622
+ # Hard lock to CPU on M-series
623
+ self.cmb_provider.addItem("CPUExecutionProvider")
624
+ self.cmb_provider.setCurrentText("CPUExecutionProvider")
625
+ self.cmb_provider.setEnabled(False)
626
+ # also turn off Auto GPU and disable that checkbox
627
+ self.chk_auto.setChecked(False)
628
+ self.chk_auto.setEnabled(False)
629
+ return
630
+
631
+ # Other platforms: show all, sane default
632
+ for name in avail:
633
+ self.cmb_provider.addItem(name)
634
+
635
+ if "DmlExecutionProvider" in avail:
636
+ self.cmb_provider.setCurrentText("DmlExecutionProvider")
637
+ elif "CUDAExecutionProvider" in avail:
638
+ self.cmb_provider.setCurrentText("CUDAExecutionProvider")
639
+ elif "CPUExecutionProvider" in avail:
640
+ self.cmb_provider.setCurrentText("CPUExecutionProvider")
641
+ elif "CoreMLExecutionProvider" in avail:
642
+ self.cmb_provider.setCurrentText("CoreMLExecutionProvider")
643
+
644
+ # ----- download -----
645
+ def _download_latest_model(self):
646
+ if requests is None:
647
+ QMessageBox.warning(self, "Network", "The 'requests' package is required."); return
648
+ dst = _app_model_dir()
649
+ self.progress.setRange(0, 0) # busy
650
+ self.btn_run.setEnabled(False)
651
+ self._dl = _DownloadWorker(dst)
652
+ self._dl.progressed.connect(self.progress.setValue)
653
+ self._dl.failed.connect(self._on_download_failed)
654
+ self._dl.finished_ok.connect(self._on_download_ok)
655
+ self._dl.finished.connect(lambda: (self.progress.setRange(0, 100), self.btn_run.setEnabled(True)))
656
+ self._dl.start()
657
+
658
+ def _on_download_failed(self, msg: str):
659
+ QMessageBox.critical(self, "Download", msg)
660
+
661
+ def _on_download_ok(self, path: str):
662
+ self.progress.setValue(100)
663
+ self._set_model_path(path)
664
+
665
+ # Download becomes the active model unless custom is explicitly enabled
666
+ if not QSettings().value("AberrationAI/use_custom_model", False, type=bool):
667
+ self._set_custom_model_path(None)
668
+
669
+ QMessageBox.information(self, "Model", f"Downloaded: {os.path.basename(path)}")
670
+
671
+ self._refresh_model_label()
672
+ self._refresh_custom_row_visibility()
673
+
674
+ # ----- run -----
675
+ def _run(self):
676
+ if ort is None:
677
+ QMessageBox.critical(
678
+ self,
679
+ "Unsupported ONNX Runtime",
680
+ "The currently installed onnxruntime is not supported on this machine.\n"
681
+ "Please try installing an earlier version (for example 1.19.x) and try again."
682
+ )
683
+ return
684
+
685
+ # Choose model path (normal vs custom)
686
+ use_custom = QSettings().value("AberrationAI/use_custom_model", False, type=bool)
687
+ downloaded = QSettings().value("AberrationAI/model_path", type=str) or ""
688
+ custom = QSettings().value("AberrationAI/custom_model_path", type=str) or ""
689
+
690
+ model_path = custom if use_custom else downloaded
691
+ if self.chk_use_custom.isChecked():
692
+ cp = QSettings().value("AberrationAI/custom_model_path", type=str)
693
+ if cp and os.path.isfile(cp):
694
+ model_path = cp
695
+ else:
696
+ QMessageBox.warning(self, "Model", "Custom model is enabled but the file is missing. Please browse to a valid .onnx.")
697
+ return
698
+
699
+ if not model_path or not os.path.isfile(model_path):
700
+ QMessageBox.warning(self, "Model", "Please select or download a valid .onnx model first.")
701
+ return
702
+
703
+ doc = self.get_active_doc()
704
+ if doc is None or getattr(doc, "image", None) is None:
705
+ QMessageBox.warning(self, "Image", "No active image.")
706
+ return
707
+
708
+ img = np.asarray(doc.image)
709
+ self._orig_for_border = img.copy()
710
+
711
+ patch = int(self.spin_patch.value())
712
+ overlap = int(self.spin_overlap.value())
713
+
714
+ # -------- providers (always choose, then always run) --------
715
+ if IS_APPLE_ARM:
716
+ providers = ["CPUExecutionProvider"]
717
+ self.chk_auto.setChecked(False)
718
+ else:
719
+ if self.chk_auto.isChecked():
720
+ providers = pick_providers(auto_gpu=True)
721
+ else:
722
+ sel = self.cmb_provider.currentText()
723
+ providers = [sel] if sel else ["CPUExecutionProvider"]
724
+
725
+ # --- make patch match the model's requirement (if fixed) ---
726
+ req = _model_required_patch(model_path)
727
+ if req and req > 0:
728
+ patch = req
729
+ try:
730
+ self.spin_patch.blockSignals(True)
731
+ self.spin_patch.setValue(req)
732
+ finally:
733
+ self.spin_patch.blockSignals(False)
734
+
735
+ # --- CoreML guard on Intel: if model needs >128, run on CPU instead ---
736
+ if ("CoreMLExecutionProvider" in providers) and (req and req > 128):
737
+ self._log(f"CoreML limited to small tiles; model requires {req}px → using CPU.")
738
+ providers = ["CPUExecutionProvider"]
739
+ try:
740
+ self.cmb_provider.setCurrentText("CPUExecutionProvider")
741
+ self.chk_auto.setChecked(False)
742
+ except Exception:
743
+ pass
744
+
745
+ self._t_start = time.perf_counter()
746
+ prov_txt = ("auto" if self.chk_auto.isChecked() else self.cmb_provider.currentText() or "CPU")
747
+ self._log(f"🚀 Aberration AI: model={os.path.basename(model_path)}, "
748
+ f"provider={prov_txt}, patch={patch}, overlap={overlap}")
749
+
750
+ self._effective_model_path = model_path
751
+
752
+ # -------- run worker --------
753
+ self.progress.setValue(0)
754
+ self.btn_run.setEnabled(False)
755
+
756
+ self._worker = _ONNXWorker(model_path, img, patch, overlap, providers)
757
+ self._worker.progressed.connect(self.progress.setValue)
758
+ self._worker.failed.connect(self._on_failed)
759
+ self._worker.finished_ok.connect(self._on_ok)
760
+ self._worker.finished.connect(self._on_worker_finished)
761
+ self._worker.start()
762
+
763
+
764
+ def _on_failed(self, msg: str):
765
+ model_path = getattr(self, "_effective_model_path", self._model_path)
766
+ self._log(f"❌ Aberration AI failed: {msg}")
767
+ QMessageBox.critical(self, "ONNX Error", msg)
768
+ self.reject() # closes the dialog
769
+
770
+ def _on_ok(self, out: np.ndarray):
771
+ used = getattr(self._worker, "used_provider", None) or \
772
+ (self.cmb_provider.currentText() if not self.chk_auto.isChecked() else "auto")
773
+ model_path = getattr(self, "_effective_model_path", self._model_path)
774
+ doc = self.get_active_doc()
775
+ if doc is None or getattr(doc, "image", None) is None:
776
+ QMessageBox.warning(self, "Image", "No active image.")
777
+ return
778
+
779
+ # 1) Preserve a thin border from the original image (prevents “eaten” edges)
780
+ BORDER_PX = 10
781
+ src = getattr(self, "_orig_for_border", None)
782
+ if src is None or src.shape != out.shape:
783
+ try:
784
+ src = np.asarray(doc.image)
785
+ except Exception:
786
+ src = None
787
+ out = _preserve_border(out, src, BORDER_PX)
788
+
789
+ # 2) Metadata for this step (stored on the document)
790
+ meta = {
791
+ "is_mono": (out.ndim == 2),
792
+ "processing_parameters": {
793
+ **(getattr(doc, "metadata", {}) or {}).get("processing_parameters", {}),
794
+ "AberrationAI": {
795
+ "model_path": model_path,
796
+ "patch_size": int(self.spin_patch.value()),
797
+ "overlap": int(self.spin_overlap.value()),
798
+ "provider": used,
799
+ "border_px": BORDER_PX,
800
+ }
801
+ }
802
+ }
803
+
804
+ # 3) Apply through history-aware API (either path is fine)
805
+ try:
806
+ # Preferred: directly on the document
807
+ if hasattr(doc, "apply_edit"):
808
+ doc.apply_edit(out, meta, step_name="Aberration AI")
809
+ # Or via DocManager (same effect)
810
+ elif hasattr(self.docman, "update_active_document"):
811
+ self.docman.update_active_document(out, metadata=meta, step_name="Aberration AI")
812
+ else:
813
+ # Last-resort fallback (no undo): avoid if possible
814
+ doc.image = out
815
+ try:
816
+ doc.metadata.update(meta)
817
+ doc.changed.emit()
818
+ except Exception:
819
+ pass
820
+ except Exception as e:
821
+ self._log(f"❌ Aberration AI apply failed: {e}")
822
+ QMessageBox.critical(self, "Apply Error", f"Failed to apply result:\n{e}")
823
+ return
824
+
825
+ # 3.5) Register this as last_headless_command for Replay Last Action ← NEW
826
+ try:
827
+ main = self.parent()
828
+ if main is not None:
829
+ auto_gpu = bool(self.chk_auto.isChecked())
830
+ preset = {
831
+ "model": model_path,
832
+ "patch": int(self.spin_patch.value()),
833
+ "overlap": int(self.spin_overlap.value()),
834
+ "border_px": int(BORDER_PX),
835
+ "auto_gpu": auto_gpu,
836
+ }
837
+ if not auto_gpu:
838
+ preset["provider"] = self.cmb_provider.currentText() or "CPUExecutionProvider"
839
+
840
+ payload = {
841
+ "command_id": "aberrationai",
842
+ "preset": preset,
843
+ }
844
+ setattr(main, "_last_headless_command", payload)
845
+
846
+ # optional log
847
+ try:
848
+ if hasattr(main, "_log"):
849
+ prov = preset.get("provider", "auto" if auto_gpu else "CPUExecutionProvider")
850
+ main._log(
851
+ f"[Replay] Registered Aberration AI as last action "
852
+ f"(patch={preset['patch']}, overlap={preset['overlap']}, "
853
+ f"border={preset['border_px']}px, provider={prov})"
854
+ )
855
+ except Exception:
856
+ pass
857
+ except Exception:
858
+ # never break the tool if replay wiring fails
859
+ pass
860
+
861
+ # 4) Refresh the active view
862
+ mw = self.parent()
863
+ sw = getattr(getattr(mw, "mdi", None), "activeSubWindow", lambda: None)()
864
+ if sw and hasattr(sw, "widget"):
865
+ w = sw.widget()
866
+ if hasattr(w, "reload_from_doc"):
867
+ try: w.reload_from_doc()
868
+ except Exception as e:
869
+ import logging
870
+ logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
871
+ elif hasattr(w, "update_view"):
872
+ try: w.update_view()
873
+ except Exception as e:
874
+ import logging
875
+ logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
876
+ elif hasattr(w, "update"):
877
+ w.update()
878
+
879
+ dt = 0.0
880
+ try:
881
+ if self._t_start is not None:
882
+ dt = time.perf_counter() - self._t_start
883
+ except Exception:
884
+ pass
885
+ used = getattr(self._worker, "used_provider", None) or \
886
+ (self.cmb_provider.currentText() if not self.chk_auto.isChecked() else "auto")
887
+ BORDER_PX = 10 # same value used above
888
+ self._log(
889
+ f"✅ Aberration AI applied "
890
+ f"(model={os.path.basename(model_path)}, provider={used}, "
891
+ f"patch={int(self.spin_patch.value())}, overlap={int(self.spin_overlap.value())}, "
892
+ f"border={BORDER_PX}px, time={dt:.2f}s)"
893
+ )
894
+
895
+ self.progress.setValue(100)
896
+ # NEW: close this UI after a successful run
897
+ self.accept() # or self.close()
898
+ return
899
+
900
+ def _on_worker_finished(self):
901
+ # Dialog might have been closed by _on_ok()
902
+ if not self.isVisible():
903
+ return
904
+
905
+ if hasattr(self, "btn_run"):
906
+ try:
907
+ self.btn_run.setEnabled(True)
908
+ except RuntimeError:
909
+ pass
910
+ self._worker = None