3
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?

記事の内容

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 メッシュを生成するフレームワークです。

image.png

1. 環境準備

まずは、Replicate API トークンを取得します。

  1. https://replicate.com でアカウント作成
  2. API Tokens でトークンを発行
  3. 残高(クレジット)を確認

image.png

.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![instantmesh_00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/986141/b3e8c546-92e2-443f-9998-e7a86cabed18.png)
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/に置いておきます。

サンプル画像はこちらを用意しました。

sample.png

pip install -r requirements.txt
python -m src.cli --image samples\sample.png

このような 3D イメージができました。背中側もきれいにできましたね。

instantmesh_00.png

出力として、動画や.glbファイルも生成されます。

image.png

参考

API リファレンス:

3
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
3
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?