setiastrosuitepro 1.6.12__py3-none-any.whl → 1.7.1.post2__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.
- setiastro/images/TextureClarity.svg +56 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- 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/gui/main_window.py +285 -44
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +8 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +115 -6
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +1345 -0
- 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/remove_green.py +1 -1
- setiastro/saspro/resources.py +6 -0
- setiastro/saspro/rgbalign.py +456 -12
- setiastro/saspro/ser_stack_config.py +82 -0
- setiastro/saspro/ser_stacker.py +2321 -0
- setiastro/saspro/ser_stacker_dialog.py +1838 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1625 -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 +2 -4
- setiastro/saspro/texture_clarity.py +593 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/METADATA +3 -2
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/RECORD +42 -30
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.12.dist-info → setiastrosuitepro-1.7.1.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -1328,7 +1328,23 @@ class LiveStackWindow(QDialog):
|
|
|
1328
1328
|
QApplication.processEvents()
|
|
1329
1329
|
finally:
|
|
1330
1330
|
self._poll_busy = False
|
|
1331
|
-
|
|
1331
|
+
|
|
1332
|
+
def _match_master_to_image(self, master: np.ndarray, img: np.ndarray) -> np.ndarray:
|
|
1333
|
+
"""
|
|
1334
|
+
Coerce master (dark/flat) to match img dimensionality.
|
|
1335
|
+
- If img is RGB (H,W,3) and master is mono (H,W), expand to (H,W,1).
|
|
1336
|
+
- If img is mono (H,W) and master is RGB (H,W,3), collapse to mono via mean.
|
|
1337
|
+
"""
|
|
1338
|
+
if master is None:
|
|
1339
|
+
return None
|
|
1340
|
+
|
|
1341
|
+
if img.ndim == 3 and master.ndim == 2:
|
|
1342
|
+
return master[..., None] # (H,W,1) broadcasts to (H,W,3)
|
|
1343
|
+
if img.ndim == 2 and master.ndim == 3:
|
|
1344
|
+
return master.mean(axis=2) # (H,W)
|
|
1345
|
+
return master
|
|
1346
|
+
|
|
1347
|
+
|
|
1332
1348
|
def process_frame(self, path):
|
|
1333
1349
|
if self._should_stop():
|
|
1334
1350
|
return
|
|
@@ -1422,12 +1438,16 @@ class LiveStackWindow(QDialog):
|
|
|
1422
1438
|
|
|
1423
1439
|
# ——— 2b) CALIBRATION (once) ————————————————————————
|
|
1424
1440
|
if self.master_dark is not None:
|
|
1425
|
-
|
|
1441
|
+
md = self._match_master_to_image(self.master_dark, img).astype(np.float32, copy=False)
|
|
1442
|
+
img = img.astype(np.float32, copy=False) - md
|
|
1426
1443
|
# prefer per-filter flat if we’re in mono→color and have one
|
|
1427
1444
|
if mono_key and mono_key in self.master_flats:
|
|
1428
|
-
|
|
1445
|
+
mf = self._match_master_to_image(self.master_flats[mono_key], img).astype(np.float32, copy=False)
|
|
1446
|
+
img = apply_flat_division_numba(img, mf)
|
|
1429
1447
|
elif self.master_flat is not None:
|
|
1430
|
-
|
|
1448
|
+
mf = self._match_master_to_image(self.master_flat, img).astype(np.float32, copy=False)
|
|
1449
|
+
img = apply_flat_division_numba(img, mf)
|
|
1450
|
+
|
|
1431
1451
|
|
|
1432
1452
|
if self._should_stop():
|
|
1433
1453
|
return
|
|
@@ -120,37 +120,41 @@ def soft_threshold(x: np.ndarray, t: float):
|
|
|
120
120
|
def apply_layer_ops(
|
|
121
121
|
w: np.ndarray,
|
|
122
122
|
bias_gain: float,
|
|
123
|
-
thr_sigma: float,
|
|
123
|
+
thr_sigma: float,
|
|
124
124
|
amount: float,
|
|
125
125
|
denoise_strength: float = 0.0,
|
|
126
126
|
sigma: float | np.ndarray | None = None,
|
|
127
|
+
layer_index: int | None = None,
|
|
127
128
|
*,
|
|
128
129
|
mode: str = "μ–σ Thresholding",
|
|
129
130
|
):
|
|
130
131
|
w2 = w
|
|
131
132
|
|
|
132
|
-
# Normalize mode to something robust to label wording
|
|
133
133
|
m = (mode or "").strip().lower()
|
|
134
134
|
is_linear = m.startswith("linear")
|
|
135
|
-
|
|
136
|
-
# --- Linear mode: strictly linear multiscale transform ---
|
|
137
135
|
if is_linear:
|
|
138
|
-
|
|
139
|
-
if abs(bias_gain - 1.0) > 1e-6:
|
|
140
|
-
return w * bias_gain
|
|
141
|
-
return w
|
|
136
|
+
return w * bias_gain if abs(bias_gain - 1.0) > 1e-6 else w
|
|
142
137
|
|
|
143
|
-
# --- μ–σ Thresholding mode (robust nonlinear) ---
|
|
144
138
|
# 1) Noise reduction step (MMT-style NR)
|
|
145
139
|
if denoise_strength > 0.0:
|
|
146
140
|
if sigma is None:
|
|
147
141
|
sigma = _robust_sigma(w2)
|
|
148
142
|
sigma_f = float(sigma)
|
|
149
|
-
|
|
150
|
-
|
|
143
|
+
|
|
144
|
+
i = int(layer_index or 0)
|
|
145
|
+
|
|
146
|
+
# --- SMOOTH scaling option (pick ONE) ---
|
|
147
|
+
# Option A: linear growth (very controllable)
|
|
148
|
+
# scale = 1.0 + 0.75 * i
|
|
149
|
+
|
|
150
|
+
# Option B: sqrt growth of 2^i (gentle, "natural")
|
|
151
|
+
scale = (2.0 ** i) ** 0.5 # 1, 1.41, 2, 2.83, 4, ...
|
|
152
|
+
|
|
153
|
+
# Base: 3σ at denoise=1 for layer 0, increases by scale
|
|
154
|
+
t_dn = denoise_strength * 3.0 * scale * sigma_f
|
|
155
|
+
|
|
151
156
|
if t_dn > 0.0:
|
|
152
157
|
w_dn = soft_threshold(w2, t_dn)
|
|
153
|
-
# Blend original vs denoised based on denoise_strength
|
|
154
158
|
w2 = (1.0 - denoise_strength) * w2 + denoise_strength * w_dn
|
|
155
159
|
|
|
156
160
|
# 2) Threshold in σ units + bias shaping
|
|
@@ -158,7 +162,7 @@ def apply_layer_ops(
|
|
|
158
162
|
if sigma is None:
|
|
159
163
|
sigma = _robust_sigma(w2)
|
|
160
164
|
sigma_f = float(sigma)
|
|
161
|
-
t = thr_sigma * sigma_f
|
|
165
|
+
t = thr_sigma * sigma_f
|
|
162
166
|
if t > 0.0:
|
|
163
167
|
wt = soft_threshold(w2, t)
|
|
164
168
|
w2 = (1.0 - amount) * w2 + amount * wt
|
|
@@ -167,7 +171,6 @@ def apply_layer_ops(
|
|
|
167
171
|
w2 = w2 * bias_gain
|
|
168
172
|
return w2
|
|
169
173
|
|
|
170
|
-
|
|
171
174
|
def _robust_sigma(arr: np.ndarray) -> float:
|
|
172
175
|
"""
|
|
173
176
|
Robust per-layer sigma estimate using MAD, fallback to std if needed.
|
|
@@ -455,7 +458,7 @@ class MultiscaleDecompDialog(QDialog):
|
|
|
455
458
|
|
|
456
459
|
# --- Spin boxes ---
|
|
457
460
|
self.spin_gain = QDoubleSpinBox()
|
|
458
|
-
self.spin_gain.setRange(0.0,
|
|
461
|
+
self.spin_gain.setRange(0.0, 10.0)
|
|
459
462
|
self.spin_gain.setSingleStep(0.05)
|
|
460
463
|
self.spin_gain.setValue(1.0)
|
|
461
464
|
self.spin_gain.setToolTip(
|
|
@@ -491,7 +494,7 @@ class MultiscaleDecompDialog(QDialog):
|
|
|
491
494
|
|
|
492
495
|
# --- Sliders (int ranges, mapped to spins) ---
|
|
493
496
|
self.slider_gain = QSlider(Qt.Orientation.Horizontal)
|
|
494
|
-
self.slider_gain.setRange(0,
|
|
497
|
+
self.slider_gain.setRange(0, 1000) # 0..10.00
|
|
495
498
|
self.slider_gain.setToolTip(self.spin_gain.toolTip())
|
|
496
499
|
|
|
497
500
|
self.slider_thr = QSlider(Qt.Orientation.Horizontal)
|
|
@@ -748,6 +751,9 @@ class MultiscaleDecompDialog(QDialog):
|
|
|
748
751
|
cfg = self.cfgs[i]
|
|
749
752
|
if not cfg.enabled:
|
|
750
753
|
return i, np.zeros_like(w)
|
|
754
|
+
|
|
755
|
+
layer_sigma = self.base_sigma * (2 ** i)
|
|
756
|
+
|
|
751
757
|
sigma = self._layer_noise[i] if self._layer_noise and i < len(self._layer_noise) else None
|
|
752
758
|
out = apply_layer_ops(
|
|
753
759
|
w,
|
|
@@ -756,6 +762,7 @@ class MultiscaleDecompDialog(QDialog):
|
|
|
756
762
|
cfg.amount,
|
|
757
763
|
cfg.denoise,
|
|
758
764
|
sigma,
|
|
765
|
+
layer_index=i,
|
|
759
766
|
mode=mode,
|
|
760
767
|
)
|
|
761
768
|
return i, out
|
|
@@ -1264,11 +1271,17 @@ class MultiscaleDecompDialog(QDialog):
|
|
|
1264
1271
|
cfg = self.cfgs[i]
|
|
1265
1272
|
if not cfg.enabled:
|
|
1266
1273
|
return i, np.zeros_like(w)
|
|
1274
|
+
|
|
1275
|
+
layer_sigma = base_sigma * (2 ** i)
|
|
1276
|
+
|
|
1267
1277
|
return i, apply_layer_ops(
|
|
1268
1278
|
w, cfg.bias_gain, cfg.thr, cfg.amount, cfg.denoise,
|
|
1269
|
-
layer_noise[i],
|
|
1279
|
+
layer_noise[i],
|
|
1280
|
+
layer_index=i,
|
|
1281
|
+
mode=mode
|
|
1270
1282
|
)
|
|
1271
1283
|
|
|
1284
|
+
|
|
1272
1285
|
tuned = [None] * len(details)
|
|
1273
1286
|
max_workers = min(os.cpu_count() or 4, len(details) or 1)
|
|
1274
1287
|
with ThreadPoolExecutor(max_workers=max_workers) as ex:
|