Spaces:
Running
Running
| """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']}") | |