0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

「もう一人のあなた」をPythonで作る ― DICOMからデジタルツイン・ペイシェント最小実装ガイド

0
Posted at

はじめに

「手術してみないと分からない」――現代医療がずっと抱えてきたこの不確実性を、根本から覆す技術が デジタルツイン・ペイシェント(Digital Twin Patient) です。CT・MRI・心電図・血液検査・遺伝子情報までを統合し、患者の 生理学的な振る舞いごと 仮想空間に複製する。執刀前夜、医師は「もう一人のあなた」で100通りの手術リハーサルを行う――そんな未来が、もう動き始めています。

「でもデジタルツイン医療って、GE・Philips・Siemensと国家プロジェクトの世界では?」――いえ、Python + OSS(pydicom / SimpleITK / TotalSegmentator / PyVista / SciPy) を組み合わせれば、 DICOM画像から3D臓器モデルを自動生成し、簡易PK/PDで薬物反応をシミュレートする までを手元のPCで体験できます。

本記事では、note記事のテーマを開発者目線で落とし込み、Pythonでデジタルツイン・ペイシェントの最小ひな形を構築する具体的な手順 を解説します。

参考にした解説記事:手術前夜、医師は「もう一人のあなた」を執刀する——デジタルツイン・ペイシェントが命のリスクを消す | AI Robotics Quantum Lab

ゴールは以下のとおりです。

  • DICOMボリュームを 読み込み・前処理・3D配列化 する
  • AIモデル(TotalSegmentator)で 臓器を自動セグメンテーション する
  • セグメントから メッシュ(STL)と3D可視化 を生成する
  • SciPy ODEで 薬物動態(PK/PD)の個別シミュレーション を行う
  • 規制・倫理・品質管理の現実的な注意点を押さえる

対象読者は、Pythonと簡単な機械学習・科学計算に触れたことがあるエンジニア・データサイエンティストです。臨床知識の前提は最小限でOKです。


1. 全体像 ― 何を作るのか

[DICOM (CT/MRI)] ──► [pydicom / SimpleITKで読込・前処理]
                          │
                          ▼
              [TotalSegmentator (AIセグメンテーション)]
                          │
                          ▼
              [vtk / trimeshでメッシュ化+PyVista可視化]
                          │
                          ▼
              [患者パラメータ(体重・eGFRなど)]
                          │
                          ▼
              [SciPy ODE: 2-Compartment PK/PDモデル]
                          │
                          ▼
              [Streamlit ダッシュボードで統合表示]

2. 環境構築

2.1 Pythonと仮想環境

mkdir digital-twin-lab && cd digital-twin-lab
python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip

2.2 必要ライブラリ

# 医療画像
pip install pydicom SimpleITK nibabel

# AIセグメンテーション(PyTorchベース)
pip install torch torchvision           # 環境に合わせてCUDA版を
pip install TotalSegmentator            # 全身臓器セグメンテーション

# 3Dメッシュと可視化
pip install vtk pyvista trimesh

# 数値計算・可視化
pip install numpy scipy pandas matplotlib

# UI(オプション)
pip install streamlit

⚠️ TotalSegmentatorはGPU推奨です。CPUでも動きますが、1ボリュームあたり数十分かかることがあります。

2.3 プロジェクト構成

digital-twin-lab/
├── data/
│   ├── dicom/          # 元DICOM
│   └── nifti/          # 変換後NIfTI
├── outputs/
│   ├── seg/            # セグメンテーション結果
│   └── mesh/           # STL
├── src/
│   ├── io_dicom.py
│   ├── segment.py
│   ├── mesh.py
│   ├── pkpd.py
│   └── viewer.py
└── app.py              # Streamlitダッシュボード

2.4 公開DICOMデータの入手

検証用には、The Cancer Imaging Archive (TCIA) などが公開している匿名化済みCT/MRIが便利です。プロジェクト初期は 3D Slicer に同梱の MRHead などからNIfTI形式で出力したものでもOKです。

⚠️ 患者由来データは原則として 倫理審査・同意 を経て扱います。学習・検証では公開データセット+ライセンスをまず確認してください。


