Improve odometry with rotation estimation, daemon stability, and RTSP fixes

- Add rotation estimation via log-polar phase correlation (EstimateRotation,
  rotateGray) — de-rotate before translation to avoid aliasing
- Track cumulative field rotation in Tracker, reject low-confidence frames
- Extract RTSP grabber into internal/rtspgrab module
- Daemon: pause odometry during slew (motion blur), auto-resume after settle
  delay, add /pause and /resume HTTP endpoints, switch shooting mode before
  opening camera, use ReqOpenCamera with rtsp_encode_type=1
- WebSocket heartbeat (ping/pong every 10s) to prevent idle disconnects
- FFmpeg low-latency flags (-fflags nobuffer, -probesize, -analyzeduration)
- Add SendRaw API for custom payloads, increase daemon HTTP timeout to 60s

💘 Generated with Crush

Assisted-by: Crush:/models/Qwen3.6-27B-uncensored-heretic-v2-Native-MTP-Preserved-Q4_K_M.gguf
This commit is contained in:
Jacquin Antoine
2026-07-13 23:00:30 +02:00
parent 8daa9b4d58
commit d319ef6b4a
12 changed files with 522 additions and 143 deletions

View File

@ -6,6 +6,11 @@ import (
"sync"
)
// minConfidence is the threshold below which a frame is rejected (not
// accumulated into the orientation). Motion-blurred frames or frames with
// too little texture produce confidence near 0 and corrupt the estimate.
const minConfidence = 0.15
// Tracker is a stateful visual odometry integrator. It accumulates the
// frame-to-frame rotation measured by Estimate into a cumulative pointing
// orientation (pan/tilt) relative to wherever the first frame was captured.
@ -29,6 +34,7 @@ type Tracker struct {
// Cumulative orientation in degrees, relative to the first frame.
panDeg float64
tiltDeg float64
rotDeg float64 // cumulative field rotation
// Number of frames integrated.
frames int
@ -61,11 +67,12 @@ func NewTracker(fovXDeg, fovYDeg float64, size int) *Tracker {
// Orientation is a snapshot of the tracker's current state.
type Orientation struct {
PanDeg float64 // cumulative pan (right positive), degrees
TiltDeg float64 // cumulative tilt (up positive), degrees
Frames int // number of frames integrated
Confidence float64 // EMA of per-frame phase-correlation confidence [0,1]
Primed bool // true once at least one frame has been processed
PanDeg float64
TiltDeg float64
RotDeg float64 // cumulative field rotation (degrees)
Frames int
Confidence float64
Primed bool
}
func (o Orientation) String() string {
@ -73,8 +80,8 @@ func (o Orientation) String() string {
if o.Primed {
state = "tracking"
}
return fmt.Sprintf("pan=%+.4f° tilt=%+.4f° conf=%.3f frames=%d [%s]",
o.PanDeg, o.TiltDeg, o.Confidence, o.Frames, state)
return fmt.Sprintf("pan=%+.4f° tilt=%+.4f° rot=%+.4f° conf=%.3f frames=%d [%s]",
o.PanDeg, o.TiltDeg, o.RotDeg, o.Confidence, o.Frames, state)
}
// Update feeds the tracker a new frame and returns the updated orientation.
@ -96,16 +103,34 @@ func (t *Tracker) Update(img image.Image) (Orientation, error) {
return t.snapshot(), nil
}
dx, dy, conf := phaseCorrelation(t.prev, cur, t.size, t.size)
dx, dy, confTrans := phaseCorrelation(t.prev, cur, t.size, t.size)
rotDeg, confRot := EstimateRotation(t.prev, cur, t.size, t.size)
// Convert pixel shift to degrees. The scene shifts OPPOSITE to the telescope
// motion: if the telescope pans right, the background moves left in the
// image. We negate so PanDeg/TiltDeg track the optical axis, not the scene.
// Use the higher confidence for the EMA.
conf := confTrans
if confRot > conf {
conf = confRot
}
// Reject low-confidence frames (motion blur, bad texture, etc.).
if conf < minConfidence {
t.prev = cur // still update the reference frame
return t.snapshot(), nil
}
// De-rotate for accurate translation if rotation is significant.
if absf(rotDeg) > 0.3 {
curDerot := rotateGray(cur, t.size, rotDeg)
dx, dy, confTrans = phaseCorrelation(t.prev, curDerot, t.size, t.size)
}
// Convert pixel shift to degrees.
panStep := -dx * t.fovXDeg / float64(t.size)
tiltStep := -dy * t.fovYDeg / float64(t.size)
t.panDeg += panStep
t.tiltDeg += tiltStep
t.rotDeg += rotDeg
t.frames++
// Exponential moving average of the confidence.
@ -134,6 +159,7 @@ func (t *Tracker) Reset() {
defer t.mu.Unlock()
t.panDeg = 0
t.tiltDeg = 0
t.rotDeg = 0
t.frames = 0
t.confidence = 0
t.prev = nil
@ -157,6 +183,7 @@ func (t *Tracker) snapshot() Orientation {
return Orientation{
PanDeg: t.panDeg,
TiltDeg: t.tiltDeg,
RotDeg: t.rotDeg,
Frames: t.frames,
Confidence: t.confidence,
Primed: t.primed,