今回は3Dモデリングソフトで作成したオブジェクトの頂点情報を2D画像に変換する方法を説明します。この画像はシミュレーションのファントムや線源の配置などに利用できます。
STL→2D stack TIFF画像です。
※2D stack TIFF画像→STLの話は含まれません。
3Dオブジェクトの作成
適当に3Dオブジェクトを作成します。
今回は無料で3Dモデリングができるblender ver.3.2.1を使用します。
※2024/9/11の時点で4.2.1がリリースされていますので、やり方が変化している可能性もあります。
また、日本語表示にしています。日本語表示に切り替える方法はこちらを参考にしてください。
まず、blenderを起動したら、初めから用意されているカメラ、立方体、ライトを選択してdeleteします。
次に物体を作成します。今回は球の中に小さな球が2つ入っているようなオブジェクトを作ります。適宜、作りたいものに変更してください。
Shiftを押しながらAを押すと、メニューが開かれますので、メッシュの中のUV球を選択します。
球が表示されたら、そのオブジェクトの情報を表示するため右上の「<」をクリックしてウィンドウを表示させます。
次に球の寸法を変えます。
今回はx, y, z座標ともに30cmの球とします。
サイズを変更すると表示が小さくなったり、大きくなったりするのでマウスホイールを回転させて表示を調整してください。
手のマークを左クリックしながらマウスを動かすと平行移動ができるので、都度、調整します。
右上のアイコンをクリックするとワイヤーフレーム表示にすることができます。
今回は球を3つ作りたいので、コピー&ペーストします。
右上の球を選択してCntl+Cでコピー、Cntl+Vでペーストします。
全く同じ球が作成され、完全に重なっている状態なので、コピーできていないように感じるかもしれませんが、右上に球001と新しいオブジェクトができていることが確認できます。
球001のサイズをx, y, z = 5cm, 5cm, 5cmとします。
位置をy方向に10cm移動させます。
最後の球を作成します。
球001をコピー&ペーストして、位置をy=10cmからy=-10cmに変更します。
今度はメッシュの細かさを変更します。
今回保存するのは、頂点の情報なので、頂点が繋がって線ができるくらい細かくします。
一番大きな球を選択した後、右下の工具マーク(モディファイア―プロパティ)をクリックします。
モディファイア―を追加をクリックします。
リメッシュを選択し、ボクセルを選択した状態で、ボクセルサイズを小さくしてください。
下の画像では0.01mとしていますが、後々、微調整したところ0.007mが最適でした。
最後に③の適用をしてください。適用をしないと、変更が反映されません。
これによって結構なメモリを使いますので、古いPCを使っている方は細かくし過ぎないようにしてください。
同様に、残りの2つの球もリメッシュします。
適用を忘れないでください。
できた3Dモデルを出力します。
ファイル→エクスポート→STLファイルを選択します。
STL頂点情報を2D stack画像へ変換
3Dモデルから2次元stack画像への変換はpythonプログラムで行います。
以下のプログラムをSTLファイルがある場所で実行してください。
モジュールが無いなど、エラーが出たら適宜インストールしてください。
実行して、
Please enter file BASE name:
と聞かれたらファイル名を入力して、Enterを押してください。
(sphere.stlならsphereを入力)
import os
import imageio
import numpy as np
from stl import mesh
from scipy.ndimage import binary_fill_holes
filename = input('Please enter file BASE name: ');
# STLファイルを読み込む
print("STLファイルを読み込んでいます...")
your_mesh = mesh.Mesh.from_file(f'{filename}.stl')
# 空の3次元配列を作成 (サイズは適宜調整)
grid_size = 256 # グリッドサイズを設定
margin = 10 # 周囲に持たせたい余裕(ピクセル単位)
padded_grid_size = grid_size + 2 * margin # 周囲に余裕を追加したグリッドサイズ
voxels = np.zeros((padded_grid_size, padded_grid_size, padded_grid_size))
# STLメッシュの最小/最大座標を取得し、正規化
min_bounds = your_mesh.min_
max_bounds = your_mesh.max_
scale = grid_size / (max_bounds - min_bounds).max() # オブジェクトのサイズをグリッドに合わせてスケール
# メッシュのボクセル化
print("メッシュをボクセル化しています...")
total_triangles = len(your_mesh.vectors)
for idx, triangle in enumerate(your_mesh.vectors):
scaled_triangle = (triangle - min_bounds) * scale + margin # スケールした後に余裕分のオフセットを追加
for vert in scaled_triangle:
x, y, z = np.clip(np.round(vert).astype(int), 0, padded_grid_size - 1) # クリッピングと四捨五入を追加
voxels[x, y, z] = 1 # 頂点をボクセルに変換
# ボクセル化の進捗を表示
progress = (idx + 1) / total_triangles * 100
print(f"ボクセル化進行中: {progress:.2f}% 完了 ({idx + 1}/{total_triangles})", end='\r')
print("\nボクセル化が完了しました。")
# 3次元ボクセルを埋める(3次元的に閉じていれば内側が塗りつぶされる)------------
#print("ボクセルの穴を埋めています...")
#voxels = binary_fill_holes(voxels)
#print("ボクセルの穴埋めが完了しました。")
# --------------------------------------------------------------------------------
# 出力フォルダを作成
output_folder = filename
os.makedirs(output_folder, exist_ok=True)
# TIFF形式でスライスごとに保存
total_slices = voxels.shape # z軸方向のスライス数
for i in range(total_slices): # z軸方向のスライス
# スライスを保存
imageio.imwrite(os.path.join(output_folder, f"output_voxels_slice_{i}.tiff"), filled_voxels[:, :, i].astype(np.uint8))
# スライス保存の進捗を表示
progress = (i + 1) / total_slices * 100
print(f"TIFF保存進行中: {progress:.2f}% 完了 ({i + 1}/{total_slices})", end='\r')
print(f"\nTIFFファイルが {output_folder} フォルダに保存されました。")
このコードの中間でコメントアウトされている
# 3次元ボクセルを埋める(3次元的に閉じていれば内側が塗りつぶされる)------------
#print("ボクセルの穴を埋めています...")
#voxels = binary_fill_holes(voxels)
#print("ボクセルの穴埋めが完了しました。")
# --------------------------------------------------------------------------------
を有効にすると、3次元的に閉じた物体であれば中を埋めて出力します。
細かすぎる2次元画像だと、一見繋がっているようで繋がっていませんので解像度を下げるか、リメッシュを細かくするようにしてください。
中を埋めないで出力した画像です。画素値はかなり小さいので、
Procee→Math→Multiplyで100倍にしてください。
中を埋める処理をした画像です。
大きな球は繋がっていないので、埋められていません。
これにより、3Dモデルを2次元stack画像にすることができました。
この画像を使って、次回はシミュレーションしてみます