setiastrosuitepro 1.6.10__py3-none-any.whl → 1.7.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.
- setiastro/images/colorwheel.svg +97 -0
- setiastro/images/narrowbandnormalization.png +0 -0
- setiastro/images/planetarystacker.png +0 -0
- setiastro/saspro/__main__.py +1 -1
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/aberration_ai.py +49 -11
- setiastro/saspro/aberration_ai_preset.py +29 -3
- setiastro/saspro/backgroundneutral.py +73 -33
- setiastro/saspro/blink_comparator_pro.py +116 -71
- setiastro/saspro/convo.py +9 -6
- setiastro/saspro/curve_editor_pro.py +72 -22
- setiastro/saspro/curves_preset.py +249 -47
- setiastro/saspro/doc_manager.py +178 -11
- setiastro/saspro/gui/main_window.py +218 -66
- setiastro/saspro/gui/mixins/dock_mixin.py +245 -24
- setiastro/saspro/gui/mixins/file_mixin.py +35 -16
- setiastro/saspro/gui/mixins/menu_mixin.py +31 -1
- setiastro/saspro/gui/mixins/toolbar_mixin.py +132 -10
- setiastro/saspro/histogram.py +179 -7
- setiastro/saspro/imageops/narrowband_normalization.py +816 -0
- setiastro/saspro/imageops/serloader.py +769 -0
- setiastro/saspro/imageops/starbasedwhitebalance.py +23 -52
- setiastro/saspro/imageops/stretch.py +66 -15
- setiastro/saspro/legacy/numba_utils.py +25 -48
- setiastro/saspro/live_stacking.py +24 -4
- setiastro/saspro/multiscale_decomp.py +30 -17
- setiastro/saspro/narrowband_normalization.py +1618 -0
- setiastro/saspro/numba_utils.py +0 -55
- setiastro/saspro/ops/script_editor.py +5 -0
- setiastro/saspro/ops/scripts.py +119 -0
- setiastro/saspro/remove_green.py +1 -1
- setiastro/saspro/resources.py +4 -0
- setiastro/saspro/ser_stack_config.py +68 -0
- setiastro/saspro/ser_stacker.py +2245 -0
- setiastro/saspro/ser_stacker_dialog.py +1481 -0
- setiastro/saspro/ser_tracking.py +206 -0
- setiastro/saspro/serviewer.py +1242 -0
- setiastro/saspro/sfcc.py +602 -214
- setiastro/saspro/shortcuts.py +35 -16
- setiastro/saspro/stacking_suite.py +332 -87
- setiastro/saspro/star_alignment.py +243 -122
- setiastro/saspro/stat_stretch.py +220 -31
- setiastro/saspro/subwindow.py +2 -4
- setiastro/saspro/whitebalance.py +24 -0
- setiastro/saspro/widgets/resource_monitor.py +122 -74
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/METADATA +2 -2
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/RECORD +51 -40
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.6.10.dist-info → setiastrosuitepro-1.7.0.dist-info}/licenses/license.txt +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# src/setiastro/saspro/ser_tracking.py
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
import cv2
|
|
7
|
+
except Exception:
|
|
8
|
+
cv2 = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _to_mono01(img: np.ndarray) -> np.ndarray:
|
|
12
|
+
"""Convert frame float [0..1] mono/RGB -> mono float32 [0..1]."""
|
|
13
|
+
if img.ndim == 2:
|
|
14
|
+
m = img
|
|
15
|
+
else:
|
|
16
|
+
# simple luma; keep fast
|
|
17
|
+
m = 0.2126 * img[..., 0] + 0.7152 * img[..., 1] + 0.0722 * img[..., 2]
|
|
18
|
+
return np.asarray(m, dtype=np.float32)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PlanetaryTracker:
|
|
22
|
+
"""
|
|
23
|
+
Tracks by centroid of the brightest connected component inside ROI.
|
|
24
|
+
Good for: planets, full disk objects.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self, smooth_sigma: float = 1.5, thresh_pct: float = 92.0):
|
|
27
|
+
self.smooth_sigma = float(smooth_sigma)
|
|
28
|
+
self.thresh_pct = float(thresh_pct)
|
|
29
|
+
self._ref_center = None # (cx, cy)
|
|
30
|
+
|
|
31
|
+
def reset(self):
|
|
32
|
+
self._ref_center = None
|
|
33
|
+
|
|
34
|
+
def _blur(self, m: np.ndarray) -> np.ndarray:
|
|
35
|
+
if cv2 is None:
|
|
36
|
+
return m
|
|
37
|
+
# sigma->ksize
|
|
38
|
+
sigma = max(0.0, self.smooth_sigma)
|
|
39
|
+
if sigma <= 0:
|
|
40
|
+
return m
|
|
41
|
+
k = int(max(3, (sigma * 6) // 2 * 2 + 1))
|
|
42
|
+
return cv2.GaussianBlur(m, (k, k), sigmaX=sigma, sigmaY=sigma)
|
|
43
|
+
|
|
44
|
+
def _largest_component_mask(self, mask: np.ndarray) -> np.ndarray:
|
|
45
|
+
if cv2 is None:
|
|
46
|
+
return mask
|
|
47
|
+
# mask uint8 0/255
|
|
48
|
+
num, labels, stats, _ = cv2.connectedComponentsWithStats(mask, connectivity=8)
|
|
49
|
+
if num <= 1:
|
|
50
|
+
return mask
|
|
51
|
+
# skip background 0
|
|
52
|
+
areas = stats[1:, cv2.CC_STAT_AREA]
|
|
53
|
+
k = 1 + int(np.argmax(areas))
|
|
54
|
+
out = (labels == k).astype(np.uint8) * 255
|
|
55
|
+
return out
|
|
56
|
+
|
|
57
|
+
def _centroid(self, m: np.ndarray, mask: np.ndarray) -> tuple[float, float, float]:
|
|
58
|
+
if cv2 is None:
|
|
59
|
+
# fallback: simple average of mask pixels
|
|
60
|
+
ys, xs = np.nonzero(mask > 0)
|
|
61
|
+
if len(xs) < 10:
|
|
62
|
+
return (0.0, 0.0, 0.0)
|
|
63
|
+
return (float(xs.mean()), float(ys.mean()), 1.0)
|
|
64
|
+
|
|
65
|
+
mm = cv2.moments((mask > 0).astype(np.uint8), binaryImage=True)
|
|
66
|
+
if mm["m00"] <= 0:
|
|
67
|
+
return (0.0, 0.0, 0.0)
|
|
68
|
+
cx = float(mm["m10"] / mm["m00"])
|
|
69
|
+
cy = float(mm["m01"] / mm["m00"])
|
|
70
|
+
# confidence: area fraction
|
|
71
|
+
conf = float(np.clip(mm["m00"] / float(mask.size), 0.0, 1.0))
|
|
72
|
+
return (cx, cy, conf)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def step(self, img01: np.ndarray) -> tuple[float, float, float]:
|
|
76
|
+
"""
|
|
77
|
+
Returns (dx, dy, conf) where dx/dy shifts FROM current frame TO reference.
|
|
78
|
+
"""
|
|
79
|
+
m = _to_mono01(img01)
|
|
80
|
+
m2 = self._blur(m)
|
|
81
|
+
|
|
82
|
+
# adaptive threshold by percentile
|
|
83
|
+
t = float(np.percentile(m2, self.thresh_pct))
|
|
84
|
+
if not np.isfinite(t):
|
|
85
|
+
return 0.0, 0.0, 0.0
|
|
86
|
+
|
|
87
|
+
mask = (m2 >= t).astype(np.uint8) * 255
|
|
88
|
+
mask = self._largest_component_mask(mask)
|
|
89
|
+
|
|
90
|
+
cx, cy, conf = self._centroid(m2, mask)
|
|
91
|
+
if conf <= 0.0:
|
|
92
|
+
return 0.0, 0.0, 0.0
|
|
93
|
+
|
|
94
|
+
if self._ref_center is None:
|
|
95
|
+
self._ref_center = (cx, cy)
|
|
96
|
+
return 0.0, 0.0, conf
|
|
97
|
+
|
|
98
|
+
rx, ry = self._ref_center
|
|
99
|
+
dx = rx - cx
|
|
100
|
+
dy = ry - cy
|
|
101
|
+
return float(dx), float(dy), conf
|
|
102
|
+
|
|
103
|
+
def compute_center(self, img01: np.ndarray) -> tuple[float, float, float]:
|
|
104
|
+
"""
|
|
105
|
+
Compute (cx, cy, conf) in pixels in the provided image coordinate system.
|
|
106
|
+
Uses the same pipeline as step(): blur -> percentile thresh -> largest CC -> centroid.
|
|
107
|
+
"""
|
|
108
|
+
m = _to_mono01(img01)
|
|
109
|
+
m2 = self._blur(m)
|
|
110
|
+
|
|
111
|
+
t = float(np.percentile(m2, self.thresh_pct))
|
|
112
|
+
if not np.isfinite(t):
|
|
113
|
+
return 0.0, 0.0, 0.0
|
|
114
|
+
|
|
115
|
+
mask = (m2 >= t).astype(np.uint8) * 255
|
|
116
|
+
mask = self._largest_component_mask(mask)
|
|
117
|
+
|
|
118
|
+
cx, cy, conf = self._centroid(m2, mask)
|
|
119
|
+
return float(cx), float(cy), float(conf)
|
|
120
|
+
|
|
121
|
+
def shift_to_ref(self, img01: np.ndarray, ref_center: tuple[float, float]) -> tuple[float, float, float]:
|
|
122
|
+
"""
|
|
123
|
+
Returns (dx, dy, conf) shifting FROM current frame TO the provided reference center.
|
|
124
|
+
"""
|
|
125
|
+
cx, cy, conf = self.compute_center(img01)
|
|
126
|
+
if conf <= 0.0:
|
|
127
|
+
return 0.0, 0.0, 0.0
|
|
128
|
+
rx, ry = ref_center
|
|
129
|
+
return float(rx - cx), float(ry - cy), float(conf)
|
|
130
|
+
|
|
131
|
+
class SurfaceTracker:
|
|
132
|
+
"""
|
|
133
|
+
Tracks by translation using phase correlation between anchor patch and current patch.
|
|
134
|
+
Good for: lunar/solar/planetary surface close-ups.
|
|
135
|
+
"""
|
|
136
|
+
def __init__(self, anchor_patch: np.ndarray, hann_window: bool = True):
|
|
137
|
+
self.ref = np.asarray(anchor_patch, dtype=np.float32)
|
|
138
|
+
self.ref = _to_mono01(self.ref)
|
|
139
|
+
self.hann_window = bool(hann_window)
|
|
140
|
+
self._ref_fft = None
|
|
141
|
+
self._win = None
|
|
142
|
+
self._prep()
|
|
143
|
+
|
|
144
|
+
def _prep(self):
|
|
145
|
+
h, w = self.ref.shape
|
|
146
|
+
if self.hann_window:
|
|
147
|
+
wy = np.hanning(h).astype(np.float32)
|
|
148
|
+
wx = np.hanning(w).astype(np.float32)
|
|
149
|
+
self._win = (wy[:, None] * wx[None, :]).astype(np.float32)
|
|
150
|
+
else:
|
|
151
|
+
self._win = None
|
|
152
|
+
|
|
153
|
+
a = self.ref.copy()
|
|
154
|
+
a -= float(np.mean(a))
|
|
155
|
+
if self._win is not None:
|
|
156
|
+
a *= self._win
|
|
157
|
+
self._ref_fft = np.fft.rfft2(a)
|
|
158
|
+
|
|
159
|
+
def _phase_corr_shift(ref_m: np.ndarray, cur_m: np.ndarray) -> tuple[float, float, float]:
|
|
160
|
+
"""
|
|
161
|
+
Returns (dx, dy, response) such that shifting cur by (dx,dy) aligns to ref.
|
|
162
|
+
Uses cv2.phaseCorrelate with mean subtraction + Hann window.
|
|
163
|
+
"""
|
|
164
|
+
if cv2 is None:
|
|
165
|
+
return 0.0, 0.0, 0.0
|
|
166
|
+
|
|
167
|
+
ref = ref_m.astype(np.float32, copy=False)
|
|
168
|
+
cur = cur_m.astype(np.float32, copy=False)
|
|
169
|
+
|
|
170
|
+
# stabilize
|
|
171
|
+
ref = ref - float(ref.mean())
|
|
172
|
+
cur = cur - float(cur.mean())
|
|
173
|
+
|
|
174
|
+
# hann window (OpenCV expects float32)
|
|
175
|
+
try:
|
|
176
|
+
win = cv2.createHanningWindow((ref.shape[1], ref.shape[0]), cv2.CV_32F)
|
|
177
|
+
(dx, dy), resp = cv2.phaseCorrelate(ref, cur, win)
|
|
178
|
+
except Exception:
|
|
179
|
+
(dx, dy), resp = cv2.phaseCorrelate(ref, cur)
|
|
180
|
+
|
|
181
|
+
return float(dx), float(dy), float(resp)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@staticmethod
|
|
185
|
+
def _phase_corr(ref_m: np.ndarray, cur_m: np.ndarray) -> tuple[float, float, float]:
|
|
186
|
+
if cv2 is None:
|
|
187
|
+
return 0.0, 0.0, 0.0
|
|
188
|
+
|
|
189
|
+
ref = ref_m.astype(np.float32, copy=False)
|
|
190
|
+
cur = cur_m.astype(np.float32, copy=False)
|
|
191
|
+
|
|
192
|
+
ref = ref - float(ref.mean())
|
|
193
|
+
cur = cur - float(cur.mean())
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
win = cv2.createHanningWindow((ref.shape[1], ref.shape[0]), cv2.CV_32F)
|
|
197
|
+
(dx, dy), resp = cv2.phaseCorrelate(ref, cur, win)
|
|
198
|
+
except Exception:
|
|
199
|
+
(dx, dy), resp = cv2.phaseCorrelate(ref, cur)
|
|
200
|
+
|
|
201
|
+
return float(dx), float(dy), float(resp)
|
|
202
|
+
|
|
203
|
+
def step(self, cur_patch: np.ndarray) -> tuple[float, float, float]:
|
|
204
|
+
cur = _to_mono01(np.asarray(cur_patch, dtype=np.float32))
|
|
205
|
+
ref = self.ref
|
|
206
|
+
return self._phase_corr(ref, cur)
|