コンテンツへスキップ

ボリューム、点群、動画でモデルを学習する

塗りつぶしたCTマスクで3D U-Netを学習し、ライダーのキュボイドに対して自前のOpenPCDet検出器を実行し、動画トラッカーを学習します。いずれもホールドアウトした項目でスコアを出します。

v2.14.0の新機能

このチュートリアルでは、Potatoの研究が持つ独自のアノテーションから、3Dデータと動くデータを対象にした3つのモデルを学習します。volume-unetはCTとMRのボリュームをセグメンテーションする3D U-Netを学習し、external-3dはご自身で構築して実行するライダー検出器を動かし、video-trackは動画の物体トラッカーを学習します。このページは、すでにPotatoでボリューム、点群、動画にアノテーションを付けていて、残りの項目をモデルに事前ラベル付けさせたいチーム向けです。各トレーナーについて、学習データを作るスキーム、potato validateを通る設定、実行が行うこと、ホールドアウトスコア、予測がアノテーターに届く仕組みを説明します。最初のモデルのチュートリアルでは、GPUが不要なテキスト分類器でTrainingページを一通り説明しており、このチュートリアルはその内容を前提としています。

トレーナーの概要

3つのトレーナーはいずれも、実行ごとに1つのスキームを学習し、実行のホールドアウトしたvalとtestの項目でモデルをスコアリングし、アノテーターが編集する事前ラベルとして予測を書き戻します。volume-unetとexternal-3dの背後にある検出器は、実際にはGPUが必要になります。そのため、この2つのトレーナーの設定では、リモート学習のチュートリアルで設定するとおり、実行をssh経由でGPUサーバーに送ります。

トレーナースキーム項目学習マシンへのインストール代表のスコア
volume-unetvolume_annotation(塗りつぶしマスク)NIfTIボリューム、DICOMシリーズpip install "potato-annotation[train-vision,volumes]"、実際にはGPUが必要mean_dice
external-3dspatial_annotation(キュボイド)点群ご自身の検出器(OpenPCDet、MMDetection3D)map_3d_50
video-tracktrackingまたはcombinedモードのvideo_annotation動画pip install "potato-annotation[train-vision]" opencv-pythonidf1

どのトレーナーも、ディスク上のファイルからメディアを読み込みます。メディアのパスは、タスクディレクトリまたはmedia_directoryの内側を指している必要があり、メディアがhttp(s)のURLである項目は、学習と予測から除外されます。

volume-unetでCTとMRのボリュームをセグメンテーションする

volume-unetは、アノテーターがvolume_annotationスキームで塗りつぶしたマスクを学習し、まだ誰もセグメンテーションしていないボリュームにマスクを塗ります。このスキームは、NIfTIファイルまたはDICOMシリーズを、軸位断、冠状断、矢状断の3つの連動したビューで表示し、アノテーターはブラシで、またはポリゴンを閉じて、各ラベルを塗ります。source_fieldが指す項目のフィールドには、media_directoryからの相対パスで、.niiまたは.nii.gzのファイル、あるいは1つのDICOMシリーズを含むディレクトリを指定します。スライド上に描いたボックスにはマスクがないため、ボックスだけの答えは学習の正解になりません。Potatoは研究用ソフトウェアであり、臨床上または規制上の承認を受けていません。ここで学習したモデルが提案するのは、アノテーターが修正するためのマスクです。

次の設定は、脾臓と肝臓をセグメンテーションし、GPUサーバーで学習し、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

データの各行が、1つのボリュームを指します。

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

patchの値は、ネットワークが学習する立方体パッチの一辺の大きさを決め、既定の3レベルでは4の倍数(2のlevelsマイナス1乗)でなければなりません。96ボクセルのパッチは、既定の64よりも各ボクセルの周囲の文脈を多くネットワークに与え、その分GPUメモリを多く必要とします。min_free_gpu_mb: 8000を指定すると、Potatoは空きメモリが8,000 MB以上あるGPUでのみ実行を開始します。Potatoは、塗りつぶした各ラベルのマスク内でのモデルの平均確率を求め、それらの平均のうち最も低い値を、そのボリュームの信頼度として報告します。min_confidence: 0.6を指定すると、いずれかの臓器の平均確率が0.6を下回る確率で塗られたボリュームには、事前ラベルが付きません。

