2
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?

生成AIと一緒に特徴量抽出までやってみた!(PyRadiomics編)

Last updated at Posted at 2025-10-21

0. はじめに

Pythonを始めて約一年。
これまで学んできた内容を踏まえ、ChatGPTと一緒に医用画像解析の流れを整理しながら進めてみようと思います。
テーマは特徴量抽出(Feature Extraction)
画像から定量的な情報を取り出し、後の機械学習や統計解析に使うための大事なステップです。

本記事では、

生成AI(ChatGPT)と一緒にRadiomicsの特徴量抽出をPythonで実装していく流れ

を紹介します。

:pencil:Radiomicsとは?

Radiomics(ラジオミクス)とは、CTやMRIなどの医用画像から数百〜数千種類のテクスチャ特徴量を抽出し、疾患の特徴を定量的に捉える技術です。
腫瘍の悪性度や治療反応、頸動脈プラークの不安定性などを予測する研究にも広く使われています。

Radiomics解析では、大きく以下のようなステップを踏みます:

1.画像の取得(DICOMやNIfTI形式など)
2.領域(ROI)の抽出(腫瘍・プラークなどの関心領域をマスク化)
3.特徴量の計算(firstorder, GLCM, GLRLM, GLSZMなど)
4.モデル構築や統計解析(分類・予測など)

この記事では、特にステップ 3.特徴量の計算に焦点を当て、
PythonとPyRadiomicsライブラリを用いた実装を、生成AIの支援を受けながら進めていきます。

では次に、ChatGPTと一緒にどのようにコード設計を組み立てていったのかを見ていきましょう。

1. 生成AIと一緒に進めるRadiomics準備

Radiomicsの特徴量抽出を実際に動かすには、
Python環境の安定化」と「DICOM画像とマスクの対応処理」が大きな壁になります。
とはいえ、最初からすべてのコードを自力で書くのはなかなか大変。
そこで今回は ChatGPT(生成AI)をコーディングの相棒として活用しながら、一緒に進めていきました。

1.1 Python環境の安定化

最初の壁が、Python3.12 でのPyRadiomicsの依存関係
PyPIのバージョンだとSimpleITKやPyWaveletsまわりで不整合が出たため、
ChatGPTと相談しながら、一緒にColab向けの安定セットアップスクリプトを整えていきました。

# === Colab: Radiomics 安定セットアップ for Python 3.12 ===
# 1) ビルド系を最新化
!pip install --upgrade pip setuptools wheel

# 2) ipython 警告対策(jedi 不足)※Colab の古い ipython が叫ぶので入れる
!pip install "jedi>=0.16"

# 3) 数値系(3.12 安定ライン)。NumPy を 1.26.x に固定、SciPy は 1.13 系
!pip install "numpy==1.26.4" "scipy>=1.11.2,<1.14"

# 4) 画像 I/O 基盤
!pip install SimpleITK pydicom

# 5) 表と可視化
!pip install pandas matplotlib

# 6) PyRadiomics(PyPI 3.1.0 の版ズレ回避で GitHub タグから)
!pip install "pyradiomics @ git+https://github.com/AIM-Harvard/pyradiomics@v3.1.0"

# 7) PyRadiomics が内部で使う小物(足りないとき用の保険)
# pip -q install pywavelets

補足(2025年10月21日時点):セッション再起動について
このセルを実行すると、環境によっては Colab のランタイムが自動で再起動 します。
これは、Python 3.12環境で numpy や scipy、SimpleITK、PyRadiomics が
内部で使用している C++ バイナリを再リンクするための正常な挙動です。

特に Python 3.12 環境では、以下のように 2回再起動 が発生することがあります:

  1. NumPy/SciPy の再ビルド時
  2. SimpleITK/PyRadiomics の再リンク時

どちらも異常ではないので、再起動後にノートブックを再実行すればOKです。

  • 再起動が起きた場合は、次のセルからではなく
    インストール部分(1.1)から再実行してください。
  • 環境変数がリセットされるため、モジュールが見つからないことがあります。

1.2 必要なライブラリをインポート

環境構築が完了したら、まずは必要なライブラリをまとめて読み込みます。
Radiomics解析では画像処理・数値計算・表操作が中心になるため、
最初にそれらをひとまとめでインポートしておくと後のセルがスッキリします。

