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,645 @@
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
+
206
+ self._doc = doc
207
+ base = getattr(doc, "image", None)
208
+ if base is None:
209
+ raise RuntimeError("Active document has no image.")
210
+
211
+ # normalize to float32 [0..1] RGB for processing/preview
212
+ img = np.asarray(base, dtype=np.float32)
213
+ if img.ndim == 2:
214
+ img_rgb = np.repeat(img[:, :, None], 3, axis=2)
215
+ self._was_mono = True
216
+ self._mono_shape = img.shape
217
+ elif img.ndim == 3 and img.shape[2] == 1:
218
+ img_rgb = np.repeat(img, 3, axis=2)
219
+ self._was_mono = True
220
+ self._mono_shape = img.shape
221
+ else:
222
+ img_rgb = img[:, :, :3]
223
+ self._was_mono = False
224
+ self._mono_shape = None
225
+
226
+ if img.dtype.kind in "ui":
227
+ maxv = float(np.nanmax(img_rgb)) or 1.0
228
+ img_rgb = img_rgb / max(1.0, maxv)
229
+ img_rgb = np.clip(img_rgb, 0.0, 1.0).astype(np.float32, copy=False)
230
+
231
+ self.original_rgb = img_rgb
232
+ self.preview_rgb = img_rgb.copy()
233
+
234
+ # scene/view (⚠️ use ZoomableGraphicsView)
235
+ self.scene = QGraphicsScene(self)
236
+ self.view = ZoomableGraphicsView(self.scene, self)
237
+ self.view.setAlignment(Qt.AlignmentFlag.AlignCenter)
238
+ self.pix = QGraphicsPixmapItem()
239
+ self.scene.addItem(self.pix)
240
+
241
+ # optional: keep your scroll area wrapper
242
+ self.scroll = QScrollArea(self)
243
+ self.scroll.setWidgetResizable(True)
244
+ self.scroll.setWidget(self.view)
245
+
246
+ # controls (add zoom row)
247
+ self.grp = QGroupBox(self.tr("HDR Controls"))
248
+ form = QFormLayout(self.grp)
249
+
250
+ self.s_scales = QSlider(Qt.Orientation.Horizontal); self.s_scales.setRange(2, 10); self.s_scales.setValue(5)
251
+ self.s_comp = QSlider(Qt.Orientation.Horizontal); self.s_comp.setRange(10, 500); self.s_comp.setValue(150)
252
+ self.s_gamma = QSlider(Qt.Orientation.Horizontal); self.s_gamma.setRange(10, 1000); self.s_gamma.setValue(500)
253
+
254
+ form.addRow(self.tr("Number of Scales:"), self.s_scales)
255
+ form.addRow(self.tr("Coarse Compression:"), self.s_comp)
256
+ form.addRow(self.tr("Mask Gamma:"), self.s_gamma)
257
+
258
+ row = QHBoxLayout()
259
+ self.btn_preview = QPushButton(self.tr("Preview"))
260
+ self.btn_toggle = QPushButton(self.tr("Show Original")); self.btn_toggle.setCheckable(True)
261
+ row.addWidget(self.btn_preview); row.addWidget(self.btn_toggle)
262
+ form.addRow(row)
263
+
264
+ # ↓ NEW: zoom controls
265
+ zoom_row = QHBoxLayout()
266
+ self.btn_zoom_in = QPushButton(self.tr("Zoom In"))
267
+ self.btn_zoom_out = QPushButton(self.tr("Zoom Out"))
268
+ self.btn_fit = QPushButton(self.tr("Fit to Preview"))
269
+ zoom_row.addWidget(self.btn_zoom_in)
270
+ zoom_row.addWidget(self.btn_zoom_out)
271
+ zoom_row.addWidget(self.btn_fit)
272
+ form.addRow(zoom_row)
273
+
274
+ # progress group (unchanged)
275
+ self.prog_grp = QGroupBox(self.tr("Processing Progress"))
276
+ vprog = QVBoxLayout(self.prog_grp)
277
+ self.lbl_step = QLabel(self.tr("Idle"))
278
+ self.bar = QProgressBar(); self.bar.setRange(0, 100); self.bar.setValue(0)
279
+ vprog.addWidget(self.lbl_step); vprog.addWidget(self.bar)
280
+
281
+ # bottom buttons (unchanged)
282
+ bot = QHBoxLayout()
283
+ self.btn_apply = QPushButton(self.tr("Apply to Document")); self.btn_apply.setEnabled(False)
284
+ self.btn_reset = QPushButton(self.tr("Reset"))
285
+ self.btn_close = QPushButton(self.tr("Close"))
286
+ bot.addStretch(1); bot.addWidget(self.btn_apply); bot.addWidget(self.btn_reset); bot.addWidget(self.btn_close)
287
+
288
+ # layout (unchanged)
289
+ main = QVBoxLayout(self)
290
+ main.addWidget(self.scroll)
291
+ h = QHBoxLayout()
292
+ h.addWidget(self.grp, 3)
293
+ h.addWidget(self.prog_grp, 1)
294
+ main.addLayout(h)
295
+ main.addLayout(bot)
296
+
297
+ # mask window
298
+ self.mask_win = MaskDisplayWindow(self)
299
+ if not self._headless:
300
+ self.mask_win.show()
301
+
302
+
303
+ # kernel
304
+ self.base_kernel = _B3
305
+
306
+ # connections
307
+ self.btn_preview.clicked.connect(self._start_preview)
308
+ self.btn_apply.clicked.connect(self._apply_to_doc)
309
+ self.btn_close.clicked.connect(self.reject)
310
+ self.btn_reset.clicked.connect(self._reset)
311
+ self.btn_toggle.clicked.connect(self._toggle)
312
+
313
+ self.btn_zoom_in.clicked.connect(self.view.zoom_in)
314
+ self.btn_zoom_out.clicked.connect(self.view.zoom_out)
315
+ self.btn_fit.clicked.connect(lambda: self.view.fit_item(self.pix))
316
+
317
+ # ── Mask shown immediately ───────────────────────────────────────────
318
+ # Precompute L from original and push initial mask to the small window
319
+ self._lab_original = _rgb_to_lab(self.original_rgb)
320
+ self._L_original = self._lab_original[..., 0].astype(np.float32, copy=True)
321
+ self._mask_timer = QTimer(self)
322
+ self._mask_timer.setSingleShot(True)
323
+ self._mask_timer.timeout.connect(self._update_mask_from_gamma)
324
+ self.s_gamma.valueChanged.connect(self._schedule_mask_refresh)
325
+
326
+ # show initial mask right away
327
+ self._update_mask_from_gamma()
328
+
329
+ # initial pix
330
+ self._set_pix(self.preview_rgb)
331
+
332
+ def apply_preset(self, p: dict):
333
+ # sliders are integer; map floats to their scales
334
+ ns = int(p.get("n_scales", 5))
335
+ comp = float(p.get("compression_factor", 1.5))
336
+ mg = float(p.get("mask_gamma", 5.0)) # dialog default is 5.0 (slider 500)
337
+ # clamp safely
338
+ ns = max(2, min(10, ns))
339
+ comp_i = int(max(10, min(500, round(comp*100)))) # 1.0..5.0 -> 100..500
340
+ mg_i = int(max(10, min(1000, round(mg*100)))) # 0.1..10.0 -> 10..1000
341
+ self.s_scales.setValue(ns)
342
+ self.s_comp.setValue(comp_i)
343
+ self.s_gamma.setValue(mg_i)
344
+ # refresh mask preview (even if window is hidden)
345
+ self._update_mask_from_gamma()
346
+
347
+ def _headless_guard_active(self) -> bool:
348
+ """Only guard true concurrent *headless* runs; ignore stale locks."""
349
+ # If we are not launching headless, never block the interactive UI.
350
+ if not self._headless:
351
+ return False
352
+
353
+ # Parent flags
354
+ p = self.parent()
355
+ if p and (getattr(p, "_wavescale_guard", False) or getattr(p, "_wavescale_headless_running", False)):
356
+ return True
357
+
358
+ # Settings lock with TTL
359
+ try:
360
+ s = QSettings()
361
+ in_prog = bool(s.value("wavescale/headless_in_progress", False))
362
+ started = float(s.value("wavescale/headless_started_at", 0.0))
363
+ except Exception:
364
+ in_prog, started = False, 0.0
365
+
366
+ if not in_prog:
367
+ return False
368
+
369
+ # consider anything older than 5 minutes stale
370
+ import time
371
+ if (time.time() - started) > 5 * 60:
372
+ try:
373
+ s.remove("wavescale/headless_in_progress")
374
+ s.remove("wavescale/headless_started_at")
375
+ except Exception:
376
+ pass
377
+ return False
378
+
379
+ return True
380
+
381
+ def showEvent(self, e):
382
+ super().showEvent(e)
383
+ if not self._bypass_guard and self._headless_guard_active():
384
+ # Soft warning instead of rejecting the dialog
385
+ try:
386
+ QMessageBox.information(
387
+ self, self.tr("WaveScale HDR"),
388
+ self.tr("A headless HDR run appears to be in progress. "
389
+ "This window will remain open; you can still preview safely.")
390
+ )
391
+ except Exception:
392
+ pass
393
+
394
+ def exec(self) -> int:
395
+ if not self._bypass_guard and self._headless_guard_active():
396
+ return 0
397
+ return super().exec()
398
+
399
+ def _get_doc_active_mask_2d(self) -> np.ndarray | None:
400
+ """
401
+ Return the document's active mask as a 2-D float32 in [0..1],
402
+ resized to the current image size. If none, return None.
403
+ """
404
+ doc = getattr(self, "_doc", None)
405
+ if doc is None:
406
+ return None
407
+
408
+ mid = getattr(doc, "active_mask_id", None)
409
+ if not mid:
410
+ return None
411
+
412
+ masks = getattr(doc, "masks", {}) or {}
413
+ layer = masks.get(mid)
414
+ if layer is None:
415
+ return None
416
+
417
+ # Safely pick the first non-None payload without using boolean 'or'
418
+ data = None
419
+ # object with attributes
420
+ for attr in ("data", "mask", "image", "array"):
421
+ if hasattr(layer, attr):
422
+ val = getattr(layer, attr)
423
+ if val is not None:
424
+ data = val
425
+ break
426
+ # plain ndarray?
427
+ if data is None and isinstance(layer, np.ndarray):
428
+ data = layer
429
+ # dict-like layer?
430
+ if data is None and isinstance(layer, dict):
431
+ for key in ("data", "mask", "image", "array"):
432
+ if key in layer and layer[key] is not None:
433
+ data = layer[key]
434
+ break
435
+
436
+ if data is None:
437
+ return None
438
+
439
+ m = np.asarray(data)
440
+
441
+ # collapse RGB/alpha to gray if needed
442
+ if m.ndim == 3:
443
+ m = m.mean(axis=2)
444
+
445
+ m = m.astype(np.float32, copy=False)
446
+ # normalize to [0,1] if it looks like 0..255 or 0..65535
447
+ if m.dtype.kind in "ui":
448
+ m /= float(np.iinfo(m.dtype).max)
449
+ else:
450
+ mx = float(m.max()) if m.size else 1.0
451
+ if mx > 1.0:
452
+ m /= mx
453
+ m = np.clip(m, 0.0, 1.0)
454
+
455
+ # resize to current image size (nearest)
456
+ H, W = self.original_rgb.shape[:2]
457
+ if m.shape != (H, W):
458
+ yi = (np.linspace(0, m.shape[0] - 1, H)).astype(np.int32)
459
+ xi = (np.linspace(0, m.shape[1] - 1, W)).astype(np.int32)
460
+ m = m[yi][:, xi]
461
+
462
+ return m
463
+
464
+
465
+ def _combine_with_doc_mask(self, hdr_mask: np.ndarray) -> np.ndarray:
466
+ """
467
+ Multiply the HDR luminance mask by the document active mask (if any).
468
+ Shapes are matched to image size.
469
+ """
470
+ m_doc = self._get_doc_active_mask_2d()
471
+ if m_doc is None:
472
+ return hdr_mask
473
+ # both are already (H, W) float32 in [0..1]
474
+ return np.clip(hdr_mask * m_doc, 0.0, 1.0)
475
+
476
+
477
+ def _set_pix(self, rgb: np.ndarray):
478
+ arr = (np.clip(rgb, 0, 1) * 255).astype(np.uint8)
479
+ h, w, _ = arr.shape
480
+ q = QImage(arr.data, w, h, 3*w, QImage.Format.Format_RGB888)
481
+ self.pix.setPixmap(QPixmap.fromImage(q))
482
+ self.view.setSceneRect(self.pix.boundingRect())
483
+
484
+ def _toggle(self):
485
+ if self.btn_toggle.isChecked():
486
+ self.btn_toggle.setText(self.tr("Show Preview"))
487
+ self._set_pix(self.original_rgb)
488
+ else:
489
+ self.btn_toggle.setText(self.tr("Show Original"))
490
+ self._set_pix(self.preview_rgb)
491
+
492
+ def _reset(self):
493
+ self.s_scales.setValue(5)
494
+ self.s_comp.setValue(150)
495
+ self.s_gamma.setValue(500)
496
+ self.preview_rgb = self.original_rgb.copy()
497
+ self._set_pix(self.preview_rgb)
498
+ self.lbl_step.setText(self.tr("Idle")); self.bar.setValue(0)
499
+ self.btn_apply.setEnabled(False)
500
+ self.btn_toggle.setChecked(False); self.btn_toggle.setText(self.tr("Show Original"))
501
+
502
+ def _start_preview(self):
503
+ self.btn_preview.setEnabled(False); self.btn_apply.setEnabled(False)
504
+ n_scales = int(self.s_scales.value())
505
+ comp = float(self.s_comp.value()) / 100.0
506
+ mgamma = float(self.s_gamma.value()) / 100.0
507
+
508
+ self.thread = QThread(self)
509
+ self.worker = HDRWorker(self.original_rgb, n_scales, comp, mgamma, self.base_kernel)
510
+ self.worker.moveToThread(self.thread)
511
+ self.thread.started.connect(self.worker.run)
512
+ self.worker.progress_update.connect(self._on_progress)
513
+ self.worker.finished.connect(self._on_finished)
514
+ self.worker.finished.connect(self.thread.quit)
515
+ self.worker.finished.connect(self.worker.deleteLater)
516
+ self.thread.finished.connect(self.thread.deleteLater)
517
+ self.thread.start()
518
+
519
+ def _on_progress(self, step: str, pct: int):
520
+ self.lbl_step.setText(step); self.bar.setValue(pct)
521
+
522
+ def _on_finished(self, transformed_rgb: np.ndarray, mask: np.ndarray):
523
+ self.btn_preview.setEnabled(True)
524
+ if transformed_rgb is None:
525
+ QMessageBox.critical(self, self.tr("WaveScale HDR"), self.tr("Processing failed."))
526
+ return
527
+
528
+ # ← NEW: combine HDR's luminance mask with the doc's active mask (if present)
529
+ mask_comb = self._combine_with_doc_mask(mask)
530
+
531
+ # blend preview: original*(1-mask) + transformed*mask
532
+ m3 = np.repeat(mask_comb[..., None], 3, axis=2)
533
+ self.preview_rgb = self.original_rgb * (1.0 - m3) + transformed_rgb * m3
534
+ self._set_pix(self.preview_rgb)
535
+
536
+ # show the *combined* mask in the little window
537
+ self.mask_win.setWindowTitle(
538
+ self.tr("HDR Mask (L × Active Mask)") if self._get_doc_active_mask_2d() is not None else self.tr("HDR Mask (L-based)")
539
+ )
540
+ self.mask_win.update_mask(mask_comb)
541
+
542
+ self.btn_apply.setEnabled(True)
543
+ self.btn_toggle.setChecked(False); self.btn_toggle.setText(self.tr("Show Original"))
544
+ self.lbl_step.setText(self.tr("Preview ready")); self.bar.setValue(100)
545
+ # Headless: apply immediately (exactly like clicking "Apply to Document")
546
+ if self._headless:
547
+ QTimer.singleShot(0, self._apply_to_doc)
548
+
549
+ def _apply_to_doc(self):
550
+ out = self.preview_rgb
551
+ if self._was_mono:
552
+ # collapse back to mono (keep original shape: 2D or H×W×1)
553
+ mono = np.mean(out, axis=2, dtype=np.float32)
554
+ if self._mono_shape and len(self._mono_shape) == 3 and self._mono_shape[2] == 1:
555
+ mono = mono[:, :, None]
556
+ out = mono
557
+
558
+ out = np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
559
+ try:
560
+ if hasattr(self._doc, "set_image"):
561
+ self._doc.set_image(out, step_name="WaveScale HDR")
562
+ elif hasattr(self._doc, "apply_numpy"):
563
+ self._doc.apply_numpy(out, step_name="WaveScale HDR")
564
+ else:
565
+ self._doc.image = out
566
+ except Exception as e:
567
+ QMessageBox.critical(self, self.tr("WaveScale HDR"), self.tr("Failed to write to document:\n{0}").format(e))
568
+ return
569
+
570
+ # ── Build preset from current sliders ─────────────────────────
571
+ try:
572
+ preset = {
573
+ "n_scales": int(self.s_scales.value()),
574
+ "compression_factor": float(self.s_comp.value()) / 100.0,
575
+ "mask_gamma": float(self.s_gamma.value()) / 100.0,
576
+ }
577
+ except Exception:
578
+ preset = {}
579
+
580
+ # ── Register as last_headless_command on the main window ─────
581
+ try:
582
+ main = self.parent()
583
+ if main is not None:
584
+ payload = {
585
+ "command_id": "wavescale_hdr",
586
+ "preset": dict(preset),
587
+ }
588
+ setattr(main, "_last_headless_command", payload)
589
+
590
+ # Optional debug log (mirrors other tools)
591
+ try:
592
+ if hasattr(main, "_log"):
593
+ ns = int(preset.get("n_scales", 5))
594
+ comp = float(preset.get("compression_factor", 1.5))
595
+ mg = float(preset.get("mask_gamma", 5.0))
596
+ main._log(
597
+ f"[Replay] Registered WaveScale HDR as last action "
598
+ f"(n_scales={ns}, compression={comp:.2f}, mask_gamma={mg:.2f})"
599
+ )
600
+ except Exception:
601
+ pass
602
+ except Exception:
603
+ # never let replay wiring break the apply
604
+ pass
605
+
606
+ # ── (optional) keep emitting signal if you want it elsewhere ──
607
+ try:
608
+ self.applied_preset.emit(self._doc, preset)
609
+ except Exception:
610
+ pass
611
+
612
+ # Dialog stays open so user can apply to other images
613
+ # Refresh document reference for next operation
614
+ self._refresh_document_from_active()
615
+
616
+ def _refresh_document_from_active(self):
617
+ """
618
+ Refresh the dialog's document reference to the currently active document.
619
+ This allows reusing the same dialog on different images.
620
+ """
621
+ try:
622
+ main = self.parent()
623
+ if main and hasattr(main, "_active_doc"):
624
+ new_doc = main._active_doc()
625
+ if new_doc is not None and new_doc is not self._doc:
626
+ self._doc = new_doc
627
+ # Reset L channel and refresh preview for new document
628
+ self._L_original = None
629
+ self._last_preview = None
630
+ except Exception:
631
+ pass
632
+
633
+
634
+ def _schedule_mask_refresh(self, _value):
635
+ # debounce to ~0.25s
636
+ self._mask_timer.start(250)
637
+
638
+ def _update_mask_from_gamma(self):
639
+ gamma = float(self.s_gamma.value()) / 100.0
640
+ hdr_mask = _mask_from_L(self._L_original, gamma=gamma)
641
+ mask_comb = self._combine_with_doc_mask(hdr_mask)
642
+ self.mask_win.setWindowTitle(
643
+ self.tr("HDR Mask (L × Active Mask)") if self._get_doc_active_mask_2d() is not None else self.tr("HDR Mask (L-based)")
644
+ )
645
+ self.mask_win.update_mask(mask_comb)