""" services/mesh_utils.py ----------------------- Tumor mesh generation from a segmentation mask. This is a PLACEHOLDER implementation. It consumes the M1 output mask (pred_mask.npy — a (D, H, W) int label volume) and produces a triangle mesh via marching cubes, then writes it as .obj (always) and .glb (best-effort, via trimesh if available). It deliberately does NOT touch the MRI volume itself — only the mask — per the pipeline contract. Swap-out point: Replace `generate_tumor_mesh()` internals with a real meshing pipeline (e.g. smoothed marching cubes + decimation + a proper glTF exporter) without changing its signature, and the rest of pipeline.py keeps working unmodified. """ from __future__ import annotations import os from dataclasses import dataclass from typing import Optional import numpy as np from skimage import measure @dataclass class MeshResult: obj_path: str glb_path: Optional[str] n_vertices: int n_faces: int label_value: int error: Optional[str] = None def to_dict(self) -> dict: return { "obj_path": self.obj_path, "glb_path": self.glb_path, "n_vertices": self.n_vertices, "n_faces": self.n_faces, "label_value": self.label_value, "error": self.error, } def _write_obj(path: str, verts: np.ndarray, faces: np.ndarray) -> None: with open(path, "w") as f: for v in verts: f.write(f"v {v[0]:.6f} {v[1]:.6f} {v[2]:.6f}\n") for face in faces: # OBJ is 1-indexed f.write(f"f {face[0] + 1} {face[1] + 1} {face[2] + 1}\n") def _write_glb(path: str, verts: np.ndarray, faces: np.ndarray) -> bool: """ Best-effort .glb export via trimesh. Returns True on success, False if trimesh isn't installed or export fails — callers should treat a missing .glb as non-fatal (the .obj is the source of truth). """ try: import trimesh # optional dependency mesh = trimesh.Trimesh(vertices=verts, faces=faces, process=False) mesh.export(path) return True except Exception: return False def generate_tumor_mesh( mask_path: str, out_dir: str, label_value: int = 1, level: float = 0.5, step_size: int = 1, ) -> MeshResult: """ Generate a tumor surface mesh from a saved segmentation mask. Args: mask_path: path to pred_mask.npy ((D, H, W) int array, as produced by M1's main()/postprocessing). out_dir: directory to write tumor.obj / tumor.glb into. label_value: which label in the mask to mesh. BraTS-style masks are multi-class (e.g. edema / enhancing / necrotic); callers needing a "whole tumor" mesh should binarize before calling this, or call once per label. level: marching-cubes iso-level on the binarized mask. step_size: marching-cubes step size (>1 downsamples for speed). Returns: MeshResult with paths and basic stats. On failure, obj_path/ glb_path will be empty strings and `error` will be set — callers should treat meshing as best-effort and not abort the rest of the pipeline if it fails. """ os.makedirs(out_dir, exist_ok=True) obj_path = os.path.join(out_dir, "tumor.obj") glb_path = os.path.join(out_dir, "tumor.glb") try: mask = np.load(mask_path) binary = (mask == label_value).astype(np.uint8) if binary.sum() == 0: return MeshResult( obj_path="", glb_path=None, n_vertices=0, n_faces=0, label_value=label_value, error=f"No voxels found for label_value={label_value}; mesh not generated.", ) verts, faces, _normals, _values = measure.marching_cubes( binary, level=level, step_size=step_size ) _write_obj(obj_path, verts, faces) glb_written = _write_glb(glb_path, verts, faces) return MeshResult( obj_path=obj_path, glb_path=glb_path if glb_written else None, n_vertices=int(verts.shape[0]), n_faces=int(faces.shape[0]), label_value=label_value, ) except Exception as e: return MeshResult( obj_path="", glb_path=None, n_vertices=0, n_faces=0, label_value=label_value, error=f"Mesh generation failed: {e}", )