# Visual Odometry — Guide technique

L'odométrie visuelle de dwarfctl estime le pointage du télescope entre deux
images de la caméra wide en utilisant la **corrélation de phase par FFT**.
Pas besoin de reconnaissance d'étoiles, de plate solving ou d'alignement
polaire — ça fonctionne sur n'importe quelle scène texturée (ciel, horizon,
nuages, paysage diurne).

---

## Concept fondamental

Quand le télescope effectue un petit mouvement (pan ou tilt), le contenu de
l'image subit un **décalage quasi-pur** dans le plan image :

- Pan (azimut) → décalage horizontal
- Tilt (altitude) → décalage vertical
- Rotation azimutale près du zénith → rotation du champ (field rotation)

La corrélation de phase récupère ce décalage avec une précision sub-pixell,
qui se convertit en degrés via le champ de vision (FoV) de la caméra.

---

## Pipeline de corrélation de phase

```
Image 1 ─┐                    ┌─→ dx, dy (pixels) ─→ degrés
          ├→ FFT² → CPS → IFFT ├─→ pic de corrélation
Image 2 ─┘                    └─→ confiance [0, 1]
```

### Détails du pipeline

1. **Conversion grayscale + downsampling** — les images JPEG/PNG sont réduites
   à une grille carrée de taille `size` (256 par défaut). Le downsampling
   utilise une moyenne de bloc pour éviter le aliasing.