volume-unetの実行における学習の手順

実行は、アノテーション済みの各ボリュームを、ウィジェットが塗るときのボクセルの順序で読み込み、マスクをボクセルごとに1つのラベルへ変換します。2つのラベルが重なる場所では、NIfTIのエクスポートと同じく、バンドルのラベル順で後ろにあるラベルが優先されます。強度は、各ボリュームの0.5パーセンタイルと99.5パーセンタイルでクリップされ、標準化されます。ネットワークは、素のtorchで実装した3D U-Net(3レベル、インスタンス正規化)で、1エポックあたり100個のランダムなパッチに対して、クロスエントロピーとソフトDiceで学習します。パッチの半分は塗りつぶしたボクセルを中心にしているため、小さな構造も学習に登場します。各エポックの後に、実行はvalのボリュームで平均Diceをスコアリングして最良のエポックを保持し、10エポック連続でスコアが改善しなければ停止します。予測では、パッチの半分ずつ重ねながらボリューム全体にパッチをスライドさせ、確率を平均します。

train分割には、アノテーション済みのボリュームが3つ以上必要です(min_instances)。塗りつぶした形状がファイルの形状と異なるボリュームは、実行ログに警告を出して除外され、ファイルが見つからないボリュームも同様です。1つのボリュームに複数の読影者がいる場合、集約関数は答え全体を比較するため、実行はaggregationが統合した1人の読影者のマスクで学習します。読影者間の一致度と、Potatoの外で作るコンセンサスマスクについては、Segment CT Volumes With Several Readers and STAPLEで説明しています。

ボリュームのホールドアウトスコア

Potatoは、ホールドアウトした各ボリュームの各ラベルについてDice係数を計算し(dice_<label>)、すべてのボリュームとラベルにわたる平均をmean_diceとして報告します。そのラベルを持つボリュームで、モデルがそのラベルを出力しなかった場合は、0点になります。トレーナーはvalで最良のエポックを選ぶため、実行テーブルは代表の値をtestから取ります(test_mean_dice)。ラベルごとのスコアの完全な一覧は、実行のページにあります。

スライドビューでのマスクの事前ラベル

予測されたマスクは、ウィジェット自身の形式、つまりラベルごと・軸位断スライスごとの形で、prefilled_from: modelが付いて返され、アノテーターがボリュームを開くとスライドビューに表示されます。アノテーターはブラシと消しゴムでマスクを修正し、修正したマスクはそのアノテーターの答えとして保存されます。実行のtrain分割の項目に、事前ラベルは付きません。include_unlabeledがない場合、実行が予測するのは、実行のvalとtestのボリュームだけです。

external-3dで3Dキュボイドを検出する

external-3dは、アノテーション済みのスキャンを、学習ホストにインストールした3D検出器に渡し、ご自身の学習コマンドと予測コマンドを実行し、予測されたボックスをキュボイドとして読み戻します。Potatoは点群の検出器を同梱していません。このトレーナーが対象とする2つの検出器、OpenPCDetとMMDetection3Dは、GPUホスト上でコンパイルしたCUDAオペレーターを必要とします。Potato側でこのトレーナーが必要とするのは、numpyだけです。

学習データは、cuboid_3dツールを使うspatial_annotationスキームから得られます。アノテーターは、PCD、PLY、LAS、またはKITTIの.binスキャンに、向きのあるボックスを配置します。ボックスは、スキャン自身の座標系でメートル単位で保存されます(3D Point Clouds and Cuboidsを参照)。学習の正解になるのはキュボイドだけなので、同じスキームの点、ポリライン、点のセグメントは無視されます。各項目は1つのスキャンであり、トラックIDを持つフレーム列は学習の正解になりません。

検出器のために書き出されるデータセットフォルダー

