0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

医用画像(DICOM/NIfTI)のPNG変換と機械学習の基礎的アプローチ【備忘録】

0
Last updated at Posted at 2026-08-19

はじめに

機械学習の発展に伴い、医用画像×機械学習はホットな研究分野になっています。私もMRIやCT画像を使った研究をやっていましたが、最近は触れていなかったため、当時の知見を忘れそうになっていました。

まだなんとか思い出せたので、備忘録としてまとめておこうと思います。
本記事では、データセットとなる医用画像(DICOM・NIfTI形式)のPNG変換方法や、医用画像を用いた機械学習の基礎的なアプローチについてまとめています。~2024年に得た知見整理のため、情報は最新ではない可能性がある旨、ご了承ください。
(間違ってること書いてたらゴメンネ)

代表的な医用画像の形式

DICOM(.dcm)

医療現場で撮影されたMRI・CT・X線などの画像を保存する代表的な国際標準規格です。
基本は1スライス1ファイルとして保存されており、画像だけでなく、患者情報や撮影条件などのメタデータも含むファイルです。グレースケール画像は基本的には1画素あたり8bitの範囲のコントラスト範囲ですが、DICOMで取り扱う画像では12~16bitを取り扱うことも特徴です。

臓器領域、腫瘍領域など複数のアノテーション情報を1つのファイルにまとめたDICOM SEGなどもあります。

DICOMの画素値はそのままでは使えず、HU値変換やウィンドウ処理が必要です。

MRI・CTには単純CT・MRIと造影CT・MRIがあり、造影CT・MRIでは病変(腫瘍など)が強調されて映ります。また、造影CTには「動脈相」「門脈相」「静脈相」という複数の相があり、それぞれ見え方が異なります。

NIfTI(.nii)

医療画像研究で使用される代表的なフォーマットです。DICOMと異なり、複数枚のスライスをまとめた3Dボリュームが1ファイルに含まれています。
患者情報などは含まれず、画像以外のメタ情報はDICOMに比べ最小限の情報に絞られています。そのため研究用のデータセットとして配布しやすい形式となっています。

NIfTI形式のデータも、元がCTなどでHU値を保持している場合はDICOMと同様にHU値変換・ウィンドウ処理が必要です。ただし、データセットによってはあらかじめ正規化された状態で配布されていることもあります。

医用画像のPNG変換

実際にDICOM形式、NIfTI形式のデータを機械学習モデルへのインプットとして使用できるように、PNG変換をしてみます。
※コードはClaude Codeで生成しています。コードの処理説明は割愛します。

DICOM

DICOM形式のファイル読み込みはpythonのオープンソースライブラリであるpydicomを使用します。
また、データはTCIA(The Cancer Imaging Archive)で公開されているHCC-TACE-Seg | Multimodality annotated HCC cases with and without advanced imaging segmentation [1]のデータセットを使用します。

PNG変換の前に、以下のコードを実行し、DICOMに含まれるメタ情報を出力してみます。

print_dcm_meta_data.py
import argparse
from pathlib import Path

import pydicom

DEFAULT_FILE = "{input_file}.dcm"


def get(ds: pydicom.dataset.FileDataset, tag: str, default: str = "N/A"):
    value = ds.get(tag, default)
    return value if value not in (None, "") else default


def print_section(title: str) -> None:
    print(f"\n[{title}]")


