setiastrosuitepro 1.6.12__py3-none-any.whl → 1.7.3__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/images/3dplanet.png +0 -0
- setiastro/images/TextureClarity.svg +56 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__init__.py +9 -8
- setiastro/saspro/__main__.py +326 -285
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +128 -13
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/astrospike_python.py +45 -3
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +4 -1
- setiastro/saspro/gui/main_window.py +326 -46
- setiastro/saspro/gui/mixins/file_mixin.py +41 -18
- setiastro/saspro/gui/mixins/menu_mixin.py +9 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +123 -7
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1429 -0
- setiastro/saspro/layers.py +186 -10
- setiastro/saspro/layers_dock.py +198 -5
- setiastro/saspro/legacy/image_manager.py +10 -4
- setiastro/saspro/legacy/numba_utils.py +1 -1
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/planetprojection.py +3854 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +8 -0
- setiastro/saspro/rgbalign.py +456 -12
- setiastro/saspro/save_options.py +45 -13
- setiastro/saspro/ser_stack_config.py +102 -0
- setiastro/saspro/ser_stacker.py +2327 -0
- setiastro/saspro/ser_stacker_dialog.py +1865 -0
- setiastro/saspro/ser_tracking.py +228 -0
- setiastro/saspro/serviewer.py +1773 -0
- setiastro/saspro/sfcc.py +298 -64
- setiastro/saspro/shortcuts.py +14 -7
- setiastro/saspro/stacking_suite.py +21 -6
- setiastro/saspro/stat_stretch.py +179 -31
- setiastro/saspro/subwindow.py +38 -5
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/METADATA +3 -2
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/RECORD +51 -37
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/license.txt +0 -0
|
@@ -0,0 +1,816 @@
|
|
|
1
|
+
# src/setiastro/saspro/imageops/narrowband_normalization.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import os
|
|
4
|
+
import threading
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Callable, Optional, Tuple
|
|
8
|
+
import numpy as np
|
|
9
|
+
import traceback
|
|
10
|
+
|
|
11
|
+
ProgressCB = Optional[Callable[[int, str], None]]
|
|
12
|
+
|
|
13
|
+
# ---------------- params ----------------
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, slots=True)
|
|
16
|
+
class NBNParams:
|
|
17
|
+
scenario: str # "HOO"/"SHO"/"HSO"/"HOS"
|
|
18
|
+
mode: int # 0 linear, 1 non-linear
|
|
19
|
+
lightness: int # HOO: 0..3, others: 0..4
|
|
20
|
+
blackpoint: float # 0..1
|
|
21
|
+
hlrecover: float # >= 0.25
|
|
22
|
+
hlreduct: float # >= 0.25
|
|
23
|
+
brightness: float # >= 0.25
|
|
24
|
+
blendmode: int = 0 # HOO only: 0/1/2
|
|
25
|
+
hablend: float = 0.6 # HOO only: 0..1
|
|
26
|
+
oiiiboost: float = 1.0 # HOO OIII boost
|
|
27
|
+
siiboost: float = 1.0 # SHO/HSO/HOS
|
|
28
|
+
oiiiboost2: float = 1.0 # SHO/HSO/HOS
|
|
29
|
+
scnr: bool = False # SHO/HSO/HOS
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MissingChannelsError(ValueError):
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = ["NBNParams", "MissingChannelsError", "normalize_narrowband"]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------- PixelMath primitives ----------------
|
|
40
|
+
|
|
41
|
+
_EPS = 1e-12
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _clip01(x: np.ndarray) -> np.ndarray:
|
|
45
|
+
return np.clip(x, 0.0, 1.0)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _inv01(x: np.ndarray) -> np.ndarray:
|
|
49
|
+
"""PixelMath '~' complement for normalized images."""
|
|
50
|
+
return 1.0 - x
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _rescale(x: np.ndarray, lo: float | np.ndarray, hi: float | np.ndarray) -> np.ndarray:
|
|
54
|
+
"""Map x from [lo, hi] -> [0, 1] (clipped)."""
|
|
55
|
+
loa = np.asarray(lo, dtype=np.float32)
|
|
56
|
+
hia = np.asarray(hi, dtype=np.float32)
|
|
57
|
+
denom = np.maximum(hia - loa, _EPS)
|
|
58
|
+
return _clip01((x - loa) / denom)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _mtf(m: float | np.ndarray, x: np.ndarray) -> np.ndarray:
|
|
62
|
+
"""
|
|
63
|
+
PixInsight Midtones Transfer Function.
|
|
64
|
+
For m in (0,1): m is the midtone (pivot) value.
|
|
65
|
+
"""
|
|
66
|
+
m = np.asarray(m, dtype=np.float32)
|
|
67
|
+
x = np.asarray(x, dtype=np.float32)
|
|
68
|
+
m = np.clip(m, _EPS, 1.0 - _EPS)
|
|
69
|
+
x = _clip01(x)
|
|
70
|
+
|
|
71
|
+
# y = (m - 1) * x / ((2*m - 1)*x - m)
|
|
72
|
+
num = (m - 1.0) * x
|
|
73
|
+
den = (2.0 * m - 1.0) * x - m
|
|
74
|
+
|
|
75
|
+
# IMPORTANT: never allow 0 denominator (np.sign(0) == 0). Use ±EPS.
|
|
76
|
+
safe_den = np.where(
|
|
77
|
+
np.abs(den) < _EPS,
|
|
78
|
+
np.where(den >= 0.0, _EPS, -_EPS).astype(np.float32),
|
|
79
|
+
den,
|
|
80
|
+
)
|
|
81
|
+
return _clip01(num / safe_den)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _adev(x: np.ndarray) -> float:
|
|
85
|
+
"""Approx absolute deviation. PixelMath adev()."""
|
|
86
|
+
med = np.nanmedian(x)
|
|
87
|
+
return float(np.nanmedian(np.abs(x - med)))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _stats_min_med_mean(chs: Tuple[np.ndarray, ...]) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
91
|
+
"""
|
|
92
|
+
Match PI per-channel stats behavior: compute min/median/mean for each channel.
|
|
93
|
+
Returns float32 vectors of shape (C,).
|
|
94
|
+
"""
|
|
95
|
+
c = len(chs)
|
|
96
|
+
mins = np.empty((c,), dtype=np.float32)
|
|
97
|
+
meds = np.empty((c,), dtype=np.float32)
|
|
98
|
+
means = np.empty((c,), dtype=np.float32)
|
|
99
|
+
for i, ch in enumerate(chs):
|
|
100
|
+
mins[i] = float(np.nanmin(ch))
|
|
101
|
+
meds[i] = float(np.nanmedian(ch))
|
|
102
|
+
means[i] = float(np.nanmean(ch))
|
|
103
|
+
return mins, meds, means
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _stats_adev_vec(chs: Tuple[np.ndarray, ...]) -> np.ndarray:
|
|
107
|
+
v = np.empty((len(chs),), dtype=np.float32)
|
|
108
|
+
for i, ch in enumerate(chs):
|
|
109
|
+
v[i] = float(_adev(ch))
|
|
110
|
+
return v
|
|
111
|
+
|
|
112
|
+
def _default_workers() -> int:
|
|
113
|
+
# Don’t go crazy; too many workers can reduce perf due to memory bandwidth.
|
|
114
|
+
n = os.cpu_count() or 4
|
|
115
|
+
return max(1, min(32, n))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _run_tiles_parallel(
|
|
119
|
+
tiles: list[tuple[int, int, int, int]],
|
|
120
|
+
worker_fn, # callable(y0,y1,x0,x1,ti)
|
|
121
|
+
*,
|
|
122
|
+
progress_cb: ProgressCB,
|
|
123
|
+
p0: int,
|
|
124
|
+
p1: int,
|
|
125
|
+
label: str,
|
|
126
|
+
max_workers: Optional[int] = None,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Run per-tile worker_fn in parallel. worker_fn must write into shared output using non-overlapping slices.
|
|
130
|
+
"""
|
|
131
|
+
ntiles = len(tiles)
|
|
132
|
+
if ntiles == 0:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
workers = int(max_workers or _default_workers())
|
|
136
|
+
workers = max(1, min(workers, ntiles))
|
|
137
|
+
|
|
138
|
+
# Progress bookkeeping
|
|
139
|
+
done = 0
|
|
140
|
+
lock = threading.Lock()
|
|
141
|
+
last_emit = {"p": -1}
|
|
142
|
+
|
|
143
|
+
def _on_done():
|
|
144
|
+
nonlocal done
|
|
145
|
+
with lock:
|
|
146
|
+
done += 1
|
|
147
|
+
# Throttle: emit only when percent changes by >=1
|
|
148
|
+
if progress_cb:
|
|
149
|
+
p = _map_progress(done, ntiles, p0, p1)
|
|
150
|
+
if p != last_emit["p"]:
|
|
151
|
+
last_emit["p"] = p
|
|
152
|
+
progress_cb(p, f"{label} {done}/{ntiles}")
|
|
153
|
+
|
|
154
|
+
if progress_cb:
|
|
155
|
+
progress_cb(p0, f"{label} 0/{ntiles} (workers={workers})")
|
|
156
|
+
|
|
157
|
+
with ThreadPoolExecutor(max_workers=workers) as ex:
|
|
158
|
+
futs = []
|
|
159
|
+
for ti, (y0, y1, x0, x1) in enumerate(tiles):
|
|
160
|
+
futs.append(ex.submit(worker_fn, y0, y1, x0, x1, ti))
|
|
161
|
+
|
|
162
|
+
# Drain futures; propagate exceptions
|
|
163
|
+
for f in as_completed(futs):
|
|
164
|
+
f.result()
|
|
165
|
+
_on_done()
|
|
166
|
+
|
|
167
|
+
if progress_cb:
|
|
168
|
+
progress_cb(p1, f"{label} {ntiles}/{ntiles}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _iter_tiles(h: int, w: int, tile: int = 1024):
|
|
172
|
+
"""Yield (y0,y1,x0,x1) tiles covering an HxW image."""
|
|
173
|
+
for y0 in range(0, h, tile):
|
|
174
|
+
y1 = min(y0 + tile, h)
|
|
175
|
+
for x0 in range(0, w, tile):
|
|
176
|
+
x1 = min(x0 + tile, w)
|
|
177
|
+
yield y0, y1, x0, x1
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _map_progress(i: int, n: int, p0: int, p1: int) -> int:
|
|
181
|
+
"""Map tile index i in [0..n] to integer percent in [p0..p1]."""
|
|
182
|
+
if n <= 0:
|
|
183
|
+
return int(p1)
|
|
184
|
+
return int(p0 + (p1 - p0) * (i / n))
|
|
185
|
+
|
|
186
|
+
def _finish_tiled(out: np.ndarray, params: NBNParams, progress_cb: ProgressCB) -> np.ndarray:
|
|
187
|
+
h, w, _ = out.shape
|
|
188
|
+
tiles = list(_iter_tiles(h, w, tile=1024))
|
|
189
|
+
|
|
190
|
+
if progress_cb:
|
|
191
|
+
progress_cb(80, f"Finishing tiles 0/{len(tiles)}")
|
|
192
|
+
|
|
193
|
+
def _worker(y0, y1, x0, x1, ti):
|
|
194
|
+
out[y0:y1, x0:x1, :] = _apply_hl_reduction_and_brightness_and_recover(
|
|
195
|
+
out[y0:y1, x0:x1, :], params
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
_run_tiles_parallel(tiles, _worker, progress_cb=progress_cb, p0=80, p1=98, label="Finishing tiles")
|
|
199
|
+
return out
|
|
200
|
+
|
|
201
|
+
# ---------------- Color space helpers (as in script) ----------------
|
|
202
|
+
|
|
203
|
+
def _srgb_to_linear(u: np.ndarray) -> np.ndarray:
|
|
204
|
+
u = np.asarray(u, dtype=np.float32)
|
|
205
|
+
return np.where(u > 0.04045, ((u + 0.055) / 1.055) ** 2.4, u / 12.92)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _linear_to_srgb(u: np.ndarray) -> np.ndarray:
|
|
209
|
+
u = np.asarray(u, dtype=np.float32)
|
|
210
|
+
# Gamma encoding is undefined for negatives; clamp them.
|
|
211
|
+
u = np.clip(u, 0.0, 1.0)
|
|
212
|
+
u = np.where(np.isfinite(u), u, 0.0)
|
|
213
|
+
u = np.maximum(u, 0.0)
|
|
214
|
+
return np.where(u > 0.0031308, 1.055 * (u ** (1.0 / 2.4)) - 0.055, 12.92 * u)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _rgb_to_xyz_pi(r: np.ndarray, g: np.ndarray, b: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
218
|
+
# Matches coefficients in the PixelMath script (PI's D65-ish matrix used there)
|
|
219
|
+
r1 = _srgb_to_linear(_clip01(r))
|
|
220
|
+
g1 = _srgb_to_linear(_clip01(g))
|
|
221
|
+
b1 = _srgb_to_linear(_clip01(b))
|
|
222
|
+
|
|
223
|
+
X = (r1 * 0.4360747) + (g1 * 0.3850649) + (b1 * 0.1430804)
|
|
224
|
+
Y = (r1 * 0.2225045) + (g1 * 0.7168786) + (b1 * 0.0606169)
|
|
225
|
+
Z = (r1 * 0.0139322) + (g1 * 0.0971045) + (b1 * 0.7141733)
|
|
226
|
+
return X, Y, Z
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _xyz_to_lab_pi(X: np.ndarray, Y: np.ndarray, Z: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
230
|
+
# PixelMath uses the 0.008856 threshold and the affine segment
|
|
231
|
+
def f(t: np.ndarray) -> np.ndarray:
|
|
232
|
+
return np.where(t > 0.008856, t ** (1.0 / 3.0), (7.787 * t) + (16.0 / 116.0))
|
|
233
|
+
|
|
234
|
+
X1 = f(X)
|
|
235
|
+
Y1 = f(Y)
|
|
236
|
+
Z1 = f(Z)
|
|
237
|
+
|
|
238
|
+
L = 116.0 * Y1 - 16.0
|
|
239
|
+
a = 500.0 * (X1 - Y1)
|
|
240
|
+
b = 200.0 * (Y1 - Z1)
|
|
241
|
+
return L, a, b
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _xyz_to_rgb_pi(X: np.ndarray, Y: np.ndarray, Z: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
245
|
+
# Inverse matrix from script
|
|
246
|
+
R2 = (X * 3.1338561) + (Y * -1.6168667) + (Z * -0.4906146)
|
|
247
|
+
G2 = (X * -0.9787684) + (Y * 1.9161415) + (Z * 0.0334540)
|
|
248
|
+
B2 = (X * 0.0719453) + (Y * -0.2289914) + (Z * 1.4052427)
|
|
249
|
+
|
|
250
|
+
R3 = _linear_to_srgb(R2)
|
|
251
|
+
G3 = _linear_to_srgb(G2)
|
|
252
|
+
B3 = _linear_to_srgb(B2)
|
|
253
|
+
return _clip01(R3), _clip01(G3), _clip01(B3)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _ciel_lightness_from_rgb(rgb: np.ndarray) -> np.ndarray:
|
|
257
|
+
r, g, b = rgb[..., 0], rgb[..., 1], rgb[..., 2]
|
|
258
|
+
X, Y, Z = _rgb_to_xyz_pi(r, g, b)
|
|
259
|
+
L, _, _ = _xyz_to_lab_pi(X, Y, Z)
|
|
260
|
+
return L / 100.0 # normalized-ish 0..1
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _lab_lightness_replace(
|
|
264
|
+
R: np.ndarray,
|
|
265
|
+
G: np.ndarray,
|
|
266
|
+
B: np.ndarray,
|
|
267
|
+
Y2: np.ndarray,
|
|
268
|
+
) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
269
|
+
"""
|
|
270
|
+
Apply the script's Lab lightness replacement path:
|
|
271
|
+
- Convert RGB -> XYZ -> Lab
|
|
272
|
+
- Replace the Y-like term using caller-supplied Y2 (already in the script's (..+0.16)/1.16 space)
|
|
273
|
+
- Rebuild XYZ using a/b and Y2 exactly like the script (no extra normalization)
|
|
274
|
+
- Convert XYZ -> RGB
|
|
275
|
+
"""
|
|
276
|
+
X, Y, Z = _rgb_to_xyz_pi(R, G, B)
|
|
277
|
+
L, a, b = _xyz_to_lab_pi(X, Y, Z)
|
|
278
|
+
|
|
279
|
+
# Script rebuild:
|
|
280
|
+
X2 = (a / 500.0) + Y2
|
|
281
|
+
Z2 = Y2 - (b / 200.0)
|
|
282
|
+
|
|
283
|
+
def finv(t: np.ndarray) -> np.ndarray:
|
|
284
|
+
return np.where(t > 0.008856, t ** 3, (t - 16.0 / 116.0) / 7.787)
|
|
285
|
+
|
|
286
|
+
X3 = finv(X2)
|
|
287
|
+
Y3 = finv(Y2)
|
|
288
|
+
Z3 = finv(Z2)
|
|
289
|
+
return _xyz_to_rgb_pi(X3, Y3, Z3)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ---------------- Common finishing steps ----------------
|
|
293
|
+
|
|
294
|
+
def _apply_hl_reduction_and_brightness_and_recover(E10: np.ndarray, params: NBNParams) -> np.ndarray:
|
|
295
|
+
hlr = max(float(params.hlreduct), 0.25) # HLReduction (0.5..2.0 typical)
|
|
296
|
+
br = max(float(params.brightness), 0.25) # Brightness (0.5..2.0 typical)
|
|
297
|
+
hrec = max(float(params.hlrecover), 0.25) # HLRecover (0.5..2.0 typical)
|
|
298
|
+
|
|
299
|
+
# E11 = (mtf(~(1/HLReduction*.5),E10)*E10) + (E10*~E10);
|
|
300
|
+
# NOTE: 1/HLReduction*.5 means (1/HLReduction)*0.5
|
|
301
|
+
m_hlr = 1.0 - (0.5 / hlr) # ~(0.5/hlr)
|
|
302
|
+
m_hlr = float(np.clip(m_hlr, _EPS, 1.0 - _EPS))
|
|
303
|
+
E11 = (_mtf(m_hlr, E10) * E10) + (E10 * _inv01(E10))
|
|
304
|
+
|
|
305
|
+
# E12 = mtf((1/Brightness*.5),E11);
|
|
306
|
+
m_b = float(np.clip(0.5 / br, _EPS, 1.0 - _EPS))
|
|
307
|
+
E12 = _mtf(m_b, E11)
|
|
308
|
+
|
|
309
|
+
# E13 = rescale(E12,0,HLRecover);
|
|
310
|
+
E13 = _rescale(E12, 0.0, hrec)
|
|
311
|
+
return _clip01(E13)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ---------------- Shared “core normalize” building blocks ----------------
|
|
315
|
+
|
|
316
|
+
def _compute_M_E0(chs: Tuple[np.ndarray, ...], blackpoint: float) -> Tuple[np.ndarray, np.ndarray]:
|
|
317
|
+
"""
|
|
318
|
+
Implements:
|
|
319
|
+
M = min($T) + Blackpoint*(med($T)-min($T))
|
|
320
|
+
E0 = adev($T)/1.2533 + mean($T) - M
|
|
321
|
+
on a per-channel basis.
|
|
322
|
+
"""
|
|
323
|
+
mins, meds, means = _stats_min_med_mean(chs)
|
|
324
|
+
M = mins + float(blackpoint) * (meds - mins)
|
|
325
|
+
adevs = _stats_adev_vec(chs)
|
|
326
|
+
E0 = (adevs / 1.2533) + means - M
|
|
327
|
+
return M.astype(np.float32), E0.astype(np.float32)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _pm_norm_channel(
|
|
331
|
+
Tref: np.ndarray,
|
|
332
|
+
Ta: float,
|
|
333
|
+
Tb: float,
|
|
334
|
+
Mref: float,
|
|
335
|
+
E0: np.ndarray,
|
|
336
|
+
boost: float,
|
|
337
|
+
) -> np.ndarray:
|
|
338
|
+
"""
|
|
339
|
+
PixelMath pattern used repeatedly:
|
|
340
|
+
A = E0 / ~Mref
|
|
341
|
+
E = (A[ref]*(1-A[other])/(A[ref]-2*A[ref]*A[other] + A[other])) / boost
|
|
342
|
+
E2 = rescale(Tref, Mref, 1)
|
|
343
|
+
E3 = ~(~mtf(E, E2) * ~min(Tref, Mref))
|
|
344
|
+
Caller supplies indices/values already extracted as scalars Ta/Tb etc.
|
|
345
|
+
"""
|
|
346
|
+
invM = max(float(_inv01(np.asarray(Mref, dtype=np.float32))), _EPS)
|
|
347
|
+
A = E0 / invM
|
|
348
|
+
|
|
349
|
+
denom = (Ta - 2.0 * Ta * Tb + Tb)
|
|
350
|
+
E = (Ta * (1.0 - Tb)) / max(float(denom), _EPS)
|
|
351
|
+
E = E / max(float(boost), _EPS)
|
|
352
|
+
|
|
353
|
+
E2 = _rescale(Tref, float(Mref), 1.0)
|
|
354
|
+
min_T_M = np.minimum(Tref, float(Mref))
|
|
355
|
+
E3 = _inv01(_inv01(_mtf(E, E2)) * _inv01(min_T_M))
|
|
356
|
+
return _clip01(E3)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ---------------- Scenario cores ----------------
|
|
360
|
+
|
|
361
|
+
def _normalize_hoo(ha: np.ndarray, oiii: np.ndarray, params: NBNParams, progress_cb: ProgressCB) -> np.ndarray:
|
|
362
|
+
T0 = ha
|
|
363
|
+
T1 = oiii
|
|
364
|
+
T2 = oiii
|
|
365
|
+
|
|
366
|
+
if progress_cb:
|
|
367
|
+
progress_cb(12, "Computing global stats")
|
|
368
|
+
M, E0 = _compute_M_E0((T0, T1, T2), params.blackpoint)
|
|
369
|
+
|
|
370
|
+
# --- scalar E1 for OIII normalize ---
|
|
371
|
+
invM1 = max(float(_inv01(M[1])), _EPS)
|
|
372
|
+
A0 = E0 / invM1
|
|
373
|
+
Ta = float(A0[1])
|
|
374
|
+
Tb = float(A0[0])
|
|
375
|
+
denom = (Ta - 2.0 * Ta * Tb + Tb)
|
|
376
|
+
E1 = (Ta * (1.0 - Tb)) / max(float(denom), _EPS)
|
|
377
|
+
E1 = E1 / max(float(params.oiiiboost), _EPS)
|
|
378
|
+
|
|
379
|
+
hb = float(np.clip(params.hablend, 0.0, 1.0))
|
|
380
|
+
inv_hb = 1.0 - hb
|
|
381
|
+
|
|
382
|
+
# Prealloc output
|
|
383
|
+
h, w = T0.shape
|
|
384
|
+
out = np.empty((h, w, 3), dtype=np.float32)
|
|
385
|
+
|
|
386
|
+
tiles = list(_iter_tiles(h, w, tile=1024))
|
|
387
|
+
|
|
388
|
+
if progress_cb:
|
|
389
|
+
progress_cb(18, "Normalizing channels (tiled)")
|
|
390
|
+
|
|
391
|
+
def _tile_worker(y0, y1, x0, x1, ti):
|
|
392
|
+
t0 = T0[y0:y1, x0:x1]
|
|
393
|
+
t1 = T1[y0:y1, x0:x1]
|
|
394
|
+
|
|
395
|
+
# E3 for OIII
|
|
396
|
+
E2 = _rescale(t1, float(M[1]), 1.0)
|
|
397
|
+
min_t1_m1 = np.minimum(t1, float(M[1]))
|
|
398
|
+
E3 = _inv01(_inv01(_mtf(E1, E2)) * _inv01(min_t1_m1))
|
|
399
|
+
E3 = _clip01(E3)
|
|
400
|
+
|
|
401
|
+
# Blend E4
|
|
402
|
+
if params.blendmode == 0:
|
|
403
|
+
E4 = (t0 * hb) + (E3 * inv_hb)
|
|
404
|
+
elif params.blendmode == 1:
|
|
405
|
+
E4 = (E3 * hb) + (t1 * inv_hb)
|
|
406
|
+
else:
|
|
407
|
+
E4 = (t0 * hb) + (t1 * inv_hb)
|
|
408
|
+
|
|
409
|
+
R = t0
|
|
410
|
+
G = E4
|
|
411
|
+
B = E3
|
|
412
|
+
|
|
413
|
+
if params.mode == 0:
|
|
414
|
+
out[y0:y1, x0:x1, 0] = R
|
|
415
|
+
out[y0:y1, x0:x1, 1] = G
|
|
416
|
+
out[y0:y1, x0:x1, 2] = B
|
|
417
|
+
else:
|
|
418
|
+
if params.lightness == 0:
|
|
419
|
+
X, Y, Z = _rgb_to_xyz_pi(R, G, B)
|
|
420
|
+
L, _, _ = _xyz_to_lab_pi(X, Y, Z)
|
|
421
|
+
Y2 = (L + 16.0) / 116.0
|
|
422
|
+
elif params.lightness == 1:
|
|
423
|
+
rgbT = np.stack([t0, t1, t1], axis=-1)
|
|
424
|
+
ciel = _ciel_lightness_from_rgb(rgbT)
|
|
425
|
+
Y2 = (ciel + 0.16) / 1.16
|
|
426
|
+
elif params.lightness == 2:
|
|
427
|
+
Y2 = (t0 + 0.16) / 1.16
|
|
428
|
+
else:
|
|
429
|
+
Y2 = (t1 + 0.16) / 1.16
|
|
430
|
+
|
|
431
|
+
r3, g3, b3 = _lab_lightness_replace(R, G, B, Y2.astype(np.float32))
|
|
432
|
+
out[y0:y1, x0:x1, 0] = r3
|
|
433
|
+
out[y0:y1, x0:x1, 1] = g3
|
|
434
|
+
out[y0:y1, x0:x1, 2] = b3
|
|
435
|
+
|
|
436
|
+
_run_tiles_parallel(
|
|
437
|
+
tiles,
|
|
438
|
+
_tile_worker,
|
|
439
|
+
progress_cb=progress_cb,
|
|
440
|
+
p0=18,
|
|
441
|
+
p1=75,
|
|
442
|
+
label="Processing tiles",
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
if progress_cb:
|
|
447
|
+
progress_cb(80, "Finishing (HL reduction / brightness / recover)")
|
|
448
|
+
|
|
449
|
+
# Finish as a single pass (still vectorized), or tile if you want even more granular updates
|
|
450
|
+
out = _finish_tiled(out, params, progress_cb)
|
|
451
|
+
return out
|
|
452
|
+
|
|
453
|
+
def _normalize_sho(ha: np.ndarray, oiii: np.ndarray, sii: np.ndarray, params: NBNParams, progress_cb: ProgressCB) -> np.ndarray:
|
|
454
|
+
# $T[0]=SII (R), $T[1]=Ha (G), $T[2]=OIII (B)
|
|
455
|
+
T0 = sii
|
|
456
|
+
T1 = ha
|
|
457
|
+
T2 = oiii
|
|
458
|
+
|
|
459
|
+
if progress_cb:
|
|
460
|
+
progress_cb(12, "Computing global stats")
|
|
461
|
+
M, E0 = _compute_M_E0((T0, T1, T2), params.blackpoint)
|
|
462
|
+
|
|
463
|
+
# --- scalar params for SII normalize ---
|
|
464
|
+
invM0 = max(float(_inv01(M[0])), _EPS)
|
|
465
|
+
A = E0 / invM0
|
|
466
|
+
Ta_sii = float(A[0])
|
|
467
|
+
Tb_sii = float(A[1])
|
|
468
|
+
denom = (Ta_sii - 2.0 * Ta_sii * Tb_sii + Tb_sii)
|
|
469
|
+
E1_sii = (Ta_sii * (1.0 - Tb_sii)) / max(float(denom), _EPS)
|
|
470
|
+
E1_sii = E1_sii / max(float(params.siiboost), _EPS)
|
|
471
|
+
|
|
472
|
+
# --- scalar params for OIII normalize ---
|
|
473
|
+
invM2 = max(float(_inv01(M[2])), _EPS)
|
|
474
|
+
A = E0 / invM2
|
|
475
|
+
Ta_oiii = float(A[2])
|
|
476
|
+
Tb_oiii = float(A[1])
|
|
477
|
+
denom = (Ta_oiii - 2.0 * Ta_oiii * Tb_oiii + Tb_oiii)
|
|
478
|
+
E1_oiii = (Ta_oiii * (1.0 - Tb_oiii)) / max(float(denom), _EPS)
|
|
479
|
+
E1_oiii = E1_oiii / max(float(params.oiiiboost2), _EPS)
|
|
480
|
+
|
|
481
|
+
h, w = T0.shape
|
|
482
|
+
out = np.empty((h, w, 3), dtype=np.float32)
|
|
483
|
+
|
|
484
|
+
tiles = list(_iter_tiles(h, w, tile=1024))
|
|
485
|
+
|
|
486
|
+
if progress_cb:
|
|
487
|
+
progress_cb(18, "Normalizing channels (tiled)")
|
|
488
|
+
|
|
489
|
+
def _tile_worker(y0, y1, x0, x1, ti):
|
|
490
|
+
t0 = T0[y0:y1, x0:x1] # SII
|
|
491
|
+
t1 = T1[y0:y1, x0:x1] # Ha
|
|
492
|
+
t2 = T2[y0:y1, x0:x1] # OIII
|
|
493
|
+
|
|
494
|
+
# SII -> E3
|
|
495
|
+
E2 = _rescale(t0, float(M[0]), 1.0)
|
|
496
|
+
min_t0_m0 = np.minimum(t0, float(M[0]))
|
|
497
|
+
E3 = _inv01(_inv01(_mtf(E1_sii, E2)) * _inv01(min_t0_m0))
|
|
498
|
+
E3 = _clip01(E3)
|
|
499
|
+
|
|
500
|
+
# OIII -> E6
|
|
501
|
+
E5 = _rescale(t2, float(M[2]), 1.0)
|
|
502
|
+
min_t2_m2 = np.minimum(t2, float(M[2]))
|
|
503
|
+
E6 = _inv01(_inv01(_mtf(E1_oiii, E5)) * _inv01(min_t2_m2))
|
|
504
|
+
E6 = _clip01(E6)
|
|
505
|
+
|
|
506
|
+
R = E3
|
|
507
|
+
if not params.scnr:
|
|
508
|
+
G = t1
|
|
509
|
+
else:
|
|
510
|
+
G = np.minimum((R + E6) * 0.5, t1)
|
|
511
|
+
B = E6
|
|
512
|
+
|
|
513
|
+
if params.mode == 0:
|
|
514
|
+
out[y0:y1, x0:x1, 0] = R
|
|
515
|
+
out[y0:y1, x0:x1, 1] = G
|
|
516
|
+
out[y0:y1, x0:x1, 2] = B
|
|
517
|
+
else:
|
|
518
|
+
if params.lightness == 0:
|
|
519
|
+
X, Y, Z = _rgb_to_xyz_pi(R, G, B)
|
|
520
|
+
L, _, _ = _xyz_to_lab_pi(X, Y, Z)
|
|
521
|
+
Y2 = (L + 16.0) / 116.0
|
|
522
|
+
elif params.lightness == 1:
|
|
523
|
+
rgbT = np.stack([t0, t1, t2], axis=-1)
|
|
524
|
+
ciel = _ciel_lightness_from_rgb(rgbT)
|
|
525
|
+
Y2 = (ciel + 0.16) / 1.16
|
|
526
|
+
elif params.lightness == 2:
|
|
527
|
+
Y2 = (t1 + 0.16) / 1.16 # Ha
|
|
528
|
+
elif params.lightness == 3:
|
|
529
|
+
Y2 = (t0 + 0.16) / 1.16 # SII
|
|
530
|
+
else:
|
|
531
|
+
Y2 = (t2 + 0.16) / 1.16 # OIII
|
|
532
|
+
|
|
533
|
+
r3, g3, b3 = _lab_lightness_replace(R, G, B, Y2.astype(np.float32))
|
|
534
|
+
out[y0:y1, x0:x1, 0] = r3
|
|
535
|
+
out[y0:y1, x0:x1, 1] = g3
|
|
536
|
+
out[y0:y1, x0:x1, 2] = b3
|
|
537
|
+
|
|
538
|
+
_run_tiles_parallel(
|
|
539
|
+
tiles,
|
|
540
|
+
_tile_worker,
|
|
541
|
+
progress_cb=progress_cb,
|
|
542
|
+
p0=18,
|
|
543
|
+
p1=75,
|
|
544
|
+
label="Processing tiles",
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
if progress_cb:
|
|
549
|
+
progress_cb(80, "Finishing (HL reduction / brightness / recover)")
|
|
550
|
+
out = _finish_tiled(out, params, progress_cb)
|
|
551
|
+
return out
|
|
552
|
+
|
|
553
|
+
def _normalize_hso(ha: np.ndarray, oiii: np.ndarray, sii: np.ndarray, params: NBNParams, progress_cb: ProgressCB) -> np.ndarray:
|
|
554
|
+
# $T[0]=Ha, $T[1]=SII, $T[2]=OIII
|
|
555
|
+
T0 = ha
|
|
556
|
+
T1 = sii
|
|
557
|
+
T2 = oiii
|
|
558
|
+
|
|
559
|
+
if progress_cb:
|
|
560
|
+
progress_cb(12, "Computing global stats")
|
|
561
|
+
M, E0 = _compute_M_E0((T0, T1, T2), params.blackpoint)
|
|
562
|
+
|
|
563
|
+
# scalar for SII normalize (uses M[1], A[1] vs A[0])
|
|
564
|
+
invM1 = max(float(_inv01(M[1])), _EPS)
|
|
565
|
+
A = E0 / invM1
|
|
566
|
+
Ta_sii = float(A[1])
|
|
567
|
+
Tb_sii = float(A[0])
|
|
568
|
+
denom = (Ta_sii - 2.0 * Ta_sii * Tb_sii + Tb_sii)
|
|
569
|
+
E1_sii = (Ta_sii * (1.0 - Tb_sii)) / max(float(denom), _EPS)
|
|
570
|
+
E1_sii = E1_sii / max(float(params.siiboost), _EPS)
|
|
571
|
+
|
|
572
|
+
# scalar for OIII normalize (uses M[2], A[2] vs A[0])
|
|
573
|
+
invM2 = max(float(_inv01(M[2])), _EPS)
|
|
574
|
+
A = E0 / invM2
|
|
575
|
+
Ta_oiii = float(A[2])
|
|
576
|
+
Tb_oiii = float(A[0])
|
|
577
|
+
denom = (Ta_oiii - 2.0 * Ta_oiii * Tb_oiii + Tb_oiii)
|
|
578
|
+
E1_oiii = (Ta_oiii * (1.0 - Tb_oiii)) / max(float(denom), _EPS)
|
|
579
|
+
E1_oiii = E1_oiii / max(float(params.oiiiboost2), _EPS)
|
|
580
|
+
|
|
581
|
+
h, w = T0.shape
|
|
582
|
+
out = np.empty((h, w, 3), dtype=np.float32)
|
|
583
|
+
tiles = list(_iter_tiles(h, w, tile=1024))
|
|
584
|
+
|
|
585
|
+
if progress_cb:
|
|
586
|
+
progress_cb(18, "Normalizing channels (tiled)")
|
|
587
|
+
|
|
588
|
+
def _tile_worker(y0, y1, x0, x1, ti):
|
|
589
|
+
t0 = T0[y0:y1, x0:x1] # Ha
|
|
590
|
+
t1 = T1[y0:y1, x0:x1] # SII
|
|
591
|
+
t2 = T2[y0:y1, x0:x1] # OIII
|
|
592
|
+
|
|
593
|
+
# SII -> E3 (HSO uses T1 and M[1])
|
|
594
|
+
E2 = _rescale(t1, float(M[1]), 1.0)
|
|
595
|
+
min_t1_m1 = np.minimum(t1, float(M[1]))
|
|
596
|
+
E3 = _inv01(_inv01(_mtf(E1_sii, E2)) * _inv01(min_t1_m1))
|
|
597
|
+
E3 = _clip01(E3)
|
|
598
|
+
# OIII -> E6
|
|
599
|
+
E5 = _rescale(t2, float(M[2]), 1.0)
|
|
600
|
+
min_t2_m2 = np.minimum(t2, float(M[2]))
|
|
601
|
+
E6 = _inv01(_inv01(_mtf(E1_oiii, E5)) * _inv01(min_t2_m2))
|
|
602
|
+
E6 = _clip01(E6)
|
|
603
|
+
|
|
604
|
+
R = t0
|
|
605
|
+
if not params.scnr:
|
|
606
|
+
G = E3
|
|
607
|
+
else:
|
|
608
|
+
G = np.minimum((R + E6) * 0.5, E3)
|
|
609
|
+
B = E6
|
|
610
|
+
|
|
611
|
+
if params.mode == 0:
|
|
612
|
+
out[y0:y1, x0:x1, 0] = R
|
|
613
|
+
out[y0:y1, x0:x1, 1] = G
|
|
614
|
+
out[y0:y1, x0:x1, 2] = B
|
|
615
|
+
else:
|
|
616
|
+
if params.lightness == 0:
|
|
617
|
+
X, Y, Z = _rgb_to_xyz_pi(R, G, B)
|
|
618
|
+
L, _, _ = _xyz_to_lab_pi(X, Y, Z)
|
|
619
|
+
Y2 = (L + 16.0) / 116.0
|
|
620
|
+
elif params.lightness == 1:
|
|
621
|
+
rgbT = np.stack([t0, t1, t2], axis=-1)
|
|
622
|
+
ciel = _ciel_lightness_from_rgb(rgbT)
|
|
623
|
+
Y2 = (ciel + 0.16) / 1.16
|
|
624
|
+
elif params.lightness == 2:
|
|
625
|
+
Y2 = (t1 + 0.16) / 1.16 # Ha
|
|
626
|
+
elif params.lightness == 3:
|
|
627
|
+
Y2 = (t0 + 0.16) / 1.16 # SII
|
|
628
|
+
else:
|
|
629
|
+
Y2 = (t2 + 0.16) / 1.16 # OIII
|
|
630
|
+
|
|
631
|
+
r3, g3, b3 = _lab_lightness_replace(R, G, B, Y2.astype(np.float32))
|
|
632
|
+
out[y0:y1, x0:x1, 0] = r3
|
|
633
|
+
out[y0:y1, x0:x1, 1] = g3
|
|
634
|
+
out[y0:y1, x0:x1, 2] = b3
|
|
635
|
+
|
|
636
|
+
_run_tiles_parallel(
|
|
637
|
+
tiles,
|
|
638
|
+
_tile_worker,
|
|
639
|
+
progress_cb=progress_cb,
|
|
640
|
+
p0=18,
|
|
641
|
+
p1=75,
|
|
642
|
+
label="Processing tiles",
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
if progress_cb:
|
|
647
|
+
progress_cb(80, "Finishing (HL reduction / brightness / recover)")
|
|
648
|
+
out = _finish_tiled(out, params, progress_cb)
|
|
649
|
+
return out
|
|
650
|
+
|
|
651
|
+
|
|
652
|
+
def _normalize_hos(ha: np.ndarray, oiii: np.ndarray, sii: np.ndarray, params: NBNParams, progress_cb: ProgressCB) -> np.ndarray:
|
|
653
|
+
# $T[0]=Ha, $T[1]=OIII, $T[2]=SII
|
|
654
|
+
T0 = ha
|
|
655
|
+
T1 = oiii
|
|
656
|
+
T2 = sii
|
|
657
|
+
|
|
658
|
+
if progress_cb:
|
|
659
|
+
progress_cb(12, "Computing global stats")
|
|
660
|
+
M, E0 = _compute_M_E0((T0, T1, T2), params.blackpoint)
|
|
661
|
+
|
|
662
|
+
# scalar for OIII normalize (uses M[1], A[1] vs A[0])
|
|
663
|
+
invM1 = max(float(_inv01(M[1])), _EPS)
|
|
664
|
+
A = E0 / invM1
|
|
665
|
+
Ta_oiii = float(A[1])
|
|
666
|
+
Tb_oiii = float(A[0])
|
|
667
|
+
denom = (Ta_oiii - 2.0 * Ta_oiii * Tb_oiii + Tb_oiii)
|
|
668
|
+
E1_oiii = (Ta_oiii * (1.0 - Tb_oiii)) / max(float(denom), _EPS)
|
|
669
|
+
E1_oiii = E1_oiii / max(float(params.oiiiboost2), _EPS)
|
|
670
|
+
|
|
671
|
+
# scalar for SII normalize (uses M[2], A[2] vs A[0])
|
|
672
|
+
invM2 = max(float(_inv01(M[2])), _EPS)
|
|
673
|
+
A = E0 / invM2
|
|
674
|
+
Ta_sii = float(A[2])
|
|
675
|
+
Tb_sii = float(A[0])
|
|
676
|
+
denom = (Ta_sii - 2.0 * Ta_sii * Tb_sii + Tb_sii)
|
|
677
|
+
E1_sii = (Ta_sii * (1.0 - Tb_sii)) / max(float(denom), _EPS)
|
|
678
|
+
E1_sii = E1_sii / max(float(params.siiboost), _EPS)
|
|
679
|
+
|
|
680
|
+
h, w = T0.shape
|
|
681
|
+
out = np.empty((h, w, 3), dtype=np.float32)
|
|
682
|
+
|
|
683
|
+
tiles = list(_iter_tiles(h, w, tile=1024))
|
|
684
|
+
|
|
685
|
+
if progress_cb:
|
|
686
|
+
progress_cb(18, "Normalizing channels (tiled)")
|
|
687
|
+
|
|
688
|
+
def _tile_worker(y0, y1, x0, x1, ti):
|
|
689
|
+
t0 = T0[y0:y1, x0:x1] # Ha
|
|
690
|
+
t1 = T1[y0:y1, x0:x1] # OIII
|
|
691
|
+
t2 = T2[y0:y1, x0:x1] # SII
|
|
692
|
+
|
|
693
|
+
# OIII -> E3 (uses t1 and M[1])
|
|
694
|
+
E2 = _rescale(t1, float(M[1]), 1.0)
|
|
695
|
+
min_t1_m1 = np.minimum(t1, float(M[1]))
|
|
696
|
+
E3 = _inv01(_inv01(_mtf(E1_oiii, E2)) * _inv01(min_t1_m1))
|
|
697
|
+
E3 = _clip01(E3)
|
|
698
|
+
|
|
699
|
+
# SII -> E6 (uses t2 and M[2])
|
|
700
|
+
E5 = _rescale(t2, float(M[2]), 1.0)
|
|
701
|
+
min_t2_m2 = np.minimum(t2, float(M[2]))
|
|
702
|
+
E6 = _inv01(_inv01(_mtf(E1_sii, E5)) * _inv01(min_t2_m2))
|
|
703
|
+
E6 = _clip01(E6)
|
|
704
|
+
|
|
705
|
+
R = t0
|
|
706
|
+
if not params.scnr:
|
|
707
|
+
G = E3
|
|
708
|
+
else:
|
|
709
|
+
G = np.minimum((R + E6) * 0.5, E3)
|
|
710
|
+
B = E6
|
|
711
|
+
|
|
712
|
+
if params.mode == 0:
|
|
713
|
+
out[y0:y1, x0:x1, 0] = R
|
|
714
|
+
out[y0:y1, x0:x1, 1] = G
|
|
715
|
+
out[y0:y1, x0:x1, 2] = B
|
|
716
|
+
else:
|
|
717
|
+
if params.lightness == 0:
|
|
718
|
+
X, Y, Z = _rgb_to_xyz_pi(R, G, B)
|
|
719
|
+
L, _, _ = _xyz_to_lab_pi(X, Y, Z)
|
|
720
|
+
Y2 = (L + 16.0) / 116.0
|
|
721
|
+
elif params.lightness == 1:
|
|
722
|
+
rgbT = np.stack([t0, t1, t2], axis=-1)
|
|
723
|
+
ciel = _ciel_lightness_from_rgb(rgbT)
|
|
724
|
+
Y2 = (ciel + 0.16) / 1.16
|
|
725
|
+
elif params.lightness == 2:
|
|
726
|
+
Y2 = (t1 + 0.16) / 1.16 # Ha
|
|
727
|
+
elif params.lightness == 3:
|
|
728
|
+
Y2 = (t0 + 0.16) / 1.16 # SII
|
|
729
|
+
else:
|
|
730
|
+
Y2 = (t2 + 0.16) / 1.16 # OIII
|
|
731
|
+
|
|
732
|
+
r3, g3, b3 = _lab_lightness_replace(R, G, B, Y2.astype(np.float32))
|
|
733
|
+
out[y0:y1, x0:x1, 0] = r3
|
|
734
|
+
out[y0:y1, x0:x1, 1] = g3
|
|
735
|
+
out[y0:y1, x0:x1, 2] = b3
|
|
736
|
+
|
|
737
|
+
_run_tiles_parallel(
|
|
738
|
+
tiles,
|
|
739
|
+
_tile_worker,
|
|
740
|
+
progress_cb=progress_cb,
|
|
741
|
+
p0=18,
|
|
742
|
+
p1=75,
|
|
743
|
+
label="Processing tiles",
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
if progress_cb:
|
|
748
|
+
progress_cb(80, "Finishing (HL reduction / brightness / recover)")
|
|
749
|
+
out = _finish_tiled(out, params, progress_cb)
|
|
750
|
+
return out
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
def normalize_narrowband(
|
|
754
|
+
ha: np.ndarray | None,
|
|
755
|
+
oiii: np.ndarray | None,
|
|
756
|
+
sii: np.ndarray | None,
|
|
757
|
+
params: NBNParams,
|
|
758
|
+
*,
|
|
759
|
+
progress_cb: ProgressCB = None,
|
|
760
|
+
) -> np.ndarray:
|
|
761
|
+
"""
|
|
762
|
+
Entry point used by the UI/worker. Dispatches to the correct scenario core.
|
|
763
|
+
|
|
764
|
+
Inputs are expected to be mono float32 arrays in [0..1] (or at least clipped-ish).
|
|
765
|
+
Returns RGB float32 [0..1].
|
|
766
|
+
"""
|
|
767
|
+
scen = (params.scenario or "").split()[0].strip().upper()
|
|
768
|
+
|
|
769
|
+
# small helper so we can always give sane progress ranges
|
|
770
|
+
def cb(p: int, msg: str = ""):
|
|
771
|
+
if progress_cb:
|
|
772
|
+
progress_cb(int(max(0, min(100, p))), msg)
|
|
773
|
+
|
|
774
|
+
cb(0, f"Starting {scen}")
|
|
775
|
+
|
|
776
|
+
# Validate requirements
|
|
777
|
+
if scen == "HOO":
|
|
778
|
+
if ha is None or oiii is None:
|
|
779
|
+
raise MissingChannelsError("HOO requires Ha and OIII.")
|
|
780
|
+
# sii ignored for HOO
|
|
781
|
+
cb(5, "Dispatching HOO")
|
|
782
|
+
out = _normalize_hoo(
|
|
783
|
+
ha.astype(np.float32, copy=False),
|
|
784
|
+
oiii.astype(np.float32, copy=False),
|
|
785
|
+
params,
|
|
786
|
+
cb,
|
|
787
|
+
)
|
|
788
|
+
cb(100, "Done")
|
|
789
|
+
return _clip01(out).astype(np.float32, copy=False)
|
|
790
|
+
|
|
791
|
+
if scen in ("SHO", "HSO", "HOS"):
|
|
792
|
+
missing = []
|
|
793
|
+
if ha is None: missing.append("Ha")
|
|
794
|
+
if oiii is None: missing.append("OIII")
|
|
795
|
+
if sii is None: missing.append("SII")
|
|
796
|
+
if missing:
|
|
797
|
+
raise MissingChannelsError(f"{scen} requires " + ", ".join(missing) + ".")
|
|
798
|
+
|
|
799
|
+
ha = ha.astype(np.float32, copy=False)
|
|
800
|
+
oiii = oiii.astype(np.float32, copy=False)
|
|
801
|
+
sii = sii.astype(np.float32, copy=False)
|
|
802
|
+
|
|
803
|
+
cb(5, f"Dispatching {scen}")
|
|
804
|
+
|
|
805
|
+
if scen == "SHO":
|
|
806
|
+
out = _normalize_sho(ha, oiii, sii, params, cb)
|
|
807
|
+
elif scen == "HSO":
|
|
808
|
+
out = _normalize_hso(ha, oiii, sii, params, cb)
|
|
809
|
+
else: # "HOS"
|
|
810
|
+
out = _normalize_hos(ha, oiii, sii, params, cb)
|
|
811
|
+
|
|
812
|
+
cb(100, "Done")
|
|
813
|
+
return _clip01(out).astype(np.float32, copy=False)
|
|
814
|
+
|
|
815
|
+
# Unknown scenario
|
|
816
|
+
raise ValueError(f"Unknown narrowband normalization scenario: {params.scenario!r}")
|