setiastrosuitepro 1.6.1__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 (342) 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/HRDiagram.png +0 -0
  16. setiastro/images/LExtract.png +0 -0
  17. setiastro/images/LInsert.png +0 -0
  18. setiastro/images/Oxygenation-atm-2.svg.png +0 -0
  19. setiastro/images/RGB080604.png +0 -0
  20. setiastro/images/abeicon.png +0 -0
  21. setiastro/images/aberration.png +0 -0
  22. setiastro/images/andromedatry.png +0 -0
  23. setiastro/images/andromedatry_satellited.png +0 -0
  24. setiastro/images/annotated.png +0 -0
  25. setiastro/images/aperture.png +0 -0
  26. setiastro/images/astrosuite.ico +0 -0
  27. setiastro/images/astrosuite.png +0 -0
  28. setiastro/images/astrosuitepro.icns +0 -0
  29. setiastro/images/astrosuitepro.ico +0 -0
  30. setiastro/images/astrosuitepro.png +0 -0
  31. setiastro/images/background.png +0 -0
  32. setiastro/images/background2.png +0 -0
  33. setiastro/images/benchmark.png +0 -0
  34. setiastro/images/big_moon_stabilizer_timeline.png +0 -0
  35. setiastro/images/big_moon_stabilizer_timeline_clean.png +0 -0
  36. setiastro/images/blaster.png +0 -0
  37. setiastro/images/blink.png +0 -0
  38. setiastro/images/clahe.png +0 -0
  39. setiastro/images/collage.png +0 -0
  40. setiastro/images/colorwheel.png +0 -0
  41. setiastro/images/contsub.png +0 -0
  42. setiastro/images/convo.png +0 -0
  43. setiastro/images/copyslot.png +0 -0
  44. setiastro/images/cosmic.png +0 -0
  45. setiastro/images/cosmicsat.png +0 -0
  46. setiastro/images/crop1.png +0 -0
  47. setiastro/images/cropicon.png +0 -0
  48. setiastro/images/curves.png +0 -0
  49. setiastro/images/cvs.png +0 -0
  50. setiastro/images/debayer.png +0 -0
  51. setiastro/images/denoise_cnn_custom.png +0 -0
  52. setiastro/images/denoise_cnn_graph.png +0 -0
  53. setiastro/images/disk.png +0 -0
  54. setiastro/images/dse.png +0 -0
  55. setiastro/images/exoicon.png +0 -0
  56. setiastro/images/eye.png +0 -0
  57. setiastro/images/fliphorizontal.png +0 -0
  58. setiastro/images/flipvertical.png +0 -0
  59. setiastro/images/font.png +0 -0
  60. setiastro/images/freqsep.png +0 -0
  61. setiastro/images/functionbundle.png +0 -0
  62. setiastro/images/graxpert.png +0 -0
  63. setiastro/images/green.png +0 -0
  64. setiastro/images/gridicon.png +0 -0
  65. setiastro/images/halo.png +0 -0
  66. setiastro/images/hdr.png +0 -0
  67. setiastro/images/histogram.png +0 -0
  68. setiastro/images/hubble.png +0 -0
  69. setiastro/images/imagecombine.png +0 -0
  70. setiastro/images/invert.png +0 -0
  71. setiastro/images/isophote.png +0 -0
  72. setiastro/images/isophote_demo_figure.png +0 -0
  73. setiastro/images/isophote_demo_image.png +0 -0
  74. setiastro/images/isophote_demo_model.png +0 -0
  75. setiastro/images/isophote_demo_residual.png +0 -0
  76. setiastro/images/jwstpupil.png +0 -0
  77. setiastro/images/linearfit.png +0 -0
  78. setiastro/images/livestacking.png +0 -0
  79. setiastro/images/mask.png +0 -0
  80. setiastro/images/maskapply.png +0 -0
  81. setiastro/images/maskcreate.png +0 -0
  82. setiastro/images/maskremove.png +0 -0
  83. setiastro/images/morpho.png +0 -0
  84. setiastro/images/mosaic.png +0 -0
  85. setiastro/images/multiscale_decomp.png +0 -0
  86. setiastro/images/nbtorgb.png +0 -0
  87. setiastro/images/neutral.png +0 -0
  88. setiastro/images/nuke.png +0 -0
  89. setiastro/images/openfile.png +0 -0
  90. setiastro/images/pedestal.png +0 -0
  91. setiastro/images/pen.png +0 -0
  92. setiastro/images/pixelmath.png +0 -0
  93. setiastro/images/platesolve.png +0 -0
  94. setiastro/images/ppp.png +0 -0
  95. setiastro/images/pro.png +0 -0
  96. setiastro/images/project.png +0 -0
  97. setiastro/images/psf.png +0 -0
  98. setiastro/images/redo.png +0 -0
  99. setiastro/images/redoicon.png +0 -0
  100. setiastro/images/rescale.png +0 -0
  101. setiastro/images/rgbalign.png +0 -0
  102. setiastro/images/rgbcombo.png +0 -0
  103. setiastro/images/rgbextract.png +0 -0
  104. setiastro/images/rotate180.png +0 -0
  105. setiastro/images/rotateclockwise.png +0 -0
  106. setiastro/images/rotatecounterclockwise.png +0 -0
  107. setiastro/images/satellite.png +0 -0
  108. setiastro/images/script.png +0 -0
  109. setiastro/images/selectivecolor.png +0 -0
  110. setiastro/images/simbad.png +0 -0
  111. setiastro/images/slot0.png +0 -0
  112. setiastro/images/slot1.png +0 -0
  113. setiastro/images/slot2.png +0 -0
  114. setiastro/images/slot3.png +0 -0
  115. setiastro/images/slot4.png +0 -0
  116. setiastro/images/slot5.png +0 -0
  117. setiastro/images/slot6.png +0 -0
  118. setiastro/images/slot7.png +0 -0
  119. setiastro/images/slot8.png +0 -0
  120. setiastro/images/slot9.png +0 -0
  121. setiastro/images/spcc.png +0 -0
  122. setiastro/images/spin_precession_vs_lunar_distance.png +0 -0
  123. setiastro/images/spinner.gif +0 -0
  124. setiastro/images/stacking.png +0 -0
  125. setiastro/images/staradd.png +0 -0
  126. setiastro/images/staralign.png +0 -0
  127. setiastro/images/starnet.png +0 -0
  128. setiastro/images/starregistration.png +0 -0
  129. setiastro/images/starspike.png +0 -0
  130. setiastro/images/starstretch.png +0 -0
  131. setiastro/images/statstretch.png +0 -0
  132. setiastro/images/supernova.png +0 -0
  133. setiastro/images/uhs.png +0 -0
  134. setiastro/images/undoicon.png +0 -0
  135. setiastro/images/upscale.png +0 -0
  136. setiastro/images/viewbundle.png +0 -0
  137. setiastro/images/whitebalance.png +0 -0
  138. setiastro/images/wimi_icon_256x256.png +0 -0
  139. setiastro/images/wimilogo.png +0 -0
  140. setiastro/images/wims.png +0 -0
  141. setiastro/images/wrench_icon.png +0 -0
  142. setiastro/images/xisfliberator.png +0 -0
  143. setiastro/saspro/__init__.py +20 -0
  144. setiastro/saspro/__main__.py +809 -0
  145. setiastro/saspro/_generated/__init__.py +7 -0
  146. setiastro/saspro/_generated/build_info.py +2 -0
  147. setiastro/saspro/abe.py +1295 -0
  148. setiastro/saspro/abe_preset.py +196 -0
  149. setiastro/saspro/aberration_ai.py +694 -0
  150. setiastro/saspro/aberration_ai_preset.py +224 -0
  151. setiastro/saspro/accel_installer.py +218 -0
  152. setiastro/saspro/accel_workers.py +30 -0
  153. setiastro/saspro/add_stars.py +621 -0
  154. setiastro/saspro/astrobin_exporter.py +1007 -0
  155. setiastro/saspro/astrospike.py +153 -0
  156. setiastro/saspro/astrospike_python.py +1839 -0
  157. setiastro/saspro/autostretch.py +196 -0
  158. setiastro/saspro/backgroundneutral.py +560 -0
  159. setiastro/saspro/batch_convert.py +325 -0
  160. setiastro/saspro/batch_renamer.py +519 -0
  161. setiastro/saspro/blemish_blaster.py +488 -0
  162. setiastro/saspro/blink_comparator_pro.py +2926 -0
  163. setiastro/saspro/bundles.py +61 -0
  164. setiastro/saspro/bundles_dock.py +114 -0
  165. setiastro/saspro/cheat_sheet.py +178 -0
  166. setiastro/saspro/clahe.py +342 -0
  167. setiastro/saspro/comet_stacking.py +1377 -0
  168. setiastro/saspro/common_tr.py +107 -0
  169. setiastro/saspro/config.py +38 -0
  170. setiastro/saspro/config_bootstrap.py +40 -0
  171. setiastro/saspro/config_manager.py +316 -0
  172. setiastro/saspro/continuum_subtract.py +1617 -0
  173. setiastro/saspro/convo.py +1397 -0
  174. setiastro/saspro/convo_preset.py +414 -0
  175. setiastro/saspro/copyastro.py +187 -0
  176. setiastro/saspro/cosmicclarity.py +1564 -0
  177. setiastro/saspro/cosmicclarity_preset.py +407 -0
  178. setiastro/saspro/crop_dialog_pro.py +956 -0
  179. setiastro/saspro/crop_preset.py +189 -0
  180. setiastro/saspro/curve_editor_pro.py +2544 -0
  181. setiastro/saspro/curves_preset.py +375 -0
  182. setiastro/saspro/debayer.py +670 -0
  183. setiastro/saspro/debug_utils.py +29 -0
  184. setiastro/saspro/dnd_mime.py +35 -0
  185. setiastro/saspro/doc_manager.py +2641 -0
  186. setiastro/saspro/exoplanet_detector.py +2166 -0
  187. setiastro/saspro/file_utils.py +284 -0
  188. setiastro/saspro/fitsmodifier.py +745 -0
  189. setiastro/saspro/fix_bom.py +32 -0
  190. setiastro/saspro/free_torch_memory.py +48 -0
  191. setiastro/saspro/frequency_separation.py +1343 -0
  192. setiastro/saspro/function_bundle.py +1594 -0
  193. setiastro/saspro/generate_translations.py +2378 -0
  194. setiastro/saspro/ghs_dialog_pro.py +660 -0
  195. setiastro/saspro/ghs_preset.py +284 -0
  196. setiastro/saspro/graxpert.py +634 -0
  197. setiastro/saspro/graxpert_preset.py +287 -0
  198. setiastro/saspro/gui/__init__.py +0 -0
  199. setiastro/saspro/gui/main_window.py +8567 -0
  200. setiastro/saspro/gui/mixins/__init__.py +33 -0
  201. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  202. setiastro/saspro/gui/mixins/file_mixin.py +443 -0
  203. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  204. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  205. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  206. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  207. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  208. setiastro/saspro/gui/mixins/toolbar_mixin.py +1457 -0
  209. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  210. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  211. setiastro/saspro/halobgon.py +462 -0
  212. setiastro/saspro/header_viewer.py +448 -0
  213. setiastro/saspro/headless_utils.py +88 -0
  214. setiastro/saspro/histogram.py +753 -0
  215. setiastro/saspro/history_explorer.py +939 -0
  216. setiastro/saspro/i18n.py +156 -0
  217. setiastro/saspro/image_combine.py +414 -0
  218. setiastro/saspro/image_peeker_pro.py +1601 -0
  219. setiastro/saspro/imageops/__init__.py +37 -0
  220. setiastro/saspro/imageops/mdi_snap.py +292 -0
  221. setiastro/saspro/imageops/scnr.py +36 -0
  222. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  223. setiastro/saspro/imageops/stretch.py +244 -0
  224. setiastro/saspro/isophote.py +1179 -0
  225. setiastro/saspro/layers.py +208 -0
  226. setiastro/saspro/layers_dock.py +714 -0
  227. setiastro/saspro/lazy_imports.py +193 -0
  228. setiastro/saspro/legacy/__init__.py +2 -0
  229. setiastro/saspro/legacy/image_manager.py +2226 -0
  230. setiastro/saspro/legacy/numba_utils.py +3659 -0
  231. setiastro/saspro/legacy/xisf.py +1071 -0
  232. setiastro/saspro/linear_fit.py +534 -0
  233. setiastro/saspro/live_stacking.py +1830 -0
  234. setiastro/saspro/log_bus.py +5 -0
  235. setiastro/saspro/logging_config.py +460 -0
  236. setiastro/saspro/luminancerecombine.py +309 -0
  237. setiastro/saspro/main_helpers.py +201 -0
  238. setiastro/saspro/mask_creation.py +928 -0
  239. setiastro/saspro/masks_core.py +56 -0
  240. setiastro/saspro/mdi_widgets.py +353 -0
  241. setiastro/saspro/memory_utils.py +666 -0
  242. setiastro/saspro/metadata_patcher.py +75 -0
  243. setiastro/saspro/mfdeconv.py +3826 -0
  244. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  245. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  246. setiastro/saspro/mfdeconvsport.py +2382 -0
  247. setiastro/saspro/minorbodycatalog.py +567 -0
  248. setiastro/saspro/morphology.py +382 -0
  249. setiastro/saspro/multiscale_decomp.py +1290 -0
  250. setiastro/saspro/nbtorgb_stars.py +531 -0
  251. setiastro/saspro/numba_utils.py +3044 -0
  252. setiastro/saspro/numba_warmup.py +141 -0
  253. setiastro/saspro/ops/__init__.py +9 -0
  254. setiastro/saspro/ops/command_help_dialog.py +623 -0
  255. setiastro/saspro/ops/command_runner.py +217 -0
  256. setiastro/saspro/ops/commands.py +1594 -0
  257. setiastro/saspro/ops/script_editor.py +1102 -0
  258. setiastro/saspro/ops/scripts.py +1413 -0
  259. setiastro/saspro/ops/settings.py +679 -0
  260. setiastro/saspro/parallel_utils.py +554 -0
  261. setiastro/saspro/pedestal.py +121 -0
  262. setiastro/saspro/perfect_palette_picker.py +1070 -0
  263. setiastro/saspro/pipeline.py +110 -0
  264. setiastro/saspro/pixelmath.py +1600 -0
  265. setiastro/saspro/plate_solver.py +2444 -0
  266. setiastro/saspro/project_io.py +797 -0
  267. setiastro/saspro/psf_utils.py +136 -0
  268. setiastro/saspro/psf_viewer.py +549 -0
  269. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  270. setiastro/saspro/remove_green.py +314 -0
  271. setiastro/saspro/remove_stars.py +1625 -0
  272. setiastro/saspro/remove_stars_preset.py +404 -0
  273. setiastro/saspro/resources.py +477 -0
  274. setiastro/saspro/rgb_combination.py +207 -0
  275. setiastro/saspro/rgb_extract.py +19 -0
  276. setiastro/saspro/rgbalign.py +723 -0
  277. setiastro/saspro/runtime_imports.py +7 -0
  278. setiastro/saspro/runtime_torch.py +754 -0
  279. setiastro/saspro/save_options.py +72 -0
  280. setiastro/saspro/selective_color.py +1552 -0
  281. setiastro/saspro/sfcc.py +1430 -0
  282. setiastro/saspro/shortcuts.py +3043 -0
  283. setiastro/saspro/signature_insert.py +1099 -0
  284. setiastro/saspro/stacking_suite.py +18181 -0
  285. setiastro/saspro/star_alignment.py +7420 -0
  286. setiastro/saspro/star_alignment_preset.py +329 -0
  287. setiastro/saspro/star_metrics.py +49 -0
  288. setiastro/saspro/star_spikes.py +681 -0
  289. setiastro/saspro/star_stretch.py +470 -0
  290. setiastro/saspro/stat_stretch.py +506 -0
  291. setiastro/saspro/status_log_dock.py +78 -0
  292. setiastro/saspro/subwindow.py +3267 -0
  293. setiastro/saspro/supernovaasteroidhunter.py +1716 -0
  294. setiastro/saspro/swap_manager.py +99 -0
  295. setiastro/saspro/torch_backend.py +89 -0
  296. setiastro/saspro/torch_rejection.py +434 -0
  297. setiastro/saspro/translations/de_translations.py +3733 -0
  298. setiastro/saspro/translations/es_translations.py +3923 -0
  299. setiastro/saspro/translations/fr_translations.py +3842 -0
  300. setiastro/saspro/translations/integrate_translations.py +234 -0
  301. setiastro/saspro/translations/it_translations.py +3662 -0
  302. setiastro/saspro/translations/ja_translations.py +3585 -0
  303. setiastro/saspro/translations/pt_translations.py +3853 -0
  304. setiastro/saspro/translations/saspro_de.qm +0 -0
  305. setiastro/saspro/translations/saspro_de.ts +253 -0
  306. setiastro/saspro/translations/saspro_es.qm +0 -0
  307. setiastro/saspro/translations/saspro_es.ts +12520 -0
  308. setiastro/saspro/translations/saspro_fr.qm +0 -0
  309. setiastro/saspro/translations/saspro_fr.ts +12514 -0
  310. setiastro/saspro/translations/saspro_it.qm +0 -0
  311. setiastro/saspro/translations/saspro_it.ts +12520 -0
  312. setiastro/saspro/translations/saspro_ja.qm +0 -0
  313. setiastro/saspro/translations/saspro_ja.ts +257 -0
  314. setiastro/saspro/translations/saspro_pt.qm +0 -0
  315. setiastro/saspro/translations/saspro_pt.ts +257 -0
  316. setiastro/saspro/translations/saspro_zh.qm +0 -0
  317. setiastro/saspro/translations/saspro_zh.ts +12520 -0
  318. setiastro/saspro/translations/zh_translations.py +3659 -0
  319. setiastro/saspro/versioning.py +71 -0
  320. setiastro/saspro/view_bundle.py +1555 -0
  321. setiastro/saspro/wavescale_hdr.py +624 -0
  322. setiastro/saspro/wavescale_hdr_preset.py +101 -0
  323. setiastro/saspro/wavescalede.py +658 -0
  324. setiastro/saspro/wavescalede_preset.py +230 -0
  325. setiastro/saspro/wcs_update.py +374 -0
  326. setiastro/saspro/whitebalance.py +456 -0
  327. setiastro/saspro/widgets/__init__.py +48 -0
  328. setiastro/saspro/widgets/common_utilities.py +306 -0
  329. setiastro/saspro/widgets/graphics_views.py +122 -0
  330. setiastro/saspro/widgets/image_utils.py +518 -0
  331. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  332. setiastro/saspro/widgets/spinboxes.py +275 -0
  333. setiastro/saspro/widgets/themed_buttons.py +13 -0
  334. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  335. setiastro/saspro/window_shelf.py +185 -0
  336. setiastro/saspro/xisf.py +1123 -0
  337. setiastrosuitepro-1.6.1.dist-info/METADATA +267 -0
  338. setiastrosuitepro-1.6.1.dist-info/RECORD +342 -0
  339. setiastrosuitepro-1.6.1.dist-info/WHEEL +4 -0
  340. setiastrosuitepro-1.6.1.dist-info/entry_points.txt +6 -0
  341. setiastrosuitepro-1.6.1.dist-info/licenses/LICENSE +674 -0
  342. setiastrosuitepro-1.6.1.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,37 @@
