setiastrosuitepro 1.6.2.post1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of setiastrosuitepro might be problematic. Click here for more details.

Files changed (367) 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/andromedatry.png +0 -0
  24. setiastro/images/andromedatry_satellited.png +0 -0
  25. setiastro/images/annotated.png +0 -0
  26. setiastro/images/aperture.png +0 -0
  27. setiastro/images/astrosuite.ico +0 -0
  28. setiastro/images/astrosuite.png +0 -0
  29. setiastro/images/astrosuitepro.icns +0 -0
  30. setiastro/images/astrosuitepro.ico +0 -0
  31. setiastro/images/astrosuitepro.png +0 -0
  32. setiastro/images/background.png +0 -0
  33. setiastro/images/background2.png +0 -0
  34. setiastro/images/benchmark.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  37. setiastro/images/blaster.png +0 -0
  38. setiastro/images/blink.png +0 -0
  39. setiastro/images/clahe.png +0 -0
  40. setiastro/images/collage.png +0 -0
  41. setiastro/images/colorwheel.png +0 -0
  42. setiastro/images/contsub.png +0 -0
  43. setiastro/images/convo.png +0 -0
  44. setiastro/images/copyslot.png +0 -0
  45. setiastro/images/cosmic.png +0 -0
  46. setiastro/images/cosmicsat.png +0 -0
  47. setiastro/images/crop1.png +0 -0
  48. setiastro/images/cropicon.png +0 -0
  49. setiastro/images/curves.png +0 -0
  50. setiastro/images/cvs.png +0 -0
  51. setiastro/images/debayer.png +0 -0
  52. setiastro/images/denoise_cnn_custom.png +0 -0
  53. setiastro/images/denoise_cnn_graph.png +0 -0
  54. setiastro/images/disk.png +0 -0
  55. setiastro/images/dse.png +0 -0
  56. setiastro/images/exoicon.png +0 -0
  57. setiastro/images/eye.png +0 -0
  58. setiastro/images/fliphorizontal.png +0 -0
  59. setiastro/images/flipvertical.png +0 -0
  60. setiastro/images/font.png +0 -0
  61. setiastro/images/freqsep.png +0 -0
  62. setiastro/images/functionbundle.png +0 -0
  63. setiastro/images/graxpert.png +0 -0
  64. setiastro/images/green.png +0 -0
  65. setiastro/images/gridicon.png +0 -0
  66. setiastro/images/halo.png +0 -0
  67. setiastro/images/hdr.png +0 -0
  68. setiastro/images/histogram.png +0 -0
  69. setiastro/images/hubble.png +0 -0
  70. setiastro/images/imagecombine.png +0 -0
  71. setiastro/images/invert.png +0 -0
  72. setiastro/images/isophote.png +0 -0
  73. setiastro/images/isophote_demo_figure.png +0 -0
  74. setiastro/images/isophote_demo_image.png +0 -0
  75. setiastro/images/isophote_demo_model.png +0 -0
  76. setiastro/images/isophote_demo_residual.png +0 -0
  77. setiastro/images/jwstpupil.png +0 -0
  78. setiastro/images/linearfit.png +0 -0
  79. setiastro/images/livestacking.png +0 -0
  80. setiastro/images/mask.png +0 -0
  81. setiastro/images/maskapply.png +0 -0
  82. setiastro/images/maskcreate.png +0 -0
  83. setiastro/images/maskremove.png +0 -0
  84. setiastro/images/morpho.png +0 -0
  85. setiastro/images/mosaic.png +0 -0
  86. setiastro/images/multiscale_decomp.png +0 -0
  87. setiastro/images/nbtorgb.png +0 -0
  88. setiastro/images/neutral.png +0 -0
  89. setiastro/images/nuke.png +0 -0
  90. setiastro/images/openfile.png +0 -0
  91. setiastro/images/pedestal.png +0 -0
  92. setiastro/images/pen.png +0 -0
  93. setiastro/images/pixelmath.png +0 -0
  94. setiastro/images/platesolve.png +0 -0
  95. setiastro/images/ppp.png +0 -0
  96. setiastro/images/pro.png +0 -0
  97. setiastro/images/project.png +0 -0
  98. setiastro/images/psf.png +0 -0
  99. setiastro/images/redo.png +0 -0
  100. setiastro/images/redoicon.png +0 -0
  101. setiastro/images/rescale.png +0 -0
  102. setiastro/images/rgbalign.png +0 -0
  103. setiastro/images/rgbcombo.png +0 -0
  104. setiastro/images/rgbextract.png +0 -0
  105. setiastro/images/rotate180.png +0 -0
  106. setiastro/images/rotateclockwise.png +0 -0
  107. setiastro/images/rotatecounterclockwise.png +0 -0
  108. setiastro/images/satellite.png +0 -0
  109. setiastro/images/script.png +0 -0
  110. setiastro/images/selectivecolor.png +0 -0
  111. setiastro/images/simbad.png +0 -0
  112. setiastro/images/slot0.png +0 -0
  113. setiastro/images/slot1.png +0 -0
  114. setiastro/images/slot2.png +0 -0
  115. setiastro/images/slot3.png +0 -0
  116. setiastro/images/slot4.png +0 -0
  117. setiastro/images/slot5.png +0 -0
  118. setiastro/images/slot6.png +0 -0
  119. setiastro/images/slot7.png +0 -0
  120. setiastro/images/slot8.png +0 -0
  121. setiastro/images/slot9.png +0 -0
  122. setiastro/images/spcc.png +0 -0
  123. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  124. setiastro/images/spinner.gif +0 -0
  125. setiastro/images/stacking.png +0 -0
  126. setiastro/images/staradd.png +0 -0
  127. setiastro/images/staralign.png +0 -0
  128. setiastro/images/starnet.png +0 -0
  129. setiastro/images/starregistration.png +0 -0
  130. setiastro/images/starspike.png +0 -0
  131. setiastro/images/starstretch.png +0 -0
  132. setiastro/images/statstretch.png +0 -0
  133. setiastro/images/supernova.png +0 -0
  134. setiastro/images/uhs.png +0 -0
  135. setiastro/images/undoicon.png +0 -0
  136. setiastro/images/upscale.png +0 -0
  137. setiastro/images/viewbundle.png +0 -0
  138. setiastro/images/whitebalance.png +0 -0
  139. setiastro/images/wimi_icon_256x256.png +0 -0
  140. setiastro/images/wimilogo.png +0 -0
  141. setiastro/images/wims.png +0 -0
  142. setiastro/images/wrench_icon.png +0 -0
  143. setiastro/images/xisfliberator.png +0 -0
  144. setiastro/qml/ResourceMonitor.qml +126 -0
  145. setiastro/saspro/__init__.py +20 -0
  146. setiastro/saspro/__main__.py +945 -0
  147. setiastro/saspro/_generated/__init__.py +7 -0
  148. setiastro/saspro/_generated/build_info.py +3 -0
  149. setiastro/saspro/abe.py +1346 -0
  150. setiastro/saspro/abe_preset.py +196 -0
  151. setiastro/saspro/aberration_ai.py +694 -0
  152. setiastro/saspro/aberration_ai_preset.py +224 -0
  153. setiastro/saspro/accel_installer.py +218 -0
  154. setiastro/saspro/accel_workers.py +30 -0
  155. setiastro/saspro/add_stars.py +624 -0
  156. setiastro/saspro/astrobin_exporter.py +1010 -0
  157. setiastro/saspro/astrospike.py +153 -0
  158. setiastro/saspro/astrospike_python.py +1841 -0
  159. setiastro/saspro/autostretch.py +198 -0
  160. setiastro/saspro/backgroundneutral.py +602 -0
  161. setiastro/saspro/batch_convert.py +328 -0
  162. setiastro/saspro/batch_renamer.py +522 -0
  163. setiastro/saspro/blemish_blaster.py +491 -0
  164. setiastro/saspro/blink_comparator_pro.py +2926 -0
  165. setiastro/saspro/bundles.py +61 -0
  166. setiastro/saspro/bundles_dock.py +114 -0
  167. setiastro/saspro/cheat_sheet.py +213 -0
  168. setiastro/saspro/clahe.py +368 -0
  169. setiastro/saspro/comet_stacking.py +1442 -0
  170. setiastro/saspro/common_tr.py +107 -0
  171. setiastro/saspro/config.py +38 -0
  172. setiastro/saspro/config_bootstrap.py +40 -0
  173. setiastro/saspro/config_manager.py +316 -0
  174. setiastro/saspro/continuum_subtract.py +1617 -0
  175. setiastro/saspro/convo.py +1400 -0
  176. setiastro/saspro/convo_preset.py +414 -0
  177. setiastro/saspro/copyastro.py +190 -0
  178. setiastro/saspro/cosmicclarity.py +1589 -0
  179. setiastro/saspro/cosmicclarity_preset.py +407 -0
  180. setiastro/saspro/crop_dialog_pro.py +973 -0
  181. setiastro/saspro/crop_preset.py +189 -0
  182. setiastro/saspro/curve_editor_pro.py +2562 -0
  183. setiastro/saspro/curves_preset.py +375 -0
  184. setiastro/saspro/debayer.py +673 -0
  185. setiastro/saspro/debug_utils.py +29 -0
  186. setiastro/saspro/dnd_mime.py +35 -0
  187. setiastro/saspro/doc_manager.py +2664 -0
  188. setiastro/saspro/exoplanet_detector.py +2166 -0
  189. setiastro/saspro/file_utils.py +284 -0
  190. setiastro/saspro/fitsmodifier.py +748 -0
  191. setiastro/saspro/fix_bom.py +32 -0
  192. setiastro/saspro/free_torch_memory.py +48 -0
  193. setiastro/saspro/frequency_separation.py +1349 -0
  194. setiastro/saspro/function_bundle.py +1596 -0
  195. setiastro/saspro/generate_translations.py +3092 -0
  196. setiastro/saspro/ghs_dialog_pro.py +663 -0
  197. setiastro/saspro/ghs_preset.py +284 -0
  198. setiastro/saspro/graxpert.py +637 -0
  199. setiastro/saspro/graxpert_preset.py +287 -0
  200. setiastro/saspro/gui/__init__.py +0 -0
  201. setiastro/saspro/gui/main_window.py +8810 -0
  202. setiastro/saspro/gui/mixins/__init__.py +33 -0
  203. setiastro/saspro/gui/mixins/dock_mixin.py +362 -0
  204. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  205. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  206. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  207. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  208. setiastro/saspro/gui/mixins/menu_mixin.py +389 -0
  209. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  210. setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
  211. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  212. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  213. setiastro/saspro/gui/statistics_dialog.py +47 -0
  214. setiastro/saspro/halobgon.py +488 -0
  215. setiastro/saspro/header_viewer.py +448 -0
  216. setiastro/saspro/headless_utils.py +88 -0
  217. setiastro/saspro/histogram.py +756 -0
  218. setiastro/saspro/history_explorer.py +941 -0
  219. setiastro/saspro/i18n.py +168 -0
  220. setiastro/saspro/image_combine.py +417 -0
  221. setiastro/saspro/image_peeker_pro.py +1604 -0
  222. setiastro/saspro/imageops/__init__.py +37 -0
  223. setiastro/saspro/imageops/mdi_snap.py +292 -0
  224. setiastro/saspro/imageops/scnr.py +36 -0
  225. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  226. setiastro/saspro/imageops/stretch.py +236 -0
  227. setiastro/saspro/isophote.py +1182 -0
  228. setiastro/saspro/layers.py +208 -0
  229. setiastro/saspro/layers_dock.py +714 -0
  230. setiastro/saspro/lazy_imports.py +193 -0
  231. setiastro/saspro/legacy/__init__.py +2 -0
  232. setiastro/saspro/legacy/image_manager.py +2226 -0
  233. setiastro/saspro/legacy/numba_utils.py +3676 -0
  234. setiastro/saspro/legacy/xisf.py +1071 -0
  235. setiastro/saspro/linear_fit.py +537 -0
  236. setiastro/saspro/live_stacking.py +1841 -0
  237. setiastro/saspro/log_bus.py +5 -0
  238. setiastro/saspro/logging_config.py +460 -0
  239. setiastro/saspro/luminancerecombine.py +309 -0
  240. setiastro/saspro/main_helpers.py +201 -0
  241. setiastro/saspro/mask_creation.py +931 -0
  242. setiastro/saspro/masks_core.py +56 -0
  243. setiastro/saspro/mdi_widgets.py +353 -0
  244. setiastro/saspro/memory_utils.py +666 -0
  245. setiastro/saspro/metadata_patcher.py +75 -0
  246. setiastro/saspro/mfdeconv.py +3831 -0
  247. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  248. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  249. setiastro/saspro/mfdeconvsport.py +2382 -0
  250. setiastro/saspro/minorbodycatalog.py +567 -0
  251. setiastro/saspro/morphology.py +407 -0
  252. setiastro/saspro/multiscale_decomp.py +1293 -0
  253. setiastro/saspro/nbtorgb_stars.py +541 -0
  254. setiastro/saspro/numba_utils.py +3145 -0
  255. setiastro/saspro/numba_warmup.py +141 -0
  256. setiastro/saspro/ops/__init__.py +9 -0
  257. setiastro/saspro/ops/command_help_dialog.py +623 -0
  258. setiastro/saspro/ops/command_runner.py +217 -0
  259. setiastro/saspro/ops/commands.py +1594 -0
  260. setiastro/saspro/ops/script_editor.py +1102 -0
  261. setiastro/saspro/ops/scripts.py +1473 -0
  262. setiastro/saspro/ops/settings.py +637 -0
  263. setiastro/saspro/parallel_utils.py +554 -0
  264. setiastro/saspro/pedestal.py +121 -0
  265. setiastro/saspro/perfect_palette_picker.py +1071 -0
  266. setiastro/saspro/pipeline.py +110 -0
  267. setiastro/saspro/pixelmath.py +1604 -0
  268. setiastro/saspro/plate_solver.py +2445 -0
  269. setiastro/saspro/project_io.py +797 -0
  270. setiastro/saspro/psf_utils.py +136 -0
  271. setiastro/saspro/psf_viewer.py +549 -0
  272. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  273. setiastro/saspro/remove_green.py +331 -0
  274. setiastro/saspro/remove_stars.py +1599 -0
  275. setiastro/saspro/remove_stars_preset.py +404 -0
  276. setiastro/saspro/resources.py +501 -0
  277. setiastro/saspro/rgb_combination.py +208 -0
  278. setiastro/saspro/rgb_extract.py +19 -0
  279. setiastro/saspro/rgbalign.py +723 -0
  280. setiastro/saspro/runtime_imports.py +7 -0
  281. setiastro/saspro/runtime_torch.py +754 -0
  282. setiastro/saspro/save_options.py +73 -0
  283. setiastro/saspro/selective_color.py +1552 -0
  284. setiastro/saspro/sfcc.py +1472 -0
  285. setiastro/saspro/shortcuts.py +3043 -0
  286. setiastro/saspro/signature_insert.py +1102 -0
  287. setiastro/saspro/stacking_suite.py +18470 -0
  288. setiastro/saspro/star_alignment.py +7435 -0
  289. setiastro/saspro/star_alignment_preset.py +329 -0
  290. setiastro/saspro/star_metrics.py +49 -0
  291. setiastro/saspro/star_spikes.py +765 -0
  292. setiastro/saspro/star_stretch.py +507 -0
  293. setiastro/saspro/stat_stretch.py +538 -0
  294. setiastro/saspro/status_log_dock.py +78 -0
  295. setiastro/saspro/subwindow.py +3328 -0
  296. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  297. setiastro/saspro/swap_manager.py +99 -0
  298. setiastro/saspro/torch_backend.py +89 -0
  299. setiastro/saspro/torch_rejection.py +434 -0
  300. setiastro/saspro/translations/all_source_strings.json +3654 -0
  301. setiastro/saspro/translations/ar_translations.py +3865 -0
  302. setiastro/saspro/translations/de_translations.py +3749 -0
  303. setiastro/saspro/translations/es_translations.py +3939 -0
  304. setiastro/saspro/translations/fr_translations.py +3858 -0
  305. setiastro/saspro/translations/hi_translations.py +3571 -0
  306. setiastro/saspro/translations/integrate_translations.py +270 -0
  307. setiastro/saspro/translations/it_translations.py +3678 -0
  308. setiastro/saspro/translations/ja_translations.py +3601 -0
  309. setiastro/saspro/translations/pt_translations.py +3869 -0
  310. setiastro/saspro/translations/ru_translations.py +2848 -0
  311. setiastro/saspro/translations/saspro_ar.qm +0 -0
  312. setiastro/saspro/translations/saspro_ar.ts +255 -0
  313. setiastro/saspro/translations/saspro_de.qm +0 -0
  314. setiastro/saspro/translations/saspro_de.ts +253 -0
  315. setiastro/saspro/translations/saspro_es.qm +0 -0
  316. setiastro/saspro/translations/saspro_es.ts +12520 -0
  317. setiastro/saspro/translations/saspro_fr.qm +0 -0
  318. setiastro/saspro/translations/saspro_fr.ts +12514 -0
  319. setiastro/saspro/translations/saspro_hi.qm +0 -0
  320. setiastro/saspro/translations/saspro_hi.ts +257 -0
  321. setiastro/saspro/translations/saspro_it.qm +0 -0
  322. setiastro/saspro/translations/saspro_it.ts +12520 -0
  323. setiastro/saspro/translations/saspro_ja.qm +0 -0
  324. setiastro/saspro/translations/saspro_ja.ts +257 -0
  325. setiastro/saspro/translations/saspro_pt.qm +0 -0
  326. setiastro/saspro/translations/saspro_pt.ts +257 -0
  327. setiastro/saspro/translations/saspro_ru.qm +0 -0
  328. setiastro/saspro/translations/saspro_ru.ts +237 -0
  329. setiastro/saspro/translations/saspro_sw.qm +0 -0
  330. setiastro/saspro/translations/saspro_sw.ts +257 -0
  331. setiastro/saspro/translations/saspro_uk.qm +0 -0
  332. setiastro/saspro/translations/saspro_uk.ts +10771 -0
  333. setiastro/saspro/translations/saspro_zh.qm +0 -0
  334. setiastro/saspro/translations/saspro_zh.ts +12520 -0
  335. setiastro/saspro/translations/sw_translations.py +3671 -0
  336. setiastro/saspro/translations/uk_translations.py +3700 -0
  337. setiastro/saspro/translations/zh_translations.py +3675 -0
  338. setiastro/saspro/versioning.py +77 -0
  339. setiastro/saspro/view_bundle.py +1558 -0
  340. setiastro/saspro/wavescale_hdr.py +645 -0
  341. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  342. setiastro/saspro/wavescalede.py +680 -0
  343. setiastro/saspro/wavescalede_preset.py +230 -0
  344. setiastro/saspro/wcs_update.py +374 -0
  345. setiastro/saspro/whitebalance.py +492 -0
  346. setiastro/saspro/widgets/__init__.py +48 -0
  347. setiastro/saspro/widgets/common_utilities.py +306 -0
  348. setiastro/saspro/widgets/graphics_views.py +122 -0
  349. setiastro/saspro/widgets/image_utils.py +518 -0
  350. setiastro/saspro/widgets/minigame/game.js +986 -0
  351. setiastro/saspro/widgets/minigame/index.html +53 -0
  352. setiastro/saspro/widgets/minigame/style.css +241 -0
  353. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  354. setiastro/saspro/widgets/resource_monitor.py +237 -0
  355. setiastro/saspro/widgets/spinboxes.py +275 -0
  356. setiastro/saspro/widgets/themed_buttons.py +13 -0
  357. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  358. setiastro/saspro/wimi.py +7996 -0
  359. setiastro/saspro/wims.py +578 -0
  360. setiastro/saspro/window_shelf.py +185 -0
  361. setiastro/saspro/xisf.py +1123 -0
  362. setiastrosuitepro-1.6.2.post1.dist-info/METADATA +278 -0
  363. setiastrosuitepro-1.6.2.post1.dist-info/RECORD +367 -0
  364. setiastrosuitepro-1.6.2.post1.dist-info/WHEEL +4 -0
  365. setiastrosuitepro-1.6.2.post1.dist-info/entry_points.txt +6 -0
  366. setiastrosuitepro-1.6.2.post1.dist-info/licenses/LICENSE +674 -0
  367. setiastrosuitepro-1.6.2.post1.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,637 @@
