CVAT で DICOM をアノテーションできるようになるまで

CVAT で DICOM をアノテーションできるようになるまで

TL;DR

– 医療 AI 開発のアノテーションツールとして CVAT を使用しました
– もともとの状態では DICOM が読み込めなかったので CVAT の改造を行いました
cvat/apps/engine/media_extractors.py に DICOM を読み込むクラスを追加しました
– さらに、デフォルトのラベル設定など実際のプロジェクトで便利な仕様も追加しました

1. CVAT について

こんにちは、HACARUS インターンの住江です。機械学習アルゴリズムを開発するためには、与えられたデータを最終的にどのように出力するかの「正解」となるものが必要です。機械学習の分野ではこれを「アノテーション」と呼び、開発の目標となるアノテーションデータは人間が作成することが多いです。例えば、病変を検知するモジュールを開発するために必要なのは、患者さんの CT/MRI 画像のどの位置が病変となっているかなどの情報です。その正解データを作成するために様々なアノテーションツールが公開されています。
その中の一つに、OpenCV のプロジェクトの中で作られた CVAT (Computer Vision Annotation Tool)というものがあります。CVAT は無料で使用できるオープンソースのアノテーションツールで、ブラウザで動作します。今回は医療画像を扱うプロジェクトでなぜ CVAT が採用されたかや、実際に使用できるようになるまでに改良した部分について書いていきます。

 

2. CVAT が採用された理由について

HACARUS が携わる医療画像検知 AI のアノテーションにはいくつか課題があります。
1つ目がデータ自体の形式の特殊性です。
通常、医療分野で扱われる CT や MRI の画像は DICOM(Digital Imaging and COmmunications in Medicine) という特殊なフォーマットとなっています。DICOM はコンテナフォーマットで画像だけではなく、いつ撮影されたかや、撮影された環境の情報などが含まれています。そのため、JPEG や PNG などの通常の画像を読み込むライブラリでは処理できません。実際、Python で DICOM を処理する際には pydicom などの専用のライブラリをインストールして使用します。
2つ目はデータの秘匿する必要があるということです。
我々が扱うデータは医療機関において匿名加工という処理が適切に行われ、個人を特定することはできないように加工されています。しかしだからと言って、公共のクラウドにデータをアップロードして処理するといったことは好ましくなく、適切にデータを管理することが求められます。そのような適切な管理を行うために、自社でサーバーを立ち上げれるようなツールが求められました。
3つ目が PC にアノテーションツールをインストールするハードルです。
病変のアノテーションには専門的知識が必要であり、お医者さんに実際におこなってもらう必要があります。その際、逐一医療機関で使用している PC にツールをインストールしてもらうのは環境や手間の問題が生じます。その点を考慮してブラウザで動作するアノテーションツールが求められました。
CVAT は2つ目と3つ目の課題を解決できています。CVAT を自社のサーバーにデプロイし、そこに外部のPC のブラウザからアクセスし、アノテーションデータをアップロードしてもらえば、ツールをインストールする必要がなく、データも外に出さずに扱うことができます。
1つ目の課題に関しては公開されているソースコードそのままでは対応していないのですが、CVAT が OSS であり、また Django (Python の Web フレームワーク) で書かれていることもあり、少し手を加えることでこの課題を解決できるのではないか、となり CVAT がアノテーションツールとして採用されました。

 

3. DICOM をアノテーションできるようにするまで

CVAT の基本仕様
– Docker / docker-compose
– nginx
– Django, React, TypeScript
– Ubuntu, Windows 10, Mac OS Mojave で動作
– CVATフォーマット, PASCAL VOC, YOLO, MS COCO フォーマット等でアノテーション出力できる
 追加・修正項目
DICOM をアノテーションできるようにすることに加え、いくつか使う際の手間が減るような改造を行いました。
– DICOM を取り込めるようにする
– 複数の DICOM をまとめて読み込めるようにする
– DICOM の Series ごとに job を分ける
– デフォルトのアノテーションラベルを設定しておく
DICOM を取り込めるようにする
まず、pydicom を使用するので requirements に追加します。

 

```
# cvat/requirements/base.txt


+ pydicom
```

 

CVAT では PDF を JPEG に変換してアノテーションを行うということもでき、それが行われているクラスを参考に DICOM を JPEG に変換するクラスを追加しました。

 

```python
# cvat/apps/engine/media_extractors.py

class DicomListExtractor(MediaExtractor):
    def __init__(self, source_path, dest_path, image_quality, step=1, start=0, stop=0):
        if not source_path:
            raise Exception('No Dicom found')

        import pydicom
        super().__init__(
            source_path=sorted(source_path),
            dest_path=dest_path,
            image_quality=image_quality,
            step=1,
            start=0,
            stop=0,
        )

        self._dimensions = []
        series = dict()
        self._jpeg_source_paths = []

        for i, source in enumerate(self._source_path):
            dcm = pydicom.read_file(source)

            series_time = dcm.get("SeriesTime", "")
            if series_time not in series:
                series[series_time] = Series(i, dcm.get("SeriesDescription", ""))
            else:
                series[series_time].stop_frame = i     

            img = _normalize_image(dcm.pixel_array)
            pilImg = Image.fromarray(img)
            self._dimensions.append(pilImg.size)
            jpeg_source_path = os.path.splitext(source)[0] + '.jpg'
            pilImg.save(jpeg_source_path, 'JPEG')
            self._jpeg_source_paths.append(jpeg_source_path)
        
        # SeriesTimeで昇順に並べかえたSeriesのリストを取得
        self._series = [v for _, v in sorted(series.items())]

        ...

def _normalize_image(img, min_percent = 0, max_percent = 99, gamma = 1.2):
    vmin = np.percentile(img, min_percent)
    vmax = np.percentile(img, max_percent)
    img = ((img - vmin) / (vmax - vmin))
    img[img < 0] = 0
    img = pow(img, gamma) * 255
    img = np.clip(img, 0, 255)
    return img.astype(np.uint8)
```

 

