setiastrosuitepro 1.6.5.post3__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.
Files changed (368) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/data/SASP_data.fits +0 -0
  3. setiastro/data/catalogs/List_of_Galaxies_with_Distances_Gly.csv +488 -0
  4. setiastro/data/catalogs/astrobin_filters.csv +890 -0
  5. setiastro/data/catalogs/astrobin_filters_page1_local.csv +51 -0
  6. setiastro/data/catalogs/cali2.csv +63 -0
  7. setiastro/data/catalogs/cali2color.csv +65 -0
  8. setiastro/data/catalogs/celestial_catalog - original.csv +16471 -0
  9. setiastro/data/catalogs/celestial_catalog.csv +24031 -0
  10. setiastro/data/catalogs/detected_stars.csv +24784 -0
  11. setiastro/data/catalogs/fits_header_data.csv +46 -0
  12. setiastro/data/catalogs/test.csv +8 -0
  13. setiastro/data/catalogs/updated_celestial_catalog.csv +16471 -0
  14. setiastro/images/Astro_Spikes.png +0 -0
  15. setiastro/images/Background_startup.jpg +0 -0
  16. setiastro/images/HRDiagram.png +0 -0
  17. setiastro/images/LExtract.png +0 -0
  18. setiastro/images/LInsert.png +0 -0
  19. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  20. setiastro/images/RGB080604.png +0 -0
  21. setiastro/images/abeicon.png +0 -0
  22. setiastro/images/aberration.png +0 -0
  23. setiastro/images/andromedatry.png +0 -0
  24. setiastro/images/andromedatry_satellited.png +0 -0
  25. setiastro/images/annotated.png +0 -0
  26. setiastro/images/aperture.png +0 -0
  27. setiastro/images/astrosuite.ico +0 -0
  28. setiastro/images/astrosuite.png +0 -0
  29. setiastro/images/astrosuitepro.icns +0 -0
  30. setiastro/images/astrosuitepro.ico +0 -0
  31. setiastro/images/astrosuitepro.png +0 -0
  32. setiastro/images/background.png +0 -0
  33. setiastro/images/background2.png +0 -0
  34. setiastro/images/benchmark.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  36. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  37. setiastro/images/blaster.png +0 -0
  38. setiastro/images/blink.png +0 -0
  39. setiastro/images/clahe.png +0 -0
  40. setiastro/images/collage.png +0 -0
  41. setiastro/images/colorwheel.png +0 -0
  42. setiastro/images/contsub.png +0 -0
  43. setiastro/images/convo.png +0 -0
  44. setiastro/images/copyslot.png +0 -0
  45. setiastro/images/cosmic.png +0 -0
  46. setiastro/images/cosmicsat.png +0 -0
  47. setiastro/images/crop1.png +0 -0
  48. setiastro/images/cropicon.png +0 -0
  49. setiastro/images/curves.png +0 -0
  50. setiastro/images/cvs.png +0 -0
  51. setiastro/images/debayer.png +0 -0
  52. setiastro/images/denoise_cnn_custom.png +0 -0
  53. setiastro/images/denoise_cnn_graph.png +0 -0
  54. setiastro/images/disk.png +0 -0
  55. setiastro/images/dse.png +0 -0
  56. setiastro/images/exoicon.png +0 -0
  57. setiastro/images/eye.png +0 -0
  58. setiastro/images/fliphorizontal.png +0 -0
  59. setiastro/images/flipvertical.png +0 -0
  60. setiastro/images/font.png +0 -0
  61. setiastro/images/freqsep.png +0 -0
  62. setiastro/images/functionbundle.png +0 -0
  63. setiastro/images/graxpert.png +0 -0
  64. setiastro/images/green.png +0 -0
  65. setiastro/images/gridicon.png +0 -0
  66. setiastro/images/halo.png +0 -0
  67. setiastro/images/hdr.png +0 -0
  68. setiastro/images/histogram.png +0 -0
  69. setiastro/images/hubble.png +0 -0
  70. setiastro/images/imagecombine.png +0 -0
  71. setiastro/images/invert.png +0 -0
  72. setiastro/images/isophote.png +0 -0
  73. setiastro/images/isophote_demo_figure.png +0 -0
  74. setiastro/images/isophote_demo_image.png +0 -0
  75. setiastro/images/isophote_demo_model.png +0 -0
  76. setiastro/images/isophote_demo_residual.png +0 -0
  77. setiastro/images/jwstpupil.png +0 -0
  78. setiastro/images/linearfit.png +0 -0
  79. setiastro/images/livestacking.png +0 -0
  80. setiastro/images/mask.png +0 -0
  81. setiastro/images/maskapply.png +0 -0
  82. setiastro/images/maskcreate.png +0 -0
  83. setiastro/images/maskremove.png +0 -0
  84. setiastro/images/morpho.png +0 -0
  85. setiastro/images/mosaic.png +0 -0
  86. setiastro/images/multiscale_decomp.png +0 -0
  87. setiastro/images/nbtorgb.png +0 -0
  88. setiastro/images/neutral.png +0 -0
  89. setiastro/images/nuke.png +0 -0
  90. setiastro/images/openfile.png +0 -0
  91. setiastro/images/pedestal.png +0 -0
  92. setiastro/images/pen.png +0 -0
  93. setiastro/images/pixelmath.png +0 -0
  94. setiastro/images/platesolve.png +0 -0
  95. setiastro/images/ppp.png +0 -0
  96. setiastro/images/pro.png +0 -0
  97. setiastro/images/project.png +0 -0
  98. setiastro/images/psf.png +0 -0
  99. setiastro/images/redo.png +0 -0
  100. setiastro/images/redoicon.png +0 -0
  101. setiastro/images/rescale.png +0 -0
  102. setiastro/images/rgbalign.png +0 -0
  103. setiastro/images/rgbcombo.png +0 -0
  104. setiastro/images/rgbextract.png +0 -0
  105. setiastro/images/rotate180.png +0 -0
  106. setiastro/images/rotatearbitrary.png +0 -0
  107. setiastro/images/rotateclockwise.png +0 -0
  108. setiastro/images/rotatecounterclockwise.png +0 -0
  109. setiastro/images/satellite.png +0 -0
  110. setiastro/images/script.png +0 -0
  111. setiastro/images/selectivecolor.png +0 -0
  112. setiastro/images/simbad.png +0 -0
  113. setiastro/images/slot0.png +0 -0
  114. setiastro/images/slot1.png +0 -0
  115. setiastro/images/slot2.png +0 -0
  116. setiastro/images/slot3.png +0 -0
  117. setiastro/images/slot4.png +0 -0
  118. setiastro/images/slot5.png +0 -0
  119. setiastro/images/slot6.png +0 -0
  120. setiastro/images/slot7.png +0 -0
  121. setiastro/images/slot8.png +0 -0
  122. setiastro/images/slot9.png +0 -0
  123. setiastro/images/spcc.png +0 -0
  124. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  125. setiastro/images/spinner.gif +0 -0
  126. setiastro/images/stacking.png +0 -0
  127. setiastro/images/staradd.png +0 -0
  128. setiastro/images/staralign.png +0 -0
  129. setiastro/images/starnet.png +0 -0
  130. setiastro/images/starregistration.png +0 -0
  131. setiastro/images/starspike.png +0 -0
  132. setiastro/images/starstretch.png +0 -0
  133. setiastro/images/statstretch.png +0 -0
  134. setiastro/images/supernova.png +0 -0
  135. setiastro/images/uhs.png +0 -0
  136. setiastro/images/undoicon.png +0 -0
  137. setiastro/images/upscale.png +0 -0
  138. setiastro/images/viewbundle.png +0 -0
  139. setiastro/images/whitebalance.png +0 -0
  140. setiastro/images/wimi_icon_256x256.png +0 -0
  141. setiastro/images/wimilogo.png +0 -0
  142. setiastro/images/wims.png +0 -0
  143. setiastro/images/wrench_icon.png +0 -0
  144. setiastro/images/xisfliberator.png +0 -0
  145. setiastro/qml/ResourceMonitor.qml +126 -0
  146. setiastro/saspro/__init__.py +20 -0
  147. setiastro/saspro/__main__.py +958 -0
  148. setiastro/saspro/_generated/__init__.py +7 -0
  149. setiastro/saspro/_generated/build_info.py +3 -0
  150. setiastro/saspro/abe.py +1346 -0
  151. setiastro/saspro/abe_preset.py +196 -0
  152. setiastro/saspro/aberration_ai.py +698 -0
  153. setiastro/saspro/aberration_ai_preset.py +224 -0
  154. setiastro/saspro/accel_installer.py +218 -0
  155. setiastro/saspro/accel_workers.py +30 -0
  156. setiastro/saspro/add_stars.py +624 -0
  157. setiastro/saspro/astrobin_exporter.py +1010 -0
  158. setiastro/saspro/astrospike.py +153 -0
  159. setiastro/saspro/astrospike_python.py +1841 -0
  160. setiastro/saspro/autostretch.py +198 -0
  161. setiastro/saspro/backgroundneutral.py +611 -0
  162. setiastro/saspro/batch_convert.py +328 -0
  163. setiastro/saspro/batch_renamer.py +522 -0
  164. setiastro/saspro/blemish_blaster.py +491 -0
  165. setiastro/saspro/blink_comparator_pro.py +3149 -0
  166. setiastro/saspro/bundles.py +61 -0
  167. setiastro/saspro/bundles_dock.py +114 -0
  168. setiastro/saspro/cheat_sheet.py +213 -0
  169. setiastro/saspro/clahe.py +368 -0
  170. setiastro/saspro/comet_stacking.py +1442 -0
  171. setiastro/saspro/common_tr.py +107 -0
  172. setiastro/saspro/config.py +38 -0
  173. setiastro/saspro/config_bootstrap.py +40 -0
  174. setiastro/saspro/config_manager.py +316 -0
  175. setiastro/saspro/continuum_subtract.py +1617 -0
  176. setiastro/saspro/convo.py +1400 -0
  177. setiastro/saspro/convo_preset.py +414 -0
  178. setiastro/saspro/copyastro.py +190 -0
  179. setiastro/saspro/cosmicclarity.py +1589 -0
  180. setiastro/saspro/cosmicclarity_preset.py +407 -0
  181. setiastro/saspro/crop_dialog_pro.py +983 -0
  182. setiastro/saspro/crop_preset.py +189 -0
  183. setiastro/saspro/curve_editor_pro.py +2562 -0
  184. setiastro/saspro/curves_preset.py +375 -0
  185. setiastro/saspro/debayer.py +673 -0
  186. setiastro/saspro/debug_utils.py +29 -0
  187. setiastro/saspro/dnd_mime.py +35 -0
  188. setiastro/saspro/doc_manager.py +2664 -0
  189. setiastro/saspro/exoplanet_detector.py +2166 -0
  190. setiastro/saspro/file_utils.py +284 -0
  191. setiastro/saspro/fitsmodifier.py +748 -0
  192. setiastro/saspro/fix_bom.py +32 -0
  193. setiastro/saspro/free_torch_memory.py +48 -0
  194. setiastro/saspro/frequency_separation.py +1349 -0
  195. setiastro/saspro/function_bundle.py +1596 -0
  196. setiastro/saspro/generate_translations.py +3092 -0
  197. setiastro/saspro/ghs_dialog_pro.py +663 -0
  198. setiastro/saspro/ghs_preset.py +284 -0
  199. setiastro/saspro/graxpert.py +637 -0
  200. setiastro/saspro/graxpert_preset.py +287 -0
  201. setiastro/saspro/gui/__init__.py +0 -0
  202. setiastro/saspro/gui/main_window.py +8792 -0
  203. setiastro/saspro/gui/mixins/__init__.py +33 -0
  204. setiastro/saspro/gui/mixins/dock_mixin.py +375 -0
  205. setiastro/saspro/gui/mixins/file_mixin.py +450 -0
  206. setiastro/saspro/gui/mixins/geometry_mixin.py +503 -0
  207. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  208. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  209. setiastro/saspro/gui/mixins/menu_mixin.py +390 -0
  210. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  211. setiastro/saspro/gui/mixins/toolbar_mixin.py +1619 -0
  212. setiastro/saspro/gui/mixins/update_mixin.py +323 -0
  213. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  214. setiastro/saspro/gui/statistics_dialog.py +47 -0
  215. setiastro/saspro/halobgon.py +488 -0
  216. setiastro/saspro/header_viewer.py +448 -0
  217. setiastro/saspro/headless_utils.py +88 -0
  218. setiastro/saspro/histogram.py +756 -0
  219. setiastro/saspro/history_explorer.py +941 -0
  220. setiastro/saspro/i18n.py +168 -0
  221. setiastro/saspro/image_combine.py +417 -0
  222. setiastro/saspro/image_peeker_pro.py +1604 -0
  223. setiastro/saspro/imageops/__init__.py +37 -0
  224. setiastro/saspro/imageops/mdi_snap.py +292 -0
  225. setiastro/saspro/imageops/scnr.py +36 -0
  226. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  227. setiastro/saspro/imageops/stretch.py +236 -0
  228. setiastro/saspro/isophote.py +1182 -0
  229. setiastro/saspro/layers.py +208 -0
  230. setiastro/saspro/layers_dock.py +714 -0
  231. setiastro/saspro/lazy_imports.py +193 -0
  232. setiastro/saspro/legacy/__init__.py +2 -0
  233. setiastro/saspro/legacy/image_manager.py +2360 -0
  234. setiastro/saspro/legacy/numba_utils.py +3676 -0
  235. setiastro/saspro/legacy/xisf.py +1213 -0
  236. setiastro/saspro/linear_fit.py +537 -0
  237. setiastro/saspro/live_stacking.py +1854 -0
  238. setiastro/saspro/log_bus.py +5 -0
  239. setiastro/saspro/logging_config.py +460 -0
  240. setiastro/saspro/luminancerecombine.py +510 -0
  241. setiastro/saspro/main_helpers.py +201 -0
  242. setiastro/saspro/mask_creation.py +1086 -0
  243. setiastro/saspro/masks_core.py +56 -0
  244. setiastro/saspro/mdi_widgets.py +353 -0
  245. setiastro/saspro/memory_utils.py +666 -0
  246. setiastro/saspro/metadata_patcher.py +75 -0
  247. setiastro/saspro/mfdeconv.py +3909 -0
  248. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  249. setiastro/saspro/mfdeconvcudnn.py +3312 -0
  250. setiastro/saspro/mfdeconvsport.py +2459 -0
  251. setiastro/saspro/minorbodycatalog.py +567 -0
  252. setiastro/saspro/morphology.py +407 -0
  253. setiastro/saspro/multiscale_decomp.py +1747 -0
  254. setiastro/saspro/nbtorgb_stars.py +541 -0
  255. setiastro/saspro/numba_utils.py +3145 -0
  256. setiastro/saspro/numba_warmup.py +141 -0
  257. setiastro/saspro/ops/__init__.py +9 -0
  258. setiastro/saspro/ops/command_help_dialog.py +623 -0
  259. setiastro/saspro/ops/command_runner.py +217 -0
  260. setiastro/saspro/ops/commands.py +1594 -0
  261. setiastro/saspro/ops/script_editor.py +1105 -0
  262. setiastro/saspro/ops/scripts.py +1476 -0
  263. setiastro/saspro/ops/settings.py +637 -0
  264. setiastro/saspro/parallel_utils.py +554 -0
  265. setiastro/saspro/pedestal.py +121 -0
  266. setiastro/saspro/perfect_palette_picker.py +1105 -0
  267. setiastro/saspro/pipeline.py +110 -0
  268. setiastro/saspro/pixelmath.py +1604 -0
  269. setiastro/saspro/plate_solver.py +2445 -0
  270. setiastro/saspro/project_io.py +797 -0
  271. setiastro/saspro/psf_utils.py +136 -0
  272. setiastro/saspro/psf_viewer.py +549 -0
  273. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  274. setiastro/saspro/remove_green.py +331 -0
  275. setiastro/saspro/remove_stars.py +1599 -0
  276. setiastro/saspro/remove_stars_preset.py +446 -0
  277. setiastro/saspro/resources.py +503 -0
  278. setiastro/saspro/rgb_combination.py +208 -0
  279. setiastro/saspro/rgb_extract.py +19 -0
  280. setiastro/saspro/rgbalign.py +723 -0
  281. setiastro/saspro/runtime_imports.py +7 -0
  282. setiastro/saspro/runtime_torch.py +754 -0
  283. setiastro/saspro/save_options.py +73 -0
  284. setiastro/saspro/selective_color.py +1611 -0
  285. setiastro/saspro/sfcc.py +1472 -0
  286. setiastro/saspro/shortcuts.py +3116 -0
  287. setiastro/saspro/signature_insert.py +1102 -0
  288. setiastro/saspro/stacking_suite.py +19066 -0
  289. setiastro/saspro/star_alignment.py +7380 -0
  290. setiastro/saspro/star_alignment_preset.py +329 -0
  291. setiastro/saspro/star_metrics.py +49 -0
  292. setiastro/saspro/star_spikes.py +765 -0
  293. setiastro/saspro/star_stretch.py +507 -0
  294. setiastro/saspro/stat_stretch.py +538 -0
  295. setiastro/saspro/status_log_dock.py +78 -0
  296. setiastro/saspro/subwindow.py +3407 -0
  297. setiastro/saspro/supernovaasteroidhunter.py +1719 -0
  298. setiastro/saspro/swap_manager.py +134 -0
  299. setiastro/saspro/torch_backend.py +89 -0
  300. setiastro/saspro/torch_rejection.py +434 -0
  301. setiastro/saspro/translations/all_source_strings.json +4726 -0
  302. setiastro/saspro/translations/ar_translations.py +4096 -0
  303. setiastro/saspro/translations/de_translations.py +3728 -0
  304. setiastro/saspro/translations/es_translations.py +4169 -0
  305. setiastro/saspro/translations/fr_translations.py +4090 -0
  306. setiastro/saspro/translations/hi_translations.py +3803 -0
  307. setiastro/saspro/translations/integrate_translations.py +271 -0
  308. setiastro/saspro/translations/it_translations.py +4728 -0
  309. setiastro/saspro/translations/ja_translations.py +3834 -0
  310. setiastro/saspro/translations/pt_translations.py +3847 -0
  311. setiastro/saspro/translations/ru_translations.py +3082 -0
  312. setiastro/saspro/translations/saspro_ar.qm +0 -0
  313. setiastro/saspro/translations/saspro_ar.ts +16019 -0
  314. setiastro/saspro/translations/saspro_de.qm +0 -0
  315. setiastro/saspro/translations/saspro_de.ts +14548 -0
  316. setiastro/saspro/translations/saspro_es.qm +0 -0
  317. setiastro/saspro/translations/saspro_es.ts +16202 -0
  318. setiastro/saspro/translations/saspro_fr.qm +0 -0
  319. setiastro/saspro/translations/saspro_fr.ts +15870 -0
  320. setiastro/saspro/translations/saspro_hi.qm +0 -0
  321. setiastro/saspro/translations/saspro_hi.ts +14855 -0
  322. setiastro/saspro/translations/saspro_it.qm +0 -0
  323. setiastro/saspro/translations/saspro_it.ts +19046 -0
  324. setiastro/saspro/translations/saspro_ja.qm +0 -0
  325. setiastro/saspro/translations/saspro_ja.ts +14980 -0
  326. setiastro/saspro/translations/saspro_pt.qm +0 -0
  327. setiastro/saspro/translations/saspro_pt.ts +15024 -0
  328. setiastro/saspro/translations/saspro_ru.qm +0 -0
  329. setiastro/saspro/translations/saspro_ru.ts +11835 -0
  330. setiastro/saspro/translations/saspro_sw.qm +0 -0
  331. setiastro/saspro/translations/saspro_sw.ts +15237 -0
  332. setiastro/saspro/translations/saspro_uk.qm +0 -0
  333. setiastro/saspro/translations/saspro_uk.ts +15248 -0
  334. setiastro/saspro/translations/saspro_zh.qm +0 -0
  335. setiastro/saspro/translations/saspro_zh.ts +15289 -0
  336. setiastro/saspro/translations/sw_translations.py +3897 -0
  337. setiastro/saspro/translations/uk_translations.py +3929 -0
  338. setiastro/saspro/translations/zh_translations.py +3910 -0
  339. setiastro/saspro/versioning.py +77 -0
  340. setiastro/saspro/view_bundle.py +1558 -0
  341. setiastro/saspro/wavescale_hdr.py +645 -0
  342. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  343. setiastro/saspro/wavescalede.py +680 -0
  344. setiastro/saspro/wavescalede_preset.py +230 -0
  345. setiastro/saspro/wcs_update.py +374 -0
  346. setiastro/saspro/whitebalance.py +513 -0
  347. setiastro/saspro/widgets/__init__.py +48 -0
  348. setiastro/saspro/widgets/common_utilities.py +306 -0
  349. setiastro/saspro/widgets/graphics_views.py +122 -0
  350. setiastro/saspro/widgets/image_utils.py +518 -0
  351. setiastro/saspro/widgets/minigame/game.js +991 -0
  352. setiastro/saspro/widgets/minigame/index.html +53 -0
  353. setiastro/saspro/widgets/minigame/style.css +241 -0
  354. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  355. setiastro/saspro/widgets/resource_monitor.py +263 -0
  356. setiastro/saspro/widgets/spinboxes.py +290 -0
  357. setiastro/saspro/widgets/themed_buttons.py +13 -0
  358. setiastro/saspro/widgets/wavelet_utils.py +331 -0
  359. setiastro/saspro/wimi.py +7996 -0
  360. setiastro/saspro/wims.py +578 -0
  361. setiastro/saspro/window_shelf.py +185 -0
  362. setiastro/saspro/xisf.py +1213 -0
  363. setiastrosuitepro-1.6.5.post3.dist-info/METADATA +278 -0
  364. setiastrosuitepro-1.6.5.post3.dist-info/RECORD +368 -0
  365. setiastrosuitepro-1.6.5.post3.dist-info/WHEEL +4 -0
  366. setiastrosuitepro-1.6.5.post3.dist-info/entry_points.txt +6 -0
  367. setiastrosuitepro-1.6.5.post3.dist-info/licenses/LICENSE +674 -0
  368. setiastrosuitepro-1.6.5.post3.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,513 @@
