Files
dwarf-go/dwarfctl/ODOMETRY.md
Jacquin Antoine e8ae644d58 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
2026-07-13 23:10:14 +02:00

234 lines
8.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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