import os # ファイルパスの結合・管理
import numpy as np # 画像データの配列操作
import pandas as pd # Radiomics特徴量を表形式(DataFrame)で扱う
import SimpleITK as sitk # DICOM/NIfTIなど医用画像の読み込みと処理
import matplotlib.pyplot as plt # 画像の可視化(確認用)
import radiomics # 特徴量抽出のメインモジュール
from radiomics import featureextractor

1.3 Google Drive のマウント

特徴量を抽出した後にCSVを保存するため、ここでGoogle Driveをマウントします。

# === Google Driveのマウント ===
from google.colab import drive
drive.mount('/content/drive')

では次に、PyRadiomicsのサンプルデータ(MRI画像とマスク)を使って、
一緒に特徴量抽出を動かしていきましょう。

2. サンプル取得 & 読み込み & 表示

PyRadiomicsには、動作確認用のサンプルデータがいくつか同梱されています。
その中の brain1 は、MRI画像と対応するマスク(腫瘍領域)がペアになっており、
自分のDICOMデータを使う前にコードを試すのに最適です。

2.1 サンプルデータの取得

PyRadiomicsのgetTestCase()関数を使うと、指定したサンプル名のデータパスを自動で取得できます。
ここでは "brain1" を指定します。

# サンプル取得 & 読み込み(brain1:MRI + マスク)
img_path, msk_path = radiomics.getTestCase("brain1")
print("MRI image path:", img_path)
print("MRI Mask path :", msk_path)

出力例:

MRI image path: /tmp/pyradiomics/data/brain1_image.nrrd
Mask path     : /tmp/pyradiomics/data/brain1_label.nrrd

2.2 画像とマスクの読み込み

SimpleITKを使って画像とマスクを読み込みます。
マスク画像は「0/1の二値化」を行い、画像と同じ空間情報(解像度・原点・方向)をコピーしておきます。
これを忘れると、後のRadiomics抽出で位置ずれが起きることがあるため要注意です。

# 画像
img3d = sitk.ReadImage(img_path)

# ★マスクは“>0 を 1”で二値化 → UInt8 にキャスト → 幾何情報をコピー
raw_mask = sitk.ReadImage(msk_path)
mask3d   = sitk.Cast(raw_mask > 0, sitk.sitkUInt8)
mask3d.CopyInformation(img3d)

ポイント

  • 医用画像では、マスクの型や空間情報(spacing, origin, direction)がずれると
    抽出に失敗します。
  • CopyInformation() を使うことで、画像とマスクを同一座標系に統一できます。
  • マスクをsitk.sitkUInt8に変換しておくのは、PyRadiomicsが「整数ラベル画像」を
    前提としているためです。

次のステップでは、このMRI画像とマスクを一緒に可視化して、正しく対応しているか
確認してみましょう。

2.3 原画像とマスクの確認表示

特徴量の抽出の前に、画像とマスクが正しく対応しているかを確認しておきましょう。
PyRadiomicsは座標整合性に厳しいため、ここでズレを見落とすと後で「No labels found」などのエラーになります。
SimpleITKで読み込んだデータは3D配列(Z×Y×X)になっているため、マスクが含まれるスライスの中から中央付近を選んで可視化します。

# === 原画像とマスク(白黒)を確認表示 ===
arr_img = sitk.GetArrayFromImage(img3d)
arr_msk = sitk.GetArrayFromImage(mask3d)

# マスクがあるスライスの中から1枚(真ん中あたり)を選ぶ
nz_slices = [z for z in range(arr_msk.shape[0]) if arr_msk[z].any()]
if not nz_slices:
    raise RuntimeError("マスクが空です(全スライス0)。")
