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,728 @@
1
+ # pro/ghs_dialog_pro.py
2
+ from PyQt6.QtCore import Qt, QEvent, QPointF, QTimer
3
+ from PyQt6.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
4
+ QScrollArea, QComboBox, QSlider, QToolButton, QWidget, QMessageBox)
5
+ from PyQt6.QtGui import QPixmap, QImage, QPen, QColor
6
+ import numpy as np
7
+
8
+
9
+
10
+ # Reuse the engine from curves_editor_pro
11
+ from .curve_editor_pro import (
12
+ CurveEditor, _CurvesWorker, _apply_mode_any, build_curve_lut,
13
+ _float_to_qimage_rgb8, _downsample_for_preview, ImageLabel
14
+ )
15
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
16
+
17
+ class GhsDialogPro(QDialog):
18
+ """
19
+ Hyperbolic Stretch dialog:
20
+ - Left: α/β/γ + LP/HP + channel selector
21
+ - Right: same preview/zoom/pan as CurvesDialogPro
22
+ - Uses CurveEditor for the actual curve, but the points are generated from parameters.
23
+ """
24
+ def __init__(self, parent, document):
25
+ super().__init__(parent)
26
+ self.setWindowTitle(self.tr("Hyperbolic Stretch"))
27
+ self.setWindowFlag(Qt.WindowType.Window, True)
28
+ self.setWindowModality(Qt.WindowModality.NonModal)
29
+ self.setModal(False)
30
+ try:
31
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
32
+ except Exception:
33
+ pass # older PyQt6 versions
34
+ self.doc = document
35
+ self._preview_img = None
36
+ self._full_img = None
37
+ self._pix = None
38
+ self._zoom = 0.25
39
+ self._panning = False
40
+ self._pan_start = QPointF()
41
+ self._sym_u = 0.5 # pivot in [0..1]
42
+
43
+ # ---------- layout ----------
44
+ main = QHBoxLayout(self)
45
+
46
+ # Left controls
47
+ left = QVBoxLayout()
48
+ self.editor = CurveEditor(self)
49
+ left.addWidget(self.editor)
50
+
51
+ hint = QLabel(self.tr("Tip: Ctrl+Click (or double-click) the image to set the symmetry pivot"))
52
+ hint.setStyleSheet("color: #888; font-size: 11px;")
53
+ left.addWidget(hint)
54
+ self.editor.setToolTip(self.tr("Ctrl+Click (or double-click) the image to set the symmetry pivot"))
55
+
56
+ # channel selector
57
+ ch_row = QHBoxLayout()
58
+ ch_row.addWidget(QLabel(self.tr("Channel:")))
59
+ self.cmb_ch = QComboBox(self)
60
+ self.cmb_ch.addItems(["K (Brightness)", "R", "G", "B"])
61
+ ch_row.addWidget(self.cmb_ch)
62
+ left.addLayout(ch_row)
63
+
64
+ # α / β / γ
65
+ def _mk_slider_row(name, rng, val):
66
+ row = QHBoxLayout()
67
+ lab = QLabel(name); row.addWidget(lab)
68
+ s = QSlider(Qt.Orientation.Horizontal); s.setRange(*rng); s.setValue(val); row.addWidget(s)
69
+ v = QLabel(f"{val/100:.2f}" if name=="γ" else f"{val/50:.2f}"); row.addWidget(v)
70
+ return row, s, v
71
+
72
+ rowA, self.sA, self.labA = _mk_slider_row("α", (1, 500), 50) # 1.0
73
+ rowB, self.sB, self.labB = _mk_slider_row("β", (1, 500), 50) # 1.0
74
+ rowG, self.sG, self.labG = _mk_slider_row("γ", (1, 500), 100) # 1.0
75
+ left.addLayout(rowA); left.addLayout(rowB); left.addLayout(rowG)
76
+
77
+ # LP / HP (protect)
78
+ rowLP = QHBoxLayout(); rowHP = QHBoxLayout()
79
+ rowLP.addWidget(QLabel("LP")); self.sLP = QSlider(Qt.Orientation.Horizontal); self.sLP.setRange(0,360); rowLP.addWidget(self.sLP); self.labLP = QLabel("0.00"); rowLP.addWidget(self.labLP)
80
+ rowHP.addWidget(QLabel("HP")); self.sHP = QSlider(Qt.Orientation.Horizontal); self.sHP.setRange(0,360); rowHP.addWidget(self.sHP); self.labHP = QLabel("0.00"); rowHP.addWidget(self.labHP)
81
+ left.addLayout(rowLP); left.addLayout(rowHP)
82
+
83
+ # Buttons
84
+ rowb = QHBoxLayout()
85
+ self.btn_apply = QPushButton(self.tr("Apply"))
86
+ self.btn_reset = QToolButton(); self.btn_reset.setText(self.tr("Reset"))
87
+ self.btn_hist = QToolButton(); self.btn_hist.setText(self.tr("Histogram"))
88
+ self.btn_hist.setToolTip(self.tr("Open a Histogram for this image.\n"
89
+ "Ctrl+Click on the histogram to set the GHS pivot."))
90
+ rowb.addWidget(self.btn_apply)
91
+ rowb.addWidget(self.btn_reset)
92
+ rowb.addWidget(self.btn_hist)
93
+ left.addLayout(rowb)
94
+ left.addStretch(1)
95
+
96
+ main.addLayout(left, 0)
97
+
98
+ # --- Right preview panel ---
99
+ right = QVBoxLayout()
100
+ zoombar = QHBoxLayout()
101
+ zoombar.addStretch(1)
102
+
103
+ b_out = themed_toolbtn("zoom-out", self.tr("Zoom Out"))
104
+ b_in = themed_toolbtn("zoom-in", self.tr("Zoom In"))
105
+ b_fit = themed_toolbtn("zoom-fit-best", self.tr("Fit to Preview"))
106
+
107
+ zoombar.addWidget(b_out)
108
+ zoombar.addWidget(b_in)
109
+ zoombar.addWidget(b_fit)
110
+
111
+ right.addLayout(zoombar)
112
+ self.scroll = QScrollArea()
113
+ self.scroll.setWidgetResizable(True)
114
+ self.scroll.setAlignment(Qt.AlignmentFlag.AlignCenter)
115
+
116
+ # CREATE LABEL FIRST
117
+ self.label = ImageLabel(self) # <- make sure ImageLabel is imported
118
+ self.label.setAlignment(Qt.AlignmentFlag.AlignCenter)
119
+ self.label.mouseMoved.connect(self._on_preview_mouse_moved)
120
+ self.label.installEventFilter(self)
121
+
122
+ self.scroll.setWidget(self.label)
123
+ # INSTALL FILTERS AFTER label exists
124
+ self.scroll.viewport().installEventFilter(self)
125
+
126
+ right.addWidget(self.scroll, 1)
127
+ main.addLayout(right, 1)
128
+
129
+ # ---------- wiring ----------
130
+ self._suppress_editor_preview = False
131
+ self.editor.setPreviewCallback(lambda _lut8: self._on_editor_preview())
132
+ self.editor.setSymmetryCallback(self._on_symmetry_pick)
133
+
134
+ for s in (self.sA, self.sB, self.sG, self.sLP, self.sHP):
135
+ s.valueChanged.connect(self._schedule_rebuild_from_params)
136
+
137
+ # Track dragging so we can “flush” on release
138
+ s.sliderPressed.connect(self._on_any_slider_pressed)
139
+ s.sliderReleased.connect(self._on_any_slider_released)
140
+ self.cmb_ch.currentTextChanged.connect(self._recolor_curve)
141
+
142
+ self.btn_apply.clicked.connect(self._apply)
143
+ self.btn_reset.clicked.connect(self._reset)
144
+ self._hist_dlg = None # will hold our per-GHS histogram dialog
145
+ self.btn_hist.clicked.connect(self._open_histogram)
146
+
147
+ b_out.clicked.connect(lambda: self._set_zoom(self._zoom / 1.25))
148
+ b_in .clicked.connect(lambda: self._set_zoom(self._zoom * 1.25))
149
+ b_fit.clicked.connect(self._fit)
150
+
151
+ # seed image data
152
+ self._load_from_doc()
153
+
154
+ # start with Fit to Preview (avoids offset issues)
155
+ QTimer.singleShot(0, self._fit)
156
+
157
+ self._rebuild_debounce = QTimer(self)
158
+ self._rebuild_debounce.setSingleShot(True)
159
+ self._rebuild_debounce.setInterval(75) # 50–120ms feels good; 75 is a nice default
160
+ self._rebuild_debounce.timeout.connect(self._rebuild_from_params_now)
161
+
162
+ self._slider_dragging = False
163
+
164
+ # first curve
165
+ self._rebuild_from_params()
166
+
167
+ # ---------- params → handles/curve ----------
168
+ def _open_histogram(self):
169
+ """Open (or raise) a HistogramDialog bound to this document and
170
+ connect its pivot signal to our symmetry pivot."""
171
+ try:
172
+ from .histogram import HistogramDialog
173
+ except Exception as e:
174
+ QMessageBox.warning(self, self.tr("Histogram"),
175
+ self.tr("Could not import histogram module:\n{0}").format(e))
176
+ return
177
+
178
+ # If we already created one and it's still alive, just bring it forward.
179
+ if self._hist_dlg is not None:
180
+ try:
181
+ if self._hist_dlg.isVisible():
182
+ self._hist_dlg.raise_()
183
+ self._hist_dlg.activateWindow()
184
+ return
185
+ except RuntimeError:
186
+ # dialog was destroyed; fall through to recreate
187
+ self._hist_dlg = None
188
+
189
+ dlg = HistogramDialog(self, self.doc)
190
+ self._hist_dlg = dlg
191
+ try:
192
+ dlg.pivotPicked.connect(self._on_hist_pivot)
193
+ except Exception:
194
+ # if signal isn't there for some reason, just ignore
195
+ pass
196
+ dlg.show()
197
+
198
+ def _on_hist_pivot(self, u: float):
199
+ """
200
+ Receive normalized pivot from Histogram (0..1) and update our symmetry
201
+ point & curve.
202
+ """
203
+ u = float(np.clip(u, 0.0, 1.0))
204
+ self._sym_u = u
205
+ # CurveEditor uses 0..360 in X; Y doesn't matter for the vertical line
206
+ self.editor.setSymmetryPoint(u * 360.0, 0.0)
207
+ self._rebuild_from_params()
208
+
209
+ def _on_editor_preview(self):
210
+ if getattr(self, "_suppress_editor_preview", False):
211
+ return
212
+ self._quick_preview()
213
+
214
+ def _on_any_slider_pressed(self):
215
+ self._slider_dragging = True
216
+
217
+ def _on_any_slider_released(self):
218
+ self._slider_dragging = False
219
+ # Flush immediately to final value
220
+ if self._rebuild_debounce.isActive():
221
+ self._rebuild_debounce.stop()
222
+ self._rebuild_from_params_now()
223
+
224
+ def _schedule_rebuild_from_params(self):
225
+ """
226
+ Called on every valueChanged tick. Updates labels fast, but defers the heavy work.
227
+ """
228
+ # lightweight label updates (no LUT/preview work here)
229
+ a = self.sA.value() / 50.0
230
+ b = self.sB.value() / 50.0
231
+ g = self.sG.value() / 100.0
232
+ self.labA.setText(f"{a:.2f}")
233
+ self.labB.setText(f"{b:.2f}")
234
+ self.labG.setText(f"{g:.2f}")
235
+ self.labLP.setText(f"{self.sLP.value()/360.0:.2f}")
236
+ self.labHP.setText(f"{self.sHP.value()/360.0:.2f}")
237
+
238
+ # debounce heavy rebuild
239
+ self._rebuild_debounce.start()
240
+
241
+ def _rebuild_from_params(self):
242
+ """
243
+ Keep existing call sites working (hist pivot, symmetry pick, etc).
244
+ For non-slider callers we generally WANT immediate rebuild.
245
+ """
246
+ if self._rebuild_debounce.isActive():
247
+ self._rebuild_debounce.stop()
248
+ self._rebuild_from_params_now()
249
+
250
+
251
+ def _on_symmetry_pick(self, u, v):
252
+ self._sym_u = float(u)
253
+ self._rebuild_from_params()
254
+
255
+ def _rebuild_from_params_now(self):
256
+ a = self.sA.value()/50.0
257
+ b = self.sB.value()/50.0
258
+ g = self.sG.value()/100.0
259
+ self.labA.setText(f"{a:.2f}")
260
+ self.labB.setText(f"{b:.2f}")
261
+ self.labG.setText(f"{g:.2f}")
262
+
263
+ # number of handles (keep existing count or default to 20)
264
+ N = len(self.editor.control_points) or 20
265
+ if len(self.editor.control_points) == 0:
266
+ for _ in range(N):
267
+ self.editor.addControlPoint(0, 0)
268
+
269
+ SP = float(self._sym_u)
270
+ eps = 1e-6
271
+
272
+ # --- sample around 0.5, then REMAP x to SP (this is the key) ---
273
+ us = np.linspace(0.0, 1.0, N) # even sampling
274
+ left = us <= 0.5
275
+ right = ~left
276
+
277
+ # generalized hyperbolic (two shapes, mirrored at 0.5)
278
+ rawL = us**a / (us**a + b*(1.0-us)**a)
279
+ rawR = us**a / (us**a + (1.0/b)*(1.0-us)**a)
280
+
281
+ midL = (0.5**a) / (0.5**a + b*(0.5)**a)
282
+ midR = (0.5**a) / (0.5**a + (1.0/b)*(0.5)**a)
283
+
284
+ # map domain to pivoted x ("up") and scaled y ("vp")
285
+ up = np.empty_like(us)
286
+ vp = np.empty_like(us)
287
+
288
+ # left half → [0 .. SP]
289
+ up[left] = 2.0 * SP * us[left]
290
+ vp[left] = rawL[left] * (SP / max(midL, eps))
291
+
292
+ # right half → [SP .. 1]
293
+ up[right] = SP + 2.0*(1.0 - SP)*(us[right] - 0.5)
294
+ vp[right] = SP + (rawR[right] - midR) * ((1.0 - SP) / max(1.0 - midR, eps))
295
+
296
+ # LP/HP protection: blend toward identity (vp == up)
297
+ LP = self.sLP.value()/360.0
298
+ HP = self.sHP.value()/360.0
299
+
300
+ if LP > 0:
301
+ m = up <= SP
302
+ vp[m] = (1.0 - LP)*vp[m] + LP*up[m]
303
+ if HP > 0:
304
+ m = up >= SP
305
+ vp[m] = (1.0 - HP)*vp[m] + HP*up[m]
306
+
307
+ self.labLP.setText(f"{LP:.2f}")
308
+ self.labHP.setText(f"{HP:.2f}")
309
+
310
+ # gamma lift
311
+ if abs(g - 1.0) > 1e-6:
312
+ vp = np.clip(vp, 0.0, 1.0) ** (1.0 / g)
313
+
314
+ # keep in range & gently enforce monotonicity to avoid tiny dips
315
+ vp = np.clip(vp, 0.0, 1.0)
316
+ vp = np.maximum.accumulate(vp)
317
+
318
+ # write handles back (x rightward, y inverted for the grid)
319
+ xs = up * 360.0
320
+ ys = (1.0 - vp) * 360.0
321
+ pts = list(zip(xs.astype(float), ys.astype(float)))
322
+
323
+ self._suppress_editor_preview = True
324
+ try:
325
+ cps_sorted = sorted(self.editor.control_points, key=lambda p: p.scenePos().x())
326
+ for p, (x, y) in zip(cps_sorted, pts):
327
+ p.setPos(x, y)
328
+
329
+ self._recolor_curve()
330
+ self.editor.updateCurve()
331
+ finally:
332
+ self._suppress_editor_preview = False
333
+
334
+ self._quick_preview()
335
+
336
+
337
+ def _recolor_curve(self):
338
+ color_map = {
339
+ "K (Brightness)": Qt.GlobalColor.white,
340
+ "R": Qt.GlobalColor.red, "G": Qt.GlobalColor.green, "B": Qt.GlobalColor.blue
341
+ }
342
+ ch = self.cmb_ch.currentText()
343
+ if getattr(self.editor, "curve_item", None):
344
+ pen = QPen(color_map[ch]); pen.setWidth(3)
345
+ self.editor.curve_item.setPen(pen)
346
+ self._quick_preview()
347
+
348
+ # ---------- preview/apply (same as CurvesDialogPro) ----------
349
+ def _build_lut01(self):
350
+ fn = getattr(self.editor, "getCurveFunction", None)
351
+ if not fn: return None
352
+ f = fn()
353
+ if f is None: return None
354
+ try:
355
+ return build_curve_lut(f, size=65536)
356
+ except Exception:
357
+ return None
358
+
359
+ def _quick_preview(self):
360
+ if self._preview_img is None:
361
+ return
362
+ lut01 = self._build_lut01()
363
+ if lut01 is None:
364
+ return
365
+ mode = self.cmb_ch.currentText()
366
+ out = _apply_mode_any(self._preview_img, mode, lut01)
367
+ out = self._blend_with_mask(out) # ✅ blend with mask
368
+ self._update_preview_pix(out)
369
+
370
+ def _apply(self):
371
+ if self._full_img is None:
372
+ return
373
+
374
+ luts = self._build_all_active_luts()
375
+
376
+ self.btn_apply.setEnabled(False)
377
+ self._thr = _CurvesWorker(self._full_img, luts, self)
378
+ # ⬇️ use the handler you ALREADY have, which commits + metadata + reset
379
+ self._thr.done.connect(self._on_apply_ready)
380
+ self._thr.finished.connect(lambda: self.btn_apply.setEnabled(True))
381
+ self._thr.start()
382
+
383
+ def _build_all_active_luts(self) -> dict[str, np.ndarray]:
384
+ """
385
+ For GHS we really only have ONE curve at a time – the one in self.editor –
386
+ and we apply it to the currently selected channel.
387
+ The worker wants a dict like {"K": lut} or {"R": lut}.
388
+ """
389
+ lut = self._build_lut01()
390
+ if lut is None:
391
+ return {}
392
+
393
+ ch = self.cmb_ch.currentText()
394
+ # map UI text → worker key
395
+ ui2key = {
396
+ "K (Brightness)": "K",
397
+ "R": "R",
398
+ "G": "G",
399
+ "B": "B",
400
+ }
401
+ key = ui2key.get(ch, "K")
402
+ return {key: lut}
403
+
404
+ def _apply_all_curves_once(self, img: np.ndarray, luts: dict[str, np.ndarray]) -> np.ndarray:
405
+ """
406
+ This is what _CurvesWorker will call.
407
+ We only ever expect 0 or 1 LUT here.
408
+ """
409
+ if not luts:
410
+ return img
411
+
412
+ # pull the single entry
413
+ (key, lut), = luts.items()
414
+
415
+ # map worker key → mode string used by _apply_mode_any
416
+ key2mode = {
417
+ "K": "K (Brightness)",
418
+ "R": "R",
419
+ "G": "G",
420
+ "B": "B",
421
+ }
422
+ mode = key2mode.get(key, "K (Brightness)")
423
+
424
+ out = _apply_mode_any(img, mode, lut)
425
+ return out.astype(np.float32, copy=False)
426
+
427
+ def _on_apply_commit_ready(self, out01: np.ndarray):
428
+ # honor mask, same as preview
429
+ out01 = self._blend_with_mask(out01)
430
+
431
+ # 🔴 safety: if the document currently holds RGB but we got mono back,
432
+ # make it 3-channel so apply_edit doesn’t silently ignore it
433
+ doc_img = np.asarray(self.doc.image)
434
+ if doc_img.ndim == 3 and out01.ndim == 2:
435
+ out01 = np.repeat(out01[..., None], 3, axis=2)
436
+
437
+ # now do the normal commit (history, reload, reset curves, etc.)
438
+ self._commit(out01)
439
+
440
+
441
+ def _on_apply_ready(self, out01: np.ndarray):
442
+ try:
443
+ # honor mask, same as preview
444
+ out_masked = self._blend_with_mask(out01)
445
+
446
+ # 🔹 build a single params dict used by:
447
+ # - metadata["ghs"]
448
+ # - replay_last_action preset
449
+ ghs_params = {
450
+ "alpha": self.sA.value() / 50.0,
451
+ "beta": self.sB.value() / 50.0,
452
+ "gamma": self.sG.value() / 100.0,
453
+ "lp": self.sLP.value() / 360.0,
454
+ "hp": self.sHP.value() / 360.0,
455
+ "pivot": float(self._sym_u),
456
+ "channel": self.cmb_ch.currentText(),
457
+ }
458
+
459
+ _marr, mid, mname = self._active_mask_layer()
460
+ meta = {
461
+ "step_name": "Hyperbolic Stretch",
462
+ "ghs": ghs_params,
463
+ "masked": bool(mid),
464
+ "mask_id": mid,
465
+ "mask_name": mname,
466
+ "mask_blend": "m*out + (1-m)*src",
467
+ }
468
+
469
+ # 🔁 Register this as "last action" for *both* dialog-replay and headless replay
470
+ mw = self.parent()
471
+ # Walk up to the main window
472
+ while mw is not None and not (
473
+ hasattr(mw, "_remember_last_action_from_dialog")
474
+ or hasattr(mw, "_remember_last_headless_command")
475
+ ):
476
+ mw = mw.parent()
477
+
478
+ if mw is not None:
479
+ # Dialog-style (keeps your existing mechanism, if used elsewhere)
480
+ if hasattr(mw, "_remember_last_action_from_dialog"):
481
+ try:
482
+ mw._remember_last_action_from_dialog("ghs", ghs_params)
483
+ except Exception:
484
+ pass
485
+
486
+ # Headless-style (this is what ROI replay uses)
487
+ if hasattr(mw, "_remember_last_headless_command"):
488
+ try:
489
+ mw._remember_last_headless_command(
490
+ "ghs",
491
+ ghs_params,
492
+ description="Hyperbolic Stretch",
493
+ )
494
+ # DEBUG
495
+ try:
496
+ mw._log(
497
+ f"[Replay] GHS stored as headless command: "
498
+ f"preset_keys={list(ghs_params.keys())}"
499
+ )
500
+ except Exception:
501
+ print(
502
+ "[Replay] GHS stored as headless command, "
503
+ "preset_keys=",
504
+ list(ghs_params.keys()),
505
+ )
506
+ except Exception as e:
507
+ print("[Replay] GHS remember_last_headless_command failed:", e)
508
+
509
+
510
+ # Commit result to the document
511
+ self.doc.apply_edit(out_masked.copy(),
512
+ metadata=meta,
513
+ step_name="Hyperbolic Stretch")
514
+
515
+ # 🔄 Refresh buffers from the updated doc
516
+ self._load_from_doc()
517
+
518
+ # 🔄 Reset pivot + curve drawing for the next pass
519
+ self._sym_u = 0.5
520
+ self.editor.clearSymmetryLine()
521
+ self.editor.initCurve()
522
+ self.sA.setValue(50); self.sB.setValue(50); self.sG.setValue(100)
523
+ self.sLP.setValue(0); self.sHP.setValue(0)
524
+ self._rebuild_from_params()
525
+ QTimer.singleShot(0, self._fit)
526
+
527
+ except Exception as e:
528
+ QMessageBox.critical(self, self.tr("Apply failed"), str(e))
529
+
530
+
531
+ # ---------- image plumbing / zoom/pan ----------
532
+ def _load_from_doc(self):
533
+ img = self.doc.image
534
+ if img is None:
535
+ QMessageBox.information(self, self.tr("No image"), self.tr("Open an image first."))
536
+ return
537
+ arr = np.asarray(img).astype(np.float32)
538
+ if arr.dtype.kind in "ui":
539
+ arr = arr / np.iinfo(img.dtype).max
540
+ self._full_img = arr
541
+ self._preview_img = _downsample_for_preview(arr, 1200)
542
+ self._update_preview_pix(self._preview_img)
543
+
544
+ def _update_preview_pix(self, img01):
545
+ if img01 is None:
546
+ self.label.clear(); self._pix = None; return
547
+ qimg = _float_to_qimage_rgb8(img01)
548
+ pm = QPixmap.fromImage(qimg)
549
+ self._pix = pm
550
+ self._apply_zoom()
551
+
552
+ def _apply_zoom(self):
553
+ if self._pix is None: return
554
+ scaled = self._pix.scaled(self._pix.size()*self._zoom,
555
+ Qt.AspectRatioMode.KeepAspectRatio,
556
+ Qt.TransformationMode.SmoothTransformation)
557
+ self.label.setPixmap(scaled)
558
+ self.label.resize(scaled.size())
559
+
560
+ def _set_zoom(self, z):
561
+ self._zoom = float(max(0.05, min(z, 8.0)))
562
+ self._apply_zoom()
563
+
564
+ def _fit(self):
565
+ if self._pix is None: return
566
+ vp = self.scroll.viewport().size()
567
+ if self._pix.width()==0 or self._pix.height()==0: return
568
+ s = min(vp.width()/self._pix.width(), vp.height()/self._pix.height())
569
+ self._set_zoom(max(0.05, s))
570
+
571
+ def _k_from_label_point(self, lbl_pt):
572
+ """lbl_pt is in label (pixmap) coordinates."""
573
+ if self._preview_img is None or self.label.pixmap() is None:
574
+ return None
575
+ pix = self.label.pixmap()
576
+ pw, ph = pix.width(), pix.height()
577
+ x, y = int(lbl_pt.x()), int(lbl_pt.y())
578
+ if not (0 <= x < pw and 0 <= y < ph):
579
+ return None
580
+ ih, iw = self._preview_img.shape[:2]
581
+ ix = int(x * iw / pw)
582
+ iy = int(y * ih / ph)
583
+ ix = max(0, min(iw - 1, ix))
584
+ iy = max(0, min(ih - 1, iy))
585
+ px = self._preview_img[iy, ix]
586
+ k = float(np.mean(px)) if self._preview_img.ndim == 3 else float(px)
587
+ return max(0.0, min(1.0, k))
588
+
589
+ # ctrl+wheel zoom + panning + ctrl+click on preview to move pivot
590
+ def eventFilter(self, obj, ev):
591
+ lbl = getattr(self, "label", None)
592
+ if lbl is None:
593
+ return False
594
+ # --- set pivot on DOUBLE-CLICK (or Ctrl+click) anywhere over the image ---
595
+ if (obj is self.label or obj is self.scroll.viewport()):
596
+ # Double-click → set pivot
597
+ if ev.type() == QEvent.Type.MouseButtonDblClick and ev.button() == Qt.MouseButton.LeftButton:
598
+ lbl_pt = (ev.position().toPoint() if obj is self.label
599
+ else self.label.mapFrom(self.scroll.viewport(), ev.position().toPoint()))
600
+ k = self._k_from_label_point(lbl_pt)
601
+ if k is not None:
602
+ self._sym_u = k
603
+ self.editor.setSymmetryPoint(k * 360.0, 0)
604
+ self._rebuild_from_params()
605
+ ev.accept(); return True
606
+
607
+ # Keep Ctrl+single-click support too
608
+ if (ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton
609
+ and (ev.modifiers() & Qt.KeyboardModifier.ControlModifier)):
610
+ lbl_pt = (ev.position().toPoint() if obj is self.label
611
+ else self.label.mapFrom(self.scroll.viewport(), ev.position().toPoint()))
612
+ k = self._k_from_label_point(lbl_pt)
613
+ if k is not None:
614
+ self._sym_u = k
615
+ self.editor.setSymmetryPoint(k * 360.0, 0)
616
+ self._rebuild_from_params()
617
+ ev.accept(); return True
618
+
619
+ # --- existing zoom/pan handling (unchanged) ---
620
+ if obj is self.scroll.viewport():
621
+ if ev.type() == QEvent.Type.Wheel and (ev.modifiers() & Qt.KeyboardModifier.ControlModifier):
622
+ self._set_zoom(self._zoom * (1.25 if ev.angleDelta().y() > 0 else 0.8))
623
+ ev.accept(); return True
624
+ if ev.type() == QEvent.Type.MouseButtonPress and ev.button() == Qt.MouseButton.LeftButton:
625
+ self._panning = True; self._pan_start = ev.position()
626
+ self.scroll.viewport().setCursor(Qt.CursorShape.ClosedHandCursor)
627
+ ev.accept(); return True
628
+ if ev.type() == QEvent.Type.MouseMove and self._panning:
629
+ d = ev.position() - self._pan_start
630
+ h = self.scroll.horizontalScrollBar(); v = self.scroll.verticalScrollBar()
631
+ h.setValue(h.value() - int(d.x())); v.setValue(v.value() - int(d.y()))
632
+ self._pan_start = ev.position()
633
+ ev.accept(); return True
634
+ if ev.type() == QEvent.Type.MouseButtonRelease and ev.button() == Qt.MouseButton.LeftButton:
635
+ self._panning = False
636
+ self.scroll.viewport().setCursor(Qt.CursorShape.ArrowCursor)
637
+ ev.accept(); return True
638
+
639
+ return super().eventFilter(obj, ev)
640
+
641
+ def _on_preview_mouse_moved(self, x: float, y: float):
642
+ if self._panning or self._preview_img is None or self._pix is None:
643
+ return
644
+ ix = int(x / max(self._zoom, 1e-6))
645
+ iy = int(y / max(self._zoom, 1e-6))
646
+ ix = max(0, min(self._pix.width() - 1, ix))
647
+ iy = max(0, min(self._pix.height() - 1, iy))
648
+
649
+ img = self._preview_img
650
+ if img.ndim == 2 or (img.ndim == 3 and img.shape[2] == 1):
651
+ v = float(img[iy, ix] if img.ndim == 2 else img[iy, ix, 0])
652
+ v = float(np.clip(v, 0.0, 1.0))
653
+ self.editor.updateValueLines(v, 0.0, 0.0, grayscale=True)
654
+ else:
655
+ r, g, b = img[iy, ix, 0], img[iy, ix, 1], img[iy, ix, 2]
656
+ r = float(np.clip(r, 0.0, 1.0)); g = float(np.clip(g, 0.0, 1.0)); b = float(np.clip(b, 0.0, 1.0))
657
+ self.editor.updateValueLines(r, g, b, grayscale=False)
658
+
659
+ # --- mask helpers ---------------------------------------------------
660
+ def _active_mask_layer(self):
661
+ """Return (mask_float01, mask_id, mask_name) or (None, None, None)."""
662
+ mid = getattr(self.doc, "active_mask_id", None)
663
+ if not mid: return None, None, None
664
+ layer = getattr(self.doc, "masks", {}).get(mid)
665
+ if layer is None: return None, None, None
666
+ m = np.asarray(getattr(layer, "data", None))
667
+ if m is None or m.size == 0: return None, None, None
668
+ m = m.astype(np.float32, copy=False)
669
+ if m.dtype.kind in "ui":
670
+ m /= float(np.iinfo(m.dtype).max)
671
+ else:
672
+ mx = float(m.max()) if m.size else 1.0
673
+ if mx > 1.0: m /= mx
674
+ return np.clip(m, 0.0, 1.0), mid, getattr(layer, "name", "Mask")
675
+
676
+ def _resample_mask_if_needed(self, mask: np.ndarray, out_hw: tuple[int,int]) -> np.ndarray:
677
+ """Nearest-neighbor resize via integer indexing."""
678
+ mh, mw = mask.shape[:2]
679
+ th, tw = out_hw
680
+ if (mh, mw) == (th, tw): return mask
681
+ yi = np.linspace(0, mh - 1, th).astype(np.int32)
682
+ xi = np.linspace(0, mw - 1, tw).astype(np.int32)
683
+ return mask[yi][:, xi]
684
+
685
+ def _blend_with_mask(self, processed: np.ndarray) -> np.ndarray:
686
+ """
687
+ Blend processed image with original using active mask (if any).
688
+ Chooses original from preview/full buffers to match shape.
689
+ """
690
+ mask, _mid, _mname = self._active_mask_layer()
691
+ if mask is None:
692
+ return processed
693
+
694
+ out = processed.astype(np.float32, copy=False)
695
+
696
+ # choose the matching original buffer (same HxW as 'out')
697
+ if (hasattr(self, "_full_img") and self._full_img is not None
698
+ and out.shape[:2] == self._full_img.shape[:2]):
699
+ src = self._full_img
700
+ else:
701
+ src = self._preview_img
702
+
703
+ m = self._resample_mask_if_needed(mask, out.shape[:2])
704
+ if out.ndim == 3 and out.shape[2] == 3:
705
+ m = m[..., None]
706
+
707
+ # reconcile mono vs RGB
708
+ if src.ndim == 2 and out.ndim == 3:
709
+ src = np.stack([src]*3, axis=-1)
710
+ elif src.ndim == 3 and out.ndim == 2:
711
+ src = src[..., 0]
712
+
713
+ return (m * out + (1.0 - m) * src).astype(np.float32, copy=False)
714
+
715
+ def _reset(self):
716
+ for s in (self.sA, self.sB, self.sG, self.sLP, self.sHP):
717
+ s.blockSignals(True)
718
+ try:
719
+ self.sA.setValue(50); self.sB.setValue(50); self.sG.setValue(100)
720
+ self.sLP.setValue(0); self.sHP.setValue(0)
721
+ finally:
722
+ for s in (self.sA, self.sB, self.sG, self.sLP, self.sHP):
723
+ s.blockSignals(False)
724
+
725
+ self._sym_u = 0.5
726
+ self.editor.clearSymmetryLine()
727
+ self._rebuild_from_params() # immediate
728
+