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?

wavefront obj -> Animation GIF - blender を python で操作する -

0
Posted at

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くらいがちょうどいいバランスです。

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?