3. DICOMの読み込みと前処理

3.1 シリーズ単位で読み込む

# src/io_dicom.py
from pathlib import Path
import SimpleITK as sitk


def read_dicom_series(dicom_dir: str) -> sitk.Image:
    reader = sitk.ImageSeriesReader()
    series_ids = reader.GetGDCMSeriesIDs(dicom_dir)
    if not series_ids:
        raise RuntimeError(f"No DICOM series under {dicom_dir}")
    files = reader.GetGDCMSeriesFileNames(dicom_dir, series_ids[0])
    reader.SetFileNames(files)
    image = reader.Execute()
    return image


def to_nifti(image: sitk.Image, out_path: str) -> Path:
    out = Path(out_path)
    out.parent.mkdir(parents=True, exist_ok=True)
    sitk.WriteImage(image, str(out))
    return out


def normalize_hu(image: sitk.Image, lower: int = -1000, upper: int = 1000) -> sitk.Image:
    arr = sitk.GetArrayFromImage(image).astype("float32")
    arr = arr.clip(lower, upper)
    arr = (arr - lower) / (upper - lower)  # [0,1]
    out = sitk.GetImageFromArray(arr)
    out.CopyInformation(image)
    return out

3.2 NIfTI化(後段のAIモデルが扱いやすい)

# 例
img = read_dicom_series("data/dicom/case01")
to_nifti(img, "data/nifti/case01.nii.gz")
print("size:", img.GetSize(), "spacing:", img.GetSpacing())

spacing(ボクセルの物理サイズ)と direction を保持したまま処理するのが鉄則です。後で実寸の体積・距離を計算する際に必須になります。


4. AIで臓器を自動セグメンテーション

TotalSegmentator は、CTから100以上の解剖構造(肝臓、腎臓、心臓、血管…)を自動でラベル付けできるOSSモデルです。

4.1 CLIで動かす(最短)

TotalSegmentator -i data/nifti/case01.nii.gz -o outputs/seg/case01

outputs/seg/case01/ に各臓器ごとの NIfTI ラベルマップが書き出されます(liver.nii.gz, heart.nii.gz, ...)。

4.2 PythonからAPIで呼ぶ

# src/segment.py
from pathlib import Path
from totalsegmentator.python_api import totalsegmentator


def segment(input_nifti: str, output_dir: str, fast: bool = True) -> Path:
    out = Path(output_dir)
    out.mkdir(parents=True, exist_ok=True)
    totalsegmentator(input_nifti, str(out), fast=fast)
    return out
segment("data/nifti/case01.nii.gz", "outputs/seg/case01", fast=True)

fast=True は精度を少し落とす代わりに高速。デモ・教育用にはこちらで十分です。


5. セグメンテーションをメッシュ化する

3Dビューアで「執刀リハーサル」感を出すために、ラベルマップを STLメッシュ に変換します。

# src/mesh.py
from pathlib import Path
import numpy as np
import SimpleITK as sitk
import pyvista as pv


def label_to_mesh(label_nifti: str, out_stl: str, smoothing_iters: int = 30) -> Path:
    img = sitk.ReadImage(label_nifti)
    arr = sitk.GetArrayFromImage(img)  # (Z, Y, X)
    spacing = img.GetSpacing()         # (X, Y, Z)
    origin = img.GetOrigin()

    # PyVista用にXYZ順に並べ替え
    grid = pv.ImageData(
        dimensions=np.array(arr.shape[::-1]) + 1,
        spacing=spacing,
        origin=origin,
    )
    grid.cell_data["values"] = arr.flatten(order="F")

    surf = grid.contour([0.5], scalars="values", method="contour")
    surf = surf.smooth(n_iter=smoothing_iters, relaxation_factor=0.1)

    out = Path(out_stl)
    out.parent.mkdir(parents=True, exist_ok=True)
    surf.save(out)
    return out

5.1 心臓と肝臓を可視化

# src/viewer.py
import pyvista as pv


