# 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 (`42.65°` × `24.45°`) correspondent au FoV physique réel rapporté par le firmware (`GetDeviceState`), pas à la valeur d'affichage de l'app (8.0×6.5 qui est un crop numériquement zoomé). ```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`).