CloneForge / cloneforge /examples.py
avaliev's picture
Upload 75 files
8f1f637 verified
Raw
History Blame Contribute Delete
8.22 kB
"""Examples library — reference objects with GROUND TRUTH for quality evaluation.
Two kinds of ground truth (per datasets research):
- Standard parts (LEGO 2×4, ISO 7089 washer, DIN 934 nut, 16 mm die) have
PUBLISHED EXACT DIMENSIONS → numeric "accurate to X mm" ground truth.
- A reference mesh is also generated so we can report Chamfer / voxel-IoU.
We build the reference meshes parametrically from published specs and render a
clean preview that doubles as a demo input image (the live app also takes real
webcam/upload photos — photograph the real part and the published dims still apply).
`build_library(out_dir)` writes per-object reference.stl + preview.png + manifest.json.
"""
from __future__ import annotations
import json
import os
import urllib.request
import numpy as np
import trimesh
from .render import render_single
# Google Scanned Objects (CC-BY 4.0) via the kevinzakka/mujoco_scanned_objects mirror.
# Real-world 3D scans — used as ground-truth reference meshes (Chamfer/voxel-IoU) and to
# show the fidelity gap vs our parametric clones. Scans are in metres → scaled to mm.
GSO_BASE = "https://raw.githubusercontent.com/kevinzakka/mujoco_scanned_objects/main/models/{}/model.obj"
GSO = [
{"name": "gso_mug", "gso": "ACE_Coffee_Mug_Kristen_16_oz_cup",
"title": "Coffee mug (GSO scan)", "category": "real · medium",
"goal": "Clone this mug as a 3D-printable model"},
{"name": "gso_teapot", "gso": "Threshold_Porcelain_Teapot_White",
"title": "Porcelain teapot (GSO scan)", "category": "real · complex",
"goal": "Clone this teapot as a 3D-printable model"},
{"name": "gso_panda", "gso": "Android_Figure_Panda",
"title": "Android panda figure (GSO scan)", "category": "real · complex",
"goal": "Clone this figurine as a 3D-printable model"},
]
GSO_ATTRIB = "Google Scanned Objects (CC-BY 4.0)"
def _load_gso(gso_name: str, dl_dir: str) -> trimesh.Trimesh:
"""Download a GSO model.obj (cached), scale m→mm, center, decimate if large."""
os.makedirs(dl_dir, exist_ok=True)
path = os.path.join(dl_dir, gso_name + ".obj")
if not os.path.exists(path):
urllib.request.urlretrieve(GSO_BASE.format(gso_name), path)
m = trimesh.load(path, force="mesh")
m.apply_scale(1000.0)
m.apply_translation(-m.bounding_box.centroid)
if len(m.faces) > 12000:
try:
m = m.simplify_quadric_decimation(face_count=12000)
except Exception: # noqa: BLE001
pass
return m
# --- reference mesh builders (dims in mm, from published specs) -------------
def _lego_2x4():
"""LEGO brick 3001: body 31.8×15.8×9.6, studs Ø4.8 h1.8, pitch 8.0."""
body = trimesh.creation.box(extents=[31.8, 15.8, 9.6])
parts = [body]
x0, y0 = -3 * 8.0 / 2, -8.0 / 2
for ix in range(4):
for iy in range(2):
stud = trimesh.creation.cylinder(radius=2.4, height=1.8, sections=32)
stud.apply_translation([x0 + ix * 8.0, y0 + iy * 8.0, 9.6 / 2 + 0.9])
parts.append(stud)
return trimesh.boolean.union(parts), "LEGO 2×4 brick (3001)"
def _washer_m10():
"""ISO 7089 M10: ID 10.5, OD 20, thickness 2.0."""
outer = trimesh.creation.cylinder(radius=10.0, height=2.0, sections=64)
inner = trimesh.creation.cylinder(radius=5.25, height=4.0, sections=64)
return trimesh.boolean.difference([outer, inner]), "ISO 7089 flat washer (M10)"
def _hex_nut_m8():
"""DIN 934 M8: width across flats 13 (circumradius 7.51), height 6.5, bore 8."""
body = trimesh.creation.cylinder(radius=7.506, height=6.5, sections=6)
bore = trimesh.creation.cylinder(radius=4.0, height=10.0, sections=48)
return trimesh.boolean.difference([body, bore]), "DIN 934 hex nut (M8)"
def _die_16():
"""16 mm six-sided die with shallow pips."""
cube = trimesh.creation.box(extents=[16, 16, 16])
return cube, "Six-sided die (16 mm)"
def _mug():
outer = trimesh.creation.cylinder(radius=40, height=95, sections=64)
inner = trimesh.creation.cylinder(radius=34, height=85, sections=64)
inner.apply_translation([0, 0, 8])
body = trimesh.boolean.difference([outer, inner])
handle = trimesh.creation.torus(major_radius=22, minor_radius=6)
handle.apply_translation([44, 0, 0])
return trimesh.boolean.union([body, handle]), "Coffee mug"
def _spur_gear():
"""Complex case: 20-tooth spur gear — stresses primitive decomposition."""
base = trimesh.creation.cylinder(radius=18, height=6, sections=64)
parts = [base]
for i in range(20):
a = 2 * np.pi * i / 20
tooth = trimesh.creation.box(extents=[5, 3, 6])
tooth.apply_translation([20, 0, 0])
tooth.apply_transform(trimesh.transformations.rotation_matrix(a, [0, 0, 1]))
parts.append(tooth)
bore = trimesh.creation.cylinder(radius=5, height=8, sections=48)
return trimesh.boolean.difference([trimesh.boolean.union(parts), bore]), "20-tooth spur gear"
REGISTRY = [
{"name": "washer_m10", "build": _washer_m10, "category": "simple",
"goal": "Clone this washer as a 3D-printable model",
"note": "ISO 7089 M10: OD 20, ID 10.5, thickness 2.0 mm"},
{"name": "hex_nut_m8", "build": _hex_nut_m8, "category": "simple",
"goal": "Clone this hex nut as a 3D-printable model",
"note": "DIN 934 M8: across-flats 13, height 6.5 mm"},
{"name": "die_16", "build": _die_16, "category": "simple",
"goal": "Clone this die as a 3D-printable model",
"note": "Standard 16 mm cube"},
{"name": "lego_2x4", "build": _lego_2x4, "category": "medium",
"goal": "Clone this LEGO brick as a 3D-printable model",
"note": "LEGO 3001: 31.8×15.8×9.6 mm + 8 studs"},
{"name": "mug", "build": _mug, "category": "medium",
"goal": "Clone this mug as a 3D-printable model",
"note": "~95 mm tall, 80 mm dia (approx)"},
{"name": "spur_gear", "build": _spur_gear, "category": "complex",
"goal": "Clone this gear as a 3D-printable model",
"note": "20-tooth gear — shows the fidelity gap on complex geometry"},
]
def _write_entry(out_dir, name, mesh, title, category, goal, note):
d = os.path.join(out_dir, name)
os.makedirs(d, exist_ok=True)
ref_stl = os.path.join(d, "reference.stl")
preview = os.path.join(d, "preview.png")
mesh.export(ref_stl)
render_single(mesh, preview, view="iso")
dims = sorted(round(float(x), 1) for x in (mesh.bounds[1] - mesh.bounds[0]))
return {"name": name, "title": title, "category": category, "goal": goal,
"note": note, "reference_stl": ref_stl, "preview": preview, "dims_mm": dims}
def build_library(out_dir: str = "data/examples", include_gso: bool = True) -> dict:
"""Generate reference meshes + previews + manifest. GSO scans are best-effort
(skipped if offline). Returns the manifest dict."""
os.makedirs(out_dir, exist_ok=True)
manifest = []
for spec in REGISTRY:
mesh, title = spec["build"]()
manifest.append(_write_entry(out_dir, spec["name"], mesh, title,
spec["category"], spec["goal"], spec["note"]))
if include_gso:
dl = os.path.join(out_dir, "_gso_cache")
for spec in GSO:
try:
mesh = _load_gso(spec["gso"], dl)
manifest.append(_write_entry(out_dir, spec["name"], mesh, spec["title"],
spec["category"], spec["goal"], GSO_ATTRIB))
except Exception as e: # noqa: BLE001 — offline / fetch failure is non-fatal
print(f" (skipped GSO {spec['name']}: {e})")
with open(os.path.join(out_dir, "manifest.json"), "w") as f:
json.dump(manifest, f, indent=2)
return {"out_dir": out_dir, "items": manifest}
def load_manifest(out_dir: str = "data/examples") -> list[dict]:
path = os.path.join(out_dir, "manifest.json")
if not os.path.exists(path):
return build_library(out_dir)["items"]
with open(path) as f:
return json.load(f)
if __name__ == "__main__":
lib = build_library()
for it in lib["items"]:
print(f"{it['category']:8} {it['name']:12} dims={it['dims_mm']} -> {it['preview']}")