From 9880194f0d97676ba7cbe6dbe4b8f0bb11767ea9 Mon Sep 17 00:00:00 2001 From: William Silversmith Date: Thu, 10 Oct 2024 13:56:11 -0400 Subject: [PATCH 1/4] perf: using new zmesh.chunk_mesh and recursive simplification --- igneous/tasks/mesh/multires.py | 59 +++++++++++++--------------------- 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/igneous/tasks/mesh/multires.py b/igneous/tasks/mesh/multires.py index b44423e..c5f59b9 100644 --- a/igneous/tasks/mesh/multires.py +++ b/igneous/tasks/mesh/multires.py @@ -298,6 +298,7 @@ def generate_lods( assert num_lods >= 0, num_lods lods = [ mesh ] + next_mesh = mesh # from pyfqmr documentation: # threshold = alpha * (iteration + K) ** agressiveness @@ -306,9 +307,9 @@ def generate_lods( # deleting a vertex. for i in range(1, num_lods+1): simplifier = pyfqmr.Simplify() - simplifier.setMesh(mesh.vertices, mesh.faces) + simplifier.setMesh(next_mesh.vertices, next_mesh.faces) simplifier.simplify_mesh( - target_count=max(int(len(mesh.faces) / (decimation_factor ** i)), 4), + target_count=max(int(len(next_mesh.faces) / decimation_factor), 4), aggressiveness=aggressiveness, preserve_border=True, verbose=False, @@ -324,6 +325,7 @@ def generate_lods( lods.append( Mesh(*simplifier.getMesh()) ) + next_mesh = lods[-1] return lods @@ -500,46 +502,29 @@ def create_octree_level_from_mesh(mesh, chunk_shape, lod, num_lods): if lod == num_lods - 1: return ([ mesh ], ((0,0,0),) ) - mesh = trimesh.Trimesh(vertices=mesh.vertices, faces=mesh.faces) - scale = Vec(*(np.array(chunk_shape) * (2 ** lod))) - offset = Vec(*np.floor(mesh.vertices.min(axis=0))) - grid_size = Vec(*np.ceil((mesh.vertices.max(axis=0) - offset) / scale), dtype=int) - - nx, ny, nz = np.eye(3) - ox, oy, oz = offset * np.eye(3) + chunked_meshes = zmesh.chunk_mesh(mesh, scale) + gpts = list(chunked_meshes.keys()) - submeshes = [] nodes = [] - for x in range(0, grid_size.x): - # list(...) required b/c it doesn't like Vec classes - mesh_x = trimesh.intersections.slice_mesh_plane(mesh, plane_normal=nx, plane_origin=list(nx*x*scale.x+ox)) - mesh_x = trimesh.intersections.slice_mesh_plane(mesh_x, plane_normal=-nx, plane_origin=list(nx*(x+1)*scale.x+ox)) - for y in range(0, grid_size.y): - mesh_y = trimesh.intersections.slice_mesh_plane(mesh_x, plane_normal=ny, plane_origin=list(ny*y*scale.y+oy)) - mesh_y = trimesh.intersections.slice_mesh_plane(mesh_y, plane_normal=-ny, plane_origin=list(ny*(y+1)*scale.y+oy)) - for z in range(0, grid_size.z): - mesh_z = trimesh.intersections.slice_mesh_plane(mesh_y, plane_normal=nz, plane_origin=list(nz*z*scale.z+oz)) - mesh_z = trimesh.intersections.slice_mesh_plane(mesh_z, plane_normal=-nz, plane_origin=list(nz*(z+1)*scale.z+oz)) - - if len(mesh_z.vertices) == 0: - continue - - # test for totally degenerate meshes by checking if - # all of two axes match, meaning the mesh must be a - # point or a line. - if np.sum([ np.all(mesh_z.vertices[:,i] == mesh_z.vertices[0,i]) for i in range(3) ]) >= 2: - continue - - submeshes.append(mesh_z) - nodes.append((x, y, z)) + + for gpt in gpts: + mesh = chunked_meshes[gpt] + if mesh.is_empty(): + continue + # test for totally degenerate meshes by checking if + # all of two axes match, meaning the mesh must be a + # point or a line. + if np.sum([ np.all(mesh.vertices[:,i] == mesh.vertices[0,i]) for i in range(3) ]) >= 2: + continue + + nodes.append(gpt) # Sort in Z-curve order - submeshes, nodes = zip( - *sorted(zip(submeshes, nodes), - key=functools.cmp_to_key(lambda x, y: cmp_zorder(x[1], y[1]))) + nodes.sort( + key=functools.cmp_to_key(lambda x, y: cmp_zorder(x, y)) ) - # convert back from trimesh to CV Mesh class - submeshes = [ Mesh(m.vertices, m.faces) for m in submeshes ] + + submeshes = [ chunked_meshes[node] for node in nodes ] return (submeshes, nodes) From 352fc7787ba8223c8c2185574c0d9485a77322b3 Mon Sep 17 00:00:00 2001 From: William Silversmith Date: Mon, 4 Nov 2024 17:51:42 -0500 Subject: [PATCH 2/4] fix: ensure the octree code always works by using the old code on error --- igneous/tasks/mesh/multires.py | 65 ++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/igneous/tasks/mesh/multires.py b/igneous/tasks/mesh/multires.py index c5f59b9..48b3a13 100644 --- a/igneous/tasks/mesh/multires.py +++ b/igneous/tasks/mesh/multires.py @@ -499,6 +499,17 @@ def create_octree_level_from_mesh(mesh, chunk_shape, lod, num_lods): This creates (2^lod)^3 submeshes. """ + try: + return create_octree_level_from_mesh_accelerated(mesh, chunk_shape, lod, num_lods) + except RuntimeError: + return create_octree_level_from_mesh_generalized(mesh, chunk_shape, lod, num_lods) + +def create_octree_level_from_mesh_accelerated(mesh, chunk_shape, lod, num_lods): + """ + Uses zmesh.chunk_mesh to accelerate slicing meshes, but it can only handle + vertices that are one 26-connected grid space apart and raises a RuntimeError + if this condition isn't met. + """ if lod == num_lods - 1: return ([ mesh ], ((0,0,0),) ) @@ -528,3 +539,57 @@ def create_octree_level_from_mesh(mesh, chunk_shape, lod, num_lods): submeshes = [ chunked_meshes[node] for node in nodes ] return (submeshes, nodes) + +def create_octree_level_from_mesh_generalized(mesh, chunk_shape, lod, num_lods): + """ + Create submeshes by slicing the orignal mesh to produce smaller chunks + by slicing them from x,y,z dimensions. + + This creates (2^lod)^3 submeshes. + """ + if lod == num_lods - 1: + return ([ mesh ], ((0,0,0),) ) + + mesh = trimesh.Trimesh(vertices=mesh.vertices, faces=mesh.faces) + + scale = Vec(*(np.array(chunk_shape) * (2 ** lod))) + offset = Vec(*np.floor(mesh.vertices.min(axis=0))) + grid_size = Vec(*np.ceil((mesh.vertices.max(axis=0) - offset) / scale), dtype=int) + + nx, ny, nz = np.eye(3) + ox, oy, oz = offset * np.eye(3) + + submeshes = [] + nodes = [] + for x in range(0, grid_size.x): + # list(...) required b/c it doesn't like Vec classes + mesh_x = trimesh.intersections.slice_mesh_plane(mesh, plane_normal=nx, plane_origin=list(nx*x*scale.x+ox)) + mesh_x = trimesh.intersections.slice_mesh_plane(mesh_x, plane_normal=-nx, plane_origin=list(nx*(x+1)*scale.x+ox)) + for y in range(0, grid_size.y): + mesh_y = trimesh.intersections.slice_mesh_plane(mesh_x, plane_normal=ny, plane_origin=list(ny*y*scale.y+oy)) + mesh_y = trimesh.intersections.slice_mesh_plane(mesh_y, plane_normal=-ny, plane_origin=list(ny*(y+1)*scale.y+oy)) + for z in range(0, grid_size.z): + mesh_z = trimesh.intersections.slice_mesh_plane(mesh_y, plane_normal=nz, plane_origin=list(nz*z*scale.z+oz)) + mesh_z = trimesh.intersections.slice_mesh_plane(mesh_z, plane_normal=-nz, plane_origin=list(nz*(z+1)*scale.z+oz)) + + if len(mesh_z.vertices) == 0: + continue + + # test for totally degenerate meshes by checking if + # all of two axes match, meaning the mesh must be a + # point or a line. + if np.sum([ np.all(mesh_z.vertices[:,i] == mesh_z.vertices[0,i]) for i in range(3) ]) >= 2: + continue + + submeshes.append(mesh_z) + nodes.append((x, y, z)) + + # Sort in Z-curve order + submeshes, nodes = zip( + *sorted(zip(submeshes, nodes), + key=functools.cmp_to_key(lambda x, y: cmp_zorder(x[1], y[1]))) + ) + # convert back from trimesh to CV Mesh class + submeshes = [ Mesh(m.vertices, m.faces) for m in submeshes ] + + return (submeshes, nodes) From fc26bbb98af110bfcc2a9bc60db67cd0b7ac749b Mon Sep 17 00:00:00 2001 From: William Silversmith Date: Fri, 8 Nov 2024 16:07:18 -0500 Subject: [PATCH 3/4] feat: use alternative codec neuroglancer when special codecs are used --- igneous_cli/cli.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/igneous_cli/cli.py b/igneous_cli/cli.py index ab9c6b6..7565779 100644 --- a/igneous_cli/cli.py +++ b/igneous_cli/cli.py @@ -1617,9 +1617,8 @@ def memory_used(data_width, shape, factor): @click.argument("path", type=CloudPath()) @click.option('--browser/--no-browser', default=True, is_flag=True, help="Open the dataset in the system's default web browser.") @click.option('--port', default=1337, help="localhost server port for the file server.", show_default=True) -@click.option('--ng', default="https://neuroglancer-demo.appspot.com/", help="Alternative Neuroglancer webpage to use.", show_default=True) +@click.option('--ng', default=None, help="Alternative Neuroglancer webpage to use.", show_default=True) @click.option('--pos', type=Tuple3(), default=None, help="Position in volume to open to.", show_default=True) - def view(path, browser, port, ng, pos): """ Open an on-disk dataset for viewing in neuroglancer. @@ -1688,6 +1687,17 @@ def view(path, browser, port, ng, pos): config["layers"][0]["shader"] = rgb_shader fragment = urllib.parse.quote(jsonify(config)) + + has_alternative_codec = any([ + scale["encoding"] in ["crackle", "zfpc", "kempressed", "fpzip"] + for scale in cv.scales + ]) + + if ng is None: + if has_alternative_codec: + ng = "https://allcodecs-dot-neuromancer-seung-import.appspot.com/" + else: + ng = "https://neuroglancer-demo.appspot.com/" url = f"{ng}#!{fragment}" if browser: From b6539687d13da8490866ee91efaa695ccd0e957a Mon Sep 17 00:00:00 2001 From: William Silversmith Date: Wed, 13 Nov 2024 14:13:19 -0500 Subject: [PATCH 4/4] fix: handle chunk_size is specified in ds_shape Numpy deprecation --- igneous/task_creation/image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/igneous/task_creation/image.py b/igneous/task_creation/image.py index d72e79f..14fee11 100644 --- a/igneous/task_creation/image.py +++ b/igneous/task_creation/image.py @@ -244,7 +244,7 @@ def create_downsampling_tasks( """ def ds_shape(mip, chunk_size=None, factor=None): nonlocal num_mips - if chunk_size: + if chunk_size is not None: shape = Vec(*chunk_size) else: shape = vol.meta.chunk_size(mip)[:3]