はじめに
「手術してみないと分からない」――現代医療がずっと抱えてきたこの不確実性を、根本から覆す技術が デジタルツイン・ペイシェント(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を描き出すところから始めてください。そこから先、デジタルツイン医療への解像度は驚くほど一気に上がります。