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,621 @@
1
+ # pro/add_stars.py
2
+ from __future__ import annotations
3
+ import os
4
+ import numpy as np
5
+
6
+ # Qt
7
+ from PyQt6.QtCore import Qt, pyqtSignal, QTimer
8
+ from PyQt6.QtGui import QImage, QPixmap, QWheelEvent
9
+ from PyQt6.QtWidgets import (
10
+ QDialog, QVBoxLayout, QHBoxLayout, QGridLayout,
11
+ QLabel, QPushButton, QScrollArea, QSizePolicy,
12
+ QComboBox, QSlider, QMessageBox, QFileDialog, QFormLayout
13
+ )
14
+
15
+ # I/O (use your legacy functions)
16
+ from setiastro.saspro.legacy.image_manager import load_image
17
+ from setiastro.saspro.widgets.themed_buttons import themed_toolbtn
18
+
19
+
20
+ try:
21
+ import cv2
22
+ except Exception:
23
+ cv2 = None
24
+
25
+
26
+ # ──────────────────────────────────────────────────────────────────────────────
27
+ # Helpers to enumerate docs and masks from the Pro app
28
+ # ──────────────────────────────────────────────────────────────────────────────
29
+ # REPLACE OLD _iter_open_docs WITH THIS
30
+ def _iter_open_docs(main):
31
+ """
32
+ Find open views/docs by:
33
+ 1) docman.{documents|docs|open_docs|views|iter_docs|all_docs}
34
+ 2) Any attribute on main that has subWindowList() (QMdiArea), pulling docs
35
+ from subwindow.widget().{doc,_doc,document} or the widget itself if it
36
+ exposes an image.
37
+ Returns a list of (label, provider) where provider can be a doc or widget.
38
+ """
39
+ def _label_for(obj, fallback):
40
+ name = ""
41
+ try:
42
+ if hasattr(obj, "display_name") and callable(obj.display_name):
43
+ name = obj.display_name()
44
+ else:
45
+ name = getattr(obj, "name", "") or ""
46
+ except Exception:
47
+ name = ""
48
+ return name or fallback or f"View {len(items)}"
49
+
50
+ def _image_from_any(x):
51
+ """Robustly get a numpy-ish image from doc/widget."""
52
+ if x is None:
53
+ return None
54
+ chain = [x, getattr(x, "doc", None), getattr(x, "_doc", None), getattr(x, "document", None)]
55
+ for c in chain:
56
+ if c is None:
57
+ continue
58
+ img = getattr(c, "image", None)
59
+ if img is not None:
60
+ try:
61
+ a = np.asarray(img)
62
+ if a is not None and a.size:
63
+ return a
64
+ except Exception:
65
+ pass
66
+ # method fallbacks
67
+ for m in ("get_image", "current_image", "image_array"):
68
+ f = getattr(c, m, None)
69
+ if callable(f):
70
+ try:
71
+ a = f()
72
+ a = np.asarray(a) if a is not None else None
73
+ if a is not None and a.size:
74
+ return a
75
+ except Exception:
76
+ pass
77
+ return None
78
+
79
+ def _add_item(obj, label_hint=None):
80
+ img = _image_from_any(obj)
81
+ if img is None:
82
+ return
83
+ key = id(getattr(obj, "image", obj)) # stable-ish identity
84
+ if key in seen:
85
+ return
86
+ seen.add(key)
87
+ items.append((_label_for(obj, label_hint), obj))
88
+
89
+ items, seen = [], set()
90
+
91
+ # 1) docman sources
92
+ dm = getattr(main, "docman", None)
93
+ if dm is not None:
94
+ for attr in ("documents", "docs", "open_docs", "views"):
95
+ coll = getattr(dm, attr, None)
96
+ if isinstance(coll, dict):
97
+ for d in coll.values():
98
+ _add_item(d)
99
+ elif isinstance(coll, (list, tuple, set)):
100
+ for d in coll:
101
+ _add_item(d)
102
+ for meth in ("iter_docs", "all_docs", "iter"):
103
+ fn = getattr(dm, meth, None)
104
+ if callable(fn):
105
+ try:
106
+ for d in fn():
107
+ _add_item(d)
108
+ except Exception:
109
+ pass
110
+
111
+ # 2) any QMdiArea on main
112
+ for attr in dir(main):
113
+ try:
114
+ val = getattr(main, attr)
115
+ except Exception:
116
+ continue
117
+ if hasattr(val, "subWindowList"):
118
+ try:
119
+ for sw in val.subWindowList():
120
+ title = ""
121
+ try:
122
+ title = sw.windowTitle()
123
+ except Exception:
124
+ pass
125
+ w = None
126
+ try:
127
+ w = sw.widget()
128
+ except Exception:
129
+ pass
130
+ # prefer an actual doc if present; fallback to widget
131
+ for candidate in (
132
+ getattr(w, "doc", None),
133
+ getattr(w, "_doc", None),
134
+ getattr(w, "document", None),
135
+ w,
136
+ ):
137
+ if candidate is None:
138
+ continue
139
+ if _image_from_any(candidate) is not None:
140
+ _add_item(candidate, label_hint=title)
141
+ break
142
+ except Exception:
143
+ continue
144
+
145
+ return items
146
+
147
+
148
+
149
+ def _doc_image(doc_like) -> np.ndarray | None:
150
+ """
151
+ Accepts a doc or a view widget and returns a float32 image array
152
+ (mono 2D or RGB 3D). No boolean ops on arrays to avoid ambiguity.
153
+ """
154
+ def _grab(x):
155
+ if x is None:
156
+ return None
157
+ # direct attribute
158
+ img = getattr(x, "image", None)
159
+ if img is not None:
160
+ return img
161
+ # method fallbacks
162
+ for m in ("get_image", "current_image", "image_array"):
163
+ fn = getattr(x, m, None)
164
+ if callable(fn):
165
+ try:
166
+ return fn()
167
+ except Exception:
168
+ pass
169
+ return None
170
+
171
+ img = _grab(doc_like)
172
+ if img is None:
173
+ img = _grab(getattr(doc_like, "doc", None))
174
+ if img is None:
175
+ img = _grab(getattr(doc_like, "_doc", None))
176
+ if img is None:
177
+ img = _grab(getattr(doc_like, "document", None))
178
+ if img is None:
179
+ return None
180
+
181
+ a = np.asarray(img).astype(np.float32, copy=False)
182
+ if a.ndim == 3 and a.shape[2] == 1:
183
+ a = a[..., 0]
184
+ # Defensive normalization for big float ranges
185
+ if a.dtype.kind == "f" and a.size:
186
+ mx = float(a.max())
187
+ if mx > 5.0:
188
+ a = a / mx
189
+ return a
190
+
191
+
192
+
193
+
194
+ def _active_mask_array_from_doc(doc) -> np.ndarray | None:
195
+ """
196
+ Return active mask (H,W) float32 in [0,1] from the document, if present.
197
+ """
198
+ try:
199
+ mid = getattr(doc, "active_mask_id", None)
200
+ if not mid:
201
+ return None
202
+ masks = getattr(doc, "masks", {}) or {}
203
+ layer = masks.get(mid)
204
+ data = getattr(layer, "data", None) if layer is not None else None
205
+ if data is None:
206
+ return None
207
+ a = np.asarray(data)
208
+ if a.ndim == 3:
209
+ if cv2 is not None:
210
+ a = cv2.cvtColor(a, cv2.COLOR_BGR2GRAY)
211
+ else:
212
+ a = a.mean(axis=2)
213
+ a = a.astype(np.float32, copy=False)
214
+ a = np.clip(a, 0.0, 1.0)
215
+ return a
216
+ except Exception:
217
+ return None
218
+
219
+
220
+ # ──────────────────────────────────────────────────────────────────────────────
221
+ # Dialog
222
+ # ──────────────────────────────────────────────────────────────────────────────
223
+ class AddStarsDialog(QDialog):
224
+ stars_added = pyqtSignal(object, object)
225
+ def __init__(self, main, parent=None):
226
+ super().__init__(parent)
227
+ self.setWindowTitle(self.tr("Add Stars to Image"))
228
+
229
+ self.setWindowFlag(Qt.WindowType.Window, True)
230
+ self.setWindowModality(Qt.WindowModality.ApplicationModal)
231
+ self.setModal(False)
232
+ #self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
233
+
234
+ self.main = main
235
+ self.starless = None
236
+ self.stars_only = None
237
+ self.blended_image = None
238
+ self.scale_factor = 1.0
239
+ self._fit_once = False
240
+
241
+ self._build_ui()
242
+ self._populate_doc_combos()
243
+
244
+ # UI -----------------------------------------------------------------------
245
+ def _build_ui(self):
246
+ layout = QVBoxLayout(self)
247
+
248
+ # Preview
249
+ self.preview_label = QLabel()
250
+ self.preview_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
251
+ self.preview_label.setSizePolicy(QSizePolicy.Policy.Ignored, QSizePolicy.Policy.Ignored)
252
+ self.preview_label.setScaledContents(False)
253
+
254
+ self.scroll_area = QScrollArea(self)
255
+ self.scroll_area.setWidgetResizable(False)
256
+ self.scroll_area.setAlignment(Qt.AlignmentFlag.AlignCenter)
257
+ self.scroll_area.setWidget(self.preview_label)
258
+ layout.addWidget(self.scroll_area)
259
+
260
+ # Zoom row (standardized themed toolbuttons)
261
+ zrow = QHBoxLayout()
262
+ self.btn_zoom_in = themed_toolbtn("zoom-in", "Zoom In")
263
+ self.btn_zoom_out = themed_toolbtn("zoom-out", "Zoom Out")
264
+ self.btn_fit = themed_toolbtn("zoom-fit-best", "Fit to Preview")
265
+
266
+ self.btn_zoom_in.clicked.connect(self.zoom_in)
267
+ self.btn_zoom_out.clicked.connect(self.zoom_out)
268
+ self.btn_fit.clicked.connect(self.fit_to_preview)
269
+
270
+ zrow.addWidget(self.btn_zoom_in)
271
+ zrow.addWidget(self.btn_zoom_out)
272
+ zrow.addWidget(self.btn_fit)
273
+ zrow.addStretch(1)
274
+ layout.addLayout(zrow)
275
+
276
+ # Selection + blend
277
+ grid = QGridLayout()
278
+
279
+ # Blend type
280
+ grid.addWidget(QLabel(self.tr("Blend Type:")), 0, 0)
281
+ self.cmb_blend = QComboBox(); self.cmb_blend.addItems(["Screen", "Add"])
282
+ self.cmb_blend.currentIndexChanged.connect(self.update_preview)
283
+ grid.addWidget(self.cmb_blend, 0, 1)
284
+
285
+ # Starless source
286
+ grid.addWidget(QLabel(self.tr("Starless View:")), 1, 0)
287
+ self.cmb_starless = QComboBox(); grid.addWidget(self.cmb_starless, 1, 1)
288
+ btn_sless_file = QPushButton(self.tr("Load from File")); btn_sless_file.clicked.connect(lambda: self._load_from_file('starless'))
289
+ grid.addWidget(btn_sless_file, 1, 2)
290
+
291
+ # Stars-only source
292
+ grid.addWidget(QLabel(self.tr("Stars-Only View:")), 2, 0)
293
+ self.cmb_stars = QComboBox(); grid.addWidget(self.cmb_stars, 2, 1)
294
+ btn_stars_file = QPushButton(self.tr("Load from File")); btn_stars_file.clicked.connect(lambda: self._load_from_file('stars'))
295
+ grid.addWidget(btn_stars_file, 2, 2)
296
+
297
+ layout.addLayout(grid)
298
+
299
+ refresh_row = QHBoxLayout()
300
+ btn_refresh = QPushButton(self.tr("Refresh Views"))
301
+ btn_refresh.clicked.connect(self._populate_doc_combos)
302
+ refresh_row.addStretch(1)
303
+ refresh_row.addWidget(btn_refresh)
304
+ layout.addLayout(refresh_row)
305
+
306
+ # Ratio slider
307
+ row = QHBoxLayout()
308
+ row.addWidget(QLabel(self.tr("Blend Ratio (Screen/Add Intensity):")))
309
+ self.slider_ratio = QSlider(Qt.Orientation.Horizontal)
310
+ self.slider_ratio.setRange(0, 100); self.slider_ratio.setValue(100)
311
+ self.slider_ratio.setTickInterval(10); self.slider_ratio.setTickPosition(QSlider.TickPosition.TicksBelow)
312
+ self.slider_ratio.valueChanged.connect(self.update_preview)
313
+ row.addWidget(self.slider_ratio)
314
+ layout.addLayout(row)
315
+
316
+ # Buttons
317
+ brow = QHBoxLayout(); brow.addStretch(1)
318
+ btn_apply = QPushButton(self.tr("Apply")); btn_apply.clicked.connect(self._apply)
319
+ btn_cancel= QPushButton(self.tr("Cancel")); btn_cancel.clicked.connect(self.reject)
320
+ brow.addWidget(btn_apply); brow.addWidget(btn_cancel)
321
+ layout.addLayout(brow)
322
+
323
+ self.setMinimumSize(900, 650)
324
+
325
+ # signals for combos
326
+ self.cmb_starless.currentIndexChanged.connect(self._pick_starless_from_combo)
327
+ self.cmb_stars.currentIndexChanged.connect(self._pick_stars_from_combo)
328
+
329
+ # Populate combos with open docs (+ sentinel for file)
330
+ def _populate_doc_combos(self):
331
+ items = [("Select View", None)]
332
+ for name, d in _iter_open_docs(self.main):
333
+ items.append((name, d))
334
+ items.append(("Load from File", "file"))
335
+
336
+ self.cmb_starless.clear()
337
+ self.cmb_stars.clear()
338
+ for label, data in items:
339
+ self.cmb_starless.addItem(label, data)
340
+ self.cmb_stars.addItem(label, data)
341
+
342
+ # File load ----------------------------------------------------------------
343
+ def _load_from_file(self, which: str):
344
+ fn, _ = QFileDialog.getOpenFileName(
345
+ self, f"Select {'Starless' if which=='starless' else 'Stars-Only'} Image", "",
346
+ "Image Files (*.png *.tif *.tiff *.fits *.fit *.xisf *.jpg *.jpeg)"
347
+ )
348
+ if not fn:
349
+ return
350
+ img, _, _, _ = load_image(fn)
351
+ if img is None:
352
+ QMessageBox.critical(self, "Load Error", f"Failed to load: {os.path.basename(fn)}")
353
+ return
354
+ if which == 'starless':
355
+ self.starless = self._to_rgb01(img)
356
+ self.cmb_starless.setCurrentIndex(self.cmb_starless.count()-1) # "Load from File"
357
+ else:
358
+ self.stars_only = self._to_rgb01(img)
359
+ self.cmb_stars.setCurrentIndex(self.cmb_stars.count()-1)
360
+ self.update_preview()
361
+
362
+ @staticmethod
363
+ def _resolve_doc_object(doc_like):
364
+ if doc_like is None:
365
+ return None
366
+ for c in (doc_like,
367
+ getattr(doc_like, "doc", None),
368
+ getattr(doc_like, "_doc", None),
369
+ getattr(doc_like, "document", None)):
370
+ if c is None:
371
+ continue
372
+ if hasattr(c, "apply_edit") and any(
373
+ hasattr(c, a) for a in ("image", "get_image", "current_image", "image_array")
374
+ ):
375
+ return c
376
+ return None
377
+
378
+ def _target_doc_for_mask(self):
379
+ """Use the selected Starless View's doc (fallback to active doc)."""
380
+ sel = self.cmb_starless.currentData()
381
+ if sel is None or sel == "file":
382
+ doc = getattr(self.main, "_active_doc", None)
383
+ if callable(doc): doc = doc()
384
+ return self._resolve_doc_object(doc)
385
+ return self._resolve_doc_object(sel)
386
+
387
+ # Combo selects ------------------------------------------------------------
388
+ def _pick_starless_from_combo(self):
389
+ data = self.cmb_starless.currentData()
390
+ if data is None or data == "file":
391
+ # None or "Load from File" (the button sets image)
392
+ self.update_preview()
393
+ return
394
+ img = _doc_image(data)
395
+ if img is None:
396
+ QMessageBox.warning(self, "Empty View", "Selected starless view has no image.")
397
+ return
398
+ self.starless = self._to_rgb01(img)
399
+ self.update_preview()
400
+
401
+ def _pick_stars_from_combo(self):
402
+ data = self.cmb_stars.currentData()
403
+ if data is None or data == "file":
404
+ self.update_preview()
405
+ return
406
+ img = _doc_image(data)
407
+ if img is None:
408
+ QMessageBox.warning(self, "Empty View", "Selected stars-only view has no image.")
409
+ return
410
+ self.stars_only = self._to_rgb01(img)
411
+ self.update_preview()
412
+
413
+ # Math ---------------------------------------------------------------------
414
+ @staticmethod
415
+ def _to_rgb01(a: np.ndarray) -> np.ndarray:
416
+ a = np.asarray(a).astype(np.float32, copy=False)
417
+ if a.ndim == 2:
418
+ a = np.stack([a]*3, axis=-1)
419
+ elif a.ndim == 3 and a.shape[2] == 1:
420
+ a = np.repeat(a, 3, axis=2)
421
+ a = np.clip(a, 0.0, 1.0)
422
+ return a
423
+
424
+ def _blend_images(self) -> np.ndarray | None:
425
+ if self.starless is None or self.stars_only is None:
426
+ return None
427
+
428
+ # same size?
429
+ if self.starless.shape != self.stars_only.shape:
430
+ QMessageBox.critical(self, "Size Mismatch", "Starless and Stars-Only views are different sizes.")
431
+ return None
432
+
433
+ mode = self.cmb_blend.currentText()
434
+ r = self.slider_ratio.value() / 100.0
435
+
436
+ if mode == "Screen":
437
+ base = self.starless + self.stars_only - (self.starless * self.stars_only)
438
+ else:
439
+ base = self.starless + self.stars_only
440
+
441
+ blended = (1.0 - r) * self.starless + r * base
442
+ blended = np.clip(blended, 0.0, 1.0)
443
+
444
+ # mask from the *destination* doc (selected Starless View)
445
+ tgt = self._target_doc_for_mask()
446
+ if tgt is not None:
447
+ m = _active_mask_array_from_doc(tgt)
448
+ if m is not None:
449
+ h, w = blended.shape[:2]
450
+ if m.shape != (h, w):
451
+ if cv2 is not None:
452
+ m = cv2.resize(m, (w, h), interpolation=cv2.INTER_NEAREST)
453
+ else:
454
+ yi = (np.linspace(0, m.shape[0]-1, h)).astype(np.int32)
455
+ xi = (np.linspace(0, m.shape[1]-1, w)).astype(np.int32)
456
+ m = m[yi][:, xi]
457
+ m3 = np.repeat(m[:, :, None], 3, axis=2)
458
+ # only replace where mask==1; keep original starless elsewhere
459
+ blended = np.clip(self.starless * (1.0 - m3) + blended * m3, 0.0, 1.0).astype(np.float32, copy=False)
460
+
461
+ return blended
462
+
463
+ # Preview ------------------------------------------------------------------
464
+ def update_preview(self):
465
+ out = self._blend_images()
466
+ self.blended_image = out
467
+ if out is None:
468
+ self.preview_label.clear()
469
+ return
470
+
471
+ pix = self._to_pixmap(out)
472
+ # keep scroll position
473
+ hs = self.scroll_area.horizontalScrollBar().value()
474
+ vs = self.scroll_area.verticalScrollBar().value()
475
+
476
+ scaled = pix.scaled(
477
+ pix.size() * self.scale_factor,
478
+ Qt.AspectRatioMode.KeepAspectRatio,
479
+ Qt.TransformationMode.SmoothTransformation
480
+ )
481
+ self.preview_label.setPixmap(scaled)
482
+ self.preview_label.adjustSize()
483
+ self.scroll_area.horizontalScrollBar().setValue(hs)
484
+ self.scroll_area.verticalScrollBar().setValue(vs)
485
+
486
+ def _to_pixmap(self, img: np.ndarray) -> QPixmap:
487
+ im = np.clip(img, 0.0, 1.0)
488
+ u8 = (im * 255.0 + 0.5).astype(np.uint8)
489
+ if u8.ndim == 2:
490
+ q = QImage(u8.data, u8.shape[1], u8.shape[0], u8.strides[0], QImage.Format.Format_Grayscale8)
491
+ else:
492
+ # RGB888
493
+ q = QImage(u8.data, u8.shape[1], u8.shape[0], u8.strides[0], QImage.Format.Format_RGB888)
494
+ return QPixmap.fromImage(q)
495
+
496
+ # Zoom/fit -----------------------------------------------------------------
497
+ def wheelEvent(self, ev: QWheelEvent):
498
+ if ev.angleDelta().y() > 0:
499
+ self.zoom_in()
500
+ else:
501
+ self.zoom_out()
502
+ ev.accept()
503
+
504
+ def zoom_in(self):
505
+ self.scale_factor *= 1.25
506
+ self._refresh_scaled()
507
+
508
+ def zoom_out(self):
509
+ self.scale_factor /= 1.25
510
+ self._refresh_scaled()
511
+
512
+ def fit_to_preview(self):
513
+ if self.blended_image is None:
514
+ return
515
+ QTimer.singleShot(0, self._do_fit)
516
+
517
+ def _do_fit(self):
518
+ if self.blended_image is None:
519
+ return
520
+ pix = self._to_pixmap(self.blended_image)
521
+ vsz = self.scroll_area.viewport().size()
522
+ if pix.isNull() or pix.width() == 0 or pix.height() == 0:
523
+ return
524
+ sw = vsz.width() / pix.width()
525
+ sh = vsz.height() / pix.height()
526
+ self.scale_factor = min(sw, sh)
527
+ self.update_preview()
528
+
529
+ def _refresh_scaled(self):
530
+ if self.blended_image is None:
531
+ return
532
+ pix = self._to_pixmap(self.blended_image)
533
+ scaled = pix.scaled(
534
+ pix.size() * self.scale_factor,
535
+ Qt.AspectRatioMode.KeepAspectRatio,
536
+ Qt.TransformationMode.SmoothTransformation
537
+ )
538
+ self.preview_label.setPixmap(scaled)
539
+ self.preview_label.adjustSize()
540
+
541
+ # Apply --------------------------------------------------------------------
542
+ def _apply(self):
543
+ """
544
+ Applies the blended image to the selected *Starless View* (or, if the starless
545
+ source is "Load from File", falls back to the active doc).
546
+ """
547
+ if self.blended_image is None:
548
+ QMessageBox.warning(self, "No Blend", "No blended image to apply.")
549
+ return
550
+
551
+ sel = self.cmb_starless.currentData()
552
+ target_doc = None
553
+
554
+ if sel is None: # "Select View"
555
+ # Fallback: active doc
556
+ doc = getattr(self.main, "_active_doc", None)
557
+ if callable(doc):
558
+ doc = doc()
559
+ target_doc = self._resolve_doc_object(doc)
560
+ elif sel == "file":
561
+ # Starless came from a file; no view to overwrite → fallback to active
562
+ doc = getattr(self.main, "_active_doc", None)
563
+ if callable(doc):
564
+ doc = doc()
565
+ target_doc = self._resolve_doc_object(doc)
566
+ else:
567
+ # A real view/doc was chosen
568
+ target_doc = self._resolve_doc_object(sel)
569
+
570
+ if target_doc is None:
571
+ QMessageBox.warning(self, "No Target",
572
+ "Pick a starless view to overwrite (or activate a destination window).")
573
+ return
574
+
575
+ # Emit (target_doc, blended_image)
576
+ self.stars_added.emit(target_doc, self.blended_image.astype(np.float32, copy=False))
577
+ self.accept()
578
+
579
+
580
+ # Ensure initial fit once shown
581
+ def showEvent(self, ev):
582
+ super().showEvent(ev)
583
+ # repopulate in case windows opened after dialog construction
584
+ self._populate_doc_combos()
585
+ if not self._fit_once:
586
+ self._fit_once = True
587
+ QTimer.singleShot(0, self.fit_to_preview)
588
+
589
+
590
+ # ──────────────────────────────────────────────────────────────────────────────
591
+ # Public entry point: open dialog, then apply to active doc
592
+ # ──────────────────────────────────────────────────────────────────────────────
593
+ def add_stars(main):
594
+ doc = getattr(main, "_active_doc", None)
595
+ if callable(doc):
596
+ doc = doc()
597
+ if doc is None or getattr(doc, "image", None) is None:
598
+ QMessageBox.warning(main, "No Image", "Please activate a destination image window first.")
599
+ return
600
+
601
+ dlg = AddStarsDialog(main, parent=main)
602
+ dlg.stars_added.connect(lambda target, arr: _apply_to_doc(main, target, arr))
603
+ dlg.exec()
604
+
605
+
606
+ def _apply_to_doc(main, doc, arr: np.ndarray):
607
+ """Overwrite the given document with the blended (stars added) result."""
608
+ if doc is None:
609
+ QMessageBox.warning(main, "No Target Document", "No document to apply to.")
610
+ return
611
+ try:
612
+ meta = {
613
+ "step_name": "Stars Added",
614
+ "bit_depth": "32-bit floating point",
615
+ "is_mono": (arr.ndim == 2),
616
+ }
617
+ doc.apply_edit(arr.astype(np.float32, copy=False), metadata=meta, step_name="Stars Added")
618
+ if hasattr(main, "_log"):
619
+ main._log("Stars Added")
620
+ except Exception as e:
621
+ QMessageBox.critical(main, "Add Stars", f"Failed to apply result:\n{e}")