1
+ # imageops/__init__.py
2
+
3
+ from .stretch import (
4
+ stretch_mono_image,
5
+ stretch_color_image,
6
+ apply_curves_adjustment,
7
+ )
8
+
9
+ # --- Backward-compatible aliases (old SASv2-style names) ---
10
+ def stretch_color_image_linked(img, target_median, normalize=False,
11
+ apply_curves=False, curves_boost=0.0):
12
+ return stretch_color_image(
13
+ img, target_median,
14
+ linked=True,
15
+ normalize=normalize,
16
+ apply_curves=apply_curves,
17
+ curves_boost=curves_boost,
18
+ )
19
+
20
+ def stretch_color_image_unlinked(img, target_median, normalize=False,
21
+ apply_curves=False, curves_boost=0.0):
22
+ return stretch_color_image(
23
+ img, target_median,
24
+ linked=False,
25
+ normalize=normalize,
26
+ apply_curves=apply_curves,
27
+ curves_boost=curves_boost,
28
+ )
29
+
30
+ __all__ = [
31
+ "stretch_mono_image",
32
+ "stretch_color_image",
33
+ "apply_curves_adjustment",
34
+ "stretch_color_image_linked",
35
+ "stretch_color_image_unlinked",
36
+ "apply_average_neutral_scnr",
37
+ ]
@@ -0,0 +1,292 @@
1
+ # pro/mdi_snap.py
2
+ from __future__ import annotations
3
+ from PyQt6.QtCore import Qt, QRect, QPoint, QObject, QEvent, QSize
4
+ from PyQt6.QtGui import QPainter, QPen, QPalette
5
+ from PyQt6.QtWidgets import QWidget, QMdiArea, QMdiSubWindow
6
+
7
+ def _dpi_scaled(widget: QWidget, px: int) -> int:
8
+ try:
9
+ ratio = float(widget.devicePixelRatioF())
10
+ except Exception:
11
+ ratio = 1.0
12
+ return max(1, int(round(px * ratio)))
13
+
14
+ class _GuideOverlay(QWidget):
15
+ """Thin, non-interactive overlay that draws snap guides on the MDI viewport."""
16
+ def __init__(self, parent: QWidget):
17
+ super().__init__(parent)
18
+ self.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents, True)
19
+ self.setAttribute(Qt.WidgetAttribute.WA_NoSystemBackground, True)
20
+ self._rects: list[QRect] = []
21
+ self.hide()
22
+
23
+ def set_guides(self, rects: list[QRect]):
24
+ self._rects = rects or []
25
+ if self._rects:
26
+ if self.isHidden():
27
+ self.show()
28
+ else:
29
+ if self.isVisible():
30
+ self.hide()
31
+ self.update()
32
+
33
+ def paintEvent(self, _ev):
34
+ if not self._rects:
35
+ return
36
+ p = QPainter(self)
37
+ pen = QPen()
38
+ pen.setWidth(1)
39
+ pen.setColor(self.palette().color(QPalette.ColorRole.Highlight))
40
+ pen.setStyle(Qt.PenStyle.SolidLine)
41
+ p.setPen(pen)
42
+ for r in self._rects:
43
+ p.drawRect(r)
44
+
45
+ class MdiSnapController(QObject):
46
+ """
47
+ Adds 'sticky' snapping for QMdiSubWindow edges inside a QMdiArea (Qt6-safe).
48
+ - Snaps to sibling subwindow edges and viewport edges.
49
+ - Shows faint guide lines while snapping.
50
+ - Hold Alt to temporarily disable snapping.
51
+ """
52
+ def __init__(self, mdi: QMdiArea, threshold_px: int = 8, show_guides: bool = False):
53
+ super().__init__(mdi)
54
+ self.mdi = mdi
55
+ self.view = mdi.viewport() # geometry space of subwindows
56
+ self.overlay = _GuideOverlay(self.view)
57
+ self.threshold = max(1, int(threshold_px))
58
+ self._active: QMdiSubWindow | None = None
59
+ self._snap_enabled = True
60
+ self._show_guides = bool(show_guides)
61
+ self._install()
62
+
63
+ def set_show_guides(self, enabled: bool):
64
+ self._show_guides = bool(enabled)
65
+ if not self._show_guides:
66
+ self.overlay.set_guides([])
67
+
68
+ # --- public knobs ---
69
+ def set_threshold(self, px: int):
70
+ self.threshold = max(1, int(px))
71
+
72
+ def install_on(self, sub: QMdiSubWindow):
73
+ # Avoid double-install
74
+ sub.removeEventFilter(self)
75
+ sub.installEventFilter(self)
76
+
77
+ # --- internals ---
78
+ def _install(self):
79
+ # Track active subwindow
80
+ self.mdi.subWindowActivated.connect(self._on_activated)
81
+
82
+ # Watch existing subs
83
+ for sw in self.mdi.subWindowList():
84
+ self.install_on(sw)
85
+
86
+ # Keep overlay matched to viewport size/visibility
87
+ self.view.installEventFilter(self)
88
+ self.overlay.setGeometry(self.view.rect())
89
+
90
+ # Periodically re-check list on activation (new windows)
91
+ self.mdi.subWindowActivated.connect(lambda _sw: self._refresh_watch_list())
92
+
93
+ def _refresh_watch_list(self):
94
+ for sw in self.mdi.subWindowList():
95
+ self.install_on(sw)
96
+
97
+ def _on_activated(self, sw: QMdiSubWindow | None):
98
+ self._active = sw
99
+
100
+ # Gather candidate snap edges (x/y positions) from siblings & viewport
101
+ def _collect_edges(self, ignore: QMdiSubWindow | None):
102
+ siblings = [s for s in self.mdi.subWindowList() if s is not ignore]
103
+ vp = self.view.rect()
104
+
105
+ xs, ys = set(), set()
106
+ rects = []
107
+ for s in siblings:
108
+ r = s.geometry() # already in viewport coords
109
+ rects.append(r)
110
+ xs.update([r.left(), r.right()])
111
+ ys.update([r.top(), r.bottom()])
112
+
113
+ # viewport edges (no center)
114
+ xs.update([vp.left(), vp.right()])
115
+ ys.update([vp.top(), vp.bottom()])
116
+ return sorted(xs), sorted(ys), rects, vp
117
+
118
+ @staticmethod
119
+ def _nearest(value: int, candidates: list[int], tol: int) -> tuple[bool, int]:
120
+ """
121
+ Return (True, snapped_value) if any candidate is within tol of value,
122
+ otherwise (False, value).
123
+ """
124
+ best_val = value
125
+ best_d = tol + 1
126
+ for c in candidates:
127
+ d = abs(c - value)
128
+ if d < best_d:
129
+ best_d = d
130
+ best_val = c
131
+ if best_d <= tol:
132
+ return True, best_val
133
+ return False, value
134
+
135
+ def _build_guides(self, snap_rect: QRect, vp: QRect) -> list[QRect]:
136
+ """Horizontal and vertical guides along the snapped rect edges."""
137
+ lines: list[QRect] = []
138
+ w = _dpi_scaled(self.view, 2)
139
+ # horizontal
140
+ lines.append(QRect(vp.left(), snap_rect.top(), vp.width(), w))
141
+ lines.append(QRect(vp.left(), snap_rect.bottom(), vp.width(), w))
142
+ # vertical
143
+ lines.append(QRect(snap_rect.left(), vp.top(), w, vp.height()))
144
+ lines.append(QRect(snap_rect.right(), vp.top(), w, vp.height()))
145
+ return lines
146
+
147
+ def _snap_geometry(
148
+ self,
149
+ g: QRect,
150
+ xs: list[int],
151
+ ys: list[int],
152
+ tol: int,
153
+ size_snap: bool
154
+ ) -> tuple[QRect, list[QRect]]:
155
+ """
156
+ - If size_snap is False (Move): keep W/H fixed and move the rect so the
157
+ nearest edges line up with candidates.
158
+ - If size_snap is True (Resize): keep top/left fixed and adjust W/H so
159
+ right/bottom edges snap to nearby candidates.
160
+ """
161
+ L, T = g.left(), g.top()
162
+ W, H = g.width(), g.height()
163
+ R, B = L + W - 1, T + H - 1
164
+
165
+ vp = self.view.rect()
166
+ snapped = False
167
+
168
+ if not size_snap:
169
+ # --- MOVE MODE: translate the rect, no size change ---
170
+ okL, snapL = self._nearest(L, xs, tol)
171
+ okR, snapR = self._nearest(R, xs, tol)
172
+
173
+ dx = 0
174
+ if okL and okR:
175
+ dL = snapL - L
176
+ dR = snapR - R
177
+ dx = dL if abs(dL) <= abs(dR) else dR
178
+ snapped = True
179
+ elif okL:
180
+ dx = snapL - L
181
+ snapped = True
182
+ elif okR:
183
+ dx = snapR - R
184
+ snapped = True
185
+
186
+ okT, snapT = self._nearest(T, ys, tol)
187
+ okB, snapB = self._nearest(B, ys, tol)
188
+
189
+ dy = 0
190
+ if okT and okB:
191
+ dT = snapT - T
192
+ dB = snapB - B
193
+ dy = dT if abs(dT) <= abs(dB) else dB
194
+ snapped = True
195
+ elif okT:
196
+ dy = snapT - T
197
+ snapped = True
198
+ elif okB:
199
+ dy = snapB - B
200
+ snapped = True
201
+
202
+ new_L = L + dx
203
+ new_T = T + dy
204
+ g2 = QRect(QPoint(new_L, new_T), QSize(W, H))
205
+
206
+ else:
207
+ # --- RESIZE MODE: keep L/T fixed, snap R/B by changing W/H ---
208
+ okR, snapR = self._nearest(R, xs, tol)
209
+ okB, snapB = self._nearest(B, ys, tol)
210
+
211
+ new_W = W
212
+ new_H = H
213
+
214
+ if okR:
215
+ new_W = max(1, (snapR - L + 1))
216
+ snapped = True
217
+ if okB:
218
+ new_H = max(1, (snapB - T + 1))
219
+ snapped = True
220
+
221
+ g2 = QRect(QPoint(L, T), QSize(new_W, new_H))
222
+
223
+ guides = (
224
+ self._build_guides(g2, vp)
225
+ if (snapped and self._show_guides)
226
+ else []
227
+ )
228
+ return g2, guides
229
+
230
+ # --- Event filter on each subwindow + viewport ---
231
+ def eventFilter(self, obj: QObject, ev: QEvent) -> bool:
232
+ t = ev.type()
233
+
234
+ # Keep overlay sized to the viewport
235
+ if obj is self.view:
236
+ if t == QEvent.Type.Resize:
237
+ self.overlay.setGeometry(self.view.rect())
238
+ self.overlay.update()
239
+ elif t in _CLEAR_EVENTS:
240
+ self.overlay.set_guides([])
241
+ return False
242
+
243
+ if not isinstance(obj, QMdiSubWindow):
244
+ return super().eventFilter(obj, ev)
245
+
246
+ # Alt disables snapping while held
247
+ try:
248
+ mods = obj.window().keyboardModifiers()
249
+ self._snap_enabled = not bool(mods & Qt.KeyboardModifier.AltModifier)
250
+ except Exception:
251
+ self._snap_enabled = True
252
+
253
+ if t in (QEvent.Type.Move, QEvent.Type.Resize):
254
+ if not self._snap_enabled:
255
+ self.overlay.set_guides([])
256
+ return super().eventFilter(obj, ev)
257
+
258
+ xs, ys, _rects, _vp = self._collect_edges(ignore=obj)
259
+ tol = _dpi_scaled(self.view, self.threshold)
260
+ cur = obj.geometry()
261
+
262
+ # <<< key change: size_snap only during Resize >>>
263
+ size_snap = (t == QEvent.Type.Resize)
264
+ snapped_rect, guides = self._snap_geometry(
265
+ cur, xs, ys, tol, size_snap=size_snap
266
+ )
267
+
268
+ if snapped_rect != cur:
269
+ # Minimize feedback loops
270
+ obj.blockSignals(True)
271
+ try:
272
+ obj.setGeometry(snapped_rect)
273
+ finally:
274
+ obj.blockSignals(False)
275
+
276
+ self.overlay.set_guides(guides if (self._show_guides and guides) else [])
277
+ return False
278
+
279
+ # Qt6-safe "clear overlay" conditions
280
+ if t in (QEvent.Type.Hide, QEvent.Type.Leave):
281
+ if self._show_guides:
282
+ self.overlay.set_guides([])
283
+
284
+ return super().eventFilter(obj, ev)
285
+
286
+
287
+ # ---- Qt6-safe clear events set (no MoveAboutToBeAnimated) -------------------
288
+ _CLEAR_EVENTS = set()
289
+ for _name in ("Hide", "Leave", "MouseButtonRelease", "WindowDeactivate", "FocusOut"):
290
+ _val = getattr(QEvent.Type, _name, None)
291
+ if _val is not None:
292
+ _CLEAR_EVENTS.add(_val)
@@ -0,0 +1,36 @@
1
+ # pro/imageops/scnr.py
2
+ from __future__ import annotations
3
+ import numpy as np
4
+
5
+ def apply_average_neutral_scnr(image: np.ndarray, amount: float = 1.0) -> np.ndarray:
6
+ """
7
+ Average Neutral SCNR (green removal).
8
+ Expects an RGB image normalized to [0, 1]. Returns float32 in [0, 1].
9
+
10
+ amount: 0.0 → no effect, 1.0 → full SCNR
11
+ """
12
+ if not isinstance(image, np.ndarray):
13
+ raise TypeError("Input image must be a NumPy array.")
14
+ if image.ndim != 3 or image.shape[2] != 3:
15
+ raise ValueError("Input image must have three channels (RGB).")
16
+ if not (0.0 <= amount <= 1.0):
17
+ raise ValueError("Amount parameter must be between 0.0 and 1.0.")
18
+
19
+ img = image.astype(np.float32, copy=False)
20
+
21
+ R = img[..., 0]
22
+ G = img[..., 1]
23
+ B = img[..., 2]
24
+
25
+ # G' = min(G, 0.5*(R + B)) - optimized: compute blended G directly without full array copy
26
+ G_scnr = np.minimum(G, 0.5 * (R + B))
27
+
28
+ # Blend original G and SCNR G directly: avoids allocating a full copy of the image
29
+ G_blended = G + amount * (G_scnr - G) # Equivalent to (1-amount)*G + amount*G_scnr
30
+
31
+ # Build output array only once
32
+ out = np.empty_like(img, dtype=np.float32)
33
+ out[..., 0] = R
34
+ out[..., 1] = np.clip(G_blended, 0.0, 1.0)
35
+ out[..., 2] = B
36
+ return out
@@ -0,0 +1,210 @@
1
+ # imageops/starbasedwhitebalance.py
2
+ from __future__ import annotations
3
+
4
+ import numpy as np
5
+
6
+ # Optional deps
7
+ try:
8
+ import cv2 # for ellipse overlay
9
+ except Exception: # pragma: no cover
10
+ cv2 = None
11
+
12
+ try:
13
+ import sep # Source Extractor
14
+ except Exception as e: # pragma: no cover
15
+ sep = None
16
+ _sep_import_error = e
17
+ else:
18
+ _sep_import_error = None
19
+
20
+ from typing import Tuple, Optional
21
+ from .stretch import stretch_color_image
22
+
23
+ # Shared utilities
24
+ from setiastro.saspro.widgets.image_utils import to_float01 as _to_float01
25
+
26
+ __all__ = ["apply_star_based_white_balance"]
27
+
28
+ # simple cache (reused when reuse_cached_sources=True)
29
+ cached_star_sources: Optional[np.ndarray] = None
30
+ cached_flux_radii: Optional[np.ndarray] = None
31
+
32
+
33
+ def _tone_preserve_bg_neutralize(rgb: np.ndarray) -> np.ndarray:
34
+ """
35
+ Neutralize background using the darkest grid patch in a tone-preserving way.
36
+ Operates in-place on a copy; returns the neutralized image (float32 [0,1]).
37
+ """
38
+ h, w = rgb.shape[:2]
39
+ patch_size = 10
40
+ ph = max(1, h // patch_size)
41
+ pw = max(1, w // patch_size)
42
+
43
+ best = None
44
+ best_sum = float("inf")
45
+ for i in range(patch_size):
46
+ for j in range(patch_size):
47
+ y0, x0 = i * ph, j * pw
48
+ y1, x1 = min(y0 + ph, h), min(x0 + pw, w)
49
+ patch = rgb[y0:y1, x0:x1, :]
50
+ med = np.median(patch, axis=(0, 1))
51
+ s = float(np.sum(med))
52
+ if s < best_sum:
53
+ best_sum = s
54
+ best = med
55
+
56
+ out = rgb.copy()
57
+ if best is not None:
58
+ avg = float(np.mean(best))
59
+ # “tone-preserving” shift+scale channel-wise toward avg
60
+ for c in range(3):
61
+ diff = float(best[c] - avg)
62
+ denom = (1.0 - diff) if abs(1.0 - diff) > 1e-8 else 1e-8
63
+ out[:, :, c] = np.clip((out[:, :, c] - diff) / denom, 0.0, 1.0)
64
+ return out
65
+
66
+
67
+ def apply_star_based_white_balance(
68
+ image: np.ndarray,
69
+ threshold: float = 1.5,
70
+ autostretch: bool = True,
71
+ reuse_cached_sources: bool = False,
72
+ return_star_colors: bool = False
73
+ ) -> Tuple[np.ndarray, int, np.ndarray, np.ndarray, np.ndarray] | Tuple[np.ndarray, int, np.ndarray]:
74
+ """
75
+ Star-based white balance with background neutralization and an RGB overlay of detected stars.
76
+
77
+ Parameters
78
+ ----------
79
+ image : np.ndarray
80
+ RGB image (any dtype). Assumed RGB ordering.
81
+ threshold : float
82
+ SExtractor detection threshold (in background sigma).
83
+ autostretch : bool
84
+ If True, overlay is built from an autostretched view for visibility.
85
+ reuse_cached_sources : bool
86
+ If True, reuses star positions measured on a previous call (same scene).
87
+ return_star_colors : bool
88
+ If True, also returns (raw_star_pixels, after_star_pixels).
89
+
90
+ Returns
91
+ -------
92
+ balanced_rgb : float32 RGB in [0,1]
93
+ star_count : int
94
+ overlay_rgb : float32 RGB in [0,1] with star ellipses drawn
95
+ (optional) raw_star_pixels : (N,3) float array, colors sampled from ORIGINAL image
96
+ (optional) after_star_pixels : (N,3) float array, colors sampled after WB
97
+ """
98
+ if image.ndim != 3 or image.shape[2] != 3:
99
+ raise ValueError("apply_star_based_white_balance: input must be an RGB image (H,W,3).")
100
+
101
+ # 0) normalize
102
+ img_rgb = _to_float01(image)
103
+
104
+ # 1) first background neutralization (tone-preserving)
105
+ bg_neutral = _tone_preserve_bg_neutralize(img_rgb)
106
+
107
+ # 2) detect / reuse star positions
108
+ if sep is None:
109
+ raise ImportError(
110
+ "apply_star_based_white_balance requires the 'sep' package. "
111
+ f"Import error was: {_sep_import_error!r}"
112
+ )
113
+
114
+ gray = np.mean(bg_neutral, axis=2).astype(np.float32, copy=False)
115
+ bkg = sep.Background(gray)
116
+ data_sub = gray - bkg.back()
117
+ err_val = float(bkg.globalrms)
118
+
119
+ global cached_star_sources, cached_flux_radii
120
+
121
+ if reuse_cached_sources and cached_star_sources is not None:
122
+ sources = cached_star_sources
123
+ r = cached_flux_radii
124
+ else:
125
+ sources = sep.extract(data_sub, threshold, err=err_val)
126
+ if sources is None or len(sources) == 0:
127
+ raise ValueError("No sources detected for Star-Based White Balance.")
128
+ r, _ = sep.flux_radius(
129
+ gray,
130
+ sources["x"], sources["y"],
131
+ 2.0 * sources["a"], 0.2,
132
+ normflux=sources["flux"],
133
+ subpix=5
134
+ )
135
+ cached_star_sources = sources
136
+ cached_flux_radii = r
137
+
138
+ # filter: small-ish, star-like
139
+ mask = (r > 0) & (r <= 10)
140
+ sources = sources[mask]
141
+ r = r[mask]
142
+ if len(sources) == 0:
143
+ raise ValueError("All detected sources were rejected as non-stellar (too large).")
144
+
145
+ h, w = gray.shape
146
+ # raw colors from ORIGINAL image - optimized vectorized extraction
147
+ xs = sources["x"].astype(np.int32)
148
+ ys = sources["y"].astype(np.int32)
149
+ valid = (xs >= 0) & (xs < w) & (ys >= 0) & (ys < h)
150
+ raw_star_pixels = img_rgb[ys[valid], xs[valid], :]
151
+
152
+ # 3) build overlay (autostretched if requested) and draw ellipses
153
+ disp = stretch_color_image(bg_neutral.copy(), 0.25) if autostretch else bg_neutral.copy()
154
+
155
+ if cv2 is not None:
156
+ overlay_bgr = cv2.cvtColor((disp * 255).astype(np.uint8), cv2.COLOR_RGB2BGR)
157
+ for i in range(len(sources)):
158
+ cx = float(sources["x"][i]); cy = float(sources["y"][i])
159
+ a = float(sources["a"][i]); b = float(sources["b"][i])
160
+ theta_deg = float(sources["theta"][i] * 180.0 / np.pi)
161
+ center = (int(round(cx)), int(round(cy)))
162
+ axes = (max(1, int(round(3 * a))), max(1, int(round(3 * b))))
163
+ # red ellipse in BGR
164
+ cv2.ellipse(overlay_bgr, center, axes, angle=theta_deg, startAngle=0, endAngle=360,
165
+ color=(0, 0, 255), thickness=1)
166
+ overlay_rgb = cv2.cvtColor(overlay_bgr, cv2.COLOR_BGR2RGB).astype(np.float32) / 255.0
167
+ else:
168
+ # fallback: no ellipses, just the display image
169
+ overlay_rgb = disp.astype(np.float32, copy=False)
170
+
171
+ # 4) compute WB scale using star colors sampled on bg_neutral image
172
+ # Optimized: vectorized extraction instead of Python loop (10-50x faster)
173
+ xs = sources["x"].astype(np.int32)
174
+ ys = sources["y"].astype(np.int32)
175
+ valid_mask = (xs >= 0) & (xs < w) & (ys >= 0) & (ys < h)
176
+
177
+ if not np.any(valid_mask):
178
+ raise ValueError("No stellar samples available for white balance.")
179
+
180
+ star_pixels = bg_neutral[ys[valid_mask], xs[valid_mask], :].astype(np.float32)
181
+ avg_color = np.mean(star_pixels, axis=0)
182
+ max_val = float(np.max(avg_color))
183
+ # protect against divide-by-zero
184
+ avg_color = np.where(avg_color <= 1e-8, 1e-8, avg_color)
185
+ scaling = max_val / avg_color
186
+
187
+ balanced = (bg_neutral * scaling.reshape((1, 1, 3))).clip(0.0, 1.0)
188
+
189
+ # 5) second background neutralization pass on balanced image
190
+ balanced = _tone_preserve_bg_neutralize(balanced)
191
+
192
+ # 6) collect after-WB star samples - optimized vectorized extraction
193
+ after_star_pixels = balanced[ys[valid_mask], xs[valid_mask], :]
194
+
195
+ if return_star_colors:
196
+ return (
197
+ balanced.astype(np.float32, copy=False),
198
+ int(len(star_pixels)),
199
+ overlay_rgb.astype(np.float32, copy=False),
200
+ np.asarray(raw_star_pixels, dtype=np.float32),
201
+ np.asarray(after_star_pixels, dtype=np.float32),
202
+ )
203
+
204
+ return (
205
+ balanced.astype(np.float32, copy=False),
206
+ int(len(star_pixels)),
207
+ overlay_rgb.astype(np.float32, copy=False),
208
+ )
209
+
210
+