学習コマンドを実行する前に、Potatoはスキャンを{dataset}に書き出します。これは実行のフォルダー内にあるdatasetという名前のフォルダーで、形式はOpenPCDetのカスタムデータセットのレイアウトです。

  • **points/<id>.npy**は、1つのスキャンをx y z intensityのfloat32の行として保持します。ファイルに強度がない場合、強度は0です。
  • **labels/<id>.txt**は、trainとvalのスキャンについて、キュボイドごとに1行をx y z dx dy dz heading labelの形式で保持します。headingは+z軸まわりの回転をラジアンで表したもので、ピッチとロールは保持されません。ラベル名の中の空白は、アンダースコアになります。
  • **ImageSets/train.txt、val.txt、predict.txt**は、スキャンのIDを列挙します。predict.txtには、valとtestのスキャンが挙がり、writeback.include_unlabeledがオンのときは、ラベルのないスキャンも挙がります。

各<id>は、項目IDのSHA-1の先頭16桁の16進文字です。項目IDには、ファイル名に使えない文字が含まれることがあるためです。testのスキャンのラベルは、フォルダーに書き出されません。測地座標を持つスキャンは、座標の値がfloat32には大きすぎるため、Potatoがスキャンの原点からの相対座標で書き出し、予測されたボックスを同じオフセットだけ元に戻します。

検出器から読み戻される予測ファイル

予測コマンドは、スキャンごとに1つのファイルを{predictions}/<id>.txtに書き出します。1行に1つのボックスを、x y z dx dy dz heading label scoreの形式で書きます。scoreは省略できます。Potatoは、解析できない行、nanまたはinfを含む行、スキームにないラベルを指す行、大きさのないボックスを表す行を除外し、実行ログには除外した行数が記録されます。残ったボックスは、source: modelが付いたcuboid_3dオブジェクトとして返され、各ボックスのスコアがそのボックスの信頼度になります。Potatoは、スキャンの信頼度を、そのスキャンで最も低いボックスのスコアに設定し、min_confidenceはその値に適用されます。

学習コマンドと予測コマンド

2つのコマンドは、どちらもトレーナーのパラメーターとして設定します。Potatoは、各コマンドをシェルと同じ規則で単語に分割し、{dataset}、{workdir}(実行のフォルダー)、{predictions}を対応するパスに置き換え、シェルを介さずに実行のフォルダーでコマンドを実行します。コマンド内のそれ以外の波括弧は、書かれたまま残ります。パイプ、&&、$VARIABLESは、文字どおりの単語としてプログラムに渡されるため、複数の手順にわたる処理はスクリプトにまとめます。次のブロックを、点群の研究の設定に追加します。

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

パスは学習ホスト上のものであり、実行のフォルダーもそこにあります。timeout_sは各コマンドの時間の上限を決め、ここでの12時間は、既定の24時間に代わるものです。学習コマンドが0以外の終了コードで終わると、実行は失敗し、実行のエラーには、コマンドの出力の最後の数行が表示されます。

train.shは、ご自身のスクリプトです。OpenPCDetの場合、このスクリプトは、OpenPCDetのカスタムデータセットの設定を{dataset}に向け、python -m pcdet.datasets.custom.custom_dataset create_custom_infos <dataset config>でinfoファイルを作成し、OpenPCDetの学習スクリプトを実行し、チェックポイントを{workdir}の下に保存します。検出器の設定にあるクラス名は、labels/に書かれたラベル名と一致していなければなりません。MMDetection3Dのカスタムデータセットのガイドは、点群を.binファイルとしてしか読み込まないため、MMDetection3Dでは、スクリプトが最初に各points/<id>.npyを.binファイルとして書き出します。train_commandを空にすると学習をスキップし、predict_commandは、別の場所で学習したモデルを実行します。

predict.pyは、predict.txtのIDを読み込み、各スキャンに検出器を実行して、ボックスを書き出します。load_model関数とdetect関数は、学習した検出器を包むご自身のコードです。

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")

