setiastrosuitepro 1.6.0__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 (174) hide show
  1. setiastro/__init__.py +2 -0
  2. setiastro/saspro/__init__.py +20 -0
  3. setiastro/saspro/__main__.py +784 -0
  4. setiastro/saspro/_generated/__init__.py +7 -0
  5. setiastro/saspro/_generated/build_info.py +2 -0
  6. setiastro/saspro/abe.py +1295 -0
  7. setiastro/saspro/abe_preset.py +196 -0
  8. setiastro/saspro/aberration_ai.py +694 -0
  9. setiastro/saspro/aberration_ai_preset.py +224 -0
  10. setiastro/saspro/accel_installer.py +218 -0
  11. setiastro/saspro/accel_workers.py +30 -0
  12. setiastro/saspro/add_stars.py +621 -0
  13. setiastro/saspro/astrobin_exporter.py +1007 -0
  14. setiastro/saspro/astrospike.py +153 -0
  15. setiastro/saspro/astrospike_python.py +1839 -0
  16. setiastro/saspro/autostretch.py +196 -0
  17. setiastro/saspro/backgroundneutral.py +560 -0
  18. setiastro/saspro/batch_convert.py +325 -0
  19. setiastro/saspro/batch_renamer.py +519 -0
  20. setiastro/saspro/blemish_blaster.py +488 -0
  21. setiastro/saspro/blink_comparator_pro.py +2923 -0
  22. setiastro/saspro/bundles.py +61 -0
  23. setiastro/saspro/bundles_dock.py +114 -0
  24. setiastro/saspro/cheat_sheet.py +168 -0
  25. setiastro/saspro/clahe.py +342 -0
  26. setiastro/saspro/comet_stacking.py +1377 -0
  27. setiastro/saspro/config.py +38 -0
  28. setiastro/saspro/config_bootstrap.py +40 -0
  29. setiastro/saspro/config_manager.py +316 -0
  30. setiastro/saspro/continuum_subtract.py +1617 -0
  31. setiastro/saspro/convo.py +1397 -0
  32. setiastro/saspro/convo_preset.py +414 -0
  33. setiastro/saspro/copyastro.py +187 -0
  34. setiastro/saspro/cosmicclarity.py +1564 -0
  35. setiastro/saspro/cosmicclarity_preset.py +407 -0
  36. setiastro/saspro/crop_dialog_pro.py +948 -0
  37. setiastro/saspro/crop_preset.py +189 -0
  38. setiastro/saspro/curve_editor_pro.py +2544 -0
  39. setiastro/saspro/curves_preset.py +375 -0
  40. setiastro/saspro/debayer.py +670 -0
  41. setiastro/saspro/debug_utils.py +29 -0
  42. setiastro/saspro/dnd_mime.py +35 -0
  43. setiastro/saspro/doc_manager.py +2634 -0
  44. setiastro/saspro/exoplanet_detector.py +2166 -0
  45. setiastro/saspro/file_utils.py +284 -0
  46. setiastro/saspro/fitsmodifier.py +744 -0
  47. setiastro/saspro/free_torch_memory.py +48 -0
  48. setiastro/saspro/frequency_separation.py +1343 -0
  49. setiastro/saspro/function_bundle.py +1594 -0
  50. setiastro/saspro/ghs_dialog_pro.py +660 -0
  51. setiastro/saspro/ghs_preset.py +284 -0
  52. setiastro/saspro/graxpert.py +634 -0
  53. setiastro/saspro/graxpert_preset.py +287 -0
  54. setiastro/saspro/gui/__init__.py +0 -0
  55. setiastro/saspro/gui/main_window.py +8494 -0
  56. setiastro/saspro/gui/mixins/__init__.py +33 -0
  57. setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
  58. setiastro/saspro/gui/mixins/file_mixin.py +445 -0
  59. setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
  60. setiastro/saspro/gui/mixins/header_mixin.py +441 -0
  61. setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
  62. setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
  63. setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
  64. setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
  65. setiastro/saspro/gui/mixins/update_mixin.py +309 -0
  66. setiastro/saspro/gui/mixins/view_mixin.py +435 -0
  67. setiastro/saspro/halobgon.py +462 -0
  68. setiastro/saspro/header_viewer.py +445 -0
  69. setiastro/saspro/headless_utils.py +88 -0
  70. setiastro/saspro/histogram.py +753 -0
  71. setiastro/saspro/history_explorer.py +939 -0
  72. setiastro/saspro/image_combine.py +414 -0
  73. setiastro/saspro/image_peeker_pro.py +1596 -0
  74. setiastro/saspro/imageops/__init__.py +37 -0
  75. setiastro/saspro/imageops/mdi_snap.py +292 -0
  76. setiastro/saspro/imageops/scnr.py +36 -0
  77. setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
  78. setiastro/saspro/imageops/stretch.py +244 -0
  79. setiastro/saspro/isophote.py +1179 -0
  80. setiastro/saspro/layers.py +208 -0
  81. setiastro/saspro/layers_dock.py +714 -0
  82. setiastro/saspro/lazy_imports.py +193 -0
  83. setiastro/saspro/legacy/__init__.py +2 -0
  84. setiastro/saspro/legacy/image_manager.py +2226 -0
  85. setiastro/saspro/legacy/numba_utils.py +3659 -0
  86. setiastro/saspro/legacy/xisf.py +1071 -0
  87. setiastro/saspro/linear_fit.py +534 -0
  88. setiastro/saspro/live_stacking.py +1830 -0
  89. setiastro/saspro/log_bus.py +5 -0
  90. setiastro/saspro/logging_config.py +460 -0
  91. setiastro/saspro/luminancerecombine.py +309 -0
  92. setiastro/saspro/main_helpers.py +201 -0
  93. setiastro/saspro/mask_creation.py +928 -0
  94. setiastro/saspro/masks_core.py +56 -0
  95. setiastro/saspro/mdi_widgets.py +353 -0
  96. setiastro/saspro/memory_utils.py +666 -0
  97. setiastro/saspro/metadata_patcher.py +75 -0
  98. setiastro/saspro/mfdeconv.py +3826 -0
  99. setiastro/saspro/mfdeconv_earlystop.py +71 -0
  100. setiastro/saspro/mfdeconvcudnn.py +3263 -0
  101. setiastro/saspro/mfdeconvsport.py +2382 -0
  102. setiastro/saspro/minorbodycatalog.py +567 -0
  103. setiastro/saspro/morphology.py +382 -0
  104. setiastro/saspro/multiscale_decomp.py +1290 -0
  105. setiastro/saspro/nbtorgb_stars.py +531 -0
  106. setiastro/saspro/numba_utils.py +3044 -0
  107. setiastro/saspro/numba_warmup.py +141 -0
  108. setiastro/saspro/ops/__init__.py +9 -0
  109. setiastro/saspro/ops/command_help_dialog.py +623 -0
  110. setiastro/saspro/ops/command_runner.py +217 -0
  111. setiastro/saspro/ops/commands.py +1594 -0
  112. setiastro/saspro/ops/script_editor.py +1102 -0
  113. setiastro/saspro/ops/scripts.py +1413 -0
  114. setiastro/saspro/ops/settings.py +560 -0
  115. setiastro/saspro/parallel_utils.py +554 -0
  116. setiastro/saspro/pedestal.py +121 -0
  117. setiastro/saspro/perfect_palette_picker.py +1053 -0
  118. setiastro/saspro/pipeline.py +110 -0
  119. setiastro/saspro/pixelmath.py +1600 -0
  120. setiastro/saspro/plate_solver.py +2435 -0
  121. setiastro/saspro/project_io.py +797 -0
  122. setiastro/saspro/psf_utils.py +136 -0
  123. setiastro/saspro/psf_viewer.py +549 -0
  124. setiastro/saspro/pyi_rthook_astroquery.py +95 -0
  125. setiastro/saspro/remove_green.py +314 -0
  126. setiastro/saspro/remove_stars.py +1625 -0
  127. setiastro/saspro/remove_stars_preset.py +404 -0
  128. setiastro/saspro/resources.py +472 -0
  129. setiastro/saspro/rgb_combination.py +207 -0
  130. setiastro/saspro/rgb_extract.py +19 -0
  131. setiastro/saspro/rgbalign.py +723 -0
  132. setiastro/saspro/runtime_imports.py +7 -0
  133. setiastro/saspro/runtime_torch.py +754 -0
  134. setiastro/saspro/save_options.py +72 -0
  135. setiastro/saspro/selective_color.py +1552 -0
  136. setiastro/saspro/sfcc.py +1425 -0
  137. setiastro/saspro/shortcuts.py +2807 -0
  138. setiastro/saspro/signature_insert.py +1099 -0
  139. setiastro/saspro/stacking_suite.py +17712 -0
  140. setiastro/saspro/star_alignment.py +7420 -0
  141. setiastro/saspro/star_alignment_preset.py +329 -0
  142. setiastro/saspro/star_metrics.py +49 -0
  143. setiastro/saspro/star_spikes.py +681 -0
  144. setiastro/saspro/star_stretch.py +470 -0
  145. setiastro/saspro/stat_stretch.py +502 -0
  146. setiastro/saspro/status_log_dock.py +78 -0
  147. setiastro/saspro/subwindow.py +3267 -0
  148. setiastro/saspro/supernovaasteroidhunter.py +1712 -0
  149. setiastro/saspro/swap_manager.py +99 -0
  150. setiastro/saspro/torch_backend.py +89 -0
  151. setiastro/saspro/torch_rejection.py +434 -0
  152. setiastro/saspro/view_bundle.py +1555 -0
  153. setiastro/saspro/wavescale_hdr.py +624 -0
  154. setiastro/saspro/wavescale_hdr_preset.py +100 -0
  155. setiastro/saspro/wavescalede.py +657 -0
  156. setiastro/saspro/wavescalede_preset.py +228 -0
  157. setiastro/saspro/wcs_update.py +374 -0
  158. setiastro/saspro/whitebalance.py +456 -0
  159. setiastro/saspro/widgets/__init__.py +48 -0
  160. setiastro/saspro/widgets/common_utilities.py +305 -0
  161. setiastro/saspro/widgets/graphics_views.py +122 -0
  162. setiastro/saspro/widgets/image_utils.py +518 -0
  163. setiastro/saspro/widgets/preview_dialogs.py +280 -0
  164. setiastro/saspro/widgets/spinboxes.py +275 -0
  165. setiastro/saspro/widgets/themed_buttons.py +13 -0
  166. setiastro/saspro/widgets/wavelet_utils.py +299 -0
  167. setiastro/saspro/window_shelf.py +185 -0
  168. setiastro/saspro/xisf.py +1123 -0
  169. setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
  170. setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
  171. setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
  172. setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
  173. setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
  174. setiastrosuitepro-1.6.0.dist-info/licenses/license.txt +2580 -0
