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:
233
dwarfctl/ODOMETRY.md
Normal file
233
dwarfctl/ODOMETRY.md
Normal 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
|
||||||
|
```
|
||||||
|
|
||||||
|
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`).
|
||||||
Reference in New Issue
Block a user