docs: add visual odometry technical guide

Cover phase correlation pipeline, log-polar rotation estimation,
de-rotation pass, Tracker integration, motion-blur handling, FoV
calibration, and limitations.

💘 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:10:14 +02:00
parent 905dc1dbc6
commit e8ae644d58

233
dwarfctl/ODOMETRY.md Normal file
View File

@ -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
```
`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`).