Un décor d'après le plan d'un bâtiment ====================================== Un bâtiment réel se dessine une fois, en 2D, dans LibreCAD. Ce plan donne ensuite deux choses : - un **décor Gazebo** (``.sdf``), pour essayer la navigation dans le bâtiment avant d'y faire rouler le robot ; - la **carte** de navigation (``.pgm`` + ``.yaml``), dessinée d'après ce décor par la même méthode que celle de tous les autres décors. .. code-block:: text plan.dxf ──dxf_world──▶ decor.sdf ──world_map──▶ carte.pgm + carte.yaml (LibreCAD) (Gazebo) (Nav2) Le décor ``lisv``, le laboratoire en entier (34 × 38 m, 50 portes), est fait ainsi depuis le 2026-10-25 : plan ``ratbot_gazebo/worlds/lisv.dxf``, décor ``lisv.sdf``, carte ``ratbot_maps/maps/lisv.yaml``. Pourquoi un plan plutôt que le SLAM : une carte faite au SLAM hérite des erreurs de celui qui l'a faite (odométrie, réglages), alors que le plan est exact. Les cartes des décors simulés sont toutes dessinées d'après leur plan pour cette raison (voir :doc:`simulation`). Dessiner le plan dans LibreCAD ------------------------------ .. figure:: ../_static/images/simulation/librecad_plan_lisv.png :width: 100% :alt: Le plan du LISV dans LibreCAD : murs en magenta, fenêtres en cyan, portes en jaune, charnières en vert ; à droite la liste des calques. Le plan du LISV dans LibreCAD, et ses quatre calques (liste à droite) : murs en magenta, fenêtres en cyan, portes en jaune, charnières en vert. Les couleurs sont libres ; seuls les noms des calques comptent. **L'unité est le mètre.** Dans LibreCAD, régler l'unité du dessin sur le mètre (*Options › Préférences du dessin*, onglet *Unités*) avant de commencer : une porte de 80 cm mesure 0,8 sur le plan. Une grille de 10 cm et l'accrochage aux extrémités et à la grille (barre du bas) évitent les petits écarts entre deux segments qui devraient se toucher. **Quatre calques, aux noms exacts** (en majuscules), créés depuis la *Liste des calques* (bouton ``+``) : .. list-table:: :header-rows: 1 :widths: 14 14 72 * - Calque - Entité - Ce qu'on y dessine * - ``WALLS`` - lignes - Les murs, un segment par pan, le long de l'**axe** du mur. À chaque porte, laisser dans le mur un **trou de la largeur de la porte** : c'est ce trou que la carte verra, pas le battant. * - ``WINDOWS`` - lignes - Les fenêtres, dans les trous des murs extérieurs. Un mur extérieur est une suite continue de murs et de fenêtres, sans aucun vide. * - ``DOORS`` - lignes - **Un segment par battant.** Une porte simple a un battant, une double porte deux. Le battant se dessine ouvert (perpendiculaire au mur, dans la pièce où il s'ouvre) ou fermé (dans le trou du mur, dans le prolongement du mur). Sa longueur est la largeur qu'il ferme : 0,8 pour une porte de 80 cm, 2 × 0,4 pour une double porte de 80 cm. * - ``HINGES`` - points - **La charnière de chaque battant** : un point (outil *Point*), posé exactement sur l'extrémité du battant côté gonds. Une charnière par battant, donc deux pour une double porte. Trois règles évitent presque toutes les erreurs : - **Seules les lignes et les points sont lus.** Une polyligne, un arc ou un rectangle est ignoré : les éclater en lignes avant d'enregistrer (*Modifier › Éclater*). - **Une charnière est toujours au bout d'un battant**, à moins d'un millimètre. L'accrochage aux extrémités la place exactement ; l'outil refuse un battant sans charnière, ou une charnière seule. - **Le bâtiment doit être fermé.** Le moindre vide dans un mur extérieur fait communiquer l'intérieur avec l'extérieur, et la carte est refusée (voir *Vérifier* ci-dessous). Enregistrer au format DXF (*Fichier › Enregistrer sous*, type *Drawing Exchange DXF 2007*, le format par défaut de LibreCAD), dans ``ros2_ws/src/ratbot_gazebo/worlds/NOM.dxf``. Générer le décor et la carte ---------------------------- Une seule fois, la bibliothèque qui lit le DXF (elle est déjà dans l'image Docker et dans ``./scripts/ratbot deps``) : .. code-block:: bash sudo apt install python3-ezdxf **Les places de départ** des robots s'écrivent à la main, à côté du plan, dans ``worlds/NOM.spawn.yaml`` : quatre places (le maximum de la simulation), le robot 1 en tête, dans un endroit libre, à 60 cm l'une de l'autre et à plus de 30 cm des murs. Les coordonnées sont celles du plan. Celles du LISV, dans le grand couloir : .. code-block:: yaml spawn: - {x: 19.20, y: 10.00, yaw: 1.5708} - {x: 19.20, y: 10.60, yaw: 1.5708} - {x: 19.20, y: 11.20, yaw: 1.5708} - {x: 19.20, y: 11.80, yaw: 1.5708} Puis, depuis ``ros2_ws/src/ratbot_gazebo`` : .. code-block:: bash # le plan -> le décor Gazebo python3 -m ratbot_gazebo.dxf_world worlds/NOM.dxf worlds/NOM.sdf # le décor -> la carte (coupe à la hauteur du lidar, 22 cm) python3 -m ratbot_gazebo.world_map worlds/NOM.sdf ../ratbot_maps/maps/NOM # installer, puis s'en servir cd ../.. && colcon build --symlink-install --packages-select ratbot_gazebo ratbot_maps ./scripts/ratbot up NOM # le bâtiment en simulation ./scripts/ratbot nav 1 NOM # naviguer sur sa carte Sur la carte du décor en cours, ``ratbot nav N`` démarre AMCL à la place de départ du robot N (lue dans ``NOM.spawn.yaml``), et non à l'origine : dans un bâtiment en L comme le LISV, l'origine du plan est hors des murs. Une carte faite au SLAM garde la pose de départ (0, 0), qui est la sienne. Pour une autre pose : ``ratbot nav N NOM initial_x:=… initial_y:=… initial_yaw:=…``. ``dxf_world`` affiche ce qu'il a lu (``176 walls, 53 windows, 50 door leaves``) ; ``world_map`` la taille de la carte et son origine. La carte remplit l'espace libre **à partir de la place du robot 1** : ce qui est accessible depuis là est libre (blanc), les murs sont noirs, le reste est inconnu (gris). **L'état des portes** se choisit à la génération du décor : .. list-table:: :header-rows: 1 :widths: 22 78 * - ``--doors`` - Portes * - ``as-drawn`` (défaut) - Chaque battant tel que dessiné, ouvert ou fermé. * - ``open`` - Les battants fermés sont retirés, les autres gardés : toutes les portes laissent passer. * - ``closed`` - Chaque battant ouvert est rabattu dans son trou, autour de sa charnière, et prolongé jusqu'au mur d'en face : aucune porte ne laisse passer. Utile pour tester une navigation bloquée. Ce que devient le plan ---------------------- .. list-table:: :header-rows: 1 :widths: 22 78 * - Calque - Dans le décor * - ``WALLS`` - Boîtes de 10 cm d'épaisseur et 2,50 m de haut, prolongées de 5 cm à chaque bout pour que les angles se ferment. * - ``WINDOWS`` - Le **mur sous l'appui**, de 0,90 m de haut : à 22 cm du sol, c'est tout ce que voit le lidar d'une fenêtre. Elle ferme donc la carte comme un mur. * - ``DOORS`` + ``HINGES`` - Des battants de 4 cm d'épaisseur et 2,00 m de haut, immobiles. Ces valeurs sont en tête de ``ratbot_gazebo/dxf_world.py``. Le sol est uni, gris beige, comme les dalles plastiques du LISV. .. figure:: ../_static/images/simulation/carte_lisv.png :width: 70% :alt: La carte du LISV : murs noirs, espace libre blanc, pièces inaccessibles grises. La carte du LISV produite par ``world_map`` (5 cm par pixel). Les zones grises sont des pièces sans porte, ou derrière une porte fermée : depuis le couloir, le robot ne peut pas y entrer. Comment se fabrique la carte ---------------------------- La carte de navigation est une image (``NOM.pgm``) et sa description (``NOM.yaml``), le format que lit le serveur de carte de Nav2. ``world_map`` la tire du décor en quatre étapes : 1. **La coupe à la hauteur du lidar.** Seules les formes de collision du décor (boîtes et cylindres) qui traversent le plan horizontal à 22 cm sont gardées : ce que le lidar verrait. Un mur, un battant, le mur sous une fenêtre traversent ce plan ; le plateau d'une table ou le pied bas d'une chaise, non. Ceux-là, seul le capteur de distance avant doit les trouver. 2. **La grille.** Le plan coupé est découpé en cases de 5 cm, avec 25 cm de marge autour du bâtiment. Une case est un mur dès qu'un de ses points d'échantillonnage, espacés de 1 cm, tombe dans une forme : une planche de 2 cm ou une jonction entre deux murs ne laisse ainsi aucun trou. 3. **Le remplissage depuis le robot.** Partant de la place du robot 1, toutes les cases libres qu'il peut atteindre de proche en proche deviennent *libres* ; les cases murs restent *occupées* ; tout le reste, pièces fermées et extérieur, devient *inconnu*. Si ce remplissage atteint le bord de l'image, le bâtiment n'est pas fermé et la carte est refusée ; si la place de départ tombe dans un mur, aussi. 4. **L'écriture.** L'image est en niveaux de gris, une case par pixel : 0 (noir) pour un mur, 254 (blanc) pour le libre, 205 (gris) pour l'inconnu. Le ``.yaml`` dit comment la lire : .. list-table:: :header-rows: 1 :widths: 24 22 54 * - Champ - LISV - Sens * - ``image`` - ``lisv.pgm`` - L'image, à côté du ``.yaml``. * - ``mode`` - ``trinary`` - Trois valeurs seulement : occupé, libre, inconnu. * - ``resolution`` - ``0.05`` - Mètres par pixel. * - ``origin`` - ``[-0.35, -0.35, 0]`` - Position, dans le repère du décor, du coin **en bas à gauche** de l'image (x, y, rotation). Ici, 35 cm hors du coin du plan : la marge de 25 cm, le demi-mur et l'arrondi à la case. * - ``negate`` - ``0`` - Noir = occupé (et non l'inverse). * - ``occupied_thresh``, ``free_thresh`` - ``0.65``, ``0.25`` - Seuils de lecture des gris ; en ``trinary``, ils séparent simplement les trois valeurs ci-dessus. Les deux réglages de ``world_map`` : ``--height`` (hauteur de la coupe, 0,22 m par défaut, celle du lidar) et ``--resolution`` (0,05 m par défaut ; une carte à 2 cm, essayée au banc, améliorait la localisation mais rapprochait le robot des pieds de table : non retenue). La carte s'ouvre avec n'importe quel visualiseur d'images (``eog lisv.pgm``), ou dans RViz une fois la navigation lancée. .. note:: **Plan ou exploration ?** Une carte se fait de deux façons. *En roulant* (``ratbot explore`` ou ``ratbot slam``) : le robot part sans rien connaître et la construit au fur et à mesure ; c'est la méthode d'un lieu inconnu, et la seule qui voie les meubles. *D'après un plan*, comme ici : exacte pour les murs, sans rouler, mais aveugle à ce que le plan ne montre pas. Les cartes des décors de simulation sont toutes dessinées d'après leur plan, parce qu'une carte faite au SLAM en simulation hérite des erreurs de la simulation. Pour le vrai laboratoire, les deux se complètent : la carte du plan sert à préparer et à tester en simulation, une exploration du labo meublé donne la carte réelle, et la première sert de référence pour juger la seconde. Vérifier -------- - **« no hinge at either end »** ou **« hinges without a door »** : un battant sans charnière, ou une charnière seule. Le message donne ses coordonnées ; la reprendre avec l'accrochage aux extrémités. - **« the free space reaches the map border: decor not closed »** : un vide dans un mur extérieur. Sur le plan du LISV, il manquait 40 cm de mur entre deux fenêtres (x = 34, y de 21,7 à 22,1). Pour le trouver, parcourir le mur extérieur de segment en segment : chaque fin de segment doit être le début du suivant. - **Une zone grise inattendue** : une pièce qui devrait être accessible a sa porte fermée, ou son trou de porte n'a pas été laissé dans le mur. - **Une porte trop étroite sur la carte** : le trou du mur est plus petit que la porte réelle. C'est le trou qui compte : un battant un peu court ne gêne pas, un trou trop petit, si. Les règles de l'outil sont au banc de tests du paquet (``test/test_dxf_world.py`` : appariement des charnières, modes des portes, porte rabattue jusqu'au mur, fenêtre qui ferme la carte) : .. code-block:: bash cd ros2_ws/src/ratbot_gazebo && python3 -m pytest -q test/ Limites ------- - **Le plan ne montre que les murs.** Les meubles, les armoires, les cartons n'y sont pas : sur le vrai robot, le lidar les voit, et la navigation les traite comme des obstacles nouveaux. Pour naviguer au quotidien, une carte faite au SLAM dans le bâtiment meublé reste utile ; le plan sert à préparer et à tester. - **Le lidar voit souvent à travers une vitre basse** : là où le plan met le mur sous une fenêtre, une carte réelle peut avoir un trou. - **Les portes sont figées** dans l'état choisi à la génération ; elles ne s'ouvrent pas pendant une simulation.