Lesson 7 / 12

DICOM画像を扱う

このレッスンで学ぶこと

  • 医用画像の標準DICOMが「画像+メタデータ(タグ)」の一体構造だと理解する
  • pydicom で読み込み、pixel_arrayで画素を取り出す流れを読む
  • HU値変換(RescaleSlope/Intercept)とウィンドウ処理(肺野/縦隔)を理解する
  • 患者情報の匿名化——AIに画像を入れる前に必ず行うことを知る

1. このレッスンの読み方

ここからUnit 3では医用画像AIを扱います。画像の深層学習はPyTorchで書きますが、 PyTorchはブラウザ内のPython(Pyodide)では動きません。 そのため本UnitはE資格コーディング対策コースと同じく、 コードを実行せず「読み解く」形式で進めます。 このレッスンのコードには実行ボタンがなく、代わりに「実行したらどう出るか」を「▼ 出力」として下に示します (出力は pydicom に同梱された公開テストデータで実際に確認した値です)。

💡 なぜ画像は「別扱い」なのか: テーブルデータ(レッスン3〜6)は数値がそのまま特徴量でした。 しかし医用画像は、ファイルを開いて画素にする・値を意味のある単位に直す・見たい部位が見えるよう調整する—— AIに入れる前の前処理が一段深いのです。まずはその入口、DICOMを知りましょう。

2. DICOMとは — 画像とカルテ情報が一体のファイル

DICOM(ダイコム)は、CT・MRI・レントゲンなどの医用画像の標準フォーマットです。 ふつうの画像(JPEGやPNG)と決定的に違うのは、1つのファイルの中に「画像」と「メタデータ(タグ)」が同居していること。 タグには患者情報・撮影条件・画素を意味に直すための係数などが詰まっています。 ファイルの中身は「タグ・型・長さ・値」からなる要素がタグの番号順に並んだ構造で、画像本体(画素データ)もその最後の要素の1つです。

DICOMファイルの構造。1つのファイルの中に、患者名・モダリティ・画素間隔・Rescale係数などのタグ(メタデータ)と画素データ (7FE0,0010) が入っている。画素データは pydicom の ds.pixel_array で取り出せる
DICOMは「ヘッダー(タグ)」と「画素データ」から成る。タグは (グループ番号, 要素番号) の番号順に並び、 画素データ (7FE0,0010) はその最後に置かれる。図はレッスン本文と同じ pydicom 同梱のテストデータ(CT_small.dcm)

Pythonでは pydicom というライブラリで読み込みます。まずはファイルを開いて、主要なタグを見てみましょう。

read_dicom.py(pydicom・読むだけ)
import pydicom

# DICOMファイルを読み込む(1ファイル=CTの1スライス)
ds = pydicom.dcmread("ct_slice.dcm")

# 主要なタグ(メタデータ)を見る
print("モダリティ:", ds.Modality)
print("画像サイズ:", ds.Rows, "x", ds.Columns)
print("画素間隔(mm):", ds.PixelSpacing)
print("Rescale係数:", ds.RescaleSlope, ds.RescaleIntercept)
▼ 出力
モダリティ: CT
画像サイズ: 128 x 128
画素間隔(mm): [0.661468, 0.661468]
Rescale係数: 1.0 -1024.0

ds.Modality で「これはCT画像」、ds.PixelSpacing で「1画素=約0.66mm四方」—— 画像を実寸で扱えるのがDICOMの強みです。属性名(Modality など)でタグを読めるので、 辞書のように直感的に扱えます。

3. 画素を取り出す — pixel_array

画像本体(画素の数値)は ds.pixel_array で、NumPy配列として取り出せます。 ここから先は、あなたがNumPyコースで学んだ配列操作の世界です。

pixel_array.py(pydicom・読むだけ)
arr = ds.pixel_array          # NumPy配列として取り出す

print("shape:", arr.shape)     # (高さ, 幅)
print("dtype:", arr.dtype)     # 整数型
print("生の画素 min/max:", arr.min(), arr.max())
▼ 出力
shape: (128, 128)
dtype: int16
生の画素 min/max: 128 2191