1
+ # pro/whitebalance.py
2
+ from __future__ import annotations
3
+
4
+ import numpy as np
5
+ from typing import Dict, Tuple, Optional
6
+
7
+ from PyQt6.QtCore import Qt, QTimer
8
+ from PyQt6.QtGui import QIcon, QImage, QPixmap
9
+ from PyQt6.QtWidgets import (
10
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QWidget, QGroupBox,
11
+ QGridLayout, QSlider, QCheckBox, QPushButton, QMessageBox, QDoubleSpinBox
12
+ )
13
+
14
+ # imageops
15
+ from setiastro.saspro.imageops.starbasedwhitebalance import apply_star_based_white_balance
16
+ from setiastro.saspro.imageops.stretch import stretch_color_image
17
+
18
+ # Shared utilities
19
+ from setiastro.saspro.widgets.image_utils import (
20
+ to_float01 as _to_float01,
21
+ extract_mask_from_document as _active_mask_array_from_doc
22
+ )
23
+
24
+ from matplotlib import pyplot as plt # NEW
25
+ from matplotlib.patches import Circle # NEW
26
+ from matplotlib.ticker import MaxNLocator # NEW
27
+ # ----------------------------
28
+ # Core WB implementations
29
+ # ----------------------------
30
+ def plot_star_color_ratios_comparison(raw_pixels: np.ndarray, after_pixels: np.ndarray):
31
+ """
32
+ Replicates the SASv2 diagnostic plot: star color ratios before/after WB,
33
+ with an RGB background grid, best-fit line, and neutral markers.
34
+ Expects Nx3 arrays of star RGB samples in [0,1] (or any common scale).
35
+ """
36
+ def compute_ratios(pixels: np.ndarray):
37
+ eps = 1e-8
38
+ rb = pixels[:, 0] / (pixels[:, 2] + eps) # R/B
39
+ gb = pixels[:, 1] / (pixels[:, 2] + eps) # G/B
40
+ return rb, gb
41
+
42
+ rb_before, gb_before = compute_ratios(raw_pixels)
43
+ rb_after, gb_after = compute_ratios(after_pixels)
44
+
45
+ # Optional: keep only finite points
46
+ def _finite(x, y):
47
+ m = np.isfinite(x) & np.isfinite(y)
48
+ return x[m], y[m]
49
+ rb_before, gb_before = _finite(rb_before, gb_before)
50
+ rb_after, gb_after = _finite(rb_after, gb_after)
51
+
52
+ # Plot bounds + background grid
53
+ rmin, rmax = 0.5, 2.0
54
+ gmin, gmax = 0.5, 2.0
55
+ res = 200
56
+
57
+ rb_vals = np.linspace(rmin, rmax, res)
58
+ gb_vals = np.linspace(gmin, gmax, res)
59
+ rb_grid, gb_grid = np.meshgrid(rb_vals, gb_vals)
60
+ rgb_image = np.stack([rb_grid, gb_grid, np.ones_like(rb_grid)], axis=-1)
61
+ rgb_image /= np.maximum(rgb_image.max(axis=2, keepdims=True), 1e-8)
62
+
63
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6), sharex=True, sharey=True)
64
+
65
+ def plot_panel(ax, rb_data, gb_data, title):
66
+ ax.imshow(rgb_image, extent=(rmin, rmax, gmin, gmax), origin='lower', aspect='auto')
67
+ ax.scatter(rb_data, gb_data, alpha=0.6, edgecolors='k', label="Stars")
68
+
69
+ if rb_data.size >= 2:
70
+ m, b = np.polyfit(rb_data, gb_data, 1)
71
+ xs = np.linspace(rmin, rmax, 100)
72
+ ax.plot(xs, m * xs + b, 'r--', label=f"Best Fit\ny = {m:.2f}x + {b:.2f}")
73
+
74
+ ax.axhline(1.0, color='gray', linestyle=':', linewidth=1)
75
+ ax.axvline(1.0, color='gray', linestyle=':', linewidth=1)
76
+ ax.add_patch(Circle((1.0, 1.0), 0.2, fill=False, edgecolor='blue', linestyle='--', linewidth=1.5))
77
+ ax.text(1.03, 1.17, "Neutral Region", color='blue', fontsize=9)
78
+
79
+ ax.set_xlim(rmin, rmax); ax.set_ylim(gmin, gmax)
80
+ ax.set_title(f"{title} White Balance")
81
+ ax.set_xlabel("Red / Blue Ratio"); ax.set_ylabel("Green / Blue Ratio")
82
+ ax.xaxis.set_major_locator(MaxNLocator(integer=True))
83
+ ax.yaxis.set_major_locator(MaxNLocator(integer=True))
84
+ ax.grid(True); ax.legend()
85
+
86
+ plot_panel(ax1, rb_before, gb_before, "Before")
87
+ plot_panel(ax2, rb_after, gb_after, "After")
88
+
89
+ plt.suptitle("Star Color Ratios with RGB Mapping", fontsize=14)
90
+ plt.tight_layout()
91
+ plt.show(block=False)
92
+
93
+ def apply_manual_white_balance(img: np.ndarray, r_gain: float, g_gain: float, b_gain: float) -> np.ndarray:
94
+ """Simple per-channel gain, clipped to [0,1]."""
95
+ if img.ndim != 3 or img.shape[2] != 3:
96
+ raise ValueError("Manual WB requires RGB image.")
97
+ out = _to_float01(img).copy()
98
+ gains = np.array([r_gain, g_gain, b_gain], dtype=np.float32).reshape((1, 1, 3))
99
+ out = np.clip(out * gains, 0.0, 1.0)
100
+ return out.astype(np.float32, copy=False)
101
+
102
+
103
+ def apply_auto_white_balance(img: np.ndarray) -> np.ndarray:
104
+ """
105
+ Gray-world auto WB: scale each channel so its mean equals the overall mean.
106
+ """
107
+ if img.ndim != 3 or img.shape[2] != 3:
108
+ raise ValueError("Auto WB requires RGB image.")
109
+ rgb = _to_float01(img)
110
+ means = np.mean(rgb, axis=(0, 1))
111
+ overall = float(np.mean(means))
112
+ means = np.where(means <= 1e-8, 1e-8, means)
113
+ scale = (overall / means).reshape((1, 1, 3))
114
+ out = np.clip(rgb * scale, 0.0, 1.0)
115
+ return out.astype(np.float32, copy=False)
116
+
117
+
118
+ # --------------------------------
119
+ # Headless entry point for DnD
120
+ # --------------------------------
121
+ def apply_white_balance_to_doc(doc, preset: Optional[Dict] = None):
122
+ """
123
+ Preset schema:
124
+ {
125
+ "mode": "star" | "manual" | "auto", # default "star"
126
+ # star mode:
127
+ "threshold": float (default 50),
128
+ "reuse_cached_sources": bool (default True),
129
+ # manual mode:
130
+ "r_gain": float (default 1.0), "g_gain": float (default 1.0), "b_gain": float (default 1.0)
131
+ }
132
+ """
133
+ import numpy as np
134
+
135
+ p = dict(preset or {})
136
+ mode = (p.get("mode") or "star").lower()
137
+
138
+ base = np.asarray(doc.image).astype(np.float32, copy=False)
139
+ if base.ndim != 3 or base.shape[2] != 3:
140
+ raise ValueError("White Balance requires an RGB image.")
141
+
142
+ base_n = _to_float01(base)
143
+
144
+ try:
145
+ if mode == "manual":
146
+ r = float(p.get("r_gain", 1.0))
147
+ g = float(p.get("g_gain", 1.0))
148
+ b = float(p.get("b_gain", 1.0))
149
+ out = apply_manual_white_balance(base_n, r, g, b)
150
+
151
+ elif mode == "auto":
152
+ out = apply_auto_white_balance(base_n)
153
+
154
+ else: # "star"
155
+ thr = float(p.get("threshold", 50.0))
156
+ reuse = bool(p.get("reuse_cached_sources", True))
157
+ out, _count, _overlay = apply_star_based_white_balance(
158
+ base_n, threshold=thr, autostretch=False,
159
+ reuse_cached_sources=reuse, return_star_colors=False
160
+ )
161
+ except Exception as e:
162
+ # Fallback: if SEP missing or star detection fails, try Auto WB
163
+ if mode == "star":
164
+ try:
165
+ out = apply_auto_white_balance(base_n)
166
+ except Exception:
167
+ raise e
168
+ else:
169
+ raise
170
+
171
+ # Destination-mask blend (if any)
172
+ m = _active_mask_array_from_doc(doc)
173
+ if m is not None:
174
+ if out.ndim == 3:
175
+ m3 = np.repeat(m[..., None], 3, axis=2).astype(np.float32, copy=False)
176
+ else:
177
+ m3 = m.astype(np.float32, copy=False)
178
+ base_for_blend = _to_float01(np.asarray(doc.image).astype(np.float32, copy=False))
179
+ out = base_for_blend * (1.0 - m3) + out * m3
180
+
181
+ doc.apply_edit(
182
+ out.astype(np.float32, copy=False),
183
+ metadata={"step_name": "White Balance", "preset": p},
184
+ step_name="White Balance",
185
+ )
186
+
187
+
188
+ # -------------------------
189
+ # Interactive dialog (UI)
190
+ # -------------------------
191
+ class WhiteBalanceDialog(QDialog):
192
+ def __init__(self, parent, doc, icon: QIcon | None = None):
193
+ super().__init__(parent)
194
+ self._main = parent
195
+ self.doc = doc
196
+
197
+ # Connect to active document change signal
198
+ if hasattr(self._main, "currentDocumentChanged"):
199
+ self._main.currentDocumentChanged.connect(self._on_active_doc_changed)
200
+ if icon:
201
+ self.setWindowIcon(icon)
202
+ self.setWindowTitle(self.tr("White Balance"))
203
+ self.setWindowFlag(Qt.WindowType.Window, True)
204
+ self.setWindowModality(Qt.WindowModality.NonModal)
205
+ self.setModal(False)
206
+ self.resize(900, 600)
207
+
208
+ self._build_ui()
209
+ self._wire_events()
210
+
211
+ # default to Star-Based, like SASv2
212
+ self.type_combo.setCurrentText("Star-Based")
213
+ self._update_mode_widgets()
214
+ # kick off a first detection preview
215
+ QTimer.singleShot(200, self._update_star_preview)
216
+
217
+ # ---- UI construction ------------------------------------------------
218
+ def _build_ui(self):
219
+ self.main_layout = QVBoxLayout(self)
220
+
221
+ # Type selector
222
+ row = QHBoxLayout()
223
+ row.addWidget(QLabel(self.tr("White Balance Type:")))
224
+ self.type_combo = QComboBox()
225
+ self.type_combo.addItems([self.tr("Star-Based"), self.tr("Manual"), self.tr("Auto")])
226
+ row.addWidget(self.type_combo); row.addStretch()
227
+ self.main_layout.addLayout(row)
228
+
229
+ # Manual group
230
+ self.manual_group = QGroupBox(self.tr("Manual Gains"))
231
+ g = QGridLayout(self.manual_group)
232
+ self.r_spin = QDoubleSpinBox(); self._cfg_gain(self.r_spin, 1.0)
233
+ self.g_spin = QDoubleSpinBox(); self._cfg_gain(self.g_spin, 1.0)
234
+ self.b_spin = QDoubleSpinBox(); self._cfg_gain(self.b_spin, 1.0)
235
+ g.addWidget(QLabel(self.tr("Red gain:")), 0, 0); g.addWidget(self.r_spin, 0, 1)
236
+ g.addWidget(QLabel(self.tr("Green gain:")), 1, 0); g.addWidget(self.g_spin, 1, 1)
237
+ g.addWidget(QLabel(self.tr("Blue gain:")), 2, 0); g.addWidget(self.b_spin, 2, 1)
238
+ self.main_layout.addWidget(self.manual_group)
239
+
240
+ # Star-based controls + preview
241
+ self.star_group = QGroupBox(self.tr("Star-Based Settings"))
242
+ sg = QVBoxLayout(self.star_group)
243
+ # threshold slider
244
+ thr_row = QHBoxLayout()
245
+ thr_row.addWidget(QLabel(self.tr("Detection threshold (σ):")))
246
+ self.thr_slider = QSlider(Qt.Orientation.Horizontal)
247
+ self.thr_slider.setMinimum(1); self.thr_slider.setMaximum(100)
248
+ self.thr_slider.setValue(50); self.thr_slider.setTickInterval(10)
249
+ self.thr_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
250
+ self.thr_label = QLabel("50")
251
+ thr_row.addWidget(self.thr_slider); thr_row.addWidget(self.thr_label)
252
+ sg.addLayout(thr_row)
253
+
254
+ self.chk_reuse = QCheckBox(self.tr("Reuse cached star detections")); self.chk_reuse.setChecked(True)
255
+ sg.addWidget(self.chk_reuse)
256
+
257
+ self.chk_autostretch_overlay = QCheckBox(self.tr("Autostretch overlay preview")); self.chk_autostretch_overlay.setChecked(True)
258
+ sg.addWidget(self.chk_autostretch_overlay)
259
+
260
+ # star count + image preview
261
+ self.star_count = QLabel(self.tr("Detecting stars..."))
262
+ sg.addWidget(self.star_count)
263
+
264
+ self.preview = QLabel(); self.preview.setMinimumSize(640, 360)
265
+ self.preview.setStyleSheet("border: 1px solid #333;")
266
+ sg.addWidget(self.preview)
267
+ self.main_layout.addWidget(self.star_group)
268
+
269
+ # Buttons
270
+ btn_row = QHBoxLayout()
271
+ btn_row.addStretch()
272
+ self.btn_apply = QPushButton(self.tr("Apply"))
273
+ self.btn_cancel = QPushButton(self.tr("Cancel"))
274
+ btn_row.addWidget(self.btn_apply)
275
+ btn_row.addWidget(self.btn_cancel)
276
+ self.main_layout.addLayout(btn_row)
277
+
278
+ self.setLayout(self.main_layout)
279
+
280
+ # debounce timer for star preview
281
+ self._debounce = QTimer(self); self._debounce.setSingleShot(True); self._debounce.setInterval(600)
282
+ self._debounce.timeout.connect(self._update_star_preview)
283
+
284
+ def _cfg_gain(self, box: QDoubleSpinBox, val: float):
285
+ box.setRange(0.5, 1.5); box.setDecimals(3); box.setSingleStep(0.01); box.setValue(val)
286
+
287
+ # ---- events ---------------------------------------------------------
288
+ def _wire_events(self):
289
+ self.type_combo.currentTextChanged.connect(self._update_mode_widgets)
290
+ self.btn_cancel.clicked.connect(self.reject)
291
+ self.btn_apply.clicked.connect(self._on_apply)
292
+
293
+ self.thr_slider.valueChanged.connect(lambda v: (self.thr_label.setText(str(v)), self._debounce.start()))
294
+ self.chk_autostretch_overlay.toggled.connect(lambda _=None: self._debounce.start())
295
+ self.chk_reuse.toggled.connect(lambda _=None: self._debounce.start())
296
+
297
+ def _update_mode_widgets(self):
298
+ t = self.type_combo.currentText()
299
+ self.manual_group.setVisible(t == "Manual")
300
+ self.star_group.setVisible(t == "Star-Based")
301
+
302
+ # ---- active document change ------------------------------------
303
+ def _on_active_doc_changed(self, doc):
304
+ """Called when user clicks a different image window."""
305
+ if doc is None or getattr(doc, "image", None) is None:
306
+ return
307
+ self.doc = doc
308
+ # Force fresh star detection by temporarily disabling cache
309
+ old_reuse = self.chk_reuse.isChecked()
310
+ self.chk_reuse.setChecked(False)
311
+ self._update_star_preview()
312
+ self.chk_reuse.setChecked(old_reuse)
313
+
314
+ # ---- preview --------------------------------------------------------
315
+ def _update_star_preview(self):
316
+ if self.type_combo.currentText() != "Star-Based":
317
+ return
318
+ try:
319
+ img = _to_float01(np.asarray(self.doc.image))
320
+ thr = float(self.thr_slider.value())
321
+ reuse = bool(self.chk_reuse.isChecked())
322
+ auto = bool(self.chk_autostretch_overlay.isChecked())
323
+
324
+ _, count, overlay = apply_star_based_white_balance(
325
+ img, threshold=thr, autostretch=auto,
326
+ reuse_cached_sources=reuse, return_star_colors=False
327
+ )
328
+ self.star_count.setText(self.tr("Detected {0} stars.").format(count))
329
+ # to pixmap
330
+ h, w, _ = overlay.shape
331
+ qimg = QImage((overlay * 255).astype(np.uint8).data, w, h, 3 * w, QImage.Format.Format_RGB888)
332
+ pm = QPixmap.fromImage(qimg).scaled(self.preview.width(), self.preview.height(),
333
+ Qt.AspectRatioMode.KeepAspectRatio,
334
+ Qt.TransformationMode.SmoothTransformation)
335
+ self.preview.setPixmap(pm)
336
+ except Exception as e:
337
+ self.star_count.setText(self.tr("Detection failed."))
338
+ self.preview.clear()
339
+
340
+ # ---- apply ----------------------------------------------------------
341
+ def _on_apply(self):
342
+ mode = self.type_combo.currentText()
343
+
344
+ # Find the main window that carries the replay machinery
345
+ main = self.parent()
346
+ while main is not None and not hasattr(main, "replay_last_action_on_base"):
347
+ main = main.parent()
348
+
349
+ def _record_preset_for_replay(preset: dict):
350
+ """Helper to stash the WB preset on the main window for Replay Last Action."""
351
+ if main is None:
352
+ return
353
+ try:
354
+ main._last_headless_command = {
355
+ "command_id": "white_balance",
356
+ "preset": preset,
357
+ }
358
+ if hasattr(main, "_log"):
359
+ mode_str = str(preset.get("mode", "star")).lower()
360
+ if mode_str == "manual":
361
+ r = float(preset.get("r_gain", 1.0))
362
+ g = float(preset.get("g_gain", 1.0))
363
+ b = float(preset.get("b_gain", 1.0))
364
+ main._log(
365
+ f"[Replay] Recorded White Balance preset "
366
+ f"(mode=manual, R={r:.3f}, G={g:.3f}, B={b:.3f})"
367
+ )
368
+ elif mode_str == "auto":
369
+ main._log("[Replay] Recorded White Balance preset (mode=auto)")
370
+ else:
371
+ thr = float(preset.get("threshold", 50.0))
372
+ reuse = bool(preset.get("reuse_cached_sources", True))
373
+ main._log(
374
+ f"[Replay] Recorded White Balance preset "
375
+ f"(mode=star, threshold={thr:.1f}, "
376
+ f"reuse={'yes' if reuse else 'no'})"
377
+ )
378
+ except Exception:
379
+ # Logging/recording must never break the dialog
380
+ pass
381
+
382
+ try:
383
+ if mode == "Manual":
384
+ preset = {
385
+ "mode": "manual",
386
+ "r_gain": float(self.r_spin.value()),
387
+ "g_gain": float(self.g_spin.value()),
388
+ "b_gain": float(self.b_spin.value()),
389
+ }
390
+
391
+ # Record for Replay Last Action
392
+ _record_preset_for_replay(preset)
393
+
394
+ # Use the headless helper so doc metadata is consistent
395
+ apply_white_balance_to_doc(self.doc, preset)
396
+ # Dialog stays open - refresh document for next operation
397
+ self._finish_and_close()
398
+ return
399
+
400
+ elif mode == "Auto":
401
+ preset = {"mode": "auto"}
402
+
403
+ # Record for Replay Last Action
404
+ _record_preset_for_replay(preset)
405
+
406
+ apply_white_balance_to_doc(self.doc, preset)
407
+ # Dialog stays open - refresh document for next operation
408
+ self._finish_and_close()
409
+ return
410
+
411
+ else: # --- Star-Based: compute here so we can plot like SASv2 ---
412
+ thr = float(self.thr_slider.value())
413
+ reuse = bool(self.chk_reuse.isChecked())
414
+
415
+ preset = {
416
+ "mode": "star",
417
+ "threshold": thr,
418
+ "reuse_cached_sources": reuse,
419
+ }
420
+
421
+ base = _to_float01(
422
+ np.asarray(self.doc.image).astype(np.float32, copy=False)
423
+ )
424
+
425
+ # Ask for star colors so we can plot
426
+ result = apply_star_based_white_balance(
427
+ base,
428
+ threshold=thr,
429
+ autostretch=False,
430
+ reuse_cached_sources=reuse,
431
+ return_star_colors=True,
432
+ )
433
+
434
+ # Expected: (out, count, overlay, raw_colors, after_colors)
435
+ if len(result) < 5:
436
+ raise RuntimeError(
437
+ "Star-based WB did not return star color arrays. "
438
+ "Ensure return_star_colors=True is supported."
439
+ )
440
+
441
+ out, star_count, _overlay, raw_colors, after_colors = result
442
+
443
+ # Optional destination-mask blend, same as headless path
444
+ m = _active_mask_array_from_doc(self.doc)
445
+ if m is not None:
446
+ m3 = np.repeat(m[..., None], 3, axis=2).astype(np.float32, copy=False)
447
+ base_for_blend = _to_float01(
448
+ np.asarray(self.doc.image).astype(np.float32, copy=False)
449
+ )
450
+ out = base_for_blend * (1.0 - m3) + out * m3
451
+
452
+ # Commit to the document, including the preset in metadata
453
+ self.doc.apply_edit(
454
+ out.astype(np.float32, copy=False),
455
+ metadata={"step_name": "White Balance", "preset": preset},
456
+ step_name="White Balance",
457
+ )
458
+
459
+ # Record for Replay Last Action (using the same preset we just stored in metadata)
460
+ _record_preset_for_replay(preset)
461
+
462
+ # 🔬 Show the same diagnostic plot SASv2 shows
463
+ if (
464
+ raw_colors is not None
465
+ and after_colors is not None
466
+ and raw_colors.size
467
+ and after_colors.size
468
+ ):
469
+ plot_star_color_ratios_comparison(raw_colors, after_colors)
470
+
471
+ QMessageBox.information(
472
+ self,
473
+ self.tr("White Balance"),
474
+ self.tr("Star-Based WB applied.\nDetected {0} stars.").format(int(star_count)),
475
+ )
476
+ # Dialog stays open - refresh document for next operation
477
+ self._finish_and_close()
478
+ return
479
+
480
+ except Exception as e:
481
+ QMessageBox.critical(self, self.tr("White Balance"), self.tr("Failed to apply White Balance:\n{0}").format(e))
482
+
483
+ def _refresh_document_from_active(self):
484
+ """
485
+ Refresh the dialog's document reference to the currently active document.
486
+ This allows reusing the same dialog on different images.
487
+ """
488
+ try:
489
+ main = self.parent()
490
+ if main and hasattr(main, "_active_doc"):
491
+ new_doc = main._active_doc()
492
+ if new_doc is not None and new_doc is not self.doc:
493
+ self.doc = new_doc
494
+ except Exception:
495
+ pass
496
+
497
+ def _finish_and_close(self):
498
+ """
499
+ Close this dialog after a successful apply.
500
+ Use accept() so it behaves like a successful completion.
501
+ """
502
+ try:
503
+ self.accept()
504
+ except Exception:
505
+ self.close()
506
+
507
+ def closeEvent(self, ev):
508
+ try:
509
+ if hasattr(self._main, "currentDocumentChanged"):
510
+ self._main.currentDocumentChanged.disconnect(self._on_active_doc_changed)
511
+ except Exception:
512
+ pass
513
+ super().closeEvent(ev)
@@ -0,0 +1,48 @@
1
+ # pro/widgets/__init__.py
2
+ """
3
+ Shared UI widgets for Seti Astro Suite Pro.
4
+
5
+ This package contains reusable UI components to avoid code duplication.
6
+ """
7
+
8
+ from setiastro.saspro.widgets.spinboxes import CustomSpinBox, CustomDoubleSpinBox
9
+ from setiastro.saspro.widgets.graphics_views import ZoomableGraphicsView
10
+ from setiastro.saspro.widgets.preview_dialogs import ImagePreviewDialog
11
+ from setiastro.saspro.widgets.image_utils import (
12
+ numpy_to_qimage,
13
+ numpy_to_qpixmap,
14
+ qimage_to_numpy,
15
+ create_preview_image,
16
+ normalize_image
17
+ )
18
+ from setiastro.saspro.widgets.common_utilities import (
19
+ AboutDialog,
20
+ ProjectSaveWorker,
21
+ DECOR_GLYPHS,
22
+ strip_ui_decorations,
23
+ _strip_ui_decorations,
24
+ install_crash_handlers,
25
+ get_version,
26
+ get_build_timestamp,
27
+ )
28
+
29
+ __all__ = [
30
+ 'CustomSpinBox',
31
+ 'CustomDoubleSpinBox',
32
+ 'ZoomableGraphicsView',
33
+ 'ImagePreviewDialog',
34
+ 'numpy_to_qimage',
35
+ 'numpy_to_qpixmap',
36
+ 'qimage_to_numpy',
37
+ 'create_preview_image',
38
+ 'normalize_image',
39
+ # Common utilities
40
+ 'AboutDialog',
41
+ 'ProjectSaveWorker',
42
+ 'DECOR_GLYPHS',
43
+ 'strip_ui_decorations',
44
+ '_strip_ui_decorations',
45
+ 'install_crash_handlers',
46
+ 'get_version',
47
+ 'get_build_timestamp',
48
+ ]