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,648 @@
1
+ # pro/wavescale_hdr.py
2
+ from __future__ import annotations
3
+ import numpy as np
4
+
5
+ from PyQt6.QtCore import Qt, QObject, pyqtSignal, QThread, QTimer, QSettings
6
+ from PyQt6.QtGui import QImage, QPixmap, QIcon
7
+ from PyQt6.QtWidgets import (
8
+ QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, QLabel, QPushButton,
9
+ QSlider, QGraphicsScene, QGraphicsPixmapItem, QScrollArea,
10
+ QMessageBox, QProgressBar
11
+ )
12
+
13
+ # Import centralized widget
14
+ from setiastro.saspro.widgets.graphics_views import ZoomableGraphicsView
15
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
16
+
17
+ # Import shared wavelet utilities
18
+ from setiastro.saspro.widgets.wavelet_utils import (
19
+ conv_sep_reflect as _conv_sep_reflect,
20
+ build_spaced_kernel as _build_spaced_kernel,
21
+ atrous_decompose as _atrous_decompose,
22
+ atrous_reconstruct as _atrous_reconstruct,
23
+ rgb_to_lab as _rgb_to_lab,
24
+ lab_to_rgb as _lab_to_rgb,
25
+ B3_KERNEL as _B3,
26
+ )
27
+
28
+ # ──────────────────────────────────────────────────────────────────────────────
29
+ # Core math (shared by dialog + headless apply)
30
+ # ──────────────────────────────────────────────────────────────────────────────
31
+
32
+ def _mask_from_L(L: np.ndarray, gamma: float) -> np.ndarray:
33
+ m = np.clip(L / 100.0, 0.0, 1.0).astype(np.float32)
34
+ if gamma != 1.0:
35
+ m = np.power(m, gamma, dtype=np.float32)
36
+ return m
37
+
38
+ def _apply_dim_curve(rgb: np.ndarray, gamma: float) -> np.ndarray:
39
+ return np.power(np.clip(rgb, 0.0, 1.0), gamma, dtype=np.float32)
40
+
41
+ def compute_wavescale_hdr(rgb_image: np.ndarray,
42
+ n_scales: int = 5,
43
+ compression_factor: float = 1.5,
44
+ mask_gamma: float = 1.0,
45
+ base_kernel: np.ndarray = _B3,
46
+ decay_rate: float = 0.5) -> tuple[np.ndarray, np.ndarray]:
47
+ """
48
+ Returns (transformed_rgb, luminance_mask). transformed_rgb is already
49
+ reconstructed from modified L and gamma-dimmed.
50
+ """
51
+ lab = _rgb_to_lab(rgb_image)
52
+ L0 = lab[..., 0].astype(np.float32, copy=True)
53
+ scales = _atrous_decompose(L0, n_scales, base_kernel)
54
+
55
+ mask = _mask_from_L(L0, mask_gamma)
56
+ planes, residual = scales[:-1], scales[-1]
57
+
58
+ for i, wp in enumerate(planes):
59
+ decay = decay_rate ** i
60
+ scale = (1.0 + (compression_factor - 1.0) * mask * decay) * 2.0
61
+ planes[i] = wp * scale
62
+
63
+ Lr = _atrous_reconstruct(planes + [residual])
64
+
65
+ # midtones alignment
66
+ med0 = float(np.median(L0))
67
+ med1 = float(np.median(Lr)) or 1.0
68
+ Lr = np.clip(Lr * (med0 / med1), 0.0, 100.0)
69
+
70
+ lab[..., 0] = Lr
71
+ rgb = _lab_to_rgb(lab)
72
+
73
+ # gentle dimming curve to tame highlights
74
+ rgb = _apply_dim_curve(rgb, gamma=1.0 + n_scales * 0.2)
75
+ return rgb, mask
76
+
77
+ def compute_wavescale_hdr(rgb_image: np.ndarray,
78
+ n_scales: int = 5,
79
+ compression_factor: float = 1.5,
80
+ mask_gamma: float = 1.0,
81
+ base_kernel: np.ndarray = _B3,
82
+ decay_rate: float = 0.5,
83
+ dim_gamma: float | None = None) -> tuple[np.ndarray, np.ndarray]:
84
+ """
85
+ Returns (transformed_rgb, luminance_mask).
86
+ If dim_gamma is None, uses auto gamma = 1.0 + 0.2 * n_scales.
87
+ """
88
+ lab = _rgb_to_lab(rgb_image)
89
+ L0 = lab[..., 0].astype(np.float32, copy=True)
90
+ scales = _atrous_decompose(L0, n_scales, base_kernel)
91
+
92
+ mask = _mask_from_L(L0, mask_gamma)
93
+ planes, residual = scales[:-1], scales[-1]
94
+
95
+ for i, wp in enumerate(planes):
96
+ decay = decay_rate ** i
97
+ scale = (1.0 + (compression_factor - 1.0) * mask * decay) * 2.0
98
+ planes[i] = wp * scale
99
+
100
+ Lr = _atrous_reconstruct(planes + [residual])
101
+
102
+ # midtones alignment
103
+ med0 = float(np.median(L0))
104
+ med1 = float(np.median(Lr)) or 1.0
105
+ Lr = np.clip(Lr * (med0 / med1), 0.0, 100.0)
106
+
107
+ lab[..., 0] = Lr
108
+ rgb = _lab_to_rgb(lab)
109
+
110
+ # dimming curve
111
+ g = (1.0 + n_scales * 0.2) if dim_gamma is None else float(dim_gamma)
112
+ rgb = _apply_dim_curve(rgb, gamma=g)
113
+ return rgb, mask
114
+
115
+
116
+ # ──────────────────────────────────────────────────────────────────────────────
117
+ # Worker (QObject in its own QThread) for the dialog
118
+ # ──────────────────────────────────────────────────────────────────────────────
119
+
120
+ class HDRWorker(QObject):
121
+ progress_update = pyqtSignal(str, int) # (step, percent)
122
+ finished = pyqtSignal(np.ndarray, np.ndarray) # (transformed_rgb, mask)
123
+
124
+ def __init__(self, rgb_image: np.ndarray, n_scales: int, compression_factor: float,
125
+ mask_gamma: float, base_kernel: np.ndarray):
126
+ super().__init__()
127
+ self.rgb_image = rgb_image
128
+ self.n_scales = n_scales
129
+ self.compression_factor = compression_factor
130
+ self.mask_gamma = mask_gamma
131
+ self.base_kernel = base_kernel
132
+
133
+ def run(self):
134
+ try:
135
+ self.progress_update.emit(self.tr("Converting to Lab color space…"), 10)
136
+ # progress checkpoints inline here are cosmetic
137
+ self.progress_update.emit(self.tr("Decomposing luminance with starlet…"), 20)
138
+ # full compute
139
+ transformed, mask = compute_wavescale_hdr(
140
+ self.rgb_image, self.n_scales, self.compression_factor, self.mask_gamma, self.base_kernel
141
+ )
142
+ self.progress_update.emit(self.tr("Finalizing…"), 95)
143
+ self.finished.emit(transformed, mask)
144
+ except Exception as e:
145
+ print("WaveScale HDR error:", e)
146
+ self.finished.emit(None, None)
147
+
148
+ # ──────────────────────────────────────────────────────────────────────────────
149
+ # Simple mask window
150
+ # ──────────────────────────────────────────────────────────────────────────────
151
+
152
+ class MaskDisplayWindow(QDialog):
153
+ def __init__(self, parent=None):
154
+ super().__init__(parent)
155
+ self.setWindowTitle(self.tr("HDR Mask (L-based)"))
156
+ self.lbl = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
157
+ self.lbl.setFixedSize(400, 400) # keep it small
158
+ lay = QVBoxLayout(self)
159
+ lay.addWidget(self.lbl)
160
+
161
+ def update_mask(self, mask: np.ndarray):
162
+ if mask is None:
163
+ return
164
+ m = np.clip(mask, 0, 1).astype(np.float32)
165
+ m8 = (m * 255.0).astype(np.uint8)
166
+ if m8.ndim == 2:
167
+ h, w = m8.shape
168
+ rgb = np.repeat(m8[..., None], 3, axis=2)
169
+ else:
170
+ h, w, _ = m8.shape
171
+ rgb = m8
172
+ qimg = QImage(rgb.data, w, h, 3*w, QImage.Format.Format_RGB888)
173
+ pix = QPixmap.fromImage(qimg).scaled(
174
+ self.lbl.size(), Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation
175
+ )
176
+ self.lbl.setPixmap(pix)
177
+
178
+ # ──────────────────────────────────────────────────────────────────────────────
179
+ # Dialog
180
+ # ──────────────────────────────────────────────────────────────────────────────
181
+
182
+ class WaveScaleHDRDialogPro(QDialog):
183
+ applied_preset = pyqtSignal(object, dict)
184
+
185
+ def __init__(self, parent, doc, icon_path: str | None = None, *, headless: bool=False, bypass_guard: bool=False):
186
+ super().__init__(parent)
187
+ self.setWindowTitle(self.tr("WaveScale HDR"))
188
+ self._headless = bool(headless)
189
+ self._bypass_guard = bool(bypass_guard)
190
+ if self._headless:
191
+ # Don’t show any windows; we’ll still exec() to run the event loop.
192
+ try: self.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen, True)
193
+ except Exception as e:
194
+ import logging
195
+ logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
196
+ if icon_path:
197
+ try: self.setWindowIcon(QIcon(icon_path))
198
+ except Exception as e:
199
+ import logging
200
+ logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
201
+ self.resize(980, 700)
202
+ self.setWindowFlag(Qt.WindowType.Window, True)
203
+ self.setWindowModality(Qt.WindowModality.NonModal)
204
+ self.setModal(False)
205
+ try:
206
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
207
+ except Exception:
208
+ pass # older PyQt6 versions
209
+ self._doc = doc
210
+ base = getattr(doc, "image", None)
211
+ if base is None:
212
+ raise RuntimeError("Active document has no image.")
213
+
214
+ # normalize to float32 [0..1] RGB for processing/preview
215
+ img = np.asarray(base, dtype=np.float32)
216
+ if img.ndim == 2:
217
+ img_rgb = np.repeat(img[:, :, None], 3, axis=2)
218
+ self._was_mono = True
219
+ self._mono_shape = img.shape
220
+ elif img.ndim == 3 and img.shape[2] == 1:
221
+ img_rgb = np.repeat(img, 3, axis=2)
222
+ self._was_mono = True
223
+ self._mono_shape = img.shape
224
+ else:
225
+ img_rgb = img[:, :, :3]
226
+ self._was_mono = False
227
+ self._mono_shape = None
228
+
229
+ if img.dtype.kind in "ui":
230
+ maxv = float(np.nanmax(img_rgb)) or 1.0
231
+ img_rgb = img_rgb / max(1.0, maxv)
232
+ img_rgb = np.clip(img_rgb, 0.0, 1.0).astype(np.float32, copy=False)
233
+
234
+ self.original_rgb = img_rgb
235
+ self.preview_rgb = img_rgb.copy()
236
+
237
+ # scene/view (⚠️ use ZoomableGraphicsView)
238
+ self.scene = QGraphicsScene(self)
239
+ self.view = ZoomableGraphicsView(self.scene, self)
240
+ self.view.setAlignment(Qt.AlignmentFlag.AlignCenter)
241
+ self.pix = QGraphicsPixmapItem()
242
+ self.scene.addItem(self.pix)
243
+
244
+ # optional: keep your scroll area wrapper
245
+ self.scroll = QScrollArea(self)
246
+ self.scroll.setWidgetResizable(True)
247
+ self.scroll.setWidget(self.view)
248
+
249
+ # controls (add zoom row)
250
+ self.grp = QGroupBox(self.tr("HDR Controls"))
251
+ form = QFormLayout(self.grp)
252
+
253
+ self.s_scales = QSlider(Qt.Orientation.Horizontal); self.s_scales.setRange(2, 10); self.s_scales.setValue(5)
254
+ self.s_comp = QSlider(Qt.Orientation.Horizontal); self.s_comp.setRange(10, 500); self.s_comp.setValue(150)
255
+ self.s_gamma = QSlider(Qt.Orientation.Horizontal); self.s_gamma.setRange(10, 1000); self.s_gamma.setValue(500)
256
+
257
+ form.addRow(self.tr("Number of Scales:"), self.s_scales)
258
+ form.addRow(self.tr("Coarse Compression:"), self.s_comp)
259
+ form.addRow(self.tr("Mask Gamma:"), self.s_gamma)
260
+
261
+ row = QHBoxLayout()
262
+ self.btn_preview = QPushButton(self.tr("Preview"))
263
+ self.btn_toggle = QPushButton(self.tr("Show Original")); self.btn_toggle.setCheckable(True)
264
+ row.addWidget(self.btn_preview); row.addWidget(self.btn_toggle)
265
+ form.addRow(row)
266
+
267
+ # ↓ NEW: zoom controls
268
+ zoom_row = QHBoxLayout()
269
+ self.btn_zoom_in = QPushButton(self.tr("Zoom In"))
270
+ self.btn_zoom_out = QPushButton(self.tr("Zoom Out"))
271
+ self.btn_fit = QPushButton(self.tr("Fit to Preview"))
272
+ zoom_row.addWidget(self.btn_zoom_in)
273
+ zoom_row.addWidget(self.btn_zoom_out)
274
+ zoom_row.addWidget(self.btn_fit)
275
+ form.addRow(zoom_row)
276
+
277
+ # progress group (unchanged)
278
+ self.prog_grp = QGroupBox(self.tr("Processing Progress"))
279
+ vprog = QVBoxLayout(self.prog_grp)
280
+ self.lbl_step = QLabel(self.tr("Idle"))
281
+ self.bar = QProgressBar(); self.bar.setRange(0, 100); self.bar.setValue(0)
282
+ vprog.addWidget(self.lbl_step); vprog.addWidget(self.bar)
283
+
284
+ # bottom buttons (unchanged)
285
+ bot = QHBoxLayout()
286
+ self.btn_apply = QPushButton(self.tr("Apply to Document")); self.btn_apply.setEnabled(False)
287
+ self.btn_reset = QPushButton(self.tr("Reset"))
288
+ self.btn_close = QPushButton(self.tr("Close"))
289
+ bot.addStretch(1); bot.addWidget(self.btn_apply); bot.addWidget(self.btn_reset); bot.addWidget(self.btn_close)
290
+
291
+ # layout (unchanged)
292
+ main = QVBoxLayout(self)
293
+ main.addWidget(self.scroll)
294
+ h = QHBoxLayout()
295
+ h.addWidget(self.grp, 3)
296
+ h.addWidget(self.prog_grp, 1)
297
+ main.addLayout(h)
298
+ main.addLayout(bot)
299
+
300
+ # mask window
301
+ self.mask_win = MaskDisplayWindow(self)
302
+ if not self._headless:
303
+ self.mask_win.show()
304
+
305
+
306
+ # kernel
307
+ self.base_kernel = _B3
308
+
309
+ # connections
310
+ self.btn_preview.clicked.connect(self._start_preview)
311
+ self.btn_apply.clicked.connect(self._apply_to_doc)
312
+ self.btn_close.clicked.connect(self.reject)
313
+ self.btn_reset.clicked.connect(self._reset)
314
+ self.btn_toggle.clicked.connect(self._toggle)
315
+
316
+ self.btn_zoom_in.clicked.connect(self.view.zoom_in)
317
+ self.btn_zoom_out.clicked.connect(self.view.zoom_out)
318
+ self.btn_fit.clicked.connect(lambda: self.view.fit_item(self.pix))
319
+
320
+ # ── Mask shown immediately ───────────────────────────────────────────
321
+ # Precompute L from original and push initial mask to the small window
322
+ self._lab_original = _rgb_to_lab(self.original_rgb)
323
+ self._L_original = self._lab_original[..., 0].astype(np.float32, copy=True)
324
+ self._mask_timer = QTimer(self)
325
+ self._mask_timer.setSingleShot(True)
326
+ self._mask_timer.timeout.connect(self._update_mask_from_gamma)
327
+ self.s_gamma.valueChanged.connect(self._schedule_mask_refresh)
328
+
329
+ # show initial mask right away
330
+ self._update_mask_from_gamma()
331
+
332
+ # initial pix
333
+ self._set_pix(self.preview_rgb)
334
+
335
+ def apply_preset(self, p: dict):
336
+ # sliders are integer; map floats to their scales
337
+ ns = int(p.get("n_scales", 5))
338
+ comp = float(p.get("compression_factor", 1.5))
339
+ mg = float(p.get("mask_gamma", 5.0)) # dialog default is 5.0 (slider 500)
340
+ # clamp safely
341
+ ns = max(2, min(10, ns))
342
+ comp_i = int(max(10, min(500, round(comp*100)))) # 1.0..5.0 -> 100..500
343
+ mg_i = int(max(10, min(1000, round(mg*100)))) # 0.1..10.0 -> 10..1000
344
+ self.s_scales.setValue(ns)
345
+ self.s_comp.setValue(comp_i)
346
+ self.s_gamma.setValue(mg_i)
347
+ # refresh mask preview (even if window is hidden)
348
+ self._update_mask_from_gamma()
349
+
350
+ def _headless_guard_active(self) -> bool:
351
+ """Only guard true concurrent *headless* runs; ignore stale locks."""
352
+ # If we are not launching headless, never block the interactive UI.
353
+ if not self._headless:
354
+ return False
355
+
356
+ # Parent flags
357
+ p = self.parent()
358
+ if p and (getattr(p, "_wavescale_guard", False) or getattr(p, "_wavescale_headless_running", False)):
359
+ return True
360
+
361
+ # Settings lock with TTL
362
+ try:
363
+ s = QSettings()
364
+ in_prog = bool(s.value("wavescale/headless_in_progress", False))
365
+ started = float(s.value("wavescale/headless_started_at", 0.0))
366
+ except Exception:
367
+ in_prog, started = False, 0.0
368
+
369
+ if not in_prog:
370
+ return False
371
+
372
+ # consider anything older than 5 minutes stale
373
+ import time
374
+ if (time.time() - started) > 5 * 60:
375
+ try:
376
+ s.remove("wavescale/headless_in_progress")
377
+ s.remove("wavescale/headless_started_at")
378
+ except Exception:
379
+ pass
380
+ return False
381
+
382
+ return True
383
+
384
+ def showEvent(self, e):
385
+ super().showEvent(e)
386
+ if not self._bypass_guard and self._headless_guard_active():
387
+ # Soft warning instead of rejecting the dialog
388
+ try:
389
+ QMessageBox.information(
390
+ self, self.tr("WaveScale HDR"),
391
+ self.tr("A headless HDR run appears to be in progress. "
392
+ "This window will remain open; you can still preview safely.")
393
+ )
394
+ except Exception:
395
+ pass
396
+
397
+ def exec(self) -> int:
398
+ if not self._bypass_guard and self._headless_guard_active():
399
+ return 0
400
+ return super().exec()
401
+
402
+ def _get_doc_active_mask_2d(self) -> np.ndarray | None:
403
+ """
404
+ Return the document's active mask as a 2-D float32 in [0..1],
405
+ resized to the current image size. If none, return None.
406
+ """
407
+ doc = getattr(self, "_doc", None)
408
+ if doc is None:
409
+ return None
410
+
411
+ mid = getattr(doc, "active_mask_id", None)
412
+ if not mid:
413
+ return None
414
+
415
+ masks = getattr(doc, "masks", {}) or {}
416
+ layer = masks.get(mid)
417
+ if layer is None:
418
+ return None
419
+
420
+ # Safely pick the first non-None payload without using boolean 'or'
421
+ data = None
422
+ # object with attributes
423
+ for attr in ("data", "mask", "image", "array"):
424
+ if hasattr(layer, attr):
425
+ val = getattr(layer, attr)
426
+ if val is not None:
427
+ data = val
428
+ break
429
+ # plain ndarray?
430
+ if data is None and isinstance(layer, np.ndarray):
431
+ data = layer
432
+ # dict-like layer?
433
+ if data is None and isinstance(layer, dict):
434
+ for key in ("data", "mask", "image", "array"):
435
+ if key in layer and layer[key] is not None:
436
+ data = layer[key]
437
+ break
438
+
439
+ if data is None:
440
+ return None
441
+
442
+ m = np.asarray(data)
443
+
444
+ # collapse RGB/alpha to gray if needed
445
+ if m.ndim == 3:
446
+ m = m.mean(axis=2)
447
+
448
+ m = m.astype(np.float32, copy=False)
449
+ # normalize to [0,1] if it looks like 0..255 or 0..65535
450
+ if m.dtype.kind in "ui":
451
+ m /= float(np.iinfo(m.dtype).max)
452
+ else:
453
+ mx = float(m.max()) if m.size else 1.0
454
+ if mx > 1.0:
455
+ m /= mx
456
+ m = np.clip(m, 0.0, 1.0)
457
+
458
+ # resize to current image size (nearest)
459
+ H, W = self.original_rgb.shape[:2]
460
+ if m.shape != (H, W):
461
+ yi = (np.linspace(0, m.shape[0] - 1, H)).astype(np.int32)
462
+ xi = (np.linspace(0, m.shape[1] - 1, W)).astype(np.int32)
463
+ m = m[yi][:, xi]
464
+
465
+ return m
466
+
467
+
468
+ def _combine_with_doc_mask(self, hdr_mask: np.ndarray) -> np.ndarray:
469
+ """
470
+ Multiply the HDR luminance mask by the document active mask (if any).
471
+ Shapes are matched to image size.
472
+ """
473
+ m_doc = self._get_doc_active_mask_2d()
474
+ if m_doc is None:
475
+ return hdr_mask
476
+ # both are already (H, W) float32 in [0..1]
477
+ return np.clip(hdr_mask * m_doc, 0.0, 1.0)
478
+
479
+
480
+ def _set_pix(self, rgb: np.ndarray):
481
+ arr = (np.clip(rgb, 0, 1) * 255).astype(np.uint8)
482
+ h, w, _ = arr.shape
483
+ q = QImage(arr.data, w, h, 3*w, QImage.Format.Format_RGB888)
484
+ self.pix.setPixmap(QPixmap.fromImage(q))
485
+ self.view.setSceneRect(self.pix.boundingRect())
486
+
487
+ def _toggle(self):
488
+ if self.btn_toggle.isChecked():
489
+ self.btn_toggle.setText(self.tr("Show Preview"))
490
+ self._set_pix(self.original_rgb)
491
+ else:
492
+ self.btn_toggle.setText(self.tr("Show Original"))
493
+ self._set_pix(self.preview_rgb)
494
+
495
+ def _reset(self):
496
+ self.s_scales.setValue(5)
497
+ self.s_comp.setValue(150)
498
+ self.s_gamma.setValue(500)
499
+ self.preview_rgb = self.original_rgb.copy()
500
+ self._set_pix(self.preview_rgb)
501
+ self.lbl_step.setText(self.tr("Idle")); self.bar.setValue(0)
502
+ self.btn_apply.setEnabled(False)
503
+ self.btn_toggle.setChecked(False); self.btn_toggle.setText(self.tr("Show Original"))
504
+
505
+ def _start_preview(self):
506
+ self.btn_preview.setEnabled(False); self.btn_apply.setEnabled(False)
507
+ n_scales = int(self.s_scales.value())
508
+ comp = float(self.s_comp.value()) / 100.0
509
+ mgamma = float(self.s_gamma.value()) / 100.0
510
+
511
+ self.thread = QThread(self)
512
+ self.worker = HDRWorker(self.original_rgb, n_scales, comp, mgamma, self.base_kernel)
513
+ self.worker.moveToThread(self.thread)
514
+ self.thread.started.connect(self.worker.run)
515
+ self.worker.progress_update.connect(self._on_progress)
516
+ self.worker.finished.connect(self._on_finished)
517
+ self.worker.finished.connect(self.thread.quit)
518
+ self.worker.finished.connect(self.worker.deleteLater)
519
+ self.thread.finished.connect(self.thread.deleteLater)
520
+ self.thread.start()
521
+
522
+ def _on_progress(self, step: str, pct: int):
523
+ self.lbl_step.setText(step); self.bar.setValue(pct)
524
+
525
+ def _on_finished(self, transformed_rgb: np.ndarray, mask: np.ndarray):
526
+ self.btn_preview.setEnabled(True)
527
+ if transformed_rgb is None:
528
+ QMessageBox.critical(self, self.tr("WaveScale HDR"), self.tr("Processing failed."))
529
+ return
530
+
531
+ # ← NEW: combine HDR's luminance mask with the doc's active mask (if present)
532
+ mask_comb = self._combine_with_doc_mask(mask)
533
+
534
+ # blend preview: original*(1-mask) + transformed*mask
535
+ m3 = np.repeat(mask_comb[..., None], 3, axis=2)
536
+ self.preview_rgb = self.original_rgb * (1.0 - m3) + transformed_rgb * m3
537
+ self._set_pix(self.preview_rgb)
538
+
539
+ # show the *combined* mask in the little window
540
+ self.mask_win.setWindowTitle(
541
+ self.tr("HDR Mask (L × Active Mask)") if self._get_doc_active_mask_2d() is not None else self.tr("HDR Mask (L-based)")
542
+ )
543
+ self.mask_win.update_mask(mask_comb)
544
+
545
+ self.btn_apply.setEnabled(True)
546
+ self.btn_toggle.setChecked(False); self.btn_toggle.setText(self.tr("Show Original"))
547
+ self.lbl_step.setText(self.tr("Preview ready")); self.bar.setValue(100)
548
+ # Headless: apply immediately (exactly like clicking "Apply to Document")
549
+ if self._headless:
550
+ QTimer.singleShot(0, self._apply_to_doc)
551
+
552
+ def _apply_to_doc(self):
553
+ out = self.preview_rgb
554
+ if self._was_mono:
555
+ # collapse back to mono (keep original shape: 2D or H×W×1)
556
+ mono = np.mean(out, axis=2, dtype=np.float32)
557
+ if self._mono_shape and len(self._mono_shape) == 3 and self._mono_shape[2] == 1:
558
+ mono = mono[:, :, None]
559
+ out = mono
560
+
561
+ out = np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
562
+ try:
563
+ if hasattr(self._doc, "set_image"):
564
+ self._doc.set_image(out, step_name="WaveScale HDR")
565
+ elif hasattr(self._doc, "apply_numpy"):
566
+ self._doc.apply_numpy(out, step_name="WaveScale HDR")
567
+ else:
568
+ self._doc.image = out
569
+ except Exception as e:
570
+ QMessageBox.critical(self, self.tr("WaveScale HDR"), self.tr("Failed to write to document:\n{0}").format(e))
571
+ return
572
+
573
+ # ── Build preset from current sliders ─────────────────────────
574
+ try:
575
+ preset = {
576
+ "n_scales": int(self.s_scales.value()),
577
+ "compression_factor": float(self.s_comp.value()) / 100.0,
578
+ "mask_gamma": float(self.s_gamma.value()) / 100.0,
579
+ }
580
+ except Exception:
581
+ preset = {}
582
+
583
+ # ── Register as last_headless_command on the main window ─────
584
+ try:
585
+ main = self.parent()
586
+ if main is not None:
587
+ payload = {
588
+ "command_id": "wavescale_hdr",
589
+ "preset": dict(preset),
590
+ }
591
+ setattr(main, "_last_headless_command", payload)
592
+
593
+ # Optional debug log (mirrors other tools)
594
+ try:
595
+ if hasattr(main, "_log"):
596
+ ns = int(preset.get("n_scales", 5))
597
+ comp = float(preset.get("compression_factor", 1.5))
598
+ mg = float(preset.get("mask_gamma", 5.0))
599
+ main._log(
600
+ f"[Replay] Registered WaveScale HDR as last action "
601
+ f"(n_scales={ns}, compression={comp:.2f}, mask_gamma={mg:.2f})"
602
+ )
603
+ except Exception:
604
+ pass
605
+ except Exception:
606
+ # never let replay wiring break the apply
607
+ pass
608
+
609
+ # ── (optional) keep emitting signal if you want it elsewhere ──
610
+ try:
611
+ self.applied_preset.emit(self._doc, preset)
612
+ except Exception:
613
+ pass
614
+
615
+ # Dialog stays open so user can apply to other images
616
+ # Refresh document reference for next operation
617
+ self._refresh_document_from_active()
618
+
619
+ def _refresh_document_from_active(self):
620
+ """
621
+ Refresh the dialog's document reference to the currently active document.
622
+ This allows reusing the same dialog on different images.
623
+ """
624
+ try:
625
+ main = self.parent()
626
+ if main and hasattr(main, "_active_doc"):
627
+ new_doc = main._active_doc()
628
+ if new_doc is not None and new_doc is not self._doc:
629
+ self._doc = new_doc
630
+ # Reset L channel and refresh preview for new document
631
+ self._L_original = None
632
+ self._last_preview = None
633
+ except Exception:
634
+ pass
635
+
636
+
637
+ def _schedule_mask_refresh(self, _value):
638
+ # debounce to ~0.25s
639
+ self._mask_timer.start(250)
640
+
641
+ def _update_mask_from_gamma(self):
642
+ gamma = float(self.s_gamma.value()) / 100.0
643
+ hdr_mask = _mask_from_L(self._L_original, gamma=gamma)
644
+ mask_comb = self._combine_with_doc_mask(hdr_mask)
645
+ self.mask_win.setWindowTitle(
646
+ self.tr("HDR Mask (L × Active Mask)") if self._get_doc_active_mask_2d() is not None else self.tr("HDR Mask (L-based)")
647
+ )
648
+ self.mask_win.update_mask(mask_comb)