Spaces:
Build error
Build error
Refactor skin weight calculations to handle division by zero and ensure valid index access in Exporter and SAMPart3DDataset classes
27fa9cc
| import numpy as np | |
| from numpy import ndarray | |
| from typing import List, Union, Tuple | |
| from collections import defaultdict | |
| import os | |
| try: | |
| import open3d as o3d | |
| OPEN3D_EQUIPPED = True | |
| except: | |
| print("do not have open3d") | |
| OPEN3D_EQUIPPED = False | |
| class Exporter(): | |
| def _safe_make_dir(self, path): | |
| if os.path.dirname(path) == '': | |
| return | |
| os.makedirs(os.path.dirname(path), exist_ok=True) | |
| def _export_skeleton(self, joints: ndarray, parents: List[Union[int, None]], path: str): | |
| format = path.split('.')[-1] | |
| assert format in ['obj'] | |
| name = path.removesuffix('.obj') | |
| path = name + ".obj" | |
| self._safe_make_dir(path) | |
| J = joints.shape[0] | |
| with open(path, 'w') as file: | |
| file.write("o spring_joint\n") | |
| _joints = [] | |
| for id in range(J): | |
| pid = parents[id] | |
| if pid is None or pid == -1: | |
| continue | |
| bx, by, bz = joints[id] | |
| ex, ey, ez = joints[pid] | |
| _joints.extend([ | |
| f"v {bx} {bz} {-by}\n", | |
| f"v {ex} {ez} {-ey}\n", | |
| f"v {ex} {ez} {-ey + 0.00001}\n" | |
| ]) | |
| file.writelines(_joints) | |
| _faces = [f"f {id*3+1} {id*3+2} {id*3+3}\n" for id in range(J)] | |
| file.writelines(_faces) | |
| def _export_bones(self, bones: ndarray, path: str): | |
| format = path.split('.')[-1] | |
| assert format in ['obj'] | |
| name = path.removesuffix('.obj') | |
| path = name + ".obj" | |
| self._safe_make_dir(path) | |
| J = bones.shape[0] | |
| with open(path, 'w') as file: | |
| file.write("o bones\n") | |
| _joints = [] | |
| for bone in bones: | |
| bx, by, bz = bone[:3] | |
| ex, ey, ez = bone[3:] | |
| _joints.extend([ | |
| f"v {bx} {bz} {-by}\n", | |
| f"v {ex} {ez} {-ey}\n", | |
| f"v {ex} {ez} {-ey + 0.00001}\n" | |
| ]) | |
| file.writelines(_joints) | |
| _faces = [f"f {id*3+1} {id*3+2} {id*3+3}\n" for id in range(J)] | |
| file.writelines(_faces) | |
| def _export_skeleton_sequence(self, joints: ndarray, parents: List[Union[int, None]], path: str): | |
| format = path.split('.')[-1] | |
| assert format in ['obj'] | |
| name = path.removesuffix('.obj') | |
| path = name + ".obj" | |
| self._safe_make_dir(path) | |
| J = joints.shape[0] | |
| for i in range(J): | |
| file = open(name + f"_{i}.obj", 'w') | |
| file.write("o spring_joint\n") | |
| _joints = [] | |
| for id in range(i + 1): | |
| pid = parents[id] | |
| if pid is None: | |
| continue | |
| bx, by, bz = joints[id] | |
| ex, ey, ez = joints[pid] | |
| _joints.extend([ | |
| f"v {bx} {bz} {-by}\n", | |
| f"v {ex} {ez} {-ey}\n", | |
| f"v {ex} {ez} {-ey + 0.00001}\n" | |
| ]) | |
| file.writelines(_joints) | |
| _faces = [f"f {id*3+1} {id*3+2} {id*3+3}\n" for id in range(J)] | |
| file.writelines(_faces) | |
| file.close() | |
| def _export_mesh(self, vertices: ndarray, faces: ndarray, path: str): | |
| format = path.split('.')[-1] | |
| assert format in ['obj', 'ply'] | |
| if path.endswith('ply'): | |
| if not OPEN3D_EQUIPPED: | |
| raise RuntimeError("open3d is not available") | |
| mesh = o3d.geometry.TriangleMesh() | |
| mesh.vertices = o3d.utility.Vector3dVector(vertices) | |
| mesh.triangles = o3d.utility.Vector3iVector(faces) | |
| self._safe_make_dir(path) | |
| o3d.io.write_triangle_mesh(path, mesh) | |
| return | |
| name = path.removesuffix('.obj') | |
| path = name + ".obj" | |
| self._safe_make_dir(path) | |
| with open(path, 'w') as file: | |
| file.write("o mesh\n") | |
| _vertices = [] | |
| for co in vertices: | |
| _vertices.append(f"v {co[0]} {co[2]} {-co[1]}\n") | |
| file.writelines(_vertices) | |
| _faces = [] | |
| for face in faces: | |
| _faces.append(f"f {face[0]+1} {face[1]+1} {face[2]+1}\n") | |
| file.writelines(_faces) | |
| def _export_pc(self, vertices: ndarray, path: str, vertex_normals: Union[ndarray, None]=None, normal_size: float=0.01): | |
| if path.endswith('.ply'): | |
| if vertex_normals is not None: | |
| print("normal result will not be displayed in .ply format") | |
| name = path.removesuffix('.ply') | |
| path = name + ".ply" | |
| pc = o3d.geometry.PointCloud() | |
| pc.points = o3d.utility.Vector3dVector(vertices) | |
| # segment fault when numpy >= 2.0 !! use torch environment | |
| self._safe_make_dir(path) | |
| o3d.io.write_point_cloud(path, pc) | |
| return | |
| name = path.removesuffix('.obj') | |
| path = name + ".obj" | |
| self._safe_make_dir(path) | |
| with open(path, 'w') as file: | |
| file.write("o pc\n") | |
| _vertex = [] | |
| for co in vertices: | |
| _vertex.append(f"v {co[0]} {co[2]} {-co[1]}\n") | |
| file.writelines(_vertex) | |
| if vertex_normals is not None: | |
| new_path = path.replace('.obj', '_normal.obj') | |
| nfile = open(new_path, 'w') | |
| nfile.write("o normal\n") | |
| _normal = [] | |
| for i in range(vertices.shape[0]): | |
| co = vertices[i] | |
| x = vertex_normals[i, 0] | |
| y = vertex_normals[i, 1] | |
| z = vertex_normals[i, 2] | |
| _normal.extend([ | |
| f"v {co[0]} {co[2]} {-co[1]}\n", | |
| f"v {co[0]+0.0001} {co[2]} {-co[1]}\n", | |
| f"v {co[0]+x*normal_size} {co[2]+z*normal_size} {-(co[1]+y*normal_size)}\n", | |
| f"f {i*3+1} {i*3+2} {i*3+3}\n", | |
| ]) | |
| nfile.writelines(_normal) | |
| def _make_armature( | |
| self, | |
| vertices: Union[ndarray, None], | |
| joints: ndarray, | |
| skin: Union[ndarray, None], | |
| parents: List[Union[int, None]], | |
| names: List[str], | |
| faces: Union[ndarray, None]=None, | |
| extrude_size: float=0.03, | |
| group_per_vertex: int=-1, | |
| add_root: bool=False, | |
| do_not_normalize: bool=False, | |
| use_extrude_bone: bool=True, | |
| use_connect_unique_child: bool=True, | |
| extrude_from_parent: bool=True, | |
| tails: Union[ndarray, None]=None, | |
| ): | |
| import bpy # type: ignore | |
| from mathutils import Vector # type: ignore | |
| # make collection | |
| collection = bpy.data.collections.new('new_collection') | |
| bpy.context.scene.collection.children.link(collection) | |
| # make mesh | |
| if vertices is not None: | |
| mesh = bpy.data.meshes.new('mesh') | |
| if faces is None: | |
| faces = [] | |
| mesh.from_pydata(vertices, [], faces) | |
| mesh.update() | |
| # make object from mesh | |
| object = bpy.data.objects.new('character', mesh) | |
| # add object to scene collection | |
| collection.objects.link(object) | |
| # deselect mesh | |
| bpy.ops.object.armature_add(enter_editmode=True) | |
| armature = bpy.data.armatures.get('Armature') | |
| edit_bones = armature.edit_bones | |
| J = joints.shape[0] | |
| if tails is None: | |
| tails = joints.copy() | |
| tails[:, 2] += extrude_size | |
| connects = [False for _ in range(J)] | |
| children = defaultdict(list) | |
| for i in range(1, J): | |
| children[parents[i]].append(i) | |
| if tails is not None: | |
| if use_extrude_bone: | |
| for i in range(J): | |
| if len(children[i]) != 1 and extrude_from_parent and i != 0: | |
| pjoint = joints[parents[i]] | |
| joint = joints[i] | |
| d = joint - pjoint | |
| if np.linalg.norm(d) < 0.000001: | |
| d = np.array([0., 0., 1.]) # in case son.head == parent.head | |
| else: | |
| d = d / np.linalg.norm(d) | |
| tails[i] = joint + d * extrude_size | |
| if use_connect_unique_child: | |
| for i in range(J): | |
| if len(children[i]) == 1: | |
| child = children[i][0] | |
| tails[i] = joints[child] | |
| if parents[i] is not None and len(children[parents[i]]) == 1: | |
| connects[i] = True | |
| if add_root: | |
| bone_root = edit_bones.get('Bone') | |
| bone_root.name = 'Root' | |
| bone_root.tail = Vector((joints[0, 0], joints[0, 1], joints[0, 2])) | |
| else: | |
| bone_root = edit_bones.get('Bone') | |
| bone_root.name = names[0] | |
| bone_root.head = Vector((joints[0, 0], joints[0, 1], joints[0, 2])) | |
| bone_root.tail = Vector((joints[0, 0], joints[0, 1], joints[0, 2] + extrude_size)) | |
| def extrude_bone( | |
| edit_bones, | |
| name: str, | |
| parent_name: str, | |
| head: Tuple[float, float, float], | |
| tail: Tuple[float, float, float], | |
| connect: bool | |
| ): | |
| bone = edit_bones.new(name) | |
| bone.head = Vector((head[0], head[1], head[2])) | |
| bone.tail = Vector((tail[0], tail[1], tail[2])) | |
| bone.name = name | |
| parent_bone = edit_bones.get(parent_name) | |
| bone.parent = parent_bone | |
| bone.use_connect = connect | |
| assert not np.isnan(head).any(), f"nan found in head of bone {name}" | |
| assert not np.isnan(tail).any(), f"nan found in tail of bone {name}" | |
| for i in range(J): | |
| if add_root is False and i==0: | |
| continue | |
| edit_bones = armature.edit_bones | |
| pname = 'Root' if parents[i] is None else names[parents[i]] | |
| extrude_bone(edit_bones, names[i], pname, joints[i], tails[i], connects[i]) | |
| for i in range(J): | |
| bone = edit_bones.get(names[i]) | |
| bone.head = Vector((joints[i, 0], joints[i, 1], joints[i, 2])) | |
| bone.tail = Vector((tails[i, 0], tails[i, 1], tails[i, 2])) | |
| if vertices is None or skin is None: | |
| return | |
| # must set to object mode to enable parent_set | |
| bpy.ops.object.mode_set(mode='OBJECT') | |
| objects = bpy.data.objects | |
| for o in bpy.context.selected_objects: | |
| o.select_set(False) | |
| ob = objects['character'] | |
| arm = bpy.data.objects['Armature'] | |
| ob.select_set(True) | |
| arm.select_set(True) | |
| bpy.ops.object.parent_set(type='ARMATURE_NAME') | |
| vis = [] | |
| for x in ob.vertex_groups: | |
| vis.append(x.name) | |
| #sparsify | |
| argsorted = np.argsort(-skin, axis=1) | |
| vertex_group_reweight = skin[np.arange(skin.shape[0])[..., None], argsorted] | |
| if group_per_vertex == -1: | |
| group_per_vertex = vertex_group_reweight.shape[-1] | |
| # Ensure we don't access more columns than available | |
| max_groups = min(group_per_vertex, argsorted.shape[1]) | |
| if not do_not_normalize: | |
| vertex_group_sum = vertex_group_reweight[..., :max_groups].sum(axis=1)[..., None] | |
| vertex_group_sum = np.where(vertex_group_sum == 0, 1.0, vertex_group_sum) # Avoid division by zero | |
| vertex_group_reweight = vertex_group_reweight / vertex_group_sum | |
| for v, w in enumerate(skin): | |
| for ii in range(max_groups): | |
| i = argsorted[v, ii] | |
| if i >= J: | |
| continue | |
| n = names[i] | |
| if n not in vis: | |
| continue | |
| ob.vertex_groups[n].add([v], vertex_group_reweight[v, ii], 'REPLACE') | |
| def _clean_bpy(self): | |
| import bpy # type: ignore | |
| for c in bpy.data.actions: | |
| bpy.data.actions.remove(c) | |
| for c in bpy.data.armatures: | |
| bpy.data.armatures.remove(c) | |
| for c in bpy.data.cameras: | |
| bpy.data.cameras.remove(c) | |
| for c in bpy.data.collections: | |
| bpy.data.collections.remove(c) | |
| for c in bpy.data.images: | |
| bpy.data.images.remove(c) | |
| for c in bpy.data.materials: | |
| bpy.data.materials.remove(c) | |
| for c in bpy.data.meshes: | |
| bpy.data.meshes.remove(c) | |
| for c in bpy.data.objects: | |
| bpy.data.objects.remove(c) | |
| for c in bpy.data.textures: | |
| bpy.data.textures.remove(c) | |
| def _export_fbx( | |
| self, | |
| path: str, | |
| vertices: Union[ndarray, None], | |
| joints: ndarray, | |
| skin: Union[ndarray, None], | |
| parents: List[Union[int, None]], | |
| names: List[str], | |
| faces: Union[ndarray, None]=None, | |
| extrude_size: float=0.03, | |
| group_per_vertex: int=-1, | |
| add_root: bool=False, | |
| do_not_normalize: bool=False, | |
| use_extrude_bone: bool=True, | |
| use_connect_unique_child: bool=True, | |
| extrude_from_parent: bool=True, | |
| tails: Union[ndarray, None]=None, | |
| ): | |
| ''' | |
| Requires bpy installed | |
| ''' | |
| import bpy # type: ignore | |
| self._safe_make_dir(path) | |
| self._clean_bpy() | |
| self._make_armature( | |
| vertices=vertices, | |
| joints=joints, | |
| skin=skin, | |
| parents=parents, | |
| names=names, | |
| faces=faces, | |
| extrude_size=extrude_size, | |
| group_per_vertex=group_per_vertex, | |
| add_root=add_root, | |
| do_not_normalize=do_not_normalize, | |
| use_extrude_bone=use_extrude_bone, | |
| use_connect_unique_child=use_connect_unique_child, | |
| extrude_from_parent=extrude_from_parent, | |
| tails=tails, | |
| ) | |
| # always enable add_leaf_bones to keep leaf bones | |
| bpy.ops.export_scene.fbx(filepath=path, check_existing=False, add_leaf_bones=False) | |
| def _export_render( | |
| self, | |
| path: str, | |
| vertices: Union[ndarray, None], | |
| faces: Union[ndarray, None], | |
| bones: Union[ndarray, None], | |
| resolution: Tuple[float, float]=[256, 256], | |
| ): | |
| import bpy # type: ignore | |
| import bpy_extras # type: ignore | |
| from mathutils import Vector # type: ignore | |
| self._safe_make_dir(path) | |
| # normalize into [-1, 1]^3 | |
| # copied from augment | |
| assert (vertices is not None) or (bones is not None) | |
| bounds = [] | |
| if vertices is not None: | |
| bounds.append(vertices) | |
| if bones is not None: | |
| bounds.append(bones[:, :3]) | |
| bounds.append(bones[:, 3:]) | |
| bounds = np.concatenate(bounds, axis=0) | |
| bound_min = bounds.min(axis=0) | |
| bound_max = bounds.max(axis=0) | |
| trans_vertex = np.eye(4) | |
| trans_vertex = _trans_to_m(-(bound_max + bound_min)/2) @ trans_vertex | |
| # scale into the cube [-1, 1] | |
| scale = np.max((bound_max - bound_min) / 2) | |
| trans_vertex = _scale_to_m(1. / scale) @ trans_vertex | |
| def _apply(v: ndarray, trans: ndarray) -> ndarray: | |
| return np.matmul(v, trans[:3, :3].transpose()) + trans[:3, 3] | |
| if vertices is not None: | |
| vertices = _apply(vertices, trans_vertex) | |
| if bones is not None: | |
| bones[:, :3] = _apply(bones[:, :3], trans_vertex) | |
| bones[:, 3:] = _apply(bones[:, 3:], trans_vertex) | |
| # bpy api calls | |
| self._clean_bpy() | |
| bpy.context.scene.render.engine = 'BLENDER_WORKBENCH' | |
| bpy.context.scene.render.film_transparent = True | |
| bpy.context.scene.display.shading.background_type = 'VIEWPORT' | |
| collection = bpy.data.collections.new('new_collection') | |
| bpy.context.scene.collection.children.link(collection) | |
| if vertices is not None: | |
| mesh_data = bpy.data.meshes.new(name="MeshData") | |
| mesh_obj = bpy.data.objects.new(name="MeshObject", object_data=mesh_data) | |
| collection.objects.link(mesh_obj) | |
| mesh_data.from_pydata((vertices).tolist(), [], faces.tolist()) | |
| mesh_data.update() | |
| def look_at(camera, point): | |
| direction = point - camera.location | |
| rot_quat = direction.to_track_quat('-Z', 'Y') | |
| camera.rotation_euler = rot_quat.to_euler() | |
| bpy.ops.object.camera_add(location=(4, -4, 2.5)) | |
| camera = bpy.context.object | |
| camera.data.angle = np.radians(25.0) | |
| look_at(camera, Vector((0, 0, -0.2))) | |
| bpy.context.scene.camera = camera | |
| bpy.context.scene.render.resolution_x = resolution[0] | |
| bpy.context.scene.render.resolution_y = resolution[1] | |
| bpy.context.scene.render.image_settings.file_format = 'PNG' | |
| bpy.context.scene.render.filepath = path | |
| bpy.ops.render.render(write_still=True) | |
| # some AI generated code to draw bones over mesh | |
| if bones is not None: | |
| # TODO: do not save image after rendering | |
| from PIL import Image, ImageDraw | |
| img_pil = Image.open(path).convert("RGBA") | |
| draw = ImageDraw.Draw(img_pil) | |
| from bpy_extras.image_utils import load_image # type: ignore | |
| bpy.context.scene.use_nodes = True | |
| nodes = bpy.context.scene.node_tree.nodes | |
| # nodes.clear() | |
| img = load_image(path) | |
| image_node = nodes.new(type='CompositorNodeImage') | |
| image_node.image = img | |
| for i, bone in enumerate(bones): | |
| head, tail = bone[:3], bone[3:] | |
| head_2d = bpy_extras.object_utils.world_to_camera_view(bpy.context.scene, camera, Vector(head)) | |
| tail_2d = bpy_extras.object_utils.world_to_camera_view(bpy.context.scene, camera, Vector(tail)) | |
| res_x, res_y = resolution | |
| head_pix = (head_2d.x * res_x, (1 - head_2d.y) * res_y) | |
| tail_pix = (tail_2d.x * res_x, (1 - tail_2d.y) * res_y) | |
| draw.line([head_pix, tail_pix], fill=(255, 0, 0, 255), width=1) | |
| img_pil.save(path) | |
| def _trans_to_m(v: ndarray): | |
| m = np.eye(4) | |
| m[0:3, 3] = v | |
| return m | |
| def _scale_to_m(r: ndarray): | |
| m = np.zeros((4, 4)) | |
| m[0, 0] = r | |
| m[1, 1] = r | |
| m[2, 2] = r | |
| m[3, 3] = 1. | |
| return m |