fix #1: calibration stéréo HD-3000 (YUYV/V4L2, ORB-SLAM3 YAML, gate RMS<0.5px)
- stereo_capture.py : backend V4L2 + fourcc YUYV 640×480@30 (HD-3000 ne supporte pas MJPG), indices /dev/video0 et /dev/video2, mode --headless pour SSH, warmup AE/AGC, grab+retrieve pour meilleur synchronisme, sortie datasets/calib_raw/ à la racine du repo. - stereo_calibrate.py : log RMS per-cam + RMS stéréo global, écrit config/stereo_params.yaml au format ORB-SLAM3 (Camera1/2.fx, Stereo.T_c1_c2, Stereo.b/bf, IMU.T_b_c1 placeholder), génère config/calibration_report.md avec baseline mesurée vs cible 110 mm. Gate exit-code 2 si RMS > 0.5 px (bypass --no-gate). - .gitignore : ajoute datasets/calib_raw/ (captures brutes volumineuses). - docs/source/calibration.rst : procédure SSH headless reproduite, format YAML ORB-SLAM3 documenté, gate RMS<0.5 px expliqué. Note : la calibration numérique (RMS, K/D/R/T, baseline mesurée) doit être exécutée sur le Pi avec les caméras connectées — code prêt, hardware run à faire manuellement. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -7,92 +7,129 @@ Matériel nécessaire
|
||||
- Damier imprimé **9×6 cases** (intérieur), case **25 mm**
|
||||
- Support rigide (plastifié recommandé)
|
||||
- Éclairage uniforme sans reflets
|
||||
- 2× Microsoft LifeCam HD-3000 sur ``/dev/video0`` (gauche) et
|
||||
``/dev/video2`` (droite), baseline cible **110 mm**
|
||||
|
||||
.. warning::
|
||||
Imprimer sur fond blanc mat. Plastifier sans brillance pour éviter
|
||||
les reflets. Ne pas utiliser d'écran LCD (déformation optique).
|
||||
|
||||
Procédure
|
||||
---------
|
||||
.. note::
|
||||
Les HD-3000 ne supportent **pas** le format MJPG. La capture est forcée
|
||||
en **YUYV 640×480 @ 30 fps** (cf. ``cap.set(CAP_PROP_FOURCC, "YUYV")``).
|
||||
La résolution doit être appliquée *après* le fourcc, sinon V4L2 l'ignore.
|
||||
|
||||
Étape 1 — Capture des paires
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
Procédure (issue #1)
|
||||
--------------------
|
||||
|
||||
Étape 0 — Déploiement sur le Pi
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python src/calibration/stereo_capture.py
|
||||
bash scripts/deploy_pi.sh 192.168.0.174
|
||||
|
||||
- Lance les 2 webcams simultanément (USB 0 et 1)
|
||||
- Appuyer sur ``ESPACE`` pour capturer une paire
|
||||
- Objectif : **30 paires valides** minimum
|
||||
- Varier : distance (0.3–1.5 m), inclinaison (±30°), position dans frame
|
||||
|
||||
.. tip::
|
||||
Couvrir les 4 coins du champ + centre. Inclure des poses inclinées
|
||||
pour mieux contraindre les coefficients de distorsion tangentielle.
|
||||
|
||||
Étape 2 — Calibration
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
Étape 1 — Capture des paires (sur le Pi, en SSH)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python src/calibration/stereo_calibrate.py
|
||||
ssh pi@192.168.0.174
|
||||
cd ~/slam_stereo
|
||||
# Mode interactif (avec écran X11 forwardé) :
|
||||
python3 src/calibration/stereo_capture.py
|
||||
# OU mode headless (recommandé en SSH, pas d'écran) :
|
||||
python3 src/calibration/stereo_capture.py --headless --count 30 --interval 2.0
|
||||
|
||||
Résultat attendu :
|
||||
- Indices V4L2 par défaut : gauche = ``0``, droite = ``2``
|
||||
(override : ``--left N --right M``)
|
||||
- Sorties dans ``datasets/calib_raw/`` (gitignored)
|
||||
- Objectif : **≥ 25 paires valides** (varier distance 0.3–1.5 m, inclinaison ±30°,
|
||||
position dans le frame, couvrir les 4 coins + centre)
|
||||
- Alim USB du Pi flaky : éviter les sessions longues, libérer les caméras
|
||||
entre les runs (``cap.release()`` est appelé dans ``finally``).
|
||||
|
||||
Étape 2 — Calibration (sur le Pi ou rapatrié sur PC)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python3 src/calibration/stereo_calibrate.py
|
||||
|
||||
Sortie attendue :
|
||||
|
||||
.. code-block:: text
|
||||
|
||||
Chargement 30 paires de damier...
|
||||
Calibration stéréo...
|
||||
RMS reprojection error: 0.42 px ← bon si < 0.8 px
|
||||
Sauvegarde: config/stereo_calib.yaml
|
||||
[INFO] Chargement 30 paires depuis datasets/calib_raw
|
||||
[INFO] Gauche: 28/30 images valides
|
||||
[INFO] Droite: 27/30 images valides
|
||||
[INFO] 26 paires valides pour calibration stéréo
|
||||
[INFO] RMS gauche = 0.31 px
|
||||
[INFO] RMS droite = 0.34 px
|
||||
[INFO] RMS stéréo global = 0.42 px
|
||||
[OK] YAML ORB-SLAM3 → config/stereo_params.yaml
|
||||
[OK] Rapport → config/calibration_report.md
|
||||
[OK] RMS stéréo 0.4200 < 0.5 px ✓
|
||||
|
||||
Fichier YAML généré
|
||||
-------------------
|
||||
**Gate** : si RMS stéréo > 0.5 px → exit-code 2 (``[STOP]``). Recapturer
|
||||
la mire (mauvais positionnement, flou de bougé, cams sous-focales).
|
||||
Le bypass ``--no-gate`` existe mais ne dispense pas de re-capturer.
|
||||
|
||||
``config/stereo_calib.yaml`` contient :
|
||||
Livrables (commit après validation)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- ``config/stereo_params.yaml`` — format ORB-SLAM3 stéréo
|
||||
(``Camera1.fx``, ``…``, ``Stereo.T_c1_c2``, ``Stereo.bf``, ``IMU.T_b_c1``…)
|
||||
- ``config/calibration_report.md`` — RMS per-cam + RMS stéréo + baseline mesurée
|
||||
|
||||
Format YAML ORB-SLAM3 généré
|
||||
----------------------------
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
K1: [[fx, 0, cx], [0, fy, cy], [0, 0, 1]] # caméra gauche
|
||||
D1: [k1, k2, p1, p2, k3] # distorsion gauche
|
||||
K2: [[fx, 0, cx], [0, fy, cy], [0, 0, 1]] # caméra droite
|
||||
D2: [k1, k2, p1, p2, k3] # distorsion droite
|
||||
R: [[...], [...], [...]] # rotation C2→C1
|
||||
T: [tx, ty, tz] # translation (baseline)
|
||||
E: [[...]] # matrice essentielle
|
||||
F: [[...]] # matrice fondamentale
|
||||
R1, P1, R2, P2, Q # rectification
|
||||
%YAML:1.0
|
||||
File.version: "1.0"
|
||||
Camera.type: "PinHole"
|
||||
|
||||
Paramètres OpenCV
|
||||
-----------------
|
||||
Camera1.fx: 525.123 # cam gauche
|
||||
Camera1.fy: 524.987
|
||||
Camera1.cx: 319.876
|
||||
Camera1.cy: 239.234
|
||||
Camera1.k1: -0.05432 # distorsion Plumb-Bob (4 coefs)
|
||||
Camera1.k2: 0.00821
|
||||
Camera1.p1: 0.00012
|
||||
Camera1.p2: -0.00045
|
||||
|
||||
``stereoCalibrate`` avec flags :
|
||||
Camera2.fx: 525.456 # cam droite
|
||||
# …
|
||||
|
||||
.. code-block:: python
|
||||
Camera.width: 640
|
||||
Camera.height: 480
|
||||
Camera.fps: 30
|
||||
|
||||
flags = (
|
||||
cv2.CALIB_FIX_INTRINSIC # si calibration individuelle faite avant
|
||||
# ou cv2.CALIB_RATIONAL_MODEL # pour modèle 8 coefs
|
||||
)
|
||||
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 100, 1e-5)
|
||||
Stereo.T_c1_c2: !!opencv-matrix # transfo cam_droite → cam_gauche
|
||||
rows: 4
|
||||
cols: 4
|
||||
dt: f
|
||||
data: [...]
|
||||
Stereo.b: 0.110000 # baseline (m)
|
||||
Stereo.bf: 57.7600 # baseline * fx
|
||||
Stereo.ThDepth: 40.0
|
||||
|
||||
``stereoRectify`` avec ``alpha=0`` (recadrage maximal, pas de bandes noires).
|
||||
IMU.T_b_c1: !!opencv-matrix # placeholder identité
|
||||
rows: 4
|
||||
cols: 4
|
||||
dt: f
|
||||
data: [1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1]
|
||||
|
||||
Validation
|
||||
----------
|
||||
|
||||
Après calibration, vérifier :
|
||||
|
||||
1. Erreur RMS < 0.8 px (idéalement < 0.5 px)
|
||||
1. Erreur RMS stéréo **< 0.5 px** (gate strict, issue #1)
|
||||
2. Lignes épipolaires horizontales sur image rectifiée
|
||||
3. Translation T ≈ [−0.11, 0, 0] m (baseline sur axe X)
|
||||
4. Épipole à l'infini (caméras parallèles après rectif)
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
# Visualisation rectification
|
||||
python src/calibration/stereo_calibrate.py --show-rectified
|
||||
(``python3 src/calibration/stereo_calibrate.py --show-rectified``)
|
||||
3. Translation ``T ≈ [-0.110, 0, 0]`` m (baseline sur axe X)
|
||||
4. Épipole à l'infini après rectification (caméras parallèles)
|
||||
|
||||
Références
|
||||
----------
|
||||
|
||||
Reference in New Issue
Block a user