Spaces:
Sleeping
Sleeping
File size: 8,218 Bytes
8f1f637 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 | """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']}")
|