def main() -> None:
    parser = argparse.ArgumentParser(description="DICOMファイルの基本情報を表示する")
    parser.add_argument("dcm_path", nargs="?", default=DEFAULT_FILE, help="読み込む.dcmファイルのパス")
    args = parser.parse_args()

    dcm_path = Path(args.dcm_path)
    if not dcm_path.is_file():
        raise SystemExit(f"ファイルが見つかりません: {dcm_path}")

    ds = pydicom.dcmread(str(dcm_path))

    print(f"読み込みファイル: {dcm_path}")

    print_section("患者情報 (Patient)")
    print(f"  PatientID          : {get(ds, 'PatientID')}")
    print(f"  PatientName         : {get(ds, 'PatientName')}")
    print(f"  PatientSex          : {get(ds, 'PatientSex')}")
    print(f"  PatientAge          : {get(ds, 'PatientAge')}")
    print(f"  PatientBirthDate    : {get(ds, 'PatientBirthDate')}")

    print_section("検査情報 (Study)")
    print(f"  StudyInstanceUID    : {get(ds, 'StudyInstanceUID')}")
    print(f"  StudyDate           : {get(ds, 'StudyDate')}")
    print(f"  StudyTime           : {get(ds, 'StudyTime')}")
    print(f"  StudyDescription    : {get(ds, 'StudyDescription')}")
    print(f"  AccessionNumber     : {get(ds, 'AccessionNumber')}")

    print_section("画像データ情報 (Pixel Data)")
    print(f"  Rows x Columns      : {get(ds, 'Rows')} x {get(ds, 'Columns')}")
    print(f"  NumberOfFrames      : {get(ds, 'NumberOfFrames', '1')}")
    print(f"  BitsAllocated       : {get(ds, 'BitsAllocated')}")
    print(f"  BitsStored          : {get(ds, 'BitsStored')}")
    print(f"  PixelRepresentation : {get(ds, 'PixelRepresentation')}")
    print(f"  SamplesPerPixel     : {get(ds, 'SamplesPerPixel')}")
    print(f"  PhotometricInterpretation: {get(ds, 'PhotometricInterpretation')}")
    print(f"  RescaleSlope/Intercept: {get(ds, 'RescaleSlope', '1')} / {get(ds, 'RescaleIntercept', '0')}")
    print(f"  WindowCenter/Width  : {get(ds, 'WindowCenter')} / {get(ds, 'WindowWidth')}")

    if hasattr(ds, "pixel_array"):
        arr = ds.pixel_array
        print(f"  pixel_array shape   : {arr.shape}")
        print(f"  pixel_array dtype   : {arr.dtype}")
        print(f"  pixel_array min/max : {arr.min()} / {arr.max()}")


if __name__ == "__main__":
    main()

実行結果は以下です。

読み込みファイル: {input_file}.dcm

[患者情報 (Patient)]
  PatientID          : HCC_001
  PatientName         : HCC_001
  PatientSex          : N/A
  PatientAge          : N/A
  PatientBirthDate    : N/A

[検査情報 (Study)]
  StudyInstanceUID    : 1.3.6.1.4.1.14519.5.2.1.1706.8374.164750580271137946982420100377
  StudyDate           : 19991130
  StudyTime           : N/A
  StudyDescription    : CT-C/A/P W/WO CON
  AccessionNumber     : N/A

[画像データ情報 (Pixel Data)]
  Rows x Columns      : 512 x 512
  NumberOfFrames      : 1
  BitsAllocated       : 16
  BitsStored          : 16
  PixelRepresentation : 1
  SamplesPerPixel     : 1
  PhotometricInterpretation: MONOCHROME2
  RescaleSlope/Intercept: 1 / -1024
  WindowCenter/Width  : 55 / 500
  pixel_array shape   : (512, 512)
  pixel_array dtype   : int16
  pixel_array min/max : -1024 / 2220

メタ情報が出力され、DICOMファイルに画像以外のデータが含まれていることが確認できました。
他にもメタ情報は含まれますが、今回は絞って出力しています。
では、PNG変換をしていきましょう。
以下を実行します。

convert_dcm_to_png.py
import numpy as np
import pydicom
from PIL import Image

DCM_PATH = "{input_file}.dcm"
PNG_PATH = "{output_file}.png"

# DICOM読み込み
ds = pydicom.dcmread(DCM_PATH)
arr = ds.pixel_array.astype(np.float64)

# RescaleSlope / RescaleIntercept を適用(HU値へ変換)
slope = float(getattr(ds, "RescaleSlope", 1))
intercept = float(getattr(ds, "RescaleIntercept", 0))
arr = arr * slope + intercept

# WindowCenter / WindowWidth で表示レンジを絞り、0〜255に変換
center = float(ds.WindowCenter[0] if isinstance(ds.WindowCenter, pydicom.multival.MultiValue) else ds.WindowCenter)
width = float(ds.WindowWidth[0] if isinstance(ds.WindowWidth, pydicom.multival.MultiValue) else ds.WindowWidth)
low, high = center - width / 2, center + width / 2
arr = np.clip(arr, low, high)
arr = (arr - low) / (high - low) * 255.0
arr = arr.astype(np.uint8)

# PNGとして保存
Image.fromarray(arr, mode="L").save(PNG_PATH)

print(f"変換完了: {DCM_PATH} -> {PNG_PATH}")

実行後、以下の画像が出力され、正常にPNG変換することができました。

