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,683 @@
1
+ # pro/wavescalede.py
2
+ from __future__ import annotations
3
+ import math
4
+ import numpy as np
5
+
6
+ from PyQt6.QtCore import Qt, QObject, pyqtSignal, QThread, QTimer
7
+ from PyQt6.QtGui import QImage, QPixmap, QIcon, QWheelEvent
8
+ from PyQt6.QtWidgets import (
9
+ QDialog, QVBoxLayout, QHBoxLayout, QGroupBox, QFormLayout, QLabel, QPushButton,
10
+ QSlider, QGraphicsView, QGraphicsScene, QGraphicsPixmapItem, QScrollArea,
11
+ QMessageBox, QProgressBar, QMainWindow
12
+ )
13
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
14
+
15
+ # Import shared wavelet utilities
16
+ from setiastro.saspro.widgets.wavelet_utils import (
17
+ conv_sep_reflect as _conv_sep_reflect,
18
+ build_spaced_kernel as _build_spaced_kernel,
19
+ atrous_decompose as _atrous_decompose,
20
+ atrous_reconstruct as _atrous_reconstruct,
21
+ rgb_to_lab as _rgb_to_lab,
22
+ lab_to_rgb as _lab_to_rgb,
23
+ gauss_blur as _gauss_blur,
24
+ B3_KERNEL as _B3,
25
+ )
26
+
27
+ # ─────────────────────────────────────────────────────────────────────────────
28
+ # Core math (shared)
29
+ # ─────────────────────────────────────────────────────────────────────────────
30
+
31
+ def _resize_mask_nn(mask2d: np.ndarray, target_hw: tuple[int, int]) -> np.ndarray:
32
+ H, W = target_hw
33
+ if mask2d.shape == (H, W):
34
+ return mask2d.astype(np.float32, copy=False)
35
+ yi = (np.linspace(0, mask2d.shape[0] - 1, H)).astype(np.int32)
36
+ xi = (np.linspace(0, mask2d.shape[1] - 1, W)).astype(np.int32)
37
+ return mask2d[yi][:, xi].astype(np.float32, copy=False)
38
+
39
+ # Darkness mask (scales 2–4, negative parts, mean → normalize → gamma → smooth → mild S-curve)
40
+ def _darkness_mask(L: np.ndarray, n_scales: int, base_k: np.ndarray, gamma: float) -> np.ndarray:
41
+ planes = _atrous_decompose(L, n_scales, base_k)
42
+ # mid-scales: 1:4 (0-based → skip 0)
43
+ sel = planes[1:4]
44
+ neg = [np.clip(-p, 0, None) for p in sel]
45
+ if len(neg) == 0:
46
+ m = np.zeros_like(L, dtype=np.float32)
47
+ else:
48
+ combined = np.mean(neg, axis=0).astype(np.float32)
49
+ denom = float(np.max(combined) + 1e-8)
50
+ m = combined / denom
51
+ if gamma != 1.0:
52
+ m = np.power(m, float(gamma), dtype=np.float32)
53
+ m = _gauss_blur(m, sigma=3.0).astype(np.float32)
54
+ # gentle brighten of mids
55
+ m = np.clip(1.5 * m - 0.5 * (m * m), 0.0, 1.0).astype(np.float32)
56
+ return m
57
+
58
+ # Main compute (mono or RGB)
59
+ def compute_wavescale_dse(image: np.ndarray,
60
+ n_scales: int = 6,
61
+ boost_factor: float = 5.0,
62
+ mask_gamma: float = 1.0,
63
+ iterations: int = 2,
64
+ base_kernel: np.ndarray = _B3,
65
+ decay_rate: float = 0.5,
66
+ external_mask: np.ndarray | None = None # ← NEW
67
+ ) -> tuple[np.ndarray, np.ndarray]:
68
+ """
69
+ WaveScale Dark Enhancer.
70
+ Returns (output_image, darkness_mask_used).
71
+ If external_mask is provided (2-D [0..1]), it will be multiplied into the darkness mask.
72
+ """
73
+ arr = np.asarray(image, dtype=np.float32)
74
+
75
+ # normalize external mask now
76
+ ext = None
77
+ if external_mask is not None:
78
+ m = np.asarray(external_mask)
79
+ if m.ndim == 3: # collapse RGB(A)
80
+ m = m.mean(axis=2)
81
+ m = np.clip(m.astype(np.float32), 0.0, 1.0)
82
+ ext = _resize_mask_nn(m, arr.shape[:2])
83
+
84
+ if arr.ndim == 2 or (arr.ndim == 3 and arr.shape[2] == 1):
85
+ L = arr.squeeze().astype(np.float32, copy=True) # [0..1]
86
+ mask = np.zeros_like(L, dtype=np.float32) # define for return
87
+ for it in range(int(iterations)):
88
+ mask = _darkness_mask(L, n_scales, base_kernel, mask_gamma)
89
+ if ext is not None:
90
+ mask = np.clip(mask * ext, 0.0, 1.0) # ← combine here
91
+
92
+ planes = _atrous_decompose(L, n_scales, base_kernel)
93
+ residual = planes.pop()
94
+ for i in range(len(planes)):
95
+ if i == 0:
96
+ continue # skip highest frequency
97
+ decay = decay_rate ** i
98
+ neg = np.clip(-planes[i], 0, None)
99
+ enhancement = neg * mask * (boost_factor - 1.0) * decay
100
+ planes[i] = planes[i] - enhancement
101
+ L = np.clip(_atrous_reconstruct(planes + [residual]), 0.0, 1.0)
102
+
103
+ out = L.astype(np.float32, copy=False)
104
+ return out, mask.astype(np.float32, copy=False)
105
+
106
+ # RGB path
107
+ rgb = np.clip(arr[:, :, :3], 0.0, 1.0).astype(np.float32, copy=False)
108
+ lab = _rgb_to_lab(rgb)
109
+ L = lab[..., 0].astype(np.float32, copy=True)
110
+ mask = np.zeros(L.shape, dtype=np.float32) # define for return
111
+ for it in range(int(iterations)):
112
+ mask = _darkness_mask(np.clip(L / 100.0, 0.0, 1.0), n_scales, base_kernel, mask_gamma)
113
+ if ext is not None:
114
+ mask = np.clip(mask * ext, 0.0, 1.0) # ← combine here
115
+
116
+ planes = _atrous_decompose(L, n_scales, base_kernel)
117
+ residual = planes.pop()
118
+ for i in range(len(planes)):
119
+ if i == 0:
120
+ continue
121
+ decay = decay_rate ** i
122
+ neg = np.clip(-planes[i], 0, None)
123
+ enhancement = neg * mask * (boost_factor - 1.0) * decay
124
+ planes[i] = planes[i] - enhancement
125
+ L = np.clip(_atrous_reconstruct(planes + [residual]), 0.0, 100.0)
126
+
127
+ lab[..., 0] = L
128
+ out_rgb = _lab_to_rgb(lab)
129
+ return out_rgb.astype(np.float32, copy=False), mask.astype(np.float32, copy=False)
130
+
131
+ # ─────────────────────────────────────────────────────────────────────────────
132
+ # Worker
133
+ # ─────────────────────────────────────────────────────────────────────────────
134
+ class DSEWorker(QObject):
135
+ progress_update = pyqtSignal(str, int)
136
+ finished = pyqtSignal(np.ndarray, np.ndarray) # (output, mask)
137
+
138
+ def __init__(self, image: np.ndarray, n_scales: int, boost: float, gamma: float,
139
+ base_kernel: np.ndarray, iterations: int,
140
+ external_mask: np.ndarray | None = None):
141
+ super().__init__()
142
+ self.image = image
143
+ self.n_scales = n_scales
144
+ self.boost = boost
145
+ self.gamma = gamma
146
+ self.base_kernel = base_kernel
147
+ self.iterations = iterations
148
+ self.external_mask = external_mask
149
+
150
+ def run(self):
151
+ try:
152
+ self.progress_update.emit(self.tr("Analyzing dark structure…"), 20)
153
+ out, mask = compute_wavescale_dse(
154
+ self.image, self.n_scales, self.boost, self.gamma,
155
+ self.iterations, self.base_kernel,
156
+ external_mask=self.external_mask # ← NEW
157
+ )
158
+ self.progress_update.emit(self.tr("Finalizing…"), 95)
159
+ self.finished.emit(out, mask)
160
+ except Exception as e:
161
+ print("WaveScale DSE error:", e)
162
+ self.finished.emit(None, None)
163
+
164
+ # ─────────────────────────────────────────────────────────────────────────────
165
+ # Small mask window (fixed ~400×400, always shows a zoomed-out mask)
166
+ # ─────────────────────────────────────────────────────────────────────────────
167
+ class _MaskWindow(QDialog):
168
+ def __init__(self, parent=None):
169
+ super().__init__(parent)
170
+ self.setWindowTitle(self.tr("Dark Mask"))
171
+ self.setMinimumSize(300, 300)
172
+ self.resize(400, 400)
173
+ v = QVBoxLayout(self)
174
+ self.lbl = QLabel(alignment=Qt.AlignmentFlag.AlignCenter)
175
+ v.addWidget(self.lbl)
176
+
177
+ def set_mask(self, mask: np.ndarray):
178
+ m = np.clip(mask, 0, 1).astype(np.float32)
179
+ m8 = (m * 255.0).astype(np.uint8)
180
+ if m8.ndim == 2:
181
+ h, w = m8.shape
182
+ q = QImage(m8.data, w, h, w, QImage.Format.Format_Grayscale8)
183
+ else:
184
+ h, w, _ = m8.shape
185
+ q = QImage(m8.data, w, h, 3*w, QImage.Format.Format_RGB888)
186
+ pm = QPixmap.fromImage(q)
187
+ box = self.size()
188
+ pm2 = pm.scaled(box, Qt.AspectRatioMode.KeepAspectRatio, Qt.TransformationMode.SmoothTransformation)
189
+ self.lbl.setPixmap(pm2)
190
+
191
+ # ─────────────────────────────────────────────────────────────────────────────
192
+ # Dialog
193
+ # ─────────────────────────────────────────────────────────────────────────────
194
+ class WaveScaleDarkEnhancerDialogPro(QDialog):
195
+ def __init__(self, parent, doc, icon_path: str | None = None):
196
+ super().__init__(parent)
197
+ self.setWindowTitle(self.tr("WaveScale Dark Enhancer"))
198
+ if icon_path:
199
+ try: self.setWindowIcon(QIcon(icon_path))
200
+ except Exception as e:
201
+ import logging
202
+ logging.debug(f"Exception suppressed: {type(e).__name__}: {e}")
203
+ self.resize(980, 700)
204
+ self.setWindowFlag(Qt.WindowType.Window, True)
205
+ self.setWindowModality(Qt.WindowModality.NonModal)
206
+ self.setModal(False)
207
+ try:
208
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
209
+ except Exception:
210
+ pass # older PyQt6 versions
211
+ self._doc = doc
212
+ base = getattr(doc, "image", None)
213
+ if base is None:
214
+ raise RuntimeError("Active document has no image.")
215
+
216
+ img = np.asarray(base, dtype=np.float32)
217
+ if img.ndim == 2:
218
+ self._was_mono = True
219
+ self._mono_shape = img.shape
220
+ rgb = np.repeat(img[:, :, None], 3, axis=2)
221
+ elif img.ndim == 3 and img.shape[2] == 1:
222
+ self._was_mono = True
223
+ self._mono_shape = img.shape
224
+ rgb = np.repeat(img, 3, axis=2)
225
+ else:
226
+ self._was_mono = False
227
+ self._mono_shape = None
228
+ rgb = img[:, :, :3]
229
+ if img.dtype.kind in "ui":
230
+ maxv = float(np.nanmax(rgb)) or 1.0
231
+ rgb = rgb / max(1.0, maxv)
232
+ rgb = np.clip(rgb, 0.0, 1.0).astype(np.float32, copy=False)
233
+
234
+ self.original = rgb
235
+ self.preview = rgb.copy()
236
+
237
+ # scene/view
238
+ self.scene = QGraphicsScene(self)
239
+ self.view = QGraphicsView(self.scene)
240
+ self.view.setAlignment(Qt.AlignmentFlag.AlignCenter)
241
+ self.pix = QGraphicsPixmapItem()
242
+ self.scene.addItem(self.pix)
243
+ self.scroll = QScrollArea(self); self.scroll.setWidgetResizable(True); self.scroll.setWidget(self.view)
244
+
245
+ # zoom state
246
+ self.zoom_factor = 1.0
247
+ self.zoom_step = 1.25
248
+ self.zoom_min = 0.1
249
+ self.zoom_max = 5.0
250
+
251
+ # controls
252
+ self.grp = QGroupBox(self.tr("Dark Enhancer Controls"))
253
+ form = QFormLayout(self.grp)
254
+
255
+ self.s_scales = QSlider(Qt.Orientation.Horizontal); self.s_scales.setRange(2, 10); self.s_scales.setValue(6)
256
+ self.s_boost = QSlider(Qt.Orientation.Horizontal); self.s_boost.setRange(10, 1000); self.s_boost.setValue(500) # 0.10..10.00
257
+ self.s_gamma = QSlider(Qt.Orientation.Horizontal); self.s_gamma.setRange(10, 1000); self.s_gamma.setValue(100) # 0.10..10.00
258
+ self.s_iters = QSlider(Qt.Orientation.Horizontal); self.s_iters.setRange(1, 10); self.s_iters.setValue(2)
259
+
260
+ form.addRow(self.tr("Number of Scales:"), self.s_scales)
261
+ form.addRow(self.tr("Boost Factor:"), self.s_boost)
262
+ form.addRow(self.tr("Mask Gamma:"), self.s_gamma)
263
+ form.addRow(self.tr("Iterations:"), self.s_iters)
264
+
265
+ row = QHBoxLayout()
266
+ self.btn_preview = QPushButton(self.tr("Preview"))
267
+ self.btn_toggle = QPushButton(self.tr("Show Original")); self.btn_toggle.setCheckable(True)
268
+ row.addWidget(self.btn_preview); row.addWidget(self.btn_toggle)
269
+ form.addRow(row)
270
+
271
+ # progress
272
+ self.prog_grp = QGroupBox(self.tr("Progress"))
273
+ vprog = QVBoxLayout(self.prog_grp)
274
+ self.lbl_step = QLabel(self.tr("Idle"))
275
+ self.bar = QProgressBar(); self.bar.setRange(0, 100); self.bar.setValue(0)
276
+ vprog.addWidget(self.lbl_step); vprog.addWidget(self.bar)
277
+
278
+ # bottom
279
+ bot = QHBoxLayout()
280
+ self.btn_apply = QPushButton(self.tr("Apply to Document")); self.btn_apply.setEnabled(False)
281
+ self.btn_reset = QPushButton(self.tr("Reset"))
282
+ self.btn_close = QPushButton(self.tr("Close"))
283
+ bot.addStretch(1); bot.addWidget(self.btn_apply); bot.addWidget(self.btn_reset); bot.addWidget(self.btn_close)
284
+
285
+ # layout
286
+ main = QVBoxLayout(self)
287
+ main.addWidget(self.scroll)
288
+
289
+ zoom_box = QGroupBox(self.tr("Zoom Controls"))
290
+ zr = QHBoxLayout(zoom_box)
291
+ self.btn_zin = QPushButton(self.tr("Zoom In"))
292
+ self.btn_zout = QPushButton(self.tr("Zoom Out"))
293
+ self.btn_fit = QPushButton(self.tr("Fit to Preview"))
294
+ zr.addWidget(self.btn_zin); zr.addWidget(self.btn_zout); zr.addWidget(self.btn_fit)
295
+ main.addWidget(zoom_box)
296
+
297
+ h = QHBoxLayout()
298
+ h.addWidget(self.grp, 3)
299
+ h.addWidget(self.prog_grp, 1)
300
+ main.addLayout(h)
301
+ main.addLayout(bot)
302
+
303
+ # mask window (show immediately)
304
+ self.mask_win = _MaskWindow(self); self.mask_win.show()
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_zin.clicked.connect(self._zoom_in)
317
+ self.btn_zout.clicked.connect(self._zoom_out)
318
+ self.btn_fit.clicked.connect(self._fit_to_preview)
319
+
320
+ # gamma debounce → live mask updates (250ms)
321
+ self._gamma_timer = QTimer(self)
322
+ self._gamma_timer.setSingleShot(True)
323
+ self._gamma_timer.timeout.connect(self._update_mask_only)
324
+ self.s_gamma.valueChanged.connect(lambda _v: self._gamma_timer.start(250))
325
+
326
+ # init preview & initial mask
327
+ self._set_pix(self.preview)
328
+ self._update_mask_only()
329
+
330
+ def _combine_with_doc_mask(self, op_mask: np.ndarray | None) -> np.ndarray | None:
331
+ doc_m = self._get_doc_active_mask_2d()
332
+ if doc_m is None:
333
+ return op_mask
334
+ if op_mask is None:
335
+ return doc_m
336
+ H, W = op_mask.shape[:2]
337
+ if doc_m.shape != (H, W):
338
+ yi = (np.linspace(0, doc_m.shape[0] - 1, H)).astype(np.int32)
339
+ xi = (np.linspace(0, doc_m.shape[1] - 1, W)).astype(np.int32)
340
+ doc_m = doc_m[yi][:, xi]
341
+ return np.clip(op_mask * doc_m, 0.0, 1.0)
342
+
343
+ def _get_doc_active_mask_2d(self) -> np.ndarray | None:
344
+ """
345
+ Return active document mask as 2-D float32 [0..1], resized to current image.
346
+ """
347
+ doc = getattr(self, "_doc", None)
348
+ if doc is None:
349
+ return None
350
+
351
+ mid = getattr(doc, "active_mask_id", None)
352
+ if not mid:
353
+ return None
354
+
355
+ masks = getattr(doc, "masks", {}) or {}
356
+ layer = masks.get(mid)
357
+ if layer is None:
358
+ return None
359
+
360
+ # pick first non-None payload without boolean 'or'
361
+ data = None
362
+ for attr in ("data", "mask", "image", "array"):
363
+ if hasattr(layer, attr):
364
+ val = getattr(layer, attr)
365
+ if val is not None:
366
+ data = val
367
+ break
368
+ if data is None and isinstance(layer, dict):
369
+ for key in ("data", "mask", "image", "array"):
370
+ if key in layer and layer[key] is not None:
371
+ data = layer[key]
372
+ break
373
+ if data is None and isinstance(layer, np.ndarray):
374
+ data = layer
375
+ if data is None:
376
+ return None
377
+
378
+ m = np.asarray(data)
379
+ if m.ndim == 3:
380
+ m = m.mean(axis=2)
381
+
382
+ m = m.astype(np.float32, copy=False)
383
+ # normalize to [0,1] if needed
384
+ mx = float(m.max()) if m.size else 1.0
385
+ if mx > 1.0:
386
+ m /= mx
387
+ m = np.clip(m, 0.0, 1.0)
388
+
389
+ # resize (nearest) to current image size
390
+ H, W = self.original.shape[:2]
391
+ if m.shape != (H, W):
392
+ yi = (np.linspace(0, m.shape[0] - 1, H)).astype(np.int32)
393
+ xi = (np.linspace(0, m.shape[1] - 1, W)).astype(np.int32)
394
+ m = m[yi][:, xi]
395
+
396
+ return m
397
+
398
+
399
+ def _combine_with_doc_mask(self, algo_mask: np.ndarray) -> np.ndarray:
400
+ m_doc = self._get_doc_active_mask_2d()
401
+ if m_doc is None:
402
+ return algo_mask
403
+ return np.clip(algo_mask.astype(np.float32) * m_doc.astype(np.float32), 0.0, 1.0)
404
+
405
+
406
+ # --- preview pixmap ---
407
+ def _set_pix(self, rgb: np.ndarray):
408
+ arr = (np.clip(rgb, 0, 1) * 255).astype(np.uint8)
409
+ h, w, _ = arr.shape
410
+ q = QImage(arr.data, w, h, 3*w, QImage.Format.Format_RGB888)
411
+ self.pix.setPixmap(QPixmap.fromImage(q))
412
+ self.view.setSceneRect(self.pix.boundingRect())
413
+
414
+ # --- toggle ---
415
+ def _toggle(self):
416
+ if self.btn_toggle.isChecked():
417
+ self.btn_toggle.setText(self.tr("Show Preview"))
418
+ self._set_pix(self.original)
419
+ else:
420
+ self.btn_toggle.setText(self.tr("Show Original"))
421
+ self._set_pix(self.preview)
422
+
423
+ # --- reset ---
424
+ def _reset(self):
425
+ self.s_scales.setValue(6)
426
+ self.s_boost.setValue(500)
427
+ self.s_gamma.setValue(100)
428
+ self.s_iters.setValue(2)
429
+ self.preview = self.original.copy()
430
+ self._set_pix(self.preview)
431
+ self.lbl_step.setText(self.tr("Idle")); self.bar.setValue(0)
432
+ self.btn_apply.setEnabled(False)
433
+ self.btn_toggle.setChecked(False); self.btn_toggle.setText(self.tr("Show Original"))
434
+ self._update_mask_only()
435
+
436
+ # --- zoom + Ctrl+Wheel ---
437
+ def wheelEvent(self, e: QWheelEvent):
438
+ if e.modifiers() & Qt.KeyboardModifier.ControlModifier:
439
+ if e.angleDelta().y() > 0: self._zoom_in()
440
+ else: self._zoom_out()
441
+ e.accept(); return
442
+ super().wheelEvent(e)
443
+
444
+ def _zoom_in(self):
445
+ z = self.zoom_factor * self.zoom_step
446
+ if z <= self.zoom_max:
447
+ self.zoom_factor = z
448
+ self._apply_zoom()
449
+
450
+ def _zoom_out(self):
451
+ z = self.zoom_factor / self.zoom_step
452
+ if z >= self.zoom_min:
453
+ self.zoom_factor = z
454
+ self._apply_zoom()
455
+
456
+ def _fit_to_preview(self):
457
+ if not self.pix.pixmap().isNull():
458
+ self.view.fitInView(self.pix, Qt.AspectRatioMode.KeepAspectRatio)
459
+ self.zoom_factor = 1.0
460
+
461
+ def _apply_zoom(self):
462
+ self.view.resetTransform()
463
+ self.view.scale(self.zoom_factor, self.zoom_factor)
464
+
465
+ # --- live mask (no full recompute) ---
466
+ def _update_mask_only(self):
467
+ mgamma = float(self.s_gamma.value()) / 100.0
468
+ base = self.original
469
+ lab = _rgb_to_lab(base)
470
+ L = lab[..., 0] / 100.0
471
+ algo_mask = _darkness_mask(np.clip(L, 0.0, 1.0),
472
+ int(self.s_scales.value()),
473
+ self.base_kernel, mgamma)
474
+ mask_comb = self._combine_with_doc_mask(algo_mask)
475
+ self.mask_win.setWindowTitle(
476
+ self.tr("Dark Mask (Algo × Active Mask)") if self._get_doc_active_mask_2d() is not None else self.tr("Dark Mask")
477
+ )
478
+ self.mask_win.set_mask(mask_comb)
479
+ # --- threaded preview ---
480
+ def _start_preview(self):
481
+ self.btn_preview.setEnabled(False); self.btn_apply.setEnabled(False)
482
+ n_scales = int(self.s_scales.value())
483
+ boost = float(self.s_boost.value()) / 100.0
484
+ mgamma = float(self.s_gamma.value()) / 100.0
485
+ iters = int(self.s_iters.value())
486
+ docmask = self._get_doc_active_mask_2d()
487
+
488
+ self.thread = QThread(self)
489
+ self.worker = DSEWorker(self.original, n_scales, boost, mgamma,
490
+ self.base_kernel, iters,
491
+ external_mask=docmask)
492
+ self.worker.moveToThread(self.thread)
493
+ self.thread.started.connect(self.worker.run)
494
+ self.worker.progress_update.connect(self._on_progress)
495
+ self.worker.finished.connect(self._on_finished)
496
+ self.worker.finished.connect(self.thread.quit)
497
+ self.worker.finished.connect(self.worker.deleteLater)
498
+ self.thread.finished.connect(self.thread.deleteLater)
499
+ self.thread.start()
500
+
501
+ def _on_progress(self, step: str, pct: int):
502
+ self.lbl_step.setText(step); self.bar.setValue(pct)
503
+
504
+ def _on_finished(self, out: np.ndarray, mask: np.ndarray):
505
+ self.btn_preview.setEnabled(True)
506
+ if out is None:
507
+ QMessageBox.critical(self, self.tr("WaveScale Dark Enhancer"), self.tr("Processing failed."))
508
+ return
509
+
510
+ # Respect the document mask
511
+ doc_m = self._get_doc_active_mask_2d()
512
+ if out.ndim == 2:
513
+ out_rgb = np.repeat(out[:, :, None], 3, axis=2)
514
+ else:
515
+ out_rgb = out
516
+
517
+ if doc_m is not None:
518
+ M3 = np.repeat(doc_m[:, :, None], 3, axis=2).astype(np.float32)
519
+ self.preview = self.original * (1.0 - M3) + out_rgb * M3
520
+ else:
521
+ self.preview = out_rgb
522
+
523
+ # show combined mask (internal darkness mask × doc mask)
524
+ mask = self._combine_with_doc_mask(mask)
525
+
526
+ self._set_pix(self.preview)
527
+ self.mask_win.set_mask(mask)
528
+ self.btn_apply.setEnabled(True)
529
+ self.btn_toggle.setChecked(False); self.btn_toggle.setText(self.tr("Show Original"))
530
+ self.lbl_step.setText(self.tr("Preview ready")); self.bar.setValue(100)
531
+
532
+ # --- apply back to doc ---
533
+ # --- apply back to doc ---
534
+ def _apply_to_doc(self):
535
+ out = self.preview
536
+ if self._was_mono:
537
+ mono = np.mean(out, axis=2, dtype=np.float32)
538
+ if self._mono_shape and len(self._mono_shape) == 3 and self._mono_shape[2] == 1:
539
+ mono = mono[:, :, None]
540
+ out = mono
541
+ out = np.clip(out, 0.0, 1.0).astype(np.float32, copy=False)
542
+ try:
543
+ if hasattr(self._doc, "set_image"):
544
+ self._doc.set_image(out, step_name="WaveScale Dark Enhancer")
545
+ elif hasattr(self._doc, "apply_numpy"):
546
+ self._doc.apply_numpy(out, step_name="WaveScale Dark Enhancer")
547
+ else:
548
+ self._doc.image = out
549
+ except Exception as e:
550
+ QMessageBox.critical(self, self.tr("WaveScale Dark Enhancer"), self.tr("Failed to write to document:\n{0}").format(e))
551
+ return
552
+
553
+ # ── Build preset from current sliders ─────────────────────────
554
+ try:
555
+ preset = {
556
+ "n_scales": int(self.s_scales.value()),
557
+ "boost_factor": float(self.s_boost.value()) / 100.0,
558
+ "mask_gamma": float(self.s_gamma.value()) / 100.0,
559
+ "iterations": int(self.s_iters.value()),
560
+ }
561
+ except Exception:
562
+ preset = {}
563
+
564
+ # ── Register as last_headless_command on the main window ─────
565
+ try:
566
+ main = self.parent()
567
+ if main is not None:
568
+ payload = {
569
+ "command_id": "wavescale_dark_enhance",
570
+ "preset": dict(preset),
571
+ }
572
+ setattr(main, "_last_headless_command", payload)
573
+
574
+ # Optional debug logging similar to other tools
575
+ try:
576
+ if hasattr(main, "_log"):
577
+ main._log(
578
+ "[Replay] Registered WaveScale Dark Enhancer as "
579
+ f"last action (n_scales={preset.get('n_scales')}, "
580
+ f"boost={preset.get('boost_factor')}, "
581
+ f"mask_gamma={preset.get('mask_gamma')}, "
582
+ f"iterations={preset.get('iterations')})"
583
+ )
584
+ except Exception:
585
+ pass
586
+ except Exception:
587
+ # Never let replay wiring break the apply
588
+ pass
589
+
590
+ # Dialog stays open so user can apply to other images
591
+ # Refresh document reference for next operation
592
+ self._refresh_document_from_active()
593
+
594
+ def _refresh_document_from_active(self):
595
+ """
596
+ Refresh the dialog's document reference to the currently active document.
597
+ This allows reusing the same dialog on different images.
598
+ """
599
+ try:
600
+ main = self.parent()
601
+ if main and hasattr(main, "_active_doc"):
602
+ new_doc = main._active_doc()
603
+ if new_doc is not None and new_doc is not self._doc:
604
+ self._doc = new_doc
605
+ # Reset state and refresh for new document
606
+ self._L_original = None
607
+ self._last_preview = None
608
+ except Exception:
609
+ pass
610
+
611
+
612
+ # ─────────────────────────────────────────────────────────────────────────────
613
+ # Installer helpers
614
+ # ─────────────────────────────────────────────────────────────────────────────
615
+ def install_wavescale_dark_enhancer(main_window: QMainWindow,
616
+ dse_icon_path: str,
617
+ *,
618
+ command_id: str = "wavescale_dark_enhancer",
619
+ menu_name: str = "Pro",
620
+ toolbar_name: str = "Pro Tools"):
621
+ """
622
+ Creates the QAction, hooks it into menu+toolbar, and registers it
623
+ with your ShortcutManager under `command_id`.
624
+ Expects main_window to expose:
625
+ • .docman.current_document() → returns doc with .image
626
+ • ._spawn_subwindow_for(doc) (normal in your app)
627
+ • .shortcut_manager (your ShortcutManager) — optional
628
+ """
629
+ # 1) QAction
630
+ act = getattr(main_window, "act_wavescalede", None)
631
+ if act is None:
632
+ from PyQt6.QtGui import QAction
633
+ act = QAction(QIcon(dse_icon_path), "WaveScale Dark Enhancer", main_window)
634
+ act.setObjectName(command_id)
635
+ act.setProperty("command_id", command_id)
636
+
637
+ def _run_dialog():
638
+ docman = getattr(main_window, "docman", None)
639
+ doc = None
640
+ if docman and hasattr(docman, "current_document"):
641
+ doc = docman.current_document()
642
+ if doc is None or getattr(doc, "image", None) is None:
643
+ from PyQt6.QtCore import QCoreApplication
644
+ QMessageBox.warning(main_window, QCoreApplication.translate("WaveScaleDarkEnhancerDialogPro", "WaveScale Dark Enhancer"), QCoreApplication.translate("WaveScaleDarkEnhancerDialogPro", "No active image."))
645
+ return
646
+ dlg = WaveScaleDarkEnhancerDialogPro(main_window, doc, icon_path=dse_icon_path)
647
+ dlg.exec()
648
+
649
+ act.triggered.connect(_run_dialog)
650
+ setattr(main_window, "act_wavescalede", act)
651
+
652
+ # 2) Menu hookup
653
+ menubar = main_window.menuBar()
654
+ menu = None
655
+ for m in menubar.findChildren(type(menubar)):
656
+ # best-effort: ignore; we’ll just create/find by title
657
+ pass
658
+ menu = None
659
+ for i in range(menubar.actions().__len__()):
660
+ if menubar.actions()[i].text().replace("&", "") == menu_name:
661
+ menu = menubar.actions()[i].menu()
662
+ break
663
+ if menu is None:
664
+ menu = menubar.addMenu(menu_name)
665
+ menu.addAction(act)
666
+
667
+ # 3) Toolbar hookup
668
+ tb = None
669
+ for t in main_window.findChildren(type(main_window.addToolBar("tmp"))):
670
+ # naive scan (we won't rely on this); we'll create if needed
671
+ pass
672
+ tb = getattr(main_window, "_tb_" + toolbar_name.replace(" ", "_").lower(), None)
673
+ if tb is None:
674
+ tb = main_window.addToolBar(toolbar_name)
675
+ setattr(main_window, "_tb_" + toolbar_name.replace(" ", "_").lower(), tb)
676
+ tb.addAction(act)
677
+
678
+ # 4) Register with ShortcutManager (if present)
679
+ sm = getattr(main_window, "shortcut_manager", None)
680
+ if sm and hasattr(sm, "register_action"):
681
+ sm.register_action(command_id, act)
682
+
683
+ return act