Mat de vision à base de tags ArUco, écrit en C++17 avec OpenCV : une
caméra fixée sur un mât au-dessus d'une table de jeu repère les 4 tags de coin de
la table, en déduit la pose de la caméra dans le repère de la table
(solvePnP), puis renvoie la position (x, y, z, angle) des objets marqués sur le
tapis via une API REST accessible sur le réseau local — avec une interface
web de pilotage intégrée.
La position de chaque objet est obtenue par changement de base : la pose de
son tag (calculée par solvePnP) est composée avec la pose caméra-table. Il n'y
a aucune homographie.
- Cible matérielle : LattePanda Delta (x86, Ubuntu) + webcam USB Logitech 4K Stream Edition.
- Table : 2000 mm × 3000 mm ; tags de coin à ±400 mm / ±900 mm du centre.
- Sortie : JSON en millimètres dans le repère table, origine au centre.
- Calibration intrinsèque obligatoire : sans elle, aucune pose (donc aucune position) ne peut être calculée.
| # | Fonction | Où |
|---|---|---|
| 1 | API web (JSON, CORS, aperçu JPEG, flux MJPEG, supervision) cpp-httplib | src/api.cpp, src/main_server.cpp |
| 2 | Calibration automatique des intrinsèques (damier, sans toucher au clavier) | src/calibration.cpp, src/main_calibrate.cpp |
| 3 | Module vision ArUco : 4 tags de coin → pose caméra (solvePnP) → objets |
src/aruco.cpp, src/table.cpp |
| 4 | Relevé des positions une entrée par tag détecté (mm / degrés, par changement de base) | src/vision.cpp |
| 5 | Interface web multi-onglets : stratégie, live table, vision, robot principal | web/ |
| 6 | Flotte de robots : pilotage stratégie/couleur du principal, du chasseur et de l'essaim | src/fleet.cpp |
L'image complète n'est pas analysée : les 4 tags de coin servent d'abord à
estimer la pose caméra, puis une région d'intérêt (ROI) couvrant toute la
surface de jeu ; la détection des objets n'est faite qu'à l'intérieur de cette
zone (table.roi_mode, voir §9).
flowchart LR
CAM["Webcam USB<br/>Logitech 4K Stream"] --> ENG["VisionEngine<br/>(une seule boucle)"]
ENG --> AR["ArucoDetector<br/>détection des tags"]
AR --> CORN["4 tags de coin"]
CORN --> POSE["Pose caméra dans le repère table<br/>solvePnP (intrinsèques requises)"]
POSE --> ROI["Masque ROI"]
ROI --> AR2["Détection des objets"]
AR2 --> TAG["Pose de chaque tag<br/>solvePnP"]
TAG --> MAP["Changement de base<br/>caméra → table"]
MAP --> OUT["Position en mm / degrés<br/>repère table"]
OUT --> API["API HTTP (cpp-httplib)"]
API -->|JSON / JPEG / MJPEG| WEB["Interface web<br/>/ui"]
API -->|JSON| LAN["Réseau local<br/>curl, robot, supervision"]
CAL["AutoCalibrator<br/>(damier)"] -.->|intrinsèques| POSE
WEB -.->|POST /start, /stop,<br/>/calibration/*| API
Mat-CDFR2027/
├── CMakeLists.txt # build C++ (OpenCV + cpp-httplib + nlohmann/json)
├── config/
│ └── default.json # configuration complète (caméra, table, objets…)
├── data/ # calibration et captures (généré)
├── include/matvision/ # en-têtes publics
│ ├── api.hpp # serveur HTTP (routes, aperçu, flux)
│ ├── aruco.hpp # détecteur ArUco + tracé
│ ├── calibration.hpp # Intrinsics, AutoCalibrator
│ ├── camera.hpp # source d'images (webcam USB, IFrameSource)
│ ├── config.hpp # structures de configuration
│ ├── fleet.hpp # flotte de robots
│ ├── geometry.hpp # Position, angles, coins de tags, ROI, projection
│ ├── logger.hpp # journalisation console
│ ├── table.hpp # pose caméra (solvePnP) + changement de base
│ └── vision.hpp # moteur (boucle, modes, relevé des objets)
├── src/
│ ├── api.cpp # routes + interface web + MJPEG
│ ├── aruco.cpp
│ ├── calibration.cpp
│ ├── camera.cpp
│ ├── config.cpp
│ ├── fleet.cpp
│ ├── geometry.cpp
│ ├── logger.cpp
│ ├── table.cpp
│ ├── vision.cpp
│ ├── main_server.cpp # binaire matvision-server
│ └── main_calibrate.cpp # binaire matvision-calibrate
├── web/
│ ├── index.html # page de l'interface web (4 onglets)
│ └── static/
│ ├── app.js # onglet Vision (aperçu, plan, calibration)
│ ├── tabs.js # navigation entre les onglets
│ ├── fleet.js # onglets Stratégie et Live table
│ ├── robot.js # onglet Robot principal (iframes)
│ ├── map.svg # map de l'année (fond de la table live)
│ └── style.css # thème sombre
├── tests/
│ └── test_main.cpp # tests d'intégration sans matériel
└── third_party/ # cpp-httplib (HTTP) + nlohmann/json
Prérequis : un compilateur C++17, CMake ≥ 3.16 et OpenCV ≥ 4.7
(modules core, imgproc, imgcodecs, videoio, highgui, calib3d,
objdetect, aruco). Sur Debian/Ubuntu :
sudo apt install build-essential cmake libopencv-devLes dépendances HTTP/JSON (cpp-httplib, nlohmann/json) sont vendorées
dans third_party/ : aucune installation supplémentaire.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -jDeux binaires sont produits :
build/matvision-server— API REST + détection ;build/matvision-calibrate— calibration intrinsèque et contrôle du repère.
Vérification rapide de la caméra :
./build/matvision-calibrate --check-table --seconds 2 # voit-elle les tags de coin ?Sans indication des chemins d'en-têtes, l'éditeur signale
'httplib.h' file not found (et les en-têtes OpenCV) : third_party/ et
include/ ne sont pas des répertoires système.
Le dépôt règle le problème par CMake : CMAKE_EXPORT_COMPILE_COMMANDS génère
build/compile_commands.json, qui contient les chemins exacts (include/,
third_party/, OpenCV). clangd le découvre automatiquement — aucune
configuration d'éditeur n'est donc nécessaire, il suffit d'avoir configuré CMake
une fois :
cmake -S . -B buildLes fichiers propres à l'éditeur (.clangd, .vscode/settings.json) sont
locaux à cette machine et ignorés par git (.gitignore). Pour l'extension
Microsoft C/C++, renseignez au besoin C_Cpp.default.includePath avec
include/, third_party/ et /usr/include/opencv4.
Repère table (millimètres), origine au centre de la table :
+x: vers la droite (largeur 2000 mm) ;+y: vers le fond (longueur 3000 mm) ;+z: vers le haut, perpendiculaire au tapis ;a: lacet en degrés, sens trigonométrique vu du dessus,0°= axe+x, normalisé dans]-180, 180].
Tags de coin (section table.markers de la configuration) :
| id | x (mm) | y (mm) | a (°) |
|---|---|---|---|
| 20 | −400 | −900 | 90 |
| 21 | −400 | +900 | 90 |
| 22 | +400 | −900 | 90 |
| 23 | +400 | +900 | 90 |
a est l'orientation du tag posé sur le tapis (et non la direction du
robot) : il doit correspondre à la rotation physique du tag. Les quatre tags de
coin de ce tapis sont posés à 90° ; une erreur de a (ou deux tags aux mêmes
coordonnées) rend la pose inexploitable — voir §11.
Équivalent C du type retourné :
const position_t ARUCO_POSITIONS_TABLE[] = {
position_t{.x = -400, .y = -900, .a = 90},
position_t{.x = -400, .y = 900, .a = 90},
position_t{.x = 400, .y = -900, .a = 90},
position_t{.x = 400, .y = 900, .a = 90}};Les objets suivis se déclarent dans objects — un robot peut porter n'importe
quel tag de sa plage de couleur :
"objects": [
{ "id": 1, "label": "blue", "angle_offset": 0.0, "offset": [0.0, 0.0] },
{ "id": 2, "label": "blue", "angle_offset": 0.0, "offset": [0.0, 0.0] },
... ,
{ "id": 10, "label": "yellow", "angle_offset": 0.0, "offset": [0.0, 0.0] },
{ "id": 13, "label": "element", "angle_offset": 0.0, "offset": [0.0, 0.0],
"size": 100.0,
"box": { "length_mm": 320.0, "width_mm": 110.0, "height_mm": 110.0 } }
]| Plage de tags | Libellé | Rôle |
|---|---|---|
1 … 5 |
blue |
robots bleus (chaque robot porte l'un de ces tags) |
6 … 10 |
yellow |
robots jaunes |
13 |
element |
élément de jeu : boîte de 320 × 110 × 110 mm, un tag de 100 mm sur chacune de ses faces de 320 × 110 mm |
La position renvoyée est toujours celle du centre du tag, exprimée dans le
repère de la table. z est la hauteur mesurée par solvePnP : pour un tag posé
à plat elle vaut ≈ 0 mm, avec une imprécision de quelques centimètres pour un
petit tag (la profondeur est la grandeur la moins bien contrainte par solvePnP).
Les coordonnées x, y et le lacet a sont les grandeurs utiles au jeu.
Deux réglages optionnels par tag :
angle_offset— correction si le « devant » de l'objet n'est pas dans l'axe+xdu tag (cas fréquent pour un robot) ;offset— décalage[x, y]en mm dans le repère du tag, pour viser un point autre que le centre du tag. Il tourne avec l'orientation mesurée. Défaut[0, 0](centre du tag).
Deux précisions géométriques :
size— côté du tag imprimé (mm, défaut100). Indispensable : c'est la taille physique utilisée parsolvePnPpour retrouver la pose du tag ;box— dimensions (length_mm,width_mm,height_mm) de la boîte d'un élément de jeu, conservées à titre descriptif (exposées par/table).
L'API regroupe les relevés par libellé : un robot bleu en tag 1 et un autre
en tag 3 apparaissent comme deux entrées sous la clé blue. C'est ce qui
permettra d'exposer /blue et /yellow.
Tags supportés. Le dictionnaire
DICT_4X4_50couvre les identifiants 0 à 49 ; les identifiants0à49ont exactement le même motif dansDICT_4X4_100/250/1000(ces dictionnaires sont emboîtés). Un tout autre dictionnaire (DICT_5X5_100,DICT_APRILTAG_36H11…) repousse la limite jusqu'à 1000. Le moteur ne conserve toutefois que les tags déclarés ici et danstable.markers— tout autre tag est détecté puis ignoré.
Les tags se génèrent avec le dictionnaire DICT_4X4_50 d'OpenCV, par exemple
avec cv::aruco::generateImageMarker (opencv2/objdetect/aruco_dictionary.hpp)
ou n'importe quel générateur ArUco. Imprimez le damier à 100 % (« taille
réelle »), puis mesurez une case pour vérifier la cote avant la calibration.
Identifiants utilisés par ce projet :
20…23— tags des 4 coins de la table ;1…5— robots bleus,6…10— robots jaunes ;13— élément de jeu.
Contrôlez que chaque identifiant existe bien dans le dictionnaire (un id hors
plage passe inaperçu au démarrage et rend l'objet invisible) et que chaque tag
imprimé se décode correctement, seul et sous dégradation (flou, bruit,
compression JPEG, perspective). La « marge » entre deux tags — le nombre de bits
qui les séparent — conditionne le risque de confusion :
Exemple :
=== Marge de sécurité (bits vs le plus proche des autres tags du projet) ===
tag 1 : 3 bits (plus proche : tag 6) <-- marge faible
tag 2 : 4 bits (plus proche : tag 8)
...
=== Risque d'échange entre catégories ===
blue <-> yellow : 3 bits (tags 1 / 6)
blue <-> element : 3 bits (tags 1 / 13)
blue <-> coin : 4 bits (tags 3 / 20)
Si un tag imprimé ne se décode pas, vérifiez sa taille (≥ ~40 px de côté à la résolution d'exploitation), son contraste, et que le dictionnaire correspond bien à celui utilisé à l'impression.
Mesuré sur ce projet : les 15 tags configurés décodent correctement et aucune confusion n'a été observée sur 7 680 essais dégradés (jusqu'à 34 px de côté avec flou, bruit et JPEG de mauvaise qualité). Le mode d'échec est toujours « tag absent », jamais « mauvais identifiant » : un tag manqué sur une image est simplement absent du relevé de cette image.
Aucune touche à enfoncer : le programme capture les vues dès qu'elles sont nettes, complètes, correctement cadrées et suffisamment différentes de la précédente. Présentez le damier sous plusieurs angles et distances : les vues hors plan sont indispensables pour contraindre la focale.
./build/matvision-calibrate --device 0 --frames 20--grid 7 7: nombre de coins internes du damier (défaut 7×7) ;--square-size 25: côté d'une case en mm ;--no-display: mode sans fenêtre (SSH) ;--output data/camera_calibration.json.
Sortie : data/camera_calibration.json (matrice intrinsèque + distorsion +
erreur de reprojection). Un fichier data/camera_calibration.yml issu d'une
calibration OpenCV antérieure reste lisible (compatibilité cv::FileStorage).
Critères de capture automatique (réglables dans calibration) :
| Critère | Champ | Défaut |
|---|---|---|
| Damier vu | — | obligatoire |
| Surface / image | min_board_area_ratio … max_board_area_ratio |
4 % … 95 % |
| Netteté (variance du Laplacien) | min_sharpness |
60 |
| Distance au bord | border_margin_px |
10 px |
| Déplacement vs vue précédente | min_move_ratio |
12 % de la diagonale |
Non. La calibration est faite une fois, écrite dans
data/camera_calibration.json, puis rechargée automatiquement à chaque
démarrage. Le moteur ne déclenche jamais de calibration tout seul : elle ne
part que sur matvision-calibrate ou POST /calibration/start.
À refaire uniquement si :
| Changement | Recalibrer ? |
|---|---|
| Redémarrage de la LattePanda ou du service | non |
Changement de résolution (camera.width/height) |
oui ⚠ |
| Réglage de la mise au point (bague de la webcam) | oui |
| Changement de caméra, d'objectif ou de zoom numérique | oui |
| Déplacement du mât, de la table ou des tags de coin | non (la pose caméra est recalculée à chaque image) |
/status → intrinsics_warning, journal, et
panneau « Repère & caméra » de l'interface web).
solvePnP (pose
caméra puis pose de chaque tag). Sans intrinsèques, /objects reste vide et
/status renvoie table.reason = « calibration intrinsèque requise pour la pose
caméra ».
./build/matvision-calibrate --check-table --seconds 5Affiche le nombre de tags de coin détectés, l'erreur de reprojection (px) et la
pose de la caméra. Code de retour 0 si les tags de coin sont vus et la pose
stable.
./build/matvision-server # API sur 0.0.0.0:5000, détection à la demande
./build/matvision-server --autostart # détection lancée immédiatement
./build/matvision-server --display # + fenêtre locale d'aperçu
./build/matvision-server --device 1 --width 3840 --height 2160L'API démarre même si la caméra est absente : le moteur retente l'ouverture
toutes les 2 s et /health renvoie alors ok: false.
Il n'y a plus qu'un seul mode de détection (« classique ») : le moteur cherche
les 4 tags de coin pour estimer la pose caméra, puis relève les objets. Les
anciens modes --test et --match ont été supprimés.
Ouvrez simplement http://<ip-lattepanda>:5000/ (ou /ui) dans un navigateur,
depuis n'importe quelle machine du réseau local :
xdg-open http://localhost:5000/uiL'interface (HTML/CSS/JS sans aucune dépendance externe, donc fonctionnelle hors ligne) est organisée en quatre onglets :
| Onglet | Détail |
|---|---|
| Stratégie | couleur commune au principal, au chasseur et à l'essaim (un seul sélecteur), puis une stratégie par groupe (le même choix pour tous les petits robots). Relaie vers l'API des robots et se resynchronise automatiquement si la stratégie change sur un robot (§9.4). La carte Adresses des robots permet aussi de modifier à chaud l'IP et le port de chaque robot (§8, POST /fleet/<cible>/host), avec enregistrement dans le fichier de configuration. |
| Live table | vue de dessus : positions et trajectoires des robots (principal, chasseur, essaim), robot adverse (couleur opposée) et objets de jeu détectés par le mat, sur la map de l'année en fond. |
| Vision | interface historique du mat : aperçu caméra, plan de table, objets détectés, repère/caméra, calibration. |
| Robot principal | tous les onglets de l'interface du robot principal (Accueil, Control, Camera, Live Table, Lidar, PAMIs, Logs, Robot) affichés dans des iframes pointant sur son adresse. |
L'onglet Vision reprend :
| Élément | Détail |
|---|---|
| Aperçu caméra | flux MJPEG temps réel, choix de la qualité, arrêt du flux pour économiser le CPU |
| Commandes | Démarrer / Arrêter / Réinitialiser / Snapshot / Arrêter le serveur |
| Plan de la table | vue de dessus en direct : tags de coin, objets avec leur cap, axes x/y |
| Objets détectés | tableau libellé, tag, x, y, a, ancienneté |
| Repère & caméra | mode, résolution, fps, verrouillage du repère, inliers, résidu, pose caméra, intrinsèques |
| Calibration | lancement à distance, barre de progression, message d'état, résultat (fx/fy, erreur) |
Détails techniques utiles :
/négocie le contenu : un navigateur reçoit la page HTML,curlreçoit le JSON de découverte — vos scripts existants continuent de fonctionner ;- le flux
/streamest unmultipart/x-mixed-replacebornable (?fps=12&quality=75&frames=0&max_seconds=300), utilisable aussi depuis une simple balise<img>ou depuis OpenCV (cv2.VideoCapture("http://…/stream")) ; - les fichiers statiques ne sont pas mis en cache, pour que les modifications de l'interface soient visibles immédiatement après un rechargement.
Trois moyens équivalents, tous propres : le moteur est arrêté, la boucle vidéo jointe et la caméra libérée avant que le processus ne se termine.
# 1. depuis le terminal qui a lancé le serveur
Ctrl+C
# 2. depuis n'importe quel terminal (le PID est affiché au démarrage)
kill <pid>
# 3. depuis le réseau (curl, script, supervision…)
curl -X POST http://<ip-lattepanda>:5000/shutdownLe bouton « Arrêter le serveur » de l'interface web fait la même chose (demande de confirmation avant d'agir).
POST /shutdowncoupe le service : ne l'exposez pas au-delà de votre réseau local. Sur un serveur sans arrêt câblé (par exemple en test), la route répond501au lieu d'agir.
Tous les messages sont écrits sur la console (stderr), jamais dans un
fichier. Le niveau par défaut est INFO ; -v/--verbose passe en DEBUG
pour le détail par image (étapes de détection, requêtes HTTP…).
./build/matvision-server -v # logs détaillés (DEBUG)Repères utiles :
| Message | Signification |
|---|---|
configuration effective : N objets, M tags de coin, roi=… |
configuration effective au démarrage |
caméra prête : WxH @ F fps (FOURCC) |
résolution réellement négociée |
première image reçue : WxH |
résolution réelle du flux |
repère table acquis (tags=N, résidu X px) |
les tags de coin sont vus et la pose estimée (transition non défini → acquis) |
repère table non défini : <raison> (tags de coin vus N/M) |
la pose n'est pas exploitable ; la cause exacte (calibration manquante, tags de coin insuffisants, résidu trop élevé…) est répétée au plus toutes les 5 s |
perf: X fps | traitement Y ms/image (max Z ms) | mode=… | objets=N |
bilan toutes les 5 s |
calibration sur N vues (WxH)... |
début du calcul intrinsèque |
calibration enregistrée dans … |
fichiers intrinsèques écrits |
| Méthode | Route | Description |
|---|---|---|
GET |
/ |
Interface web (navigateur) ou découverte JSON (curl) |
GET |
/ui |
Interface web de pilotage |
GET |
/api |
Découverte JSON : version + liste des routes |
GET |
/health |
Test de vie (supervision) |
GET |
/status |
État complet : caméra, mode, table, fps, erreurs |
GET/POST |
/start |
Démarre la détection |
GET/POST |
/stop |
Arrête la détection |
GET/POST |
/reset |
Réinitialise le suivi (alias /reset_tracking) |
GET |
/objects |
Objets détectés, repère table (mm, degrés) |
GET |
/objects/<clé> |
Objets d'un tag (36) ou d'un libellé (bleu) |
GET |
/position |
Pose de la caméra dans le repère table |
GET |
/table |
Géométrie de la table, tags de coin, objets |
GET |
/preview |
Dernière image annotée (JPEG, ?quality=) |
GET |
/stream |
Flux MJPEG temps réel (?fps=, ?frames=, ?max_seconds=) |
GET |
/config |
Configuration effective |
POST |
/calibration/start |
Lance la calibration automatique |
GET |
/calibration/status |
Progression (vues capturées, message) |
POST |
/calibration/stop |
Annule la calibration |
GET |
/calibration/result |
Intrinsèques courantes |
POST |
/snapshot |
Enregistre l'image courante (?path=) |
POST |
/shutdown |
Arrête le serveur et le processus (501 si non câblé) |
GET |
/fleet |
État des robots : en ligne, couleur, stratégie, table |
GET |
/fleet/strategies |
Stratégies disponibles par robot |
POST |
/fleet/<cible>/strategy |
Change la stratégie (<cible> = main, hunter, swarm, swarm/<i> ou un nom) |
POST |
/fleet/<cible>/color |
Change la couleur (all = toute la flotte ; {"color": 1} bleu, {"color": 2} jaune) |
POST |
/fleet/<cible>/host |
Change l'adresse d'un robot (clé unique ou nom) : {"host": "192.168.1.10", "port": 80} ; enregistrée dans le fichier de configuration |
GET |
/fleet/live |
Positions, trajectoires, adversaire et objets de jeu |
GET/POST |
/fleet/report |
Position déclarée d'un robot sans tag ; renvoie les objets de jeu |
# Lancement puis lecture des positions
curl http://192.168.1.50:5000/start
curl http://192.168.1.50:5000/objects{
"count": 3,
"objects": [
{ "id": 1, "label": "blue", "x": 412.3, "y": -318.7, "z": 0.0, "a": 92.4 },
{ "id": 13, "label": "element", "x": 705.2, "y": -410.9, "z": 0.0, "a": 45.3 },
{ "id": 13, "label": "element", "x": 710.1, "y": -398.2, "z": 0.0, "a": -44.6 }
],
"by_label": {
"blue": [ { "id": 1, "label": "blue", "x": 412.3, "y": -318.7, "z": 0.0, "a": 92.4 } ],
"element": [
{ "id": 13, "label": "element", "x": 705.2, "y": -410.9, "z": 0.0, "a": 45.3 },
{ "id": 13, "label": "element", "x": 710.1, "y": -398.2, "z": 0.0, "a": -44.6 }
]
}
}Il n'y a pas de suivi temporel :
objectsest le relevé de la dernière image. Un même élément de jeu vu par deux de ses tags apparaît donc deux fois, à des positions très proches (il suffit à l'appelant de les moyenner ou de ne garder que la première).
# Pose caméra + qualité du repère table
curl http://192.168.1.50:5000/position
# Aperçu annoté (image unique)
curl -o preview.jpg http://192.168.1.50:5000/preview
# Flux temps réel : lecture directe dans OpenCV
# python -c "import cv2; cap = cv2.VideoCapture('http://192.168.1.50:5000/stream?fps=10')"
# Calibration à distance
curl -X POST http://192.168.1.50:5000/calibration/start
curl http://192.168.1.50:5000/calibration/status
# Arrêt complet du serveur (libère la caméra et termine le processus)
curl -X POST http://192.168.1.50:5000/shutdown# Flotte : état, stratégies disponibles, live
curl http://192.168.1.50:5000/fleet
curl http://192.168.1.50:5000/fleet/strategies
curl http://192.168.1.50:5000/fleet/live
# Changer la stratégie du principal, la couleur commune à toute l'équipe
curl -X POST http://192.168.1.50:5000/fleet/main/strategy -H 'Content-Type: application/json' -d '{"strat": "Match"}'
curl -X POST http://192.168.1.50:5000/fleet/all/color -H 'Content-Type: application/json' -d '{"color": 1}'
# Changer l'adresse d'un robot (IP et port), enregistrée dans la configuration
curl -X POST http://192.168.1.50:5000/fleet/hunter/host -H 'Content-Type: application/json' \
-d '{"host": "192.168.1.11", "port": 80}'
# Un petit robot sans tag déclare sa position et reçoit les objets de jeu
curl -X POST http://192.168.1.50:5000/fleet/report -H 'Content-Type: application/json' \
-d '{"robot": "swarm/0", "x": -320.0, "y": 810.0, "a": 90.0}'| Besoin | Clé |
|---|---|
| Passer en 4K | camera.width = 3840, camera.height = 2160 |
| Fluidité sur LattePanda | camera.width/height = 1280×720, detection.draw = false |
| Marge de la ROI | table.roi_margin_px |
| Annotation de l'aperçu | detection.draw |
Les tags de coin sont à ±400 mm en x alors que la table mesure 2000 mm : le quadrilatère des tags ne couvre pas tout le tapis.
| Valeur | Zone analysée | Quand l'utiliser |
|---|---|---|
"table" (défaut) |
toute la surface de jeu, obtenue en projetant les 4 coins de la table avec la pose caméra | les objets peuvent aller près des bords |
"markers" |
quadrilatère des 4 tags de coin (+ marge) | zone de jeu restreinte entre les tags, un peu plus rapide et moins de faux positifs |
Avec "table", un objet posé à x = 520 mm (hors des tags) est bien détecté ;
avec "markers" il est invisible. Vérifiable via matvision-tests.
Toute modification de config/default.json est prise en compte au démarrage
suivant. --config autre.json permet de gérer plusieurs terrains.
Les tags de coin ne mesurent que 100 mm sur un terrain de 3000 mm de long : la résolution d'image conditionne donc directement la détection. Pour couvrir 2000 mm de champ sur la hauteur de l'image :
| Résolution | px / mm | Taille d'un tag 100 mm | px par cellule (4×4) |
|---|---|---|---|
| 1280×720 | 0,36 | ~36 px | ~6 px ⚠ |
| 1920×1080 | 0,54 | ~54 px | ~9 px |
| 3840×2160 (4K) | 1,08 | ~108 px | ~18 px ✓ |
La Logitech 4K Stream Edition permet camera.width: 3840, camera.height: 2160,
fourcc: MJPG. Si le CPU de la LattePanda Delta sature, réduisez d'abord
detection.draw puis la résolution — ou ne détectez les tags de coin qu'en
basse résolution et les objets en pleine résolution.
La section robots décrit le robot principal, le chasseur et l'essaim. Chaque
robot est piloté via son API REST (/get_robot, /get_strategies, /set_strat,
/set_color) ; un robot sans host n'est jamais contacté (aucun appel réseau).
"robots": {
"our_color": "",
"request_timeout_s": 0.8,
"path_window_s": 30.0,
"map_image": "",
"main": { "name": "Principal", "host": "192.168.1.10", "port": 80 },
"hunter": { "name": "Chasseur", "host": "192.168.1.11", "port": 80 },
"swarm": [
{ "name": "Essaim 1", "host": "192.168.1.21", "port": 80 },
{ "name": "Essaim 2", "host": "192.168.1.22", "port": 80 },
{ "name": "Essaim 3", "host": "192.168.1.23", "port": 80 }
]
}| Clé | Rôle |
|---|---|
our_color |
"blue"/"yellow" : couleur de secours si le robot principal est injoignable (sinon lue sur /get_robot) |
request_timeout_s |
délai maximal d'un appel à un robot |
path_window_s |
durée de trajectoire conservée pour la table live |
map_image |
image de fond optionnelle (chemin ou URL) ; par défaut static/map.svg |
main.tag |
tag ArUco du principal si plusieurs tags de la couleur sont présents (sinon déduit) |
Modifier une adresse à chaud. Depuis la carte Adresses des robots de
l'onglet Stratégie, ou via POST /fleet/<cible>/host (voir §8), l'IP et le
port d'un robot peuvent être changés sans redémarrer le serveur. La
modification est appliquée immédiatement (le cache d'état de ce robot est
invalidé) puis réécrite dans le fichier de configuration chargé au démarrage
(--config), si bien qu'elle est conservée au redémarrage suivant. Un host
vide remet le robot en « non configuré » (jamais contacté).
Qui voit quoi. Le robot principal et l'adversaire sont vus par la caméra :
- le principal est le tag de notre couleur (
blue→ tags 1-5,yellow→ tags 6-10) ; sa couleur est lue sur/get_robot(team) ; - l'adversaire est tout tag de la couleur opposée (si nous sommes bleus, les tags 6-10) ;
- les objets de jeu sont les autres tags déclarés (ex.
element, tag 13).
Le chasseur et l'essaim n'ont pas de tag : ils déclarent leur position à chaque requête et reçoivent en retour les objets de jeu.
La couleur est commune au principal, au chasseur et à l'essaim :
POST /fleet/all/color l'applique à toute la flotte (l'interface ne propose donc
qu'un seul sélecteur de couleur). Chaque groupe garde en revanche sa propre
stratégie (POST /fleet/main/strategy, /fleet/hunter/strategy,
/fleet/swarm/strategy).
Contrat pour le chasseur et les petits robots :
curl -X POST http://<ip-mat>:5000/fleet/report \
-H 'Content-Type: application/json' \
-d '{"robot": "swarm/0", "x": -320.0, "y": 810.0, "a": 90.0}'robot accepte une clé (main, hunter, swarm/<i>), swarm (tous) ou le nom
du robot. La réponse contient objects (objets de jeu) et opponents
(adversaires) :
{
"robot": "swarm/0",
"our_color": "blue",
"opponent_color": "yellow",
"objects": [
{ "id": 13, "label": "element", "x": 705.2, "y": -410.9, "a": 45.3 }
],
"opponents": [ { "id": 6, "x": -250.0, "y": -400.0, "a": -75.0 } ]
}Client du robot principal. Le programme du robot parle au mat en HTTP
(TCP) (src/mat/mat.cpp, cpp-httplib) et attend le mat sur mat.local:5000
(surchargeable à la compilation : -DMAT_HOST=… -DMAT_PORT=…). Il appelle
GET /start et GET /stop (démarrage/arrêt de la détection) puis
GET /fleet/live en boucle pour récupérer la position de l'adversaire et les
objets de jeu.
La suite de tests d'intégration génère une scène synthétique (caméra en visée plongeante, tags de coin et objets) et vérifie toute la chaîne, y compris l'API avec une caméra factice :
cmake --build build -j # construit aussi matvision-tests
./build/matvision-tests # ou : ctest --test-dir buildCouverture : géométrie et conventions d'angle, détection ArUco, pose caméra par
solvePnP (sans homographie) et changement de base, positions des objets en
mm/deg, ROI, sérialisation de la calibration, calibration automatique sur damier
synthétique, avertissement de résolution, toutes les routes de l'API, l'interface
web (page HTML, ressources statiques, négociation de contenu, flux MJPEG borné)
et la flotte de robots (état, stratégies, pilotage d'un robot simulé par un petit
serveur HTTP, déclarations de position et vue live).
Exemple de sortie :
== géométrie ==
== pose caméra (solvePnP) ==
== moteur & positions ==
== intrinsèques (sérialisation) ==
== avertissement de résolution ==
== calibration automatique ==
== API HTTP & flotte ==
== route /shutdown ==
87/87 vérifications réussies
OK
| Symptôme | Piste |
|---|---|
impossible d'ouvrir la caméra |
ls -l /dev/video*, essayer --device 1, vérifier qu'aucune autre appli n'utilise la caméra |
| Image très sombre / surexposée | fixer l'exposition dans camera (auto_exposure: false) et éclairer la table |
0 tag de coin détecté |
vérifier le dictionnaire (DICT_4X4_50), la taille des tags, l'éclairage, et que le plan de table est bien visible |
calibration intrinsèque requise dans /status |
lancer matvision-calibrate : sans intrinsèques, aucune position n'est calculable |
pose caméra imprécise (résidu … px) |
tags de coin corrompus/flous ou positions table.markers erronées. La ligne repère table non défini : … (toutes les 5 s) donne la cause exacte et le nombre de tags vus |
pose caméra imprécise avec beaucoup de tags vus mais résidu énorme |
deux tags de coin partagent les mêmes coordonnées, ou l'orientation a ne correspond pas au tag physique (les quatre tags de ce tapis sont à a: 90) |
| Objets jamais renvoyés | ids absents de objects, tags trop petits (voir minMarkerPerimeterRate), ou size de l'objet faux |
| Latence / fps faible | réduire la résolution, désactiver detection.draw, passer en fourcc: MJPG |
Erreur au linkage : undefined reference to cv::aruco… |
OpenCV sans module contrib : installer libopencv-dev (paquets complets) |