NIfTI

NIfTI形式のファイル読み込みはpythonのオープンソースライブラリであるnibabelを使用します。
また、データはKaggleで公開されているMycobacterial CT Imaging Dataset [2]のデータセットを使用します。

DICOMと同様にPNG変換前に、以下のコードを実行し、NIfTIに含まれるメタ情報を出力してみます。

print_nifti_meta_data.py
import argparse
from pathlib import Path

import nibabel as nib
import numpy as np

DEFAULT_FILE = "{input_file}.nii"


def print_section(title: str) -> None:
    print(f"\n[{title}]")


def main() -> None:
    parser = argparse.ArgumentParser(description="NIfTIファイルの基本情報を表示する")
    parser.add_argument("nii_path", nargs="?", default=DEFAULT_FILE, help="読み込む.nii/.nii.gzファイルのパス")
    args = parser.parse_args()

    nii_path = Path(args.nii_path)
    if not nii_path.is_file():
        raise SystemExit(f"ファイルが見つかりません: {nii_path}")

    img = nib.load(str(nii_path))
    header = img.header

    print(f"読み込みファイル: {nii_path}")

    print_section("ボリューム情報 (Volume)")
    print(f"  shape (dim)         : {img.shape}")
    print(f"  voxel size (zooms)  : {header.get_zooms()}")
    print(f"  データ型 (datatype) : {header.get_data_dtype()}")
    print(f"  次元数 (ndim)       : {len(img.shape)}")
    print(f"  単位 (xyzt_units)   : {header.get_xyzt_units()}")

    print_section("座標変換情報 (Affine)")
    print(f"  qform_code          : {header['qform_code']}")
    print(f"  sform_code          : {header['sform_code']}")
    print("  affine:")
    for row in img.affine:
        print("    " + "  ".join(f"{v:10.4f}" for v in row))

    print_section("値の範囲・スケール (Data range / Scaling)")
    print(f"  scl_slope           : {header['scl_slope']}")
    print(f"  scl_inter           : {header['scl_inter']}")
    print(f"  cal_min / cal_max   : {header['cal_min']} / {header['cal_max']}")

    print_section("その他ヘッダ情報 (Header)")
    print(f"  descrip             : {header['descrip']}")
    print(f"  intent_code         : {header.get_intent()[0]}")
    print(f"  file形式 (extension): {''.join(nii_path.suffixes)}")

    print_section("画素データ情報 (Pixel Data)")
    data = img.get_fdata()
    print(f"  data shape          : {data.shape}")
    print(f"  data dtype (loaded) : {data.dtype}")
    print(f"  min/max             : {np.nanmin(data)} / {np.nanmax(data)}")
    print(f"  mean                : {np.nanmean(data):.4f}")
    print(f"  unique値の数        : {len(np.unique(data))}")


if __name__ == "__main__":
    main()

実行結果は以下です。

読み込みファイル: {input_file}.nii

[ボリューム情報 (Volume)]
  shape (dim)         : (391, 391, 310)
  voxel size (zooms)  : (1.0, 1.0, 1.0)
  データ型 (datatype) : int16
  次元数 (ndim)       : 3
  単位 (xyzt_units)   : ('mm', 'sec')

[座標変換情報 (Affine)]
  qform_code          : 1
  sform_code          : 1
  affine:
       -1.0000      0.0000      0.0000    168.5630
        0.0000     -1.0000      0.0000    194.9310
        0.0000      0.0000      1.0000   -366.6000
        0.0000      0.0000      0.0000      1.0000

[値の範囲・スケール (Data range / Scaling)]
  scl_slope           : nan
  scl_inter           : nan
  cal_min / cal_max   : 0.0 / 0.0

[その他ヘッダ情報 (Header)]
  descrip             : b''
  intent_code         : none
  file形式 (extension): .nii

[画素データ情報 (Pixel Data)]
  data shape          : (391, 391, 310)
  data dtype (loaded) : float64
  min/max             : 0.0 / 0.9999999997671694
  mean                : 0.4302
  unique値の数        : 1501

メタ情報が出力され、NIfTIファイルに画像以外のデータが含まれていることが確認できました。
では、PNG変換をしていきましょう。
以下を実行します。

convert_nii_to_png.py
import argparse
from pathlib import Path

import nibabel as nib
import numpy as np
from PIL import Image


