記事の内容
2次元の画像から 3D メッシュを作る流れを、Replicate 上の InstantMesh API で実装します。
Replicate は従量課金です。この記事で使う InstantMesh はおおよそ 1 回あたり $0.1〜0.2 前後、処理時間は 1〜2 分程度が目安です。
まず Replicate について
Cloudflare 傘下のプラットフォームで先端のオープンソース AI モデルをクラウド上のAPIとして手軽に実行・デプロイできます。
Hugging Face の違い
| 観点 | Replicate | Hugging Face |
|---|---|---|
| 主な役割 | ホスト済みの 推論 API | モデル配布ハブ(+ Spaces / Inference)、データセット |
| 実行のしやすさ | GPU 不要。replicate.run() で完結 |
自前 GPU / Colab / Spaces で動かすことが多い |
| モデル数 | 数千程度(画像・動画が多い) | 100万件以上 |
| コスト | 従量課金(呼び出した分だけ) | モデル取得自体は無料が多い。計算資源は別途必要 |
| 向いている用途 | 試作、アプリ組み込み、ハンズオン | 研究、自前デプロイ |
InstantMesh とは
Tencent が開発した 1 枚の画像から効率的に 3D メッシュを生成するフレームワークです。
1. 環境準備
まずは、Replicate API トークンを取得します。
- https://replicate.com でアカウント作成
- API Tokens でトークンを発行
- 残高(クレジット)を確認
.env を開き、トークンを書き込みます。
REPLICATE_API_TOKEN=r8_あなたのトークン
2. API の呼び出し
python で実装します。
呼び出し方は以下のように replicate.run() にモデルやパラメーターを設定します。
import replicate
from pathlib import Path
image_path = Path("samples/chair.png")
with image_path.open("rb") as image_file:
output = replicate.run(
"aryamansital/instant_mesh",
input={
"image_path": image_file,
"remove_background": True,
"export_video": True,
"export_texmap": False,
"sample_steps": 75,
"seed": 42,
},
)
# output は URI(または FileOutput)のリスト
print(output)
画像の出力
ローカルに保存します。
io_utils.py
from pathlib import Path
import requests
outputs_dir = Path("outputs/run1")
outputs_dir.mkdir(parents=True, exist_ok=True)
for i, item in enumerate(output):
url = item if isinstance(item, str) else getattr(item, "url", None)
if not url:
# FileOutput なら .read() でも保存できる
data = item.read()
path = outputs_dir / f"instantmesh_{i:02d}.bin"
path.write_bytes(data)
continue
suffix = Path(url.split("?", 1)[0]).suffix or ".bin"
dest = outputs_dir / f"instantmesh_{i:02d}{suffix}"
response = requests.get(url, timeout=120)
response.raise_for_status()
dest.write_bytes(response.content)
print("saved:", dest)
コード全体
ディレクトリ構成
replicate-3d-handson/
├── ARTICLE.md # この実装記事
├── README.md # セットアップの最短手順
├── requirements.txt # 依存パッケージ
├── .env.example
├── .gitignore
├── src/
| ├──__init__.py
| ├──config.py # モデル ID・デフォルトパラメータ
| ├──io_utils.py # 出力ファイルの保存
| ├──instant_mesh.py # InstantMesh(Replicate)呼び出し
| └──cli.py # CLI エントリ
└── outputs/ # 生成結果
instant_mesh.py
from __future__ import annotations
from datetime import datetime
from pathlib import Path
from typing import Any
import replicate
from dotenv import load_dotenv
from src.config import (
DEFAULT_EXPORT_TEXMAP,
DEFAULT_EXPORT_VIDEO,
DEFAULT_REMOVE_BACKGROUND,
DEFAULT_SAMPLE_STEPS,
DEFAULT_SEED,
INSTANT_MESH_MODEL,
)
from src.io_utils import ensure_outputs_dir, save_output_list
def generate_instant_mesh(
image: Path | str,
*,
remove_background: bool = DEFAULT_REMOVE_BACKGROUND,
export_video: bool = DEFAULT_EXPORT_VIDEO,
export_texmap: bool = DEFAULT_EXPORT_TEXMAP,
sample_steps: int = DEFAULT_SAMPLE_STEPS,
seed: int = DEFAULT_SEED,
output_subdir: str | None = None,
) -> dict[str, Any]:
"""
Replicateの実行
"""
load_dotenv()
image_path = Path(image).expanduser().resolve()
if not image_path.is_file():
raise FileNotFoundError(f"Input image not found: {image_path}")
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
run_name = output_subdir or f"instantmesh_{timestamp}"
run_dir = ensure_outputs_dir(run_name)
with image_path.open("rb") as image_file:
raw_output = replicate.run(
INSTANT_MESH_MODEL,
input={
"image_path": image_file,
"remove_background": remove_background,
"export_video": export_video,
"export_texmap": export_texmap,
"sample_steps": sample_steps,
"seed": seed,
},
)
if isinstance(raw_output, (list, tuple)):
items = list(raw_output)
elif raw_output is None:
items = []
else:
items = [raw_output]
saved_paths = save_output_list(items, run_dir, prefix="instantmesh")
return {
"model": INSTANT_MESH_MODEL,
"image": str(image_path),
"run_dir": str(run_dir),
"raw_output": items,
"saved_paths": [str(path) for path in saved_paths],
}
それ以外の部分
io_utils.py
from __future__ import annotations
import mimetypes
from pathlib import Path
from typing import Any
from urllib.parse import unquote, urlparse
import requests
from src.config import OUTPUTS_DIR
def ensure_outputs_dir(subdir: str | None = None) -> Path:
"""Create and return the outputs directory (optionally a subfolder)."""
path = OUTPUTS_DIR if subdir is None else OUTPUTS_DIR / subdir
path.mkdir(parents=True, exist_ok=True)
return path
def _guess_extension(url: str, content_type: str | None = None) -> str:
parsed = urlparse(url)
suffix = Path(unquote(parsed.path)).suffix.lower()
if suffix and len(suffix) <= 5:
return suffix
if content_type:
guessed = mimetypes.guess_extension(content_type.split(";")[0].strip())
if guessed:
return guessed
return ".bin"
def download_url(url: str, dest: Path) -> Path:
"""Download a remote file to dest and return the final path."""
response = requests.get(url, timeout=120, stream=True)
response.raise_for_status()
if dest.suffix == "":
dest = dest.with_suffix(
_guess_extension(url, response.headers.get("Content-Type"))
)
dest.parent.mkdir(parents=True, exist_ok=True)
with dest.open("wb") as file:
for chunk in response.iter_content(chunk_size=1024 * 256):
if chunk:
file.write(chunk)
return dest
def save_file_output(item: Any, dest: Path) -> Path:
"""
生成データの保存
"""
dest.parent.mkdir(parents=True, exist_ok=True)
# FileOutput from replicate Python SDK (has .read / .url)
if hasattr(item, "read") and callable(item.read):
data = item.read()
url = getattr(item, "url", None)
if dest.suffix == "" and isinstance(url, str):
dest = dest.with_suffix(_guess_extension(url))
with dest.open("wb") as file:
file.write(data)
return dest
if isinstance(item, (str, Path)):
text = str(item)
if text.startswith("http://") or text.startswith("https://"):
return download_url(text, dest)
source = Path(text)
if source.exists():
if dest.suffix == "":
dest = dest.with_suffix(source.suffix or ".bin")
dest.write_bytes(source.read_bytes())
return dest
raise ValueError(f"Unsupported output value (not a URL or existing path): {text}")
raise TypeError(f"Unsupported output type: {type(item)!r}")
def save_output_list(outputs: list[Any], run_dir: Path, prefix: str = "output") -> list[Path]:
saved: list[Path] = []
for index, item in enumerate(outputs):
dest = run_dir / f"{prefix}_{index:02d}"
saved.append(save_file_output(item, dest))
return saved
config.py
"""Project paths and InstantMesh defaults."""
from __future__ import annotations
from pathlib import Path
# Replicate model for InstantMesh
INSTANT_MESH_MODEL = "aryamansital/instant_mesh:e353a25cc764e0edb0aa9033df0bf4b82318dcda6d0a0cd9f2aac
e90566068ac"
# Default generation options (match Replicate API schema)
DEFAULT_REMOVE_BACKGROUND = True
DEFAULT_EXPORT_VIDEO = True
DEFAULT_EXPORT_TEXMAP = False
DEFAULT_SAMPLE_STEPS = 75
DEFAULT_SEED = 42
PROJECT_ROOT = Path(__file__).resolve().parent.parent
OUTPUTS_DIR = PROJECT_ROOT / "outputs"
SAMPLES_DIR = PROJECT_ROOT / "samples"
cli.py
from __future__ import annotations
import argparse
import json
import sys
from pathlib import Path
from src.config import (
DEFAULT_EXPORT_TEXMAP,
DEFAULT_EXPORT_VIDEO,
DEFAULT_REMOVE_BACKGROUND,
DEFAULT_SAMPLE_STEPS,
DEFAULT_SEED,
)
from src.instant_mesh import generate_instant_mesh
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Generate a 3D mesh from a 2D image using InstantMesh on Replicate.",
)
parser.add_argument(
"--image",
required=True,
help="Path to an input image (PNG or JPEG).",
)
parser.add_argument(
"--remove-background",
action=argparse.BooleanOptionalAction,
default=DEFAULT_REMOVE_BACKGROUND,
help="Remove image background before reconstruction (default: enabled).",
)
parser.add_argument(
"--export-video",
action=argparse.BooleanOptionalAction,
default=DEFAULT_EXPORT_VIDEO,
help="Export a turntable preview video (default: enabled).",
)
parser.add_argument(
"--export-texmap",
action=argparse.BooleanOptionalAction,
default=DEFAULT_EXPORT_TEXMAP,
help="Export a texture map with the mesh (default: disabled).",
)
parser.add_argument(
"--sample-steps",
type=int,
default=DEFAULT_SAMPLE_STEPS,
help=f"Diffusion sample steps (default: {DEFAULT_SAMPLE_STEPS}).",
)
parser.add_argument(
"--seed",
type=int,
default=DEFAULT_SEED,
help=f"Random seed (default: {DEFAULT_SEED}).",
)
parser.add_argument(
"--json",
action="store_true",
help="Print the result summary as JSON.",
)
return parser
def main(argv: list[str] | None = None) -> int:
args = build_parser().parse_args(argv)
try:
result = generate_instant_mesh(
Path(args.image),
remove_background=args.remove_background,
export_video=args.export_video,
export_texmap=args.export_texmap,
sample_steps=args.sample_steps,
seed=args.seed,
)
except Exception as exc: # noqa: BLE001 - surface CLI errors clearly
print(f"Error: {exc}", file=sys.stderr)
return 1
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2))
else:
print(f"Model : {result['model']}")
print(f"Image : {result['image']}")
print(f"Saved : {result['run_dir']}")
for path in result["saved_paths"]:
print(f" - {path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
試してみる
画像を用意してsample/に置いておきます。
サンプル画像はこちらを用意しました。
pip install -r requirements.txt
python -m src.cli --image samples\sample.png
このような 3D イメージができました。背中側もきれいにできましたね。
出力として、動画や.glbファイルも生成されます。
参考
API リファレンス:




