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,639 @@
1
+ # pro/backgroundneutral.py
2
+ from __future__ import annotations
3
+ import numpy as np
4
+
5
+ from PyQt6.QtCore import Qt, QPointF, QRectF, QEvent, QTimer
6
+ from PyQt6.QtGui import QImage, QPixmap, QPen, QColor, QIcon, QPainter
7
+ from PyQt6.QtWidgets import (
8
+ QDialog, QVBoxLayout, QLabel, QGraphicsView, QGraphicsScene,
9
+ QHBoxLayout, QPushButton, QMessageBox, QGraphicsRectItem
10
+ )
11
+
12
+ # Reuse existing helpers + autostretch
13
+ from setiastro.saspro.imageops.stretch import stretch_color_image
14
+ # Shared utilities
15
+ from setiastro.saspro.widgets.image_utils import extract_mask_from_document as _active_mask_array_from_doc
16
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
17
+
18
+
19
+
20
+ # ----------------------------
21
+ # Core neutralization function
22
+ # ----------------------------
23
+ def background_neutralize_rgb(img: np.ndarray, rect_xywh: tuple[int, int, int, int]) -> np.ndarray:
24
+ """
25
+ Apply Background Neutralization to an RGB float32 image in [0,1],
26
+ using an image-space rectangle (x, y, w, h) as the sample region.
27
+ Returns a new float32 array in [0,1].
28
+ """
29
+ if img.ndim != 3 or img.shape[2] != 3:
30
+ raise ValueError("Background Neutralization requires a 3-channel RGB image.")
31
+
32
+ h, w, _ = img.shape
33
+ x, y, rw, rh = rect_xywh
34
+ x = max(0, min(int(x), w - 1))
35
+ y = max(0, min(int(y), h - 1))
36
+ rw = max(1, min(int(rw), w - x))
37
+ rh = max(1, min(int(rh), h - y))
38
+
39
+ sample = img[y:y+rh, x:x+rw, :]
40
+ medians = np.median(sample, axis=(0, 1)).astype(np.float32) # (3,)
41
+ avg_med = float(np.mean(medians))
42
+
43
+ out = img.copy()
44
+ eps = 1e-8
45
+
46
+ # Vectorized neutralization
47
+ # diff shape: (3,) -> (1, 1, 3)
48
+ diffs = (medians - avg_med).reshape(1, 1, 3)
49
+
50
+ # denom shape: (1, 1, 3)
51
+ denoms = 1.0 - diffs
52
+
53
+ # Avoid div-by-zero (vectorized)
54
+ # logic: if abs(denom) < eps, set to eps (sign matched)
55
+ # We can do this efficiently:
56
+ small_mask = np.abs(denoms) < eps
57
+ denoms[small_mask] = np.where(denoms[small_mask] >= 0, eps, -eps)
58
+
59
+ # Apply formula: (pixel - diff) / denom
60
+ out = (out - diffs) / denoms
61
+ out = np.clip(out, 0.0, 1.0)
62
+
63
+ return out.astype(np.float32, copy=False)
64
+
65
+
66
+ # ------------------------------------
67
+ # Auto background finder (SASv2 logic)
68
+ # ------------------------------------
69
+ def _find_best_patch_center(lum: np.ndarray) -> tuple[int, int]:
70
+ """Port of your downhill-walk tile search (works on a luminance plane)."""
71
+ h, w = lum.shape
72
+ th, tw = h // 10, w // 10
73
+
74
+ # Optimized: compute 10x10 tile medians using strided views where possible
75
+ # This avoids repeated slicing and is cache-friendlier
76
+ meds = np.zeros((10, 10), dtype=np.float32)
77
+
78
+ # For tiles that fit evenly, use reshape + median (faster than loop)
79
+ crop_h, crop_w = th * 10, tw * 10
80
+ if crop_h <= h and crop_w <= w:
81
+ lum_crop = lum[:crop_h, :crop_w]
82
+ # Reshape to (10, th, 10, tw) and compute medians
83
+ tiles = lum_crop.reshape(10, th, 10, tw).transpose(0, 2, 1, 3).reshape(10, 10, -1)
84
+ meds = np.median(tiles, axis=2).astype(np.float32)
85
+
86
+ # Handle edge tiles if image doesn't divide evenly
87
+ if h > crop_h or w > crop_w:
88
+ # Bottom row edge
89
+ if h > crop_h:
90
+ for j in range(10):
91
+ x0, x1 = j * tw, (j + 1) * tw if j < 9 else w
92
+ meds[9, j] = np.median(lum[9*th:h, x0:x1])
93
+ # Right column edge
94
+ if w > crop_w:
95
+ for i in range(10):
96
+ y0, y1 = i * th, (i + 1) * th if i < 9 else h
97
+ meds[i, 9] = np.median(lum[y0:y1, 9*tw:w])
98
+ else:
99
+ # Fallback for very small images
100
+ for i in range(10):
101
+ for j in range(10):
102
+ y0, x0 = i * th, j * tw
103
+ y1 = (i + 1) * th if i < 9 else h
104
+ x1 = (j + 1) * tw if j < 9 else w
105
+ meds[i, j] = np.median(lum[y0:y1, x0:x1])
106
+
107
+ idxs = np.argsort(meds.flatten())[:2]
108
+
109
+ finals = []
110
+ for idx in idxs:
111
+ ti, tj = divmod(int(idx), 10)
112
+ y0, x0 = ti * th, tj * tw
113
+ y1 = (ti + 1) * th if ti < 9 else h
114
+ x1 = (tj + 1) * tw if tj < 9 else w
115
+ for _ in range(200):
116
+ y = np.random.randint(y0, y1)
117
+ x = np.random.randint(x0, x1)
118
+ while True:
119
+ mv, mpos = lum[y, x], (y, x)
120
+ for dy in (-1, 0, 1):
121
+ for dx in (-1, 0, 1):
122
+ if dy == 0 and dx == 0:
123
+ continue
124
+ ny, nx = y + dy, x + dx
125
+ if 0 <= ny < h and 0 <= nx < w and lum[ny, nx] < mv:
126
+ mv, mpos = lum[ny, nx], (ny, nx)
127
+ if mpos == (y, x):
128
+ break
129
+ y, x = mpos
130
+ finals.append((y, x))
131
+
132
+ best_val = np.inf
133
+ best_pt = (h // 2, w // 2)
134
+ for (y, x) in finals:
135
+ y0 = max(0, y - 25); y1 = min(h, y + 25)
136
+ x0 = max(0, x - 25); x1 = min(w, x + 25)
137
+ m = np.median(lum[y0:y1, x0:x1])
138
+ if m < best_val:
139
+ best_val, best_pt = m, (y, x)
140
+ return best_pt
141
+
142
+
143
+ def auto_rect_50x50(img_rgb: np.ndarray) -> tuple[int, int, int, int]:
144
+ """
145
+ Find a robust 50×50 background rectangle (≥100 px margins) in image space.
146
+ Returns (x, y, w, h).
147
+ """
148
+ h, w, ch = img_rgb.shape
149
+ if ch != 3:
150
+ raise ValueError("Auto background finder expects a 3-channel RGB image.")
151
+ lum = img_rgb.mean(axis=2).astype(np.float32)
152
+
153
+ cy, cx = _find_best_patch_center(lum)
154
+
155
+ margin = 100
156
+ half = 25
157
+ min_cx, max_cx = margin + half, w - (margin + half)
158
+ min_cy, max_cy = margin + half, h - (margin + half)
159
+ cx = int(np.clip(cx, min_cx, max_cx))
160
+ cy = int(np.clip(cy, min_cy, max_cy))
161
+
162
+ # refine by ±half
163
+ best_val = np.inf
164
+ ty, tx = cy, cx
165
+ for dy in (-half, 0, +half):
166
+ for dx in (-half, 0, +half):
167
+ y = int(np.clip(cy + dy, min_cy, max_cy))
168
+ x = int(np.clip(cx + dx, min_cx, max_cx))
169
+ y0, y1 = y - half, y + half
170
+ x0, x1 = x - half, x + half
171
+ m = np.median(lum[y0:y1, x0:x1])
172
+ if m < best_val:
173
+ best_val, ty, tx = m, y, x
174
+
175
+ return (tx - half, ty - half, 50, 50)
176
+
177
+
178
+ # --------------------------------
179
+ # Headless apply (doc + preset in)
180
+ # --------------------------------
181
+ def apply_background_neutral_to_doc(doc, preset: dict | None = None):
182
+ """
183
+ Headless entrypoint (used by DnD shortcuts).
184
+ Preset schema:
185
+ {
186
+ "mode": "auto" | "rect",
187
+ # rect in normalized coords if mode == "rect"
188
+ "rect_norm": [x0, y0, w, h] # each in 0..1
189
+ }
190
+ Defaults to {"mode": "auto"}.
191
+ """
192
+ import numpy as np
193
+
194
+ if preset is None:
195
+ preset = {}
196
+ mode = (preset.get("mode") or "auto").lower()
197
+
198
+ base = np.asarray(doc.image).astype(np.float32, copy=False)
199
+ if base.size == 0:
200
+ raise ValueError("Empty image.")
201
+
202
+ # Defensive normalization (should already be [0,1] in SASpro)
203
+ maxv = float(np.nanmax(base))
204
+ if maxv > 1.0 and np.isfinite(maxv):
205
+ base = base / maxv
206
+
207
+ if base.ndim != 3 or base.shape[2] != 3:
208
+ raise ValueError("Background Neutralization currently supports RGB images.")
209
+
210
+ if mode == "rect":
211
+ rn = preset.get("rect_norm")
212
+ if not rn or len(rn) != 4:
213
+ raise ValueError("rect mode requires rect_norm=[x,y,w,h] in normalized coords.")
214
+ H, W, _ = base.shape
215
+ x = int(np.clip(rn[0], 0, 1) * W)
216
+ y = int(np.clip(rn[1], 0, 1) * H)
217
+ w = int(np.clip(rn[2], 0, 1) * W)
218
+ h = int(np.clip(rn[3], 0, 1) * H)
219
+ rect = (x, y, max(w, 1), max(h, 1))
220
+ else:
221
+ rect = auto_rect_50x50(base)
222
+
223
+ out = background_neutralize_rgb(base, rect)
224
+
225
+ # Destination-mask blend (mask lives on the destination doc)
226
+ m = _active_mask_array_from_doc(doc)
227
+ if m is not None:
228
+ if out.ndim == 3:
229
+ m3 = np.repeat(m[..., None], 3, axis=2).astype(np.float32, copy=False)
230
+ else:
231
+ m3 = m.astype(np.float32, copy=False)
232
+ base_for_blend = np.asarray(doc.image).astype(np.float32, copy=False)
233
+ bmax = float(np.nanmax(base_for_blend))
234
+ if bmax > 1.0 and np.isfinite(bmax):
235
+ base_for_blend /= bmax
236
+ out = base_for_blend * (1.0 - m3) + out * m3
237
+
238
+ doc.apply_edit(
239
+ out.astype(np.float32, copy=False),
240
+ metadata={"step_name": "Background Neutralization", "preset": preset},
241
+ step_name="Background Neutralization",
242
+ )
243
+
244
+
245
+ # -------------------------
246
+ # Interactive BN dialog UI
247
+ # -------------------------
248
+ class BackgroundNeutralizationDialog(QDialog):
249
+ def __init__(self, parent, doc, icon: QIcon | None = None):
250
+ super().__init__(parent)
251
+ self._main = parent
252
+ self.doc = doc
253
+
254
+ self._connected_current_doc_changed = False
255
+ if hasattr(self._main, "currentDocumentChanged"):
256
+ try:
257
+ self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
258
+ self._connected_current_doc_changed = True
259
+ except Exception:
260
+ self._connected_current_doc_changed = False
261
+
262
+ self.finished.connect(self._cleanup_connections)
263
+ try:
264
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
265
+ except Exception:
266
+ pass # older PyQt6 versions
267
+
268
+ if icon:
269
+ self.setWindowIcon(icon)
270
+ self.setWindowTitle(self.tr("Background Neutralization"))
271
+ self.resize(900, 600)
272
+
273
+ self.setWindowFlag(Qt.WindowType.Window, True)
274
+ # Non-modal: allow user to switch between images while dialog is open
275
+ self.setWindowModality(Qt.WindowModality.NonModal)
276
+ self.setModal(False)
277
+ #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
278
+
279
+ self.auto_stretch = False
280
+ self.zoom_factor = 1.0
281
+ self._user_zoomed = False
282
+
283
+ # --- scene / view ---
284
+ self.scene = QGraphicsScene(self)
285
+ self.graphics_view = QGraphicsView(self)
286
+ self.graphics_view.setScene(self.scene)
287
+ self.graphics_view.setRenderHints(
288
+ QPainter.RenderHint.Antialiasing |
289
+ QPainter.RenderHint.SmoothPixmapTransform
290
+ )
291
+ self.graphics_view.setTransformationAnchor(QGraphicsView.ViewportAnchor.AnchorUnderMouse)
292
+ self.graphics_view.setResizeAnchor(QGraphicsView.ViewportAnchor.AnchorViewCenter)
293
+
294
+ # --- main layout ---
295
+ layout = QVBoxLayout(self)
296
+ instruction = QLabel("Draw a sample box or click ‘Find Background’ to auto-select.")
297
+ layout.addWidget(instruction)
298
+ layout.addWidget(self.graphics_view, 1)
299
+
300
+ # Buttons row
301
+ btn_row = QHBoxLayout()
302
+ self.btn_apply = QPushButton(self.tr("Apply Neutralization"))
303
+ self.btn_cancel = QPushButton(self.tr("Cancel"))
304
+ self.btn_toggle_stretch = QPushButton(self.tr("Enable Auto-Stretch"))
305
+ self.btn_find_bg = QPushButton(self.tr("Find Background"))
306
+ btn_row.addWidget(self.btn_apply)
307
+ btn_row.addWidget(self.btn_cancel)
308
+ btn_row.addWidget(self.btn_toggle_stretch)
309
+ btn_row.addWidget(self.btn_find_bg)
310
+ layout.addLayout(btn_row)
311
+
312
+ # Zoom row
313
+ # Zoom row (standardized themed toolbuttons)
314
+ zoom_row = QHBoxLayout()
315
+
316
+ self.btn_zoom_out = themed_toolbtn("zoom-out", "Zoom Out")
317
+ self.btn_fit = themed_toolbtn("zoom-fit-best", "Fit to View")
318
+ self.btn_zoom_in = themed_toolbtn("zoom-in", "Zoom In")
319
+
320
+ zoom_row.addWidget(self.btn_zoom_out)
321
+ zoom_row.addWidget(self.btn_fit)
322
+ zoom_row.addWidget(self.btn_zoom_in)
323
+ zoom_row.addStretch(1) # optional: keeps them left-aligned
324
+
325
+ layout.addLayout(zoom_row)
326
+
327
+ # Events
328
+ self.btn_apply.clicked.connect(self._on_apply)
329
+ self.btn_cancel.clicked.connect(self.close)
330
+ self.btn_toggle_stretch.clicked.connect(self._toggle_auto_stretch)
331
+ self.btn_find_bg.clicked.connect(self._on_find_background)
332
+ self.btn_zoom_out.clicked.connect(self.zoom_out)
333
+ self.btn_fit.clicked.connect(self.fit_to_view)
334
+ self.btn_zoom_in.clicked.connect(self.zoom_in)
335
+
336
+ self.graphics_view.viewport().installEventFilter(self)
337
+ self.origin_scene = QPointF()
338
+ self.current_rect_scene = QRectF()
339
+ self.selection_item: QGraphicsRectItem | None = None
340
+ self.drawing = False
341
+
342
+ self._load_image()
343
+
344
+ # ---- active document change ------------------------------------
345
+ def _on_active_doc_changed(self, doc):
346
+ """Called when user clicks a different image window."""
347
+ if doc is None or getattr(doc, "image", None) is None:
348
+ return
349
+ self.doc = doc
350
+ self.selection_item = None
351
+ self._load_image()
352
+
353
+ # ---------- image display ----------
354
+ def _doc_image_normalized(self) -> np.ndarray:
355
+ import numpy as np
356
+ img = np.asarray(self.doc.image).astype(np.float32, copy=False)
357
+ if img.size == 0:
358
+ return img
359
+ m = float(np.nanmax(img))
360
+ if m > 1.0 and np.isfinite(m):
361
+ img = img / m
362
+ return img
363
+
364
+ def _load_image(self):
365
+ self.scene.clear()
366
+ self.selection_item = None
367
+
368
+ img = self._doc_image_normalized()
369
+ if img is None or img.size == 0:
370
+ QMessageBox.warning(self, "No Image", "Open an image first.")
371
+ self.reject()
372
+ return
373
+
374
+ disp = img.copy()
375
+ if self.auto_stretch and disp.ndim == 3 and disp.shape[2] == 3:
376
+ disp = stretch_color_image(disp, 0.25, linked=False, normalize=False)
377
+
378
+ # Build QImage/QPixmap
379
+ if disp.ndim == 2:
380
+ h, w = disp.shape
381
+ qimg = QImage((disp * 255).astype(np.uint8).tobytes(), w, h, w, QImage.Format.Format_Grayscale8)
382
+ else:
383
+ h, w, _ = disp.shape
384
+ qimg = QImage((disp * 255).astype(np.uint8).tobytes(), w, h, 3 * w, QImage.Format.Format_RGB888)
385
+
386
+ pix = QPixmap.fromImage(qimg)
387
+
388
+ # Add to scene; force scene rect to native image pixels and place at (0,0)
389
+ self.scene.clear()
390
+ self.selection_item = None
391
+ self.pixmap_item = self.scene.addPixmap(pix)
392
+ self.pixmap_item.setPos(0, 0)
393
+ self.scene.setSceneRect(0, 0, pix.width(), pix.height())
394
+
395
+ # Reset and fit (this sets initial view, later showEvent/resizeEvent will refit)
396
+ self.graphics_view.resetTransform()
397
+ self.graphics_view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
398
+ self.zoom_factor = 1.0
399
+ self._user_zoomed = False
400
+
401
+ def _toggle_auto_stretch(self):
402
+ self.auto_stretch = not self.auto_stretch
403
+ self.btn_toggle_stretch.setText("Disable Auto-Stretch" if self.auto_stretch else "Enable Auto-Stretch")
404
+ self._load_image()
405
+
406
+ # ---------- zoom ----------
407
+ def eventFilter(self, source, event):
408
+ if source is self.graphics_view.viewport():
409
+ et = event.type()
410
+ if et == QEvent.Type.MouseButtonPress and event.button() == Qt.MouseButton.LeftButton:
411
+ self.drawing = True
412
+ self.origin_scene = self.graphics_view.mapToScene(event.pos())
413
+ if self.selection_item:
414
+ self.scene.removeItem(self.selection_item)
415
+ self.selection_item = None
416
+ elif et == QEvent.Type.MouseMove and self.drawing:
417
+ cur = self.graphics_view.mapToScene(event.pos())
418
+ self.current_rect_scene = QRectF(self.origin_scene, cur).normalized()
419
+ if self.selection_item:
420
+ self.scene.removeItem(self.selection_item)
421
+ pen = QPen(QColor(0, 255, 0), 2, Qt.PenStyle.DashLine)
422
+ self.selection_item = self.scene.addRect(self.current_rect_scene, pen)
423
+ elif et == QEvent.Type.MouseButtonRelease and event.button() == Qt.MouseButton.LeftButton and self.drawing:
424
+ self.drawing = False
425
+ cur = self.graphics_view.mapToScene(event.pos())
426
+ self.current_rect_scene = QRectF(self.origin_scene, cur).normalized()
427
+ if self.selection_item:
428
+ self.scene.removeItem(self.selection_item)
429
+ if self.current_rect_scene.width() < 10 or self.current_rect_scene.height() < 10:
430
+ QMessageBox.warning(self, "Selection Too Small", "Please draw a larger selection box.")
431
+ self.selection_item = None
432
+ self.current_rect_scene = QRectF()
433
+ else:
434
+ pen = QPen(QColor(255, 0, 0), 2, Qt.PenStyle.SolidLine)
435
+ self.selection_item = self.scene.addRect(self.current_rect_scene, pen)
436
+ return super().eventFilter(source, event)
437
+
438
+ def _on_find_background(self):
439
+ img = self._doc_image_normalized()
440
+ if img.ndim != 3 or img.shape[2] != 3:
441
+ QMessageBox.warning(self, "Not RGB", "Background Neutralization supports RGB images.")
442
+ return
443
+
444
+ x, y, w, h = auto_rect_50x50(img)
445
+
446
+ if self.selection_item:
447
+ self.scene.removeItem(self.selection_item)
448
+
449
+ pen = QPen(QColor(255, 215, 0), 2) # gold
450
+ rect_scene = QRectF(float(x), float(y), float(w), float(h)) # scene == image pixels now
451
+ self.selection_item = self.scene.addRect(rect_scene, pen)
452
+ self.current_rect_scene = rect_scene
453
+
454
+ def _scene_rect_to_image_rect(self) -> tuple[int, int, int, int]:
455
+ if not self.current_rect_scene or self.current_rect_scene.isNull():
456
+ raise ValueError("No selection rectangle defined.")
457
+
458
+ # Scene == image pixels (because we setSceneRect to pixmap bounds)
459
+ bounds = self.pixmap_item.boundingRect()
460
+ W = int(bounds.width())
461
+ H = int(bounds.height())
462
+
463
+ x = int(max(0.0, min(bounds.width(), self.current_rect_scene.left())))
464
+ y = int(max(0.0, min(bounds.height(), self.current_rect_scene.top())))
465
+ w = int(max(1.0, min(bounds.width() - x, self.current_rect_scene.width())))
466
+ h = int(max(1.0, min(bounds.height() - y, self.current_rect_scene.height())))
467
+ return (x, y, w, h)
468
+
469
+ def _on_apply(self):
470
+ try:
471
+ rect = self._scene_rect_to_image_rect()
472
+ except Exception as e:
473
+ QMessageBox.warning(self, "No Selection", str(e))
474
+ return
475
+
476
+ img = self._doc_image_normalized()
477
+ if img.ndim != 3 or img.shape[2] != 3:
478
+ QMessageBox.warning(self, "Not RGB", "Background Neutralization supports RGB images.")
479
+ return
480
+
481
+ out = background_neutralize_rgb(img, rect)
482
+
483
+ # Destination-mask blend
484
+ m = _active_mask_array_from_doc(self.doc)
485
+ if m is not None:
486
+ if out.ndim == 3:
487
+ m3 = np.repeat(m[..., None], 3, axis=2).astype(np.float32, copy=False)
488
+ else:
489
+ m3 = m.astype(np.float32, copy=False)
490
+ base_for_blend = self._doc_image_normalized()
491
+ out = base_for_blend * (1.0 - m3) + out * m3
492
+
493
+ # ---------- Build preset for Replay Last ----------
494
+ preset = None
495
+ try:
496
+ H, W = img.shape[:2]
497
+ x, y, w, h = rect
498
+ if W > 0 and H > 0:
499
+ rect_norm = [
500
+ float(x) / float(W),
501
+ float(y) / float(H),
502
+ float(w) / float(W),
503
+ float(h) / float(H),
504
+ ]
505
+ else:
506
+ rect_norm = [0.0, 0.0, 1.0, 1.0]
507
+
508
+ preset = {"mode": "rect", "rect_norm": rect_norm}
509
+
510
+ # Walk up parent chain until we find the main window that carries
511
+ # _last_headless_command
512
+ main = self.parent()
513
+ while main is not None and not hasattr(main, "_last_headless_command"):
514
+ main = main.parent()
515
+
516
+ if main is not None:
517
+ try:
518
+ main._last_headless_command = {
519
+ "command_id": "background_neutral",
520
+ "preset": preset,
521
+ }
522
+ if hasattr(main, "_log"):
523
+ main._log(
524
+ "[Replay] Recorded background_neutral "
525
+ f"(mode=rect, rect_norm={rect_norm})"
526
+ )
527
+ except Exception:
528
+ pass
529
+ except Exception:
530
+ # Fallback: at least record mode
531
+ if preset is None:
532
+ preset = {"mode": "rect"}
533
+
534
+ # ---------- Apply edit (include preset in metadata) ----------
535
+ meta = {
536
+ "step_name": "Background Neutralization",
537
+ "rect": rect,
538
+ }
539
+ if preset is not None:
540
+ meta["preset"] = preset
541
+
542
+ self.doc.apply_edit(
543
+ out.astype(np.float32, copy=False),
544
+ metadata=meta,
545
+ step_name="Background Neutralization",
546
+ )
547
+ # Dialog stays open so user can apply to other images
548
+ # Refresh to use the now-active document for next operation
549
+ self.close()
550
+
551
+ def closeEvent(self, ev):
552
+ self._cleanup_connections()
553
+ super().closeEvent(ev)
554
+
555
+ def _cleanup_connections(self):
556
+ # Disconnect active-doc tracking (Fabio hook)
557
+ try:
558
+ if self._connected_current_doc_changed and hasattr(self._main, "currentDocumentChanged"):
559
+ self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
560
+ except Exception:
561
+ pass
562
+ self._connected_current_doc_changed = False
563
+
564
+ # If you ever add threads/workers later, stop them here too (safe no-ops now)
565
+ try:
566
+ if getattr(self, "_worker", None) is not None:
567
+ try:
568
+ self._worker.requestInterruption()
569
+ except Exception:
570
+ pass
571
+ if getattr(self, "_thread", None) is not None:
572
+ self._thread.quit()
573
+ self._thread.wait(500)
574
+ except Exception:
575
+ pass
576
+
577
+
578
+ def _refresh_document_from_active(self):
579
+ """
580
+ Refresh the dialog's document reference to the currently active document.
581
+ This allows reusing the same dialog on different images.
582
+ """
583
+ try:
584
+ main = self.parent()
585
+ if main and hasattr(main, "_active_doc"):
586
+ new_doc = main._active_doc()
587
+ if new_doc is not None and new_doc is not self.doc:
588
+ self.doc = new_doc
589
+ # Refresh the preview image
590
+ self._load_preview()
591
+ except Exception:
592
+ pass
593
+
594
+ def _zoom(self, factor: float):
595
+ self._user_zoomed = True
596
+ cur = self.graphics_view.transform().m11()
597
+ new_scale = cur * factor
598
+ if new_scale < 0.01 or new_scale > 100.0:
599
+ return
600
+ self.graphics_view.scale(factor, factor)
601
+
602
+ def zoom_in(self):
603
+ self._zoom(1.25)
604
+
605
+ def zoom_out(self):
606
+ self._zoom(0.8)
607
+
608
+ def fit_to_view(self):
609
+ self._user_zoomed = False
610
+ self.graphics_view.resetTransform()
611
+ # Fit the pixmap bounds (not a default huge scene)
612
+ if hasattr(self, "pixmap_item") and self.pixmap_item is not None:
613
+ self.graphics_view.fitInView(self.pixmap_item, Qt.AspectRatioMode.KeepAspectRatio)
614
+
615
+ def showEvent(self, e):
616
+ super().showEvent(e)
617
+ # fit after the widget is actually visible
618
+ QTimer.singleShot(0, self.fit_to_view)
619
+
620
+ def resizeEvent(self, e):
621
+ super().resizeEvent(e)
622
+ # keep it fitted while the user hasn't manually zoomed
623
+ if not self._user_zoomed:
624
+ self.fit_to_view()
625
+
626
+ from setiastro.saspro.headless_utils import normalize_headless_main, unwrap_docproxy
627
+
628
+ def run_background_neutral_via_preset(main, preset=None, target_doc=None):
629
+ from PyQt6.QtWidgets import QMessageBox
630
+ from setiastro.saspro.backgroundneutral import apply_background_neutral_to_doc
631
+
632
+ p = dict(preset or {})
633
+ main, doc, _dm = normalize_headless_main(main, target_doc)
634
+
635
+ if doc is None or getattr(doc, "image", None) is None:
636
+ QMessageBox.warning(main or None, "Background Neutralization", "Load an image first.")
637
+ return
638
+
639
+ apply_background_neutral_to_doc(doc, p)