def main() -> None:
    parser = argparse.ArgumentParser(description="NIfTI(.nii/.nii.gz)をスライスごとにPNGに変換する")
    parser.add_argument("--input", default="{input_file}.nii", help="変換対象の.niiファイル")
    parser.add_argument("--output", default=None, help="PNG出力先ディレクトリ(未指定なら<input>_png)")
    parser.add_argument("--axis", type=int, default=2, choices=[0, 1, 2],
                         help="スライスする軸(0:Sagittal, 1:Coronal, 2:Axial 既定値2)")
    args = parser.parse_args()

    nii_path = Path(args.input)
    if not nii_path.is_file():
        raise SystemExit(f"入力ファイルが見つかりません: {nii_path}")

    output_dir = Path(args.output) if args.output else Path(f"{nii_path.stem}_png")
    output_dir.mkdir(parents=True, exist_ok=True)

    img = nib.load(str(nii_path))
    data = img.get_fdata()
    print(f"読み込み: {nii_path}  shape={data.shape}  dtype={data.dtype}")

    # 全スライス共通のスケールで0〜255に正規化
    data_min, data_max = data.min(), data.max()
    if data_max > data_min:
        data = (data - data_min) / (data_max - data_min) * 255.0
    else:
        data = np.zeros_like(data)
    data = data.astype(np.uint8)

    n_slices = data.shape[args.axis]
    print(f"{n_slices} 枚のスライスを軸{args.axis}方向に出力します...")

    for i in range(n_slices):
        slice_2d = np.take(data, i, axis=args.axis)
        # 表示向きを画像として見やすいように90度回転
        slice_2d = np.rot90(slice_2d)
        png_path = output_dir / f"{nii_path.stem}_{i:04d}.png"
        Image.fromarray(slice_2d, mode="L").save(png_path)

    print(f"完了: {n_slices} 枚のPNGを出力しました。")
    print(f"出力先: {output_dir.resolve()}")


if __name__ == "__main__":
    main()

NIfTI形式はボリュームデータを含むため、複数スライスの画像が出力されました。以下の画像はボリュームのうちの1スライス分です。
今回使用したデータセットではすでに正規化された値だったため、min-max正規化のみでPNG変換できました。

学習の基礎的アプローチ案

ここからは、医用画像を用いた機械学習の基礎的なアプローチ案をいくつか整理してみました。
ここでは主に分類タスクに焦点を当てます。

事前学習モデルの使用

医用画像はデータセットが少ないため、大規模データで事前学習された重みを初期値とすることで、精度向上が見込めます。しかし、ImageNetなどの自然画像で事前学習されたモデルを医用画像に適用する場合、両者の性質の違いからドメインギャップが生まれやすい点には注意が必要です。
ドメインギャップ軽減案の選択肢として、Microsoftが公開しているBiomedCLIPを事前学習モデルとして用いることも考えられます。このモデルは、PubMed Centralの論文から収集したPMC-15Mという大規模データセットを使い、CLIPをさらに医用領域向けにファインチューニングしたモデルになります。
また、本記事で紹介したような公開データセットを用いて、事前学習するのも一つの手です。

3チャネル化

医用画像は基本的にグレースケール(1チャネル)ですが、RGB3チャネルを前提とした事前学習モデルを用いる場合、複数の情報を組み合わせた3チャネル画像で追加学習するアプローチが考えられます。
3チャネル化は以下のような構成が考えられます。
・異なる相(動脈相、門脈相、静脈相)の使用
・異なるウィンドウ幅で変換した3つの画像
・対象スライス+前後スライス

領域抽出後の分類

医用画像をそのまま入力とした場合、対象タスクによっては背景領域から抽出した特徴量がノイズとなる可能性があります。例えば肝腫瘍の分類では、骨や肝臓以外の臓器の特徴量にモデルが引っ張られてしまう可能性があります。

そこで、まずセグメンテーションモデルへ入力して対象領域のみを抽出し、その後分類モデルで分類するアプローチが考えられます。これにより、対象外の領域がノイズになることを防げます。

一方で、セグメンテーションモデルの構築や精度への依存、アノテーション作成の手間が増えるといったデメリットもあります。

スクリーンショット 2026-08-19 000939.png

マルチモーダル化