@@ -0,0 +1,414 @@
1
+ # pro/convo_preset.py
2
+ from __future__ import annotations
3
+ from typing import Optional, Tuple
4
+ import numpy as np
5
+
6
+ from PyQt6.QtWidgets import (
7
+ QDialog, QFormLayout, QDialogButtonBox, QVBoxLayout, QHBoxLayout,
8
+ QLabel, QComboBox, QCheckBox
9
+ )
10
+ from PyQt6.QtCore import Qt
11
+
12
+ # Reuse widgets/utilities from convo.py
13
+ from .convo import (
14
+ ConvoDeconvoDialog, FloatSliderWithEdit,
15
+ make_elliptical_gaussian_psf, van_cittert_deconv, larson_sekanina
16
+ )
17
+
18
+ # ---------------------------- Preset Editor Dialog ----------------------------
19
+ class ConvoPresetDialog(QDialog):
20
+ """
21
+ One dialog for all Convo/Deconvo presets (including TV).
22
+ Produces a JSON-safe dict you can stash on a shortcut.
23
+ """
24
+ def __init__(self, parent=None, initial: dict | None = None):
25
+ super().__init__(parent)
26
+ self.setWindowTitle("Convolution / Deconvolution — Preset")
27
+ p = dict(initial or {})
28
+ op = p.get("op", "convolution")
29
+
30
+ root = QVBoxLayout(self)
31
+
32
+ # --- top: operation selector ---
33
+ op_row = QHBoxLayout()
34
+ op_row.addWidget(QLabel("Operation:"))
35
+ self.op_combo = QComboBox()
36
+ self.op_combo.addItems(["convolution", "deconvolution", "tv"])
37
+ self.op_combo.setCurrentText(op if op in ("convolution", "deconvolution", "tv") else "convolution")
38
+ op_row.addWidget(self.op_combo); op_row.addStretch()
39
+ root.addLayout(op_row)
40
+
41
+ # --- stacked parameter forms (we'll toggle visibility) ---
42
+ self.form_conv = QFormLayout()
43
+ self.conv_radius = FloatSliderWithEdit(minimum=0.1, maximum=200.0, step=0.1, initial=float(p.get("radius", 5.0)), suffix=" px")
44
+ self.conv_kurtosis = FloatSliderWithEdit(minimum=0.1, maximum=10.0, step=0.1, initial=float(p.get("kurtosis", 2.0)), suffix="σ")
45
+ self.conv_aspect = FloatSliderWithEdit(minimum=0.1, maximum=10.0, step=0.1, initial=float(p.get("aspect", 1.0)))
46
+ self.conv_rotation = FloatSliderWithEdit(minimum=0.0, maximum=360.0, step=1.0, initial=float(p.get("rotation", 0.0)), suffix="°")
47
+ self.conv_strength = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.01, initial=float(p.get("strength", 1.0)))
48
+ self.form_conv.addRow("Radius:", self.conv_radius)
49
+ self.form_conv.addRow("Kurtosis (σ):", self.conv_kurtosis)
50
+ self.form_conv.addRow("Aspect Ratio:", self.conv_aspect)
51
+ self.form_conv.addRow("Rotation:", self.conv_rotation)
52
+ self.form_conv.addRow("Strength:", self.conv_strength)
53
+
54
+ self.form_deconv = QFormLayout()
55
+ self.deconv_algo = QComboBox()
56
+ self.deconv_algo.addItems(["Richardson-Lucy", "Wiener", "Larson-Sekanina", "Van Cittert"])
57
+ self.deconv_algo.setCurrentText(p.get("algo", "Richardson-Lucy"))
58
+ self.form_deconv.addRow("Algorithm:", self.deconv_algo)
59
+
60
+ # RL/Wiener PSF params
61
+ self.psf_radius = FloatSliderWithEdit(minimum=0.1, maximum=100.0, step=0.1, initial=float(p.get("psf_radius", 3.0)), suffix=" px")
62
+ self.psf_kurtosis = FloatSliderWithEdit(minimum=0.1, maximum=10.0, step=0.1, initial=float(p.get("psf_kurtosis", 2.0)), suffix="σ")
63
+ self.psf_aspect = FloatSliderWithEdit(minimum=0.1, maximum=10.0, step=0.1, initial=float(p.get("psf_aspect", 1.0)))
64
+ self.psf_rot = FloatSliderWithEdit(minimum=0.0, maximum=360.0, step=1.0, initial=float(p.get("psf_rotation", 0.0)), suffix="°")
65
+ self.form_deconv.addRow("PSF Radius:", self.psf_radius)
66
+ self.form_deconv.addRow("PSF Kurtosis:", self.psf_kurtosis)
67
+ self.form_deconv.addRow("PSF Aspect:", self.psf_aspect)
68
+ self.form_deconv.addRow("PSF Rotation:", self.psf_rot)
69
+
70
+ # RL options
71
+ self.rl_iter = FloatSliderWithEdit(minimum=1, maximum=200, step=1, initial=float(p.get("rl_iter", 30)))
72
+ self.rl_reg = QComboBox(); self.rl_reg.addItems(["None (Plain R–L)", "Tikhonov (L2)", "Total Variation (TV)"])
73
+ self.rl_reg.setCurrentText(p.get("rl_reg", "None (Plain R–L)"))
74
+ self.rl_clip = QCheckBox("De-ring (bilateral)"); self.rl_clip.setChecked(bool(p.get("rl_dering", True)))
75
+ self.rl_l_only = QCheckBox("L* only"); self.rl_l_only.setChecked(bool(p.get("luminance_only", True)))
76
+ self.form_deconv.addRow("RL Iterations:", self.rl_iter)
77
+ self.form_deconv.addRow("RL Regularization:", self.rl_reg)
78
+ self.form_deconv.addRow("", self.rl_clip)
79
+ self.form_deconv.addRow("", self.rl_l_only)
80
+
81
+ # Wiener options
82
+ self.wiener_nsr = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.001, initial=float(p.get("wiener_nsr", 0.01)))
83
+ self.wiener_reg = QComboBox(); self.wiener_reg.addItems(["None (Classical Wiener)", "Tikhonov (L2)"])
84
+ self.wiener_reg.setCurrentText(p.get("wiener_reg", "None (Classical Wiener)"))
85
+ self.wiener_dering= QCheckBox("De-ring pass"); self.wiener_dering.setChecked(bool(p.get("wiener_dering", True)))
86
+ self.form_deconv.addRow("Wiener NSR:", self.wiener_nsr)
87
+ self.form_deconv.addRow("Wiener Regularization:", self.wiener_reg)
88
+ self.form_deconv.addRow("", self.wiener_dering)
89
+
90
+ # Larson–Sekanina
91
+ self.ls_rstep = FloatSliderWithEdit(minimum=0.0, maximum=50.0, step=0.1, initial=float(p.get("ls_rstep", 0.0)), suffix=" px")
92
+ self.ls_astep = FloatSliderWithEdit(minimum=0.1, maximum=360.0, step=0.1, initial=float(p.get("ls_astep", 1.0)), suffix="°")
93
+ self.ls_operator = QComboBox(); self.ls_operator.addItems(["Divide", "Subtract"]); self.ls_operator.setCurrentText(p.get("ls_operator", "Divide"))
94
+ self.ls_blend = QComboBox(); self.ls_blend.addItems(["SoftLight", "Screen"]); self.ls_blend.setCurrentText(p.get("ls_blend", "SoftLight"))
95
+ self.form_deconv.addRow("LS Radial Step:", self.ls_rstep)
96
+ self.form_deconv.addRow("LS Angular Step:", self.ls_astep)
97
+ self.form_deconv.addRow("LS Operator:", self.ls_operator)
98
+ self.form_deconv.addRow("Blend:", self.ls_blend)
99
+
100
+ # Van Cittert
101
+ self.vc_iter = FloatSliderWithEdit(minimum=1, maximum=1000, step=1, initial=float(p.get("vc_iter", 10)))
102
+ self.vc_relax = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.01, initial=float(p.get("vc_relax", 0.0)))
103
+ self.form_deconv.addRow("VC Iterations:", self.vc_iter)
104
+ self.form_deconv.addRow("VC Relaxation:", self.vc_relax)
105
+
106
+ # Strength (applies to all ops)
107
+ self.deconv_strength = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.01, initial=float(p.get("strength", 1.0)))
108
+ self.form_deconv.addRow("Strength:", self.deconv_strength)
109
+
110
+ # TV Denoise
111
+ self.form_tv = QFormLayout()
112
+ self.tv_weight = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.01, initial=float(p.get("tv_weight", 0.10)))
113
+ self.tv_iter = FloatSliderWithEdit(minimum=1, maximum=100, step=1, initial=float(p.get("tv_iter", 10)))
114
+ self.tv_multi = QCheckBox("Multi-channel"); self.tv_multi.setChecked(bool(p.get("tv_multichannel", True)))
115
+ self.tv_strength = FloatSliderWithEdit(minimum=0.0, maximum=1.0, step=0.01, initial=float(p.get("strength", 1.0)))
116
+ self.form_tv.addRow("TV Weight:", self.tv_weight)
117
+ self.form_tv.addRow("TV Iterations:", self.tv_iter)
118
+ self.form_tv.addRow("", self.tv_multi)
119
+ self.form_tv.addRow("Strength:", self.tv_strength)
120
+
121
+ # containers to show/hide
122
+ self.box_conv = _wrap_form(self.form_conv)
123
+ self.box_decv = _wrap_form(self.form_deconv)
124
+ self.box_tv = _wrap_form(self.form_tv)
125
+ root.addWidget(self.box_conv)
126
+ root.addWidget(self.box_decv)
127
+ root.addWidget(self.box_tv)
128
+
129
+ def _toggle():
130
+ v = self.op_combo.currentText()
131
+ self.box_conv.setVisible(v == "convolution")
132
+ self.box_decv.setVisible(v == "deconvolution")
133
+ self.box_tv.setVisible(v == "tv")
134
+ self.op_combo.currentTextChanged.connect(lambda _: _toggle())
135
+ _toggle()
136
+
137
+ # buttons
138
+ btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self)
139
+ btns.accepted.connect(self.accept); btns.rejected.connect(self.reject)
140
+ root.addWidget(btns)
141
+
142
+ def result_dict(self) -> dict:
143
+ op = self.op_combo.currentText()
144
+ if op == "convolution":
145
+ return {
146
+ "op": "convolution",
147
+ "radius": self.conv_radius.value(),
148
+ "kurtosis": self.conv_kurtosis.value(),
149
+ "aspect": self.conv_aspect.value(),
150
+ "rotation": self.conv_rotation.value(),
151
+ "strength": self.conv_strength.value(),
152
+ }
153
+ if op == "deconvolution":
154
+ return {
155
+ "op": "deconvolution",
156
+ "algo": self.deconv_algo.currentText(),
157
+ "psf_radius": self.psf_radius.value(),
158
+ "psf_kurtosis": self.psf_kurtosis.value(),
159
+ "psf_aspect": self.psf_aspect.value(),
160
+ "psf_rotation": self.psf_rot.value(),
161
+ "rl_iter": self.rl_iter.value(),
162
+ "rl_reg": self.rl_reg.currentText(),
163
+ "rl_dering": bool(self.rl_clip.isChecked()),
164
+ "luminance_only": bool(self.rl_l_only.isChecked()),
165
+ "wiener_nsr": self.wiener_nsr.value(),
166
+ "wiener_reg": self.wiener_reg.currentText(),
167
+ "wiener_dering": bool(self.wiener_dering.isChecked()),
168
+ "ls_rstep": self.ls_rstep.value(),
169
+ "ls_astep": self.ls_astep.value(),
170
+ "ls_operator": self.ls_operator.currentText(),
171
+ "ls_blend": self.ls_blend.currentText(),
172
+ "vc_iter": self.vc_iter.value(),
173
+ "vc_relax": self.vc_relax.value(),
174
+ "strength": self.deconv_strength.value(),
175
+ # optional center for LS (x,y) — if omitted we’ll use image center
176
+ # "center": [x, y],
177
+ }
178
+ # tv
179
+ return {
180
+ "op": "tv",
181
+ "tv_weight": self.tv_weight.value(),
182
+ "tv_iter": int(self.tv_iter.value()),
183
+ "tv_multichannel": bool(self.tv_multi.isChecked()),
184
+ "strength": self.tv_strength.value(),
185
+ }
186
+
187
+
188
+ def _wrap_form(form: QFormLayout):
189
+ from PyQt6.QtWidgets import QWidget, QVBoxLayout
190
+ w = QWidget(); l = QVBoxLayout(w); l.setContentsMargins(0,0,0,0); l.addLayout(form)
191
+ return w
192
+
193
+
194
+ # ---------------------------- Headless Apply ----------------------------
195
+ def apply_convo_via_preset(main_window, doc, preset: dict):
196
+ """
197
+ Headless executor for Convolution/Deconvolution/TV using the same kernels/flows
198
+ as the dialog. Applies result to `doc` via doc_manager.
199
+ """
200
+ import numpy as np
201
+ from skimage.color import rgb2lab, lab2rgb
202
+ from skimage.restoration import denoise_tv_chambolle
203
+
204
+ dm = getattr(main_window, "doc_manager", None) or getattr(main_window, "dm", None)
205
+ if dm is None or doc is None or getattr(doc, "image", None) is None:
206
+ return
207
+
208
+ # ⚠️ You can keep or drop this; it no longer matters for the apply step.
209
+ try:
210
+ if hasattr(dm, "set_active_document"):
211
+ dm.set_active_document(doc)
212
+ except Exception:
213
+ pass
214
+
215
+ img = np.asarray(doc.image).astype(np.float32, copy=False)
216
+ p = dict(preset or {})
217
+ op = p.get("op", "convolution")
218
+
219
+ # Create a dialog instance to reuse its helpers (no UI shown)
220
+ d = ConvoDeconvoDialog(doc_manager=dm, parent=main_window)
221
+
222
+ def _blend(a, b, s):
223
+ s = float(max(0.0, min(1.0, s)))
224
+ return np.clip(b * s + a * (1.0 - s), 0.0, 1.0).astype(np.float32)
225
+
226
+ if op == "convolution":
227
+ psf = make_elliptical_gaussian_psf(
228
+ float(p.get("radius", 5.0)),
229
+ float(p.get("kurtosis", 2.0)),
230
+ float(p.get("aspect", 1.0)),
231
+ float(p.get("rotation", 0.0)),
232
+ ).astype(np.float32)
233
+ out = d._convolve_color(img, psf)
234
+ out = _blend(img, out, float(p.get("strength", 1.0)))
235
+
236
+ elif op == "deconvolution":
237
+ algo = p.get("algo", "Richardson-Lucy")
238
+ if algo in ("Richardson-Lucy", "Wiener"):
239
+ psf = make_elliptical_gaussian_psf(
240
+ float(p.get("psf_radius", 3.0)),
241
+ float(p.get("psf_kurtosis", 2.0)),
242
+ float(p.get("psf_aspect", 1.0)),
243
+ float(p.get("psf_rotation", 0.0)),
244
+ ).astype(np.float32)
245
+
246
+ if algo == "Richardson-Lucy":
247
+ iters = int(round(float(p.get("rl_iter", 30))))
248
+ reg = p.get("rl_reg", "None (Plain R–L)")
249
+ clipf = bool(p.get("rl_dering", True))
250
+ lum_only = bool(p.get("luminance_only", True))
251
+ if lum_only and img.ndim == 3 and img.shape[2] == 3:
252
+ lab = rgb2lab(img); L = (lab[...,0] / 100.0).astype(np.float32)
253
+ Ld = d._richardson_lucy_color(L, psf, iterations=iters, reg_type=reg, clip_flag=clipf)
254
+ lab[...,0] = np.clip(Ld * 100.0, 0.0, 100.0)
255
+ tmp = lab2rgb(lab.astype(np.float32)).astype(np.float32)
256
+ out = np.clip(tmp, 0.0, 1.0)
257
+ else:
258
+ out = d._richardson_lucy_color(img, psf, iterations=iters, reg_type=reg, clip_flag=clipf)
259
+ out = _blend(img, out, float(p.get("strength", 1.0)))
260
+
261
+ elif algo == "Wiener":
262
+ nsr = float(p.get("wiener_nsr", 0.01))
263
+ reg = p.get("wiener_reg", "None (Classical Wiener)")
264
+ dering= bool(p.get("wiener_dering", True))
265
+ lum_only = bool(p.get("luminance_only", True))
266
+ if lum_only and img.ndim == 3 and img.shape[2] == 3:
267
+ lab = rgb2lab(img); L = (lab[...,0] / 100.0).astype(np.float32)
268
+ Ld = d._wiener_deconv_with_kernel(L, psf, nsr, reg, dering)
269
+ lab[...,0] = np.clip(Ld * 100.0, 0.0, 100.0)
270
+ tmp = lab2rgb(lab.astype(np.float32)).astype(np.float32)
271
+ out = np.clip(tmp, 0.0, 1.0)
272
+ else:
273
+ out = d._wiener_deconv_with_kernel(img, psf, nsr, reg, dering)
274
+ out = np.clip(out, 0.0, 1.0)
275
+ out = _blend(img, out, float(p.get("strength", 1.0)))
276
+
277
+
278
+ elif algo == "Larson-Sekanina":
279
+ H, W = img.shape[:2]
280
+ cxy = p.get("center", [W/2, H/2])
281
+ cx = float(cxy[0]); cy = float(cxy[1])
282
+
283
+ B = larson_sekanina(
284
+ image=img,
285
+ center=(cy, cx), # (y,x)
286
+ radial_step=float(p.get("ls_rstep", 0.0)),
287
+ angular_step_deg=float(p.get("ls_astep", 1.0)),
288
+ operator=p.get("ls_operator", "Divide")
289
+ )
290
+
291
+ A = img
292
+ if A.ndim == 3 and A.shape[2] == 3:
293
+ # ✅ FIX: repeat into channel axis
294
+ B_rgb = np.repeat(B[..., None], 3, axis=2)
295
+ A_rgb = A
296
+ else:
297
+ B_rgb = B[..., None]
298
+ A_rgb = A[..., None]
299
+
300
+ blend_mode = p.get("ls_blend", "SoftLight")
301
+ if blend_mode == "Screen":
302
+ C = (A_rgb + B_rgb - (A_rgb * B_rgb))
303
+ else: # SoftLight
304
+ C = (1 - 2 * B_rgb) * (A_rgb ** 2) + 2 * B_rgb * A_rgb
305
+
306
+ out = np.clip(C, 0.0, 1.0)
307
+ out = out[..., 0] if img.ndim == 2 else out
308
+ out = _blend(img, out, float(p.get("strength", 1.0)))
309
+
310
+ elif algo == "Van Cittert":
311
+ iters = int(round(float(p.get("vc_iter", 10))))
312
+ relax = float(p.get("vc_relax", 0.0))
313
+ if img.ndim == 3 and img.shape[2] == 3:
314
+ out = np.stack([van_cittert_deconv(img[...,c], iters, relax) for c in range(3)], axis=2).astype(np.float32)
315
+ else:
316
+ out = van_cittert_deconv(img, iters, relax).astype(np.float32)
317
+ out = np.clip(out, 0.0, 1.0)
318
+ out = _blend(img, out, float(p.get("strength", 1.0)))
319
+ else:
320
+ return # unknown algo
321
+
322
+ elif op == "tv":
323
+ from skimage.restoration import denoise_tv_chambolle
324
+ weight = float(p.get("tv_weight", 0.10))
325
+ max_iter = int(p.get("tv_iter", 10))
326
+ multich = bool(p.get("tv_multichannel", True))
327
+ if img.ndim == 3 and multich:
328
+ out = denoise_tv_chambolle(img.astype(np.float32), weight=weight, max_num_iter=max_iter, channel_axis=-1).astype(np.float32)
329
+ elif img.ndim == 3 and img.shape[2] == 3:
330
+ chans = [denoise_tv_chambolle(img[...,c].astype(np.float32), weight=weight, max_num_iter=max_iter, channel_axis=None) for c in range(3)]
331
+ out = np.stack(chans, axis=2).astype(np.float32)
332
+ else:
333
+ out = denoise_tv_chambolle(img.astype(np.float32), weight=weight, max_num_iter=max_iter, channel_axis=None).astype(np.float32)
334
+ out = _blend(img, np.clip(out, 0.0, 1.0), float(p.get("strength", 1.0)))
335
+
336
+ else:
337
+ return
338
+
339
+ meta = dict(getattr(doc, "metadata", {}) or {})
340
+ meta["source"] = "ConvoDeconvo"
341
+
342
+ try:
343
+ if hasattr(doc, "apply_edit"):
344
+ # Let Document handle full vs ROI, history, etc.
345
+ doc.apply_edit(
346
+ out.astype(np.float32, copy=False),
347
+ metadata=meta,
348
+ step_name="Convo/Deconvo (preset)",
349
+ )
350
+ else:
351
+ # Fallback for legacy paths
352
+ if hasattr(dm, "set_active_document"):
353
+ dm.set_active_document(doc)
354
+ dm.update_active_document(
355
+ out.astype(np.float32, copy=False),
356
+ metadata=meta,
357
+ step_name="Convo/Deconvo (preset)",
358
+ )
359
+ except Exception:
360
+ # Re-raise so replay_last_action_on_base can show the warning
361
+ raise
362
+
363
+ def run_convo_via_preset(main, doc_or_preset=None, preset: dict | None = None, *, target_doc=None):
364
+ """
365
+ Headless Convo/Deconvo/TV entrypoint for CommandSpec + Replay.
366
+
367
+ Supports BOTH call shapes:
368
+ 1) New CommandRunner shape:
369
+ run_convo_via_preset(main, target_doc, preset)
370
+ 2) Legacy shape:
371
+ run_convo_via_preset(main, preset_dict, target_doc=doc)
372
+ run_convo_via_preset(main, preset_dict)
373
+ """
374
+
375
+ from PyQt6.QtWidgets import QMessageBox
376
+
377
+ # ---- Interpret arguments for backward compat / new executor ----
378
+ if preset is None and isinstance(doc_or_preset, dict):
379
+ # Legacy: (main, preset_dict, target_doc=?)
380
+ p = dict(doc_or_preset or {})
381
+ doc = target_doc
382
+ else:
383
+ # New executor: (main, doc, preset_dict)
384
+ p = dict(preset or {})
385
+ doc = target_doc if target_doc is not None else doc_or_preset
386
+
387
+ # Resolve active doc if still None
388
+ if doc is None:
389
+ d = getattr(main, "_active_doc", None)
390
+ doc = d() if callable(d) else d
391
+
392
+ if doc is None or getattr(doc, "image", None) is None:
393
+ QMessageBox.warning(main, "Convolution / Deconvolution", "Load an image first.")
394
+ return
395
+
396
+ # ---- Record for Replay ----
397
+ try:
398
+ remember = getattr(main, "remember_last_headless_command", None)
399
+ if remember is None:
400
+ remember = getattr(main, "_remember_last_headless_command", None)
401
+
402
+ if callable(remember):
403
+ # IMPORTANT: store canonical id that exists in registry
404
+ remember("convo", p, description="Convolution / Deconvolution")
405
+ else:
406
+ setattr(main, "_last_headless_command", {
407
+ "command_id": "convo",
408
+ "preset": dict(p),
409
+ })
410
+ except Exception:
411
+ pass
412
+
413
+ apply_convo_via_preset(main, doc, p)
414
+
@@ -0,0 +1,187 @@
1
+ # pro/copyastro.py
2
+ # pro/m_header.py
3
+ from __future__ import annotations
4
+ from PyQt6.QtCore import Qt
5
+ from PyQt6.QtWidgets import (
6
+ QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox,
7
+ QPushButton, QMessageBox, QCheckBox, QMdiSubWindow
8
+ )
9
+
10
+ class CopyAstrometryDialog(QDialog):
11
+ """
12
+ Modeless picker that copies the WCS/SIP solution from a source doc
13
+ into the target doc (explicitly passed active view).
14
+ """
15
+ def __init__(self, parent=None, target=None):
16
+ super().__init__(parent)
17
+ self.setWindowTitle("Copy Astrometric Solution")
18
+ self.setMinimumWidth(420)
19
+
20
+ self._mw = parent
21
+ self._dm = getattr(parent, "doc_manager", None) or getattr(parent, "docman", None)
22
+
23
+ # --- resolve target doc from the passed-in active subwindow/view/doc
24
+ self._tgt = self._doc_from_target(target)
25
+ if self._tgt is None:
26
+ # fallback to active doc helpers, just in case
27
+ try:
28
+ self._tgt = self._dm.get_active_document() if self._dm else None
29
+ except Exception:
30
+ self._tgt = None
31
+ if self._tgt is None and hasattr(parent, "_active_doc"):
32
+ try:
33
+ self._tgt = parent._active_doc()
34
+ except Exception:
35
+ pass
36
+
37
+ lay = QVBoxLayout(self)
38
+
39
+ tgt_name = getattr(self._tgt, "display_name", lambda: None)() or "Active View"
40
+ lay.addWidget(QLabel(f"Target: <b>{tgt_name}</b>"))
41
+
42
+ lay.addWidget(QLabel("Choose a source image that already has a WCS/SIP solution:"))
43
+ self.combo = QComboBox(self)
44
+ lay.addWidget(self.combo)
45
+
46
+ self.chk_ignore_sip = QCheckBox("Ignore SIP terms (copy TAN only)")
47
+ self.chk_ignore_sip.setChecked(False)
48
+ lay.addWidget(self.chk_ignore_sip)
49
+
50
+ row = QHBoxLayout(); row.addStretch(1)
51
+ self.btn_copy = QPushButton("Copy")
52
+ self.btn_close = QPushButton("Close")
53
+ row.addWidget(self.btn_copy); row.addWidget(self.btn_close)
54
+ lay.addLayout(row)
55
+
56
+ self.btn_copy.clicked.connect(self._do_copy)
57
+ self.btn_close.clicked.connect(self.close)
58
+
59
+ self._candidates = [] # list[(doc, name, wcs_dict)]
60
+ self._load_sources()
61
+
62
+ # --- helpers --------------------------------------------------------
63
+ def _doc_from_target(self, target):
64
+ """Accept QMdiSubWindow, ImageSubWindow, or ImageDocument."""
65
+ try:
66
+ if target is None:
67
+ return None
68
+ # QMdiSubWindow → widget() → .document
69
+ if isinstance(target, QMdiSubWindow):
70
+ w = target.widget()
71
+ return getattr(w, "document", None)
72
+ # ImageSubWindow-like
73
+ if hasattr(target, "document"):
74
+ return getattr(target, "document", None)
75
+ # Already a document
76
+ if hasattr(target, "image") and hasattr(target, "metadata"):
77
+ return target
78
+ except Exception:
79
+ pass
80
+ return None
81
+
82
+ def _extract_wcs_dict_for(self, doc):
83
+ # Prefer the MW helper you already have (returns a flat dict of FITS cards)
84
+ if hasattr(self._mw, "_extract_wcs_dict"):
85
+ try:
86
+ d = self._mw._extract_wcs_dict(doc)
87
+ if d: return dict(d)
88
+ except Exception:
89
+ pass
90
+
91
+ # Fallback: read from original_header
92
+ meta = getattr(doc, "metadata", {}) or {}
93
+ hdr = meta.get("original_header") or {}
94
+ try:
95
+ keys = [str(k).upper() for k in getattr(hdr, "keys", lambda: hdr.keys())()]
96
+ if "CRVAL1" in keys and "CRVAL2" in keys:
97
+ return {k: hdr[k] for k in getattr(hdr, "keys", lambda: hdr.keys())()}
98
+ except Exception:
99
+ pass
100
+ return {}
101
+
102
+ def _load_sources(self):
103
+ self.combo.clear()
104
+ self._candidates.clear()
105
+
106
+ if not self._dm or not self._tgt:
107
+ self.combo.addItem("No target image.")
108
+ self.btn_copy.setEnabled(False)
109
+ return
110
+
111
+ try:
112
+ docs = self._dm.all_documents()
113
+ except Exception:
114
+ docs = []
115
+
116
+ found_any = False
117
+ for d in docs:
118
+ if d is self._tgt:
119
+ continue
120
+ w = self._extract_wcs_dict_for(d)
121
+ if not w:
122
+ continue
123
+ name = getattr(d, "display_name", lambda: None)() or (getattr(d, "metadata", {}).get("file_path") or "Untitled")
124
+ # hint text (RA/Dec)
125
+ ra, dec = w.get("CRVAL1"), w.get("CRVAL2")
126
+ hint = f" (RA={ra:.5f}, Dec={dec:.5f})" if isinstance(ra, (int, float)) and isinstance(dec, (int, float)) else ""
127
+ self.combo.addItem(name + hint)
128
+ self._candidates.append((d, name, w))
129
+ found_any = True
130
+
131
+ if not found_any:
132
+ self.combo.addItem("No other images with WCS found")
133
+ self.btn_copy.setEnabled(False)
134
+
135
+ # --- action ---------------------------------------------------------
136
+ def _do_copy(self):
137
+ if self._tgt is None:
138
+ QMessageBox.information(self, "Copy Astrometry", "No target image.")
139
+ return
140
+
141
+ idx = self.combo.currentIndex()
142
+ if idx < 0 or idx >= len(self._candidates):
143
+ return
144
+
145
+ _, src_name, wcs = self._candidates[idx]
146
+
147
+ # Optionally strip SIP → TAN only
148
+ if self.chk_ignore_sip.isChecked():
149
+ wcs = {
150
+ k: v for k, v in wcs.items()
151
+ if not str(k).upper().startswith(("A_", "B_", "AP_", "BP_"))
152
+ and str(k).upper() not in {"A_ORDER", "B_ORDER", "AP_ORDER", "BP_ORDER"}
153
+ }
154
+ # enforce TAN
155
+ c1 = str(wcs.get("CTYPE1", "RA---TAN"))
156
+ c2 = str(wcs.get("CTYPE2", "DEC--TAN"))
157
+ if c1.endswith("-SIP"): wcs["CTYPE1"] = "RA---TAN"
158
+ if c2.endswith("-SIP"): wcs["CTYPE2"] = "DEC--TAN"
159
+
160
+ ok = False
161
+ if hasattr(self._mw, "_apply_wcs_dict_to_doc"):
162
+ try:
163
+ ok = bool(self._mw._apply_wcs_dict_to_doc(self._tgt, dict(wcs)))
164
+ except Exception:
165
+ ok = False
166
+
167
+ if not ok:
168
+ QMessageBox.warning(self, "Copy Astrometry", "Failed to apply astrometric solution.")
169
+ return
170
+
171
+ # refresh header dock + listeners immediately
172
+ try:
173
+ if hasattr(self._mw, "_refresh_header_viewer"):
174
+ self._mw._refresh_header_viewer(self._tgt)
175
+ if hasattr(self._mw, "currentDocumentChanged"):
176
+ self._mw.currentDocumentChanged.emit(self._tgt)
177
+ except Exception:
178
+ pass
179
+
180
+ try:
181
+ tgt_name = getattr(self._tgt, "display_name", lambda: None)() or "Target"
182
+ QMessageBox.information(self, "Copy Astrometry",
183
+ f"Copied solution from “{src_name}” to “{tgt_name}”.")
184
+ except Exception:
185
+ pass
186
+
187
+ self.close()