def show_organs(mesh_paths: dict[str, str], screenshot: str | None = None):
    plotter = pv.Plotter(off_screen=screenshot is not None)
    palette = {
        "heart": "red",
        "liver": "saddlebrown",
        "kidney_left": "darkorange",
        "kidney_right": "darkorange",
    }
    for name, path in mesh_paths.items():
        mesh = pv.read(path)
        plotter.add_mesh(mesh, color=palette.get(name, "lightgray"),
                         opacity=0.7, smooth_shading=True, label=name)
    plotter.add_legend()
    if screenshot:
        plotter.screenshot(screenshot)
    else:
        plotter.show()
show_organs({
    "heart": "outputs/mesh/case01/heart.stl",
    "liver": "outputs/mesh/case01/liver.stl",
}, screenshot="outputs/mesh/case01/preview.png")

これで「患者個別の3D臓器ビュー」が完成です。Blender / 3D Slicer に持ち込めば、術前カンファレンスのための表示も作れます。


6. 薬物動態(PK/PD)を個別シミュレーションする

「患者個別の薬の反応」を、教科書的な2-コンパートメントモデル で簡易再現します。

6.1 数式

dC1/dt = -(k10 + k12) * C1 + k21 * C2 + (Dose / Vd if t in dosing window else 0)
dC2/dt = k12 * C1 - k21 * C2
  • C1:血漿中濃度
  • C2:末梢組織中濃度
  • k10:消失(腎・肝排泄)。eGFRや体重で調整
  • Vd:分布容積。体重に応じて調整

6.2 実装

# src/pkpd.py
from dataclasses import dataclass
import numpy as np
from scipy.integrate import solve_ivp


@dataclass
class PatientParams:
    weight_kg: float = 60.0
    eGFR: float = 90.0           # mL/min/1.73m^2
    age: int = 50


def two_compartment_dose_response(
    params: PatientParams,
    dose_mg: float = 100.0,
    interval_h: float = 8.0,
    n_doses: int = 6,
    sim_hours: float = 72.0,
):
    Vd = 0.6 * params.weight_kg                 # L (粗い近似)
    k10 = 0.10 * (params.eGFR / 90.0)           # eGFR連動
    k12, k21 = 0.05, 0.04

    def dosing_rate(t):
        # 各dose時刻 ±0.1h のあいだだけ短期間で投与
        for i in range(n_doses):
            t0 = i * interval_h
            if t0 <= t < t0 + 0.1:
                return dose_mg / 0.1 / Vd
        return 0.0

    def rhs(t, y):
        C1, C2 = y
        dC1 = -(k10 + k12) * C1 + k21 * C2 + dosing_rate(t)
        dC2 = k12 * C1 - k21 * C2
        return [dC1, dC2]

    sol = solve_ivp(rhs, (0, sim_hours), [0.0, 0.0],
                    max_step=0.05, dense_output=True)

    t = np.linspace(0, sim_hours, 1000)
    C1, C2 = sol.sol(t)
    return t, C1, C2

6.3 個人差を比較する

import matplotlib.pyplot as plt
from src.pkpd import PatientParams, two_compartment_dose_response

cases = [
    ("Healthy 60kg eGFR90", PatientParams(60, 90, 40)),
    ("Elderly 50kg eGFR45", PatientParams(50, 45, 78)),
    ("Athlete 80kg eGFR110", PatientParams(80, 110, 30)),
]

for label, p in cases:
    t, C1, _ = two_compartment_dose_response(p)
    plt.plot(t, C1, label=label)

plt.xlabel("Time [h]")
plt.ylabel("Plasma conc.  [mg/L]")
plt.legend()
plt.grid(True)
plt.title("Personalized PK simulation")
plt.savefig("outputs/pk_compare.png", dpi=150)

「同じ用量・同じ間隔でも、人によって血中濃度の山と谷が大きく違う」ことが見えるはずです。これが 個別化医療の最初の手触り です。


7. Streamlitで「ツインダッシュボード」を作る

# app.py
import streamlit as st
import pyvista as pv
from stpyvista import stpyvista