train_commandがない場合は、予測コマンドが実行のすべてであり、0以外の終了コードで終わる予測コマンドは、実行を失敗させます。終了コード0で終わっても、どのスキャンにも<id>.txtを書かない予測コマンド、またはPotatoが除外する行だけを書く予測コマンドも、失敗として扱われます。その出力でスコアを付けた実行は、モデルが何も見つけなかったように読めてしまうからです。空のファイルは、物体のないスキャンを表します。学習コマンドの後で予測コマンドが失敗しても、実行は失敗にはなりません。実行は、「Prediction pass failed after 0 prediction(s)」という警告とコマンドの最後の数行を付けて終了し、その実行には予測もホールドアウトスコアもありません。完了した実行が「not scored」と表示されるときは、この警告が実行ログにないか確認してください。

キュボイドのホールドアウトスコア

Potatoは、体積IoU 0.5でのラベルごとの3D平均適合率をスコアリングします(ap_3d_50_<label>)。このIoUは、どの回転角の向きのあるキュボイドに対しても厳密です。Potatoは、IoU 0.25と0.5でのラベル平均、および0.5での適合率、再現率、F1も報告します。代表の値はmap_3d_50です。train_commandがある場合、学習コマンドには{dataset}内のvalのラベルが渡され、そのラベルでチェックポイントが選ばれる可能性があるため、実行テーブルは代表の値をまずtestから取ります。train_commandがない場合、モデルは研究のvalの項目を一度も見ていないため、代表の値はvalから取られます。

点群ビューアーでのキュボイドの事前ラベル

予測されたキュボイドは、点群ビューアーに編集可能なボックスとして表示され、編集されたボックスは、アノテーターの答えとして保存されます。Potatoは、ビューアーが描画する前に、予測された各オブジェクトを検査し、ジオメトリを解析できないオブジェクト、大きさのないキュボイド、スキームにないラベルを除外します。アノテーション側の作業と3Dボックスの一致度については、How to Annotate 3D Point Cloudsで説明しています。

video-trackで動画内の物体を追跡する

video-trackは、アノテーターがtrackingまたはcombinedモードのvideo_annotationスキームで描いたトラックを学習し、まだ誰もトラッキングしていない動画のトラックを予測します。トラックとは、ラベルと、各キーフレームでのボックスのことで、ウィジェットはキーフレームの間のボックスを補間します。別のモードのvideo_annotationスキームは、トラックではなくセグメントを保存するため、そのスキームにはvideo-trackが提示されません。

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

データの各行が、media_directoryの下のクリップを指します。

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

video_fpsには、クリップの実際のフレームレートを設定します。キーフレームはフレーム単位で番号が付けられ、トレーナーは予測したキーフレームに、スキームのvideo_fps(未設定の場合は30)で番号を付けます。トレーナーは、サンプリングしたフレーム上のボックスを、各トラック自身のinterpolationの設定(線形、3次のCatmull-Rom、または一定)で読み取ります。そのため、モデルが学習するボックスは、アノテーターが見た軌跡に沿ったものになります。ウィジェットと同様に、キーフレームが1つのトラックはすべてのフレームで保持され、ウィジェットがstartFrameとendFrameを与えたトラックは、その2つのフレームの間で続きます。この2つのフレームの外にあるキーフレームも、そのキーフレーム自身のフレームには表示されます。predict_fps: 5は、毎秒2フレームではなく毎秒5フレームで検出します。毎秒5フレームにすると、動きの速い物体のキーフレームが増え、検出器の処理回数も増えます。トラックは、max_age個を超えるサンプリングフレームのあいだ対応する検出がないと終了するため、毎秒5フレームでmax_age: 4なら、トラックは0.8秒間の検出漏れを耐えられます。歩行者が柱の裏を通っても、0.8秒以内に検出器が歩行者を再び見つければ、トラックは1つのまま保たれます。この設定は、Potatoを動かしているマシン(localバックエンド)で学習します。ボリュームの設定にあるgpu-serverバックエンドも、video-trackで使えます。

video-trackの実行における学習とトラックの連結

