setiastrosuitepro 1.7.1.post2__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/saspro/__init__.py +9 -8
- setiastro/saspro/__main__.py +326 -285
- setiastro/saspro/_generated/build_info.py +2 -2
- setiastro/saspro/doc_manager.py +4 -1
- setiastro/saspro/gui/main_window.py +41 -2
- setiastro/saspro/gui/mixins/file_mixin.py +6 -2
- setiastro/saspro/gui/mixins/menu_mixin.py +1 -0
- setiastro/saspro/gui/mixins/toolbar_mixin.py +8 -1
- setiastro/saspro/imageops/serloader.py +101 -17
- setiastro/saspro/layers.py +186 -10
- setiastro/saspro/layers_dock.py +198 -5
- setiastro/saspro/legacy/image_manager.py +10 -4
- setiastro/saspro/planetprojection.py +3854 -0
- setiastro/saspro/resources.py +2 -0
- setiastro/saspro/save_options.py +45 -13
- setiastro/saspro/ser_stack_config.py +21 -1
- setiastro/saspro/ser_stacker.py +8 -2
- setiastro/saspro/ser_stacker_dialog.py +37 -10
- setiastro/saspro/ser_tracking.py +57 -35
- setiastro/saspro/serviewer.py +164 -16
- setiastro/saspro/subwindow.py +36 -1
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/METADATA +1 -1
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/RECORD +28 -26
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/WHEEL +0 -0
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/entry_points.txt +0 -0
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/LICENSE +0 -0
- {setiastrosuitepro-1.7.1.post2.dist-info → setiastrosuitepro-1.7.3.dist-info}/licenses/license.txt +0 -0
setiastro/saspro/resources.py
CHANGED
|
@@ -246,6 +246,7 @@ class Icons:
|
|
|
246
246
|
LIVE_STACKING = property(lambda self: _resource_path('livestacking.png'))
|
|
247
247
|
IMAGE_COMBINE = property(lambda self: _resource_path('imagecombine.png'))
|
|
248
248
|
PLANETARY_STACKER = property(lambda self: _resource_path('planetarystacker.png'))
|
|
249
|
+
PLANET_PROJECTION = property(lambda self: _resource_path('3dplanet.png'))
|
|
249
250
|
|
|
250
251
|
# Moon phase (WIMS)
|
|
251
252
|
MOON_NEW = property(lambda self: _resource_path('new_moon.png'))
|
|
@@ -551,6 +552,7 @@ def _init_legacy_paths():
|
|
|
551
552
|
'rgbalign_path': get_icon_path('rgbalign.png'),
|
|
552
553
|
'background_path': get_icon_path('background.png'),
|
|
553
554
|
'script_icon_path': get_icon_path('script.png'),
|
|
555
|
+
'planetprojection_path': get_icon_path('3dplanet.png'),
|
|
554
556
|
}
|
|
555
557
|
|
|
556
558
|
|
setiastro/saspro/save_options.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# pro/save_options.py
|
|
2
2
|
from __future__ import annotations
|
|
3
|
-
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QPushButton
|
|
3
|
+
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QHBoxLayout, QLabel, QComboBox, QPushButton, QSpinBox, QWidget
|
|
4
4
|
from PyQt6.QtCore import Qt
|
|
5
5
|
|
|
6
|
+
|
|
6
7
|
from setiastro.saspro.file_utils import _normalize_ext
|
|
7
8
|
|
|
8
9
|
# Allowed bit depths per output format (what your saver actually supports)
|
|
@@ -16,22 +17,28 @@ _BIT_DEPTHS = {
|
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
class SaveOptionsDialog(QDialog):
|
|
19
|
-
def __init__(
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
parent,
|
|
23
|
+
target_ext: str,
|
|
24
|
+
current_bit_depth: str | None,
|
|
25
|
+
current_jpeg_quality: int | None = None,
|
|
26
|
+
):
|
|
20
27
|
super().__init__(parent)
|
|
21
28
|
self.setWindowTitle(self.tr("Save Options"))
|
|
22
29
|
self.setWindowFlag(Qt.WindowType.Window, True)
|
|
23
30
|
self.setWindowModality(Qt.WindowModality.NonModal)
|
|
24
31
|
self.setModal(False)
|
|
25
|
-
#self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose, True)
|
|
26
32
|
|
|
27
|
-
|
|
28
|
-
raw_ext = (target_ext or "").lower().strip()
|
|
33
|
+
self.jpeg_quality_spin = None
|
|
29
34
|
|
|
30
|
-
#
|
|
35
|
+
# -----------------------------
|
|
36
|
+
# Normalize extension FIRST
|
|
37
|
+
# -----------------------------
|
|
38
|
+
raw_ext = (target_ext or "").lower().strip()
|
|
31
39
|
if "." in raw_ext:
|
|
32
40
|
raw_ext = raw_ext.split(".")[-1]
|
|
33
41
|
|
|
34
|
-
# Handle common synonyms / compressed variants
|
|
35
42
|
if raw_ext in ("fit", "fits", "fz", "fits.gz", "fit.gz"):
|
|
36
43
|
self._ext = "fits"
|
|
37
44
|
elif raw_ext in ("tif", "tiff"):
|
|
@@ -39,19 +46,42 @@ class SaveOptionsDialog(QDialog):
|
|
|
39
46
|
elif raw_ext in ("jpg", "jpeg"):
|
|
40
47
|
self._ext = "jpg"
|
|
41
48
|
else:
|
|
42
|
-
# Fallback – already lowercase, no leading dot
|
|
43
49
|
self._ext = raw_ext
|
|
44
50
|
|
|
45
51
|
allowed = _BIT_DEPTHS.get(self._ext, ["32-bit floating point"])
|
|
46
52
|
|
|
53
|
+
# -----------------------------
|
|
54
|
+
# Build layout
|
|
55
|
+
# -----------------------------
|
|
56
|
+
lay = QVBoxLayout(self)
|
|
57
|
+
|
|
58
|
+
lbl = QLabel(self.tr("Choose bit depth for export:"))
|
|
59
|
+
lbl.setWordWrap(True)
|
|
60
|
+
lay.addWidget(lbl)
|
|
61
|
+
|
|
47
62
|
self.combo = QComboBox(self)
|
|
48
63
|
self.combo.addItems(allowed)
|
|
49
64
|
if current_bit_depth in allowed:
|
|
50
65
|
self.combo.setCurrentText(current_bit_depth)
|
|
66
|
+
lay.addWidget(self.combo)
|
|
51
67
|
|
|
52
|
-
|
|
53
|
-
|
|
68
|
+
# -----------------------------
|
|
69
|
+
# JPEG quality (only for jpg)
|
|
70
|
+
# -----------------------------
|
|
71
|
+
if self._ext == "jpg":
|
|
72
|
+
qlbl = QLabel(self.tr("JPEG quality (1–100):"))
|
|
73
|
+
qlbl.setWordWrap(True)
|
|
74
|
+
lay.addWidget(qlbl)
|
|
75
|
+
|
|
76
|
+
self.jpeg_quality_spin = QSpinBox(self)
|
|
77
|
+
self.jpeg_quality_spin.setRange(1, 100)
|
|
78
|
+
default_q = int(current_jpeg_quality) if current_jpeg_quality is not None else 95
|
|
79
|
+
self.jpeg_quality_spin.setValue(max(1, min(100, default_q)))
|
|
80
|
+
lay.addWidget(self.jpeg_quality_spin)
|
|
54
81
|
|
|
82
|
+
# -----------------------------
|
|
83
|
+
# Buttons
|
|
84
|
+
# -----------------------------
|
|
55
85
|
btn_ok = QPushButton(self.tr("OK"))
|
|
56
86
|
btn_cancel = QPushButton(self.tr("Cancel"))
|
|
57
87
|
btn_ok.clicked.connect(self.accept)
|
|
@@ -62,12 +92,14 @@ class SaveOptionsDialog(QDialog):
|
|
|
62
92
|
row.addWidget(btn_ok)
|
|
63
93
|
row.addWidget(btn_cancel)
|
|
64
94
|
|
|
65
|
-
lay = QVBoxLayout(self)
|
|
66
|
-
lay.addWidget(lbl)
|
|
67
|
-
lay.addWidget(self.combo)
|
|
68
95
|
lay.addStretch(1)
|
|
69
96
|
lay.addLayout(row)
|
|
70
97
|
|
|
98
|
+
|
|
71
99
|
def selected_bit_depth(self) -> str:
|
|
72
100
|
return self.combo.currentText()
|
|
73
101
|
|
|
102
|
+
def selected_jpeg_quality(self) -> int | None:
|
|
103
|
+
if self.jpeg_quality_spin is None:
|
|
104
|
+
return None
|
|
105
|
+
return int(self.jpeg_quality_spin.value())
|
|
@@ -30,7 +30,12 @@ class SERStackConfig:
|
|
|
30
30
|
ap_multiscale: bool = False
|
|
31
31
|
ssd_refine_bruteforce: bool = False
|
|
32
32
|
keep_mask: Optional[KeepMask] = None
|
|
33
|
-
|
|
33
|
+
planet_smooth_sigma: float = 1.5
|
|
34
|
+
planet_thresh_pct: float = 92.0
|
|
35
|
+
planet_use_norm: bool = True
|
|
36
|
+
planet_norm_lo_pct: float = 1.0
|
|
37
|
+
planet_norm_hi_pct: float = 99.5
|
|
38
|
+
planet_min_val: float = 0.02
|
|
34
39
|
# ✅ Drizzle
|
|
35
40
|
drizzle_scale: float = 1.0 # 1.0 = off, 1.5, 2.0
|
|
36
41
|
drizzle_pixfrac: float = 0.80 # "drop shrink" in output pixels (roughly)
|
|
@@ -58,7 +63,22 @@ class SERStackConfig:
|
|
|
58
63
|
self.ap_multiscale = bool(kwargs.pop("ap_multiscale", False))
|
|
59
64
|
self.ssd_refine_bruteforce = bool(kwargs.pop("ssd_refine_bruteforce", False))
|
|
60
65
|
self.keep_mask = kwargs.pop("keep_mask", None)
|
|
66
|
+
# Planetary centroid knobs (pure data, no UI references)
|
|
67
|
+
self.planet_smooth_sigma = float(kwargs.pop("planet_smooth_sigma", 1.5))
|
|
68
|
+
self.planet_thresh_pct = float(kwargs.pop("planet_thresh_pct", 92.0))
|
|
69
|
+
self.planet_min_val = float(kwargs.pop("planet_min_val", 0.02))
|
|
70
|
+
self.planet_use_norm = bool(kwargs.pop("planet_use_norm", True))
|
|
71
|
+
self.planet_norm_lo_pct = float(kwargs.pop("planet_norm_lo_pct", 1.0))
|
|
72
|
+
self.planet_norm_hi_pct = float(kwargs.pop("planet_norm_hi_pct", 99.5))
|
|
61
73
|
|
|
74
|
+
# sanitize
|
|
75
|
+
self.planet_smooth_sigma = max(0.0, self.planet_smooth_sigma)
|
|
76
|
+
self.planet_thresh_pct = float(np.clip(self.planet_thresh_pct, 0.0, 100.0)) if "np" in globals() else self.planet_thresh_pct
|
|
77
|
+
self.planet_min_val = float(max(0.0, min(1.0, self.planet_min_val)))
|
|
78
|
+
self.planet_norm_lo_pct = float(np.clip(self.planet_norm_lo_pct, 0.0, 100.0)) if "np" in globals() else self.planet_norm_lo_pct
|
|
79
|
+
self.planet_norm_hi_pct = float(np.clip(self.planet_norm_hi_pct, 0.0, 100.0)) if "np" in globals() else self.planet_norm_hi_pct
|
|
80
|
+
if self.planet_norm_hi_pct <= self.planet_norm_lo_pct:
|
|
81
|
+
self.planet_norm_hi_pct = min(100.0, self.planet_norm_lo_pct + 1.0)
|
|
62
82
|
# ✅ NEW: Drizzle params
|
|
63
83
|
self.drizzle_scale = float(kwargs.pop("drizzle_scale", 1.0))
|
|
64
84
|
if self.drizzle_scale not in (1.0, 1.5, 2.0):
|
setiastro/saspro/ser_stacker.py
CHANGED
|
@@ -1601,6 +1601,9 @@ def analyze_ser(
|
|
|
1601
1601
|
tracker = PlanetaryTracker(
|
|
1602
1602
|
smooth_sigma=float(getattr(cfg, "planet_smooth_sigma", smooth_sigma)),
|
|
1603
1603
|
thresh_pct=float(getattr(cfg, "planet_thresh_pct", thresh_pct)),
|
|
1604
|
+
min_val=float(getattr(cfg, "planet_min_val", 0.02)),
|
|
1605
|
+
use_norm=bool(getattr(cfg, "planet_use_norm", False)),
|
|
1606
|
+
norm_hi_pct=float(getattr(cfg, "planet_norm_hi_pct", 99.5)),
|
|
1604
1607
|
)
|
|
1605
1608
|
|
|
1606
1609
|
# IMPORTANT: reference center is computed from the SAME reference image that Analyze chose
|
|
@@ -1954,8 +1957,11 @@ def realign_ser(
|
|
|
1954
1957
|
else:
|
|
1955
1958
|
# planetary: centroid tracking (same as viewer)
|
|
1956
1959
|
tracker = PlanetaryTracker(
|
|
1957
|
-
smooth_sigma=float(getattr(cfg, "planet_smooth_sigma",
|
|
1958
|
-
thresh_pct=float(getattr(cfg, "planet_thresh_pct",
|
|
1960
|
+
smooth_sigma=float(getattr(cfg, "planet_smooth_sigma", smooth_sigma)),
|
|
1961
|
+
thresh_pct=float(getattr(cfg, "planet_thresh_pct", thresh_pct)),
|
|
1962
|
+
min_val=float(getattr(cfg, "planet_min_val", 0.02)),
|
|
1963
|
+
use_norm=bool(getattr(cfg, "planet_use_norm", False)),
|
|
1964
|
+
norm_hi_pct=float(getattr(cfg, "planet_norm_hi_pct", 99.5)),
|
|
1959
1965
|
)
|
|
1960
1966
|
|
|
1961
1967
|
# Reference center comes from analysis.ref_image (same anchor as analyze_ser)
|
|
@@ -442,22 +442,29 @@ class APEditorDialog(QDialog):
|
|
|
442
442
|
# ---------- drawing ----------
|
|
443
443
|
def _render(self):
|
|
444
444
|
u8 = self._base_u8
|
|
445
|
+
# ✅ memmap/FITS-safe: QImage assumes row-major contiguous when we pass bytesPerLine=w
|
|
446
|
+
if not u8.flags["C_CONTIGUOUS"]:
|
|
447
|
+
u8 = np.ascontiguousarray(u8)
|
|
448
|
+
|
|
445
449
|
h, w = u8.shape[:2]
|
|
446
450
|
|
|
447
|
-
qimg = QImage(u8.data, w, h, w, QImage.Format.Format_Grayscale8)
|
|
448
|
-
base_pm = QPixmap.fromImage(qimg
|
|
451
|
+
qimg = QImage(u8.data, w, h, w, QImage.Format.Format_Grayscale8).copy()
|
|
452
|
+
base_pm = QPixmap.fromImage(qimg) # already detached above
|
|
449
453
|
|
|
450
454
|
# scale to display zoom (keeps UI sane)
|
|
451
455
|
zw = max(1, int(round(w * self._zoom)))
|
|
452
456
|
zh = max(1, int(round(h * self._zoom)))
|
|
453
|
-
pm = base_pm.scaled(
|
|
457
|
+
pm = base_pm.scaled(
|
|
458
|
+
zw, zh,
|
|
459
|
+
Qt.AspectRatioMode.IgnoreAspectRatio,
|
|
460
|
+
Qt.TransformationMode.SmoothTransformation
|
|
461
|
+
)
|
|
454
462
|
|
|
455
463
|
# draw AP boxes in *display coords* so thickness doesn't scale
|
|
456
464
|
p = QPainter(pm)
|
|
457
465
|
p.setRenderHint(QPainter.RenderHint.Antialiasing, True)
|
|
458
466
|
|
|
459
467
|
s_img = int(max(8, self._ap_size))
|
|
460
|
-
half_img = s_img // 2
|
|
461
468
|
s_disp = max(2, int(round(s_img * self._zoom)))
|
|
462
469
|
half_disp = s_disp // 2
|
|
463
470
|
|
|
@@ -832,6 +839,8 @@ class SERStackerDialog(QDialog):
|
|
|
832
839
|
debayer: bool = True,
|
|
833
840
|
keep_percent: float = 20.0,
|
|
834
841
|
bayer_pattern: Optional[str] = None,
|
|
842
|
+
planet_min_val=0.02, planet_use_norm=False, planet_norm_hi_pct=99.5,
|
|
843
|
+
planet_thresh_pct=92.0, planet_smooth_sigma=1.5, **kwargs
|
|
835
844
|
):
|
|
836
845
|
super().__init__(parent)
|
|
837
846
|
self.setWindowTitle("Planetary Stacker - Beta")
|
|
@@ -840,6 +849,13 @@ class SERStackerDialog(QDialog):
|
|
|
840
849
|
self.setModal(False)
|
|
841
850
|
self._bayer_pattern = bayer_pattern
|
|
842
851
|
self._keep_mask = None # np.ndarray bool shape (N,) or None
|
|
852
|
+
|
|
853
|
+
self._planet_min_val = float(planet_min_val)
|
|
854
|
+
self._planet_use_norm = bool(planet_use_norm)
|
|
855
|
+
self._planet_norm_hi_pct = float(planet_norm_hi_pct)
|
|
856
|
+
self._planet_thresh_pct = float(planet_thresh_pct)
|
|
857
|
+
self._planet_smooth_sigma = float(planet_smooth_sigma)
|
|
858
|
+
|
|
843
859
|
# ---- Normalize inputs ------------------------------------------------
|
|
844
860
|
# If caller provided only `source`, treat string-source as ser_path too.
|
|
845
861
|
if source is None:
|
|
@@ -1445,7 +1461,7 @@ class SERStackerDialog(QDialog):
|
|
|
1445
1461
|
surface_anchor=self._surface_anchor,
|
|
1446
1462
|
keep_percent=float(self.spin_keep.value()),
|
|
1447
1463
|
bayer_pattern=self._bayer_pattern,
|
|
1448
|
-
keep_mask=getattr(self, "_keep_mask", None),
|
|
1464
|
+
keep_mask=getattr(self, "_keep_mask", None),
|
|
1449
1465
|
|
|
1450
1466
|
ap_size=int(self.spin_ap_size.value()),
|
|
1451
1467
|
ap_spacing=int(self.spin_ap_spacing.value()),
|
|
@@ -1455,7 +1471,14 @@ class SERStackerDialog(QDialog):
|
|
|
1455
1471
|
getattr(self, "chk_ssd_bruteforce", None) and self.chk_ssd_bruteforce.isChecked()
|
|
1456
1472
|
),
|
|
1457
1473
|
|
|
1458
|
-
# ✅
|
|
1474
|
+
# ✅ NEW: planetary centroid knobs (add UI controls or set defaults)
|
|
1475
|
+
planet_smooth_sigma=self._planet_smooth_sigma,
|
|
1476
|
+
planet_thresh_pct=self._planet_thresh_pct,
|
|
1477
|
+
planet_min_val=self._planet_min_val,
|
|
1478
|
+
planet_use_norm=self._planet_use_norm,
|
|
1479
|
+
planet_norm_hi_pct=self._planet_norm_hi_pct,
|
|
1480
|
+
|
|
1481
|
+
# drizzle
|
|
1459
1482
|
drizzle_scale=float(drizzle_scale),
|
|
1460
1483
|
drizzle_pixfrac=float(self.spin_pixfrac.value()),
|
|
1461
1484
|
drizzle_kernel=str(drizzle_kernel),
|
|
@@ -1783,7 +1806,7 @@ class BlinkKeepersDialog(QDialog):
|
|
|
1783
1806
|
v = (mono - lo) / (hi - lo)
|
|
1784
1807
|
v = np.clip(v, 0.0, 1.0)
|
|
1785
1808
|
return (v * 255.0 + 0.5).astype(np.uint8)
|
|
1786
|
-
|
|
1809
|
+
|
|
1787
1810
|
def _show_index(self, i: int):
|
|
1788
1811
|
if self.keepers.size == 0:
|
|
1789
1812
|
return
|
|
@@ -1810,9 +1833,14 @@ class BlinkKeepersDialog(QDialog):
|
|
|
1810
1833
|
img = img[..., 0]
|
|
1811
1834
|
|
|
1812
1835
|
u8 = self._disp_u8(img)
|
|
1836
|
+
|
|
1837
|
+
# ✅ memmap/FITS-safe: guarantee tight row stride for bytesPerLine=w
|
|
1838
|
+
if not u8.flags["C_CONTIGUOUS"]:
|
|
1839
|
+
u8 = np.ascontiguousarray(u8)
|
|
1840
|
+
|
|
1813
1841
|
h, w = u8.shape
|
|
1814
|
-
qimg = QImage(u8.data, w, h, w, QImage.Format.Format_Grayscale8)
|
|
1815
|
-
pm = QPixmap.fromImage(qimg
|
|
1842
|
+
qimg = QImage(u8.data, w, h, w, QImage.Format.Format_Grayscale8).copy()
|
|
1843
|
+
pm = QPixmap.fromImage(qimg)
|
|
1816
1844
|
|
|
1817
1845
|
self.lbl.setPixmap(pm.scaled(
|
|
1818
1846
|
self.lbl.size(),
|
|
@@ -1821,7 +1849,6 @@ class BlinkKeepersDialog(QDialog):
|
|
|
1821
1849
|
))
|
|
1822
1850
|
self._update_labels()
|
|
1823
1851
|
|
|
1824
|
-
|
|
1825
1852
|
def resizeEvent(self, e):
|
|
1826
1853
|
super().resizeEvent(e)
|
|
1827
1854
|
self._show_index(self.sld.value())
|
setiastro/saspro/ser_tracking.py
CHANGED
|
@@ -19,14 +19,22 @@ def _to_mono01(img: np.ndarray) -> np.ndarray:
|
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
class PlanetaryTracker:
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
*,
|
|
25
|
+
smooth_sigma: float = 1.5,
|
|
26
|
+
thresh_pct: float = 92.0,
|
|
27
|
+
min_val: float = 0.02, # ✅ NEW
|
|
28
|
+
use_norm: bool = True,
|
|
29
|
+
norm_hi_pct: float = 99.5,
|
|
30
|
+
norm_lo_pct: float = 1.0,
|
|
31
|
+
):
|
|
27
32
|
self.smooth_sigma = float(smooth_sigma)
|
|
28
33
|
self.thresh_pct = float(thresh_pct)
|
|
29
|
-
self.
|
|
34
|
+
self.min_val = float(min_val) # ✅ store
|
|
35
|
+
self.use_norm = bool(use_norm)
|
|
36
|
+
self.norm_hi_pct = float(norm_hi_pct)
|
|
37
|
+
self.norm_lo_pct = float(norm_lo_pct)
|
|
30
38
|
|
|
31
39
|
def reset(self):
|
|
32
40
|
self._ref_center = None
|
|
@@ -71,23 +79,20 @@ class PlanetaryTracker:
|
|
|
71
79
|
conf = float(np.clip(mm["m00"] / float(mask.size), 0.0, 1.0))
|
|
72
80
|
return (cx, cy, conf)
|
|
73
81
|
|
|
82
|
+
def _normalize_for_detect(self, m: np.ndarray) -> np.ndarray:
|
|
83
|
+
if not self.use_norm:
|
|
84
|
+
return m
|
|
74
85
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
+
lo = float(np.percentile(m, self.norm_lo_pct))
|
|
87
|
+
hi = float(np.percentile(m, self.norm_hi_pct))
|
|
88
|
+
if (not np.isfinite(lo)) or (not np.isfinite(hi)) or (hi <= lo + 1e-12):
|
|
89
|
+
return m
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
det = (m - lo) / (hi - lo)
|
|
92
|
+
return np.clip(det, 0.0, 1.0).astype(np.float32, copy=False)
|
|
89
93
|
|
|
90
|
-
|
|
94
|
+
def step(self, img01: np.ndarray) -> tuple[float, float, float]:
|
|
95
|
+
cx, cy, conf = self.compute_center(img01)
|
|
91
96
|
if conf <= 0.0:
|
|
92
97
|
return 0.0, 0.0, 0.0
|
|
93
98
|
|
|
@@ -98,25 +103,42 @@ class PlanetaryTracker:
|
|
|
98
103
|
rx, ry = self._ref_center
|
|
99
104
|
dx = rx - cx
|
|
100
105
|
dy = ry - cy
|
|
101
|
-
return float(dx), float(dy), conf
|
|
106
|
+
return float(dx), float(dy), float(conf)
|
|
107
|
+
|
|
108
|
+
def _prep_mono01(self, img01: np.ndarray) -> np.ndarray:
|
|
109
|
+
m = _to_mono01(img01).astype(np.float32, copy=False)
|
|
110
|
+
m = np.nan_to_num(m, nan=0.0, posinf=0.0, neginf=0.0)
|
|
102
111
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
Uses the same pipeline as step(): blur -> percentile thresh -> largest CC -> centroid.
|
|
107
|
-
"""
|
|
108
|
-
m = _to_mono01(img01)
|
|
109
|
-
m2 = self._blur(m)
|
|
112
|
+
# match step(): blur then lo/hi percentile normalize
|
|
113
|
+
if self.smooth_sigma > 0.0 and cv2 is not None:
|
|
114
|
+
m = cv2.GaussianBlur(m, (0, 0), float(self.smooth_sigma))
|
|
110
115
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
m = self._normalize_for_detect(m)
|
|
117
|
+
return np.clip(m, 0.0, 1.0).astype(np.float32, copy=False)
|
|
118
|
+
|
|
119
|
+
def compute_center(self, img01: np.ndarray):
|
|
120
|
+
m = self._prep_mono01(img01) # already blurred + normalized
|
|
121
|
+
|
|
122
|
+
thr = float(np.percentile(m, np.clip(self.thresh_pct, 0.0, 100.0)))
|
|
123
|
+
if not np.isfinite(thr):
|
|
124
|
+
return (m.shape[1] * 0.5), (m.shape[0] * 0.5), 0.0
|
|
125
|
+
|
|
126
|
+
# ✅ critical: threshold cannot be below min_val (same domain: normalized [0..1])
|
|
127
|
+
thr = max(thr, float(self.min_val))
|
|
128
|
+
|
|
129
|
+
mask = (m >= thr).astype(np.uint8)
|
|
130
|
+
if int(mask.sum()) < 10:
|
|
131
|
+
return (m.shape[1] * 0.5), (m.shape[0] * 0.5), 0.0
|
|
132
|
+
|
|
133
|
+
ys, xs = np.nonzero(mask)
|
|
134
|
+
w = m[ys, xs]
|
|
135
|
+
sw = float(w.sum()) + 1e-12
|
|
136
|
+
cx = float((xs * w).sum() / sw)
|
|
137
|
+
cy = float((ys * w).sum() / sw)
|
|
114
138
|
|
|
115
|
-
|
|
116
|
-
|
|
139
|
+
conf = float(np.clip((float(np.mean(w)) - thr) / max(1e-6, (1.0 - thr)), 0.0, 1.0))
|
|
140
|
+
return cx, cy, conf
|
|
117
141
|
|
|
118
|
-
cx, cy, conf = self._centroid(m2, mask)
|
|
119
|
-
return float(cx), float(cy), float(conf)
|
|
120
142
|
|
|
121
143
|
def shift_to_ref(self, img01: np.ndarray, ref_center: tuple[float, float]) -> tuple[float, float, float]:
|
|
122
144
|
"""
|