1
+ # pro/graxpert.py
2
+ from __future__ import annotations
3
+ import os
4
+ import platform
5
+ import shutil
6
+ import tempfile
7
+ import stat
8
+ import glob
9
+ import subprocess
10
+ import numpy as np
11
+
12
+ from PyQt6.QtCore import QThread, pyqtSignal, Qt
13
+ from PyQt6.QtWidgets import (
14
+ QDialog, QVBoxLayout, QTextEdit, QPushButton, QFileDialog,
15
+ QMessageBox, QInputDialog, QFormLayout, QDialogButtonBox, QDoubleSpinBox,
16
+ QRadioButton, QLabel, QComboBox, QCheckBox, QWidget
17
+ )
18
+ from setiastro.saspro.config import Config
19
+
20
+ # Prefer the exact loader you used in SASv2
21
+ try:
22
+ # adjust this import path if your loader lives elsewhere
23
+ from setiastro.saspro.legacy.image_manager import load_image as _legacy_load_image
24
+ except Exception:
25
+ _legacy_load_image = None
26
+
27
+
28
+ class GraXpertOperationDialog(QDialog):
29
+ """Choose operation + parameter (smoothing or strength) + (optional) denoise model."""
30
+ def __init__(self, parent=None):
31
+ super().__init__(parent)
32
+
33
+ self.setWindowTitle("GraXpert")
34
+ self.setWindowFlag(Qt.WindowType.Window, True)
35
+ self.setWindowModality(Qt.WindowModality.NonModal)
36
+ self.setModal(False)
37
+ root = QVBoxLayout(self)
38
+
39
+ # radios
40
+ self.rb_bg = QRadioButton("Remove gradient")
41
+ self.rb_dn = QRadioButton("Denoise")
42
+ self.rb_bg.setChecked(True)
43
+
44
+ # param widgets
45
+ self.spin = QDoubleSpinBox()
46
+ self.spin.setRange(0.0, 1.0)
47
+ self.spin.setDecimals(2)
48
+ self.spin.setSingleStep(0.01)
49
+ self.spin.setValue(0.10) # default for smoothing
50
+
51
+ # dynamic label
52
+ self.param_label = QLabel("Smoothing (0–1):")
53
+
54
+ # denoise model (optional)
55
+ self.model_label = QLabel("Denoise model:")
56
+ self.model_combo = QComboBox()
57
+ # Index 0 = auto/latest (empty payload → omit flag)
58
+ self.model_combo.addItem("Latest (auto)", "") # omit -ai_version
59
+ for v in ["3.0.2", "3.0.1", "3.0.0", "2.0.0", "1.1.0", "1.0.0"]:
60
+ self.model_combo.addItem(v, v)
61
+
62
+ # GPU toggle (persists via QSettings if available)
63
+ self.cb_gpu = QCheckBox("Use GPU acceleration")
64
+ use_gpu_default = True
65
+ try:
66
+ settings = getattr(parent, "settings", None)
67
+ if settings is not None:
68
+ use_gpu_default = settings.value("graxpert/use_gpu", True, type=bool)
69
+ except Exception:
70
+ pass
71
+ self.cb_gpu.setChecked(bool(use_gpu_default))
72
+
73
+
74
+ # layout
75
+ form = QFormLayout()
76
+ form.addRow(self.rb_bg)
77
+ form.addRow(self.rb_dn)
78
+ form.addRow(self.param_label, self.spin)
79
+ form.addRow(self.model_label, self.model_combo)
80
+ form.addRow(self.cb_gpu)
81
+ root.addLayout(form)
82
+
83
+ # switch label/defaults and enable/disable model picker
84
+ def _to_bg():
85
+ self.param_label.setText("Smoothing (0–1):")
86
+ # If param was the denoise default, flip back to smoothing default
87
+ self.spin.setValue(0.10 if abs(self.spin.value() - 0.50) < 1e-6 else self.spin.value())
88
+ self.model_label.setEnabled(False)
89
+ self.model_combo.setEnabled(False)
90
+
91
+ def _to_dn():
92
+ self.param_label.setText("Strength (0–1):")
93
+ # If param was the smoothing default, flip to denoise default
94
+ self.spin.setValue(0.50 if abs(self.spin.value() - 0.10) < 1e-6 else self.spin.value())
95
+ self.model_label.setEnabled(True)
96
+ self.model_combo.setEnabled(True)
97
+
98
+ self.rb_bg.toggled.connect(lambda checked: _to_bg() if checked else None)
99
+ self.rb_dn.toggled.connect(lambda checked: _to_dn() if checked else None)
100
+
101
+ # initialize state
102
+ _to_bg()
103
+
104
+ btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel)
105
+ btns.accepted.connect(self.accept)
106
+ btns.rejected.connect(self.reject)
107
+ root.addWidget(btns)
108
+
109
+ def result(self):
110
+ op = "background" if self.rb_bg.isChecked() else "denoise"
111
+ val = float(self.spin.value())
112
+ ai_version = self.model_combo.currentData() if not self.rb_bg.isChecked() else ""
113
+ use_gpu = self.cb_gpu.isChecked()
114
+ return op, val, (ai_version or None), use_gpu
115
+
116
+ def _build_graxpert_cmd(
117
+ exe: str,
118
+ operation: str,
119
+ input_path: str,
120
+ *,
121
+ smoothing: float | None = None,
122
+ strength: float | None = None,
123
+ ai_version: str | None = None,
124
+ gpu: bool = True,
125
+ batch_size: int | None = None
126
+ ) -> list[str]:
127
+ op = "denoising" if operation == "denoise" else "background-extraction"
128
+ cmd = [exe, "-cmd", op, input_path, "-cli", "-gpu", "true" if gpu else "false"]
129
+ if op == "denoising":
130
+ if strength is not None:
131
+ cmd += ["-strength", f"{strength:.2f}"]
132
+ if batch_size is not None:
133
+ cmd += ["-batch_size", str(int(batch_size))]
134
+ # Only include if user chose a specific model
135
+ if ai_version:
136
+ cmd += ["-ai_version", ai_version]
137
+ else:
138
+ if smoothing is not None:
139
+ cmd += ["-smoothing", f"{smoothing:.2f}"]
140
+ return cmd
141
+
142
+ # ---------- Public entry point (call this from your main window) ----------
143
+ def remove_gradient_with_graxpert(main_window, target_doc=None):
144
+ """
145
+ Exactly mirror SASv2 flow:
146
+ - write input_image.tif
147
+ - run GraXpert
148
+ - read input_image_GraXpert.{fits|tif|tiff|png} using legacy loader
149
+ - apply to target document
150
+ """
151
+ if getattr(main_window, "_graxpert_headless_running", False):
152
+ return
153
+ if getattr(main_window, "_graxpert_guard", False): # cool-down guard
154
+ return
155
+
156
+ # 1) pick the document: explicit > fallback
157
+ doc = target_doc
158
+
159
+ if doc is None:
160
+ # Backwards compatibility: fall back to _active_doc
161
+ doc = getattr(main_window, "_active_doc", None)
162
+ if callable(doc):
163
+ doc = doc()
164
+
165
+ if doc is None and hasattr(main_window, "mdi"):
166
+ # Extra fallback: resolve from active subwindow if possible
167
+ try:
168
+ sw = main_window.mdi.activeSubWindow()
169
+ if sw is not None:
170
+ view = sw.widget()
171
+ doc = getattr(view, "document", None)
172
+ except Exception:
173
+ pass
174
+
175
+ if doc is None or getattr(doc, "image", None) is None:
176
+ QMessageBox.warning(
177
+ main_window,
178
+ "No Image",
179
+ "Please load an image before removing the gradient."
180
+ )
181
+ return
182
+
183
+ # 2) smoothing/denoise prompt
184
+ op_dlg = GraXpertOperationDialog(main_window)
185
+ if op_dlg.exec() != QDialog.DialogCode.Accepted:
186
+ return
187
+ operation, param, ai_version, use_gpu = op_dlg.result()
188
+
189
+ # 3) resolve GraXpert executable
190
+ exe = _resolve_graxpert_exec(main_window)
191
+ if not exe:
192
+ return
193
+
194
+ # Persist the checkbox choice for next time
195
+ try:
196
+ if hasattr(main_window, "settings"):
197
+ main_window.settings.setValue("graxpert/use_gpu", bool(use_gpu))
198
+ except Exception:
199
+ pass
200
+
201
+ # 🔁 NEW: record this as a replayable headless-style command
202
+ try:
203
+ remember = getattr(main_window, "remember_last_headless_command", None)
204
+ if remember is None:
205
+ remember = getattr(main_window, "_remember_last_headless_command", None)
206
+
207
+ if callable(remember):
208
+ preset = {
209
+ "op": operation, # "background" or "denoise"
210
+ "gpu": bool(use_gpu),
211
+ }
212
+ if operation == "background":
213
+ preset["smoothing"] = float(param)
214
+ desc = "GraXpert Gradient Removal"
215
+ else:
216
+ preset["strength"] = float(param)
217
+ if ai_version:
218
+ preset["ai_version"] = ai_version
219
+ desc = "GraXpert Denoise"
220
+
221
+ remember("graxpert", preset, description=desc)
222
+
223
+ # Optional log entry, if you want:
224
+ if hasattr(main_window, "_log"):
225
+ try:
226
+ main_window._log(
227
+ f"[Replay] GraXpert preset stored from dialog: "
228
+ f"op={operation}, keys={list(preset.keys())}"
229
+ )
230
+ except Exception:
231
+ pass
232
+ except Exception:
233
+ # Don't let replay bookkeeping break GraXpert itself
234
+ pass
235
+
236
+ # 4) write input to a temp working dir but KEEP THE SAME BASENAMES as v2
237
+ workdir = tempfile.mkdtemp(prefix="saspro_graxpert_")
238
+ input_basename = "input_image"
239
+ input_path = os.path.join(workdir, f"{input_basename}.tif")
240
+ try:
241
+ _write_tiff_float32(doc.image, input_path)
242
+ except Exception as e:
243
+ QMessageBox.critical(main_window, "GraXpert", f"Failed to write temporary input:\n{e}")
244
+ shutil.rmtree(workdir, ignore_errors=True)
245
+ return
246
+
247
+ # 5) build the exact v2 command (now with optional ai_version for denoise)
248
+ command = _build_graxpert_cmd(
249
+ exe,
250
+ operation,
251
+ input_path,
252
+ smoothing=param if operation == "background" else None,
253
+ strength=param if operation == "denoise" else None,
254
+ ai_version=ai_version if operation == "denoise" else None,
255
+ gpu=bool(use_gpu),
256
+ batch_size=(4 if use_gpu else 1)
257
+ )
258
+
259
+ # Label + metadata for history/undo
260
+ op_label = "GraXpert Denoise" if operation == "denoise" else "GraXpert Gradient Removal"
261
+ meta_extras = {
262
+ "graxpert_operation": operation, # "denoise" | "background"
263
+ "graxpert_param": float(param),
264
+ "graxpert_ai_version": (ai_version or "latest") if operation == "denoise" else None,
265
+ "graxpert_gpu": bool(use_gpu),
266
+ }
267
+
268
+ # 6) run and wait with a small log dialog
269
+ output_basename = f"{input_basename}_GraXpert"
270
+ _run_graxpert_command(
271
+ main_window,
272
+ command,
273
+ output_basename,
274
+ workdir,
275
+ target_doc=doc,
276
+ op_label=op_label,
277
+ meta_extras=meta_extras,
278
+ )
279
+
280
+
281
+ # ---------- helpers ----------
282
+ def _resolve_graxpert_exec(main_window) -> str | None:
283
+ # prefer QSettings if available (all OS)
284
+ path = None
285
+ if hasattr(main_window, "settings"):
286
+ try:
287
+ path = main_window.settings.value("paths/graxpert", type=str)
288
+ except Exception:
289
+ path = None
290
+ if path and os.path.exists(path):
291
+ _ensure_exec_bit(path)
292
+ return path
293
+
294
+ sysname = platform.system()
295
+ default = Config.get_graxpert_default_path()
296
+
297
+ if sysname == "Windows":
298
+ # rely on PATH (like v2) or default
299
+ return default if default else "GraXpert.exe"
300
+
301
+ if sysname == "Darwin":
302
+ if default and os.path.exists(default):
303
+ _ensure_exec_bit(default)
304
+ if hasattr(main_window, "settings"):
305
+ main_window.settings.setValue("paths/graxpert", default)
306
+ return default
307
+ return _pick_graxpert_path_and_store(main_window)
308
+
309
+ if sysname == "Linux":
310
+ # in v2 you asked user and saved; do the same
311
+ return _pick_graxpert_path_and_store(main_window)
312
+
313
+ QMessageBox.critical(main_window, "GraXpert", f"Unsupported operating system: {sysname}")
314
+ return None
315
+
316
+ def _pick_graxpert_path_and_store(main_window) -> str | None:
317
+ path, _ = QFileDialog.getOpenFileName(main_window, "Select GraXpert Executable")
318
+ if not path:
319
+ QMessageBox.warning(main_window, "Cancelled", "GraXpert path selection was cancelled.")
320
+ return None
321
+ try:
322
+ _ensure_exec_bit(path)
323
+ except Exception as e:
324
+ QMessageBox.critical(main_window, "GraXpert", f"Failed to set execute permissions:\n{e}")
325
+ return None
326
+ if hasattr(main_window, "settings"):
327
+ main_window.settings.setValue("paths/graxpert", path)
328
+ return path
329
+
330
+
331
+ def _ensure_exec_bit(path: str) -> None:
332
+ if platform.system() == "Windows":
333
+ return
334
+ try:
335
+ st = os.stat(path)
336
+ os.chmod(path, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
337
+ except Exception:
338
+ pass
339
+
340
+
341
+ def _write_tiff_float32(image, path: str, *, clip01: bool = True):
342
+ """
343
+ Always write a 32-bit floating-point TIFF for GraXpert.
344
+ - Mono stays 2D; RGB stays HxWx3.
345
+ - Values are clipped to [0,1] by default to avoid weird HDR ranges.
346
+ """
347
+ import numpy as np
348
+
349
+ arr = np.asarray(image)
350
+ if arr.ndim == 3 and arr.shape[2] == 1:
351
+ arr = arr[..., 0]
352
+
353
+ # Convert to float32 in [0,1]
354
+ if np.issubdtype(arr.dtype, np.floating):
355
+ a32 = arr.astype(np.float32, copy=False)
356
+ if clip01:
357
+ a32 = np.clip(a32, 0.0, 1.0)
358
+ elif np.issubdtype(arr.dtype, np.integer):
359
+ # Scale integers to [0,1] float32
360
+ maxv = np.float32(np.iinfo(arr.dtype).max)
361
+ a32 = (arr.astype(np.float32) / maxv)
362
+ else:
363
+ a32 = arr.astype(np.float32)
364
+
365
+ if clip01:
366
+ a32 = np.clip(a32, 0.0, 1.0)
367
+
368
+ # Prefer tifffile to guarantee float32 TIFFs
369
+ try:
370
+ import tifffile as tiff
371
+ # Write a plain, contiguous, uncompressed float32 TIFF
372
+ # (GraXpert doesn't need ImageJ tags; photometric=minisblack is fine)
373
+ tiff.imwrite(
374
+ path,
375
+ a32,
376
+ dtype=np.float32,
377
+ photometric='minisblack' if a32.ndim == 2 else None,
378
+ planarconfig='contig',
379
+ compression=None,
380
+ imagej=False,
381
+ )
382
+ return
383
+ except Exception as e1:
384
+ pass
385
+
386
+ # Fallback: imageio (uses tifffile under the hood in many installs)
387
+ try:
388
+ import imageio.v3 as iio
389
+ iio.imwrite(path, a32.astype(np.float32))
390
+ return
391
+ except Exception as e2:
392
+ raise RuntimeError(
393
+ "Could not write 32-bit TIFF for GraXpert. "
394
+ "Please install 'tifffile' or 'imageio'.\n"
395
+ f"tifffile error: {e1}\nimageio error: {e2}"
396
+ )
397
+
398
+
399
+
400
+ # ---------- runner + dialog ----------
401
+ class _GraXpertThread(QThread):
402
+ stdout_signal = pyqtSignal(str)
403
+ finished_signal = pyqtSignal(int)
404
+
405
+ def __init__(self, command: list[str], cwd: str | None = None, parent=None):
406
+ super().__init__(parent)
407
+ self.command = command
408
+ self.cwd = cwd
409
+
410
+ def run(self):
411
+ env = os.environ.copy()
412
+ for k in ("PYTHONHOME", "PYTHONPATH", "DYLD_LIBRARY_PATH",
413
+ "DYLD_FALLBACK_LIBRARY_PATH", "PYTHONEXECUTABLE"):
414
+ env.pop(k, None)
415
+ try:
416
+ p = subprocess.Popen(
417
+ self.command,
418
+ cwd=self.cwd,
419
+ stdout=subprocess.PIPE,
420
+ stderr=subprocess.STDOUT, # merge; avoids ResourceWarning + deadlocks
421
+ text=True,
422
+ universal_newlines=True,
423
+ env=env,
424
+ start_new_session=True
425
+ )
426
+ for line in iter(p.stdout.readline, ""):
427
+ if not line:
428
+ break
429
+ self.stdout_signal.emit(line.rstrip())
430
+ try:
431
+ p.stdout.close()
432
+ except Exception:
433
+ pass
434
+ rc = p.wait()
435
+ except Exception as e:
436
+ self.stdout_signal.emit(str(e))
437
+ rc = -1
438
+ self.finished_signal.emit(rc)
439
+
440
+
441
+ def _run_graxpert_command(parent, command: list[str], output_basename: str,
442
+ working_dir: str, target_doc,
443
+ op_label: str | None = None,
444
+ meta_extras: dict | None = None):
445
+ dlg = QDialog(parent)
446
+ dlg.setWindowTitle("GraXpert Progress")
447
+ dlg.setMinimumSize(600, 420)
448
+ lay = QVBoxLayout(dlg)
449
+ log = QTextEdit(readOnly=True)
450
+ lay.addWidget(log)
451
+ btn_cancel = QPushButton("Cancel")
452
+ lay.addWidget(btn_cancel)
453
+
454
+ thr = _GraXpertThread(command, cwd=working_dir)
455
+ thr.stdout_signal.connect(lambda s: log.append(s))
456
+ thr.finished_signal.connect(
457
+ lambda code: _on_graxpert_finished(
458
+ parent,
459
+ code,
460
+ output_basename,
461
+ working_dir,
462
+ target_doc,
463
+ dlg,
464
+ op_label,
465
+ meta_extras,
466
+ )
467
+ )
468
+ btn_cancel.clicked.connect(thr.terminate)
469
+
470
+ thr.start()
471
+ dlg.exec()
472
+
473
+
474
+
475
+ # ---------- finish: import EXACT base like v2, via legacy loader ----------
476
+ def _persist_output_file(src_path: str) -> str | None:
477
+ """Optional: move/copy GraXpert output to an app cache we control."""
478
+ try:
479
+ from PyQt6.QtCore import QStandardPaths
480
+ cache_root = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.CacheLocation)
481
+ except Exception:
482
+ cache_root = None
483
+ try:
484
+ base = os.path.join(cache_root or os.path.expanduser("~/.saspro_cache"), "graxpert")
485
+ os.makedirs(base, exist_ok=True)
486
+ dst = os.path.join(base, os.path.basename(src_path))
487
+ # prefer move (cheaper); fall back to copy if cross-device issues
488
+ try:
489
+ shutil.move(src_path, dst)
490
+ except Exception:
491
+ shutil.copy2(src_path, dst)
492
+ return dst
493
+ except Exception:
494
+ return None
495
+
496
+
497
+ def _on_graxpert_finished(parent,
498
+ return_code: int,
499
+ output_basename: str,
500
+ working_dir: str,
501
+ target_doc,
502
+ dlg,
503
+ op_label: str | None = None,
504
+ meta_extras: dict | None = None):
505
+ try:
506
+ dlg.close()
507
+ except Exception:
508
+ pass
509
+
510
+ if return_code != 0:
511
+ QMessageBox.critical(parent, "GraXpert", "GraXpert process failed.")
512
+ shutil.rmtree(working_dir, ignore_errors=True)
513
+ return
514
+
515
+ # 1) find output file in the temp working dir
516
+ output_file = _pick_exact_output(working_dir, output_basename)
517
+ if not output_file:
518
+ QMessageBox.critical(parent, "GraXpert", "GraXpert output file not found.")
519
+ shutil.rmtree(working_dir, ignore_errors=True)
520
+ return
521
+
522
+ # 2) read pixels (we *do not* want its header to replace ours)
523
+ arr, header = None, None
524
+ if _legacy_load_image is not None:
525
+ try:
526
+ out = _legacy_load_image(output_file, return_metadata=True)
527
+ if out and len(out) == 5:
528
+ a, h, bit_depth, is_mono, out_meta = out
529
+ else:
530
+ a, h, bit_depth, is_mono = out
531
+ out_meta = {}
532
+ arr, header = a, h
533
+ except Exception:
534
+ arr = None
535
+ header = None
536
+ bit_depth = "32-bit floating point"
537
+ is_mono = None
538
+ out_meta = {}
539
+ else:
540
+ out_meta = {}
541
+ bit_depth = "32-bit floating point"
542
+ is_mono = None
543
+ # Decide how it appears in history/undo
544
+ step_label = op_label or "GraXpert Gradient Removal"
545
+
546
+ # 3) base metadata: START FROM EXISTING DOC METADATA
547
+ base_meta = dict(getattr(target_doc, "metadata", {}) or {})
548
+
549
+ # Keep original_header / wcs_header from the doc.
550
+ # If you want to keep GraXpert's header for debugging, store separately:
551
+ from astropy.io import fits as _fits_mod
552
+ if header is not None and isinstance(header, _fits_mod.Header):
553
+ base_meta.setdefault("graxpert_header", header)
554
+
555
+ # Basic fields we do want to update
556
+ base_meta["step_name"] = step_label
557
+ base_meta["description"] = step_label
558
+ base_meta["bit_depth"] = "32-bit floating point"
559
+ if is_mono is not None:
560
+ base_meta["is_mono"] = bool(is_mono)
561
+
562
+ # Copy over any interesting fields from GraXpert's own metadata that are SAFE
563
+ # but do NOT overwrite original_header / wcs_header.
564
+ for k, v in (out_meta or {}).items():
565
+ if k in ("original_header", "fits_header", "wcs_header"):
566
+ continue
567
+ base_meta.setdefault(k, v)
568
+
569
+ # Operation-specific extras
570
+ if meta_extras:
571
+ # these are non-header fields like graxpert_operation, etc.
572
+ base_meta.update(meta_extras)
573
+
574
+ # 4) apply to the target doc
575
+ try:
576
+ target_doc.apply_edit(
577
+ arr.astype(np.float32, copy=False),
578
+ metadata=base_meta,
579
+ step_name=step_label,
580
+ )
581
+ except Exception as e:
582
+ QMessageBox.critical(parent, "GraXpert", f"Failed to apply result:\n{e}")
583
+ finally:
584
+ shutil.rmtree(working_dir, ignore_errors=True)
585
+
586
+
587
+ def _pick_exact_output(folder: str, base: str) -> str | None:
588
+ # exact filenames only, like v2 did
589
+ exts = ("fits", "tif", "tiff", "png")
590
+ for ext in exts:
591
+ p = os.path.join(folder, f"{base}.{ext}")
592
+ if os.path.exists(p):
593
+ return p
594
+ # also try case-variants just in case
595
+ for q in glob.glob(os.path.join(folder, f"{base}.*")):
596
+ if q.lower().endswith("." + ext):
597
+ return q
598
+ return None
599
+
600
+
601
+ def _fallback_read_float01(path: str) -> np.ndarray | None:
602
+ """Basic loader: return float32 in [0,1], mono or RGB, without being too clever."""
603
+ try:
604
+ import imageio.v3 as iio
605
+ arr = iio.imread(path)
606
+ except Exception:
607
+ try:
608
+ import tifffile as tiff
609
+ arr = tiff.imread(path)
610
+ except Exception:
611
+ try:
612
+ from astropy.io import fits
613
+ with fits.open(path, memmap=False) as hdul:
614
+ arr = hdul[0].data
615
+ except Exception:
616
+ try:
617
+ import cv2
618
+ arr = cv2.imread(path, cv2.IMREAD_UNCHANGED)
619
+ if arr is not None and arr.ndim == 3:
620
+ arr = arr[..., ::-1] # BGR->RGB
621
+ except Exception:
622
+ arr = None
623
+ if arr is None:
624
+ return None
625
+
626
+ arr = np.asarray(arr)
627
+ if arr.ndim == 3 and arr.shape[2] == 1:
628
+ arr = arr[..., 0]
629
+ if arr.dtype.kind in "ui":
630
+ scale = 65535.0 if arr.dtype.itemsize >= 2 else 255.0
631
+ arr = arr.astype(np.float32) / scale
632
+ else:
633
+ arr = arr.astype(np.float32, copy=False)
634
+ mx = float(arr.max()) if arr.size else 1.0
635
+ if mx > 5.0:
636
+ arr = arr / mx
637
+ return arr