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,230 @@
1
+ from __future__ import annotations
2
+ import numpy as np
3
+ from PyQt6.QtWidgets import QDialog, QFormLayout, QDialogButtonBox, QSpinBox, QDoubleSpinBox, QMessageBox
4
+ from .wavescalede import compute_wavescale_dse
5
+
6
+ # ─────────────────────────────────────────────────────────────────────────────
7
+ # Preset editor
8
+ # ─────────────────────────────────────────────────────────────────────────────
9
+ class WaveScaleDSEPresetDialog(QDialog):
10
+ def __init__(self, parent=None, initial: dict | None = None):
11
+ super().__init__(parent)
12
+ self.setWindowTitle(self.tr("WaveScale Dark Enhancer — Preset"))
13
+ p = dict(initial or {})
14
+ f = QFormLayout(self)
15
+
16
+ self.n_scales = QSpinBox()
17
+ self.n_scales.setRange(2, 10)
18
+ self.n_scales.setValue(int(p.get("n_scales", 6)))
19
+
20
+ self.boost = QDoubleSpinBox()
21
+ self.boost.setRange(0.10, 10.00)
22
+ self.boost.setDecimals(2)
23
+ self.boost.setSingleStep(0.05)
24
+ self.boost.setValue(float(p.get("boost_factor", 5.0)))
25
+
26
+ self.gamma = QDoubleSpinBox()
27
+ self.gamma.setRange(0.10, 10.00)
28
+ self.gamma.setDecimals(2)
29
+ self.gamma.setSingleStep(0.10)
30
+ self.gamma.setValue(float(p.get("mask_gamma", 1.0)))
31
+
32
+ self.iters = QSpinBox()
33
+ self.iters.setRange(1, 10)
34
+ self.iters.setValue(int(p.get("iterations", 2)))
35
+
36
+ f.addRow(self.tr("Number of Scales:"), self.n_scales)
37
+ f.addRow(self.tr("Boost Factor:"), self.boost)
38
+ f.addRow(self.tr("Mask Gamma:"), self.gamma)
39
+ f.addRow(self.tr("Iterations:"), self.iters)
40
+
41
+ btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self)
42
+ btns.accepted.connect(self.accept); btns.rejected.connect(self.reject)
43
+ f.addRow(btns)
44
+
45
+ def result_dict(self) -> dict:
46
+ return {
47
+ "n_scales": int(self.n_scales.value()),
48
+ "boost_factor": float(self.boost.value()),
49
+ "mask_gamma": float(self.gamma.value()),
50
+ "iterations": int(self.iters.value()),
51
+ }
52
+
53
+
54
+
55
+
56
+ # ─────────────────────────────────────────────────────────────────────────────
57
+ # Headless runner (exactly like UI: compute → blend by active mask → apply)
58
+ # ─────────────────────────────────────────────────────────────────────────────
59
+ def run_wavescalede_via_preset(main, preset: dict | None = None, target_doc=None):
60
+ import numpy as np
61
+ from PyQt6.QtWidgets import QMessageBox
62
+
63
+ p = dict(preset or {})
64
+
65
+ # --- sanitize to UI limits so replay is clean ---
66
+ try:
67
+ n_scales = int(np.clip(p.get("n_scales", 6), 2, 10))
68
+ boost = float(np.clip(p.get("boost_factor", 5.0), 0.10, 10.00))
69
+ mgamma = float(np.clip(p.get("mask_gamma", 1.0), 0.10, 10.00))
70
+ iters = int(np.clip(p.get("iterations", 2), 1, 10))
71
+ except Exception:
72
+ n_scales = int(p.get("n_scales", 6))
73
+ boost = float(p.get("boost_factor", 5.0))
74
+ mgamma = float(p.get("mask_gamma", 1.0))
75
+ iters = int(p.get("iterations", 2))
76
+
77
+ params = {
78
+ "n_scales": n_scales,
79
+ "boost_factor": boost,
80
+ "mask_gamma": mgamma,
81
+ "iterations": iters,
82
+ }
83
+
84
+ # --- store for Replay (prefer unified helper) ---
85
+ try:
86
+ remember = getattr(main, "remember_last_headless_command", None)
87
+ if remember is None:
88
+ remember = getattr(main, "_remember_last_headless_command", None)
89
+ if callable(remember):
90
+ remember("wavescale_dark_enhance", params, description="WaveScale Dark Enhance")
91
+ else:
92
+ setattr(main, "_last_headless_command", {
93
+ "command_id": "wavescale_dark_enhance",
94
+ "preset": dict(params),
95
+ })
96
+ except Exception:
97
+ pass
98
+
99
+ # resolve target doc
100
+ from setiastro.saspro.headless_utils import normalize_headless_main, unwrap_docproxy
101
+
102
+ main, doc, _dm = normalize_headless_main(main, target_doc)
103
+ if doc is None or getattr(doc, "image", None) is None:
104
+ from PyQt6.QtCore import QCoreApplication
105
+ QMessageBox.warning(main or None, QCoreApplication.translate("WaveScaleDSEPresetDialog", "WaveScale Dark Enhancer"), QCoreApplication.translate("WaveScaleDSEPresetDialog", "Load an image first."))
106
+ return
107
+
108
+ # pull & normalize image like the dialog
109
+ base = np.asarray(doc.image, dtype=np.float32)
110
+ was_mono = False
111
+ mono_shape = None
112
+ if base.ndim == 2:
113
+ was_mono = True
114
+ mono_shape = base.shape
115
+ img = np.repeat(base[:, :, None], 3, axis=2)
116
+ elif base.ndim == 3 and base.shape[2] == 1:
117
+ was_mono = True
118
+ mono_shape = base.shape
119
+ img = np.repeat(base, 3, axis=2)
120
+ else:
121
+ img = base[:, :, :3]
122
+
123
+ if base.dtype.kind in "ui":
124
+ mx = float(np.nanmax(img)) or 1.0
125
+ img = img / max(1.0, mx)
126
+ img = np.clip(img, 0.0, 1.0).astype(np.float32, copy=False)
127
+
128
+ # fetch active doc mask → 2D [0..1], resized to image (same logic as dialog)
129
+ doc_mask = _get_doc_active_mask_2d(doc, target_hw=img.shape[:2])
130
+
131
+ # compute (limit enhancement with external mask, same as UI preview)
132
+ out, _mask_used = compute_wavescale_dse(
133
+ img,
134
+ n_scales=n_scales,
135
+ boost_factor=boost,
136
+ mask_gamma=mgamma,
137
+ iterations=iters,
138
+ external_mask=doc_mask
139
+ )
140
+
141
+ # if a doc mask exists, blend final result with original (exactly like dialog)
142
+ if doc_mask is not None:
143
+ m3 = np.repeat(doc_mask[:, :, None], 3, axis=2).astype(np.float32)
144
+ blended = img * (1.0 - m3) + out * m3
145
+ else:
146
+ blended = out
147
+
148
+ # collapse back to mono if needed (like dialog apply)
149
+ result = blended
150
+ if was_mono:
151
+ mono = np.mean(result, axis=2, dtype=np.float32)
152
+ if mono_shape and len(mono_shape) == 3 and mono_shape[2] == 1:
153
+ mono = mono[:, :, None]
154
+ result = mono
155
+
156
+ result = np.clip(result, 0.0, 1.0).astype(np.float32, copy=False)
157
+
158
+ # apply to document (undoable + metadata)
159
+ meta = {
160
+ "step_name": "WaveScale Dark Enhance",
161
+ "wavescale_dark_enhance": dict(params),
162
+ "masked": bool(doc_mask is not None),
163
+ "mask_blend": "m*out + (1-m)*src" if doc_mask is not None else "none",
164
+ "bit_depth": "32-bit floating point",
165
+ "is_mono": (result.ndim == 2 or (result.ndim == 3 and result.shape[2] == 1)),
166
+ }
167
+
168
+ try:
169
+ if hasattr(doc, "apply_edit"):
170
+ doc.apply_edit(result, step_name="WaveScale Dark Enhance", metadata=meta)
171
+ elif hasattr(doc, "set_image"):
172
+ doc.set_image(result, step_name="WaveScale Dark Enhance")
173
+ elif hasattr(doc, "apply_numpy"):
174
+ doc.apply_numpy(result, step_name="WaveScale Dark Enhance")
175
+ else:
176
+ doc.image = result
177
+ except Exception as e:
178
+ from PyQt6.QtCore import QCoreApplication
179
+ QMessageBox.critical(main, QCoreApplication.translate("WaveScaleDSEPresetDialog", "WaveScale Dark Enhancer"), QCoreApplication.translate("WaveScaleDSEPresetDialog", "Failed to write to document:\n{0}").format(e))
180
+ return
181
+
182
+
183
+ # ─────────────────────────────────────────────────────────────────────────────
184
+ # Helpers (mirror dialog’s mask resolution)
185
+ # ─────────────────────────────────────────────────────────────────────────────
186
+ def _get_doc_active_mask_2d(doc, *, target_hw):
187
+ """
188
+ Return the document's active mask as 2-D float32 [0..1], resized to target_hw.
189
+ """
190
+ mid = getattr(doc, "active_mask_id", None)
191
+ if not mid:
192
+ return None
193
+ masks = getattr(doc, "masks", {}) or {}
194
+ layer = masks.get(mid)
195
+ if layer is None:
196
+ return None
197
+
198
+ data = None
199
+ for attr in ("data", "mask", "image", "array"):
200
+ if hasattr(layer, attr):
201
+ val = getattr(layer, attr)
202
+ if val is not None:
203
+ data = val
204
+ break
205
+ if data is None and isinstance(layer, dict):
206
+ for key in ("data", "mask", "image", "array"):
207
+ if key in layer and layer[key] is not None:
208
+ data = layer[key]
209
+ break
210
+ if data is None and isinstance(layer, np.ndarray):
211
+ data = layer
212
+ if data is None:
213
+ return None
214
+
215
+ m = np.asarray(data)
216
+ if m.ndim == 3:
217
+ m = m.mean(axis=2)
218
+
219
+ m = m.astype(np.float32, copy=False)
220
+ mx = float(m.max()) if m.size else 1.0
221
+ if mx > 1.0:
222
+ m /= mx
223
+ m = np.clip(m, 0.0, 1.0)
224
+
225
+ H, W = target_hw
226
+ if m.shape != (H, W):
227
+ yi = (np.linspace(0, m.shape[0] - 1, H)).astype(np.int32)
228
+ xi = (np.linspace(0, m.shape[1] - 1, W)).astype(np.int32)
229
+ m = m[yi][:, xi]
230
+ return m
@@ -0,0 +1,374 @@
1
+ # pro/wcs_update.py
2
+ from __future__ import annotations
3
+ import numpy as np
4
+
5
+
6
+ def _get_header_from_meta(meta: dict):
7
+ return (
8
+ meta.get("original_header")
9
+ or meta.get("fits_header")
10
+ or meta.get("header")
11
+ )
12
+
13
+
14
+ def _has_sip(header) -> bool:
15
+ """
16
+ Return True if the header carries any TAN-SIP style distortion keywords.
17
+ """
18
+ try:
19
+ return any(k in header for k in ("A_ORDER", "B_ORDER", "AP_ORDER", "BP_ORDER"))
20
+ except Exception:
21
+ return False
22
+
23
+
24
+ def _strip_wcs_keys(hdr):
25
+ """
26
+ Remove all WCS-related cards from a header so we can re-write them cleanly.
27
+ """
28
+ wcs_prefixes = (
29
+ "CTYPE", "CUNIT", "CDELT", "CRPIX", "CRVAL", "PC", "CD",
30
+ "PV", "PS", "LONPOLE", "LATPOLE", "PROJP", "RADESYS", "EQUINOX",
31
+ "A_", "B_", "AP_", "BP_",
32
+ "WCSAXES"
33
+ )
34
+ keys = list(hdr.keys())
35
+ for k in keys:
36
+ up = str(k).upper()
37
+ if any(up.startswith(p) for p in wcs_prefixes):
38
+ del hdr[k]
39
+
40
+
41
+ def _pixscale_rot_from_wcs(w):
42
+ """
43
+ Return (scale_x, scale_y) arcsec/pixel and rotation angle (deg).
44
+ Works for CD or PC+CDELT. Assumes celestial 2D.
45
+ """
46
+ import numpy as _np
47
+ # build CD matrix
48
+ if w.wcs.has_cd():
49
+ CD = _np.array(w.wcs.cd)
50
+ else:
51
+ # CDELT + PC
52
+ CDELT = _np.array(w.wcs.cdelt)
53
+ PC = _np.array(w.wcs.pc) if w.wcs.pc is not None else _np.eye(2)
54
+ CD = PC @ _np.diag(CDELT)
55
+ # scales are sqrt of column norms; convert deg/pix -> arcsec/pix
56
+ sx = float(np.hypot(CD[0, 0], CD[1, 0])) * 3600.0
57
+ sy = float(np.hypot(CD[0, 1], CD[1, 1])) * 3600.0
58
+ # rotation is atan2 of -CD10, CD00 (TAN convention; aligns with "east-left" images)
59
+ theta = float(np.degrees(np.arctan2(-CD[1, 0], CD[0, 0])))
60
+ return sx, sy, theta
61
+
62
+
63
+ def _needs_2d_coercion(hdr) -> bool:
64
+ """
65
+ True if the header is effectively 3-D (NAXIS>2 or WCSAXES>2).
66
+ """
67
+ try:
68
+ naxis = int(hdr.get("NAXIS", 2))
69
+ except Exception:
70
+ naxis = 2
71
+ try:
72
+ wcsaxes = int(hdr.get("WCSAXES", naxis))
73
+ except Exception:
74
+ wcsaxes = naxis
75
+ return (naxis > 2) or (wcsaxes > 2)
76
+
77
+
78
+ def _coerce_header_to_2d(hdr):
79
+ """
80
+ Make a 2-D view of a 3-D header so SIP/TAN WCS can be built.
81
+
82
+ We:
83
+ - set NAXIS=2 and WCSAXES=2
84
+ - drop axis-3 specific cards (CRPIX3, CTYPE3, CD3_*, PC3_*, etc.)
85
+ """
86
+ from astropy.io import fits
87
+
88
+ h2 = fits.Header()
89
+ # copy everything first
90
+ for k, v in hdr.items():
91
+ h2[k] = v
92
+
93
+ # set dimensionality to 2
94
+ h2["NAXIS"] = 2
95
+ h2["WCSAXES"] = 2
96
+
97
+ # kill axis-3 style cards
98
+ kill_prefixes = ("CRPIX3", "CRVAL3", "CDELT3", "CTYPE3", "CUNIT3")
99
+ to_del = []
100
+ for k in h2.keys():
101
+ uk = k.upper()
102
+ if uk in kill_prefixes:
103
+ to_del.append(k)
104
+ elif uk.startswith("CD3_") or uk.startswith("PC3_") or uk.startswith("PV3_") or uk.startswith("PS3_"):
105
+ to_del.append(k)
106
+ elif uk.endswith("3") and uk.startswith("A_"):
107
+ # very unlikely, but be safe
108
+ to_del.append(k)
109
+ elif uk.endswith("3") and uk.startswith("B_"):
110
+ to_del.append(k)
111
+ for k in to_del:
112
+ del h2[k]
113
+
114
+ return h2
115
+
116
+
117
+ def update_wcs_after_crop(metadata: dict, M_src_to_dst: np.ndarray, out_w: int, out_h: int) -> dict:
118
+ """
119
+ Refit a WCS (TAN or TAN-SIP) after crop given the src->dst homography.
120
+
121
+ This version also handles the “3-D + SIP” FITS case by coercing the header
122
+ down to 2 dimensions *before* calling astropy.wcs.WCS(...). That is exactly
123
+ the situation that produced:
124
+
125
+ FITS WCS distortion paper lookup tables and SIP distortions only work
126
+ in 2 dimensions...
127
+ """
128
+ debug = True
129
+
130
+ try:
131
+ from astropy.io import fits
132
+ from astropy.wcs import WCS
133
+ from astropy.wcs.utils import fit_wcs_from_points
134
+ from astropy.coordinates import SkyCoord
135
+ import astropy.units as u # noqa: F401
136
+ except Exception:
137
+ if debug:
138
+ print("[WCS-CROP] astropy not available; skipping WCS update.")
139
+ return metadata
140
+
141
+ hdr0 = _get_header_from_meta(metadata)
142
+ if hdr0 is None:
143
+ if debug:
144
+ print("[WCS-CROP] No header found in metadata; skipping.")
145
+ return metadata
146
+
147
+ # Normalize to fits.Header
148
+ if not isinstance(hdr0, fits.Header):
149
+ try:
150
+ tmp = fits.Header()
151
+ for k, v in dict(hdr0).items():
152
+ try:
153
+ tmp[k] = v
154
+ except Exception:
155
+ pass
156
+ hdr0 = tmp
157
+ except Exception:
158
+ if debug:
159
+ print("[WCS-CROP] Could not coerce header to fits.Header; skipping.")
160
+ return metadata
161
+
162
+ # ------------------------------------------------------------------
163
+ # 1) build the *old* WCS, but handle "3-D + SIP" first
164
+ # ------------------------------------------------------------------
165
+ hdr_for_wcs = hdr0
166
+ coerced = False
167
+
168
+ # If NAXIS>2 or WCSAXES>2, always coerce to a 2-D celestial view
169
+ if _needs_2d_coercion(hdr0):
170
+ hdr_for_wcs = _coerce_header_to_2d(hdr0)
171
+ coerced = True
172
+
173
+ try:
174
+ w_old = WCS(hdr_for_wcs, relax=True)
175
+ except Exception as e:
176
+ # If we *didn't* already coerce, try once more with a 2-D header
177
+ if not coerced:
178
+ try:
179
+ hdr_for_wcs = _coerce_header_to_2d(hdr0)
180
+ w_old = WCS(hdr_for_wcs, relax=True)
181
+ coerced = True
182
+ if debug:
183
+ print("[WCS-CROP] WCS() failed on original header; "
184
+ "succeeded after 2-D coercion.")
185
+ except Exception as e2:
186
+ if debug:
187
+ print(f"[WCS-CROP] WCS() failed even after 2-D coercion: {e2}; skipping.")
188
+ return metadata
189
+ else:
190
+ if debug:
191
+ print(f"[WCS-CROP] WCS() failed: {e}; skipping.")
192
+ return metadata
193
+
194
+ # ------------------------------------------------------------------
195
+ # Grab some "before" stats
196
+ # ------------------------------------------------------------------
197
+ try:
198
+ old_crval = (float(w_old.wcs.crval[0]), float(w_old.wcs.crval[1]))
199
+ old_crpix = (float(w_old.wcs.crpix[0]), float(w_old.wcs.crpix[1]))
200
+ except Exception:
201
+ old_crval = (np.nan, np.nan)
202
+ old_crpix = (np.nan, np.nan)
203
+ try:
204
+ old_sx, old_sy, old_rot = _pixscale_rot_from_wcs(w_old)
205
+ except Exception:
206
+ old_sx = old_sy = old_rot = float("nan")
207
+
208
+ # ------------------------------------------------------------------
209
+ # dst->src inverse homography
210
+ # ------------------------------------------------------------------
211
+ try:
212
+ M_dst_to_src = np.linalg.inv(M_src_to_dst)
213
+ except Exception as e:
214
+ if debug:
215
+ print(f"[WCS-CROP] inv(M) failed: {e}")
216
+ return metadata
217
+
218
+ # ------------------------------------------------------------------
219
+ # sample a grid across output
220
+ # ------------------------------------------------------------------
221
+ nx = min(25, max(5, out_w // max(1, out_w // 25)))
222
+ ny = min(25, max(5, out_h // max(1, out_h // 25)))
223
+ xs = np.linspace(0.5, out_w - 0.5, nx)
224
+ ys = np.linspace(0.5, out_h - 0.5, ny)
225
+ Xn, Yn = np.meshgrid(xs, ys) # shapes (ny, nx)
226
+ ones = np.ones_like(Xn)
227
+
228
+ # NEW->OLD via inverse homography
229
+ Xo_h = (M_dst_to_src[0, 0] * Xn + M_dst_to_src[0, 1] * Yn + M_dst_to_src[0, 2] * ones)
230
+ Yo_h = (M_dst_to_src[1, 0] * Xn + M_dst_to_src[1, 1] * Yn + M_dst_to_src[1, 2] * ones)
231
+ Wo_h = (M_dst_to_src[2, 0] * Xn + M_dst_to_src[2, 1] * Yn + M_dst_to_src[2, 2] * ones)
232
+ Xo = Xo_h / Wo_h
233
+ Yo = Yo_h / Wo_h
234
+
235
+ # ------------------------------------------------------------------
236
+ # Old WCS → sky coords
237
+ # ------------------------------------------------------------------
238
+ try:
239
+ sky = w_old.pixel_to_world(Xo, Yo) # SkyCoord
240
+ if not isinstance(sky, SkyCoord):
241
+ sky = SkyCoord(sky.ra, sky.dec)
242
+ except Exception:
243
+ # fall back to older API
244
+ radec = w_old.wcs_pix2world(np.column_stack([Xo.ravel(), Yo.ravel()]), 0)
245
+ sky = SkyCoord(
246
+ radec[:, 0].reshape(Xo.shape),
247
+ radec[:, 1].reshape(Yo.shape),
248
+ unit="deg"
249
+ )
250
+
251
+ # Flatten to 1-D for fitting
252
+ x_new = Xn.ravel()
253
+ y_new = Yn.ravel()
254
+ sky_flat = sky.reshape(x_new.shape)
255
+
256
+ # ------------------------------------------------------------------
257
+ # SIP degree choice
258
+ # ------------------------------------------------------------------
259
+ use_sip = _has_sip(hdr_for_wcs)
260
+ sip_degree = None
261
+ if use_sip:
262
+ try:
263
+ sip_degree = int(hdr_for_wcs.get("A_ORDER", hdr_for_wcs.get("AP_ORDER", 3)))
264
+ except Exception:
265
+ sip_degree = 3
266
+
267
+ # ------------------------------------------------------------------
268
+ # Fit NEW WCS
269
+ # ------------------------------------------------------------------
270
+ try:
271
+ w_new = fit_wcs_from_points(
272
+ (x_new, y_new),
273
+ sky_flat,
274
+ sip_degree=sip_degree,
275
+ projection='TAN',
276
+ proj_point='center'
277
+ )
278
+ w_new.array_shape = (out_h, out_w)
279
+ except Exception as e:
280
+ if debug:
281
+ print(f"[WCS-CROP] fit_wcs_from_points failed: {e}")
282
+ return metadata
283
+
284
+ # ------------------------------------------------------------------
285
+ # Residuals
286
+ # ------------------------------------------------------------------
287
+ try:
288
+ sky_fit = w_new.pixel_to_world(x_new, y_new)
289
+ sep = sky_flat.separation(sky_fit).arcsecond
290
+ rms_arcsec = float(np.sqrt(np.mean(sep ** 2)))
291
+ p50 = float(np.percentile(sep, 50))
292
+ p95 = float(np.percentile(sep, 95))
293
+ except Exception:
294
+ rms_arcsec = p50 = p95 = float("nan")
295
+
296
+ # ------------------------------------------------------------------
297
+ # After stats
298
+ # ------------------------------------------------------------------
299
+ try:
300
+ new_crval = (float(w_new.wcs.crval[0]), float(w_new.wcs.crval[1]))
301
+ new_crpix = (float(w_new.wcs.crpix[0]), float(w_new.wcs.crpix[1]))
302
+ except Exception:
303
+ new_crval = (np.nan, np.nan)
304
+ new_crpix = (np.nan, np.nan)
305
+ try:
306
+ new_sx, new_sy, new_rot = _pixscale_rot_from_wcs(w_new)
307
+ except Exception:
308
+ new_sx = new_sy = new_rot = float("nan")
309
+
310
+ # ------------------------------------------------------------------
311
+ # Build new header
312
+ # ------------------------------------------------------------------
313
+ new_hdr = hdr0.copy() # start from the original metadata header
314
+ _strip_wcs_keys(new_hdr)
315
+ wcards = w_new.to_header(relax=True)
316
+ for k, v in wcards.items():
317
+ try:
318
+ new_hdr[k] = v
319
+ except Exception:
320
+ pass
321
+ new_hdr["NAXIS"] = 2
322
+ new_hdr["NAXIS1"] = int(out_w)
323
+ new_hdr["NAXIS2"] = int(out_h)
324
+
325
+ # ------------------------------------------------------------------
326
+ # Debug print
327
+ # ------------------------------------------------------------------
328
+ if debug:
329
+ print("[WCS] === BEFORE ===")
330
+ print(f" CRVAL (deg): {old_crval}")
331
+ print(f" CRPIX (pix): {old_crpix}")
332
+ print(f" SCALE (as/px): ({old_sx:.3f}, {old_sy:.3f}) ROT(deg): {old_rot:.3f}")
333
+ print("[WCS] === AFTER ===")
334
+ print(f" CRVAL (deg): {new_crval}")
335
+ print(f" CRPIX (pix): {new_crpix} (image is {out_w}x{out_h})")
336
+ print(f" SCALE (as/px): ({new_sx:.3f}, {new_sy:.3f}) ROT(deg): {new_rot:.3f}")
337
+ print(f" SIP degree: {sip_degree if use_sip else 'None (pure TAN)'}")
338
+ print(f" Fit residuals (arcsec): RMS={rms_arcsec:.3f} p50={p50:.3f} p95={p95:.3f}")
339
+
340
+ # ------------------------------------------------------------------
341
+ # Stash a structured summary for the UI
342
+ # ------------------------------------------------------------------
343
+ debug_summary = {
344
+ "before": {
345
+ "crval_deg": old_crval,
346
+ "crpix_pix": old_crpix,
347
+ "scale_as_per_pix": (old_sx, old_sy),
348
+ "rot_deg": old_rot,
349
+ },
350
+ "after": {
351
+ "crval_deg": new_crval,
352
+ "crpix_pix": new_crpix,
353
+ "scale_as_per_pix": (new_sx, new_sy),
354
+ "rot_deg": new_rot,
355
+ "sip_degree": (sip_degree if use_sip else None),
356
+ "size": (int(out_w), int(out_h)),
357
+ },
358
+ "fit": {
359
+ "rms_arcsec": rms_arcsec,
360
+ "p50_arcsec": p50,
361
+ "p95_arcsec": p95,
362
+ "grid": (int(nx), int(ny)),
363
+ },
364
+ "coerced_to_2d": bool(coerced),
365
+ }
366
+
367
+ out_meta = dict(metadata)
368
+ out_meta["original_header"] = new_hdr
369
+ try:
370
+ out_meta["wcs"] = w_new
371
+ except Exception:
372
+ pass
373
+ out_meta["__wcs_debug__"] = debug_summary
374
+ return out_meta