Aller au contenu

Entraîner des modèles sur des volumes, des nuages de points et de la vidéo

Ajustez un U-Net 3D sur des masques de scanner peints, exécutez votre propre détecteur OpenPCDet sur des cuboïdes lidar et entraînez un suiveur vidéo, chacun évalué sur des items réservés.

Nouveauté de la v2.14.0

Ce tutoriel entraîne trois modèles sur des données 3D et animées, à partir des annotations d'une étude Potato. volume-unet entraîne un U-Net 3D qui segmente des volumes de scanner (CT) et d'IRM (MR), external-3d exécute un détecteur lidar que vous construisez et faites tourner vous-même, et video-track entraîne un suiveur d'objets vidéo. La page s'adresse aux équipes qui annotent déjà des volumes, des nuages de points ou de la vidéo dans Potato et veulent qu'un modèle pré-étiquette le reste. Pour chaque module d'entraînement, la page présente le schéma qui produit les données d'entraînement, une configuration qui passe potato validate, ce que fait une exécution, le score sur items réservés et la façon dont les prédictions parviennent aux annotateurs. Le tutoriel du premier modèle parcourt la page Training avec un classifieur de texte qui n'a pas besoin de GPU, et le présent tutoriel suppose ce parcours connu.

Récapitulatif des modules d'entraînement

Les trois modules ajustent un schéma par exécution, évaluent le modèle sur les items réservés val et test de l'exécution et écrivent les prédictions en retour sous forme de pré-étiquettes que les annotateurs modifient. volume-unet et les détecteurs qui se trouvent derrière external-3d demandent un GPU en pratique, de sorte que les configurations de ces deux modules envoient les exécutions à un serveur GPU par ssh, comme le met en place le tutoriel sur l'entraînement à distance.

ModuleSchémaItemsInstallation sur la machine d'entraînementScore principal
volume-unetvolume_annotation (masques peints)volumes NIfTI, séries DICOMpip install "potato-annotation[train-vision,volumes]" ; un GPU en pratiquemean_dice
external-3dspatial_annotation (cuboïdes)nuages de pointsvotre propre détecteur (OpenPCDet, MMDetection3D)map_3d_50
video-trackvideo_annotation en mode tracking ou combinedvidéopip install "potato-annotation[train-vision]" opencv-pythonidf1

Chaque module lit les médias dans des fichiers sur disque. Un chemin de média doit pointer à l'intérieur du répertoire de la tâche ou de media_directory, et les items dont le média est une URL http(s) sont exclus de l'entraînement et de la prédiction.

Segmenter des volumes CT et MR avec volume-unet

volume-unet apprend les masques que les annotateurs peignent dans un schéma volume_annotation et peint des masques sur les volumes que personne n'a encore segmentés. Le schéma affiche un fichier NIfTI ou une série DICOM dans trois vues liées (axiale, coronale et sagittale), et les annotateurs peignent chaque étiquette au pinceau ou en fermant un polygone. Le champ d'item que nomme source_field contient un fichier .nii ou .nii.gz, ou un répertoire contenant une série DICOM, relatif à media_directory. Une boîte tracée sur une coupe n'a pas de masque, de sorte qu'une réponse qui ne contient que des boîtes n'est pas une cible d'entraînement. Potato est un logiciel de recherche sans autorisation clinique ni réglementaire, et un modèle entraîné ici propose des masques que les annotateurs corrigent.

La configuration ci-dessous segmente la rate et le foie, s'entraîne sur un serveur GPU et pré-étiquette tous les volumes situés hors de la partition train :

yaml
annotation_task_name: "Spleen segmentation"
task_dir: .
media_directory: media
data_files:
  - data/scans.jsonl
item_properties:
  id_key: id
  text_key: volume
output_annotation_dir: annotation_output
user_config:
  allow_all_users: true
 
annotation_schemes:
  - annotation_type: volume_annotation
    name: organs
    description: "Paint the spleen and the liver on every slice where each organ appears."
    source_field: volume
    labels:
      - {name: Spleen, color: "#e6553a", key_value: "1"}
      - {name: Liver, color: "#3a8ee6", key_value: "2"}
    tools: [brush, eraser, polygon]
    default_window: Soft tissue
 
