diff --git a/dwarfctl/ODOMETRY.md b/dwarfctl/ODOMETRY.md new file mode 100644 index 0000000..a379655 --- /dev/null +++ b/dwarfctl/ODOMETRY.md @@ -0,0 +1,233 @@ +# 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`).