setiastrosuitepro 1.7.0__py3-none-any.whl → 1.7.0.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.
Potentially problematic release.
This version of setiastrosuitepro might be problematic. Click here for more details.
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/gui/main_window.py +87 -0
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +3 -1
- setiastro/saspro/imageops/serloader.py +262 -59
- setiastro/saspro/ser_stack_config.py +7 -1
- setiastro/saspro/ser_stacker.py +88 -23
- setiastro/saspro/ser_stacker_dialog.py +20 -1
- setiastro/saspro/serviewer.py +21 -5
- {setiastrosuitepro-1.7.0.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.7.0.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/RECORD +15 -15
- {setiastrosuitepro-1.7.0.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.0.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.7.0.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.7.0.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/licenses/license.txt +0 -0
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
# Auto-generated at build time. Do not edit.
|
|
2
|
-
BUILD_TIMESTAMP = "2026-01-
|
|
3
|
-
APP_VERSION = "1.7.0"
|
|
2
|
+
BUILD_TIMESTAMP = "2026-01-14T17:12:34Z"
|
|
3
|
+
APP_VERSION = "1.7.0.post2"
|
|
@@ -4430,6 +4430,93 @@ class AstroSuiteProMainWindow(
|
|
|
4430
4430
|
except Exception:
|
|
4431
4431
|
pass
|
|
4432
4432
|
|
|
4433
|
+
def _swap_rb_active(self):
|
|
4434
|
+
"""
|
|
4435
|
+
Swap R and B channels in the active RGB document (undoable).
|
|
4436
|
+
Intended for debayer/channel-order mismatches.
|
|
4437
|
+
"""
|
|
4438
|
+
dm = getattr(self, "docman", None)
|
|
4439
|
+
if dm is None:
|
|
4440
|
+
return
|
|
4441
|
+
|
|
4442
|
+
try:
|
|
4443
|
+
doc = dm.get_active_document()
|
|
4444
|
+
except Exception:
|
|
4445
|
+
doc = None
|
|
4446
|
+
if doc is None:
|
|
4447
|
+
return
|
|
4448
|
+
|
|
4449
|
+
img = getattr(doc, "image", None)
|
|
4450
|
+
if img is None:
|
|
4451
|
+
return
|
|
4452
|
+
|
|
4453
|
+
import numpy as np
|
|
4454
|
+
x = np.asarray(img)
|
|
4455
|
+
|
|
4456
|
+
# Must be RGB
|
|
4457
|
+
if not (x.ndim == 3 and x.shape[-1] == 3):
|
|
4458
|
+
try:
|
|
4459
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4460
|
+
except Exception:
|
|
4461
|
+
name = "Active"
|
|
4462
|
+
|
|
4463
|
+
if hasattr(self, "_log"):
|
|
4464
|
+
self._log(f"Swap R/B: '{name}' is not RGB (shape={getattr(x,'shape',None)}).")
|
|
4465
|
+
return
|
|
4466
|
+
|
|
4467
|
+
before_shape = x.shape
|
|
4468
|
+
before_dtype = x.dtype
|
|
4469
|
+
|
|
4470
|
+
# swap channels without changing dtype
|
|
4471
|
+
# (copy is safest so we don't mutate shared views)
|
|
4472
|
+
out = x.copy()
|
|
4473
|
+
out[..., 0], out[..., 2] = x[..., 2], x[..., 0]
|
|
4474
|
+
|
|
4475
|
+
# metadata: preserve existing, but annotate operation
|
|
4476
|
+
try:
|
|
4477
|
+
md = dict(getattr(doc, "metadata", None) or {})
|
|
4478
|
+
except Exception:
|
|
4479
|
+
md = {}
|
|
4480
|
+
|
|
4481
|
+
md["color_model"] = md.get("color_model", "RGB")
|
|
4482
|
+
md["channels"] = 3
|
|
4483
|
+
md["is_mono"] = False
|
|
4484
|
+
md["source"] = (md.get("source") or "Edit")
|
|
4485
|
+
|
|
4486
|
+
# If you track op params for history explorer
|
|
4487
|
+
md["__op_params__"] = {
|
|
4488
|
+
"op": "swap_rb",
|
|
4489
|
+
"from_shape": tuple(before_shape),
|
|
4490
|
+
"to_shape": tuple(out.shape),
|
|
4491
|
+
"dtype": str(before_dtype),
|
|
4492
|
+
}
|
|
4493
|
+
|
|
4494
|
+
try:
|
|
4495
|
+
name = getattr(doc, "display_name", lambda: None)() or getattr(doc, "name", "") or "Active"
|
|
4496
|
+
except Exception:
|
|
4497
|
+
name = "Active"
|
|
4498
|
+
|
|
4499
|
+
try:
|
|
4500
|
+
dm.update_active_document(
|
|
4501
|
+
out,
|
|
4502
|
+
metadata=md,
|
|
4503
|
+
step_name="Swap R ↔ B",
|
|
4504
|
+
doc=doc,
|
|
4505
|
+
)
|
|
4506
|
+
|
|
4507
|
+
if hasattr(self, "_log"):
|
|
4508
|
+
self._log(
|
|
4509
|
+
f"Swap R/B: '{name}' swapped channels "
|
|
4510
|
+
f"(shape={before_shape}, dtype={before_dtype})."
|
|
4511
|
+
)
|
|
4512
|
+
|
|
4513
|
+
except Exception:
|
|
4514
|
+
import traceback
|
|
4515
|
+
try:
|
|
4516
|
+
from PyQt6.QtWidgets import QMessageBox
|
|
4517
|
+
QMessageBox.critical(self, "Swap R/B", traceback.format_exc())
|
|
4518
|
+
except Exception:
|
|
4519
|
+
pass
|
|
4433
4520
|
|
|
4434
4521
|
|
|
4435
4522
|
def _on_stackingsuite_relaunch(self, old_dir: str, new_dir: str):
|
|
@@ -836,7 +836,9 @@ class ToolbarMixin:
|
|
|
836
836
|
self.act_mono_to_rgb = QAction(self.tr("Convert Mono to RGB"), self)
|
|
837
837
|
self.act_mono_to_rgb.setStatusTip(self.tr("Convert a mono image to RGB by duplicating the channel"))
|
|
838
838
|
self.act_mono_to_rgb.triggered.connect(self._convert_mono_to_rgb_active)
|
|
839
|
-
|
|
839
|
+
self.act_swap_rb = QAction(self.tr("Swap R and B Channels"), self)
|
|
840
|
+
self.act_swap_rb.setStatusTip(self.tr("Swap red and blue channels on the active RGB image"))
|
|
841
|
+
self.act_swap_rb.triggered.connect(self._swap_rb_active)
|
|
840
842
|
# Functions
|
|
841
843
|
self.act_crop = QAction(QIcon(cropicon_path), self.tr("Crop..."), self)
|
|
842
844
|
self.act_crop.setStatusTip(self.tr("Crop / rotate with handles"))
|
|
@@ -31,20 +31,44 @@ SER_HEADER_SIZE = 178
|
|
|
31
31
|
SER_SIGNATURE_LEN = 14
|
|
32
32
|
|
|
33
33
|
# Common SER color IDs (seen in the wild)
|
|
34
|
+
# NOTE: Many SER writers use:
|
|
35
|
+
# 0 = MONO
|
|
36
|
+
# 8..11 = Bayer (RGGB/GRBG/GBRG/BGGR)
|
|
37
|
+
# 24..27 = RGB/BGR/RGBA/BGRA
|
|
34
38
|
SER_COLOR = {
|
|
35
|
-
0: "MONO",
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
0: "MONO",
|
|
40
|
+
|
|
41
|
+
# Bayer (most common in planetary SER)
|
|
42
|
+
8: "BAYER_RGGB",
|
|
43
|
+
9: "BAYER_GRBG",
|
|
44
|
+
10: "BAYER_GBRG",
|
|
45
|
+
11: "BAYER_BGGR",
|
|
46
|
+
|
|
47
|
+
# Packed color
|
|
48
|
+
24: "RGB",
|
|
49
|
+
25: "BGR",
|
|
50
|
+
26: "RGBA",
|
|
51
|
+
27: "BGRA",
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
BAYER_NAMES = {"BAYER_RGGB", "BAYER_GRBG", "BAYER_GBRG", "BAYER_BGGR"}
|
|
47
|
-
|
|
55
|
+
BAYER_PATTERNS = ("BAYER_RGGB", "BAYER_GRBG", "BAYER_GBRG", "BAYER_BGGR")
|
|
56
|
+
|
|
57
|
+
def _normalize_bayer_pattern(p: Optional[str]) -> Optional[str]:
|
|
58
|
+
if not p:
|
|
59
|
+
return None
|
|
60
|
+
p = str(p).strip().upper()
|
|
61
|
+
if p == "AUTO":
|
|
62
|
+
return None
|
|
63
|
+
if p.startswith("BAYER_"):
|
|
64
|
+
if p in BAYER_PATTERNS:
|
|
65
|
+
return p
|
|
66
|
+
return None
|
|
67
|
+
# allow short names like "RGGB"
|
|
68
|
+
p2 = "BAYER_" + p
|
|
69
|
+
if p2 in BAYER_PATTERNS:
|
|
70
|
+
return p2
|
|
71
|
+
return None
|
|
48
72
|
|
|
49
73
|
@dataclass
|
|
50
74
|
class SerMeta:
|
|
@@ -112,6 +136,37 @@ def _cv2_debayer(mosaic: np.ndarray, pattern: str) -> np.ndarray:
|
|
|
112
136
|
raise ValueError(f"Unknown Bayer pattern: {pattern}")
|
|
113
137
|
return cv2.cvtColor(mosaic, code)
|
|
114
138
|
|
|
139
|
+
def _maybe_swap_rb_to_match_cv2(mosaic: np.ndarray, pattern: str, out: np.ndarray) -> np.ndarray:
|
|
140
|
+
"""
|
|
141
|
+
Ensure debayer output channel order matches OpenCV's RGB output.
|
|
142
|
+
Some fast debayers return BGR. We detect by comparing against cv2 on a small crop.
|
|
143
|
+
"""
|
|
144
|
+
if out is None or out.ndim != 3 or out.shape[2] < 3:
|
|
145
|
+
return out
|
|
146
|
+
|
|
147
|
+
# Compare on a small center crop for speed
|
|
148
|
+
H, W = mosaic.shape[:2]
|
|
149
|
+
cs = min(96, H, W)
|
|
150
|
+
y0 = max(0, (H - cs) // 2)
|
|
151
|
+
x0 = max(0, (W - cs) // 2)
|
|
152
|
+
m = mosaic[y0:y0+cs, x0:x0+cs]
|
|
153
|
+
|
|
154
|
+
ref = _cv2_debayer(m, pattern) # RGB
|
|
155
|
+
|
|
156
|
+
o = out[y0:y0+cs, x0:x0+cs, :3]
|
|
157
|
+
if o.dtype != ref.dtype:
|
|
158
|
+
# compare in float to avoid overflow
|
|
159
|
+
o_f = o.astype(np.float32)
|
|
160
|
+
ref_f = ref.astype(np.float32)
|
|
161
|
+
else:
|
|
162
|
+
o_f = o.astype(np.float32)
|
|
163
|
+
ref_f = ref.astype(np.float32)
|
|
164
|
+
|
|
165
|
+
d_same = float(np.mean(np.abs(o_f - ref_f)))
|
|
166
|
+
d_swap = float(np.mean(np.abs(o_f[..., ::-1] - ref_f)))
|
|
167
|
+
|
|
168
|
+
return out[..., ::-1].copy() if d_swap < d_same else out
|
|
169
|
+
|
|
115
170
|
|
|
116
171
|
def _try_numba_debayer(mosaic: np.ndarray, pattern: str) -> Optional[np.ndarray]:
|
|
117
172
|
"""
|
|
@@ -194,6 +249,7 @@ class SERReader:
|
|
|
194
249
|
self.meta = self._parse_header(self._mm)
|
|
195
250
|
self.meta.path = self.path
|
|
196
251
|
self._cache = _LRUCache(max_items=cache_items)
|
|
252
|
+
self._fast_debayer_is_bgr: Optional[bool] = None
|
|
197
253
|
|
|
198
254
|
def close(self):
|
|
199
255
|
try:
|
|
@@ -216,7 +272,6 @@ class SERReader:
|
|
|
216
272
|
self.close()
|
|
217
273
|
|
|
218
274
|
# ---------------- header parsing ----------------
|
|
219
|
-
|
|
220
275
|
@staticmethod
|
|
221
276
|
def _parse_header(mm: mmap.mmap) -> SerMeta:
|
|
222
277
|
if mm.size() < SER_HEADER_SIZE:
|
|
@@ -228,27 +283,12 @@ class SERReader:
|
|
|
228
283
|
sig_txt = _decode_cstr(sig)
|
|
229
284
|
|
|
230
285
|
# Be permissive: many SERs start with LUCAM-RECORDER
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
pass
|
|
286
|
+
# If not, still try parsing.
|
|
287
|
+
# (Some writers use other signatures but the v3 field layout often matches.)
|
|
234
288
|
|
|
235
|
-
# Layout (little-endian) commonly:
|
|
236
|
-
# 0: 14 bytes signature
|
|
237
|
-
# 14: uint32 LuID
|
|
238
|
-
# 18: uint32 ColorID
|
|
239
|
-
# 22: uint32 LittleEndian (0/1)
|
|
240
|
-
# 26: uint32 ImageWidth
|
|
241
|
-
# 30: uint32 ImageHeight
|
|
242
|
-
# 34: uint32 PixelDepth
|
|
243
|
-
# 38: uint32 FrameCount
|
|
244
|
-
# 42: char[40] Observer
|
|
245
|
-
# 82: char[40] Instrument
|
|
246
|
-
# 122: char[40] Telescope
|
|
247
|
-
# 162: uint64 DateTime
|
|
248
|
-
# 170: uint64 DateTimeUTC
|
|
249
289
|
try:
|
|
250
290
|
(lu_id, color_id, little_endian_u32,
|
|
251
|
-
|
|
291
|
+
w, h, pixel_depth, frames) = struct.unpack_from("<7I", hdr, SER_SIGNATURE_LEN)
|
|
252
292
|
except Exception as e:
|
|
253
293
|
raise ValueError(f"Failed to parse SER header fields: {e}")
|
|
254
294
|
|
|
@@ -261,26 +301,113 @@ class SERReader:
|
|
|
261
301
|
color_name = SER_COLOR.get(int(color_id), f"UNKNOWN({color_id})")
|
|
262
302
|
|
|
263
303
|
bps = _bytes_per_sample(int(pixel_depth))
|
|
304
|
+
data_offset = SER_HEADER_SIZE
|
|
305
|
+
file_size = mm.size()
|
|
306
|
+
|
|
307
|
+
def expected_size(frame_bytes: int, with_ts: bool) -> int:
|
|
308
|
+
base = data_offset + int(frames) * int(frame_bytes)
|
|
309
|
+
return base + (int(frames) * 8 if with_ts else 0)
|
|
264
310
|
|
|
265
|
-
#
|
|
266
|
-
#
|
|
267
|
-
|
|
268
|
-
|
|
311
|
+
# --- candidate interpretations ---
|
|
312
|
+
# Bayer/MONO: 1 sample per pixel
|
|
313
|
+
fb_1 = int(w) * int(h) * 1 * int(bps)
|
|
314
|
+
|
|
315
|
+
# RGB/BGR: 3 samples per pixel
|
|
316
|
+
fb_3 = int(w) * int(h) * 3 * int(bps)
|
|
317
|
+
|
|
318
|
+
# RGBA/BGRA: 4 samples per pixel
|
|
319
|
+
fb_4 = int(w) * int(h) * 4 * int(bps)
|
|
320
|
+
|
|
321
|
+
# Decide initial channels from color_name
|
|
269
322
|
if color_name in {"RGB", "BGR"}:
|
|
270
323
|
channels = 3
|
|
324
|
+
frame_bytes = fb_3
|
|
271
325
|
elif color_name in {"RGBA", "BGRA"}:
|
|
272
326
|
channels = 4
|
|
327
|
+
frame_bytes = fb_4
|
|
273
328
|
else:
|
|
329
|
+
# MONO + Bayer variants should land here
|
|
274
330
|
channels = 1
|
|
331
|
+
frame_bytes = fb_1
|
|
275
332
|
|
|
276
|
-
|
|
277
|
-
|
|
333
|
+
# --- sanity check against file size ---
|
|
334
|
+
# If the header mapping is wrong (very common culprit), infer channels by file size.
|
|
335
|
+
# We consider both "no timestamps" and "with timestamps".
|
|
336
|
+
def matches(frame_bytes: int) -> tuple[bool, bool]:
|
|
337
|
+
no_ts = (file_size == expected_size(frame_bytes, with_ts=False))
|
|
338
|
+
yes_ts = (file_size == expected_size(frame_bytes, with_ts=True))
|
|
339
|
+
return no_ts, yes_ts
|
|
340
|
+
|
|
341
|
+
m1_no, m1_ts = matches(fb_1)
|
|
342
|
+
m3_no, m3_ts = matches(fb_3)
|
|
343
|
+
m4_no, m4_ts = matches(fb_4)
|
|
344
|
+
|
|
345
|
+
# Prefer an exact match if one exists.
|
|
346
|
+
# Tie-break: if header says Bayer-ish, prefer 1ch; if header says RGB-ish, prefer 3/4ch.
|
|
347
|
+
picked = None # (channels, frame_bytes, has_ts)
|
|
348
|
+
|
|
349
|
+
# If our current interpretation matches, keep it
|
|
350
|
+
cur_no, cur_ts = matches(frame_bytes)
|
|
351
|
+
if cur_no or cur_ts:
|
|
352
|
+
picked = (channels, frame_bytes, bool(cur_ts))
|
|
278
353
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
354
|
+
else:
|
|
355
|
+
# Try to infer by file size
|
|
356
|
+
# Unique matches:
|
|
357
|
+
candidates = []
|
|
358
|
+
if m1_no: candidates.append((1, fb_1, False))
|
|
359
|
+
if m1_ts: candidates.append((1, fb_1, True))
|
|
360
|
+
if m3_no: candidates.append((3, fb_3, False))
|
|
361
|
+
if m3_ts: candidates.append((3, fb_3, True))
|
|
362
|
+
if m4_no: candidates.append((4, fb_4, False))
|
|
363
|
+
if m4_ts: candidates.append((4, fb_4, True))
|
|
364
|
+
|
|
365
|
+
if len(candidates) == 1:
|
|
366
|
+
picked = candidates[0]
|
|
367
|
+
elif len(candidates) > 1:
|
|
368
|
+
# tie-break using header hint
|
|
369
|
+
if _is_bayer(color_name) or color_name == "MONO":
|
|
370
|
+
# choose first 1ch match
|
|
371
|
+
for c in candidates:
|
|
372
|
+
if c[0] == 1:
|
|
373
|
+
picked = c
|
|
374
|
+
break
|
|
375
|
+
elif color_name in {"RGB", "BGR"}:
|
|
376
|
+
for c in candidates:
|
|
377
|
+
if c[0] == 3:
|
|
378
|
+
picked = c
|
|
379
|
+
break
|
|
380
|
+
elif color_name in {"RGBA", "BGRA"}:
|
|
381
|
+
for c in candidates:
|
|
382
|
+
if c[0] == 4:
|
|
383
|
+
picked = c
|
|
384
|
+
break
|
|
385
|
+
# still ambiguous: just pick the first (rare)
|
|
386
|
+
if picked is None:
|
|
387
|
+
picked = candidates[0]
|
|
388
|
+
|
|
389
|
+
if picked is None:
|
|
390
|
+
# Couldn’t reconcile sizes; fall back to header interpretation and best-effort ts flag
|
|
391
|
+
expected_no_ts = expected_size(frame_bytes, with_ts=False)
|
|
392
|
+
expected_with_ts = expected_size(frame_bytes, with_ts=True)
|
|
393
|
+
has_ts = (file_size == expected_with_ts)
|
|
394
|
+
else:
|
|
395
|
+
channels, frame_bytes, has_ts = picked
|
|
396
|
+
|
|
397
|
+
# If we inferred channels that contradict the header color_name, adjust color_name
|
|
398
|
+
# so the rest of the pipeline (debayer, etc.) behaves sensibly.
|
|
399
|
+
if channels == 1:
|
|
400
|
+
# If header said RGB but file is clearly 1ch, it's almost certainly Bayer.
|
|
401
|
+
# Keep UNKNOWN(...) if we truly don't know the Bayer order.
|
|
402
|
+
if color_name in {"RGB", "BGR", "RGBA", "BGRA"}:
|
|
403
|
+
# safest default: treat as RGGB if we have no better info
|
|
404
|
+
color_name = "BAYER_RGGB"
|
|
405
|
+
elif channels == 3:
|
|
406
|
+
if color_name not in {"RGB", "BGR"}:
|
|
407
|
+
color_name = "RGB"
|
|
408
|
+
elif channels == 4:
|
|
409
|
+
if color_name not in {"RGBA", "BGRA"}:
|
|
410
|
+
color_name = "RGBA"
|
|
284
411
|
|
|
285
412
|
return SerMeta(
|
|
286
413
|
path="",
|
|
@@ -291,7 +418,7 @@ class SERReader:
|
|
|
291
418
|
color_id=int(color_id),
|
|
292
419
|
color_name=color_name,
|
|
293
420
|
little_endian=little_endian,
|
|
294
|
-
data_offset=data_offset,
|
|
421
|
+
data_offset=int(data_offset),
|
|
295
422
|
frame_bytes=int(frame_bytes),
|
|
296
423
|
has_timestamps=bool(has_ts),
|
|
297
424
|
observer=observer,
|
|
@@ -299,6 +426,7 @@ class SERReader:
|
|
|
299
426
|
telescope=telescope,
|
|
300
427
|
)
|
|
301
428
|
|
|
429
|
+
|
|
302
430
|
# ---------------- core access ----------------
|
|
303
431
|
|
|
304
432
|
def frame_offset(self, i: int) -> int:
|
|
@@ -311,10 +439,11 @@ class SERReader:
|
|
|
311
439
|
self,
|
|
312
440
|
i: int,
|
|
313
441
|
*,
|
|
314
|
-
roi: Optional[Tuple[int, int, int, int]] = None,
|
|
442
|
+
roi: Optional[Tuple[int, int, int, int]] = None,
|
|
315
443
|
debayer: bool = True,
|
|
316
444
|
to_float01: bool = False,
|
|
317
445
|
force_rgb: bool = False,
|
|
446
|
+
bayer_pattern: Optional[str] = None, # ✅ NEW
|
|
318
447
|
) -> np.ndarray:
|
|
319
448
|
"""
|
|
320
449
|
Returns:
|
|
@@ -324,10 +453,13 @@ class SERReader:
|
|
|
324
453
|
roi is applied before debayer (and ROI origin evenized for Bayer).
|
|
325
454
|
"""
|
|
326
455
|
meta = self.meta
|
|
456
|
+
color_name = meta.color_name
|
|
457
|
+
user_pat = _normalize_bayer_pattern(bayer_pattern)
|
|
458
|
+
active_bayer = user_pat if user_pat is not None else (color_name if _is_bayer(color_name) else None)
|
|
327
459
|
|
|
328
460
|
# Cache key includes ROI + flags
|
|
329
461
|
roi_key = None if roi is None else tuple(int(v) for v in roi)
|
|
330
|
-
key = (int(i), roi_key, bool(debayer), bool(to_float01), bool(force_rgb))
|
|
462
|
+
key = (int(i), roi_key, bool(debayer), active_bayer, bool(to_float01), bool(force_rgb))
|
|
331
463
|
cached = self._cache.get(key)
|
|
332
464
|
if cached is not None:
|
|
333
465
|
return cached
|
|
@@ -342,7 +474,6 @@ class SERReader:
|
|
|
342
474
|
dtype = np.uint16
|
|
343
475
|
|
|
344
476
|
# Determine channels stored
|
|
345
|
-
color_name = meta.color_name
|
|
346
477
|
if color_name in {"RGB", "BGR"}:
|
|
347
478
|
ch = 3
|
|
348
479
|
elif color_name in {"RGBA", "BGRA"}:
|
|
@@ -369,7 +500,7 @@ class SERReader:
|
|
|
369
500
|
w = max(1, min(meta.width - x, w))
|
|
370
501
|
h = max(1, min(meta.height - y, h))
|
|
371
502
|
|
|
372
|
-
if
|
|
503
|
+
if (active_bayer is not None) and debayer:
|
|
373
504
|
x, y = _roi_evenize_for_bayer(x, y)
|
|
374
505
|
w = max(1, min(meta.width - x, w))
|
|
375
506
|
h = max(1, min(meta.height - y, h))
|
|
@@ -384,14 +515,19 @@ class SERReader:
|
|
|
384
515
|
if _is_bayer(color_name):
|
|
385
516
|
if debayer:
|
|
386
517
|
mosaic = img if img.ndim == 2 else img[..., 0]
|
|
387
|
-
|
|
518
|
+
pat = active_bayer or color_name # active_bayer will usually be set here
|
|
519
|
+
|
|
520
|
+
out = _try_numba_debayer(mosaic, pat)
|
|
388
521
|
if out is None:
|
|
389
|
-
out = _cv2_debayer(mosaic,
|
|
522
|
+
out = _cv2_debayer(mosaic, pat) # already RGB
|
|
523
|
+
else:
|
|
524
|
+
out = _maybe_swap_rb_to_match_cv2(mosaic, pat, out)
|
|
525
|
+
|
|
390
526
|
img = out
|
|
391
527
|
else:
|
|
392
|
-
# keep mosaic as mono
|
|
393
528
|
img = img if img.ndim == 2 else img[..., 0]
|
|
394
529
|
|
|
530
|
+
|
|
395
531
|
# Force RGB for mono (useful for consistent preview pipeline)
|
|
396
532
|
if force_rgb and img.ndim == 2:
|
|
397
533
|
img = np.stack([img, img, img], axis=-1)
|
|
@@ -468,6 +604,7 @@ class PlanetaryFrameSource:
|
|
|
468
604
|
debayer: bool = True,
|
|
469
605
|
to_float01: bool = False,
|
|
470
606
|
force_rgb: bool = False,
|
|
607
|
+
bayer_pattern: Optional[str] = None, # ✅ NEW
|
|
471
608
|
) -> np.ndarray:
|
|
472
609
|
raise NotImplementedError
|
|
473
610
|
|
|
@@ -548,38 +685,102 @@ class AVIReader(PlanetaryFrameSource):
|
|
|
548
685
|
debayer: bool = True,
|
|
549
686
|
to_float01: bool = False,
|
|
550
687
|
force_rgb: bool = False,
|
|
688
|
+
bayer_pattern: Optional[str] = None,
|
|
551
689
|
) -> np.ndarray:
|
|
690
|
+
|
|
552
691
|
roi_key = None if roi is None else tuple(int(v) for v in roi)
|
|
553
|
-
|
|
692
|
+
|
|
693
|
+
# User pattern:
|
|
694
|
+
# - None means AUTO (do not force debayer on 3-channel video)
|
|
695
|
+
# - A real value means: user explicitly wants debayering
|
|
696
|
+
user_pat = _normalize_bayer_pattern(bayer_pattern) # None == AUTO
|
|
697
|
+
pat_for_key = user_pat or "AUTO"
|
|
698
|
+
|
|
699
|
+
key = ("avi", int(i), roi_key, bool(debayer), pat_for_key, bool(to_float01), bool(force_rgb))
|
|
554
700
|
cached = self._cache.get(key)
|
|
555
701
|
if cached is not None:
|
|
556
702
|
return cached
|
|
557
703
|
|
|
558
|
-
|
|
704
|
+
frame = self._read_raw_frame_bgr(i) # usually (H,W,3) uint8 BGR
|
|
559
705
|
|
|
560
|
-
# ROI
|
|
706
|
+
# ROI first (but if we are going to debayer mosaic, ROI origin must be even-even)
|
|
561
707
|
if roi is not None:
|
|
562
708
|
x, y, w, h = [int(v) for v in roi]
|
|
563
|
-
H, W =
|
|
709
|
+
H, W = frame.shape[:2]
|
|
564
710
|
x = max(0, min(W - 1, x))
|
|
565
711
|
y = max(0, min(H - 1, y))
|
|
566
712
|
w = max(1, min(W - x, w))
|
|
567
713
|
h = max(1, min(H - y, h))
|
|
568
|
-
bgr = bgr[y:y + h, x:x + w]
|
|
569
714
|
|
|
570
|
-
|
|
571
|
-
|
|
715
|
+
# If user explicitly requests debayering, preserve Bayer phase
|
|
716
|
+
# (even-even origin) exactly like SER
|
|
717
|
+
if debayer and user_pat is not None:
|
|
718
|
+
x, y = _roi_evenize_for_bayer(x, y)
|
|
719
|
+
w = max(1, min(W - x, w))
|
|
720
|
+
h = max(1, min(H - y, h))
|
|
721
|
+
|
|
722
|
+
frame = frame[y:y + h, x:x + w]
|
|
723
|
+
|
|
724
|
+
img: np.ndarray
|
|
725
|
+
|
|
726
|
+
# ---------------------------------------------------------
|
|
727
|
+
# RAW MOSAIC AVI SUPPORT
|
|
728
|
+
#
|
|
729
|
+
# OpenCV often returns 3-channel frames even when the AVI is
|
|
730
|
+
# conceptually "raw mosaic". In that case, ONLY debayer when
|
|
731
|
+
# the user explicitly selected a Bayer pattern (not AUTO).
|
|
732
|
+
# ---------------------------------------------------------
|
|
733
|
+
|
|
734
|
+
# True mosaic frame decoded as single-channel
|
|
735
|
+
is_true_mosaic = (frame.ndim == 2) or (frame.ndim == 3 and frame.shape[2] == 1)
|
|
736
|
+
|
|
737
|
+
if debayer and (is_true_mosaic or (user_pat is not None)):
|
|
738
|
+
# If it's 3-channel but user requested debayer, treat as packed mosaic:
|
|
739
|
+
# take one channel (they should be identical if it's really mosaic-packed).
|
|
740
|
+
if frame.ndim == 3 and frame.shape[2] >= 3:
|
|
741
|
+
mosaic = frame[..., 0] # any channel is fine for packed mosaic
|
|
742
|
+
else:
|
|
743
|
+
mosaic = frame if frame.ndim == 2 else frame[..., 0]
|
|
744
|
+
|
|
745
|
+
# Choose pattern:
|
|
746
|
+
# - user_pat is guaranteed not None here if it's forced on 3ch
|
|
747
|
+
# - if it’s true mosaic and user left AUTO, default RGGB
|
|
748
|
+
pat = user_pat or "BAYER_RGGB"
|
|
572
749
|
|
|
573
|
-
|
|
750
|
+
out = _try_numba_debayer(mosaic, pat)
|
|
751
|
+
if out is None:
|
|
752
|
+
out = _cv2_debayer(mosaic, pat) # RGB
|
|
753
|
+
else:
|
|
754
|
+
out = _maybe_swap_rb_to_match_cv2(mosaic, pat, out)
|
|
755
|
+
|
|
756
|
+
img = out # RGB
|
|
574
757
|
|
|
575
|
-
|
|
758
|
+
else:
|
|
759
|
+
# Normal video path: decoded BGR -> RGB
|
|
760
|
+
if frame.ndim == 3 and frame.shape[2] >= 3:
|
|
761
|
+
img = frame[..., ::-1].copy()
|
|
762
|
+
else:
|
|
763
|
+
# Rare: frame came out mono but debayer is off
|
|
764
|
+
img = frame if frame.ndim == 2 else frame[..., 0]
|
|
765
|
+
if force_rgb:
|
|
766
|
+
img = np.stack([img, img, img], axis=-1)
|
|
767
|
+
|
|
768
|
+
# Normalize
|
|
576
769
|
if to_float01:
|
|
577
|
-
|
|
770
|
+
if img.dtype == np.uint8:
|
|
771
|
+
img = img.astype(np.float32) / 255.0
|
|
772
|
+
elif img.dtype == np.uint16:
|
|
773
|
+
img = img.astype(np.float32) / 65535.0
|
|
774
|
+
else:
|
|
775
|
+
img = np.clip(img.astype(np.float32), 0.0, 1.0)
|
|
776
|
+
|
|
777
|
+
# Optional force_rgb (mostly relevant if debayer=False and frame is mono)
|
|
778
|
+
if force_rgb and img.ndim == 2:
|
|
779
|
+
img = np.stack([img, img, img], axis=-1)
|
|
578
780
|
|
|
579
781
|
self._cache.put(key, img)
|
|
580
782
|
return img
|
|
581
783
|
|
|
582
|
-
|
|
583
784
|
# -----------------------------
|
|
584
785
|
# Image-sequence reader
|
|
585
786
|
# -----------------------------
|
|
@@ -687,7 +888,9 @@ class ImageSequenceReader(PlanetaryFrameSource):
|
|
|
687
888
|
debayer: bool = True,
|
|
688
889
|
to_float01: bool = False,
|
|
689
890
|
force_rgb: bool = False,
|
|
891
|
+
bayer_pattern: Optional[str] = None,
|
|
690
892
|
) -> np.ndarray:
|
|
893
|
+
_ = debayer, bayer_pattern # unused for sequences (for now)
|
|
691
894
|
i = int(i)
|
|
692
895
|
if i < 0 or i >= self.meta.frames:
|
|
693
896
|
raise IndexError(f"Frame index {i} out of range (0..{self.meta.frames-1})")
|
|
@@ -15,6 +15,7 @@ class SERStackConfig:
|
|
|
15
15
|
track_mode: TrackMode = "planetary"
|
|
16
16
|
surface_anchor: Optional[Tuple[int, int, int, int]] = None
|
|
17
17
|
keep_percent: float = 20.0
|
|
18
|
+
bayer_pattern: Optional[str] = None
|
|
18
19
|
|
|
19
20
|
# AP / alignment
|
|
20
21
|
ap_size: int = 64
|
|
@@ -38,7 +39,12 @@ class SERStackConfig:
|
|
|
38
39
|
self.track_mode = kwargs.pop("track_mode", "planetary")
|
|
39
40
|
self.surface_anchor = kwargs.pop("surface_anchor", None)
|
|
40
41
|
self.keep_percent = float(kwargs.pop("keep_percent", 20.0))
|
|
41
|
-
|
|
42
|
+
self.bayer_pattern = kwargs.pop("bayer_pattern", None)
|
|
43
|
+
if isinstance(self.bayer_pattern, str):
|
|
44
|
+
s = self.bayer_pattern.strip().upper()
|
|
45
|
+
self.bayer_pattern = s if s in ("RGGB", "BGGR", "GRBG", "GBRG") else None
|
|
46
|
+
else:
|
|
47
|
+
self.bayer_pattern = None
|
|
42
48
|
self.ap_size = int(kwargs.pop("ap_size", 64))
|
|
43
49
|
self.ap_spacing = int(kwargs.pop("ap_spacing", 48))
|
|
44
50
|
self.ap_min_mean = float(kwargs.pop("ap_min_mean", 0.03))
|
setiastro/saspro/ser_stacker.py
CHANGED
|
@@ -18,6 +18,44 @@ from setiastro.saspro.ser_stack_config import SERStackConfig
|
|
|
18
18
|
from setiastro.saspro.ser_tracking import PlanetaryTracker, SurfaceTracker, _to_mono01
|
|
19
19
|
from setiastro.saspro.imageops.serloader import open_planetary_source, PlanetaryFrameSource
|
|
20
20
|
|
|
21
|
+
_BAYER_TO_CV2 = {
|
|
22
|
+
"RGGB": cv2.COLOR_BayerRG2RGB,
|
|
23
|
+
"BGGR": cv2.COLOR_BayerBG2RGB,
|
|
24
|
+
"GRBG": cv2.COLOR_BayerGR2RGB,
|
|
25
|
+
"GBRG": cv2.COLOR_BayerGB2RGB,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def _cfg_bayer_pattern(cfg) -> str | None:
|
|
29
|
+
# cfg.bayer_pattern might be missing in older saved projects; be defensive
|
|
30
|
+
return getattr(cfg, "bayer_pattern", None)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _get_frame(src, idx: int, *, roi, debayer: bool, to_float01: bool, force_rgb: bool, bayer_pattern: str | None):
|
|
34
|
+
"""
|
|
35
|
+
Drop-in wrapper:
|
|
36
|
+
- passes cfg.bayer_pattern down to sources that support it
|
|
37
|
+
- stays compatible with sources whose get_frame() doesn't accept bayer_pattern yet
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
return src.get_frame(
|
|
41
|
+
int(idx),
|
|
42
|
+
roi=roi,
|
|
43
|
+
debayer=debayer,
|
|
44
|
+
to_float01=to_float01,
|
|
45
|
+
force_rgb=force_rgb,
|
|
46
|
+
bayer_pattern=bayer_pattern,
|
|
47
|
+
)
|
|
48
|
+
except TypeError:
|
|
49
|
+
# Back-compat: older PlanetaryFrameSource implementations
|
|
50
|
+
return src.get_frame(
|
|
51
|
+
int(idx),
|
|
52
|
+
roi=roi,
|
|
53
|
+
debayer=debayer,
|
|
54
|
+
to_float01=to_float01,
|
|
55
|
+
force_rgb=force_rgb,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
|
|
21
59
|
@dataclass
|
|
22
60
|
class AnalyzeResult:
|
|
23
61
|
frames_total: int
|
|
@@ -490,6 +528,7 @@ def _coarse_surface_ref_locked(
|
|
|
490
528
|
roi_used=None,
|
|
491
529
|
debayer: bool,
|
|
492
530
|
to_rgb: bool,
|
|
531
|
+
bayer_pattern: Optional[str] = None,
|
|
493
532
|
progress_cb=None,
|
|
494
533
|
progress_every: int = 25,
|
|
495
534
|
# tuning:
|
|
@@ -553,7 +592,8 @@ def _coarse_surface_ref_locked(
|
|
|
553
592
|
# ---------------------------
|
|
554
593
|
src0, owns0 = _ensure_source(source_obj, cache_items=2)
|
|
555
594
|
try:
|
|
556
|
-
img0 = src0
|
|
595
|
+
img0 = _get_frame(src0, 0, roi=roi, debayer=debayer, to_float01=True, force_rgb=bool(to_rgb), bayer_pattern=bayer_pattern)
|
|
596
|
+
|
|
557
597
|
ref0 = _to_mono01(img0).astype(np.float32, copy=False)
|
|
558
598
|
ref0 = _downN(ref0)
|
|
559
599
|
ref0p = _bandpass(ref0) if bandpass else (ref0 - float(ref0.mean()))
|
|
@@ -644,7 +684,8 @@ def _coarse_surface_ref_locked(
|
|
|
644
684
|
try:
|
|
645
685
|
pred_x, pred_y = float(rx0), float(ry0)
|
|
646
686
|
for b in boundaries[1:]:
|
|
647
|
-
img = srck
|
|
687
|
+
img = _get_frame(srck, b, roi=roi, debayer=debayer, to_float01=True, force_rgb=bool(to_rgb), bayer_pattern=bayer_pattern)
|
|
688
|
+
|
|
648
689
|
cur = _to_mono01(img).astype(np.float32, copy=False)
|
|
649
690
|
cur = _downN(cur)
|
|
650
691
|
curp = _bandpass(cur) if bandpass else (cur - float(cur.mean()))
|
|
@@ -682,15 +723,17 @@ def _coarse_surface_ref_locked(
|
|
|
682
723
|
pred_x, pred_y = start_pred.get(b, (float(rx0), float(ry0)))
|
|
683
724
|
# if boundary already computed above, keep it; start after b
|
|
684
725
|
i0 = b
|
|
726
|
+
if b in start_pred and b != 0:
|
|
727
|
+
i0 = b + 1 # boundary already solved with r_key
|
|
728
|
+
|
|
685
729
|
if i0 == 0:
|
|
686
|
-
i0 = 1
|
|
730
|
+
i0 = 1
|
|
687
731
|
for i in range(i0, e):
|
|
688
|
-
|
|
689
|
-
if i in start_pred and i != b:
|
|
732
|
+
if i in start_pred:
|
|
690
733
|
pred_x, pred_y = start_pred[i]
|
|
691
734
|
continue
|
|
692
735
|
|
|
693
|
-
img = src
|
|
736
|
+
img = _get_frame(src, i, roi=roi, debayer=debayer, to_float01=True, force_rgb=bool(to_rgb), bayer_pattern=bayer_pattern)
|
|
694
737
|
cur = _to_mono01(img).astype(np.float32, copy=False)
|
|
695
738
|
cur = _downN(cur)
|
|
696
739
|
curp = _bandpass(cur) if bandpass else (cur - float(cur.mean()))
|
|
@@ -720,7 +763,7 @@ def _coarse_surface_ref_locked(
|
|
|
720
763
|
try:
|
|
721
764
|
pred_x, pred_y = float(rx0), float(ry0)
|
|
722
765
|
for i in range(1, n):
|
|
723
|
-
img = src
|
|
766
|
+
img = _get_frame(src, i, roi=roi, debayer=debayer, to_float01=True, force_rgb=bool(to_rgb), bayer_pattern=bayer_pattern)
|
|
724
767
|
cur = _to_mono01(img).astype(np.float32, copy=False)
|
|
725
768
|
cur = _downN(cur)
|
|
726
769
|
curp = _bandpass(cur) if bandpass else (cur - float(cur.mean()))
|
|
@@ -854,6 +897,8 @@ def stack_ser(
|
|
|
854
897
|
keep_percent: float = 20.0,
|
|
855
898
|
track_mode: str = "planetary",
|
|
856
899
|
surface_anchor=None,
|
|
900
|
+
to_rgb: bool = False, # ✅ add this
|
|
901
|
+
bayer_pattern: Optional[str] = None, # ✅ strongly recommended since dialog passes it
|
|
857
902
|
analysis: AnalyzeResult | None = None,
|
|
858
903
|
local_warp: bool = True,
|
|
859
904
|
max_dim: int = 512,
|
|
@@ -866,6 +911,7 @@ def stack_ser(
|
|
|
866
911
|
drizzle_pixfrac: float = 0.80,
|
|
867
912
|
drizzle_kernel: str = "gaussian",
|
|
868
913
|
drizzle_sigma: float = 0.0,
|
|
914
|
+
|
|
869
915
|
) -> tuple[np.ndarray, dict]:
|
|
870
916
|
source_obj = source
|
|
871
917
|
|
|
@@ -908,7 +954,7 @@ def stack_ser(
|
|
|
908
954
|
ap_size = int(getattr(analysis, "ap_size", 64) or 64)
|
|
909
955
|
|
|
910
956
|
# frame shape for accumulator
|
|
911
|
-
first = src0
|
|
957
|
+
first = _get_frame(src0, int(keep_idx[0]), roi=roi, debayer=debayer, to_float01=True, force_rgb=False, bayer_pattern=bayer_pattern)
|
|
912
958
|
acc_shape = first.shape # (H,W) or (H,W,3)
|
|
913
959
|
finally:
|
|
914
960
|
if owns0:
|
|
@@ -986,7 +1032,7 @@ def stack_ser(
|
|
|
986
1032
|
wacc = 0.0
|
|
987
1033
|
|
|
988
1034
|
for i in chunk:
|
|
989
|
-
img = src
|
|
1035
|
+
img = _get_frame(src, int(i), roi=roi, debayer=debayer, to_float01=True, force_rgb=bool(to_rgb), bayer_pattern=bayer_pattern).astype(np.float32, copy=False)
|
|
990
1036
|
|
|
991
1037
|
# Global prior (from Analyze)
|
|
992
1038
|
gdx = float(analysis.dx[int(i)]) if (analysis.dx is not None) else 0.0
|
|
@@ -1127,6 +1173,7 @@ def _build_reference(
|
|
|
1127
1173
|
to_rgb: bool,
|
|
1128
1174
|
ref_mode: str,
|
|
1129
1175
|
ref_count: int,
|
|
1176
|
+
bayer_pattern=None,
|
|
1130
1177
|
) -> np.ndarray:
|
|
1131
1178
|
"""
|
|
1132
1179
|
ref_mode:
|
|
@@ -1134,7 +1181,7 @@ def _build_reference(
|
|
|
1134
1181
|
- "best_stack": return mean of best ref_count frames
|
|
1135
1182
|
"""
|
|
1136
1183
|
best_idx = int(order[0])
|
|
1137
|
-
f0 = src
|
|
1184
|
+
f0 = _get_frame(src, best_idx, roi=roi, debayer=debayer, to_float01=True, force_rgb=bool(to_rgb), bayer_pattern=bayer_pattern)
|
|
1138
1185
|
if ref_mode != "best_stack" or ref_count <= 1:
|
|
1139
1186
|
return f0.astype(np.float32, copy=False)
|
|
1140
1187
|
|
|
@@ -1142,7 +1189,7 @@ def _build_reference(
|
|
|
1142
1189
|
acc = np.zeros_like(f0, dtype=np.float32)
|
|
1143
1190
|
for j in range(k):
|
|
1144
1191
|
idx = int(order[j])
|
|
1145
|
-
fr = src
|
|
1192
|
+
fr = _get_frame(src, idx, roi=roi, debayer=debayer, to_float01=True, force_rgb=bool(to_rgb), bayer_pattern=bayer_pattern)
|
|
1146
1193
|
acc += fr.astype(np.float32, copy=False)
|
|
1147
1194
|
ref = acc / float(k)
|
|
1148
1195
|
return np.clip(ref, 0.0, 1.0).astype(np.float32, copy=False)
|
|
@@ -1168,6 +1215,7 @@ def analyze_ser(
|
|
|
1168
1215
|
smooth_sigma: float = 1.5, # kept for API compat
|
|
1169
1216
|
thresh_pct: float = 92.0, # kept for API compat
|
|
1170
1217
|
ref_mode: str = "best_frame", # "best_frame" or "best_stack"
|
|
1218
|
+
bayer_pattern: Optional[str] = None,
|
|
1171
1219
|
ref_count: int = 5,
|
|
1172
1220
|
max_dim: int = 512,
|
|
1173
1221
|
progress_cb=None,
|
|
@@ -1187,7 +1235,10 @@ def analyze_ser(
|
|
|
1187
1235
|
(B) AP search+refine that follows coarse, with outlier rejection,
|
|
1188
1236
|
(C) robust median -> final dx/dy/conf
|
|
1189
1237
|
"""
|
|
1238
|
+
|
|
1190
1239
|
source_obj = _cfg_get_source(cfg)
|
|
1240
|
+
bpat = bayer_pattern or _cfg_bayer_pattern(cfg)
|
|
1241
|
+
|
|
1191
1242
|
if not source_obj:
|
|
1192
1243
|
raise ValueError("SERStackConfig.source/ser_path is empty")
|
|
1193
1244
|
|
|
@@ -1254,12 +1305,13 @@ def analyze_ser(
|
|
|
1254
1305
|
src, owns = _ensure_source(source_obj, cache_items=0)
|
|
1255
1306
|
try:
|
|
1256
1307
|
for i in chunk.tolist():
|
|
1257
|
-
img =
|
|
1258
|
-
int(i),
|
|
1308
|
+
img = _get_frame(
|
|
1309
|
+
src, int(i),
|
|
1259
1310
|
roi=roi_used,
|
|
1260
1311
|
debayer=debayer,
|
|
1261
1312
|
to_float01=True,
|
|
1262
1313
|
force_rgb=bool(to_rgb),
|
|
1314
|
+
bayer_pattern=bpat,
|
|
1263
1315
|
)
|
|
1264
1316
|
m = _downsample_mono01(img, max_dim=max_dim)
|
|
1265
1317
|
|
|
@@ -1305,9 +1357,12 @@ def analyze_ser(
|
|
|
1305
1357
|
try:
|
|
1306
1358
|
if cfg.track_mode == "surface":
|
|
1307
1359
|
# Surface ref must be frame 0 in roi_used coords
|
|
1308
|
-
ref_img =
|
|
1309
|
-
|
|
1360
|
+
ref_img = _get_frame(
|
|
1361
|
+
src_ref, 0,
|
|
1362
|
+
roi=roi_used, debayer=debayer, to_float01=True, force_rgb=bool(to_rgb),
|
|
1363
|
+
bayer_pattern=bpat,
|
|
1310
1364
|
).astype(np.float32, copy=False)
|
|
1365
|
+
|
|
1311
1366
|
ref_mode = "first_frame"
|
|
1312
1367
|
ref_count = 1
|
|
1313
1368
|
else:
|
|
@@ -1319,7 +1374,9 @@ def analyze_ser(
|
|
|
1319
1374
|
to_rgb=to_rgb,
|
|
1320
1375
|
ref_mode=ref_mode,
|
|
1321
1376
|
ref_count=ref_count,
|
|
1377
|
+
bayer_pattern=bpat, # ✅ add this
|
|
1322
1378
|
).astype(np.float32, copy=False)
|
|
1379
|
+
|
|
1323
1380
|
finally:
|
|
1324
1381
|
if owns_ref:
|
|
1325
1382
|
try:
|
|
@@ -1384,6 +1441,7 @@ def analyze_ser(
|
|
|
1384
1441
|
roi_used=roi_used, # ✅ NEW
|
|
1385
1442
|
debayer=debayer,
|
|
1386
1443
|
to_rgb=to_rgb,
|
|
1444
|
+
bayer_pattern=bpat,
|
|
1387
1445
|
progress_cb=progress_cb,
|
|
1388
1446
|
progress_every=25,
|
|
1389
1447
|
down=2,
|
|
@@ -1426,12 +1484,13 @@ def analyze_ser(
|
|
|
1426
1484
|
src, owns = _ensure_source(source_obj, cache_items=0)
|
|
1427
1485
|
try:
|
|
1428
1486
|
for i in chunk.tolist():
|
|
1429
|
-
img =
|
|
1430
|
-
int(i),
|
|
1487
|
+
img = _get_frame(
|
|
1488
|
+
src, int(i),
|
|
1431
1489
|
roi=roi_used,
|
|
1432
1490
|
debayer=debayer,
|
|
1433
1491
|
to_float01=True,
|
|
1434
1492
|
force_rgb=bool(to_rgb),
|
|
1493
|
+
bayer_pattern=bpat,
|
|
1435
1494
|
)
|
|
1436
1495
|
cur_m = _to_mono01(img).astype(np.float32, copy=False)
|
|
1437
1496
|
|
|
@@ -1554,12 +1613,13 @@ def analyze_ser(
|
|
|
1554
1613
|
src, owns = _ensure_source(source_obj, cache_items=0)
|
|
1555
1614
|
try:
|
|
1556
1615
|
for i in chunk.tolist():
|
|
1557
|
-
img =
|
|
1558
|
-
int(i),
|
|
1616
|
+
img = _get_frame(
|
|
1617
|
+
src, int(i),
|
|
1559
1618
|
roi=roi_used,
|
|
1560
1619
|
debayer=debayer,
|
|
1561
1620
|
to_float01=True,
|
|
1562
1621
|
force_rgb=bool(to_rgb),
|
|
1622
|
+
bayer_pattern=bpat,
|
|
1563
1623
|
)
|
|
1564
1624
|
|
|
1565
1625
|
dx_i, dy_i, cf_i = tracker.shift_to_ref(img, ref_center)
|
|
@@ -1647,6 +1707,8 @@ def realign_ser(
|
|
|
1647
1707
|
- recompute coarse drift (ref-locked) on roi_track
|
|
1648
1708
|
- refine via AP search+refine FOLLOWING coarse + outlier rejection
|
|
1649
1709
|
"""
|
|
1710
|
+
bpat = bayer_pattern or _cfg_bayer_pattern(cfg)
|
|
1711
|
+
|
|
1650
1712
|
if analysis is None:
|
|
1651
1713
|
raise ValueError("analysis is None")
|
|
1652
1714
|
if analysis.ref_image is None:
|
|
@@ -1748,6 +1810,7 @@ def realign_ser(
|
|
|
1748
1810
|
roi_used=roi_used, # ✅ NEW
|
|
1749
1811
|
debayer=debayer,
|
|
1750
1812
|
to_rgb=to_rgb,
|
|
1813
|
+
bayer_pattern=bpat,
|
|
1751
1814
|
progress_cb=progress_cb,
|
|
1752
1815
|
progress_every=25,
|
|
1753
1816
|
down=2,
|
|
@@ -1776,12 +1839,13 @@ def realign_ser(
|
|
|
1776
1839
|
src, owns = _ensure_source(source_obj, cache_items=0)
|
|
1777
1840
|
try:
|
|
1778
1841
|
for i in chunk.tolist():
|
|
1779
|
-
img =
|
|
1780
|
-
int(i),
|
|
1842
|
+
img = _get_frame(
|
|
1843
|
+
src, int(i),
|
|
1781
1844
|
roi=roi_used,
|
|
1782
1845
|
debayer=debayer,
|
|
1783
1846
|
to_float01=True,
|
|
1784
1847
|
force_rgb=bool(to_rgb),
|
|
1848
|
+
bayer_pattern=bpat,
|
|
1785
1849
|
)
|
|
1786
1850
|
cur_m = _to_mono01(img).astype(np.float32, copy=False)
|
|
1787
1851
|
|
|
@@ -1902,12 +1966,13 @@ def realign_ser(
|
|
|
1902
1966
|
src, owns = _ensure_source(source_obj, cache_items=0)
|
|
1903
1967
|
try:
|
|
1904
1968
|
for i in chunk.tolist():
|
|
1905
|
-
img =
|
|
1906
|
-
int(i),
|
|
1969
|
+
img = _get_frame(
|
|
1970
|
+
src, int(i),
|
|
1907
1971
|
roi=roi_used,
|
|
1908
1972
|
debayer=debayer,
|
|
1909
1973
|
to_float01=True,
|
|
1910
1974
|
force_rgb=bool(to_rgb),
|
|
1975
|
+
bayer_pattern=bpat,
|
|
1911
1976
|
)
|
|
1912
1977
|
|
|
1913
1978
|
dx_i, dy_i, cf_i = tracker.shift_to_ref(img, ref_center)
|
|
@@ -252,6 +252,12 @@ class APEditorDialog(QDialog):
|
|
|
252
252
|
self.spin_ap_spacing.setRange(8, 256)
|
|
253
253
|
self.spin_ap_spacing.setSingleStep(8)
|
|
254
254
|
self.spin_ap_spacing.setValue(int(self._ap_spacing))
|
|
255
|
+
self.spin_ap_min_mean = QDoubleSpinBox(self)
|
|
256
|
+
self.spin_ap_min_mean.setRange(0.0, 1.0)
|
|
257
|
+
self.spin_ap_min_mean.setDecimals(3)
|
|
258
|
+
self.spin_ap_min_mean.setSingleStep(0.005)
|
|
259
|
+
self.spin_ap_min_mean.setValue(float(self._ap_min_mean))
|
|
260
|
+
self.spin_ap_min_mean.setToolTip("Minimum mean intensity (0..1) required for an AP tile to be placed.")
|
|
255
261
|
|
|
256
262
|
ap_row.addWidget(self.lbl_ap)
|
|
257
263
|
ap_row.addSpacing(6)
|
|
@@ -260,6 +266,9 @@ class APEditorDialog(QDialog):
|
|
|
260
266
|
ap_row.addSpacing(10)
|
|
261
267
|
ap_row.addWidget(QLabel("Spacing", self))
|
|
262
268
|
ap_row.addWidget(self.spin_ap_spacing)
|
|
269
|
+
ap_row.addSpacing(10)
|
|
270
|
+
ap_row.addWidget(QLabel("Min mean", self))
|
|
271
|
+
ap_row.addWidget(self.spin_ap_min_mean)
|
|
263
272
|
ap_row.addStretch(1)
|
|
264
273
|
|
|
265
274
|
outer.addLayout(ap_row, 0)
|
|
@@ -295,6 +304,7 @@ class APEditorDialog(QDialog):
|
|
|
295
304
|
self._ap_debounce.setSingleShot(True)
|
|
296
305
|
self._ap_debounce.setInterval(250) # ms
|
|
297
306
|
self._ap_debounce.timeout.connect(self._apply_ap_params_and_relayout)
|
|
307
|
+
self.spin_ap_min_mean.valueChanged.connect(self._schedule_ap_relayout)
|
|
298
308
|
|
|
299
309
|
# apply redraw when changed
|
|
300
310
|
self.spin_ap_size.valueChanged.connect(self._schedule_ap_relayout)
|
|
@@ -331,11 +341,13 @@ class APEditorDialog(QDialog):
|
|
|
331
341
|
# Commit params
|
|
332
342
|
self._ap_size = int(self.spin_ap_size.value())
|
|
333
343
|
self._ap_spacing = int(self.spin_ap_spacing.value())
|
|
344
|
+
self._ap_min_mean = float(self.spin_ap_min_mean.value())
|
|
334
345
|
|
|
335
346
|
# Re-autoplace using the updated params
|
|
336
347
|
self._do_autoplace()
|
|
337
348
|
|
|
338
349
|
|
|
350
|
+
|
|
339
351
|
def showEvent(self, e):
|
|
340
352
|
super().showEvent(e)
|
|
341
353
|
if self._fit_pending:
|
|
@@ -706,6 +718,7 @@ class _AnalyzeWorker(QThread):
|
|
|
706
718
|
self.cfg,
|
|
707
719
|
debayer=self.debayer,
|
|
708
720
|
to_rgb=self.to_rgb,
|
|
721
|
+
bayer_pattern=getattr(self.cfg, "bayer_pattern", None),
|
|
709
722
|
ref_mode=self.ref_mode,
|
|
710
723
|
ref_count=self.ref_count,
|
|
711
724
|
progress_cb=cb,
|
|
@@ -737,19 +750,21 @@ class _StackWorker(QThread):
|
|
|
737
750
|
self.cfg.source,
|
|
738
751
|
roi=self.cfg.roi,
|
|
739
752
|
debayer=self.debayer,
|
|
753
|
+
to_rgb=self.to_rgb,
|
|
754
|
+
bayer_pattern=getattr(self.cfg, "bayer_pattern", None), # ✅ add this
|
|
740
755
|
keep_percent=float(getattr(self.cfg, "keep_percent", 20.0)),
|
|
741
756
|
track_mode=str(getattr(self.cfg, "track_mode", "planetary")),
|
|
742
757
|
surface_anchor=getattr(self.cfg, "surface_anchor", None),
|
|
743
758
|
analysis=self.analysis,
|
|
744
759
|
local_warp=True,
|
|
745
760
|
progress_cb=cb,
|
|
746
|
-
|
|
747
761
|
drizzle_scale=float(getattr(self.cfg, "drizzle_scale", 1.0)),
|
|
748
762
|
drizzle_pixfrac=float(getattr(self.cfg, "drizzle_pixfrac", 0.80)),
|
|
749
763
|
drizzle_kernel=str(getattr(self.cfg, "drizzle_kernel", "gaussian")),
|
|
750
764
|
drizzle_sigma=float(getattr(self.cfg, "drizzle_sigma", 0.0)),
|
|
751
765
|
)
|
|
752
766
|
|
|
767
|
+
|
|
753
768
|
self.finished_ok.emit(out, diag)
|
|
754
769
|
except Exception as e:
|
|
755
770
|
msg = f"{e}\n\n{traceback.format_exc()}"
|
|
@@ -810,12 +825,14 @@ class SERStackerDialog(QDialog):
|
|
|
810
825
|
surface_anchor=None,
|
|
811
826
|
debayer: bool = True,
|
|
812
827
|
keep_percent: float = 20.0,
|
|
828
|
+
bayer_pattern: Optional[str] = None,
|
|
813
829
|
):
|
|
814
830
|
super().__init__(parent)
|
|
815
831
|
self.setWindowTitle("Planetary Stacker - Beta")
|
|
816
832
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
817
833
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
818
834
|
self.setModal(False)
|
|
835
|
+
self._bayer_pattern = bayer_pattern
|
|
819
836
|
# ---- Normalize inputs ------------------------------------------------
|
|
820
837
|
# If caller provided only `source`, treat string-source as ser_path too.
|
|
821
838
|
if source is None:
|
|
@@ -1185,6 +1202,7 @@ class SERStackerDialog(QDialog):
|
|
|
1185
1202
|
try:
|
|
1186
1203
|
self.spin_ap_size.setValue(int(dlg.ap_size()))
|
|
1187
1204
|
self.spin_ap_spacing.setValue(int(dlg.ap_spacing()))
|
|
1205
|
+
self.spin_ap_min.setValue(float(dlg.ap_min_mean()))
|
|
1188
1206
|
except Exception:
|
|
1189
1207
|
pass
|
|
1190
1208
|
|
|
@@ -1377,6 +1395,7 @@ class SERStackerDialog(QDialog):
|
|
|
1377
1395
|
track_mode=self._track_mode_value(),
|
|
1378
1396
|
surface_anchor=self._surface_anchor,
|
|
1379
1397
|
keep_percent=float(self.spin_keep.value()),
|
|
1398
|
+
bayer_pattern=self._bayer_pattern,
|
|
1380
1399
|
|
|
1381
1400
|
ap_size=int(self.spin_ap_size.value()),
|
|
1382
1401
|
ap_spacing=int(self.spin_ap_spacing.value()),
|
setiastro/saspro/serviewer.py
CHANGED
|
@@ -163,6 +163,9 @@ class SERViewer(QDialog):
|
|
|
163
163
|
|
|
164
164
|
self.chk_debayer = QCheckBox("Debayer (Bayer SER)", self)
|
|
165
165
|
self.chk_debayer.setChecked(True)
|
|
166
|
+
self.cmb_bayer = QComboBox(self)
|
|
167
|
+
self.cmb_bayer.addItems(["AUTO", "RGGB", "GRBG", "GBRG", "BGGR"])
|
|
168
|
+
self.cmb_bayer.setCurrentText("AUTO") # ✅ default for raw mosaic AVI
|
|
166
169
|
|
|
167
170
|
self.chk_autostretch = QCheckBox("Autostretch preview (linked)", self)
|
|
168
171
|
self.chk_autostretch.setChecked(False)
|
|
@@ -188,6 +191,7 @@ class SERViewer(QDialog):
|
|
|
188
191
|
form.addRow("ROI size", row2)
|
|
189
192
|
|
|
190
193
|
form.addRow("", self.chk_debayer)
|
|
194
|
+
form.addRow("Bayer pattern", self.cmb_bayer)
|
|
191
195
|
form.addRow("", self.chk_autostretch)
|
|
192
196
|
|
|
193
197
|
# --- Preview tone controls (DISPLAY ONLY) ---
|
|
@@ -264,7 +268,9 @@ class SERViewer(QDialog):
|
|
|
264
268
|
|
|
265
269
|
self.cmb_track.currentIndexChanged.connect(self._on_track_mode_changed)
|
|
266
270
|
self.btn_stack.clicked.connect(self._open_stacker_clicked)
|
|
267
|
-
|
|
271
|
+
self.cmb_bayer.currentIndexChanged.connect(self._refresh)
|
|
272
|
+
self.chk_debayer.toggled.connect(lambda v: self.cmb_bayer.setEnabled(bool(v)))
|
|
273
|
+
self.cmb_bayer.setEnabled(self.chk_debayer.isChecked())
|
|
268
274
|
self.resize(1200, 800)
|
|
269
275
|
|
|
270
276
|
|
|
@@ -728,19 +734,28 @@ class SERViewer(QDialog):
|
|
|
728
734
|
except Exception:
|
|
729
735
|
current_doc = None
|
|
730
736
|
|
|
737
|
+
debayer = bool(self.chk_debayer.isChecked())
|
|
738
|
+
|
|
739
|
+
# Normalize: "AUTO" means "let the loader decide"
|
|
740
|
+
bp = self.cmb_bayer.currentText().strip().upper()
|
|
741
|
+
if not debayer or bp == "AUTO":
|
|
742
|
+
bp = None
|
|
743
|
+
|
|
731
744
|
dlg = SERStackerDialog(
|
|
732
745
|
parent=self,
|
|
733
746
|
main=main,
|
|
734
747
|
source_doc=current_doc,
|
|
735
|
-
ser_path=ser_path,
|
|
736
|
-
source=source,
|
|
748
|
+
ser_path=ser_path,
|
|
749
|
+
source=source,
|
|
737
750
|
roi=roi,
|
|
738
751
|
track_mode=self._track_mode_value(),
|
|
739
752
|
surface_anchor=anchor,
|
|
740
|
-
debayer=
|
|
753
|
+
debayer=debayer,
|
|
754
|
+
bayer_pattern=bp, # ✅ THIS IS THE FIX
|
|
741
755
|
keep_percent=float(self.spin_keep.value()),
|
|
742
756
|
)
|
|
743
757
|
|
|
758
|
+
|
|
744
759
|
dlg.stackProduced.connect(self._on_stacker_produced)
|
|
745
760
|
dlg.show()
|
|
746
761
|
dlg.raise_()
|
|
@@ -1119,8 +1134,9 @@ class SERViewer(QDialog):
|
|
|
1119
1134
|
self._cur,
|
|
1120
1135
|
roi=roi,
|
|
1121
1136
|
debayer=debayer,
|
|
1122
|
-
to_float01=True,
|
|
1137
|
+
to_float01=True,
|
|
1123
1138
|
force_rgb=False,
|
|
1139
|
+
bayer_pattern=self.cmb_bayer.currentText(), # ✅ NEW
|
|
1124
1140
|
)
|
|
1125
1141
|
except Exception as e:
|
|
1126
1142
|
QMessageBox.warning(self, "SER Viewer", f"Frame read failed:\n{e}")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: setiastrosuitepro
|
|
3
|
-
Version: 1.7.0
|
|
3
|
+
Version: 1.7.0.post2
|
|
4
4
|
Summary: Seti Astro Suite Pro - Advanced astrophotography toolkit for image calibration, stacking, registration, photometry, and visualization
|
|
5
5
|
License: GPL-3.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -180,7 +180,7 @@ setiastro/qml/ResourceMonitor.qml,sha256=k9_qXKAZLi8vj-5BffJTJu_UkRnxunZKn53Hthd
|
|
|
180
180
|
setiastro/saspro/__init__.py,sha256=C6Puq5PQr3n0aJ-2i_YAaUhTjPI-pcSzie3_tij3hhw,608
|
|
181
181
|
setiastro/saspro/__main__.py,sha256=eghNEGSQqHQjNp0BY84T-nhQMPYSnWdIN_eRFvbLj3Q,38487
|
|
182
182
|
setiastro/saspro/_generated/__init__.py,sha256=HbruQfKNbbVL4kh_t4oVG3UeUieaW8MUaqIcDCmnTvA,197
|
|
183
|
-
setiastro/saspro/_generated/build_info.py,sha256=
|
|
183
|
+
setiastro/saspro/_generated/build_info.py,sha256=AzyXhiQpAdluKi6-5HJ5_ryjz9slXCnNho7W4Ff0wB4,117
|
|
184
184
|
setiastro/saspro/abe.py,sha256=ao9UiOneVvgubI19TCE4gk2FNfW_E3D8rr0D_bmQOwI,59779
|
|
185
185
|
setiastro/saspro/abe_preset.py,sha256=u9t16yTb9v98tLjhvh496Fsp3Z-dNiBSs5itnAaJwh8,7750
|
|
186
186
|
setiastro/saspro/aberration_ai.py,sha256=o-l0NQFy8YJXqCUAnp_fT7pjxfYgrCAGircjw3KtMuU,37196
|
|
@@ -234,16 +234,16 @@ setiastro/saspro/ghs_preset.py,sha256=Zw3MJH5rEz7nqLdlmRBm3vYXgyopoupyDGAhM-PRXq
|
|
|
234
234
|
setiastro/saspro/graxpert.py,sha256=XSLeBhlAY2DkYUw93j2OEOuPLOPfzWYcT5Dz3JhhpW8,22579
|
|
235
235
|
setiastro/saspro/graxpert_preset.py,sha256=ESds2NPMPfsBHWNBfyYZ1rFpQxZ9gPOtxwz8enHfFfc,10719
|
|
236
236
|
setiastro/saspro/gui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
237
|
-
setiastro/saspro/gui/main_window.py,sha256=
|
|
237
|
+
setiastro/saspro/gui/main_window.py,sha256=_2BS65Q10AXCtoZXkX4cSi-_XnxMkJtpW35Onil0yM0,372789
|
|
238
238
|
setiastro/saspro/gui/mixins/__init__.py,sha256=ubdTIri0xoRs6MwDnEaVsAHbMxuPqz0CZcYcue3Mkcc,836
|
|
239
239
|
setiastro/saspro/gui/mixins/dock_mixin.py,sha256=C6IWKM-uP2ekqO7ozVQQcTnEgAkUL_OaRZbncIdrh8g,23893
|
|
240
240
|
setiastro/saspro/gui/mixins/file_mixin.py,sha256=ZzKzJhnNKjbi7UQHFhq4Nr2tNLRbB_fQeMHcOj09BU8,17714
|
|
241
241
|
setiastro/saspro/gui/mixins/geometry_mixin.py,sha256=x-HyLXGFhEs8SJuXT8EF6tS3XJaH8IhAP-ZFJ2BS2Lw,19038
|
|
242
242
|
setiastro/saspro/gui/mixins/header_mixin.py,sha256=kfZUJviB61c8NBXFB3MVBEcRPX_36ZV8tUZNJkDmMJ0,17253
|
|
243
243
|
setiastro/saspro/gui/mixins/mask_mixin.py,sha256=hrC5jLWxUZgQiYUXp7nvECo2uiarK7_HKgawG4HGB9w,15711
|
|
244
|
-
setiastro/saspro/gui/mixins/menu_mixin.py,sha256=
|
|
244
|
+
setiastro/saspro/gui/mixins/menu_mixin.py,sha256=7vDRszE6Q7w9odRzoBxmn_gBQBTuRAwEQcdFDPLzt3I,16636
|
|
245
245
|
setiastro/saspro/gui/mixins/theme_mixin.py,sha256=td6hYnZn17lXlFxQOXgK7-qfKo6CIJACF8_zrULlEkc,20740
|
|
246
|
-
setiastro/saspro/gui/mixins/toolbar_mixin.py,sha256=
|
|
246
|
+
setiastro/saspro/gui/mixins/toolbar_mixin.py,sha256=m4qGqiQolMqqipdrKseXc0-eW7GeD0keBkjrrHhz6Ys,92180
|
|
247
247
|
setiastro/saspro/gui/mixins/update_mixin.py,sha256=rq7J0Pcn_gO8UehCGVtcSvf-MDbmeGf796h0JBGr0sM,16545
|
|
248
248
|
setiastro/saspro/gui/mixins/view_mixin.py,sha256=EacXxtoAvkectGgLiahf3rHv2AaqKDQUteocVV_P834,17850
|
|
249
249
|
setiastro/saspro/gui/statistics_dialog.py,sha256=celhcsHcp3lNpox_X0ByAiYE80qFaF-dYyP8StzgrcU,1955
|
|
@@ -259,7 +259,7 @@ setiastro/saspro/imageops/__init__.py,sha256=CE9mHOsruHOQ5bYOHr1_fhxd9sdK1W9BpAi
|
|
|
259
259
|
setiastro/saspro/imageops/mdi_snap.py,sha256=Xyogrv3N0KRAKcfC65hy_PKG6ZktjmwDBisCQ_cP9pY,10117
|
|
260
260
|
setiastro/saspro/imageops/narrowband_normalization.py,sha256=q1KdHS7bjRYW-N0Jbszxf0TF38tNNgXKknM80lrzUsA,26793
|
|
261
261
|
setiastro/saspro/imageops/scnr.py,sha256=jLHxBU4KQ9Mu61Cym5YUq0pwxajJmGrPQRV2pug9LXE,1330
|
|
262
|
-
setiastro/saspro/imageops/serloader.py,sha256=
|
|
262
|
+
setiastro/saspro/imageops/serloader.py,sha256=OBLD0G4UJuHgY946NE7ViYiarPKOKWfWw9uCxBlZYsM,32374
|
|
263
263
|
setiastro/saspro/imageops/starbasedwhitebalance.py,sha256=urssLdgKD7J_LOuscjbf9zFImnUWoH-sotGvtbxfxTo,6589
|
|
264
264
|
setiastro/saspro/imageops/stretch.py,sha256=E_Ydh5VxUHrehwivhvVDvF8y89U6SRyjIFUQgJjHWXI,27113
|
|
265
265
|
setiastro/saspro/isophote.py,sha256=eSzlyBN_tZJSJTKlnBjwHY0zdw0SXPV6BRk_jt9TlVU,53753
|
|
@@ -320,11 +320,11 @@ setiastro/saspro/runtime_imports.py,sha256=tgIHH10cdAEbCTOmcs7retQhAww2dEc_3mKrv
|
|
|
320
320
|
setiastro/saspro/runtime_torch.py,sha256=IEVqD8jgn6la8t3R9ngZ2egSGMAC_tr1im3R8BnvXTQ,33497
|
|
321
321
|
setiastro/saspro/save_options.py,sha256=HPMXl5ya7HlCgDle9mPUrthzIFVJWXQGrea_OkcHg6c,2722
|
|
322
322
|
setiastro/saspro/selective_color.py,sha256=hx-9X4jeso797ifoBmwp9hNagPW4cvNPs9-T0JLHce0,64804
|
|
323
|
-
setiastro/saspro/ser_stack_config.py,sha256=
|
|
324
|
-
setiastro/saspro/ser_stacker.py,sha256=
|
|
325
|
-
setiastro/saspro/ser_stacker_dialog.py,sha256=
|
|
323
|
+
setiastro/saspro/ser_stack_config.py,sha256=s5eaxMB6wuCButMkSyz1-VlOysJZ_vmGC4IA6ZPmiSw,3209
|
|
324
|
+
setiastro/saspro/ser_stacker.py,sha256=I2nHACDtvfeSeDxg6rfWvfMYBZ_7XcAly1XfVEso7bQ,84259
|
|
325
|
+
setiastro/saspro/ser_stacker_dialog.py,sha256=QLrD6-wD4oCyXV6gmtXrZP0jh7hq9WyOeoswqG3zm2A,55743
|
|
326
326
|
setiastro/saspro/ser_tracking.py,sha256=z5jHJIs0t3kK3p5C330vwz0c6zJ9KXtKIh1FlJ1LljU,7141
|
|
327
|
-
setiastro/saspro/serviewer.py,sha256=
|
|
327
|
+
setiastro/saspro/serviewer.py,sha256=HkRPKq9sTX-NEVGS5bf32VtbcJRXJh7SA4ZokfZ1e_Q,48413
|
|
328
328
|
setiastro/saspro/sfcc.py,sha256=mKdNfbMxl4fgqyZnuAlkzNNIU3huYOHgqw0FGjtVhUs,89097
|
|
329
329
|
setiastro/saspro/shortcuts.py,sha256=QvFBXN_S8jqEwaP9m4pJMLVqzBmxo5HrjWhVCV9etQg,138256
|
|
330
330
|
setiastro/saspro/signature_insert.py,sha256=pWDxUO1Rxm27_fHSo2Y99bdOD2iG9q4AUjGR20x6TiA,70401
|
|
@@ -403,9 +403,9 @@ setiastro/saspro/wimi.py,sha256=CNo833Pur9P-A1DUSntlAaQWekf6gzWIvetOMvLDrOw,3146
|
|
|
403
403
|
setiastro/saspro/wims.py,sha256=HDfVI3Ckf5OJEJLH8NI36pFc2USZnETpb4UDIvweNX4,27450
|
|
404
404
|
setiastro/saspro/window_shelf.py,sha256=jQUifB3uJ9tkzXqmscWj8lHQN5E8yleuRc7hDnes4-k,7453
|
|
405
405
|
setiastro/saspro/xisf.py,sha256=Ah1CXDAohN__ej1Lq7LPU8vGLnDz8fluLQTGE71aUoc,52669
|
|
406
|
-
setiastrosuitepro-1.7.0.dist-info/entry_points.txt,sha256=vJfnZaV6Uj3laETakqT8-D-aJZI_nYIicrhSoL0uuko,227
|
|
407
|
-
setiastrosuitepro-1.7.0.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
408
|
-
setiastrosuitepro-1.7.0.dist-info/licenses/license.txt,sha256=KCwYZ9VpVwmzjelDq1BzzWqpBvt9nbbapa-woz47hfQ,123930
|
|
409
|
-
setiastrosuitepro-1.7.0.dist-info/METADATA,sha256=
|
|
410
|
-
setiastrosuitepro-1.7.0.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
411
|
-
setiastrosuitepro-1.7.0.dist-info/RECORD,,
|
|
406
|
+
setiastrosuitepro-1.7.0.post2.dist-info/entry_points.txt,sha256=vJfnZaV6Uj3laETakqT8-D-aJZI_nYIicrhSoL0uuko,227
|
|
407
|
+
setiastrosuitepro-1.7.0.post2.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
|
|
408
|
+
setiastrosuitepro-1.7.0.post2.dist-info/licenses/license.txt,sha256=KCwYZ9VpVwmzjelDq1BzzWqpBvt9nbbapa-woz47hfQ,123930
|
|
409
|
+
setiastrosuitepro-1.7.0.post2.dist-info/METADATA,sha256=ObLW50Nm7d43lX3bqkwNRrRjwv51_0dEY52ebyNFHW0,9840
|
|
410
|
+
setiastrosuitepro-1.7.0.post2.dist-info/WHEEL,sha256=zp0Cn7JsFoX2ATtOhtaFYIiE2rmFAD4OcMhtUki8W3U,88
|
|
411
|
+
setiastrosuitepro-1.7.0.post2.dist-info/RECORD,,
|
|
File without changes
|
{setiastrosuitepro-1.7.0.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/entry_points.txt
RENAMED
|
File without changes
|
{setiastrosuitepro-1.7.0.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|
{setiastrosuitepro-1.7.0.dist-info → setiastrosuitepro-1.7.0.post2.dist-info}/licenses/license.txt
RENAMED
|
File without changes
|