model_training:
  enabled: true
  backend: gpu-server
  backends:
    gpu-server:
      type: ssh
      host: gpu.example.edu
      python: /opt/conda/bin/python3
      gpus: auto
      min_free_gpu_mb: 8000
  trainer_params:
    volume-unet:
      patch: 96
  writeback:
    schemas: [organs]
    min_confidence: 0.6
    include_unlabeled: true

Chaque ligne de données nomme un volume :

json
{"id": "case_001", "volume": "case_001.nii.gz"}
{"id": "case_002", "volume": "case_002_ct"}

La valeur patch fixe le côté du patch cubique sur lequel le réseau s'entraîne, et doit être un multiple de 4 avec les trois niveaux par défaut (2 puissance levels moins 1). Un patch de 96 voxels donne au réseau plus de contexte autour de chaque voxel que le patch de 64 par défaut et demande plus de mémoire GPU. Avec min_free_gpu_mb: 8000, Potato ne lance une exécution que sur un GPU qui dispose d'au moins 8 000 Mo libres. Potato prend la probabilité moyenne du modèle à l'intérieur du masque de chaque étiquette peinte et rapporte la plus basse de ces moyennes comme confiance du volume. Avec min_confidence: 0.6, un volume où un seul organe a été peint avec une probabilité moyenne inférieure à 0,6 ne reçoit aucune pré-étiquette.

Étapes d'entraînement d'une exécution volume-unet