pydicom.read_file() で DICOM ファイルを読み出し、_normalized_image() で画像の正規化を行っています。
このようにしてDICOM を JPEG に変換して、アノテーションの際は既存の形式と同じように扱えるようにしました。
複数の DICOM をまとめて読み込めるようにする
現在のままだと DICOM はフォルダごとにアップロードする必要があり、症例数が増えると大きな手間となります。 zip 圧縮したファイルをアップロードすることができれば便利になるのでその改造を行いました。CVAT にはもともと zip 圧縮したファイルをアップロードする機能があるので、それを使って解決を試みました。
cvat/apps/engine/media_extractors.py 内で zip ファイルを読み出しているのは ArchiveExtractorというクラスです。このクラスは DirectoryExtractor というクラスを継承しており、さらにそのクラスは、JPEG などの画像を処理する ImageListExtractor というクラスをもともと継承していました。
この継承関係の見直しを行い、DirectoryExtractor で読み出された source_pathsimg_paths(画像の path たち)と dicom_paths (DICOM の path たち) に分け、それぞれ ImageListExtractorDicomListExtractor で処理することで、画像だけでなくDICOM が含まれる zip ファイルも扱うことが出来るようにしました。
DICOM の Series ごとに job を分ける
CT や MRI で撮像されたデータには Series という一連のまとまりがあり、DICOM にはそのタグが含まれています。また、CVAT にはアノテーションを行う際、タスク分けを行う単位として job というものがあります。この Series と job を対応付けることでデータの整理が容易になり、どういったデータをアノテーションしているのかわかりやすくなります。この Series と job の対応付けをデータ読み込みの際、自動で行い、タスクが作成されたときには、job に分かれている状態を実現しました。

 

```python
# cvat/apps/engine/media_extractors.py

class DicomListExtractor(MediaExtractor):
    def __init__(self, source_path, dest_path, image_quality, step=1, start=0, stop=0):

                ...

                series = dict()

        for i, source in enumerate(self._source_path):
            dcm = pydicom.read_file(source)

            series_time = dcm.get("SeriesTime", "")
            if series_time not in series:
                series[series_time] = Series(i, dcm.get("SeriesDescription", ""))
            else:
                series[series_time].stop_frame = i     

        ...

        self._series = [v for k, v in sorted(series.items())]

class Series:
    def __init__(self, start_frame, description):
        self.start_frame = start_frame
        self.stop_frame = start_frame
        self.description = description
```

 

CVAT で内部の実装上、job は start_framestop_frameを含む segment_framesという変数で表現されていましたが、新たに Series というクラスを追加して管理するようにしました。
具体的には、pydicom の get() というメソッドを使用し series_time を取得して、その値を参考にして start_frame , stop_frame を決めるようにしました。
デフォルトのアノテーションラベルを設定しておく
今回のプロジェクトで扱ったデータに対するアノテーションのラベルは決められた数しかなく、また、数が多いので zip ファイルを上げるたびに設定するのは手間になります。そのため、アノテーションのタスクを作成するときにデフォルトのラベルを予め入れておくことにしました。

 

 

```tsx
// cvat-ui/src/components/create-task-page/create-task-content.tsx

import { DEFAULT_LABELS } from './default-label'

const defaultState = {
    basic: {
        name: '',
    },
    advanced: {
        zOrder: false,
        lfs: false,
    },
    labels: DEFAULT_LABELS,
    files: {
        local: [],
        share: [],
        remote: [],
    },
};
```

```tsx
// cvat-ui/src/components/create-task-page/default-label.tsx

export const DEFAULT_LABELS = [
  {
    "name": "cHCC",
    "attributes": []
  },
  {
    "name": "nHCC",
    "attributes": []
  },

    ...

];
```

 

この修正では CVAT 全体の UI を定義する cvat-ui フォルダの中の TypeScript を変更しました。 defaultState という変数がタスクを作成する際に入力するフォームのラベルのデフォルト値となっていたので、 DEFAULT_LABELS を定義し、外部ファイルで設定できるようにしました。

 

4.まとめ

以上のようなコード変更を行うことにより、DICOM を CVAT で扱えるように改造し、実際のプロジェクトで運用できるようになりました。今回の記事はプロジェクトの準備段階に行われていることで、本質的な AI 開発についてではなかったですが、HACARUS ではアノテーションの際にも使用するツールにも理由を持って選択していて、使用する人が少しでも便利になるようにこういった工夫がなされていることを知っていただければ幸いです。
また、開発する身としては、ただ公開されているツールを使うだけではなく、OSS の仕様を理解して手を加える作業は、いつもとは異なるコードを読んで勉強できる良い機会だと思いました。ツールのことを理解していると、他のプロジェクトにも応用できたり、使用する人の要望にも対応しやすくなります。このようにツールにもこだわりを持って携われるのは貴重な経験だと思いました。
あまり普段意識されることがない教師データ作成にもささやかな工夫があるというエントリでした。

ニュースレター購読Newsletter

登録はこちら