画像に加え、血液情報などの臨床因子を入力とすることで、画像だけでは得られない情報を特徴量として活用できます。しかし、画像から得られる特徴量と臨床因子から得られる特徴量には次元差があるため、考慮が必要です。対応として、画像から得られた特徴量をPCAなどで次元削減し、次元差を縮小する方法が考えられます。逆に、埋め込み層を用いて臨床因子側の次元数を拡張する方法も考えられます。

1クラス分類

「正常データ」など1つのクラスのみを学習し、それ以外(異常・未知)を検出する分類手法です。通常の2クラス・多クラス分類とは異なり、「正常とは何か」だけを学習し、そこから外れるものを異常とみなすというアプローチです。
医用画像では正常データは大量に集めやすいのに対し、特定の疾患・病変データは希少で集めにくいというケースが多くあります。このような場合、正常データのみで学習する1クラス分類は非常に有効です。

アンサンブル学習

複数のモデルの予測結果を組み合わせることで、単一モデルよりも高い精度・汎化性能を狙う手法です。
異なるアーキテクチャ(CNN系・Transformer系など)や、異なる入力(単一相・3チャネルなど)で学習した複数モデルの予測を組み合わせることで、それぞれのモデルの弱点を補い合う効果が期待できます。

代表的な手法として、以下のようなものがあります。
・複数モデルの予測確率を平均する方法
・多数決による方法
 
以上、医用画像の分類タスクにおける基礎的なアプローチをいくつか紹介しましたが、これらはあくまで一例です。他にも手法はたくさんあります。

学習時の注意点

データ拡張

モデル学習ではデータ拡張が一般的によく用いられますが、医用画像を学習する際には注意が必要です。
定番の拡張手法である左右反転は、胸部X線・腹部CTなど体の左右で臓器配置が異なる部位では、反転することで非現実的な構造になってしまう場合があります。一方、脳MRIのように左右対称性が比較的保たれる部位では、反転が有効なケースもあります

また、拡大・縮小についても注意が必要です。腫瘍などの病変の大きさは診断において重要な情報(ステージ分類)であるため、過度なスケーリングを行うとこの情報が歪んでしまいます。

画像生成

医用画像はデータ数が少ないため、GAN(敵対的生成ネットワーク)による画像生成でデータを補う方法も考えられますが、実在しない解剖学的構造を生成してしまうリスクがあるため、通常のデータ拡張以上に注意が必要です。
医学的にあり得ない病変パターンを生成したり、似たようなパターンを多く生成してしまう可能性があります。

最近の動向を軽く調べてみた

最近の医療画像×機械学習の研究ではどのようなモデルが使われているか気になったので、本当にかる~くですが調べてみました(内容まではちゃんと見てません)。
Vision Transformer(ViT)ベースや、CLIPのようなTransformer由来のアーキテクチャが多い印象でした。自然言語処理で発展したSelf-Attention機構を画像に応用したモデルが広く採用されていそうです。CNN系もまだまだ使われており、ResNetやEfficientNetは比較手法としても使われてるみたいですね。

余談

学生時代の研究を昨年の7月に論文として投稿していましたが、11月のリバイスを経て、今年の5月にSurgery Todayにて採択されてました(うれしい)。

参考文献

本記事では、以下2つの公開データセットを利用しました。
いずれも Creative Commons Attribution 4.0 International License
(CC BY 4.0) の下で公開されています。

[1] DICOM形式データ(The Cancer Imaging Archive)

  • Moawad, A. W., Fuentes, D., Morshid, A., Khalaf, A. M., Elmohr, M. M.,
    Abusaif, A., Hazle, J. D., Kaseb, A. O., Hassan, M., Mahvash, A.,
    Szklaruk, J., Qayyom, A., & Elsayes, K. (2021). Multimodality annotated
    HCC cases with and without advanced imaging segmentation (HCC-TACE-Seg)
    [Data set]. The Cancer Imaging Archive.
    https://doi.org/10.7937/TCIA.5FNA-0924
  • Moawad, A. W., et al. (2023). Multimodality annotated hepatocellular
    carcinoma data set including pre- and post-TACE with imaging segmentation.
    Scientific Data, 10, 33. https://doi.org/10.1038/s41597-023-01928-3
  • Clark, K., et al. (2013). The Cancer Imaging Archive (TCIA): Maintaining
    and Operating a Public Information Repository. Journal of Digital Imaging,
    26(6), 1045–1057. https://doi.org/10.1007/s10278-013-9622-7

[2] NIfTI形式データ(Kaggle)

License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?