2. **Fenêtrage Hann 2D + suppression du DC** — le composant continu (moyenne
   de l'image) est soustrait, puis une fenêtre de Hann 2D est appliquée pour
   réduire les fuites spectrales aux bords.

3. **FFT² (transformée 2D)** — FFT séparable : d'abord sur chaque ligne,
   puis sur chaque colonne. Implémentation radix-2 Cooley-Tukey itérative
   avec permutation en bit-reversal.

4. **Spectre de puissance croisée normalisé** — `conj(F1) * F2 / |F1*F2|`
   ne garde que la phase différentielle. Le pic de l'IFT donne le décalage.

5. **Recherche de pic + raffinement sub-pixel** — le pic est localisé, puis
   interpolé paraboliquement sur chaque axe pour une précision ~0.1px.

6. **Confiance** — hauteur du pic relative à la moyenne de la surface de
   corrélation. Clamé à [0, 1]. En dessous de 0.15, le Tracker rejette
   le frame (flou de mouvement, texture insuffisante, etc.).

---

## Estimation de rotation — méthode log-polaires

La rotation du champ (dominante près du zénith sur une monture az-el) est
estimée séparément via la **corrélation de phase en coordonnées log-polaires** :

```
Image ─→ FFT² ─→ |magnitude| ─→ filtrage HP ─→ resampling log-polaires ─→ PC
```

### Pourquoi ça marche

Une rotation dans le domaine spatial devient une rotation dans le domaine
fréquentiel. En convertissant le spectre de magnitude en coordonnées
log-polaires, cette rotation devient un **décalage linéaire** le long de
l'axe angulaire. La corrélation de phase récupère ce décalage, qui se
convertit directement en degrés :

```
angle = -dy * 360° / nAngles
```

où `nAngles = 256` (résolution angulaire) et `nRadii = 128`.

Le filtrage passe-haut gaussien supprime le pic DC central qui domine le
spectre et fausserait la corrélation.

### Calibration FoV

Le resampling log-polaires utilise une interpolation bilinéaire. Les
paramètres `maxR`, `logMinR` et `logMaxR` sont dérivés des dimensions de
l'image FFT.

---

## Dérotation — deux passes pour la précision

Quand la rotation dépasse 0.3°, la translation est corrompue par l'aliasing
induit par la rotation. Le pipeline en deux passes résout ce problème :

```
Pass 1: rotation (log-polaires) → rotDeg
Pass 2: dérotation de g2 → corrélation de translation sur images alignées
```

La fonction `rotateGray` applique une rotation inverse autour du centre
de l'image, avec interpolation bilinéaire. Les bords sont clampés pour
éviter les dépassements.

---

## Tracker — intégrateur cumulatif

Le `Tracker` accumule les rotations mesurées frame par frame pour maintenir
une orientation cumulative relative au premier frame :

### États

- **Priming** — le premier frame est enregistré comme référence (origine 0,0).
  Aucune mesure n'est faite.
- **Tracking** — chaque nouveau frame est comparé au précédent, le résultat
  est intégré.

### Variables intégrées

| Variable | Description |
|----------|-------------|
| `panDeg` | Pan cumulatif (droite positive), degrés |
| `tiltDeg` | Tilt cumulatif (haut positif), degrés |
| `rotDeg` | Rotation de champ cumulative (CCW positive), degrés |
| `confidence` | EMA exponentielle de la confiance par frame (alpha=0.2) |
| `frames` | Nombre de frames intégrés |

### Rejet de frames

Les frames avec une confiance inférieure à 0.15 sont rejetés — ils ne
s'intègrent pas dans l'orientation. Le frame de référence est quand même
mis à jour pour éviter l'accumulation de décalage.

### Reset

`Tracker.Reset()` remet toutes les variables à zéro et déprime le tracker.
Le prochain `Update()` deviendra la nouvelle origine.

---

## Configuration

### Taille de grille FFT (`size`)

| Taille | Résolution | Coût CPU | Usage |
|--------|-----------|----------|-------|
| 256 | ~0.1px | Bas | Défaut, suffit pour la plupart des cas |
| 512 | ~0.05px | ~4x plus | Quand la précision sub-pixel est critique |
| 1024 | ~0.025px | ~16x plus | Rarement nécessaire |

La taille doit être une puissance de deux (contrainte de l'algorithme
radix-2).

### Champ de vision (FoV)

Les valeurs par défaut (`8.0°` × `6.5°`) correspondent au DWARF Mini
affiché, mais le FoV réel rapporté par le firmware peut varier largement
(voir `analysis/DEVICE_MODELS.md`). **Calibrer avec un mouvement moteur
connu** : slew d'un angle connu, comparer avec la mesure odometry.

```bash
# Override FoV via daemon HTTP API
curl -X POST http://127.0.0.1:7777/fov -d '{"h": 10.2, "v": 8.1}'
```

### Intervalle de capture

Le daemon capture un frame toutes les `--interval` secondes (défaut 5s).
Un intervalle trop court peut capturer des frames avec flou de mouvement
pendant les slew. Le daemon gère cela via le pause automatique.

---

## Gestion du flou de mouvement dans le daemon

Le daemon met en pause automatiquement la capture odometry pendant les
mouvements de moteur :

```
/slew → pause immédiate
/stop → pause + délai de settling (3s par défaut)
  ↓
attente → reprise auto
```

Le délai de settling (`SettleDelay` dans `daemon.Config`) permet aux
moteurs de s'arrêter complètement avant la prochaine capture. Sans ce
délai, les frames flous corrompent la corrélation de phase.

---

## Limitations

1. **Texture requise** — un ciel uniforme sans nuages ni étoiles donne
   une corrélation faible (< 0.05). La caméra wide voit le ciel et
   l'horizon simultanément, ce qui aide.

2. **Mouvement limité** — le décalage entre frames ne doit pas dépasser
   ~1/4 de la taille de la grille. Des mouvements brusques peuvent
   perdre la corrélation.

3. **Pas de position absolue** — l'odométrie mesure des rotations
   **relatives** au premier frame. Pour connaître la position absolue,
   il faut soit la calibration astro (EQ calibrate), soit un encodeur.

4. **Dérive cumulative** — chaque mesure introduit une petite erreur qui
   s'accumule. Plus le nombre de frames est élevé, plus l'orientation
   cumulative dérive. Reset périodiquement ou recalibre via la calibration
   astro du télescope.

5. **Monture az-el uniquement** — conçu pour une monture alt-az statique.
   Après calibration EQ, le mapping moteur → axe change et les directions
   pan/tilt ne correspondent plus aux axes physiques.

---

## Architecture code

```
odometry/
├── fft.go          FFT 1D/2D radix-2 Cooley-Tukey itératif
├── odometry.go     Estimate() — pipeline principal + helpers (window, DC, parabola)
├── rotation.go     EstimateRotation() — log-polaires + phase correlation
├── rotate.go       rotateGray() — rotation inverse bilinéaire
└── tracker.go      Tracker — intégrateur cumulatif avec rejet de frames
```

Le code est entièrement en Go standard — aucune dépendance externe pour
l'odométrie. Le seul binaire externe requis est `ffmpeg` pour la capture
RTSP (via `internal/rtspgrab`).
