エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

DICOMを使いこなして医療画像を扱う

こんにちは、AI・機械学習チームの浮田 (id:uKita) です。今回は、医療画像においてスタンダードとなっているDICOMという規格について紹介します。

私たちのチームでは臨床現場で活用できるAIの開発に取り組んでおり、様々な医療画像を扱っています。中でもX線やCTなどの放射線画像ではDICOMという形式がメジャーですが、DICOMは以下の理由からソフトウェアエンジニアやデータサイエンティストにとっては少し扱いにくいと思っています。

  1. 公式の仕様がかなり長くてとっつきにくい
  2. 分かりやすいサイトや書籍がほとんどない
  3. 規格の自由度が大きく、カスタムタグに多くの情報が含まれていることもある
  4. 画像規格と通信規格の両方がある

この記事では、DICOM初心者のソフトウェアエンジニア・データサイエンティスト向けに、Python上でのDICOM画像の基本的な扱い方を紹介します。

DICOM画像の例。左はX線、右はCTの1スライスの画像

環境構築

Pydicomというパッケージをインストールします。

pydicom.github.io

Pydicom自体はpipでインストールできますが、公式ドキュメント通り、Numpy, Pillow, pyjpegls, GDCM, pylibjpegあたりもダウンロードしておいたほうが無難です*1

X線のサンプルデータがPydicomのパッケージの中にあります。例えばRG1_J2KR.dcmというファイルは胸部X線の画像です。

Pydicomを使えば、以下のコードだけでDICOMファイルの中身が読み込めます。

import pydicom
ds = pydicom.dcmread('RG1_J2KR.dcm')
print(ds)

DICOMタグとタイプについて

DICOMファイルは基本的にはkey(タグ)とvalue(値)の組み合わせです。全てのタグについて、Innoliticsさんが以下のページで非常に分かりやすくまとめてくれています。

dicom.innolitics.com

タグは、(0010,0010) のように数字のTupleで表す方法と、Patient Nameのように文字列で表す方法の2通りがあります。Pydicomではいずれの方法でも値を取得できます。

print(ds[(0x0010,0x0010)])
print(ds.PatientName)

さらにタグには、Type 1, Type 2, Type 3の3種類があります。

  • Type 1: タグもデータも必須
  • Type 2: タグは必須だがデータは任意
  • Type 3: タグが任意

X線でよく使うタグ

Pixel Data

画像の輝度値です。Pydicomでは以下のように取り出せ、matplotlibなどで可視化できます。

image: numpy.ndarray = ds.pixel_array

逆にPixel Dataを更新したい場合は、以下のように更新できます。

ds.PixelData = image.tobytes()  # image: numpy.ndarray

Bits Stored

画像のビット深度です。

bits_stored: int = ds.BitsStored

医療機関ごとにビット深度が異なっていることがあり、機械学習においてリークの原因になることがあります。例えば病院Aからpositive例を、病院Bからnegative例を収集し、病院Aと病院Bでビット深度が違っている場合、ビット深度の情報だけからpositive/negative分類ができてしまいます。

Photometric Interpretation

画像の輝度値の定義です。X線画像なら基本的にMONOCHROME1もしくはMONOCHROME2かと思います。

MONOCHROME1では0の値が白、MONOCHROME2では0の値が黒を意味するという違いがあります。コンピュータビジョンでは一般的に後者なので、MONOCHROME1の画像は前述のBits Storedを使って反転させると良いと思います。

def get_image_from_dicom(ds: pydicom.FileDataset) -> numpy.ndarray:
    image = ds.pixel_array
    bits_stored = ds.BitsStored
    if ds.PhotometricInterpretation == 'MONOCHROME1':
        image = -1 * image + pow(2, bits_stored) - 1
    return image

当初Photometric Interpretationの意味を知らず、MONOCHROME1の画像を反転させないまま扱ってしまい学習に失敗した経験があります。。。

CTでよく使うタグ

これまで説明したタグ以外に、以下のタグがよく用いられます。なおCTのサンプルファイルもPydicomのパッケージの中にあります。

Instance Number

CTは、2次元平面(xy)のスライスをz方向に複数枚撮像したものになります。DICOMファイル1個には1スライスのデータのみが含まれているため、例えば64スライスからなる撮影は64個のDICOMファイルから成ります。

コンピュータビジョンでCTデータを3次元として扱うには、スライスのz方向の順番の情報が必要です*2。Instance Numberがこれに該当するため、Instance Numberでソートすることでz方向の順番を正しくできます。

CT撮影のスライスの例。

Slice Thickness, Pixel Spacing

CTは、例えば5 mmのスライス間隔(スライス厚)で撮影されることもあれば0.5 mmのスライス厚で撮影されることもあります。同様にxy方向についても、1ピクセルあたりの実際の物理距離(mm)は撮影ごとに異なる可能性があります。このように撮影間で解像度が異なっているため、機械学習などの場面では解像度を統一したい場面が出てきます。

スライス厚、xy方向のピクセルあたりの距離はそれぞれSlice Thickness, Pixel Spacingのタグに格納されています。これらの値を使ってCT画像の解像度を1mm/pixelに変換する方法は、KaggleのNotebookなどにあります。

Rescale Intercept, Rescale Slope

CT値(Hounsfield値:HU)という、水が0、空気が-1000の値になるよう定義されたスケールが存在します。Rescale InterceptとRescale Slopeの値を用いて輝度値をCT値に変換することで、撮影条件の違いを吸収し、撮影間で輝度値の比較ができるようになります。

def convert_to_hu_single_image(ds: pydicom.FileDataset) -> numpy.ndarray:
    image = get_image_from_dicom(ds)
    intercept = float(ds.RescaleIntercept)
    slope = float(ds.RescaleSlope)
    return (slope * image + intercept).astype(numpy.int16)

まとめ

いかがでしたでしょうか。DICOMには沢山のタグがあり、この記事ではほんの一部だけ紹介しました。Photometric Interpretationなど、ハマりやすいポイントもあったかと思います。なお、DICOMを完全に理解するには仕様を読むしかないので、本記事ではカバーしていない範囲もたくさんあります。例えば、

  1. 通信規格としてのDICOM
  2. X線、CT以外のモダリティ(MRI、超音波など)固有のタグ

については省略しています。

We're hiring!

AI・機械学習チームでは、医療画像をはじめ様々な問題に機械学習を用いて取り組んでいます。興味を持った方は、以下のリンクからご応募お待ちしています! インターンも通年募集中です!

jobs.m3.com

*1:例えばGDCMがないと画像の中身を読み込めないようなDICOMファイルもあります

*2:ファイル名の順番とzの順番と対応している保証はないです