tl;dr
- DICOM画像を wavefront obj に変換したあと、blender を python で操作し rendering することで animation GIF にした
- code は claude sonnet 4.6 製
DICOM画像
- 脳のMRI画像を利用
DICOM->wavefront obj
* https://weblog.shak.jp/2020/06/%e8%84%b3%e3%81%aedicom%e3%83%87%e3%83%bc%e3%82%bf%e3%81%8b%e3%82%893d%e3%83%97%e3%83%aa%e3%83%b3%e3%83%88%e5%8f%af%e8%83%bd%e3%81%aa%e3%83%a2%e3%83%87%e3%83%ab%e3%82%92%e4%bd%9c%e6%88%90%e3%81%99/ を参考に
* MRIcroGLで NIFTI形式へ
* BrainSuite で 変換 を行いました。
* たくさんファイルがあったのですが T1 axi と書いてあったものがうまくいきました。
xxxxx_T1_FLAIR_axi_xxxxx_15.pial.cortex.obj
変換
- rocky10 on wsl2 で行いました
- 実測値
# CPU: Core ultra 7 265KF
# GPU: 5070Ti
# CPU only: 1枚5秒程度
# CUDA 有効: 1枚3秒程度
つまり 120枚の画像生成で5-10分程度
# パラメタ
cycles.samples = 128 # サンプル数(品質)。上げると重い
RESOLUTION = 800 # 出力解像度(px)正方形
FRAMES = 120 # 総フレーム数。24fps時: 72=3秒, 120=5秒
script
brain_gif_final.py
import bpy
import math
from mathutils import Vector
# 使い方
# blender -b -P brain_gif_final.py
#
# ffmpeg -framerate 24 -i frames/frame_%04d.png \
# -vf "fps=24,scale=800:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
# -loop 0 brain.gif
# ============================================================
# 設定
# ============================================================
OBJ_PATH = "xxxxx_T1_FLAIR_axi_xxxxx_15.pial.cortex.obj"
OUTPUT_DIR = "./frames/frame_"
RESOLUTION = 800 # 出力解像度(px)正方形
CAM_DIST = 250.0 # カメラと脳中心の距離。小さいほど拡大
FRAMES = 120 # 総フレーム数。24fps時: 72=3秒, 120=5秒
ELEVATION_DEG = 90 # 仰角の振れ幅(度)。30=緩やか / 60=標準 / 90=真上〜真下
# ============================================================
# 1. シーンのリセット
# ============================================================
bpy.ops.wm.read_factory_settings(use_empty=True)
# ============================================================
# 2. OBJ読み込み・中心計算
# ============================================================
bpy.ops.wm.obj_import(filepath=OBJ_PATH)
brain = bpy.context.selected_objects[0]
center = sum(
[brain.matrix_world @ Vector(v) for v in brain.bound_box],
Vector()
) / 8
# ============================================================
# 3. マテリアル
# ============================================================
mat = bpy.data.materials.new("BrainMat")
mat.use_nodes = True
bsdf = mat.node_tree.nodes['Principled BSDF']
bsdf.inputs[0].default_value = (0.75, 0.75, 0.75, 1) # ベースカラー(グレー)
bsdf.inputs[7].default_value = 0.3 # Roughness: 0=鏡面 / 1=完全拡散
brain.data.materials.append(mat)
# ============================================================
# 4. 背景(World)
# ※ factory_settings後はworldがNoneになるため明示的に作成が必要
# ============================================================
world = bpy.data.worlds.new("World")
bpy.context.scene.world = world
world.use_nodes = True
bg = world.node_tree.nodes['Background']
bg.inputs[0].default_value = (0.35, 0.35, 0.35, 1) # 背景色(中グレー)
bg.inputs[1].default_value = 1.5 # 環境光の強度
# ============================================================
# 5. ライト(2灯)
# ============================================================
# キーライト: 斜め上から強く照らすメインライト
bpy.ops.object.light_add(type='AREA',
location=(center[0]-300, center[1]-300, center[2]+500))
key = bpy.context.object
key.data.energy = 50000 # 強度(W)
key.data.size = 200 # 光源サイズ。大きいほどソフトな影
key.rotation_euler = (math.radians(40), math.radians(-35), 0)
# フィルライト: 影側を補助する弱いライト
bpy.ops.object.light_add(type='AREA',
location=(center[0]+300, center[1]+200, center[2]+200))
fill = bpy.context.object
fill.data.energy = 30000 # キーの60%程度が自然
fill.data.size = 300
# ============================================================
# 6. カメラ(TRACK_TOで脳を自動追従)
# ============================================================
bpy.ops.object.empty_add(type='PLAIN_AXES', location=center)
target = bpy.context.object
bpy.ops.object.camera_add(location=center + Vector((0, -CAM_DIST, 0)))
cam = bpy.context.object
bpy.context.scene.camera = cam
constraint = cam.constraints.new(type='TRACK_TO')
constraint.target = target
constraint.track_axis = 'TRACK_NEGATIVE_Z'
constraint.up_axis = 'UP_Y'
# ============================================================
# 7. カメラ周回アニメーション(全軸回転)
# azimuth: Z軸周回(水平方向)
# elevation: X軸傾き(上下方向)→ sin波で自然に変化
# ============================================================
scene = bpy.context.scene
scene.frame_start = 1
scene.frame_end = FRAMES -1 # 最終フレームを重複しないように
for i in range(FRAMES - 1):
t = i / (FRAMES - 1) # 0.0 〜 1.0 に正規化(119で割る)
azimuth = 2 * math.pi * t
elevation = math.sin(2 * math.pi * t) * math.radians(ELEVATION_DEG)
x = center[0] + CAM_DIST * math.cos(elevation) * math.sin(azimuth)
y = center[1] - CAM_DIST * math.cos(elevation) * math.cos(azimuth)
z = center[2] + CAM_DIST * math.sin(elevation)
cam.location = (x, y, z)
cam.keyframe_insert(data_path="location", frame=i + 1)
# ============================================================
# 8. GPU設定(CUDA優先、失敗時はEEVEE)
# ============================================================
try:
prefs = bpy.context.preferences.addons['cycles'].preferences
prefs.compute_device_type = 'CUDA'
prefs.refresh_devices()
for device in prefs.devices:
device.use = True
bpy.context.scene.render.engine = 'CYCLES'
bpy.context.scene.cycles.device = 'GPU'
bpy.context.scene.cycles.denoiser = 'OPENIMAGEDENOISE'
bpy.context.scene.cycles.samples = 128 # サンプル数(品質)。上げると重い
bpy.context.scene.cycles.use_denoising = True # AI denoising(samplesを下げても綺麗に)
bpy.context.scene.cycles.tile_size = 4096 # GPU向けは大きいタイルが効率的
print("🚀 GPU(CUDA)有効")
except:
print("CPU でレンダリング開始")
bpy.context.scene.cycles.device = 'CPU'
# ============================================================
# 9. レンダリング
# ============================================================
scene.render.resolution_x = RESOLUTION
scene.render.resolution_y = RESOLUTION
scene.render.filepath = OUTPUT_DIR
scene.render.image_settings.file_format = 'PNG'
bpy.ops.render.render(animation=True)
実行
mkdir -p frames
blender -b -P brain_gif_final.py
ffmpeg -framerate 24 -i frames/frame_%04d.png \
-vf "fps=24,scale=800:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
brain.gif
animation GIF パラメタ
軽量化オプション
| 方法 | コマンド変更 | サイズ目安 |
|---|---|---|
| 解像度を下げる | scale=400:-1 |
約2〜3MB |
| フレームレート下げる | fps=15 |
約5MB |
| 両方 | fps=15,scale=400:-1 |
約1〜2MB |
| フレーム数減らす | FRAMES=48 |
約7MB |
例
# 解像度400px + 15fps
ffmpeg -framerate 24 -i frames/frame_%04d.png \
-vf "fps=15,scale=400:-1:flags=lanczos,split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse" \
brain_small.gif
約2〜3MBに収まります。Qiitaの推奨は5MB以下なのでscale=500くらいがちょうどいいバランスです。