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,670 @@
|
|
|
1
|
+
# pro/debayer.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import numpy as np
|
|
4
|
+
from typing import Optional, Tuple
|
|
5
|
+
import cv2
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
|
9
|
+
from PyQt6.QtWidgets import (
|
|
10
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QDialogButtonBox,
|
|
11
|
+
QGroupBox, QMessageBox, QProgressBar
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# fast kernels you already have
|
|
15
|
+
try:
|
|
16
|
+
from setiastro.saspro.legacy.numba_utils import debayer_fits_fast
|
|
17
|
+
except Exception as e: # very unlikely in your env
|
|
18
|
+
debayer_fits_fast = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_RAW_EXTS = (".raf", ".raw", ".rw2", ".arw", ".nef", ".cr2", ".cr3", ".dng", ".orf", ".pef")
|
|
22
|
+
|
|
23
|
+
def _find_raw_sibling(path: Optional[str]) -> Optional[str]:
|
|
24
|
+
"""
|
|
25
|
+
If we only have a derived FITS/XISF path, try to locate a plausible RAW
|
|
26
|
+
with the same stem in the same folder.
|
|
27
|
+
"""
|
|
28
|
+
if not path:
|
|
29
|
+
return None
|
|
30
|
+
base, _ = os.path.splitext(os.path.basename(path))
|
|
31
|
+
folder = os.path.dirname(path)
|
|
32
|
+
if not folder or not base:
|
|
33
|
+
return None
|
|
34
|
+
try:
|
|
35
|
+
for ext in _RAW_EXTS:
|
|
36
|
+
cand = os.path.join(folder, base + ext)
|
|
37
|
+
if os.path.exists(cand):
|
|
38
|
+
return cand
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
# -------- helpers ------------------------------------------------------------
|
|
44
|
+
_BAYER_METHODS = [
|
|
45
|
+
("Edge-aware (Numba)", "edge"),
|
|
46
|
+
("Bilinear (Numba)", "bilinear"),
|
|
47
|
+
]
|
|
48
|
+
_XTRANS_METHODS = [
|
|
49
|
+
("AHD (rawpy)", "AHD"),
|
|
50
|
+
("DHT (rawpy)", "DHT"),
|
|
51
|
+
]
|
|
52
|
+
_VALID = {"RGGB", "BGGR", "GRBG", "GBRG"}
|
|
53
|
+
|
|
54
|
+
def _normalize_bayer_token(s: str) -> Optional[str]:
|
|
55
|
+
if not s:
|
|
56
|
+
return None
|
|
57
|
+
s = s.upper().replace(",", "").replace(" ", "").replace("/", "").replace("|", "")
|
|
58
|
+
if len(s) == 4 and set(s) <= {"R", "G", "B"}:
|
|
59
|
+
if s.count("R") == 1 and s.count("G") == 2 and s.count("B") == 1:
|
|
60
|
+
return s if s in _VALID else None
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
def _detect_bayer_from_header(doc) -> Optional[str]:
|
|
64
|
+
"""
|
|
65
|
+
Best-effort read of a Bayer pattern from the document header/metadata.
|
|
66
|
+
Returns 'RGGB'/'BGGR'/'GRBG'/'GBRG' or None if not found.
|
|
67
|
+
"""
|
|
68
|
+
hdr, meta, _ = _extract_doc_info(doc)
|
|
69
|
+
|
|
70
|
+
probe = {}
|
|
71
|
+
# FITS-like header (astropy Header behaves like a dict, case-insensitive)
|
|
72
|
+
if hdr is not None:
|
|
73
|
+
try:
|
|
74
|
+
keys = list(hdr.keys()) if hasattr(hdr, "keys") else []
|
|
75
|
+
for k in keys:
|
|
76
|
+
try:
|
|
77
|
+
v = hdr.get(k) if hasattr(hdr, "get") else hdr[k]
|
|
78
|
+
except Exception:
|
|
79
|
+
v = None
|
|
80
|
+
probe[str(k).upper()] = "" if v is None else str(v)
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
# Merge doc metadata (strings only)
|
|
85
|
+
if isinstance(meta, dict):
|
|
86
|
+
for k, v in list(meta.items()):
|
|
87
|
+
try:
|
|
88
|
+
probe[str(k).upper()] = "" if v is None else str(v)
|
|
89
|
+
except Exception:
|
|
90
|
+
continue
|
|
91
|
+
|
|
92
|
+
keys = [
|
|
93
|
+
"BAYERPAT", "BAYERPATN", "BAYER_PATTERN", "BAYERPATTERN",
|
|
94
|
+
"CFAPATTERN", "CFA_PATTERN", "PATTERN", "COLORTYPE", "COLORFILTERARRAY"
|
|
95
|
+
]
|
|
96
|
+
for k in keys:
|
|
97
|
+
raw = probe.get(k)
|
|
98
|
+
if not raw:
|
|
99
|
+
continue
|
|
100
|
+
s = str(raw).upper()
|
|
101
|
+
for pat in _VALID:
|
|
102
|
+
if pat in s:
|
|
103
|
+
return pat
|
|
104
|
+
norm = _normalize_bayer_token(s)
|
|
105
|
+
if norm:
|
|
106
|
+
return norm
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _detect_cfa_family(doc) -> Optional[str]:
|
|
111
|
+
"""
|
|
112
|
+
Returns 'BAYER', 'XTRANS', or None.
|
|
113
|
+
Uses header/meta hints; if a RAW path exists, asks rawpy for ground truth.
|
|
114
|
+
"""
|
|
115
|
+
hdr, meta, src_path = _extract_doc_info(doc)
|
|
116
|
+
|
|
117
|
+
# Build a searchable blob of header + meta
|
|
118
|
+
def _safe_blob(x) -> str:
|
|
119
|
+
try:
|
|
120
|
+
return str(x)
|
|
121
|
+
except Exception:
|
|
122
|
+
return ""
|
|
123
|
+
blob = (_safe_blob(hdr) + " " + _safe_blob(meta)).upper()
|
|
124
|
+
|
|
125
|
+
# Direct tokens first
|
|
126
|
+
if any(k in blob for k in ("X-TRANS", "XTRANS", "FUJIFILM X-TRANS", "X TRANS")):
|
|
127
|
+
return "XTRANS"
|
|
128
|
+
if any(k in blob for k in ("BAYER", "RGGB", "BGGR", "GRBG", "GBRG")):
|
|
129
|
+
return "BAYER"
|
|
130
|
+
|
|
131
|
+
# Camera model hint
|
|
132
|
+
model = (str(meta.get("MODEL")
|
|
133
|
+
or meta.get("CameraModel")
|
|
134
|
+
or (hdr.get("MODEL") if hasattr(hdr, "get") else None)
|
|
135
|
+
or "")).upper()
|
|
136
|
+
make = (str(meta.get("MAKE")
|
|
137
|
+
or (hdr.get("MAKE") if hasattr(hdr, "get") else None)
|
|
138
|
+
or "")).upper()
|
|
139
|
+
if "FUJIFILM" in make and any(tag in model for tag in (
|
|
140
|
+
"X-T1","X-T2","X-T3","X-T4","X-T5",
|
|
141
|
+
"X-E2","X-E2S","X-E3","X-PRO1","X-PRO2","X-PRO3",
|
|
142
|
+
"X-H1","X-H2S","X-S10","X-S20","X100","X70","X30"
|
|
143
|
+
)):
|
|
144
|
+
return "XTRANS"
|
|
145
|
+
|
|
146
|
+
# If we have a source path, ask rawpy
|
|
147
|
+
if src_path:
|
|
148
|
+
try:
|
|
149
|
+
import rawpy # type: ignore
|
|
150
|
+
with rawpy.imread(src_path) as rp:
|
|
151
|
+
if getattr(rp, "xtrans_pattern", None) is not None:
|
|
152
|
+
return "XTRANS"
|
|
153
|
+
pat = getattr(rp, "raw_pattern", None)
|
|
154
|
+
if pat is not None:
|
|
155
|
+
pat = np.array(pat)
|
|
156
|
+
if getattr(pat, "shape", None) == (2, 2):
|
|
157
|
+
return "BAYER"
|
|
158
|
+
except Exception:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _score_rgb(rgb: np.ndarray) -> float:
|
|
165
|
+
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
|
|
166
|
+
def grad_energy(x):
|
|
167
|
+
gx = cv2.Sobel(x, cv2.CV_32F, 1, 0, ksize=3)
|
|
168
|
+
gy = cv2.Sobel(x, cv2.CV_32F, 0, 1, ksize=3)
|
|
169
|
+
return float(np.mean(gx*gx + gy*gy))
|
|
170
|
+
e_rg = grad_energy(r - g)
|
|
171
|
+
e_bg = grad_energy(b - g)
|
|
172
|
+
return e_rg + e_bg
|
|
173
|
+
|
|
174
|
+
def _autodetect_bayer_by_scoring(mono: np.ndarray) -> str:
|
|
175
|
+
candidates = ["RGGB", "BGGR", "GRBG", "GBRG"]
|
|
176
|
+
best_pat, best_score = None, float("inf")
|
|
177
|
+
for pat in candidates:
|
|
178
|
+
try:
|
|
179
|
+
rgb = debayer_fits_fast(mono, pat, cfa_drizzle=False)
|
|
180
|
+
rgb32 = rgb.astype(np.float32, copy=False)
|
|
181
|
+
s = _score_rgb(rgb32)
|
|
182
|
+
if s < best_score:
|
|
183
|
+
best_score, best_pat = s, pat
|
|
184
|
+
except Exception:
|
|
185
|
+
continue
|
|
186
|
+
return best_pat or "RGGB"
|
|
187
|
+
|
|
188
|
+
def _debayer_xtrans_via_rawpy(src_path: str,
|
|
189
|
+
use_cam_wb: bool = True,
|
|
190
|
+
output_bps: int = 16,
|
|
191
|
+
alg: str = "AHD") -> np.ndarray:
|
|
192
|
+
"""
|
|
193
|
+
X-Trans demosaic via rawpy with selectable algorithm: 'AHD' or 'DHT'.
|
|
194
|
+
Returns float32 RGB in [0,1].
|
|
195
|
+
"""
|
|
196
|
+
import rawpy # type: ignore
|
|
197
|
+
alg_map = {
|
|
198
|
+
"AHD": rawpy.DemosaicAlgorithm.AHD,
|
|
199
|
+
"DHT": rawpy.DemosaicAlgorithm.DHT,
|
|
200
|
+
}
|
|
201
|
+
dem = alg_map.get((alg or "AHD").upper(), rawpy.DemosaicAlgorithm.AHD)
|
|
202
|
+
with rawpy.imread(src_path) as rp:
|
|
203
|
+
rgb16 = rp.postprocess(
|
|
204
|
+
demosaic_algorithm=dem,
|
|
205
|
+
no_auto_bright=True,
|
|
206
|
+
gamma=(1.0, 1.0),
|
|
207
|
+
output_bps=output_bps,
|
|
208
|
+
use_camera_wb=use_cam_wb,
|
|
209
|
+
fbdd_noise_reduction=rawpy.FBDDNoiseReductionMode.Off,
|
|
210
|
+
four_color_rgb=False,
|
|
211
|
+
half_size=False,
|
|
212
|
+
bright=1.0,
|
|
213
|
+
highlight_mode=rawpy.HighlightMode.Clip
|
|
214
|
+
)
|
|
215
|
+
return (rgb16.astype(np.float32) / (65535.0 if output_bps == 16 else 255.0))
|
|
216
|
+
|
|
217
|
+
def _doc_is_managed(dm, doc) -> bool:
|
|
218
|
+
"""Best-effort: is this document tracked by DocManager?"""
|
|
219
|
+
if dm is None or doc is None:
|
|
220
|
+
return False
|
|
221
|
+
# Explicit API if you have it
|
|
222
|
+
fn = getattr(dm, "is_managed_document", None)
|
|
223
|
+
if callable(fn):
|
|
224
|
+
try:
|
|
225
|
+
return bool(fn(doc))
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
# Common internal collections
|
|
229
|
+
for attr in ("_docs", "documents", "open_documents", "all_docs"):
|
|
230
|
+
coll = getattr(dm, attr, None)
|
|
231
|
+
if isinstance(coll, (list, tuple)):
|
|
232
|
+
try:
|
|
233
|
+
return any(d is doc for d in coll)
|
|
234
|
+
except Exception:
|
|
235
|
+
continue
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
def _apply_result_to_doc(dm, doc, rgb: np.ndarray, step_name: str = "Debayer"):
|
|
239
|
+
"""
|
|
240
|
+
Always apply to the specific 'doc' (never assume 'active' view).
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
# --- A) Best: the document can commit its own undoable edit ---
|
|
244
|
+
if hasattr(doc, "apply_edit") and callable(doc.apply_edit):
|
|
245
|
+
meta = dict(getattr(doc, "metadata", {}) or {})
|
|
246
|
+
meta["is_mono"] = False
|
|
247
|
+
meta.setdefault("bit_depth", "32-bit floating point")
|
|
248
|
+
try:
|
|
249
|
+
doc.apply_edit(rgb.copy(), metadata=meta, step_name=step_name)
|
|
250
|
+
except TypeError:
|
|
251
|
+
# older signature: (image, metadata) or (image,)
|
|
252
|
+
try: doc.apply_edit(rgb.copy(), metadata=meta)
|
|
253
|
+
except Exception: doc.apply_edit(rgb.copy())
|
|
254
|
+
_refresh_view_for_doc(dm, doc)
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
# --- B) Next best: mutate the doc directly, then refresh its view ---
|
|
258
|
+
try:
|
|
259
|
+
doc.image = rgb
|
|
260
|
+
meta = getattr(doc, "metadata", None)
|
|
261
|
+
if isinstance(meta, dict):
|
|
262
|
+
meta["is_mono"] = False
|
|
263
|
+
meta.setdefault("bit_depth", "32-bit floating point")
|
|
264
|
+
except Exception:
|
|
265
|
+
pass
|
|
266
|
+
_refresh_view_for_doc(dm, doc)
|
|
267
|
+
return
|
|
268
|
+
|
|
269
|
+
def _refresh_view_for_doc(dm, doc):
|
|
270
|
+
"""
|
|
271
|
+
Find the subwindow that displays 'doc' and ask it to repaint,
|
|
272
|
+
without switching the active view.
|
|
273
|
+
"""
|
|
274
|
+
try:
|
|
275
|
+
# Try a dedicated API if your app exposes it
|
|
276
|
+
fn = getattr(dm, "refresh_subwindow_for_document", None)
|
|
277
|
+
if callable(fn):
|
|
278
|
+
fn(doc); return
|
|
279
|
+
except Exception:
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
# Generic fallback: walk MDI subwindows and update the one bound to 'doc'
|
|
283
|
+
try:
|
|
284
|
+
mw = getattr(dm, "main_window", None) or getattr(dm, "mw", None)
|
|
285
|
+
if mw is None: return
|
|
286
|
+
mdi = getattr(mw, "mdi", None)
|
|
287
|
+
if mdi is None: return
|
|
288
|
+
for sw in mdi.subWindowList():
|
|
289
|
+
w = getattr(sw, "widget", lambda: None)()
|
|
290
|
+
if getattr(w, "document", None) is doc:
|
|
291
|
+
# Common update hooks
|
|
292
|
+
upd = getattr(w, "refresh_pixmap_from_document", None) \
|
|
293
|
+
or getattr(w, "refresh_view", None) \
|
|
294
|
+
or getattr(w, "update_from_doc", None)
|
|
295
|
+
if callable(upd):
|
|
296
|
+
upd(); return
|
|
297
|
+
# Absolute fallback: force a repaint
|
|
298
|
+
try:
|
|
299
|
+
w.update(); sw.update()
|
|
300
|
+
except Exception:
|
|
301
|
+
pass
|
|
302
|
+
return
|
|
303
|
+
except Exception:
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# -------- worker -------------------------------------------------------------
|
|
308
|
+
|
|
309
|
+
class _DebayerWorker(QThread):
|
|
310
|
+
progress = pyqtSignal(int, str)
|
|
311
|
+
failed = pyqtSignal(str)
|
|
312
|
+
finished = pyqtSignal(np.ndarray, str) # (rgb, used_pattern)
|
|
313
|
+
|
|
314
|
+
def __init__(self, mono: np.ndarray, pattern: str, method: str = "edge"):
|
|
315
|
+
super().__init__()
|
|
316
|
+
self.mono = mono
|
|
317
|
+
self.pattern = pattern
|
|
318
|
+
self.method = (method or "edge")
|
|
319
|
+
|
|
320
|
+
def run(self):
|
|
321
|
+
try:
|
|
322
|
+
if debayer_fits_fast is None:
|
|
323
|
+
raise RuntimeError("Numba debayer kernels not available.")
|
|
324
|
+
|
|
325
|
+
# enforce kernel-friendly layout/dtype
|
|
326
|
+
img = _mono_as_float32_contig(self.mono)
|
|
327
|
+
|
|
328
|
+
if img.ndim != 2:
|
|
329
|
+
raise ValueError("Debayer expects a single-channel (mosaic) image.")
|
|
330
|
+
if self.pattern not in _VALID:
|
|
331
|
+
raise ValueError(f"Unsupported pattern: {self.pattern}")
|
|
332
|
+
|
|
333
|
+
self.progress.emit(5, f"Debayering ({self.pattern}, {self.method}) …")
|
|
334
|
+
rgb = debayer_fits_fast(img, self.pattern, cfa_drizzle=False, method=self.method)
|
|
335
|
+
self.progress.emit(96, "Finalizing …")
|
|
336
|
+
self.finished.emit(rgb, self.pattern)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
self.failed.emit(str(e))
|
|
339
|
+
|
|
340
|
+
def _extract_doc_info(doc) -> tuple[dict | None, dict, Optional[str]]:
|
|
341
|
+
meta = getattr(doc, "metadata", {}) or {}
|
|
342
|
+
hdr = (meta.get("original_header")
|
|
343
|
+
or meta.get("fits_header")
|
|
344
|
+
or meta.get("header")
|
|
345
|
+
or getattr(doc, "header", None))
|
|
346
|
+
|
|
347
|
+
# try multiple fields for a source path
|
|
348
|
+
def _first_nonempty(*vals):
|
|
349
|
+
for v in vals:
|
|
350
|
+
if v:
|
|
351
|
+
return v
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
# header cards that might store original RAW path
|
|
355
|
+
hdr_raw = None
|
|
356
|
+
try:
|
|
357
|
+
if hdr is not None:
|
|
358
|
+
for k in ("RAW_PATH","RAWFILE","ORIGFILE","ORIGINAL","ORIGPATH","RAWORIG","SOURCE","SRCFILE"):
|
|
359
|
+
v = hdr.get(k) if hasattr(hdr, "get") else hdr[k] # may raise → caught
|
|
360
|
+
if v:
|
|
361
|
+
hdr_raw = str(v)
|
|
362
|
+
break
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
path = _first_nonempty(
|
|
367
|
+
meta.get("raw_source_path"),
|
|
368
|
+
hdr_raw,
|
|
369
|
+
meta.get("file_path"),
|
|
370
|
+
getattr(doc, "path", None),
|
|
371
|
+
getattr(doc, "file_path", None),
|
|
372
|
+
)
|
|
373
|
+
return hdr, meta, path
|
|
374
|
+
|
|
375
|
+
def _mono_as_float32_contig(arr: np.ndarray) -> np.ndarray:
|
|
376
|
+
"""
|
|
377
|
+
Ensure mono mosaic is 2D, C-contiguous, float32 in [0,1] for numba kernels.
|
|
378
|
+
Scales integer inputs by their max (8/16/32 bits).
|
|
379
|
+
"""
|
|
380
|
+
a = np.asarray(arr)
|
|
381
|
+
if a.ndim != 2:
|
|
382
|
+
raise RuntimeError("Debayer expects a single-channel (mosaic) image.")
|
|
383
|
+
if np.issubdtype(a.dtype, np.integer):
|
|
384
|
+
# pick a sensible scale based on dtype
|
|
385
|
+
info = np.iinfo(a.dtype)
|
|
386
|
+
a = a.astype(np.float32, copy=False) / float(info.max if info.max > 0 else 1.0)
|
|
387
|
+
else:
|
|
388
|
+
a = a.astype(np.float32, copy=False)
|
|
389
|
+
# if it looks like 0..65535 in float, normalize too
|
|
390
|
+
if a.max() > 2.0:
|
|
391
|
+
a = a / 65535.0
|
|
392
|
+
return np.ascontiguousarray(a)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
# -------- dialog -------------------------------------------------------------
|
|
396
|
+
|
|
397
|
+
class DebayerDialog(QDialog):
|
|
398
|
+
"""
|
|
399
|
+
One-shot debayer UI for the active view. Uses your numba kernels.
|
|
400
|
+
If the image is already RGB, will warn and exit.
|
|
401
|
+
"""
|
|
402
|
+
def __init__(self, parent, doc_manager, active_doc):
|
|
403
|
+
super().__init__(parent)
|
|
404
|
+
self.setWindowTitle("Debayer")
|
|
405
|
+
self.dm = doc_manager
|
|
406
|
+
self.doc = active_doc
|
|
407
|
+
self.worker: Optional[_DebayerWorker] = None
|
|
408
|
+
|
|
409
|
+
img = getattr(active_doc, "image", None)
|
|
410
|
+
if img is None:
|
|
411
|
+
raise RuntimeError("No image in active document.")
|
|
412
|
+
arr = np.asarray(img)
|
|
413
|
+
|
|
414
|
+
# Reject non-mosaic early
|
|
415
|
+
if arr.ndim == 3 and arr.shape[2] >= 3:
|
|
416
|
+
QMessageBox.information(self, "Debayer", "Image already has 3 channels.")
|
|
417
|
+
self.setEnabled(False)
|
|
418
|
+
self.close()
|
|
419
|
+
return
|
|
420
|
+
if arr.ndim != 2:
|
|
421
|
+
QMessageBox.warning(self, "Debayer", "Only single-channel mosaics can be debayered.")
|
|
422
|
+
self.setEnabled(False)
|
|
423
|
+
self.close()
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
# ✅ normalize for numba kernels
|
|
427
|
+
self._src = _mono_as_float32_contig(arr)
|
|
428
|
+
|
|
429
|
+
# detect CFA family (BAYER/XTRANS/None)
|
|
430
|
+
self._cfa_family = _detect_cfa_family(active_doc)
|
|
431
|
+
|
|
432
|
+
v = QVBoxLayout(self)
|
|
433
|
+
|
|
434
|
+
# pattern selection
|
|
435
|
+
detected = _detect_bayer_from_header(active_doc)
|
|
436
|
+
self._detected_pattern = detected # store for later
|
|
437
|
+
|
|
438
|
+
gb = QGroupBox("Bayer pattern", self)
|
|
439
|
+
h = QHBoxLayout(gb)
|
|
440
|
+
self.combo_pattern = QComboBox(self)
|
|
441
|
+
self.combo_pattern.addItems([
|
|
442
|
+
"Auto (from header)",
|
|
443
|
+
"RGGB", "BGGR", "GRBG", "GBRG",
|
|
444
|
+
])
|
|
445
|
+
self.combo_pattern.setCurrentIndex(0)
|
|
446
|
+
self.lbl_detect = QLabel(f"Detected: {detected or '(unknown)'}")
|
|
447
|
+
h.addWidget(self.combo_pattern, 1)
|
|
448
|
+
h.addWidget(self.lbl_detect)
|
|
449
|
+
v.addWidget(gb)
|
|
450
|
+
|
|
451
|
+
if self._cfa_family == 'XTRANS':
|
|
452
|
+
self.combo_pattern.setEnabled(False)
|
|
453
|
+
self.lbl_detect.setText("Detected: X-Trans (rawpy)")
|
|
454
|
+
else:
|
|
455
|
+
norm = _normalize_bayer_token(self._detected_pattern or "")
|
|
456
|
+
self.lbl_detect.setText(f"Detected: {norm or '(unknown)'}")
|
|
457
|
+
|
|
458
|
+
self.method_group = QGroupBox("Method", self)
|
|
459
|
+
hm = QHBoxLayout(self.method_group)
|
|
460
|
+
self.combo_method = QComboBox(self)
|
|
461
|
+
|
|
462
|
+
if self._cfa_family == 'XTRANS':
|
|
463
|
+
for label, _tok in _XTRANS_METHODS:
|
|
464
|
+
self.combo_method.addItem(label)
|
|
465
|
+
else:
|
|
466
|
+
for label, _tok in _BAYER_METHODS:
|
|
467
|
+
self.combo_method.addItem(label)
|
|
468
|
+
|
|
469
|
+
print(f"[Debayer] CFA family auto-detect → {self._cfa_family} "
|
|
470
|
+
f"path={_extract_doc_info(active_doc)[2]} "
|
|
471
|
+
f"model={getattr(active_doc, 'metadata', {}).get('MODEL')}")
|
|
472
|
+
|
|
473
|
+
self.combo_method.setCurrentIndex(0)
|
|
474
|
+
hm.addWidget(self.combo_method)
|
|
475
|
+
hm.addStretch(1)
|
|
476
|
+
v.addWidget(self.method_group)
|
|
477
|
+
|
|
478
|
+
# progress + buttons
|
|
479
|
+
self.status = QLabel("")
|
|
480
|
+
self.bar = QProgressBar(self); self.bar.setRange(0, 100)
|
|
481
|
+
v.addWidget(self.status)
|
|
482
|
+
v.addWidget(self.bar)
|
|
483
|
+
|
|
484
|
+
btns = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel, parent=self)
|
|
485
|
+
btns.accepted.connect(self._go)
|
|
486
|
+
btns.rejected.connect(self.reject)
|
|
487
|
+
v.addWidget(btns)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _chosen_pattern(self) -> str:
|
|
491
|
+
if self._cfa_family == 'XTRANS':
|
|
492
|
+
return "XTRANS"
|
|
493
|
+
txt = self.combo_pattern.currentText()
|
|
494
|
+
if txt.startswith("Auto"):
|
|
495
|
+
norm = _normalize_bayer_token(self._detected_pattern or "")
|
|
496
|
+
return norm or _autodetect_bayer_by_scoring(self._src)
|
|
497
|
+
return txt
|
|
498
|
+
|
|
499
|
+
def _go(self):
|
|
500
|
+
pat = self._chosen_pattern()
|
|
501
|
+
method_label = self.combo_method.currentText()
|
|
502
|
+
|
|
503
|
+
# X-Trans path
|
|
504
|
+
if pat == "XTRANS":
|
|
505
|
+
src_path = (getattr(self.doc, "file_path", None) or getattr(self.doc, "path", None)
|
|
506
|
+
or (getattr(self.doc, "metadata", {}) or {}).get("file_path"))
|
|
507
|
+
if not src_path or not str(src_path).lower().endswith(_RAW_EXTS):
|
|
508
|
+
# try sibling RAW next to this file
|
|
509
|
+
sib = _find_raw_sibling(getattr(self.doc, "file_path", None) or (getattr(self.doc, "metadata", {}) or {}).get("file_path"))
|
|
510
|
+
if sib:
|
|
511
|
+
src_path = sib
|
|
512
|
+
else:
|
|
513
|
+
QMessageBox.warning(self, "Debayer",
|
|
514
|
+
"X-Trans detected, but original RAW path was not found.\n"
|
|
515
|
+
"Open the RAF directly, or embed RAW_PATH in the header, or place the RAW next to the file.")
|
|
516
|
+
return
|
|
517
|
+
# map label → rawpy alg token
|
|
518
|
+
alg = next((tok for (label, tok) in _XTRANS_METHODS if label == method_label), "AHD")
|
|
519
|
+
try:
|
|
520
|
+
self.status.setText(f"Demosaicing X-Trans via rawpy ({alg}) …")
|
|
521
|
+
self.bar.setValue(10)
|
|
522
|
+
rgb = _debayer_xtrans_via_rawpy(src_path, use_cam_wb=True, output_bps=16, alg=alg)
|
|
523
|
+
self.bar.setValue(96)
|
|
524
|
+
_apply_result_to_doc(self.dm, self.doc, rgb.astype(np.float32, copy=False),
|
|
525
|
+
step_name=f"Debayer (X-Trans/{alg})")
|
|
526
|
+
self.status.setText("Done.")
|
|
527
|
+
self.accept()
|
|
528
|
+
except Exception as e:
|
|
529
|
+
QMessageBox.critical(self, "Debayer", f"X-Trans demosaic failed:\n{e}")
|
|
530
|
+
self.status.setText("Failed.")
|
|
531
|
+
return
|
|
532
|
+
|
|
533
|
+
# Bayer path (Numba) with method
|
|
534
|
+
if pat not in _VALID:
|
|
535
|
+
QMessageBox.warning(self, "Debayer", "Unknown pattern (auto-detect failed). Choose a pattern explicitly.")
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
bayer_method = next((tok for (label, tok) in _BAYER_METHODS if label == method_label), "edge")
|
|
539
|
+
self.status.setText(f"Debayering as {pat} ({bayer_method}) …")
|
|
540
|
+
self.bar.setValue(0)
|
|
541
|
+
self.worker = _DebayerWorker(self._src, pat, method=bayer_method)
|
|
542
|
+
self.worker.progress.connect(self._on_prog)
|
|
543
|
+
self.worker.failed.connect(self._on_fail)
|
|
544
|
+
self.worker.finished.connect(self._on_done)
|
|
545
|
+
self.worker.start()
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def _on_prog(self, p: int, msg: str):
|
|
549
|
+
self.bar.setValue(p); self.status.setText(msg)
|
|
550
|
+
|
|
551
|
+
def _on_fail(self, err: str):
|
|
552
|
+
QMessageBox.critical(self, "Debayer", err)
|
|
553
|
+
self.status.setText("Failed.")
|
|
554
|
+
|
|
555
|
+
def _on_done(self, rgb: np.ndarray, used_pattern: str):
|
|
556
|
+
# Hand back to doc manager with an undo step name
|
|
557
|
+
_apply_result_to_doc(self.dm, self.doc, rgb, step_name=f"Debayer ({used_pattern})")
|
|
558
|
+
self.status.setText("Done.")
|
|
559
|
+
self.accept()
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
# -------- headless (shortcut / DnD) -----------------------------------------
|
|
563
|
+
|
|
564
|
+
def apply_debayer_preset_to_doc(dm, doc, preset: dict) -> Tuple[str, np.ndarray]:
|
|
565
|
+
"""
|
|
566
|
+
preset = {
|
|
567
|
+
"pattern": "auto|RGGB|BGGR|GRBG|GBRG",
|
|
568
|
+
"method": "auto|edge|bilinear|AHD|DHT"
|
|
569
|
+
}
|
|
570
|
+
Returns (used_pattern, rgb_array).
|
|
571
|
+
"""
|
|
572
|
+
if getattr(doc, "image", None) is None:
|
|
573
|
+
raise RuntimeError("No image in document.")
|
|
574
|
+
|
|
575
|
+
# ✅ normalize for numba kernels & ensure 2D
|
|
576
|
+
mono_in = np.asarray(doc.image)
|
|
577
|
+
if mono_in.ndim != 2:
|
|
578
|
+
raise RuntimeError("Debayer expects a single-channel (mosaic) image.")
|
|
579
|
+
mono = _mono_as_float32_contig(mono_in)
|
|
580
|
+
|
|
581
|
+
family = _detect_cfa_family(doc)
|
|
582
|
+
want_method = str(preset.get("method", "auto"))
|
|
583
|
+
|
|
584
|
+
# X-Trans → rawpy path
|
|
585
|
+
if family == "XTRANS":
|
|
586
|
+
src_path = (getattr(doc, "file_path", None) or getattr(doc, "path", None)
|
|
587
|
+
or (getattr(doc, "metadata", {}) or {}).get("file_path"))
|
|
588
|
+
|
|
589
|
+
if not src_path or not str(src_path).lower().endswith(_RAW_EXTS):
|
|
590
|
+
hdr, meta, _ = _extract_doc_info(doc)
|
|
591
|
+
src_path = (meta.get("raw_source_path") or
|
|
592
|
+
(hdr.get("RAW_PATH") if hasattr(hdr, "get") else None) or
|
|
593
|
+
_find_raw_sibling(meta.get("file_path") or getattr(doc, "file_path", None)))
|
|
594
|
+
if not src_path or not str(src_path).lower().endswith(_RAW_EXTS):
|
|
595
|
+
raise RuntimeError("X-Trans detected, but no RAW found. "
|
|
596
|
+
"Embed RAW_PATH/raw_source_path or place the RAW next to the file.")
|
|
597
|
+
alg = want_method if want_method in ("AHD", "DHT") else "AHD"
|
|
598
|
+
rgb = _debayer_xtrans_via_rawpy(src_path, use_cam_wb=True, output_bps=16, alg=alg)
|
|
599
|
+
_apply_result_to_doc(dm, doc, rgb, step_name=f"Debayer (X-Trans/{alg})")
|
|
600
|
+
return "XTRANS", rgb
|
|
601
|
+
|
|
602
|
+
# Bayer → Numba path
|
|
603
|
+
want = str(preset.get("pattern", "auto")).upper()
|
|
604
|
+
if want == "AUTO":
|
|
605
|
+
pat = _normalize_bayer_token(_detect_bayer_from_header(doc) or "")
|
|
606
|
+
if pat not in _VALID:
|
|
607
|
+
pat = _autodetect_bayer_by_scoring(mono)
|
|
608
|
+
else:
|
|
609
|
+
pat = want
|
|
610
|
+
if pat not in _VALID:
|
|
611
|
+
raise ValueError(f"Unsupported Bayer pattern: {pat}")
|
|
612
|
+
|
|
613
|
+
method_tok = (want_method.lower() if want_method.lower() in ("edge", "bilinear") else "edge")
|
|
614
|
+
|
|
615
|
+
if debayer_fits_fast is None:
|
|
616
|
+
raise RuntimeError("Numba debayer kernels not available.")
|
|
617
|
+
|
|
618
|
+
rgb = debayer_fits_fast(mono, pat, cfa_drizzle=False, method=method_tok)
|
|
619
|
+
_apply_result_to_doc(dm, doc, rgb, step_name=f"Debayer ({pat}/{method_tok})")
|
|
620
|
+
return pat, rgb
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# -------- headless command runner (Scripts / Presets / Replay) ---------------
|
|
624
|
+
|
|
625
|
+
def run_debayer_via_preset(main, preset: dict | None = None, *, doc=None):
|
|
626
|
+
"""
|
|
627
|
+
Headless Debayer runner.
|
|
628
|
+
|
|
629
|
+
preset keys:
|
|
630
|
+
- pattern: "auto" | "RGGB" | "BGGR" | "GRBG" | "GBRG"
|
|
631
|
+
- method: "auto" | "edge" | "bilinear" | "AHD" | "DHT"
|
|
632
|
+
"""
|
|
633
|
+
p = dict(preset or {})
|
|
634
|
+
|
|
635
|
+
# ---- Register for Replay Last Action ----
|
|
636
|
+
try:
|
|
637
|
+
remember = getattr(main, "remember_last_headless_command", None) \
|
|
638
|
+
or getattr(main, "_remember_last_headless_command", None)
|
|
639
|
+
if callable(remember):
|
|
640
|
+
remember("debayer", p, description="Debayer")
|
|
641
|
+
else:
|
|
642
|
+
setattr(main, "_last_headless_command", {
|
|
643
|
+
"command_id": "debayer",
|
|
644
|
+
"preset": dict(p),
|
|
645
|
+
})
|
|
646
|
+
except Exception:
|
|
647
|
+
pass
|
|
648
|
+
# ----------------------------------------
|
|
649
|
+
|
|
650
|
+
dm = getattr(main, "doc_manager", None) or getattr(main, "dm", None)
|
|
651
|
+
if dm is None:
|
|
652
|
+
QMessageBox.warning(main, "Debayer", "DocManager not available.")
|
|
653
|
+
return
|
|
654
|
+
|
|
655
|
+
if doc is None:
|
|
656
|
+
d = getattr(main, "_active_doc", None)
|
|
657
|
+
doc = d() if callable(d) else d
|
|
658
|
+
|
|
659
|
+
if doc is None or getattr(doc, "image", None) is None:
|
|
660
|
+
QMessageBox.warning(main, "Debayer", "Load an image first.")
|
|
661
|
+
return
|
|
662
|
+
|
|
663
|
+
try:
|
|
664
|
+
used_pat, _rgb = apply_debayer_preset_to_doc(dm, doc, p)
|
|
665
|
+
if hasattr(main, "_log"):
|
|
666
|
+
main._log(f"✅ Debayer (headless) pattern={used_pat}, preset={p}")
|
|
667
|
+
except Exception as e:
|
|
668
|
+
QMessageBox.critical(main, "Debayer", str(e))
|
|
669
|
+
if hasattr(main, "_log"):
|
|
670
|
+
main._log(f"❌ Debayer failed: {e}")
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# pro/debug_utils.py (or near save_document)
|
|
2
|
+
|
|
3
|
+
from astropy.io import fits
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
log = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
def debug_dump_metadata(meta: dict, context: str = ""):
|
|
9
|
+
"""
|
|
10
|
+
Dump all metadata keys and highlight any fits.Header objects.
|
|
11
|
+
"""
|
|
12
|
+
if not isinstance(meta, dict):
|
|
13
|
+
log.debug("[MetaDump %s] metadata is not a dict: %r", context, type(meta))
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
log.debug("===== METADATA DUMP (%s) =====", context)
|
|
17
|
+
log.debug("keys: %s", ", ".join(sorted(meta.keys())))
|
|
18
|
+
|
|
19
|
+
for key, value in meta.items():
|
|
20
|
+
if isinstance(value, fits.Header):
|
|
21
|
+
log.debug("[MetaDump %s] %s -> fits.Header with %d cards", context, key, len(value))
|
|
22
|
+
# If you want the *full* header:
|
|
23
|
+
for card in value.cards:
|
|
24
|
+
log.debug("[MetaDump %s] %-10s = %r", context, card.keyword, card.value)
|
|
25
|
+
else:
|
|
26
|
+
log.debug("[MetaDump %s] %s -> %r (%s)",
|
|
27
|
+
context, key, value, type(value).__name__)
|
|
28
|
+
|
|
29
|
+
log.debug("===== END METADATA DUMP (%s) =====", context)
|