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.
- setiastro/__init__.py +2 -0
- setiastro/saspro/__init__.py +20 -0
- setiastro/saspro/__main__.py +784 -0
- setiastro/saspro/_generated/__init__.py +7 -0
- setiastro/saspro/_generated/build_info.py +2 -0
- setiastro/saspro/abe.py +1295 -0
- setiastro/saspro/abe_preset.py +196 -0
- setiastro/saspro/aberration_ai.py +694 -0
- setiastro/saspro/aberration_ai_preset.py +224 -0
- setiastro/saspro/accel_installer.py +218 -0
- setiastro/saspro/accel_workers.py +30 -0
- setiastro/saspro/add_stars.py +621 -0
- setiastro/saspro/astrobin_exporter.py +1007 -0
- setiastro/saspro/astrospike.py +153 -0
- setiastro/saspro/astrospike_python.py +1839 -0
- setiastro/saspro/autostretch.py +196 -0
- setiastro/saspro/backgroundneutral.py +560 -0
- setiastro/saspro/batch_convert.py +325 -0
- setiastro/saspro/batch_renamer.py +519 -0
- setiastro/saspro/blemish_blaster.py +488 -0
- setiastro/saspro/blink_comparator_pro.py +2923 -0
- setiastro/saspro/bundles.py +61 -0
- setiastro/saspro/bundles_dock.py +114 -0
- setiastro/saspro/cheat_sheet.py +168 -0
- setiastro/saspro/clahe.py +342 -0
- setiastro/saspro/comet_stacking.py +1377 -0
- setiastro/saspro/config.py +38 -0
- setiastro/saspro/config_bootstrap.py +40 -0
- setiastro/saspro/config_manager.py +316 -0
- setiastro/saspro/continuum_subtract.py +1617 -0
- setiastro/saspro/convo.py +1397 -0
- setiastro/saspro/convo_preset.py +414 -0
- setiastro/saspro/copyastro.py +187 -0
- setiastro/saspro/cosmicclarity.py +1564 -0
- setiastro/saspro/cosmicclarity_preset.py +407 -0
- setiastro/saspro/crop_dialog_pro.py +948 -0
- setiastro/saspro/crop_preset.py +189 -0
- setiastro/saspro/curve_editor_pro.py +2544 -0
- setiastro/saspro/curves_preset.py +375 -0
- setiastro/saspro/debayer.py +670 -0
- setiastro/saspro/debug_utils.py +29 -0
- setiastro/saspro/dnd_mime.py +35 -0
- setiastro/saspro/doc_manager.py +2634 -0
- setiastro/saspro/exoplanet_detector.py +2166 -0
- setiastro/saspro/file_utils.py +284 -0
- setiastro/saspro/fitsmodifier.py +744 -0
- setiastro/saspro/free_torch_memory.py +48 -0
- setiastro/saspro/frequency_separation.py +1343 -0
- setiastro/saspro/function_bundle.py +1594 -0
- setiastro/saspro/ghs_dialog_pro.py +660 -0
- setiastro/saspro/ghs_preset.py +284 -0
- setiastro/saspro/graxpert.py +634 -0
- setiastro/saspro/graxpert_preset.py +287 -0
- setiastro/saspro/gui/__init__.py +0 -0
- setiastro/saspro/gui/main_window.py +8494 -0
- setiastro/saspro/gui/mixins/__init__.py +33 -0
- setiastro/saspro/gui/mixins/dock_mixin.py +263 -0
- setiastro/saspro/gui/mixins/file_mixin.py +445 -0
- setiastro/saspro/gui/mixins/geometry_mixin.py +403 -0
- setiastro/saspro/gui/mixins/header_mixin.py +441 -0
- setiastro/saspro/gui/mixins/mask_mixin.py +421 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +361 -0
- setiastro/saspro/gui/mixins/theme_mixin.py +367 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +1324 -0
- setiastro/saspro/gui/mixins/update_mixin.py +309 -0
- setiastro/saspro/gui/mixins/view_mixin.py +435 -0
- setiastro/saspro/halobgon.py +462 -0
- setiastro/saspro/header_viewer.py +445 -0
- setiastro/saspro/headless_utils.py +88 -0
- setiastro/saspro/histogram.py +753 -0
- setiastro/saspro/history_explorer.py +939 -0
- setiastro/saspro/image_combine.py +414 -0
- setiastro/saspro/image_peeker_pro.py +1596 -0
- setiastro/saspro/imageops/__init__.py +37 -0
- setiastro/saspro/imageops/mdi_snap.py +292 -0
- setiastro/saspro/imageops/scnr.py +36 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +210 -0
- setiastro/saspro/imageops/stretch.py +244 -0
- setiastro/saspro/isophote.py +1179 -0
- setiastro/saspro/layers.py +208 -0
- setiastro/saspro/layers_dock.py +714 -0
- setiastro/saspro/lazy_imports.py +193 -0
- setiastro/saspro/legacy/__init__.py +2 -0
- setiastro/saspro/legacy/image_manager.py +2226 -0
- setiastro/saspro/legacy/numba_utils.py +3659 -0
- setiastro/saspro/legacy/xisf.py +1071 -0
- setiastro/saspro/linear_fit.py +534 -0
- setiastro/saspro/live_stacking.py +1830 -0
- setiastro/saspro/log_bus.py +5 -0
- setiastro/saspro/logging_config.py +460 -0
- setiastro/saspro/luminancerecombine.py +309 -0
- setiastro/saspro/main_helpers.py +201 -0
- setiastro/saspro/mask_creation.py +928 -0
- setiastro/saspro/masks_core.py +56 -0
- setiastro/saspro/mdi_widgets.py +353 -0
- setiastro/saspro/memory_utils.py +666 -0
- setiastro/saspro/metadata_patcher.py +75 -0
- setiastro/saspro/mfdeconv.py +3826 -0
- setiastro/saspro/mfdeconv_earlystop.py +71 -0
- setiastro/saspro/mfdeconvcudnn.py +3263 -0
- setiastro/saspro/mfdeconvsport.py +2382 -0
- setiastro/saspro/minorbodycatalog.py +567 -0
- setiastro/saspro/morphology.py +382 -0
- setiastro/saspro/multiscale_decomp.py +1290 -0
- setiastro/saspro/nbtorgb_stars.py +531 -0
- setiastro/saspro/numba_utils.py +3044 -0
- setiastro/saspro/numba_warmup.py +141 -0
- setiastro/saspro/ops/__init__.py +9 -0
- setiastro/saspro/ops/command_help_dialog.py +623 -0
- setiastro/saspro/ops/command_runner.py +217 -0
- setiastro/saspro/ops/commands.py +1594 -0
- setiastro/saspro/ops/script_editor.py +1102 -0
- setiastro/saspro/ops/scripts.py +1413 -0
- setiastro/saspro/ops/settings.py +560 -0
- setiastro/saspro/parallel_utils.py +554 -0
- setiastro/saspro/pedestal.py +121 -0
- setiastro/saspro/perfect_palette_picker.py +1053 -0
- setiastro/saspro/pipeline.py +110 -0
- setiastro/saspro/pixelmath.py +1600 -0
- setiastro/saspro/plate_solver.py +2435 -0
- setiastro/saspro/project_io.py +797 -0
- setiastro/saspro/psf_utils.py +136 -0
- setiastro/saspro/psf_viewer.py +549 -0
- setiastro/saspro/pyi_rthook_astroquery.py +95 -0
- setiastro/saspro/remove_green.py +314 -0
- setiastro/saspro/remove_stars.py +1625 -0
- setiastro/saspro/remove_stars_preset.py +404 -0
- setiastro/saspro/resources.py +472 -0
- setiastro/saspro/rgb_combination.py +207 -0
- setiastro/saspro/rgb_extract.py +19 -0
- setiastro/saspro/rgbalign.py +723 -0
- setiastro/saspro/runtime_imports.py +7 -0
- setiastro/saspro/runtime_torch.py +754 -0
- setiastro/saspro/save_options.py +72 -0
- setiastro/saspro/selective_color.py +1552 -0
- setiastro/saspro/sfcc.py +1425 -0
- setiastro/saspro/shortcuts.py +2807 -0
- setiastro/saspro/signature_insert.py +1099 -0
- setiastro/saspro/stacking_suite.py +17712 -0
- setiastro/saspro/star_alignment.py +7420 -0
- setiastro/saspro/star_alignment_preset.py +329 -0
- setiastro/saspro/star_metrics.py +49 -0
- setiastro/saspro/star_spikes.py +681 -0
- setiastro/saspro/star_stretch.py +470 -0
- setiastro/saspro/stat_stretch.py +502 -0
- setiastro/saspro/status_log_dock.py +78 -0
- setiastro/saspro/subwindow.py +3267 -0
- setiastro/saspro/supernovaasteroidhunter.py +1712 -0
- setiastro/saspro/swap_manager.py +99 -0
- setiastro/saspro/torch_backend.py +89 -0
- setiastro/saspro/torch_rejection.py +434 -0
- setiastro/saspro/view_bundle.py +1555 -0
- setiastro/saspro/wavescale_hdr.py +624 -0
- setiastro/saspro/wavescale_hdr_preset.py +100 -0
- setiastro/saspro/wavescalede.py +657 -0
- setiastro/saspro/wavescalede_preset.py +228 -0
- setiastro/saspro/wcs_update.py +374 -0
- setiastro/saspro/whitebalance.py +456 -0
- setiastro/saspro/widgets/__init__.py +48 -0
- setiastro/saspro/widgets/common_utilities.py +305 -0
- setiastro/saspro/widgets/graphics_views.py +122 -0
- setiastro/saspro/widgets/image_utils.py +518 -0
- setiastro/saspro/widgets/preview_dialogs.py +280 -0
- setiastro/saspro/widgets/spinboxes.py +275 -0
- setiastro/saspro/widgets/themed_buttons.py +13 -0
- setiastro/saspro/widgets/wavelet_utils.py +299 -0
- setiastro/saspro/window_shelf.py +185 -0
- setiastro/saspro/xisf.py +1123 -0
- setiastrosuitepro-1.6.0.dist-info/METADATA +266 -0
- setiastrosuitepro-1.6.0.dist-info/RECORD +174 -0
- setiastrosuitepro-1.6.0.dist-info/WHEEL +4 -0
- setiastrosuitepro-1.6.0.dist-info/entry_points.txt +6 -0
- setiastrosuitepro-1.6.0.dist-info/licenses/LICENSE +674 -0
- 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()
|