HunyuanWorld-Mirrorを実行できるComfyUIのカスタムノードが無かったので、ChatGPTに聞きながらColab上でプログラム書いて実行してみました。
生成されるデータの中に3D Gaussian Splattingデータがあったのでplyファイルとして保存してWebのViewerで表示させてみたのでそのやり方を紹介します。
HunyuanWorld-Mirrorとは
TencentのHunyuanチームがオープンソースの世界モデル「Hunyuan World Models」シリーズの新モデルとして発表しました。
あらゆる入力、最大限の柔軟性と忠実度:多様な幾何学的事前情報(カメラ姿勢、固有パラメータ、深度マップ)を柔軟に統合し、構造的曖昧性を解消するとともに、幾何学的に一貫した3D出力を保証します。
Colabでの実行
まずGithubからクローンします。
| !git clone https://github.com/Tencent-Hunyuan/HunyuanWorld-Mirror.git %cd HunyuanWorld-Mirror |
続いて、必要なモジュールをインストールします。
| !pip install torch torchvision # ColabのGPUに合わせてバージョンを調整 !pip install lightning pytorch-lightning lightning-utilities !pip install ninja !pip install gsplat !pip install -r requirements.txt !pip install open3d |
次から実際のPythonのプログラム部分です。
| import torch import open3d as o3d import numpy as np from pathlib import Path from src.models.models.worldmirror import WorldMirror from src.utils.inference_utils import extract_load_and_preprocess_images device = “cuda” if torch.cuda.is_available() else “cpu” |
モデルをHugging Faceからダウンロードします。
URL:https://huggingface.co/tencent/HunyuanWorld-Mirror/tree/main
checkpointsフォルダを作成してそこにダウンロードしたmodel.safetensorsを入れておきます。
| # モデルロード model = WorldMirror.from_pretrained(“tencent/HunyuanWorld-Mirror”, cache_dir=”./checkpoints”).to(device) model.eval() |
入力用のフォルダを作成してそこに画像を入れておきます。
| # 入力画像前処理 img_dir = Path(“./images”) # input.jpg を置いた場所 inputs = {} imgs = extract_load_and_preprocess_images( img_dir, fps=1, target_size=1064 ).to(device) |
以下のプログラムで推論を実行して出力データが取得できます。
| views = { “img”: imgs, } # 推論 with torch.no_grad(): outputs = model(views, cond_flags=[0], is_inference=True) print(outputs.keys()) |
出力データには色んなものが入っていて、キーの一覧をプリントすると以下のようなものがあります。
| dict_keys([‘camera_params’, ‘camera_poses’, ‘camera_intrs’, ‘depth’, ‘depth_conf’, ‘pts3d’, ‘pts3d_conf’, ‘normals’, ‘normals_conf’, ‘gs_depth’, ‘gs_depth_conf’, ‘splats’]) |
それぞれのキーの意味としては以下になります。
カメラ系
| key | 内容 | 用途 |
|---|---|---|
| camera_params | カメラの内部+外部パラメータ | 再投影・レンダリング |
| camera_intrs | 内部パラメータ(K行列) | 深度→3D変換 |
| camera_poses | 外部パラメータ(姿勢) | カメラ位置・向き |
深度・点群
| key | 内容 | 用途 |
|---|---|---|
| depth | 深度マップ(H×W) | 距離情報 |
| depth_conf | 深度の信頼度 | ノイズ除去 |
| pts3d | 各ピクセルの3D座標(H×W×3) | 点群生成 |
| pts3d_conf | 各点の信頼度 | 点群フィルタ |
法線
| key | 内容 | 用途 |
|---|---|---|
| normals | 各点の法線ベクトル | メッシュ化・ライティング |
| normals_conf | 法線の信頼度 | ノイズ除去 |
Gaussian Splatting
| key | 内容 | 用途 |
|---|---|---|
| gs_depth | Gaussian Splatting用深度 | Splat最適化 |
| gs_depth_conf | Gaussian Splatting用深度の信頼度 | フィルタ |
| splats | Gaussian Splatting用パラメータ式 | 3DGS / Web表示 |
点群データをplyファイルとして保存する処理が以下になります。
| # — データ取り出し — pts3d = outputs[“pts3d”][0] # (S, H, W, 3) or (H, W, 3) conf = outputs[“pts3d_conf”][0] # (S, H, W) or (H, W) images = views[“img”][0] # (S, 3, H, W) # 次元整理 if pts3d.ndim == 4: # (S,H,W,3) → 1枚目だけ使う pts3d = pts3d[0] conf = conf[0] images = images[0] # numpy化 xyz = pts3d.cpu().numpy().reshape(-1, 3) conf = conf.cpu().numpy().reshape(-1) rgb = images.permute(1, 2, 0).cpu().numpy().reshape(-1, 3) # 0–255へ rgb = (rgb * 255).clip(0, 255).astype(np.uint8) # — フィルタ条件 — conf_th = 0.6 # ← 0.4〜0.8くらいで調整OK mask = ( np.isfinite(xyz).all(axis=1) & (conf > conf_th) ) xyz_f = xyz[mask] rgb_f = rgb[mask] print(f”Points before: {len(xyz)}, after filtering: {len(xyz_f)}”) # — PLY保存 — ply_path = “/content/drive/MyDrive/HunyuanWorld-Mirror/output/colored_pointcloud_conf_filtered_00001.ply” with open(ply_path, “w”) as f: f.write(“ply\nformat ascii 1.0\n”) f.write(f”element vertex {len(xyz_f)}\n”) f.write(“property float x\nproperty float y\nproperty float z\n”) f.write(“property uchar red\nproperty uchar green\nproperty uchar blue\n”) f.write(“end_header\n”) for p, c in zip(xyz_f, rgb_f): f.write(f”{p[0]} {p[1]} {p[2]} {c[0]} {c[1]} {c[2]}\n”) print(“Saved:”, ply_path) |
Gaussian Splattingデータをplyファイルで保存する処理になります。
| import struct splats = outputs[“splats”] # ← ここが修正点 # 取り出し means = torch.cat(splats[“means”], dim=0).cpu().numpy() # (N,3) scales = torch.cat(splats[“scales”], dim=0).cpu().numpy() # (N,3) quats = torch.cat(splats[“quats”], dim=0).cpu().numpy() # (N,4) sh = torch.cat(splats[“sh”], dim=0).cpu().numpy() # (N,1,3) opacity = torch.cat(splats[“opacities”], dim=0).cpu().numpy() # (N,1) rgb = sh[:, 0, :] # (N,3) # HunyuanWorld → GS Viewer 変換 scales = np.log(scales + 1e-8) opacity = np.log(opacity / (1 – opacity + 1e-8) + 1e-8) N = means.shape[0] print(“Num Gaussians:”, N) # — PLY形式(3DGS互換) — gs_path = “/content/drive/MyDrive/HunyuanWorld-Mirror/output/gaussian_splats_binary_00026.ply” with open(gs_path, “wb”) as f: header = “ply\n” header += “format binary_little_endian 1.0\n” header += f”element vertex {N}\n” header += “property float x\n” header += “property float y\n” header += “property float z\n” header += “property float f_dc_0\n” header += “property float f_dc_1\n” header += “property float f_dc_2\n” # SH高次が無いのでダミー0で埋める for i in range(0, 45): header += f”property float f_rest_{i}\n” header += “property float opacity\n” header += “property float scale_0\n” header += “property float scale_1\n” header += “property float scale_2\n” header += “property float rot_0\n” header += “property float rot_1\n” header += “property float rot_2\n” header += “property float rot_3\n” header += “end_header\n” f.write(header.encode(“utf-8”)) # ← ヘッダは文字列→bytes for i in range(N): x,y,z = means[i] r,g,b = rgb[i] o = float(opacity[i]) sx,sy,sz = scales[i] q0,q1,q2,q3 = quats[i] f.write(struct.pack( “<3f 3f 45f 1f 3f 4f”, x, y, z, r, g, b, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, o, sx, sy, sz, q0, q1, q2, 3, )) print(“Saved Gaussian Splat PLY:”, gs_path) |
plyファイルはフォーマットに自由度がありすぎて、普通の点群データもGaussian Splattingデータも両方扱えてしまいます。
さらにGaussian SplattingデータをWebのViewer等で表示しようと思ったら、Viewer側が期待する形式で保存する必要があります。
| プロパティ名 | 意味 |
|---|---|
| x y z | 位置 |
| scale_0 scale_1 scale_2 | ガウスサイズ |
| quat_0 quat_1 quat_2 quat_3 | 回転(Quaternion) |
| opacity | 不透明度 |
| f_dc_0 f_dc_1 f_dc_2 | 色(SH 0次) |
| f_rest_* | SH 高次係数 |
以下のようにplyファイルの中身を確認できます。
| gs_path = “/content/drive/MyDrive/HunyuanWorld-Mirror/output/gaussian_splats_binary_00026.ply” with open(gs_path, “rb”) as f: print(f.read(1600)) |
ちゃんと作成できていれば以下のようになっているはずです。
| b’ply\nformat binary_little_endian 1.0\nelement vertex 429667\nproperty float x\nproperty float y\nproperty float z\nproperty float f_dc_0\nproperty float f_dc_1\nproperty float f_dc_2\nproperty float f_rest_0\nproperty float f_rest_1\nproperty float f_rest_2\nproperty float f_rest_3\nproperty float f_rest_4\nproperty float f_rest_5\nproperty float f_rest_6\nproperty float f_rest_7\nproperty float f_rest_8\nproperty float f_rest_9\nproperty float f_rest_10\nproperty float f_rest_11\nproperty float f_rest_12\nproperty float f_rest_13\nproperty float f_rest_14\nproperty float f_rest_15\nproperty float f_rest_16\nproperty float f_rest_17\nproperty float f_rest_18\nproperty float f_rest_19\nproperty float f_rest_20\nproperty float f_rest_21\nproperty float f_rest_22\nproperty float f_rest_23\nproperty float f_rest_24\nproperty float f_rest_25\nproperty float f_rest_26\nproperty float f_rest_27\nproperty float f_rest_28\nproperty float f_rest_29\nproperty float f_rest_30\nproperty float f_rest_31\nproperty float f_rest_32\nproperty float f_rest_33\nproperty float f_rest_34\nproperty float f_rest_35\nproperty float f_rest_36\nproperty float f_rest_37\nproperty float f_rest_38\nproperty float f_rest_39\nproperty float f_rest_40\nproperty float f_rest_41\nproperty float f_rest_42\nproperty float f_rest_43\nproperty float f_rest_44\nproperty float opacity\nproperty float scale_0\nproperty float scale_1\nproperty float scale_2\nproperty float rot_0\nproperty float rot_1\nproperty float rot_2\nproperty float rot_3\nend_header\n\xfb\x1cC\xbf\x066\xb6\xbeu\xad\x03@\xa4\x98\xf9>\x99\xa4W?\x03\xf1\xb7?\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00′ |
plyfileというモジュールを使うと、データの読み書きを簡単にできたります。
| !pip install plyfile |
以下のように読みだして、データの中身のそれぞれの値の最小値や最大値を確認して、WebのViewerの期待する形になっているか見ることが出来ます。
| from plyfile import PlyData import numpy as np # file_path = “/content/drive/MyDrive/HunyuanWorld-Mirror/output/cactus_splat3_30kSteps_142k_splats.ply” file_path = “/content/drive/MyDrive/HunyuanWorld-Mirror/output/gaussian_splats_binary_00020.ply” # PLYファイルを読み込み plydata = PlyData.read(file_path) # vertex要素を取得 vertex = plydata[‘vertex’] # データをNumPy配列として取得 # XYZ位置情報 xyz = np.stack([vertex[‘x’], vertex[‘y’], vertex[‘z’]], axis=1) # 色情報 (f_dc_0, f_dc_1, f_dc_2 が直流成分、球面調和関数) # f_rest_0 ~ f_rest_44 が高周波成分 features_dc = np.stack([vertex[‘f_dc_0’], vertex[‘f_dc_1’], vertex[‘f_dc_2’]], axis=1) # 透明度 (opacity) opacity = vertex[‘opacity’] # ガウシアンのスケール (scale_0, scale_1, scale_2) scale = np.stack([vertex[‘scale_0’], vertex[‘scale_1’], vertex[‘scale_2’]], axis=1) # ガウシアンの回転 (rot_0, rot_1, rot_2, rot_3) – クォータニオン rotation = np.stack([vertex[‘rot_0’], vertex[‘rot_1’], vertex[‘rot_2’], vertex[‘rot_3’]], axis=1) f_rest = np.stack([vertex[‘f_rest_0’], vertex[‘f_rest_1’], vertex[‘f_rest_2’], vertex[‘f_rest_3’]], axis=1) print(“xyz min/max =”, xyz.min(), xyz.max()) print(“scales min/max =”, scale.min(), scale.max()) print(“opacity min/max =”, opacity.min(), opacity.max()) print(“f_dc min/max =”, features_dc.min(), features_dc.max()) print(“rotation min/max =”, rotation.min(), rotation.max()) print(“f_rest min/max =”, f_rest.min(), f_rest.max()) |
まとめ
Colab上でPythonプログラムを書いてHunyuanWorld-Mirrorを実行してWebのViewerの表示できるフォーマットのplyファイルに保存して表示することができました。
3Dモデルに関連した他の記事
生成した3Dモデルを3Dプリンタで出力するなら…

Trellisで別視点の複数枚画像から生成する方法



コメント