実行は、アノテーション済みの各動画を、先頭から最後のキーフレームまで毎秒frame_fpsフレームでサンプリングし、サンプリングした各フレームをOpenCVで読み込みます。キャンバスのピクセル数はアノテーターの画面に依存するため、トレーナーは、そのフレーム上の各トラックのボックスを、画面に対する割合として読み取ります。トラックのないサンプリングフレームは、空のシーンの例として学習に残ります。実行は、これらのフレームでtorchvisionのFaster R-CNNをファインチューニングし、パラメーターと既定値はtorchvision-detectと同じです。検出器は、torchvisionのCOCOの重みから始まります。各エポックの後に、実行はvalのフレームでIoU 0.5でのボックスmAPをスコアリングし(frame_map_50)、最良のエポックを保持します。

予測では、各動画をpredict_fpsでサンプリングし、サンプリングしたすべてのフレームで検出し、検出をトラックに連結します。各フレームの検出は、同じラベルの生きているトラックと、各トラックの最後のボックスとの重なりによって1対1で対応づけられ、対応づけにはmatch_iou(0.3)以上のIoUが必要です。対応づけられなかった検出のうち、スコアがnew_track_score(0.5)以上のものが、新しいトラックを開始します。トレーナーは、検出がmin_track_keyframes個(既定は2)未満の予測トラックを、書き戻しとスコア計算の前に削除します。そうしないと、ウィジェットは検出が1つだけのトラックをすべてのフレームで保持してしまいます。すべての検出がキーフレームになるため、ウィジェットは、アノテーターのキーフレームの間を補間するのと同じように、モデルのキーフレームの間を補間します。

train分割には、アノテーション済みの動画が2本以上必要です(min_instances)。OpenCVがデコードできない動画は、項目名を示す警告を出して除外され、実行は残りの動画で続けられます。

トラックのホールドアウトスコア

Potatoは、予測したトラックを、ホールドアウトした各動画全体で毎秒2フレームでサンプリングしたフレームと、アノテーション済みのすべてのキーフレームで、アノテーションのトラックと比較します。スコアは、MOTA、IDF1、IDスイッチ、見逃し、誤検出で、ボックスはIoU 0.5で検出されたものと数えます。MOTAは、見逃し、誤検出、ID切り替えを、アノテーション済みのボックスの数に対する割合で評価します。IDF1は、動画全体で正しいIDに対応づけられたボックスのF1スコアで、IDF1が代表の値です(idf1)。トレーナーはvalで最良のエポックを選ぶため、実行テーブルは代表の値をtestから取ります。

動画ウィジェットでのトラックの事前ラベル

予測されたトラックは、AIの出力として印が付き、検出器のスコアを持つキーフレームとともに、動画ウィジェットにトラックとして表示されます。ウィジェットは、トラックを描画するときにボックスをウィジェット自身のキャンバスのピクセルに変換し、アノテーターは、ほかのトラックと同じようにトラックを修正します。動画の信頼度は、モデルのキーフレームのスコアの平均です。combinedモードでは、事前ラベルにはトラックだけが含まれ、セグメントはアノテーターが追加します。アノテーション側の作業、オクルージョン、時間方向の一致度については、How to Annotate Video Object Trackingで説明しています。

実行を開始して結果を確認する

model_trainingを追加したらサーバーを再起動し、/admin/trainingを開いて、スキームとトレーナーを選び、Train nowをクリックします。このページには、各スキームに合うトレーナーだけが表示され、インストールされていないトレーナーは、インストールコマンドとともに「not installed」と表示されます。最初の実行の前にGPUサーバーを確認するには、次を実行します。

bash
potato training check gpu-server config.yaml

この確認は、リモートのPythonのバージョン、各GPUとその空きメモリ、実行に割り当てられるGPU、リモートのインタープリターに足りない学習用パッケージを報告します。実行は、trainer_paramsを上書きする実行ごとのパラメーターを付けて、APIから開始することもできます。

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}}'

実行が終了すると、実行テーブルにホールドアウトスコアが表示され、分割と指標はツールチップに示されます。事前ラベルはアノテーターの判断を引きずるため、新しい研究では、ホールドアウトスコアがモデルの答えを見せるのを裏づけるようになってから、include_unlabeledをオンにしてください。信頼度の最も低い事前ラベルを先に出して修正で再学習する方法はモデルインザループのチュートリアルで、すべてのキー、バックエンド、書き戻しの検査はモデル学習の設定とルートで説明しています。

関連資料

参考文献

  • 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