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,299 @@
|
|
|
1
|
+
# pro/widgets/wavelet_utils.py
|
|
2
|
+
"""
|
|
3
|
+
Shared wavelet utilities for à-trous decomposition and reconstruction.
|
|
4
|
+
|
|
5
|
+
This module provides centralized implementations for wavelet operations
|
|
6
|
+
used across wavescale_hdr.py, wavescalede.py, and other modules.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
13
|
+
# Convolution helpers (SciPy if available; otherwise a separable fallback)
|
|
14
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
from scipy.ndimage import convolve as _nd_convolve
|
|
18
|
+
from scipy.ndimage import gaussian_filter as _nd_gauss
|
|
19
|
+
_HAVE_SCIPY = True
|
|
20
|
+
except ImportError:
|
|
21
|
+
_HAVE_SCIPY = False
|
|
22
|
+
_nd_convolve = None
|
|
23
|
+
_nd_gauss = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def conv_sep_reflect(image2d: np.ndarray, k1d: np.ndarray, axis: int) -> np.ndarray:
|
|
27
|
+
"""
|
|
28
|
+
Separable 1D convolution along a given axis with reflect padding.
|
|
29
|
+
|
|
30
|
+
Uses scipy.ndimage.convolve if available, otherwise falls back to numpy.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
image2d: 2D input array
|
|
34
|
+
k1d: 1D kernel
|
|
35
|
+
axis: 0 for vertical (y), 1 for horizontal (x)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Convolved array, same shape as input
|
|
39
|
+
"""
|
|
40
|
+
if _HAVE_SCIPY:
|
|
41
|
+
if axis == 1: # x
|
|
42
|
+
return _nd_convolve(image2d, k1d.reshape(1, -1), mode="reflect")
|
|
43
|
+
else: # y
|
|
44
|
+
return _nd_convolve(image2d, k1d.reshape(-1, 1), mode="reflect")
|
|
45
|
+
else:
|
|
46
|
+
# Fallback numpy implementation
|
|
47
|
+
image2d = np.asarray(image2d, dtype=np.float32)
|
|
48
|
+
k1d = np.asarray(k1d, dtype=np.float32)
|
|
49
|
+
r = len(k1d) // 2
|
|
50
|
+
if axis == 1: # horizontal
|
|
51
|
+
pad = np.pad(image2d, ((0, 0), (r, r)), mode="reflect")
|
|
52
|
+
out = np.empty_like(image2d, dtype=np.float32)
|
|
53
|
+
for i in range(image2d.shape[0]):
|
|
54
|
+
out[i] = np.convolve(pad[i], k1d, mode="valid")
|
|
55
|
+
return out
|
|
56
|
+
else: # vertical
|
|
57
|
+
pad = np.pad(image2d, ((r, r), (0, 0)), mode="reflect")
|
|
58
|
+
out = np.empty_like(image2d, dtype=np.float32)
|
|
59
|
+
for j in range(image2d.shape[1]):
|
|
60
|
+
out[:, j] = np.convolve(pad[:, j], k1d, mode="valid")
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def gauss1d(sigma: float) -> np.ndarray:
|
|
65
|
+
"""
|
|
66
|
+
Create a 1D Gaussian kernel.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
sigma: Standard deviation of the Gaussian
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Normalized 1D Gaussian kernel
|
|
73
|
+
"""
|
|
74
|
+
if sigma <= 0:
|
|
75
|
+
return np.array([1.0], dtype=np.float32)
|
|
76
|
+
radius = max(1, int(round(3.0 * sigma)))
|
|
77
|
+
x = np.arange(-radius, radius + 1, dtype=np.float32)
|
|
78
|
+
k = np.exp(-0.5 * (x / sigma)**2)
|
|
79
|
+
k /= np.sum(k)
|
|
80
|
+
return k.astype(np.float32)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def gauss_blur(image2d: np.ndarray, sigma: float) -> np.ndarray:
|
|
84
|
+
"""
|
|
85
|
+
Apply Gaussian blur to a 2D image.
|
|
86
|
+
|
|
87
|
+
Uses scipy.ndimage.gaussian_filter if available, otherwise separable convolution.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
image2d: 2D input array
|
|
91
|
+
sigma: Standard deviation of the Gaussian
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Blurred array
|
|
95
|
+
"""
|
|
96
|
+
if _HAVE_SCIPY and _nd_gauss is not None:
|
|
97
|
+
return _nd_gauss(image2d, sigma=sigma, mode="reflect")
|
|
98
|
+
else:
|
|
99
|
+
k = gauss1d(float(sigma))
|
|
100
|
+
tmp = conv_sep_reflect(image2d, k, axis=1)
|
|
101
|
+
return conv_sep_reflect(tmp, k, axis=0)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
# À-trous wavelet transform (B3 spline kernel)
|
|
106
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
# Standard B3-spline kernel for à-trous
|
|
109
|
+
B3_KERNEL = np.array([1, 4, 6, 4, 1], dtype=np.float32) / 16.0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def build_spaced_kernel(kernel: np.ndarray, scale_idx: int) -> np.ndarray:
|
|
113
|
+
"""
|
|
114
|
+
Build a spaced (à-trous) kernel for a given scale.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
kernel: Base 1D kernel
|
|
118
|
+
scale_idx: Scale index (0 = no spacing, 1 = step 2, 2 = step 4, etc.)
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Spaced kernel with zeros inserted
|
|
122
|
+
"""
|
|
123
|
+
if scale_idx == 0:
|
|
124
|
+
return kernel.astype(np.float32, copy=False)
|
|
125
|
+
step = 2 ** scale_idx
|
|
126
|
+
spaced_len = len(kernel) + (len(kernel) - 1) * (step - 1)
|
|
127
|
+
spaced = np.zeros(spaced_len, dtype=np.float32)
|
|
128
|
+
spaced[0::step] = kernel
|
|
129
|
+
return spaced
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def atrous_decompose(img2d: np.ndarray, n_scales: int,
|
|
133
|
+
base_kernel: np.ndarray | None = None) -> list[np.ndarray]:
|
|
134
|
+
"""
|
|
135
|
+
Perform à-trous (undecimated) wavelet decomposition.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
img2d: 2D input image
|
|
139
|
+
n_scales: Number of detail scales to extract
|
|
140
|
+
base_kernel: Base kernel (default: B3 spline)
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
List of [detail_0, detail_1, ..., detail_n-1, residual]
|
|
144
|
+
where detail_i is the wavelet plane at scale i
|
|
145
|
+
"""
|
|
146
|
+
if base_kernel is None:
|
|
147
|
+
base_kernel = B3_KERNEL
|
|
148
|
+
|
|
149
|
+
current = img2d.astype(np.float32, copy=True)
|
|
150
|
+
planes: list[np.ndarray] = []
|
|
151
|
+
|
|
152
|
+
for s in range(n_scales):
|
|
153
|
+
k = build_spaced_kernel(base_kernel, s)
|
|
154
|
+
tmp = conv_sep_reflect(current, k, axis=1)
|
|
155
|
+
smooth = conv_sep_reflect(tmp, k, axis=0)
|
|
156
|
+
planes.append(current - smooth) # detail = current - smoothed
|
|
157
|
+
current = smooth
|
|
158
|
+
|
|
159
|
+
planes.append(current) # residual (lowest frequency)
|
|
160
|
+
return planes
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def atrous_reconstruct(planes: list[np.ndarray]) -> np.ndarray:
|
|
164
|
+
"""
|
|
165
|
+
Reconstruct image from à-trous wavelet planes.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
planes: List of [detail_0, ..., detail_n-1, residual]
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
Reconstructed image
|
|
172
|
+
"""
|
|
173
|
+
out = planes[-1].astype(np.float32, copy=True) # start with residual
|
|
174
|
+
for detail in planes[:-1]:
|
|
175
|
+
out += detail
|
|
176
|
+
return out
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
180
|
+
# Color space utilities (with optional Numba acceleration)
|
|
181
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
from setiastro.saspro.legacy.numba_utils import (
|
|
185
|
+
rgb_to_xyz_numba, xyz_to_lab_numba,
|
|
186
|
+
lab_to_xyz_numba, xyz_to_rgb_numba,
|
|
187
|
+
)
|
|
188
|
+
_HAVE_NUMBA = True
|
|
189
|
+
except ImportError:
|
|
190
|
+
_HAVE_NUMBA = False
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
# sRGB -> XYZ transformation matrix
|
|
194
|
+
_RGB_TO_XYZ_MATRIX = np.array([
|
|
195
|
+
[0.4124564, 0.3575761, 0.1804375],
|
|
196
|
+
[0.2126729, 0.7151522, 0.0721750],
|
|
197
|
+
[0.0193339, 0.1191920, 0.9503041]
|
|
198
|
+
], dtype=np.float32)
|
|
199
|
+
|
|
200
|
+
# XYZ -> sRGB transformation matrix (inverse)
|
|
201
|
+
_XYZ_TO_RGB_MATRIX = np.array([
|
|
202
|
+
[ 3.2404542, -1.5371385, -0.4985314],
|
|
203
|
+
[-0.9692660, 1.8760108, 0.0415560],
|
|
204
|
+
[ 0.0556434, -0.2040259, 1.0572252]
|
|
205
|
+
], dtype=np.float32)
|
|
206
|
+
|
|
207
|
+
# D65 illuminant reference white
|
|
208
|
+
_D65_WHITE = np.array([0.95047, 1.0, 1.08883], dtype=np.float32)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def rgb_to_lab(rgb: np.ndarray) -> np.ndarray:
|
|
212
|
+
"""
|
|
213
|
+
Convert sRGB image to CIE L*a*b* color space.
|
|
214
|
+
|
|
215
|
+
Uses Numba-accelerated version if available.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
rgb: RGB image (H, W, 3) float32 in [0, 1]
|
|
219
|
+
|
|
220
|
+
Returns:
|
|
221
|
+
Lab image (H, W, 3) where L is [0, 100], a/b are roughly [-128, 127]
|
|
222
|
+
"""
|
|
223
|
+
if _HAVE_NUMBA:
|
|
224
|
+
rgb32 = np.ascontiguousarray(rgb.astype(np.float32))
|
|
225
|
+
xyz = rgb_to_xyz_numba(rgb32)
|
|
226
|
+
lab = xyz_to_lab_numba(xyz)
|
|
227
|
+
return lab
|
|
228
|
+
|
|
229
|
+
# Numpy fallback
|
|
230
|
+
rgb = np.asarray(rgb, dtype=np.float32)
|
|
231
|
+
|
|
232
|
+
# sRGB gamma linearization
|
|
233
|
+
linear = np.where(rgb > 0.04045,
|
|
234
|
+
np.power((rgb + 0.055) / 1.055, 2.4),
|
|
235
|
+
rgb / 12.92)
|
|
236
|
+
|
|
237
|
+
# RGB -> XYZ
|
|
238
|
+
xyz = np.einsum('ij,...j->...i', _RGB_TO_XYZ_MATRIX, linear)
|
|
239
|
+
|
|
240
|
+
# XYZ -> Lab
|
|
241
|
+
xyz_n = xyz / _D65_WHITE
|
|
242
|
+
|
|
243
|
+
def f(t):
|
|
244
|
+
return np.where(t > 0.008856,
|
|
245
|
+
np.power(t, 1/3),
|
|
246
|
+
7.787 * t + 16/116)
|
|
247
|
+
|
|
248
|
+
fx, fy, fz = f(xyz_n[..., 0]), f(xyz_n[..., 1]), f(xyz_n[..., 2])
|
|
249
|
+
|
|
250
|
+
L = 116 * fy - 16
|
|
251
|
+
a = 500 * (fx - fy)
|
|
252
|
+
b = 200 * (fy - fz)
|
|
253
|
+
|
|
254
|
+
return np.stack([L, a, b], axis=-1).astype(np.float32)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def lab_to_rgb(lab: np.ndarray) -> np.ndarray:
|
|
258
|
+
"""
|
|
259
|
+
Convert CIE L*a*b* image to sRGB color space.
|
|
260
|
+
|
|
261
|
+
Uses Numba-accelerated version if available.
|
|
262
|
+
|
|
263
|
+
Args:
|
|
264
|
+
lab: Lab image (H, W, 3)
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
RGB image (H, W, 3) float32 in [0, 1]
|
|
268
|
+
"""
|
|
269
|
+
if _HAVE_NUMBA:
|
|
270
|
+
lab32 = np.ascontiguousarray(lab.astype(np.float32))
|
|
271
|
+
xyz = lab_to_xyz_numba(lab32)
|
|
272
|
+
rgb = xyz_to_rgb_numba(xyz)
|
|
273
|
+
return np.clip(rgb, 0.0, 1.0)
|
|
274
|
+
|
|
275
|
+
# Numpy fallback
|
|
276
|
+
lab = np.asarray(lab, dtype=np.float32)
|
|
277
|
+
L, a, b = lab[..., 0], lab[..., 1], lab[..., 2]
|
|
278
|
+
|
|
279
|
+
# Lab -> XYZ
|
|
280
|
+
fy = (L + 16) / 116
|
|
281
|
+
fx = a / 500 + fy
|
|
282
|
+
fz = fy - b / 200
|
|
283
|
+
|
|
284
|
+
def f_inv(t):
|
|
285
|
+
return np.where(t > 0.206893,
|
|
286
|
+
np.power(t, 3),
|
|
287
|
+
(t - 16/116) / 7.787)
|
|
288
|
+
|
|
289
|
+
xyz = np.stack([f_inv(fx), f_inv(fy), f_inv(fz)], axis=-1) * _D65_WHITE
|
|
290
|
+
|
|
291
|
+
# XYZ -> linear RGB
|
|
292
|
+
linear = np.einsum('ij,...j->...i', _XYZ_TO_RGB_MATRIX, xyz)
|
|
293
|
+
|
|
294
|
+
# sRGB gamma correction
|
|
295
|
+
rgb = np.where(linear > 0.0031308,
|
|
296
|
+
1.055 * np.power(np.maximum(linear, 0), 1/2.4) - 0.055,
|
|
297
|
+
12.92 * linear)
|
|
298
|
+
|
|
299
|
+
return np.clip(rgb, 0.0, 1.0).astype(np.float32)
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# pro/window_shelf.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
from PyQt6.QtCore import Qt, QEvent, QTimer, QSize, QObject, QRect
|
|
4
|
+
from PyQt6.QtWidgets import QDockWidget, QListWidget, QListWidgetItem, QMdiSubWindow, QWidget
|
|
5
|
+
from PyQt6.QtGui import QIcon, QPixmap
|
|
6
|
+
|
|
7
|
+
WINDOW_SHELF_DEBUG = False # flip to True to log capture/restore details
|
|
8
|
+
|
|
9
|
+
def _dbg(owner, msg: str):
|
|
10
|
+
if not WINDOW_SHELF_DEBUG:
|
|
11
|
+
return
|
|
12
|
+
p = owner.parent()
|
|
13
|
+
if p and hasattr(p, "_log") and callable(getattr(p, "_log")):
|
|
14
|
+
p._log(f"[Shelf] {msg}")
|
|
15
|
+
else:
|
|
16
|
+
print(f"[Shelf] {msg}")
|
|
17
|
+
|
|
18
|
+
class WindowShelf(QDockWidget):
|
|
19
|
+
def __init__(self, parent=None):
|
|
20
|
+
super().__init__("Minimized Views", parent)
|
|
21
|
+
|
|
22
|
+
# PyQt6 dock area enum
|
|
23
|
+
self.setAllowedAreas(Qt.DockWidgetArea.AllDockWidgetAreas)
|
|
24
|
+
|
|
25
|
+
self.list = QListWidget(self)
|
|
26
|
+
self.list.setUniformItemSizes(False)
|
|
27
|
+
self.list.setIconSize(QSize(32, 32))
|
|
28
|
+
self.setWidget(self.list)
|
|
29
|
+
|
|
30
|
+
# map: QListWidgetItem -> subwindow
|
|
31
|
+
# use item id() as key because QListWidgetItem is unhashable
|
|
32
|
+
self._item2sub: dict[int, QMdiSubWindow] = {}
|
|
33
|
+
# map: subwindow -> {"geom": QRect, "max": bool}
|
|
34
|
+
self._saved_state: dict[QMdiSubWindow, dict] = {}
|
|
35
|
+
|
|
36
|
+
self.list.itemClicked.connect(self._restore)
|
|
37
|
+
|
|
38
|
+
# ---- public API used by the interceptor ----
|
|
39
|
+
def pre_capture_state(self, sub: QMdiSubWindow):
|
|
40
|
+
"""Capture normal geometry/max state BEFORE we hide/minimize."""
|
|
41
|
+
if not sub:
|
|
42
|
+
return
|
|
43
|
+
was_max = sub.isMaximized()
|
|
44
|
+
# If was maximized, normalGeometry() holds the pre-max rect; otherwise use geometry()
|
|
45
|
+
g = sub.normalGeometry() if was_max else sub.geometry()
|
|
46
|
+
if not g.isValid():
|
|
47
|
+
g = sub.geometry()
|
|
48
|
+
self._saved_state[sub] = {"geom": QRect(g), "max": bool(was_max)}
|
|
49
|
+
_dbg(self, f"CAPTURE for '{sub.windowTitle()}': max={was_max}, geom={g}")
|
|
50
|
+
|
|
51
|
+
def add_entry(self, sub: QMdiSubWindow):
|
|
52
|
+
"""Add a button to the shelf for `sub` (state must be pre-captured)."""
|
|
53
|
+
if sub is None or sub.widget() is None:
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
title = sub.windowTitle() or "Untitled"
|
|
57
|
+
# strip leading dot and Active prefix for the shelf display text only
|
|
58
|
+
|
|
59
|
+
# Remove any number of leading glyphs like ■ ● ◆ ▲ etc.
|
|
60
|
+
while len(title) >= 2 and title[1] == " " and title[0] in "■●◆▲▪▫•◼◻◾◽":
|
|
61
|
+
title = title[2:]
|
|
62
|
+
|
|
63
|
+
# Remove leading 'Active View: ' if present
|
|
64
|
+
if title.startswith("Active View: "):
|
|
65
|
+
title = title[len("Active View: "):]
|
|
66
|
+
|
|
67
|
+
# Best-effort thumbnail from the view's QLabel (if present)
|
|
68
|
+
icon = QIcon()
|
|
69
|
+
w = sub.widget()
|
|
70
|
+
pm = getattr(getattr(w, "label", None), "pixmap", lambda: None)()
|
|
71
|
+
if isinstance(pm, QPixmap) and not pm.isNull():
|
|
72
|
+
icon = QIcon(pm.scaled(
|
|
73
|
+
64, 64,
|
|
74
|
+
Qt.AspectRatioMode.KeepAspectRatio,
|
|
75
|
+
Qt.TransformationMode.SmoothTransformation
|
|
76
|
+
))
|
|
77
|
+
else:
|
|
78
|
+
icon = QIcon.fromTheme("image-x-generic")
|
|
79
|
+
|
|
80
|
+
item = QListWidgetItem(icon, title)
|
|
81
|
+
# store the subwindow via item data (so QListWidgetItem doesn't have to be a dict key)
|
|
82
|
+
item.setData(Qt.ItemDataRole.UserRole, sub)
|
|
83
|
+
self._item2sub[id(item)] = sub
|
|
84
|
+
self.list.addItem(item)
|
|
85
|
+
self.show()
|
|
86
|
+
_dbg(self, f"ADD entry for '{title}' (items={self.list.count()})")
|
|
87
|
+
|
|
88
|
+
# ---- restore flow ----
|
|
89
|
+
def _restore(self, item: QListWidgetItem):
|
|
90
|
+
sub = item.data(Qt.ItemDataRole.UserRole)
|
|
91
|
+
if not sub:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
# Remove the shelf button first
|
|
95
|
+
row = self.list.row(item)
|
|
96
|
+
self.list.takeItem(row)
|
|
97
|
+
self._item2sub.pop(id(item), None)
|
|
98
|
+
|
|
99
|
+
st = self._saved_state.get(sub, None)
|
|
100
|
+
title = sub.windowTitle()
|
|
101
|
+
_dbg(self, f"RESTORE '{title}': have_state={bool(st)}")
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
if st and st.get("max", False):
|
|
105
|
+
_dbg(self, f" → showMaximized()")
|
|
106
|
+
sub.showMaximized()
|
|
107
|
+
else:
|
|
108
|
+
# normal window → restore the exact rectangle
|
|
109
|
+
r = QRect()
|
|
110
|
+
if st and isinstance(st.get("geom"), QRect):
|
|
111
|
+
r = QRect(st["geom"])
|
|
112
|
+
_dbg(self, f" → target rect={r} (valid={r.isValid()})")
|
|
113
|
+
|
|
114
|
+
def apply_rect():
|
|
115
|
+
if r.isValid() and not sub.isMaximized():
|
|
116
|
+
# Apply both ways; some styles ignore one or the other during layout churn
|
|
117
|
+
sub.setWindowState(Qt.WindowState.WindowNoState)
|
|
118
|
+
sub.resize(r.size())
|
|
119
|
+
sub.move(r.topLeft())
|
|
120
|
+
sub.setGeometry(r)
|
|
121
|
+
_dbg(self, f" reapplied rect now={sub.geometry()}")
|
|
122
|
+
|
|
123
|
+
# Pre-apply (helps avoid the tiny default)
|
|
124
|
+
if r.isValid():
|
|
125
|
+
sub.setWindowState(Qt.WindowState.WindowNoState)
|
|
126
|
+
sub.setGeometry(r)
|
|
127
|
+
sub.resize(r.size())
|
|
128
|
+
sub.move(r.topLeft())
|
|
129
|
+
|
|
130
|
+
sub.showNormal()
|
|
131
|
+
# Once MDI has activated and re-laid out, re-apply a couple of times
|
|
132
|
+
QTimer.singleShot(0, apply_rect)
|
|
133
|
+
QTimer.singleShot(30, apply_rect)
|
|
134
|
+
QTimer.singleShot(120, apply_rect)
|
|
135
|
+
|
|
136
|
+
sub.raise_()
|
|
137
|
+
sub.activateWindow()
|
|
138
|
+
finally:
|
|
139
|
+
if self.list.count() == 0:
|
|
140
|
+
self.hide()
|
|
141
|
+
|
|
142
|
+
def remove_for_subwindow(self, sub):
|
|
143
|
+
if not sub:
|
|
144
|
+
return
|
|
145
|
+
for i in range(self.list.count()):
|
|
146
|
+
item = self.list.item(i)
|
|
147
|
+
if item.data(Qt.ItemDataRole.UserRole) is sub:
|
|
148
|
+
self._item2sub.pop(id(item), None)
|
|
149
|
+
self.list.takeItem(i)
|
|
150
|
+
break
|
|
151
|
+
self._saved_state.pop(sub, None) # ← also forget geometry for that sub
|
|
152
|
+
if self.list.count() == 0:
|
|
153
|
+
self.hide()
|
|
154
|
+
|
|
155
|
+
def clear_all(self):
|
|
156
|
+
"""Remove all thumbnails and forget saved window states."""
|
|
157
|
+
try:
|
|
158
|
+
self.list.blockSignals(True)
|
|
159
|
+
self.list.clear()
|
|
160
|
+
finally:
|
|
161
|
+
self.list.blockSignals(False)
|
|
162
|
+
self._item2sub.clear()
|
|
163
|
+
self._saved_state.clear()
|
|
164
|
+
self.hide()
|
|
165
|
+
|
|
166
|
+
class MinimizeInterceptor(QObject):
|
|
167
|
+
"""Redirect native minimize → shelf entry, capturing geometry BEFORE hiding."""
|
|
168
|
+
def __init__(self, shelf: WindowShelf, parent: QWidget | None = None):
|
|
169
|
+
super().__init__(parent)
|
|
170
|
+
self.shelf = shelf
|
|
171
|
+
|
|
172
|
+
def eventFilter(self, obj, ev):
|
|
173
|
+
if isinstance(obj, QMdiSubWindow) and ev.type() == QEvent.Type.WindowStateChange:
|
|
174
|
+
if obj.windowState() & Qt.WindowState.WindowMinimized:
|
|
175
|
+
# Capture state FIRST, then cancel minimize and hide.
|
|
176
|
+
self.shelf.pre_capture_state(obj)
|
|
177
|
+
QTimer.singleShot(0, lambda o=obj: self._redirect(o))
|
|
178
|
+
return True
|
|
179
|
+
return False
|
|
180
|
+
|
|
181
|
+
def _redirect(self, sub: QMdiSubWindow):
|
|
182
|
+
# Clear the minimized bit and hide, then add shelf entry
|
|
183
|
+
sub.setWindowState(sub.windowState() & ~Qt.WindowState.WindowMinimized)
|
|
184
|
+
sub.hide()
|
|
185
|
+
self.shelf.add_entry(sub)
|