from src.pkpd import PatientParams, two_compartment_dose_response


st.set_page_config(layout="wide", page_title="Digital Twin Patient")
st.title("Digital Twin Patient – Demo")

col1, col2 = st.columns(2)

with col1:
    st.subheader("Anatomy")
    plotter = pv.Plotter(window_size=[400, 400])
    mesh = pv.read("outputs/mesh/case01/heart.stl")
    plotter.add_mesh(mesh, color="red", opacity=0.7)
    stpyvista(plotter)

with col2:
    st.subheader("Pharmacokinetics")
    weight = st.slider("Weight (kg)", 40, 100, 60)
    egfr = st.slider("eGFR", 30, 120, 90)
    age = st.slider("Age", 20, 90, 50)
    dose = st.slider("Dose (mg)", 50, 500, 100)

    params = PatientParams(weight, egfr, age)
    t, C1, _ = two_compartment_dose_response(params, dose_mg=dose)
    st.line_chart({"Plasma": C1}, x_label="hours", y_label="mg/L")
streamlit run app.py

これで、3D臓器ビュー+個別パラメータ可変のPKシミュレータ がブラウザで動く、最小デジタルツイン・ダッシュボードが完成します。


8. 流体力学(CFD)連携への発展

血流・心拍動の物理シミュレーションは、本物のデジタルツイン医療の核心です。

  • SimVascular:血管モデルから血流CFDを行うOSS(Pythonバインディング有)
  • FEniCS / Firedrake:偏微分方程式の汎用ソルバ。血流・組織変形に応用可
  • OpenFOAM:商用級CFD。心血管系のチュートリアル多数
  • lattice Boltzmann (LBM) で軽量に血管内血流を可視化する論文例もあり

最初の一歩として「メッシュ化した血管にPoiseuille近似で流れを乗せる」だけでも、十分に学習効果のある可視化ができます。


9. 規制・倫理・運用上の注意

  • 日本で医療機器ソフトとして使う場合、SaMD(Software as a Medical Device) として薬機法の対象になります(プログラム医療機器)。研究用途と医療用途は厳密に切り分けてください。
  • 患者データは 個人情報保護法・改正法・倫理指針 に従って扱います。匿名加工と再識別リスクの評価が必須。
  • AIセグメンテーションの結果は 誤りが起こり得る前提 で運用する必要があります。臨床利用では必ず医師レビュー(ヒューマン・イン・ザ・ループ)を残しましょう。
  • データガバナンス:DICOMの匿名化(patient name / IDなど)、保管暗号化、アクセスログを必ず設計に含める。
  • 「シミュレーション結果=臨床的真実」ではない、ということを UI上で明示 する設計(disclaimer、信頼度表示)が安全です。

10. 拡張アイデア

  • マルチモーダル統合:CT+MRI+心電図+ウェアラブル(歩行、心拍)を1つのツインに束ねる
  • PINN(Physics-Informed Neural Network) で患者個別の弾性・粘性パラメータを推定
  • 強化学習で術前リハーサルにおける「最適切開順」を探索
  • VR/AR連携:Unity / Unreal で執刀医がツインに「触れる」術前カンファ
  • 連合学習(Federated Learning) で病院間でモデル共有しつつ、生データは外に出さない

まとめ

  • デジタルツイン・ペイシェントは「巨大医療機器メーカーだけの話」ではなく、OSSとPython で個人開発者がプロトタイプできる領域に下りてきている
  • 本記事のひな形で、DICOM読込 → AI臓器セグメンテーション → メッシュ化+3D可視化 → 個別PKシミュレーション → Streamlitダッシュボード までの一連を体験できる
  • 本格的にはCFD・PINN・SaMD認証など壁が増えるが、その壁の前段階を 手元で動かして学べる のは大きな強み
  • 「あなたの身体のすべてを、あなたより先に理解している存在」を作る最初の歯車は、エンジニアのノートPCで回せる

まずは公開DICOMを1つ落として、心臓の3Dを描き出すところから始めてください。そこから先、デジタルツイン医療への解像度は驚くほど一気に上がります。

0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?