z_show = nz_slices[len(nz_slices)//2]

plt.figure(figsize=(10,5))
plt.subplot(1,2,1); plt.imshow(arr_img[z_show], cmap='gray'); plt.title(f"MRI (z={z_show})"); plt.axis('off')
plt.subplot(1,2,2); plt.imshow(arr_msk[z_show], cmap='gray');  plt.title("Binary Mask");   plt.axis('off')
plt.show()

出力例:
image.png
左がMRI、右がマスクの白黒画像です。
マスク領域がしっかり抽出されていればOKです。

ポイント

  • nz_slices で「マスクが存在するスライス」だけを抽出しているので、
    空スライスは自動的にスキップされます。

この工程を終えれば、画像とマスクの整合性が保証された状態になります。
次は、いよいよ特徴量抽出のメイン処理(関数定義)に進みましょう。

3. 特徴量抽出関数の定義

さて、ここからが本番です。これまで準備した環境とデータを使って、一緒に特徴量を抽出していきましょう。
この関数部分は、ChatGPTに「2DスライスごとにRadiomics特徴量を抽出したい」と相談して、その提案をベースに自分のデータ構造に合わせて調整しています。
実際の処理では、PyRadiomicsの RadiomicsFeatureExtractor を使って、
MRI画像とマスクから特徴量を抽出していきます。
今回は「2D / Original」設定で、各スライスごとにフィルタをかけない生データ(Original)の特徴量をすべて算出します。
他にも「Wavelet」や「Laplacian of Gaussian (LoG)」などのフィルタも指定できます。

# === 抽出関数(2D/Original/全特徴量) ===
def _extract_xy_slice(sitk_img, z):
    # SimpleITKの3DからXY平面の2Dスライスを安全に切り出す(Extract使用)
    size  = list(sitk_img.GetSize())      # (X, Y, Z)
    index = [0, 0, z]
    size[2] = 0                           # Z方向サイズを0にして2D抽出
    return sitk.Extract(sitk_img, size=size, index=index)

def extract_radiomics_2d_original(case_id, image_path, mask_path, side=None, label=None):
    # 画像/マスク読込(マスクは二値化)
    img3d  = sitk.ReadImage(image_path)
    raw_m  = sitk.ReadImage(mask_path)
    mask3d = sitk.Cast(raw_m > 0, sitk.sitkUInt8)
    mask3d.CopyInformation(img3d)

    # 形状チェック
    if sitk.GetArrayFromImage(img3d).shape != sitk.GetArrayFromImage(mask3d).shape:
        raise ValueError("Image と Mask の形状が一致しません。")

    # 抽出器設定
    ext = featureextractor.RadiomicsFeatureExtractor(force2D=True, label=1)
    ext.disableAllFeatures();  ext.enableAllFeatures()
    ext.disableAllImageTypes(); ext.enableImageTypes(Original={})
    ext.enableFeaturesByName(shape2D=[])

    # スライスごと抽出
    rows = []
    for z in nz_slices:
        # ★ ここが重要:Extractで2D切り出し(インデクススライスから置き換え)
        i2d = _extract_xy_slice(img3d, z)
        m2d = _extract_xy_slice(mask3d, z)

        # 念のため:このスライスのラベル確認(0/1以外は0/1に丸める)
        m2d = sitk.Cast(m2d > 0, sitk.sitkUInt8)

        # 実行
        res = ext.execute(i2d, m2d, label=1)
        res = {k: v for k, v in res.items() if not k.startswith("diagnostics_")}
        res.update({"case_id": case_id, "z_index": z, "slice_no": z+1})
        if side is not None:  res["side"]  = side
        if label is not None: res["LABEL"] = int(label)
        rows.append(res)

    df = pd.DataFrame(rows)
    idx = ["case_id"] + (["side"] if "side" in df.columns else []) + ["slice_no"]
    return df.set_index(idx)
使用した関数のポイント解説
処理箇所 解説
_extract_xy_slice() SimpleITK.Extract() を使って、Zインデックス指定で2Dスライスを安全に切り出す関数。直接スライスするよりも座標系の破綻が起きにくい。
二値化 (sitk.Cast(raw_m > 0, sitk.sitkUInt8)) Radiomicsは「ラベル画像(整数)」を前提とするため、必ず0/1に丸めて型変換。
force2D=True 各スライスを2D平面として処理。断面ベースで特徴量を抽出する設定。
enableImageTypes(Original={}) Original画像のみ対象。ここをWavelet={}などに変更・追加すればフィルタ別特徴も抽出可。
diagnostics_ 除去 PyRadiomicsが出力するログ系項目(解析パスやSpacing情報など)を除外してクリーンに。
slice_no 列の追加 スライス番号を後で追跡しやすくするために手動で追加。

ポイント

  • CopyInformation() を行っておくことで、SimpleITKとPyRadiomicsが内部で
    空間情報を正しく引き継ぎます。
  • 特徴量数は、ImageType(Original/Wavelet)
    FeatureClass(GLCM, GLRLM, etc.)の組み合わせで変化します。

次のステップでは、定義した関数を実際に呼び出して特徴量を抽出し、CSVに保存する処理を行います。

4. 実行と出力の確認

いよいよ、ChatGPTと一緒に作ってきた関数を実際に動かしてみましょう。
先ほど定義した extract_radiomics_2d_original() 関数を呼び出して、
サンプルMRI画像(brain1)からスライスごとのRadiomics特徴量を計算してみましょう。

# === 実行:2D/Original/全特徴量を抽出 → CSV保存 ===
df = extract_radiomics_2d_original(
    case_id="brain1_MRI",
    image_path=img_path,
    mask_path=msk_path,
    side="N/A",
    label=None
)

if df is None or df.empty:
    raise RuntimeError("特徴量が抽出されませんでした。")

print("行数(スライス数):", df.shape[0])
print("列数(特徴量数)  :", df.shape[1])
display(df.head(3))

out_csv = "/content/brain1_MRI_radiomics_2D_original.csv" # CSVで保存
df.to_csv(out_csv, encoding="utf-8-sig")
print(f"[OK] CSV保存: {out_csv}")

出力例:

行数(スライス数): 7
列数(特徴量数)  : 103

| case_id    | slice_no | original_shape2D_Elongation | original_shape2D_MajorAxisLength | original_shape2D_MaximumDiameter | ... |
| :--------- | :------- | --------------------------: | -------------------------------: | -------------------------------: | --: |
| brain1_MRI | 12       |                    0.692855 |                        24.167566 |                27.08583722399955 | ... |
| brain1_MRI | 13       |                    0.249215 |                        62.780606 |                47.96900447815234 | ... |
| brain1_MRI | 14       |                    0.440320 |                        36.883648 |                36.57718722099882 | ... |

3 rows × 103 columns
[OK] CSV保存: /content/brain1_MRI_radiomics_2D_original.csv

先ほどの3.特徴量抽出関数の定義でDataFrameも指定しました。そのためDataFrameの先頭を表示すると上記のような表が得られます。

ポイント

  • 各行はスライス単位の特徴量。
  • 各列はRadiomics特徴量(例:GLCM, GLSZM, First Orderなど)。
  • case_idやslice_noをインデックスにしておくことで、後で症例単位・断面単位で
    統合解析がしやすくなります。

今回は上記の表をCSVでも保存するように指定しました。
CSVは指定したパス(ここでは/content/brain1_MRI_radiomics_2D_original.csv)に保存されます。
しかし、このままではランタイムの接続とともにファイルが消えてしまいます。
抽出結果を残したい場合は、Drive内に保存用フォルダを作成し、そこに出力しましょう。
今回はMyDrive直下にRadiomics_projectというフォルダを作り、そこに保存します。

# Drive内に保存用フォルダを作成
save_dir = "/content/drive/MyDrive/Radiomics_project"
os.makedirs(save_dir, exist_ok=True)

# 出力先を指定して保存
out_csv = os.path.join(save_dir, "brain1_MRI_radiomics_2D_original.csv")
df.to_csv(out_csv, encoding="utf-8-sig")
print(f"[OK] Driveに保存しました: {out_csv}")

ポイント

  • PyRadiomicsは一度に1000以上の特徴量を出力します。
  • 目的に応じて、後で特徴量選択(Feature Selection)を行い
    モデル構築を行うことが一般的です。
  • CSV形式で保存しておくと後の流れがスムーズです。

注意

  • /content 直下に保存した場合 → ランタイム終了で消失
  • /content/drive/... に保存した場合 → Driveに残り、永続化される

5. まとめ

今回は、生成AIと一緒にRadiomicsの特徴量抽出を実装してみました。
ChatGPTにはコードの骨格づくりを手伝ってもらい、細かな部分は自分で検証しながら調整しました。
AIは万能ではありませんが、考え方の整理役壁打ち相手として使うと、開発がぐっと進めやすくなります。


:tools: 実行環境

実行環境
項目 内容
実行環境 Google Colab(GPUなし)
Python 3.12.0
PyRadiomics v3.1.0(GitHub タグ版)
NumPy 1.26.4
SciPy 1.13.x
SimpleITK v2.3系
pydicom v2.4系
pandas / matplotlib 最新安定版
versioneer 最新版に更新済(互換性対策)

:book: References (最終更新 : 2025/10/21)


補足(バージョン互換性について)
本記事で使用している PyRadiomics v3.1.0 タグ版は、Python 3.12 環境でも動作確認済みですが、
公式では Python 3.12 以降の完全対応がまだ明記されていません。
環境によっては追加調整(例:versioneer のアップデート)が必要となる場合があります。
公式の安定ラインは Python 3.7〜3.10 程度とされています。

本記事は、自分の学習・研究記録も兼ねてまとめています。
内容に誤りや補足すべき点などありましたら、ぜひコメントやフィードバックで教えてください。
一緒により良い解析手順にブラッシュアップしていければと思います。

2
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
2
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?