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,760 @@
1
+ # pro/histogram.py
2
+ from __future__ import annotations
3
+ import numpy as np
4
+
5
+ from PyQt6.QtCore import Qt, QSettings, QTimer, QEvent, pyqtSignal
6
+ from PyQt6.QtWidgets import (
7
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QSlider, QPushButton, QScrollArea,
8
+ QTableWidget, QTableWidgetItem, QMessageBox, QToolButton, QInputDialog, QSplitter, QSizePolicy, QHeaderView
9
+ )
10
+ from PyQt6.QtGui import QPixmap, QPainter, QPen, QColor, QFont, QPalette
11
+
12
+ # Shared utilities
13
+ from setiastro.saspro.widgets.image_utils import to_float01 as _to_float01
14
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
15
+
16
+ def _to_float_preserve(img):
17
+ if img is None: return None
18
+ a = np.asarray(img)
19
+ return a.astype(np.float32, copy=False) if a.dtype != np.float32 else a
20
+
21
+
22
+
23
+ class HistogramDialog(QDialog):
24
+ """
25
+ Per-document histogram (non-modal).
26
+ - Connects to ImageDocument.changed and repaints automatically.
27
+ - Multiple dialogs can be open at once (each bound to one doc).
28
+ """
29
+ pivotPicked = pyqtSignal(float) # normalized [0..1] x position for GHS pivot
30
+ def __init__(self, parent, document):
31
+ super().__init__(parent)
32
+ self.setWindowTitle(self.tr("Histogram"))
33
+ self.setWindowFlag(Qt.WindowType.Window, True)
34
+ self.setWindowModality(Qt.WindowModality.NonModal)
35
+ self.setModal(False)
36
+ try:
37
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
38
+ except Exception:
39
+ pass # older PyQt6 versions
40
+ self.doc = document
41
+ self.image = _to_float_preserve(document.image)
42
+
43
+ self.zoom_factor = 1.0 # 1.0 = 100%
44
+ self.log_scale = False # log X
45
+ self.log_y = False # log Y
46
+ #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
47
+ self._eps_log = 1e-6 # first log bin edge (for labels)
48
+
49
+ # for mapping clicks → normalized x
50
+ self._click_mapping = None # dict or None
51
+ self.settings = QSettings()
52
+ self.sensor_max01 = 1.0
53
+ self.sensor_native_max = None # user ADU max (e.g., 65532)
54
+ self.native_theoretical_max = None
55
+
56
+ # histogram cache
57
+ self._bin_count = 512
58
+ self._bin_edges_lin = None # np.ndarray | None
59
+ self._bin_edges_log = None # np.ndarray | None
60
+ self._counts_lin = None # list[np.ndarray] | None
61
+ self._counts_log = None # list[np.ndarray] | None
62
+ self._is_color = False
63
+ self._eps_log = 1e-6 # first log bin edge (for labels)
64
+
65
+ self._load_sensor_max_setting()
66
+ self._build_ui()
67
+
68
+ # debounce timer for resize / splitter moves
69
+ self._resize_timer = QTimer(self)
70
+ self._resize_timer.setSingleShot(True)
71
+ self._resize_timer.setInterval(80) # ms; tweak if you want snappier/slower
72
+ self._resize_timer.timeout.connect(self._draw_histogram)
73
+
74
+ # prime histogram & stats from initial image
75
+ self._recompute_hist_cache()
76
+ self._update_stats()
77
+
78
+ # wire up to this specific document
79
+ self.doc.changed.connect(self._on_doc_changed)
80
+ # If the doc object goes away, close this dialog
81
+ self.doc.destroyed.connect(self.deleteLater)
82
+
83
+ self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
84
+ self._doc_conn = False
85
+ if getattr(self, "doc", None) is not None:
86
+ try:
87
+ self.doc.destroyed.connect(self._on_doc_destroyed)
88
+ self._doc_conn = True
89
+ except Exception:
90
+ pass
91
+
92
+ # Do the first draw once the widget has a real size
93
+ QTimer.singleShot(0, self._draw_histogram)
94
+
95
+
96
+
97
+ # ---------- UI ----------
98
+ def _build_ui(self):
99
+ # Make it start at a sensible size
100
+ self.setMinimumSize(800, 400)
101
+ self.resize(900, 500)
102
+
103
+ main_layout = QVBoxLayout(self)
104
+
105
+ # --- top area: splitter with histogram + stats ---
106
+ splitter = QSplitter(Qt.Orientation.Horizontal, self)
107
+
108
+ # left: scroll area + label for the pixmap
109
+ self.scroll_area = QScrollArea(self)
110
+ self.scroll_area.setWidgetResizable(True)
111
+ self.scroll_area.setSizePolicy(
112
+ QSizePolicy.Policy.Expanding,
113
+ QSizePolicy.Policy.Expanding,
114
+ )
115
+
116
+ self.hist_label = QLabel(self)
117
+ self.hist_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
118
+ self.scroll_area.setWidget(self.hist_label)
119
+ self.hist_label.installEventFilter(self)
120
+ self.hist_label.setToolTip(self.tr(
121
+ "Ctrl+Click on the histogram to send that intensity as the "
122
+ "pivot to Hyperbolic Stretch (if open)."
123
+ ))
124
+ self.scroll_area.viewport().installEventFilter(self)
125
+
126
+ splitter.addWidget(self.scroll_area)
127
+
128
+ # right: stats table
129
+ self.stats_table = QTableWidget(self)
130
+ self.stats_table.setRowCount(7)
131
+ self.stats_table.setColumnCount(1)
132
+ self.stats_table.setVerticalHeaderLabels([
133
+ self.tr("Min"), self.tr("Max"), self.tr("Median"), self.tr("StdDev"),
134
+ self.tr("MAD"), self.tr("Low Clipped"), self.tr("High Clipped")
135
+ ])
136
+
137
+ # Let it grow/shrink with the splitter
138
+ self.stats_table.setMinimumWidth(320)
139
+ self.stats_table.setSizePolicy(
140
+ QSizePolicy.Policy.Preferred, # <- was Fixed
141
+ QSizePolicy.Policy.Expanding,
142
+ )
143
+
144
+ # Make the columns use available width nicely
145
+ hdr = self.stats_table.horizontalHeader()
146
+ hdr.setSectionResizeMode(QHeaderView.ResizeMode.Stretch)
147
+ # hdr.setStretchLastSection(True)
148
+ splitter.addWidget(self.stats_table)
149
+
150
+ # Give more space to histogram side by default
151
+ splitter.setStretchFactor(0, 3)
152
+ splitter.setStretchFactor(1, 1)
153
+ # Explicit initial sizes so it doesn't start with a tiny histogram
154
+ splitter.setSizes([650, 250])
155
+
156
+ QTimer.singleShot(0, self._adjust_stats_width)
157
+
158
+ main_layout.addWidget(splitter)
159
+
160
+ # --- controls row (unchanged except for being below splitter) ---
161
+ ctl = QHBoxLayout()
162
+ self.zoom_slider = QSlider(Qt.Orientation.Horizontal, self)
163
+ self.zoom_slider.setRange(50, 1000)
164
+ self.zoom_slider.setValue(100)
165
+ self.zoom_slider.setTickInterval(10)
166
+ self.zoom_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
167
+ self.zoom_slider.valueChanged.connect(self._on_zoom_changed)
168
+
169
+ ctl.addWidget(QLabel(self.tr("Zoom:")))
170
+ ctl.addWidget(self.zoom_slider)
171
+
172
+ self.btn_logx = QPushButton(self.tr("Toggle Log X-Axis"), self)
173
+ self.btn_logx.setCheckable(True)
174
+ self.btn_logx.toggled.connect(self._toggle_log_x)
175
+ ctl.addWidget(self.btn_logx)
176
+
177
+ self.btn_logy = QPushButton(self.tr("Toggle Log Y-Axis"), self)
178
+ self.btn_logy.setCheckable(True)
179
+ self.btn_logy.toggled.connect(self._toggle_log_y)
180
+ ctl.addWidget(self.btn_logy)
181
+
182
+ self.btn_sensor_max = QToolButton(self)
183
+ self.btn_sensor_max.setText("?")
184
+ self.btn_sensor_max.setToolTip(self.tr(
185
+ "Set your camera's true saturation level for clipping warnings.\n"
186
+ "Tip: take an overexposed frame and see its max ADU."
187
+ ))
188
+ self.btn_sensor_max.clicked.connect(self._prompt_sensor_max)
189
+ ctl.addWidget(self.btn_sensor_max)
190
+
191
+ main_layout.addLayout(ctl)
192
+
193
+ btn_close = QPushButton(self.tr("Close"), self)
194
+ btn_close.clicked.connect(self.accept)
195
+ main_layout.addWidget(btn_close)
196
+
197
+ self.setLayout(main_layout)
198
+
199
+
200
+
201
+ # ---------- slots ----------
202
+ def _on_doc_changed(self):
203
+ self.image = _to_float_preserve(self.doc.image)
204
+ self._recompute_hist_cache()
205
+ self._update_stats()
206
+ self._draw_histogram()
207
+
208
+ def _on_zoom_changed(self, v: int):
209
+ self.zoom_factor = v / 100.0
210
+ self._draw_histogram()
211
+
212
+ def _toggle_log_x(self, on: bool):
213
+ self.log_scale = bool(on)
214
+ self._draw_histogram()
215
+
216
+ def _toggle_log_y(self, on: bool):
217
+ self.log_y = bool(on)
218
+ self._draw_histogram()
219
+
220
+ # ---------- drawing ----------
221
+ # ---------- drawing ----------
222
+ def _draw_histogram(self):
223
+ # nothing to draw yet
224
+ if self.image is None or self._bin_edges_lin is None:
225
+ self.hist_label.clear()
226
+ return
227
+
228
+ # use available size in the scroll area's viewport
229
+ if self.scroll_area is not None:
230
+ vp = self.scroll_area.viewport()
231
+ avail_w = max(200, vp.width())
232
+ avail_h = max(200, vp.height())
233
+ else:
234
+ avail_w = 512
235
+ avail_h = 300
236
+
237
+ base_width = avail_w
238
+ height = avail_h
239
+ width = int(base_width * self.zoom_factor)
240
+
241
+ # layout margins
242
+ left_margin = 32 # room for Y labels
243
+ top_margin = 12 # room so top ticks/text aren't clipped
244
+ bottom_margin = 24 # room for X labels
245
+ axis_y = height - bottom_margin
246
+ usable_h = max(1, axis_y - top_margin)
247
+ plot_width = max(1, width - left_margin)
248
+
249
+ # choose edges + raw counts from cache
250
+ if self.log_scale:
251
+ bin_edges = self._bin_edges_log
252
+ counts_list = self._counts_log
253
+ else:
254
+ bin_edges = self._bin_edges_lin
255
+ counts_list = self._counts_lin
256
+
257
+ if bin_edges is None or counts_list is None:
258
+ self.hist_label.clear()
259
+ return
260
+
261
+ bin_count = len(bin_edges) - 1
262
+
263
+ # precompute log range if needed
264
+ if self.log_scale:
265
+ # guard: avoid log10(<=0)
266
+ be0 = float(bin_edges[0])
267
+ if be0 <= 0:
268
+ be0 = self._eps_log
269
+ log_min = np.log10(be0)
270
+ log_max = 0.0
271
+ else:
272
+ log_min = None
273
+ log_max = None
274
+
275
+ # map X-domain edge → pixel X
276
+ def x_pos(edge: float) -> int:
277
+ if self.log_scale:
278
+ if edge <= 0:
279
+ edge = self._eps_log
280
+ if abs(log_max - log_min) < 1e-12:
281
+ return left_margin
282
+ return left_margin + int(
283
+ (np.log10(edge) - log_min) / (log_max - log_min) * plot_width
284
+ )
285
+ else:
286
+ return left_margin + int(edge * plot_width)
287
+
288
+ # --- convert counts → display values (linear or log Y) ---
289
+ vals_list: list[np.ndarray] = []
290
+ max_val = 0.0
291
+ for counts in counts_list:
292
+ if self.log_y:
293
+ vals = np.log10(counts + 1.0)
294
+ else:
295
+ vals = counts.astype(np.float32)
296
+ if vals.size:
297
+ max_val = max(max_val, float(vals.max()))
298
+ vals_list.append(vals)
299
+
300
+ if max_val <= 0:
301
+ max_val = 1.0
302
+
303
+ # theme colors
304
+ pal = self.window().palette() if self.window() else self.palette()
305
+ bg_color = pal.color(QPalette.ColorRole.Window)
306
+ text_color = pal.color(QPalette.ColorRole.Text)
307
+
308
+ if bg_color.lightness() < 128:
309
+ axis_color = QColor(210, 210, 210)
310
+ label_color = QColor(245, 245, 245)
311
+ else:
312
+ axis_color = QColor(40, 40, 40)
313
+ label_color = text_color
314
+
315
+ grid_color = QColor(axis_color)
316
+ grid_color.setAlpha(60)
317
+ grid_pen = QPen(grid_color)
318
+ grid_pen.setWidth(1)
319
+
320
+ pm = QPixmap(width, height)
321
+ pm.fill(bg_color)
322
+ p = QPainter(pm)
323
+ p.setRenderHint(QPainter.RenderHint.Antialiasing)
324
+
325
+ # helper: map normalized [0,1] → Y pixel (0 at bottom, 1 at top)
326
+ def y_pos(norm: float) -> int:
327
+ # norm in [0,1], map 0→axis_y, 1→top_margin
328
+ return int(top_margin + (1.0 - norm) * usable_h)
329
+
330
+ # ----- draw bars -----
331
+ if self._is_color:
332
+ colors = [
333
+ QColor(255, 0, 0, 140),
334
+ QColor(0, 180, 0, 140),
335
+ QColor(0, 0, 255, 140),
336
+ ]
337
+ for ch_idx, vals in enumerate(vals_list):
338
+ hn = vals / max_val
339
+ p.setPen(QPen(colors[ch_idx]))
340
+ for i in range(bin_count):
341
+ x0 = x_pos(float(bin_edges[i]))
342
+ x1 = x_pos(float(bin_edges[i + 1]))
343
+ w = max(1, x1 - x0)
344
+ h = int(hn[i] * usable_h)
345
+ y0 = axis_y - h
346
+ p.drawRect(x0, y0, w, h)
347
+ else:
348
+ vals = vals_list[0]
349
+ hn = vals / max_val
350
+ p.setPen(QPen(axis_color))
351
+ for i in range(bin_count):
352
+ x0 = x_pos(float(bin_edges[i]))
353
+ x1 = x_pos(float(bin_edges[i + 1]))
354
+ w = max(1, x1 - x0)
355
+ h = int(hn[i] * usable_h)
356
+ y0 = axis_y - h
357
+ p.drawRect(x0, y0, w, h)
358
+
359
+ # ----- axes -----
360
+ p.setPen(QPen(axis_color, 2))
361
+ # X axis at axis_y, Y axis from top_margin down to axis_y
362
+ p.drawLine(left_margin, axis_y, width - 1, axis_y)
363
+ p.drawLine(left_margin, top_margin, left_margin, axis_y)
364
+
365
+ p.setFont(QFont("Arial", 10))
366
+
367
+ # ----- X ticks + grid -----
368
+ if self.log_scale:
369
+ ticks = np.logspace(np.log10(bin_edges[0]), 0.0, 11)
370
+ for t in ticks:
371
+ x = x_pos(float(t))
372
+ if left_margin < x < width - 1:
373
+ p.setPen(grid_pen)
374
+ p.drawLine(x, top_margin, x, axis_y)
375
+ p.setPen(axis_color)
376
+ p.drawLine(x, axis_y, x, axis_y - 5)
377
+ p.setPen(label_color)
378
+ p.drawText(x - 18, axis_y + bottom_margin - 8, f"{t:.3f}")
379
+ else:
380
+ ticks = np.linspace(0.0, 1.0, 11)
381
+ for t in ticks:
382
+ x = x_pos(float(t))
383
+ if left_margin < x < width - 1:
384
+ p.setPen(grid_pen)
385
+ p.drawLine(x, top_margin, x, axis_y)
386
+ p.setPen(axis_color)
387
+ p.drawLine(x, axis_y, x, axis_y - 5)
388
+ p.setPen(label_color)
389
+ p.drawText(x - 10, axis_y + bottom_margin - 8, f"{t:.1f}")
390
+
391
+ # ----- Y ticks + grid -----
392
+ n_yticks = 6
393
+ if self.log_y:
394
+ exps = np.linspace(0.0, max_val, n_yticks)
395
+ norms = exps / max_val
396
+ labels = [f"{10**e:.0f}" for e in exps]
397
+ else:
398
+ vals_for_ticks = np.linspace(0.0, max_val, n_yticks)
399
+ norms = vals_for_ticks / max_val
400
+ labels = [f"{v:.0f}" for v in vals_for_ticks]
401
+
402
+ for i, (yn, lab) in enumerate(zip(norms, labels)):
403
+ y = y_pos(float(yn))
404
+ if 0 < i < n_yticks - 1:
405
+ p.setPen(grid_pen)
406
+ p.drawLine(left_margin, y, width - 1, y)
407
+ p.setPen(axis_color)
408
+ p.drawLine(left_margin - 5, y, left_margin, y)
409
+ p.setPen(label_color)
410
+ p.drawText(2, y + 4, lab)
411
+
412
+ # --- draw effective-max marker if user set one ---
413
+ if self.sensor_max01 < 0.9999:
414
+ x = x_pos(self.sensor_max01)
415
+ p.setPen(QPen(QColor(220, 0, 0), 2, Qt.PenStyle.DashLine))
416
+ p.drawLine(x, top_margin, x, axis_y)
417
+ p.drawText(min(x + 4, width - 80), top_margin + 12,
418
+ self.tr("True Max {0:.4f}").format(self.sensor_max01))
419
+ # store mapping info for Ctrl+click → normalized x
420
+ try:
421
+ self._click_mapping = {
422
+ "left_margin": left_margin,
423
+ "plot_width": plot_width,
424
+ "axis_y": axis_y,
425
+ "top_margin": top_margin,
426
+ "height": height,
427
+ "log_scale": bool(self.log_scale),
428
+ "log_min": log_min,
429
+ "log_max": log_max,
430
+ }
431
+ except Exception:
432
+ self._click_mapping = None
433
+ p.end()
434
+ self.hist_label.setPixmap(pm)
435
+ self.hist_label.resize(pm.size())
436
+
437
+ def _x_pix_to_u(self, x_pix: int) -> float | None:
438
+ """
439
+ Map a horizontal pixel coordinate (in the label) to a normalized
440
+ intensity in [0..1], respecting linear / log X modes.
441
+ """
442
+ m = self._click_mapping
443
+ if not m:
444
+ return None
445
+
446
+ left = m["left_margin"]
447
+ width = max(1, m["plot_width"])
448
+ if x_pix < left or x_pix > left + width:
449
+ return None
450
+
451
+ t = (x_pix - left) / float(width)
452
+ t = max(0.0, min(1.0, t))
453
+
454
+ if not m["log_scale"]:
455
+ # linear: domain is already [0..1]
456
+ return float(t)
457
+
458
+ # log X: t in [0..1] corresponds to [10^log_min .. 10^log_max] (log_max ~ 0)
459
+ log_min = m.get("log_min", None)
460
+ log_max = m.get("log_max", None)
461
+ if log_min is None or log_max is None or abs(log_max - log_min) < 1e-12:
462
+ return float(t)
463
+
464
+ log_v = log_min + t * (log_max - log_min)
465
+ v = 10.0 ** log_v
466
+ # v is in (eps .. 1]; clamp to [0..1]
467
+ return float(max(0.0, min(1.0, v)))
468
+
469
+
470
+ def _recompute_hist_cache(self):
471
+ """Compute histograms once for the current image.
472
+
473
+ This is called when the document image changes. Resizing / zooming
474
+ will only redraw using this cached data.
475
+ """
476
+ img = self.image
477
+ self._bin_edges_lin = None
478
+ self._bin_edges_log = None
479
+ self._counts_lin = None
480
+ self._counts_log = None
481
+ self._is_color = False
482
+ self._eps_log = 1e-6
483
+
484
+ if img is None:
485
+ return
486
+
487
+ a = img
488
+ if a.ndim == 3 and a.shape[2] == 1:
489
+ a = a[..., 0]
490
+
491
+ if a.ndim == 3 and a.shape[2] == 3:
492
+ chans = [a[..., i] for i in range(3)]
493
+ self._is_color = True
494
+ else:
495
+ chan = a if a.ndim == 2 else a[..., 0]
496
+ chans = [chan]
497
+ self._is_color = False
498
+
499
+ bin_count = self._bin_count
500
+
501
+ # --- linear X bins ---
502
+ bin_edges_lin = np.linspace(0.0, 1.0, bin_count + 1).astype(np.float32)
503
+ counts_lin: list[np.ndarray] = []
504
+ for c in chans:
505
+ counts, _ = np.histogram(c.ravel(), bins=bin_edges_lin)
506
+ counts_lin.append(counts.astype(np.float32))
507
+
508
+ # --- log X bins ---
509
+ pos = a[a > 0]
510
+ eps = max(1e-6, float(pos.min())) if pos.size else 1e-6
511
+ log_min, log_max = np.log10(eps), 0.0
512
+ if abs(log_max - log_min) < 1e-12:
513
+ bin_edges_log = np.linspace(eps, 1.0, bin_count + 1).astype(np.float32)
514
+ else:
515
+ bin_edges_log = np.logspace(log_min, log_max, bin_count + 1).astype(np.float32)
516
+
517
+ counts_log: list[np.ndarray] = []
518
+ for c in chans:
519
+ counts, _ = np.histogram(c.ravel(), bins=bin_edges_log)
520
+ counts_log.append(counts.astype(np.float32))
521
+
522
+ self._bin_edges_lin = bin_edges_lin
523
+ self._bin_edges_log = bin_edges_log
524
+ self._counts_lin = counts_lin
525
+ self._counts_log = counts_log
526
+ self._eps_log = float(eps)
527
+
528
+
529
+ def _schedule_redraw(self):
530
+ # Only bother if visible; restart timer each time
531
+ if self.isVisible():
532
+ self._resize_timer.start()
533
+
534
+ def resizeEvent(self, event):
535
+ super().resizeEvent(event)
536
+ self._schedule_redraw()
537
+
538
+ def eventFilter(self, obj, event):
539
+ # Ctrl+click on the histogram pixmap → emit pivotPicked(u)
540
+ if obj is self.hist_label and event.type() == QEvent.Type.MouseButtonPress:
541
+ if (event.button() == Qt.MouseButton.LeftButton and
542
+ (event.modifiers() & Qt.KeyboardModifier.ControlModifier)):
543
+ pos = event.position().toPoint()
544
+ u = self._x_pix_to_u(pos.x())
545
+ if u is not None:
546
+ # emit normalized pivot in [0..1]
547
+ self.pivotPicked.emit(u)
548
+ event.accept()
549
+ return True
550
+
551
+ # When the splitter moves, the scroll_area viewport gets a Resize event
552
+ if self.scroll_area is not None and obj is self.scroll_area.viewport():
553
+ if event.type() == QEvent.Type.Resize:
554
+ self._schedule_redraw()
555
+
556
+ return super().eventFilter(obj, event)
557
+
558
+ def _update_stats(self):
559
+ if self.image is None:
560
+ return
561
+
562
+ img = self.image
563
+ # determine channels
564
+ if img.ndim == 3 and img.shape[2] == 3:
565
+ chans = [img[..., i] for i in range(3)]
566
+ self.stats_table.setColumnCount(3)
567
+ self.stats_table.setHorizontalHeaderLabels(["R", "G", "B"])
568
+ else:
569
+ chan = img if img.ndim == 2 else img[..., 0]
570
+ chans = [chan]
571
+ self.stats_table.setColumnCount(1)
572
+ self.stats_table.setHorizontalHeaderLabels(["Gray"])
573
+
574
+ eps = 1e-6 # tolerance for "exactly 0/1" after float ops
575
+
576
+ row_defs = [
577
+ (self.tr("Min"), lambda c: float(np.min(c)), "{:.4f}"),
578
+ (self.tr("Max"), lambda c: float(np.max(c)), "{:.4f}"),
579
+ (self.tr("Median"), lambda c: float(np.median(c)), "{:.4f}"),
580
+ (self.tr("StdDev"), lambda c: float(np.std(c)), "{:.4f}"),
581
+ (self.tr("MAD"), lambda c: float(np.median(np.abs(c - np.median(c)))), "{:.4f}"),
582
+ (self.tr("Low Clipped"), lambda c: _clip_fmt(c, low=True, eps=eps), "{}"),
583
+ (self.tr("High Clipped"), lambda c: _clip_fmt(c, low=False, eps=eps), "{}"),
584
+ ]
585
+
586
+ def _clip_fmt(c, low: bool, eps: float):
587
+ flat = np.ravel(c)
588
+ n = flat.size if flat.size else 1
589
+ if low:
590
+ k = int(np.count_nonzero(flat <= eps))
591
+ else:
592
+ hi_thr = max(eps, self.sensor_max01 - eps)
593
+ k = int(np.count_nonzero(flat >= hi_thr))
594
+ pct = 100.0 * k / n
595
+ return f"{k} ({pct:.3f}%)"
596
+
597
+ # apply labels + sizes
598
+ self.stats_table.setRowCount(len(row_defs))
599
+ self.stats_table.setVerticalHeaderLabels([lab for lab, _, _ in row_defs])
600
+
601
+ # fill cells
602
+ for r, (lab, fn, fmt) in enumerate(row_defs):
603
+ for c_idx, c_arr in enumerate(chans):
604
+ val = fn(c_arr)
605
+ text = fmt.format(val)
606
+ it = QTableWidgetItem(text)
607
+ it.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
608
+
609
+ # --- visual pop for non-trivial clipping ---
610
+ if lab in (self.tr("Low Clipped"), self.tr("High Clipped")):
611
+ # text looks like: "123 (0.456%)"
612
+ try:
613
+ pct_str = text.split("(")[1].split("%")[0]
614
+ pct = float(pct_str)
615
+ except Exception:
616
+ pct = 0.0
617
+
618
+ # thresholds you can tweak
619
+ # <0.01%: ignore
620
+ # 0.01–0.1%: mild warning
621
+ # 0.1–1%: clear warning
622
+ # >1%: strong warning
623
+ if pct >= 1.0:
624
+ it.setBackground(QColor(100, 30, 30)) # strong red tint
625
+ elif pct >= 0.1:
626
+ it.setBackground(QColor(70, 30, 30)) # medium red tint
627
+ elif pct >= 0.01:
628
+ it.setBackground(QColor(40, 30, 30)) # mild red tint
629
+
630
+ self.stats_table.setItem(r, c_idx, it)
631
+
632
+ self._adjust_stats_width()
633
+
634
+ def _theoretical_native_max_from_meta(self):
635
+ meta = getattr(self.doc, "metadata", None) or {}
636
+ bd = str(meta.get("bit_depth", "")).lower()
637
+
638
+ if "16-bit" in bd:
639
+ return 65535
640
+ if "8-bit" in bd:
641
+ return 255
642
+ if "32-bit unsigned" in bd:
643
+ return 4294967295
644
+ return None
645
+
646
+ def _settings_key_for_native_max(self, native_theoretical_max):
647
+ if native_theoretical_max == 65535:
648
+ return "histogram/sensor_max_native_16"
649
+ if native_theoretical_max == 255:
650
+ return "histogram/sensor_max_native_8"
651
+ if native_theoretical_max == 4294967295:
652
+ return "histogram/sensor_max_native_32u"
653
+ return "histogram/sensor_max_native_generic"
654
+
655
+ def _load_sensor_max_setting(self):
656
+ self.native_theoretical_max = self._theoretical_native_max_from_meta()
657
+ if self.native_theoretical_max:
658
+ key = self._settings_key_for_native_max(self.native_theoretical_max)
659
+ val = self.settings.value(key, None)
660
+ if val is not None:
661
+ try:
662
+ self.sensor_native_max = float(val)
663
+ except Exception:
664
+ self.sensor_native_max = None
665
+
666
+ self._recompute_effective_max01()
667
+
668
+ def _recompute_effective_max01(self):
669
+ if self.native_theoretical_max and self.sensor_native_max:
670
+ self.sensor_max01 = float(self.sensor_native_max) / float(self.native_theoretical_max)
671
+ self.sensor_max01 = float(np.clip(self.sensor_max01, 1e-6, 1.0))
672
+ else:
673
+ self.sensor_max01 = 1.0
674
+
675
+ def _prompt_sensor_max(self):
676
+ self.native_theoretical_max = self._theoretical_native_max_from_meta()
677
+
678
+ if self.native_theoretical_max:
679
+ key = self._settings_key_for_native_max(self.native_theoretical_max)
680
+ current = self.sensor_native_max or self.native_theoretical_max
681
+
682
+ val, ok = QInputDialog.getInt(
683
+ self,
684
+ self.tr("Sensor True Max (ADU)"),
685
+ self.tr("Enter your sensor's true saturation value in native ADU.\n"
686
+ "(Typical max for this file type is {0})\n\n"
687
+ "You can measure this by taking a deliberately overexposed frame\n"
688
+ "and reading its maximum pixel value.").format(self.native_theoretical_max),
689
+ int(current),
690
+ 1,
691
+ int(self.native_theoretical_max)
692
+ )
693
+ if ok:
694
+ self.sensor_native_max = float(val)
695
+ self.settings.setValue(key, float(val))
696
+ else:
697
+ # float images / unknown depth: allow normalized max
698
+ val, ok = QInputDialog.getDouble(
699
+ self,
700
+ self.tr("Histogram Effective Max"),
701
+ self.tr("Enter effective maximum for clipping (normalized units)."),
702
+ float(self.sensor_max01),
703
+ 1e-6,
704
+ 1.0,
705
+ 6
706
+ )
707
+ if ok:
708
+ self.sensor_max01 = float(val)
709
+ self.settings.setValue("histogram/sensor_max01_generic", float(val))
710
+
711
+ self._recompute_effective_max01()
712
+ self._update_stats() # High Clipped row depends on sensor_max01
713
+ self._draw_histogram()
714
+
715
+ def _adjust_stats_width(self):
716
+ """Resize stats table so all columns are visible without a scrollbar."""
717
+ if not self.stats_table:
718
+ return
719
+
720
+ # Let Qt compute natural column widths
721
+ self.stats_table.resizeColumnsToContents()
722
+ self.stats_table.resizeRowsToContents()
723
+
724
+ vh = self.stats_table.verticalHeader()
725
+ frame = self.stats_table.frameWidth()
726
+
727
+ total_w = vh.width() + 2 * frame
728
+
729
+ for col in range(self.stats_table.columnCount()):
730
+ total_w += self.stats_table.columnWidth(col)
731
+
732
+ # Room for a possible vertical scrollbar
733
+ vbar = self.stats_table.verticalScrollBar()
734
+ if vbar is not None:
735
+ total_w += vbar.sizeHint().width()
736
+
737
+ # A tiny padding so text isn't tight
738
+ total_w += 6
739
+
740
+ self.stats_table.setMinimumWidth(total_w)
741
+
742
+
743
+ def _on_doc_destroyed(self, *args):
744
+ # Called when the owner/document goes away.
745
+ try:
746
+ # Avoid re-entrancy; schedule deletion safely.
747
+ self.deleteLater()
748
+ except RuntimeError:
749
+ pass
750
+
751
+ def closeEvent(self, event):
752
+ # Cleanly disconnect to avoid stray callbacks.
753
+ if getattr(self, "_doc_conn", False) and getattr(self, "doc", None) is not None:
754
+ try:
755
+ self.doc.destroyed.disconnect(self._on_doc_destroyed)
756
+ except (TypeError, RuntimeError):
757
+ pass
758
+ self._doc_conn = False
759
+
760
+ super().closeEvent(event)