⚠ この「生の画素値」はまだ意味を持たない: min 128・max 2191 という数字は、そのままでは「空気」や「骨」といった組織の意味に対応していません。 CT画像を正しく読むには、この生の値をHU値(次の§)に変換する必要があります。 生の pixel_array をそのままAIに入れてしまうのは、医用画像でよくある初歩のミスです。

4. HU値変換 — 画素を「組織の意味」に直す

CT画像の生の画素値は、装置ごとの都合で保存された数字にすぎません。 これをHU値(ハウンスフィールド単位)に直すと、組織の種類と結びついた世界共通の物差しになります。 変換式はシンプルで、タグに入っている RescaleSlopeRescaleIntercept を使います。

🩺 HU値のめやす: 空気 ≒ -1000/肺 ≒ -500〜-900/脂肪 ≒ -100/水 = 0/軟部組織 ≒ +40/骨 ≒ +400〜+1000。 この物差しがあるから、「HU=-800付近は肺」「HU=+700は骨」と組織を数値で狙い撃ちできるのです。

hu.py(pydicom・読むだけ)
# HU値 = 生の画素値 × RescaleSlope + RescaleIntercept
hu = arr * ds.RescaleSlope + ds.RescaleIntercept

print("HU min/max:", hu.min(), hu.max())
▼ 出力
HU min/max: -896.0 1167.0

変換後は -896〜1167。最小値がおよそ -900 で「空気〜肺」、最大が +1167 で「骨」と、 HU値のめやすと一致します。RescaleSlope=1, RescaleIntercept=-1024 なので、 実質「生の値から1024を引く」変換でした。この一手間で、画像が解剖学的に意味のあるデータになります。

5. ウィンドウ処理 — 見たい組織を浮かび上がらせる

HU値は約 -1000〜+1000 の広い範囲を持ちますが、人の目やモデルが一度に見分けられる濃淡は限られます。 そこで、見たい組織のHU範囲だけを切り出して白黒に割り当てるのがウィンドウ処理です。 中心をWL(ウィンドウレベル)、幅をWW(ウィンドウ幅)で指定します。

同じ胸部CTのHUデータを、肺野条件(WL −600/WW 1500)と縦隔条件(WL 40/WW 400)でウィンドウ処理した比較。肺野条件では肺の血管が見え、縦隔条件では肺が黒くつぶれて大動脈や椎体の軟部組織が見える
同じ胸部CT(Axial・脊椎周囲)のHUデータでも、ウィンドウ設定で見える組織が変わる。 左の肺野条件では肺の中の血管が見え、右の縦隔条件では肺が真っ黒につぶれる代わりに大動脈・椎体・筋肉の濃淡が見える。 画像はレッスン本文と同じ pydicom 同梱のテストデータ(CT_small.dcm)。下段は各条件がHUのどの範囲を白黒に割り当てているか
window.py(pydicom・読むだけ)
import numpy as np

def apply_window(hu, wl, ww):
    # WL=中心, WW=幅。範囲外は0〜255に丸める
    lo, hi = wl - ww / 2, wl + ww / 2
    img = np.clip(hu, lo, hi)
    return ((img - lo) / (hi - lo) * 255).astype(np.uint8)

lung = apply_window(hu, wl=-600, ww=1500)   # 肺野条件
med  = apply_window(hu, wl=40,  ww=400)    # 縦隔条件
print("肺野条件 min/max:", lung.min(), lung.max())
print("縦隔条件 min/max:", med.min(), med.max())
▼ 出力
肺野条件 min/max: 77 255
縦隔条件 min/max: 0 255

⚠ AIにとってのウィンドウ処理: どのウィンドウで前処理するかで、モデルに見える情報が変わります。 肺の病変を探すAIなら肺野条件、というようにタスクに合ったウィンドウを選びます。 さらに訓練と本番で同じウィンドウ設定をそろえることも重要—— 違う設定の画像を混ぜると、レッスン11で扱う分布のズレの原因になります。

6. 匿名化 — AIに入れる前の必須作業