Une exécution lit chaque volume annoté dans l'ordre des voxels où peint le widget et transforme les masques en une étiquette par voxel. Lorsque deux étiquettes se recouvrent, la dernière dans l'ordre des étiquettes du lot l'emporte, comme dans l'export NIfTI. Les intensités sont écrêtées aux percentiles 0,5 et 99,5 de chaque volume, puis standardisées. Le réseau est un U-Net 3D en torch pur (trois niveaux, normalisation d'instance), entraîné avec l'entropie croisée plus un Dice souple sur 100 patchs aléatoires par époque. La moitié des patchs sont centrés sur un voxel peint, de sorte que les petites structures apparaissent à l'entraînement. Après chaque époque, l'exécution évalue le Dice moyen sur les volumes val et garde la meilleure époque, puis s'arrête après 10 époques sans meilleur score. La prédiction fait glisser le patch sur tout le volume avec un recouvrement d'un demi-patch et moyenne les probabilités.

La partition train exige au moins trois volumes annotés (min_instances). Un volume dont la forme peinte diffère de celle du fichier est exclu avec un avertissement dans le journal de l'exécution, tout comme un volume dont le fichier est absent. Lorsque plusieurs lecteurs ont annoté un même volume, l'exécution s'entraîne sur les masques du seul lecteur que désigne aggregation, car les agrégateurs comparent des réponses entières. Segmenter des volumes CT avec plusieurs lecteurs et STAPLE traite de l'accord entre lecteurs et d'un masque de consensus construit hors de Potato.

Score sur items réservés pour les volumes

Potato calcule le coefficient de Dice pour chaque étiquette sur chaque volume réservé (dice_<label>) et rapporte la moyenne sur tous les volumes et toutes les étiquettes sous le nom mean_dice. Une étiquette que le modèle a omise sur un volume qui la contient obtient 0. Le module choisit la meilleure époque sur val, de sorte que le tableau Runs prend le score principal sur test (test_mean_dice). La liste complète des scores par étiquette se trouve sur la page de l'exécution.

Pré-étiquettes de masques sur les vues en coupes

Les masques prédits reviennent dans la forme propre au widget, par étiquette et par coupe axiale, marqués prefilled_from: model, et apparaissent sur les vues en coupes lorsqu'un annotateur ouvre le volume. L'annotateur corrige les masques avec le pinceau et la gomme, et les masques corrigés sont enregistrés comme réponse de cet annotateur. Les items de la partition train de l'exécution ne reçoivent jamais de pré-étiquette. Sans include_unlabeled, une exécution ne prédit que sur les volumes val et test de l'exécution.

Détecter des cuboïdes 3D avec external-3d

external-3d confie les scans annotés à un détecteur 3D que vous installez sur l'hôte d'entraînement, exécute vos commandes d'entraînement et de prédiction, puis relit les boîtes prédites sous forme de cuboïdes. Potato n'inclut aucun détecteur de nuages de points. Les deux détecteurs pour lesquels le module est conçu, OpenPCDet et MMDetection3D, demandent des opérateurs CUDA compilés sur l'hôte GPU. Côté Potato, le module n'a besoin que de numpy.

Les données d'entraînement viennent d'un schéma spatial_annotation avec l'outil cuboid_3d. Les annotateurs placent des boîtes orientées sur des scans PCD, PLY, LAS ou KITTI .bin, stockées en mètres dans le repère propre du scan (voir Nuages de points 3D et cuboïdes). Les cuboïdes sont les seules cibles, de sorte que les points, les polylignes et les segments de points du même schéma sont ignorés. Chaque item est un scan, et une séquence de trames avec des identifiants de piste n'est pas une cible d'entraînement.

Dossier de données écrit pour le détecteur

Avant l'exécution de la commande d'entraînement, Potato écrit les scans dans {dataset}, un dossier nommé dataset à l'intérieur du dossier de l'exécution, selon la disposition de jeu de données personnalisé d'OpenPCDet :

  • points/<id>.npy contient un scan sous forme de lignes float32 x y z intensity, avec une intensité de 0 lorsque le fichier n'en a pas.
  • labels/<id>.txt contient une ligne par cuboïde pour les scans train et val, sous la forme x y z dx dy dz heading label. Le cap est la rotation autour de +z en radians, et le tangage et le roulis ne sont pas conservés. Une espace dans un nom d'étiquette devient un trait de soulignement.
  • ImageSets/train.txt, val.txt et predict.txt listent les identifiants de scans. predict.txt liste les scans val et test, plus les scans non étiquetés lorsque writeback.include_unlabeled est activé.

Chaque <id> correspond aux 16 premiers caractères hexadécimaux du SHA-1 de l'identifiant de l'item, car les identifiants d'items peuvent contenir des caractères qu'un nom de fichier ne peut pas contenir. Les étiquettes des scans test ne sont jamais écrites dans le dossier. Potato écrit un scan géoréférencé relativement à l'origine du scan, car les coordonnées géoréférencées sont trop grandes pour du float32, et décale les boîtes prédites du même décalage.

Fichiers de prédiction relus depuis le détecteur

La commande de prédiction écrit un fichier par scan dans {predictions}/<id>.txt, une boîte par ligne, sous la forme x y z dx dy dz heading label score. Le score est facultatif. Potato écarte une ligne qui ne s'analyse pas, qui contient nan ou inf, qui nomme une étiquette que le schéma n'a pas ou qui décrit une boîte sans taille, et le journal de l'exécution compte les lignes écartées. Les boîtes restantes reviennent sous forme d'objets cuboid_3d marqués source: model, et le score de chaque boîte devient la confiance de cette boîte. Potato fixe la confiance d'un scan au score de boîte le plus bas du scan, et min_confidence s'applique à cette valeur.

Commandes d'entraînement et de prédiction

Les deux commandes se définissent comme des paramètres du module. Potato découpe chaque commande en mots comme le ferait un shell, remplace {dataset}, {workdir} (le dossier de l'exécution) et {predictions} par les chemins correspondants, et lance la commande dans le dossier de l'exécution sans shell. Les autres accolades d'une commande restent telles qu'écrites. Les tubes, && et les $VARIABLES parviennent au programme comme des mots littéraux, de sorte qu'un travail en plusieurs étapes relève d'un script. Ajoutez ce bloc à la configuration de l'étude sur les nuages de points :

yaml
model_training:
  enabled: true
  backend: gpu-server
  backends:
    gpu-server:
      type: ssh
      host: gpu.example.edu
      python: /opt/conda/envs/pcdet/bin/python
      gpus: auto
  trainer_params:
    external-3d:
      train_command: "/opt/pcdet-jobs/train.sh {dataset} {workdir}"
      predict_command: "/opt/conda/envs/pcdet/bin/python /opt/pcdet-jobs/predict.py {dataset} {workdir} {predictions}"
      timeout_s: 43200
  writeback:
    schemas: [objects]
    min_confidence: 0.3
    include_unlabeled: true

Les chemins sont ceux de l'hôte d'entraînement, où se trouve aussi le dossier de l'exécution. timeout_s plafonne chaque commande, et 12 heures remplacent ici la valeur par défaut de 24. Une commande d'entraînement qui se termine avec un code non nul fait échouer l'exécution, et l'erreur de l'exécution montre les dernières lignes de la sortie de la commande.

train.sh est votre propre script. Pour OpenPCDet, le script pointe la configuration de jeu de données personnalisé d'OpenPCDet vers {dataset}, construit les fichiers d'information avec python -m pcdet.datasets.custom.custom_dataset create_custom_infos <dataset config>, lance le script d'entraînement d'OpenPCDet et enregistre le point de contrôle sous {workdir}. Les noms de classes de la configuration du détecteur doivent correspondre aux noms d'étiquettes tels qu'écrits dans labels/. Le guide du jeu de données personnalisé de MMDetection3D ne lit les nuages de points que sous forme de fichiers .bin, de sorte que pour MMDetection3D le script écrit d'abord chaque points/<id>.npy en fichier .bin. Laissez train_command vide pour sauter l'entraînement, et predict_command exécute alors un modèle que vous avez entraîné ailleurs.

predict.py lit les identifiants de predict.txt, exécute le détecteur sur chaque scan et écrit les boîtes. Les fonctions load_model et detect sont votre propre code autour du détecteur que vous avez entraîné :

python
"""predict.py DATASET WORKDIR PREDICTIONS: one line per box, one file per scan."""
import os
import sys
 
import numpy as np
 
from my_detector import load_model, detect  # your code around OpenPCDet or MMDetection3D
 
dataset, workdir, out = sys.argv[1:4]
model = load_model(os.path.join(workdir, "output"))  # where train.sh saved the checkpoint
os.makedirs(out, exist_ok=True)
 
with open(os.path.join(dataset, "ImageSets", "predict.txt")) as fh:
    scan_ids = fh.read().split()
 
for scan_id in scan_ids:
    points = np.load(os.path.join(dataset, "points", scan_id + ".npy"))  # float32, (N, 4)
    with open(os.path.join(out, scan_id + ".txt"), "w") as fh:
        for x, y, z, dx, dy, dz, heading, label, score in detect(model, points):
            fh.write(f"{x} {y} {z} {dx} {dy} {dz} {heading} {label} {score}\n")

Sans train_command, la commande de prédiction constitue toute l'exécution, et une commande de prédiction qui se termine avec un code non nul fait échouer l'exécution. Une commande de prédiction qui se termine avec le code 0 mais n'écrit aucun <id>.txt pour aucun scan, ou n'écrit que des lignes que Potato écarte, compte aussi comme un échec, car une exécution notée sur cette sortie se lirait comme un modèle qui n'a rien trouvé. Un fichier vide est un scan sans objets. Après une commande d'entraînement, une commande de prédiction qui échoue ne fait pas échouer l'exécution. L'exécution se termine avec l'avertissement « Prediction pass failed after 0 prediction(s) » et les dernières lignes de la commande, et elle n'a ni prédictions ni score sur items réservés. Consultez le journal de l'exécution pour cet avertissement lorsqu'une exécution terminée affiche « not scored ».

Score sur items réservés pour les cuboïdes

Potato calcule la précision moyenne 3D par étiquette à une IoU volumétrique de 0,5 (ap_3d_50_<label>). L'IoU est exacte pour des cuboïdes orientés à toute rotation. Potato rapporte aussi la moyenne sur les étiquettes à des IoU de 0,25 et 0,5, ainsi que la précision, le rappel et le F1 à 0,5. Le score principal est map_3d_50. Avec une train_command, le tableau Runs prend d'abord le score principal sur test, car la commande d'entraînement reçoit les étiquettes val dans {dataset} et peut choisir un point de contrôle d'après ces étiquettes. Sans train_command, le modèle n'a jamais vu les items val de l'étude, et le score principal vient de val.

Pré-étiquettes de cuboïdes dans le visualiseur de nuages de points

Les cuboïdes prédits apparaissent dans le visualiseur de nuages de points sous forme de boîtes modifiables, et une boîte modifiée est enregistrée comme réponse de l'annotateur. Potato vérifie chaque objet prédit avant que le visualiseur ne le dessine, et écarte un objet dont la géométrie ne s'analyse pas, un cuboïde sans taille et une étiquette que le schéma n'a pas. Comment annoter des nuages de points 3D traite de l'annotation et de l'accord sur des boîtes 3D.

Suivre des objets dans une vidéo avec video-track

video-track apprend des pistes que les annotateurs tracent dans un schéma video_annotation en mode tracking ou combined, et prédit des pistes sur les vidéos que personne n'a suivies. Une piste est une étiquette et une boîte à chaque image clé, et le widget interpole la boîte entre les images clés. Un schéma video_annotation dans un autre mode stocke des segments plutôt que des pistes, et video-track n'est pas proposé pour ce schéma.

yaml
annotation_task_name: "Pedestrian tracking"
task_dir: .
media_directory: media
data_files:
  - data/clips.jsonl
item_properties:
  id_key: id
  text_key: video
output_annotation_dir: annotation_output
user_config:
  allow_all_users: true
 
annotation_schemes:
  - annotation_type: video_annotation
    name: tracks
    description: "Track every pedestrian and cyclist with a box at each keyframe."
    source_field: video
    mode: tracking
    video_fps: 25
    labels:
      - {name: pedestrian, color: "#FF6B6B"}
      - {name: cyclist, color: "#45B7D1"}
    tracking_options:
      interpolation: linear
      auto_advance_frames: 5
    frame_stepping: true
 
model_training:
  enabled: true
  trainer_params:
    video-track:
      frame_fps: 2
      predict_fps: 5
      max_age: 4
  writeback:
    schemas: [tracks]
    include_unlabeled: true

Chaque ligne de données nomme un clip sous media_directory :

json
{"id": "clip_001", "video": "/media/clip_001.mp4"}
{"id": "clip_002", "video": "/media/clip_002.mp4"}

Réglez video_fps sur la cadence réelle des clips. Les images clés sont numérotées par image, et le module numérote les images clés prédites à la valeur video_fps du schéma (30 si elle n'est pas définie). Le module lit la boîte d'une image échantillonnée selon le réglage interpolation propre à chaque piste (linéaire, Catmull-Rom cubique ou constante). Les boîtes dont le modèle apprend suivent donc le tracé que les annotateurs ont vu. Comme dans le widget, une piste à une seule image clé est maintenue sur toutes les images, et une piste à laquelle le widget a donné un startFrame et un endFrame s'étend entre ces deux images. Une image clé située hors de ces deux images s'affiche quand même sur sa propre image. predict_fps: 5 détecte sur cinq images par seconde au lieu de deux. Cinq images par seconde donnent plus d'images clés à un objet rapide et coûtent plus de passes du détecteur. Une piste se termine après plus de max_age images échantillonnées sans détection correspondante, de sorte que max_age: 4 à 5 images par seconde laisse une piste survivre à 0,8 seconde de détections manquées. Un piéton qui passe derrière un poteau garde une seule piste lorsque le détecteur le retrouve dans les 0,8 seconde. La configuration s'entraîne sur la machine qui exécute Potato (le backend local), et le backend gpu-server de la configuration pour les volumes fonctionne aussi pour video-track.

Entraînement et liaison des pistes dans une exécution video-track

Une exécution échantillonne chaque vidéo annotée à frame_fps images par seconde, du début à la dernière image clé, et lit chaque image échantillonnée avec OpenCV. Les pixels du canevas dépendent de l'écran de l'annotateur, de sorte que le module lit la boîte de chaque piste sur cette image comme une fraction de l'image. Une image échantillonnée sans piste reste dans l'entraînement comme exemple de scène vide. L'exécution affine un Faster R-CNN de torchvision sur ces images, avec les paramètres et les valeurs par défaut de torchvision-detect. Le détecteur part des poids COCO de torchvision. Après chaque époque, l'exécution évalue le mAP des boîtes à IoU 0,5 sur les images val (frame_map_50) et garde la meilleure époque.

La prédiction échantillonne chaque vidéo à predict_fps, détecte sur chaque image échantillonnée et relie les détections en pistes. Les détections de chaque image sont appariées une à une avec les pistes vivantes de la même étiquette, d'après le recouvrement avec la dernière boîte de chaque piste, et un appariement demande une IoU d'au moins match_iou (0,3). Une détection non appariée dont le score atteint au moins new_track_score (0,5) démarre une nouvelle piste. Le module écarte avant l'écriture et le calcul du score une piste prédite qui compte moins de min_track_keyframes détections (2 par défaut). Le widget maintiendrait sinon sur toutes les images une piste d'une seule détection. Chaque détection devient une image clé, de sorte que le widget interpole entre les images clés du modèle comme il le fait entre celles d'un annotateur.

La partition train exige au moins deux vidéos annotées (min_instances). Une vidéo qu'OpenCV ne sait pas décoder est exclue avec un avertissement qui nomme l'item, et l'exécution continue avec le reste.

Score sur items réservés pour les pistes

Potato compare les pistes prédites aux pistes annotées sur des images échantillonnées à 2 par seconde dans chaque vidéo réservée, plus chaque image clé annotée. Les scores sont MOTA, IDF1, les changements d'identité, les omissions et les faux positifs, une boîte étant comptée comme retrouvée à IoU 0,5. MOTA impute les omissions, les faux positifs et les changements d'identité au nombre de boîtes annotées. IDF1 est le score F1 des boîtes appariées à la bonne identité sur toute la vidéo, et IDF1 est le score principal (idf1). Le module choisit la meilleure époque sur val, de sorte que le tableau Runs prend le score principal sur test.

Pré-étiquettes de pistes dans le widget vidéo

Les pistes prédites remplissent le widget vidéo sous forme de pistes dont les images clés sont marquées comme sortie d'IA et portent le score du détecteur. Le widget convertit les boîtes en pixels de son propre canevas lorsqu'il dessine les pistes, et l'annotateur corrige les pistes comme n'importe quelle autre piste. La confiance d'une vidéo est le score moyen des images clés du modèle. En mode combined, une pré-étiquette ne contient que des pistes, et l'annotateur ajoute les segments. Comment annoter le suivi d'objets dans une vidéo traite de l'annotation, de l'occlusion et de l'accord temporel.

Lancer une exécution et vérifier le résultat

Redémarrez le serveur après avoir ajouté model_training, ouvrez /admin/training, choisissez le schéma et le module d'entraînement, puis cliquez sur Train now. La page ne propose que les modules adaptés à chaque schéma, et affiche un module non installé avec la mention « not installed » et la commande d'installation. Pour vérifier un serveur GPU avant la première exécution, lancez :

bash
potato training check gpu-server config.yaml

La vérification rapporte la version de Python distante, chaque GPU avec sa mémoire libre, le GPU qu'une exécution obtiendrait et les paquets d'entraînement qui manquent à l'interpréteur distant. Une exécution peut aussi démarrer depuis l'API, avec des paramètres par exécution qui remplacent trainer_params :

bash
curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
  http://localhost:8000/admin/training/api/runs \
  -d '{"trainer": "video-track", "schemas": ["tracks"], "params": {"predict_fps": 10}}'

Lorsque l'exécution se termine, le tableau Runs liste le score sur items réservés, avec la partition et la métrique dans une infobulle. Les pré-étiquettes ancrent les annotateurs, de sorte qu'il faut activer include_unlabeled dans une nouvelle étude une fois que le score sur items réservés justifie de montrer les réponses du modèle. Le tutoriel du modèle dans la boucle traite de la présentation des pré-étiquettes les moins sûres en premier et du réentraînement sur les corrections, et Configuration et routes de l'entraînement de modèles liste chaque clé, les backends et les vérifications de l'écriture en retour.

Pour aller plus loin

Références

  • Bernardin, K., and Stiefelhagen, R. (2008). Evaluating Multiple Object Tracking Performance: The CLEAR MOT Metrics. EURASIP Journal on Image and Video Processing. https://doi.org/10.1155/2008/246309
  • Çiçek, Ö., Abdulkadir, A., Lienkamp, S. S., Brox, T., and Ronneberger, O. (2016). 3D U-Net: Learning Dense Volumetric Segmentation from Sparse Annotation. MICCAI 2016. https://arxiv.org/abs/1606.06650
  • Ristani, E., Solera, F., Zou, R. S., Cucchiara, R., and Tomasi, C. (2016). Performance Measures and a Data Set for Multi-Target, Multi-Camera Tracking. ECCV 2016 Workshop on Benchmarking Multi-Target Tracking. https://arxiv.org/abs/1609.01775