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,456 @@
|
|
|
1
|
+
# pro/whitebalance.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from typing import Dict, Tuple, Optional
|
|
6
|
+
|
|
7
|
+
from PyQt6.QtCore import Qt, QTimer
|
|
8
|
+
from PyQt6.QtGui import QIcon, QImage, QPixmap
|
|
9
|
+
from PyQt6.QtWidgets import (
|
|
10
|
+
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QWidget, QGroupBox,
|
|
11
|
+
QGridLayout, QSlider, QCheckBox, QPushButton, QMessageBox, QDoubleSpinBox
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# imageops
|
|
15
|
+
from setiastro.saspro.imageops.starbasedwhitebalance import apply_star_based_white_balance
|
|
16
|
+
from setiastro.saspro.imageops.stretch import stretch_color_image
|
|
17
|
+
|
|
18
|
+
# Shared utilities
|
|
19
|
+
from setiastro.saspro.widgets.image_utils import (
|
|
20
|
+
to_float01 as _to_float01,
|
|
21
|
+
extract_mask_from_document as _active_mask_array_from_doc
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from matplotlib import pyplot as plt # NEW
|
|
25
|
+
from matplotlib.patches import Circle # NEW
|
|
26
|
+
from matplotlib.ticker import MaxNLocator # NEW
|
|
27
|
+
# ----------------------------
|
|
28
|
+
# Core WB implementations
|
|
29
|
+
# ----------------------------
|
|
30
|
+
def plot_star_color_ratios_comparison(raw_pixels: np.ndarray, after_pixels: np.ndarray):
|
|
31
|
+
"""
|
|
32
|
+
Replicates the SASv2 diagnostic plot: star color ratios before/after WB,
|
|
33
|
+
with an RGB background grid, best-fit line, and neutral markers.
|
|
34
|
+
Expects Nx3 arrays of star RGB samples in [0,1] (or any common scale).
|
|
35
|
+
"""
|
|
36
|
+
def compute_ratios(pixels: np.ndarray):
|
|
37
|
+
eps = 1e-8
|
|
38
|
+
rb = pixels[:, 0] / (pixels[:, 2] + eps) # R/B
|
|
39
|
+
gb = pixels[:, 1] / (pixels[:, 2] + eps) # G/B
|
|
40
|
+
return rb, gb
|
|
41
|
+
|
|
42
|
+
rb_before, gb_before = compute_ratios(raw_pixels)
|
|
43
|
+
rb_after, gb_after = compute_ratios(after_pixels)
|
|
44
|
+
|
|
45
|
+
# Optional: keep only finite points
|
|
46
|
+
def _finite(x, y):
|
|
47
|
+
m = np.isfinite(x) & np.isfinite(y)
|
|
48
|
+
return x[m], y[m]
|
|
49
|
+
rb_before, gb_before = _finite(rb_before, gb_before)
|
|
50
|
+
rb_after, gb_after = _finite(rb_after, gb_after)
|
|
51
|
+
|
|
52
|
+
# Plot bounds + background grid
|
|
53
|
+
rmin, rmax = 0.5, 2.0
|
|
54
|
+
gmin, gmax = 0.5, 2.0
|
|
55
|
+
res = 200
|
|
56
|
+
|
|
57
|
+
rb_vals = np.linspace(rmin, rmax, res)
|
|
58
|
+
gb_vals = np.linspace(gmin, gmax, res)
|
|
59
|
+
rb_grid, gb_grid = np.meshgrid(rb_vals, gb_vals)
|
|
60
|
+
rgb_image = np.stack([rb_grid, gb_grid, np.ones_like(rb_grid)], axis=-1)
|
|
61
|
+
rgb_image /= np.maximum(rgb_image.max(axis=2, keepdims=True), 1e-8)
|
|
62
|
+
|
|
63
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 6), sharex=True, sharey=True)
|
|
64
|
+
|
|
65
|
+
def plot_panel(ax, rb_data, gb_data, title):
|
|
66
|
+
ax.imshow(rgb_image, extent=(rmin, rmax, gmin, gmax), origin='lower', aspect='auto')
|
|
67
|
+
ax.scatter(rb_data, gb_data, alpha=0.6, edgecolors='k', label="Stars")
|
|
68
|
+
|
|
69
|
+
if rb_data.size >= 2:
|
|
70
|
+
m, b = np.polyfit(rb_data, gb_data, 1)
|
|
71
|
+
xs = np.linspace(rmin, rmax, 100)
|
|
72
|
+
ax.plot(xs, m * xs + b, 'r--', label=f"Best Fit\ny = {m:.2f}x + {b:.2f}")
|
|
73
|
+
|
|
74
|
+
ax.axhline(1.0, color='gray', linestyle=':', linewidth=1)
|
|
75
|
+
ax.axvline(1.0, color='gray', linestyle=':', linewidth=1)
|
|
76
|
+
ax.add_patch(Circle((1.0, 1.0), 0.2, fill=False, edgecolor='blue', linestyle='--', linewidth=1.5))
|
|
77
|
+
ax.text(1.03, 1.17, "Neutral Region", color='blue', fontsize=9)
|
|
78
|
+
|
|
79
|
+
ax.set_xlim(rmin, rmax); ax.set_ylim(gmin, gmax)
|
|
80
|
+
ax.set_title(f"{title} White Balance")
|
|
81
|
+
ax.set_xlabel("Red / Blue Ratio"); ax.set_ylabel("Green / Blue Ratio")
|
|
82
|
+
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
|
|
83
|
+
ax.yaxis.set_major_locator(MaxNLocator(integer=True))
|
|
84
|
+
ax.grid(True); ax.legend()
|
|
85
|
+
|
|
86
|
+
plot_panel(ax1, rb_before, gb_before, "Before")
|
|
87
|
+
plot_panel(ax2, rb_after, gb_after, "After")
|
|
88
|
+
|
|
89
|
+
plt.suptitle("Star Color Ratios with RGB Mapping", fontsize=14)
|
|
90
|
+
plt.tight_layout()
|
|
91
|
+
plt.show()
|
|
92
|
+
|
|
93
|
+
def apply_manual_white_balance(img: np.ndarray, r_gain: float, g_gain: float, b_gain: float) -> np.ndarray:
|
|
94
|
+
"""Simple per-channel gain, clipped to [0,1]."""
|
|
95
|
+
if img.ndim != 3 or img.shape[2] != 3:
|
|
96
|
+
raise ValueError("Manual WB requires RGB image.")
|
|
97
|
+
out = _to_float01(img).copy()
|
|
98
|
+
gains = np.array([r_gain, g_gain, b_gain], dtype=np.float32).reshape((1, 1, 3))
|
|
99
|
+
out = np.clip(out * gains, 0.0, 1.0)
|
|
100
|
+
return out.astype(np.float32, copy=False)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def apply_auto_white_balance(img: np.ndarray) -> np.ndarray:
|
|
104
|
+
"""
|
|
105
|
+
Gray-world auto WB: scale each channel so its mean equals the overall mean.
|
|
106
|
+
"""
|
|
107
|
+
if img.ndim != 3 or img.shape[2] != 3:
|
|
108
|
+
raise ValueError("Auto WB requires RGB image.")
|
|
109
|
+
rgb = _to_float01(img)
|
|
110
|
+
means = np.mean(rgb, axis=(0, 1))
|
|
111
|
+
overall = float(np.mean(means))
|
|
112
|
+
means = np.where(means <= 1e-8, 1e-8, means)
|
|
113
|
+
scale = (overall / means).reshape((1, 1, 3))
|
|
114
|
+
out = np.clip(rgb * scale, 0.0, 1.0)
|
|
115
|
+
return out.astype(np.float32, copy=False)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
# --------------------------------
|
|
119
|
+
# Headless entry point for DnD
|
|
120
|
+
# --------------------------------
|
|
121
|
+
def apply_white_balance_to_doc(doc, preset: Optional[Dict] = None):
|
|
122
|
+
"""
|
|
123
|
+
Preset schema:
|
|
124
|
+
{
|
|
125
|
+
"mode": "star" | "manual" | "auto", # default "star"
|
|
126
|
+
# star mode:
|
|
127
|
+
"threshold": float (default 50),
|
|
128
|
+
"reuse_cached_sources": bool (default True),
|
|
129
|
+
# manual mode:
|
|
130
|
+
"r_gain": float (default 1.0), "g_gain": float (default 1.0), "b_gain": float (default 1.0)
|
|
131
|
+
}
|
|
132
|
+
"""
|
|
133
|
+
import numpy as np
|
|
134
|
+
|
|
135
|
+
p = dict(preset or {})
|
|
136
|
+
mode = (p.get("mode") or "star").lower()
|
|
137
|
+
|
|
138
|
+
base = np.asarray(doc.image).astype(np.float32, copy=False)
|
|
139
|
+
if base.ndim != 3 or base.shape[2] != 3:
|
|
140
|
+
raise ValueError("White Balance requires an RGB image.")
|
|
141
|
+
|
|
142
|
+
base_n = _to_float01(base)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
if mode == "manual":
|
|
146
|
+
r = float(p.get("r_gain", 1.0))
|
|
147
|
+
g = float(p.get("g_gain", 1.0))
|
|
148
|
+
b = float(p.get("b_gain", 1.0))
|
|
149
|
+
out = apply_manual_white_balance(base_n, r, g, b)
|
|
150
|
+
|
|
151
|
+
elif mode == "auto":
|
|
152
|
+
out = apply_auto_white_balance(base_n)
|
|
153
|
+
|
|
154
|
+
else: # "star"
|
|
155
|
+
thr = float(p.get("threshold", 50.0))
|
|
156
|
+
reuse = bool(p.get("reuse_cached_sources", True))
|
|
157
|
+
out, _count, _overlay = apply_star_based_white_balance(
|
|
158
|
+
base_n, threshold=thr, autostretch=False,
|
|
159
|
+
reuse_cached_sources=reuse, return_star_colors=False
|
|
160
|
+
)
|
|
161
|
+
except Exception as e:
|
|
162
|
+
# Fallback: if SEP missing or star detection fails, try Auto WB
|
|
163
|
+
if mode == "star":
|
|
164
|
+
try:
|
|
165
|
+
out = apply_auto_white_balance(base_n)
|
|
166
|
+
except Exception:
|
|
167
|
+
raise e
|
|
168
|
+
else:
|
|
169
|
+
raise
|
|
170
|
+
|
|
171
|
+
# Destination-mask blend (if any)
|
|
172
|
+
m = _active_mask_array_from_doc(doc)
|
|
173
|
+
if m is not None:
|
|
174
|
+
if out.ndim == 3:
|
|
175
|
+
m3 = np.repeat(m[..., None], 3, axis=2).astype(np.float32, copy=False)
|
|
176
|
+
else:
|
|
177
|
+
m3 = m.astype(np.float32, copy=False)
|
|
178
|
+
base_for_blend = _to_float01(np.asarray(doc.image).astype(np.float32, copy=False))
|
|
179
|
+
out = base_for_blend * (1.0 - m3) + out * m3
|
|
180
|
+
|
|
181
|
+
doc.apply_edit(
|
|
182
|
+
out.astype(np.float32, copy=False),
|
|
183
|
+
metadata={"step_name": "White Balance", "preset": p},
|
|
184
|
+
step_name="White Balance",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# -------------------------
|
|
189
|
+
# Interactive dialog (UI)
|
|
190
|
+
# -------------------------
|
|
191
|
+
class WhiteBalanceDialog(QDialog):
|
|
192
|
+
def __init__(self, parent, doc, icon: QIcon | None = None):
|
|
193
|
+
super().__init__(parent)
|
|
194
|
+
self.doc = doc
|
|
195
|
+
if icon:
|
|
196
|
+
self.setWindowIcon(icon)
|
|
197
|
+
self.setWindowTitle("White Balance")
|
|
198
|
+
self.resize(900, 600)
|
|
199
|
+
|
|
200
|
+
self._build_ui()
|
|
201
|
+
self._wire_events()
|
|
202
|
+
|
|
203
|
+
# default to Star-Based, like SASv2
|
|
204
|
+
self.type_combo.setCurrentText("Star-Based")
|
|
205
|
+
self._update_mode_widgets()
|
|
206
|
+
# kick off a first detection preview
|
|
207
|
+
QTimer.singleShot(200, self._update_star_preview)
|
|
208
|
+
|
|
209
|
+
# ---- UI construction ------------------------------------------------
|
|
210
|
+
def _build_ui(self):
|
|
211
|
+
self.main_layout = QVBoxLayout(self)
|
|
212
|
+
|
|
213
|
+
# Type selector
|
|
214
|
+
row = QHBoxLayout()
|
|
215
|
+
row.addWidget(QLabel("White Balance Type:"))
|
|
216
|
+
self.type_combo = QComboBox()
|
|
217
|
+
self.type_combo.addItems(["Star-Based", "Manual", "Auto"])
|
|
218
|
+
row.addWidget(self.type_combo); row.addStretch()
|
|
219
|
+
self.main_layout.addLayout(row)
|
|
220
|
+
|
|
221
|
+
# Manual group
|
|
222
|
+
self.manual_group = QGroupBox("Manual Gains")
|
|
223
|
+
g = QGridLayout(self.manual_group)
|
|
224
|
+
self.r_spin = QDoubleSpinBox(); self._cfg_gain(self.r_spin, 1.0)
|
|
225
|
+
self.g_spin = QDoubleSpinBox(); self._cfg_gain(self.g_spin, 1.0)
|
|
226
|
+
self.b_spin = QDoubleSpinBox(); self._cfg_gain(self.b_spin, 1.0)
|
|
227
|
+
g.addWidget(QLabel("Red gain:"), 0, 0); g.addWidget(self.r_spin, 0, 1)
|
|
228
|
+
g.addWidget(QLabel("Green gain:"), 1, 0); g.addWidget(self.g_spin, 1, 1)
|
|
229
|
+
g.addWidget(QLabel("Blue gain:"), 2, 0); g.addWidget(self.b_spin, 2, 1)
|
|
230
|
+
self.main_layout.addWidget(self.manual_group)
|
|
231
|
+
|
|
232
|
+
# Star-based controls + preview
|
|
233
|
+
self.star_group = QGroupBox("Star-Based Settings")
|
|
234
|
+
sg = QVBoxLayout(self.star_group)
|
|
235
|
+
# threshold slider
|
|
236
|
+
thr_row = QHBoxLayout()
|
|
237
|
+
thr_row.addWidget(QLabel("Detection threshold (σ):"))
|
|
238
|
+
self.thr_slider = QSlider(Qt.Orientation.Horizontal)
|
|
239
|
+
self.thr_slider.setMinimum(1); self.thr_slider.setMaximum(100)
|
|
240
|
+
self.thr_slider.setValue(50); self.thr_slider.setTickInterval(10)
|
|
241
|
+
self.thr_slider.setTickPosition(QSlider.TickPosition.TicksBelow)
|
|
242
|
+
self.thr_label = QLabel("50")
|
|
243
|
+
thr_row.addWidget(self.thr_slider); thr_row.addWidget(self.thr_label)
|
|
244
|
+
sg.addLayout(thr_row)
|
|
245
|
+
|
|
246
|
+
self.chk_reuse = QCheckBox("Reuse cached star detections"); self.chk_reuse.setChecked(True)
|
|
247
|
+
sg.addWidget(self.chk_reuse)
|
|
248
|
+
|
|
249
|
+
self.chk_autostretch_overlay = QCheckBox("Autostretch overlay preview"); self.chk_autostretch_overlay.setChecked(True)
|
|
250
|
+
sg.addWidget(self.chk_autostretch_overlay)
|
|
251
|
+
|
|
252
|
+
# star count + image preview
|
|
253
|
+
self.star_count = QLabel("Detecting stars…")
|
|
254
|
+
sg.addWidget(self.star_count)
|
|
255
|
+
|
|
256
|
+
self.preview = QLabel(); self.preview.setMinimumSize(640, 360)
|
|
257
|
+
self.preview.setStyleSheet("border: 1px solid #333;")
|
|
258
|
+
sg.addWidget(self.preview)
|
|
259
|
+
self.main_layout.addWidget(self.star_group)
|
|
260
|
+
|
|
261
|
+
# Buttons
|
|
262
|
+
btn_row = QHBoxLayout()
|
|
263
|
+
btn_row.addStretch()
|
|
264
|
+
self.btn_apply = QPushButton("Apply")
|
|
265
|
+
self.btn_cancel = QPushButton("Cancel")
|
|
266
|
+
btn_row.addWidget(self.btn_apply)
|
|
267
|
+
btn_row.addWidget(self.btn_cancel)
|
|
268
|
+
self.main_layout.addLayout(btn_row)
|
|
269
|
+
|
|
270
|
+
self.setLayout(self.main_layout)
|
|
271
|
+
|
|
272
|
+
# debounce timer for star preview
|
|
273
|
+
self._debounce = QTimer(self); self._debounce.setSingleShot(True); self._debounce.setInterval(600)
|
|
274
|
+
self._debounce.timeout.connect(self._update_star_preview)
|
|
275
|
+
|
|
276
|
+
def _cfg_gain(self, box: QDoubleSpinBox, val: float):
|
|
277
|
+
box.setRange(0.5, 1.5); box.setDecimals(3); box.setSingleStep(0.01); box.setValue(val)
|
|
278
|
+
|
|
279
|
+
# ---- events ---------------------------------------------------------
|
|
280
|
+
def _wire_events(self):
|
|
281
|
+
self.type_combo.currentTextChanged.connect(self._update_mode_widgets)
|
|
282
|
+
self.btn_cancel.clicked.connect(self.reject)
|
|
283
|
+
self.btn_apply.clicked.connect(self._on_apply)
|
|
284
|
+
|
|
285
|
+
self.thr_slider.valueChanged.connect(lambda v: (self.thr_label.setText(str(v)), self._debounce.start()))
|
|
286
|
+
self.chk_autostretch_overlay.toggled.connect(lambda _=None: self._debounce.start())
|
|
287
|
+
self.chk_reuse.toggled.connect(lambda _=None: self._debounce.start())
|
|
288
|
+
|
|
289
|
+
def _update_mode_widgets(self):
|
|
290
|
+
t = self.type_combo.currentText()
|
|
291
|
+
self.manual_group.setVisible(t == "Manual")
|
|
292
|
+
self.star_group.setVisible(t == "Star-Based")
|
|
293
|
+
|
|
294
|
+
# ---- preview --------------------------------------------------------
|
|
295
|
+
def _update_star_preview(self):
|
|
296
|
+
if self.type_combo.currentText() != "Star-Based":
|
|
297
|
+
return
|
|
298
|
+
try:
|
|
299
|
+
img = _to_float01(np.asarray(self.doc.image))
|
|
300
|
+
thr = float(self.thr_slider.value())
|
|
301
|
+
reuse = bool(self.chk_reuse.isChecked())
|
|
302
|
+
auto = bool(self.chk_autostretch_overlay.isChecked())
|
|
303
|
+
|
|
304
|
+
_, count, overlay = apply_star_based_white_balance(
|
|
305
|
+
img, threshold=thr, autostretch=auto,
|
|
306
|
+
reuse_cached_sources=reuse, return_star_colors=False
|
|
307
|
+
)
|
|
308
|
+
self.star_count.setText(f"Detected {count} stars.")
|
|
309
|
+
# to pixmap
|
|
310
|
+
h, w, _ = overlay.shape
|
|
311
|
+
qimg = QImage((overlay * 255).astype(np.uint8).data, w, h, 3 * w, QImage.Format.Format_RGB888)
|
|
312
|
+
pm = QPixmap.fromImage(qimg).scaled(self.preview.width(), self.preview.height(),
|
|
313
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
314
|
+
Qt.TransformationMode.SmoothTransformation)
|
|
315
|
+
self.preview.setPixmap(pm)
|
|
316
|
+
except Exception as e:
|
|
317
|
+
self.star_count.setText("Detection failed.")
|
|
318
|
+
self.preview.clear()
|
|
319
|
+
|
|
320
|
+
# ---- apply ----------------------------------------------------------
|
|
321
|
+
def _on_apply(self):
|
|
322
|
+
mode = self.type_combo.currentText()
|
|
323
|
+
|
|
324
|
+
# Find the main window that carries the replay machinery
|
|
325
|
+
main = self.parent()
|
|
326
|
+
while main is not None and not hasattr(main, "replay_last_action_on_base"):
|
|
327
|
+
main = main.parent()
|
|
328
|
+
|
|
329
|
+
def _record_preset_for_replay(preset: dict):
|
|
330
|
+
"""Helper to stash the WB preset on the main window for Replay Last Action."""
|
|
331
|
+
if main is None:
|
|
332
|
+
return
|
|
333
|
+
try:
|
|
334
|
+
main._last_headless_command = {
|
|
335
|
+
"command_id": "white_balance",
|
|
336
|
+
"preset": preset,
|
|
337
|
+
}
|
|
338
|
+
if hasattr(main, "_log"):
|
|
339
|
+
mode_str = str(preset.get("mode", "star")).lower()
|
|
340
|
+
if mode_str == "manual":
|
|
341
|
+
r = float(preset.get("r_gain", 1.0))
|
|
342
|
+
g = float(preset.get("g_gain", 1.0))
|
|
343
|
+
b = float(preset.get("b_gain", 1.0))
|
|
344
|
+
main._log(
|
|
345
|
+
f"[Replay] Recorded White Balance preset "
|
|
346
|
+
f"(mode=manual, R={r:.3f}, G={g:.3f}, B={b:.3f})"
|
|
347
|
+
)
|
|
348
|
+
elif mode_str == "auto":
|
|
349
|
+
main._log("[Replay] Recorded White Balance preset (mode=auto)")
|
|
350
|
+
else:
|
|
351
|
+
thr = float(preset.get("threshold", 50.0))
|
|
352
|
+
reuse = bool(preset.get("reuse_cached_sources", True))
|
|
353
|
+
main._log(
|
|
354
|
+
f"[Replay] Recorded White Balance preset "
|
|
355
|
+
f"(mode=star, threshold={thr:.1f}, "
|
|
356
|
+
f"reuse={'yes' if reuse else 'no'})"
|
|
357
|
+
)
|
|
358
|
+
except Exception:
|
|
359
|
+
# Logging/recording must never break the dialog
|
|
360
|
+
pass
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
if mode == "Manual":
|
|
364
|
+
preset = {
|
|
365
|
+
"mode": "manual",
|
|
366
|
+
"r_gain": float(self.r_spin.value()),
|
|
367
|
+
"g_gain": float(self.g_spin.value()),
|
|
368
|
+
"b_gain": float(self.b_spin.value()),
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
# Record for Replay Last Action
|
|
372
|
+
_record_preset_for_replay(preset)
|
|
373
|
+
|
|
374
|
+
# Use the headless helper so doc metadata is consistent
|
|
375
|
+
apply_white_balance_to_doc(self.doc, preset)
|
|
376
|
+
self.accept()
|
|
377
|
+
|
|
378
|
+
elif mode == "Auto":
|
|
379
|
+
preset = {"mode": "auto"}
|
|
380
|
+
|
|
381
|
+
# Record for Replay Last Action
|
|
382
|
+
_record_preset_for_replay(preset)
|
|
383
|
+
|
|
384
|
+
apply_white_balance_to_doc(self.doc, preset)
|
|
385
|
+
self.accept()
|
|
386
|
+
|
|
387
|
+
else: # --- Star-Based: compute here so we can plot like SASv2 ---
|
|
388
|
+
thr = float(self.thr_slider.value())
|
|
389
|
+
reuse = bool(self.chk_reuse.isChecked())
|
|
390
|
+
|
|
391
|
+
preset = {
|
|
392
|
+
"mode": "star",
|
|
393
|
+
"threshold": thr,
|
|
394
|
+
"reuse_cached_sources": reuse,
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
base = _to_float01(
|
|
398
|
+
np.asarray(self.doc.image).astype(np.float32, copy=False)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
# Ask for star colors so we can plot
|
|
402
|
+
result = apply_star_based_white_balance(
|
|
403
|
+
base,
|
|
404
|
+
threshold=thr,
|
|
405
|
+
autostretch=False,
|
|
406
|
+
reuse_cached_sources=reuse,
|
|
407
|
+
return_star_colors=True,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
# Expected: (out, count, overlay, raw_colors, after_colors)
|
|
411
|
+
if len(result) < 5:
|
|
412
|
+
raise RuntimeError(
|
|
413
|
+
"Star-based WB did not return star color arrays. "
|
|
414
|
+
"Ensure return_star_colors=True is supported."
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
out, star_count, _overlay, raw_colors, after_colors = result
|
|
418
|
+
|
|
419
|
+
# Optional destination-mask blend, same as headless path
|
|
420
|
+
m = _active_mask_array_from_doc(self.doc)
|
|
421
|
+
if m is not None:
|
|
422
|
+
m3 = np.repeat(m[..., None], 3, axis=2).astype(np.float32, copy=False)
|
|
423
|
+
base_for_blend = _to_float01(
|
|
424
|
+
np.asarray(self.doc.image).astype(np.float32, copy=False)
|
|
425
|
+
)
|
|
426
|
+
out = base_for_blend * (1.0 - m3) + out * m3
|
|
427
|
+
|
|
428
|
+
# Commit to the document, including the preset in metadata
|
|
429
|
+
self.doc.apply_edit(
|
|
430
|
+
out.astype(np.float32, copy=False),
|
|
431
|
+
metadata={"step_name": "White Balance", "preset": preset},
|
|
432
|
+
step_name="White Balance",
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# Record for Replay Last Action (using the same preset we just stored in metadata)
|
|
436
|
+
_record_preset_for_replay(preset)
|
|
437
|
+
|
|
438
|
+
# 🔬 Show the same diagnostic plot SASv2 shows
|
|
439
|
+
if (
|
|
440
|
+
raw_colors is not None
|
|
441
|
+
and after_colors is not None
|
|
442
|
+
and raw_colors.size
|
|
443
|
+
and after_colors.size
|
|
444
|
+
):
|
|
445
|
+
plot_star_color_ratios_comparison(raw_colors, after_colors)
|
|
446
|
+
|
|
447
|
+
QMessageBox.information(
|
|
448
|
+
self,
|
|
449
|
+
"White Balance",
|
|
450
|
+
f"Star-Based WB applied.\nDetected {int(star_count)} stars.",
|
|
451
|
+
)
|
|
452
|
+
self.accept()
|
|
453
|
+
|
|
454
|
+
except Exception as e:
|
|
455
|
+
QMessageBox.critical(self, "White Balance", f"Failed to apply White Balance:\n{e}")
|
|
456
|
+
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# pro/widgets/__init__.py
|
|
2
|
+
"""
|
|
3
|
+
Shared UI widgets for Seti Astro Suite Pro.
|
|
4
|
+
|
|
5
|
+
This package contains reusable UI components to avoid code duplication.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from setiastro.saspro.widgets.spinboxes import CustomSpinBox, CustomDoubleSpinBox
|
|
9
|
+
from setiastro.saspro.widgets.graphics_views import ZoomableGraphicsView
|
|
10
|
+
from setiastro.saspro.widgets.preview_dialogs import ImagePreviewDialog
|
|
11
|
+
from setiastro.saspro.widgets.image_utils import (
|
|
12
|
+
numpy_to_qimage,
|
|
13
|
+
numpy_to_qpixmap,
|
|
14
|
+
qimage_to_numpy,
|
|
15
|
+
create_preview_image,
|
|
16
|
+
normalize_image
|
|
17
|
+
)
|
|
18
|
+
from setiastro.saspro.widgets.common_utilities import (
|
|
19
|
+
AboutDialog,
|
|
20
|
+
ProjectSaveWorker,
|
|
21
|
+
DECOR_GLYPHS,
|
|
22
|
+
strip_ui_decorations,
|
|
23
|
+
_strip_ui_decorations,
|
|
24
|
+
install_crash_handlers,
|
|
25
|
+
get_version,
|
|
26
|
+
get_build_timestamp,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
'CustomSpinBox',
|
|
31
|
+
'CustomDoubleSpinBox',
|
|
32
|
+
'ZoomableGraphicsView',
|
|
33
|
+
'ImagePreviewDialog',
|
|
34
|
+
'numpy_to_qimage',
|
|
35
|
+
'numpy_to_qpixmap',
|
|
36
|
+
'qimage_to_numpy',
|
|
37
|
+
'create_preview_image',
|
|
38
|
+
'normalize_image',
|
|
39
|
+
# Common utilities
|
|
40
|
+
'AboutDialog',
|
|
41
|
+
'ProjectSaveWorker',
|
|
42
|
+
'DECOR_GLYPHS',
|
|
43
|
+
'strip_ui_decorations',
|
|
44
|
+
'_strip_ui_decorations',
|
|
45
|
+
'install_crash_handlers',
|
|
46
|
+
'get_version',
|
|
47
|
+
'get_build_timestamp',
|
|
48
|
+
]
|