レッスン2で学んだとおり、 医療データは要配慮個人情報です。DICOMのタグには患者名・生年月日・IDなどが含まれるため、 研究やAI開発に使う前に匿名化(個人が特定できないようにする)が欠かせません。

anonymize.py(pydicom・読むだけ)
# 個人を特定できるタグを消す・置き換える
ds.PatientName = "ANONYMOUS"
ds.PatientID   = "0000"

# 不要な識別タグは削除(存在すれば)
for tag in ["PatientBirthDate", "PatientAddress"]:
    if tag in ds:
        del ds[tag]

print("匿名化後:", ds.PatientName)
▼ 出力
匿名化後: ANONYMOUS

⚠ タグを消すだけでは足りないことも: 匿名化は「名前タグを消せば終わり」ではありません。 頭部CTのように画素そのものから顔を再構成できる画像もあり、その場合は画像側の処理(顔のぼかし等)も検討します。 また、実際の匿名化は施設のルール・研究倫理審査に従って行うもので、 ここで学んだのは仕組みの理解です。運用の基準はレッスン10で扱います。

7. 練習問題

コードと出力を読んで答えを選び、「答えと解説を見る」で確認しましょう。

問題 1

HU値変換の式は?

生の画素値からHU値を求める正しい式はどれですか(slope=1, intercept=-1024 のとき)。

  • A. hu = arr * intercept + slope
  • B. hu = arr * slope + intercept
  • C. hu = (arr - intercept) / slope
答えと解説を見る

正解:B. hu = arr * slope + intercept

RescaleSlope を掛けて RescaleIntercept を足す、が定義です。 今回は slope=1・intercept=-1024 なので「生の値から1024を引く」ことになり、 生の min 128 → HU -896、max 2191 → HU 1167 と、出力と一致します。

問題 2

骨を白く見たいときのウィンドウは?

骨(HU ≒ +400〜+1000)をはっきり見たいとき、ウィンドウレベル(WL)の設定として最も適切なのはどれですか。

  • A. WL = -600(肺野条件)
  • B. WL = 40(縦隔条件)
  • C. WL = 500 前後(骨に合わせる)
答えと解説を見る

正解:C. WL = 500 前後

ウィンドウレベル(WL)は「注目したいHU値の中心」です。骨はHU +400〜+1000あたりなので、 その中心の500前後にWLを合わせ、幅(WW)を広めに取ると骨がよく見えます(骨条件)。 WL=-600は肺、WL=40は軟部組織向けで、骨は白飛びして見分けにくくなります。 見たい組織のHUにWLを合わせるのが基本です。

問題 3

前処理の順序

CT画像をAIに入れるまでの前処理を、正しい順に並べてください。
(ア)ウィンドウ処理 (イ)DICOM読み込み (ウ)HU値変換 (エ)pixel_array取り出し

答えと解説を見る

正解:(イ)→(エ)→(ウ)→(ア)

まず DICOMを読み込み(イ)、pixel_arrayで画素を取り出し(エ)、 HU値に変換(ウ)して意味のある物差しにし、最後にウィンドウ処理(ア)で見たい組織を浮かび上がらせます。 この後さらに「リサイズ・0〜1正規化」を経てモデルに入ります——その全体像は次のレッスン8で読み解きます。

8. まとめ

このレッスンのポイント

  • DICOMは「画像+メタデータ(タグ)」が一体のフォーマット。pydicom.dcmread で読む
  • ds.pixel_array でNumPy配列として画素を取り出す。ただし生の値はまだ意味を持たない
  • HU値変換arr * RescaleSlope + RescaleIntercept)で組織の物差しに直す(空気-1000・水0・骨+1000)
  • ウィンドウ処理(WL/WW)で見たい組織を浮かび上がらせる。訓練と本番で設定をそろえる
  • AIに入れる前に匿名化が必須。タグだけでなく画素からの再特定にも注意

画像を「AIに入れられる形」に整える前処理ができました。 次のレッスンでは、この前処理を土台に、CT画像を分類するCNNの実装コードを読み解きます。 患者単位分割やデータ拡張の是非など、医用画像ならではの注意点が主役です。

完了するとコース一覧に進捗が記録されます