From 2b75137f0f0e4c8681995c10685f8cc006ec3448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20S=2E=20Dokken?= Date: Mon, 2 Feb 2026 09:43:06 +0100 Subject: [PATCH 01/14] Start refactoring --- pyproject.toml | 85 +++++++++++++++++++ .../scripts => src/emimesh}/cle_patch.py | 0 src/emimesh/clients/generate_mesh.py | 70 +++++++++++++++ .../scripts => src/emimesh}/download_data.py | 0 .../scripts => src/emimesh}/evaluate_mesh.py | 0 .../emimesh}/extract_surfaces.py | 0 .../emimesh}/generate_analysis_plots.py | 0 .../scripts => src/emimesh}/generate_mesh.py | 0 .../emimesh}/generate_screenshot.py | 0 .../emimesh}/generate_surface_tags.py | 0 .../scripts => src/emimesh}/k3d_headless.py | 0 .../scripts => src/emimesh}/overview_table.py | 0 .../scripts => src/emimesh}/plot_utils.py | 0 .../emimesh}/process_image_data.py | 0 .../emimesh}/process_image_data_opencl.py | 0 .../scripts => src/emimesh}/profile_pycle.py | 0 .../emimesh}/seperate_touching_cells.py | 0 {workflow/scripts => src/emimesh}/utils.py | 0 workflow/Snakefile | 2 +- 19 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 pyproject.toml rename {workflow/scripts => src/emimesh}/cle_patch.py (100%) create mode 100644 src/emimesh/clients/generate_mesh.py rename {workflow/scripts => src/emimesh}/download_data.py (100%) rename {workflow/scripts => src/emimesh}/evaluate_mesh.py (100%) rename {workflow/scripts => src/emimesh}/extract_surfaces.py (100%) rename {workflow/scripts => src/emimesh}/generate_analysis_plots.py (100%) rename {workflow/scripts => src/emimesh}/generate_mesh.py (100%) rename {workflow/scripts => src/emimesh}/generate_screenshot.py (100%) rename {workflow/scripts => src/emimesh}/generate_surface_tags.py (100%) rename {workflow/scripts => src/emimesh}/k3d_headless.py (100%) rename {workflow/scripts => src/emimesh}/overview_table.py (100%) rename {workflow/scripts => src/emimesh}/plot_utils.py (100%) rename {workflow/scripts => src/emimesh}/process_image_data.py (100%) rename {workflow/scripts => src/emimesh}/process_image_data_opencl.py (100%) rename {workflow/scripts => src/emimesh}/profile_pycle.py (100%) rename {workflow/scripts => src/emimesh}/seperate_touching_cells.py (100%) rename {workflow/scripts => src/emimesh}/utils.py (100%) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b12c704 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,85 @@ +[build-system] # Require setuptool version due to https://github.com/pypa/setuptools/issues/2938 +requires = ["setuptools>=61.0.0", "wheel"] + +[project] +dependencies = [ + "matplotlib", + "pyvista", + "scikit-image", + "meshio", + "dask-image", + "seaborn", + "pyacvd", + "pytetwild", + "fastremap", + "cloud-volume", + "webknossos", + "connected-components-3d", + "cmocean", + "nbmorph", +] +name = "EMIMesh" +version = "1.0.0.dev0" +description = "Generating high-quality extracellular-membrane-intracellular meshes from imaging data" +authors = [{ name = "Marius Causemann", email = "mariusca@simula.no" }] +license = { file = "LICENSE" } +readme = "readme.md" + + +[project.scripts] +emi-generate-mesh = "emimesh.clients.generate_mesh:main" + +[project.optional-dependencies] +test = [] +dev = ["pdbpp", "ipython", "mypy", "ruff"] +h5py = ["h5py"] +docs = [ + "jupyter-book<2.0.0", + "jupytext", + "ipykernel<7.0.0", # Note: Remove once https://github.com/ipython/ipykernel/issues/1450 is in a release + "sphinx-codeautolink", +] +all = ["EMIMesh[test,dev,docs]"] + +[tool.pytest.ini_options] +addopts = ["--import-mode=importlib"] +testpaths = ["tests"] + +[tool.mypy] +ignore_missing_imports = true +# Folders to exclude +exclude = ["docs/", "build/"] +# Folder to check with mypy +files = ["src", "tests"] + +[tool.ruff] +src = ["src", "tests", "docs"] +line-length = 100 +indent-width = 4 + +[tool.ruff.lint] +select = [ + # Pyflakes + "F", + # Pycodestyle + "E", + "W", + # isort + "I001", +] + + +[tool.ruff.lint.isort] +known-first-party = ["EMIMesh"] +known-third-party = ["numpy", "pytest"] +section-order = [ + "future", + "standard-library", + "mpi", + "third-party", + "first-party", + "local-folder", +] + +[tool.ruff.lint.isort.sections] +"mpi" = ["mpi4py", "petsc4py"] diff --git a/workflow/scripts/cle_patch.py b/src/emimesh/cle_patch.py similarity index 100% rename from workflow/scripts/cle_patch.py rename to src/emimesh/cle_patch.py diff --git a/src/emimesh/clients/generate_mesh.py b/src/emimesh/clients/generate_mesh.py new file mode 100644 index 0000000..ea58abd --- /dev/null +++ b/src/emimesh/clients/generate_mesh.py @@ -0,0 +1,70 @@ +import json +import pyvista as pv +import argparse +import numpy as np +import time + +def get_values(d): + for v in d.values(): + if isinstance(v, dict): + yield from get_values(v) + else: + yield v + +def mesh_surfaces(csg_tree_path, eps, stop_quality, max_threads): + from pytetwild import tetrahedralize_csg + start = time.time() + mesh = tetrahedralize_csg(csg_tree_path, epsilon=eps, edge_length_r=eps*50, + coarsen=True, stop_energy=stop_quality, + num_threads=max_threads).clean() + print("meshing finished!") + mesh["label"] = mesh["marker"] + mesh.field_data['runtime'] = time.time() - start + mesh.field_data['threads'] = max_threads + return mesh + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--csgtree", + help="path to csgtree file", + type=str, + ) + parser.add_argument( + "--envelopsize", + help="absolut size of fTetWild surface envelop (in nm)", + type=float, + ) + parser.add_argument( + "--stopquality", help="fTetWild mesh quality score", type=float, default=10 + ) + parser.add_argument( + "--output", + help="output filename", + type=str, + ) + parser.add_argument( + "--max_threads", help="max number of threads", type=int, default=1 + ) + + args = parser.parse_args() + with open(args.csgtree) as f: + csgtree = json.load(f) + surfs = [surf for surf in get_values(csgtree) if "ply" in surf] + roifile = [s for s in surfs if "roi.ply" in s][0] + roi = pv.read(roifile) + diag = np.sqrt(3) * roi.volume ** (1 / 3) + abs_eps = args.envelopsize + volmesh = mesh_surfaces( + args.csgtree, + eps=abs_eps / diag, + stop_quality=args.stopquality, + max_threads=args.max_threads, + ) + pv.save_meshio(args.output, volmesh) + print(volmesh.array_names) + + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/workflow/scripts/download_data.py b/src/emimesh/download_data.py similarity index 100% rename from workflow/scripts/download_data.py rename to src/emimesh/download_data.py diff --git a/workflow/scripts/evaluate_mesh.py b/src/emimesh/evaluate_mesh.py similarity index 100% rename from workflow/scripts/evaluate_mesh.py rename to src/emimesh/evaluate_mesh.py diff --git a/workflow/scripts/extract_surfaces.py b/src/emimesh/extract_surfaces.py similarity index 100% rename from workflow/scripts/extract_surfaces.py rename to src/emimesh/extract_surfaces.py diff --git a/workflow/scripts/generate_analysis_plots.py b/src/emimesh/generate_analysis_plots.py similarity index 100% rename from workflow/scripts/generate_analysis_plots.py rename to src/emimesh/generate_analysis_plots.py diff --git a/workflow/scripts/generate_mesh.py b/src/emimesh/generate_mesh.py similarity index 100% rename from workflow/scripts/generate_mesh.py rename to src/emimesh/generate_mesh.py diff --git a/workflow/scripts/generate_screenshot.py b/src/emimesh/generate_screenshot.py similarity index 100% rename from workflow/scripts/generate_screenshot.py rename to src/emimesh/generate_screenshot.py diff --git a/workflow/scripts/generate_surface_tags.py b/src/emimesh/generate_surface_tags.py similarity index 100% rename from workflow/scripts/generate_surface_tags.py rename to src/emimesh/generate_surface_tags.py diff --git a/workflow/scripts/k3d_headless.py b/src/emimesh/k3d_headless.py similarity index 100% rename from workflow/scripts/k3d_headless.py rename to src/emimesh/k3d_headless.py diff --git a/workflow/scripts/overview_table.py b/src/emimesh/overview_table.py similarity index 100% rename from workflow/scripts/overview_table.py rename to src/emimesh/overview_table.py diff --git a/workflow/scripts/plot_utils.py b/src/emimesh/plot_utils.py similarity index 100% rename from workflow/scripts/plot_utils.py rename to src/emimesh/plot_utils.py diff --git a/workflow/scripts/process_image_data.py b/src/emimesh/process_image_data.py similarity index 100% rename from workflow/scripts/process_image_data.py rename to src/emimesh/process_image_data.py diff --git a/workflow/scripts/process_image_data_opencl.py b/src/emimesh/process_image_data_opencl.py similarity index 100% rename from workflow/scripts/process_image_data_opencl.py rename to src/emimesh/process_image_data_opencl.py diff --git a/workflow/scripts/profile_pycle.py b/src/emimesh/profile_pycle.py similarity index 100% rename from workflow/scripts/profile_pycle.py rename to src/emimesh/profile_pycle.py diff --git a/workflow/scripts/seperate_touching_cells.py b/src/emimesh/seperate_touching_cells.py similarity index 100% rename from workflow/scripts/seperate_touching_cells.py rename to src/emimesh/seperate_touching_cells.py diff --git a/workflow/scripts/utils.py b/src/emimesh/utils.py similarity index 100% rename from workflow/scripts/utils.py rename to src/emimesh/utils.py diff --git a/workflow/Snakefile b/workflow/Snakefile index 72d4c5e..f4874a1 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -136,7 +136,7 @@ rule generateMesh: options=lambda wildcards: filename2options(wildcards.meshing) shell: """ - python workflow/scripts/generate_mesh.py \ + emi-generate-mesh \ --csgtree {input.csgtree} \ {params.options} \ --output {output.outfile} \ From 4434faab84691cf30a1487db62d777c0f80d8d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20S=2E=20Dokken?= Date: Mon, 2 Feb 2026 10:16:27 +0100 Subject: [PATCH 02/14] Continue restructuring --- src/emimesh/clients/download_data.py | 52 ++++++++++++++++++++++++++ src/emimesh/clients/generate_mesh.py | 13 +------ src/emimesh/download_data.py | 50 +------------------------ src/emimesh/generate_mesh.py | 56 ++-------------------------- 4 files changed, 58 insertions(+), 113 deletions(-) create mode 100644 src/emimesh/clients/download_data.py diff --git a/src/emimesh/clients/download_data.py b/src/emimesh/clients/download_data.py new file mode 100644 index 0000000..1935ab8 --- /dev/null +++ b/src/emimesh/clients/download_data.py @@ -0,0 +1,52 @@ +import argparse +from pathlib import Path +from emimesh.utils import np2pv +from pathlib import Path +import argparse + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--cloudpath", + help="path to cloud data", + type=str, + default="precomputed://gs://iarpa_microns/minnie/minnie65/seg", + ) + parser.add_argument("--mip", help="resolution (0 is highest)", type=int, default=0) + parser.add_argument( + "--position", + help="point position in x-y-z integer pixel position \ + (can be copied from neuroglancer)", + type=str, + ) + parser.add_argument( + "--size", + help="cube side length of the volume to be downloaded (in nm)", + type=str, + default=1000, + ) + parser.add_argument( + "--output", help="output filename", type=str, default="data.xdmf" + ) + + args = parser.parse_args() + + position = args.position.split("-") + try: + size = [int(args.size)] * 3 + except ValueError: + size = [int(s) for s in args.size.split("-")] + + try: + img,res = download_cloudvolume(args.cloudpath, args.mip, position, size) + except: + img,res = download_webknossos(args.cloudpath, args.mip, position, size) + + print(res) + data = np2pv(img, res) + Path(args.output).parent.mkdir(exist_ok=True, parents=True) + data.save(args.output) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/emimesh/clients/generate_mesh.py b/src/emimesh/clients/generate_mesh.py index ea58abd..d98d3b6 100644 --- a/src/emimesh/clients/generate_mesh.py +++ b/src/emimesh/clients/generate_mesh.py @@ -2,7 +2,7 @@ import pyvista as pv import argparse import numpy as np -import time +from emimesh import mesh_surfaces def get_values(d): for v in d.values(): @@ -11,17 +11,6 @@ def get_values(d): else: yield v -def mesh_surfaces(csg_tree_path, eps, stop_quality, max_threads): - from pytetwild import tetrahedralize_csg - start = time.time() - mesh = tetrahedralize_csg(csg_tree_path, epsilon=eps, edge_length_r=eps*50, - coarsen=True, stop_energy=stop_quality, - num_threads=max_threads).clean() - print("meshing finished!") - mesh["label"] = mesh["marker"] - mesh.field_data['runtime'] = time.time() - start - mesh.field_data['threads'] = max_threads - return mesh def main(): parser = argparse.ArgumentParser() diff --git a/src/emimesh/download_data.py b/src/emimesh/download_data.py index 832eb7a..2309a94 100644 --- a/src/emimesh/download_data.py +++ b/src/emimesh/download_data.py @@ -1,8 +1,6 @@ import numpy as np -import pyvista as pv -import argparse -from utils import np2pv -from pathlib import Path + +__all__ = ["download_webknossos", "download_cloudvolume"] def download_webknossos(cloud_path, mip, pos, physical_size): import webknossos as wk @@ -37,47 +35,3 @@ def download_cloudvolume(cloud_path, mip, pos, physical_size): img = vol.download_point(pos, mip=mip, size=size).squeeze() return img, vol.resolution - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--cloudpath", - help="path to cloud data", - type=str, - default="precomputed://gs://iarpa_microns/minnie/minnie65/seg", - ) - parser.add_argument("--mip", help="resolution (0 is highest)", type=int, default=0) - parser.add_argument( - "--position", - help="point position in x-y-z integer pixel position \ - (can be copied from neuroglancer)", - type=str, - ) - parser.add_argument( - "--size", - help="cube side length of the volume to be downloaded (in nm)", - type=str, - default=1000, - ) - parser.add_argument( - "--output", help="output filename", type=str, default="data.xdmf" - ) - - args = parser.parse_args() - - position = args.position.split("-") - try: - size = [int(args.size)] * 3 - except ValueError: - size = [int(s) for s in args.size.split("-")] - - try: - img,res = download_cloudvolume(args.cloudpath, args.mip, position, size) - except: - img,res = download_webknossos(args.cloudpath, args.mip, position, size) - - print(res) - data = np2pv(img, res) - Path(args.output).parent.mkdir(exist_ok=True, parents=True) - data.save(args.output) diff --git a/src/emimesh/generate_mesh.py b/src/emimesh/generate_mesh.py index 5e690db..0eaac26 100644 --- a/src/emimesh/generate_mesh.py +++ b/src/emimesh/generate_mesh.py @@ -1,18 +1,10 @@ -import json -import pyvista as pv -import argparse -import numpy as np import time -def get_values(d): - for v in d.values(): - if isinstance(v, dict): - yield from get_values(v) - else: - yield v +from pytetwild import tetrahedralize_csg + +__all__ = ["make_surfaces"] def mesh_surfaces(csg_tree_path, eps, stop_quality, max_threads): - from pytetwild import tetrahedralize_csg start = time.time() mesh = tetrahedralize_csg(csg_tree_path, epsilon=eps, edge_length_r=eps*50, coarsen=True, stop_energy=stop_quality, @@ -22,45 +14,3 @@ def mesh_surfaces(csg_tree_path, eps, stop_quality, max_threads): mesh.field_data['runtime'] = time.time() - start mesh.field_data['threads'] = max_threads return mesh - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--csgtree", - help="path to csgtree file", - type=str, - ) - parser.add_argument( - "--envelopsize", - help="absolut size of fTetWild surface envelop (in nm)", - type=float, - ) - parser.add_argument( - "--stopquality", help="fTetWild mesh quality score", type=float, default=10 - ) - parser.add_argument( - "--output", - help="output filename", - type=str, - ) - parser.add_argument( - "--max_threads", help="max number of threads", type=int, default=1 - ) - - args = parser.parse_args() - with open(args.csgtree) as f: - csgtree = json.load(f) - surfs = [surf for surf in get_values(csgtree) if "ply" in surf] - roifile = [s for s in surfs if "roi.ply" in s][0] - roi = pv.read(roifile) - diag = np.sqrt(3) * roi.volume ** (1 / 3) - abs_eps = args.envelopsize - volmesh = mesh_surfaces( - args.csgtree, - eps=abs_eps / diag, - stop_quality=args.stopquality, - max_threads=args.max_threads, - ) - pv.save_meshio(args.output, volmesh) - print(volmesh.array_names) - From 5c1b1b1364181efaf65356d02c5fd00977f6639c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20S=2E=20Dokken?= Date: Mon, 2 Feb 2026 10:39:28 +0100 Subject: [PATCH 03/14] Futher simplifications --- pyproject.toml | 4 ++ src/emimesh/clients/analysis_plot.py | 22 ++++++++ src/emimesh/clients/evaluate_mesh.py | 63 +++++++++++++++++++++++ src/emimesh/clients/extract_surfaces.py | 68 +++++++++++++++++++++++++ src/emimesh/evaluate_mesh.py | 56 +------------------- src/emimesh/extract_surfaces.py | 65 ++--------------------- src/emimesh/generate_analysis_plots.py | 22 +------- workflow/Snakefile | 6 +-- 8 files changed, 165 insertions(+), 141 deletions(-) create mode 100644 src/emimesh/clients/analysis_plot.py create mode 100644 src/emimesh/clients/evaluate_mesh.py create mode 100644 src/emimesh/clients/extract_surfaces.py diff --git a/pyproject.toml b/pyproject.toml index b12c704..d19cdf7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,10 @@ readme = "readme.md" [project.scripts] emi-generate-mesh = "emimesh.clients.generate_mesh:main" +emi-download = "emimesh.clients.download_data:main" +emi-evaluate-mesh = "emimesh.clients.evaluate_mesh:main" +emi-extract-surfaces = "emimesh.clients.extract_surfaces:main" +emi-plot-analysis = "emimesh.clients.analysis_plot:main" [project.optional-dependencies] test = [] diff --git a/src/emimesh/clients/analysis_plot.py b/src/emimesh/clients/analysis_plot.py new file mode 100644 index 0000000..1c43f92 --- /dev/null +++ b/src/emimesh/clients/analysis_plot.py @@ -0,0 +1,22 @@ +import argparse +import yaml +from emimesh.generate_analysis_plots import plot_cell_sizes + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--infile", + help="path to mesh statistics infile", + type=str, + ) + parser.add_argument( + "--output", + help="outfile name", + type=str, + ) + args = parser.parse_args() + + with open(args.infile) as infile: + mesh_statistic = yaml.load(infile, Loader=yaml.FullLoader) + + plot_cell_sizes(mesh_statistic, args.output) diff --git a/src/emimesh/clients/evaluate_mesh.py b/src/emimesh/clients/evaluate_mesh.py new file mode 100644 index 0000000..528d262 --- /dev/null +++ b/src/emimesh/clients/evaluate_mesh.py @@ -0,0 +1,63 @@ +import argparse +import pyvista as pv +import numpy as np + +import yaml +from emimesh.evaluate_mesh import compute_surface_volume, ecs_id, lstr + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--infile", + help="path to mesh file", + type=str, + ) + parser.add_argument( + "--output", + help="outfile name", + type=str, + ) + + args = parser.parse_args() + mesh = pv.read_meshio(args.infile) + cell_ids = list(np.unique(mesh[lstr])) + cell_ids.remove(ecs_id) + #ecs_width, ecs_cell_volume = compute_local_width(mesh, ecs_id, cell_ids=cell_ids) + cell_volume, cell_surface = compute_surface_volume(mesh, cell_ids) + ecs_volume, ecs_surface = compute_surface_volume(mesh, [ecs_id]) + ecs_volume, ecs_surface = ecs_volume[0], ecs_surface[0] + mesh_volume = ecs_volume + sum(cell_volume) + ecs_share = ecs_volume / mesh_volume + ecs_mesh = mesh.extract_cells(mesh[lstr]==ecs_id) + ecs_surf_mesh = ecs_mesh.extract_surface() + boundary_mesh = mesh.extract_surface() + cell_boundary_mesh = boundary_mesh.extract_cells(boundary_mesh[lstr]>ecs_id) + ecs_boundary_mesh = boundary_mesh.extract_cells(boundary_mesh[lstr]==ecs_id) + n_ecs_boundary_points = boundary_mesh.number_of_points - cell_boundary_mesh.number_of_points + + results = dict(npoints=mesh.number_of_points, + ncompcells=mesh.number_of_cells, + ecs_volume=ecs_volume, ecs_surface=ecs_surface, + cell_surface=cell_surface, cell_volume=cell_volume, + ecs_share=ecs_share, + npoints_membrane=ecs_surf_mesh.number_of_points - n_ecs_boundary_points, + npoints_boundary=boundary_mesh.number_of_points, + npoints_ecs=ecs_mesh.number_of_points, + ntets_ecs=ecs_mesh.number_of_cells, + ntets_ics=mesh.number_of_cells - ecs_mesh.number_of_cells, + nfacets_membrane = ecs_surf_mesh.number_of_cells - ecs_boundary_mesh.number_of_cells, + #ecs_width=ecs_width, ecs_cell_volume=ecs_cell_volume, + ) + if "runtime" in mesh.array_names: + results["runtime"] = mesh["runtime"] + if "threads" in mesh.array_names: + results["threads"] = mesh["threads"] + + with open(args.output, "w") as outfile: + yaml.dump(results, outfile) + + #plot_local_width(width, volume, Path(args.infile).parent / "local_width.png") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/emimesh/clients/extract_surfaces.py b/src/emimesh/clients/extract_surfaces.py new file mode 100644 index 0000000..029a85b --- /dev/null +++ b/src/emimesh/clients/extract_surfaces.py @@ -0,0 +1,68 @@ +import argparse +import pathlib as Path +import pyvista as pv + +from emimesh.extract_surfaces import extract_surface, clip_closed_box, extract_cell_meshes,create_balanced_csg_tree +import numpy as np +import json +import fastremap +from emimesh.utils import get_bounding_box +import argparse + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "--infile", + help="path to image data", + type=str, + ) + parser.add_argument( + "--outdir", help="directory for output", type=str, default="output" + ) + parser.add_argument( + "--ncpus", help="number oc cores, default 1", type=int, default=1 + ) + args = parser.parse_args() + outdir = Path(args.outdir) + print(f"reading file: {args.infile}") + img_grid = pv.read(args.infile) + dims = img_grid.dimensions + resolution = img_grid.spacing + img = img_grid["data"].reshape(dims - np.array([1, 1, 1]), order="F") + cell_labels, cell_counts = fastremap.unique(img, return_counts=True) + cell_labels = list(cell_labels[np.argsort(cell_counts)]) + cell_labels.remove(0) + + outerbox = get_bounding_box(img_grid, resolution[0]*5 + img_grid.length*0.002) + + if "roimask" in img_grid.array_names: + roimask = img_grid["roimask"].reshape(dims - np.array([1, 1, 1]), + order="F") + roipadded = np.pad(roimask, 1) + grid = pv.ImageData(dimensions=roipadded.shape, spacing=resolution, + origin=(0, 0, 0)) + roisurf = extract_surface(roipadded, grid, mesh_reduction_factor=10, + taubin_smooth_iter=5) + + clip_closed_box(roisurf, outerbox) + else: + roisurf = outerbox + roi_file = outdir / "roi.ply" + + surfs = extract_cell_meshes( + img, + cell_labels[::-1], + resolution, + mesh_reduction_factor=10, + taubin_smooth_iter=5, + write_dir=outdir, + ncpus=args.ncpus + ) + + mesh_files = [outdir / f"{cid}.ply" for cid in surfs if cid] + roisurf.save(roi_file) + csg_tree = create_balanced_csg_tree([str(f) for f in mesh_files]) + csg_tree = create_balanced_csg_tree([str(roi_file), csg_tree]) + csg_tree = {"operation":"intersection","right":csg_tree, "left":str(roi_file)} + with open(outdir / "csgtree.json", "w") as outfile: + outfile.write(json.dumps(csg_tree)) diff --git a/src/emimesh/evaluate_mesh.py b/src/emimesh/evaluate_mesh.py index c9b57f1..c790f16 100644 --- a/src/emimesh/evaluate_mesh.py +++ b/src/emimesh/evaluate_mesh.py @@ -1,7 +1,5 @@ -import pyvista as pv + import numpy as np -import argparse -import yaml import matplotlib.pyplot as plt import seaborn as sns @@ -57,55 +55,3 @@ def plot_local_width(width, volume, filename): plt.tight_layout() plt.savefig(filename) -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--infile", - help="path to mesh file", - type=str, - ) - parser.add_argument( - "--output", - help="outfile name", - type=str, - ) - - args = parser.parse_args() - mesh = pv.read_meshio(args.infile) - cell_ids = list(np.unique(mesh[lstr])) - cell_ids.remove(ecs_id) - #ecs_width, ecs_cell_volume = compute_local_width(mesh, ecs_id, cell_ids=cell_ids) - cell_volume, cell_surface = compute_surface_volume(mesh, cell_ids) - ecs_volume, ecs_surface = compute_surface_volume(mesh, [ecs_id]) - ecs_volume, ecs_surface = ecs_volume[0], ecs_surface[0] - mesh_volume = ecs_volume + sum(cell_volume) - ecs_share = ecs_volume / mesh_volume - ecs_mesh = mesh.extract_cells(mesh[lstr]==ecs_id) - ecs_surf_mesh = ecs_mesh.extract_surface() - boundary_mesh = mesh.extract_surface() - cell_boundary_mesh = boundary_mesh.extract_cells(boundary_mesh[lstr]>ecs_id) - ecs_boundary_mesh = boundary_mesh.extract_cells(boundary_mesh[lstr]==ecs_id) - n_ecs_boundary_points = boundary_mesh.number_of_points - cell_boundary_mesh.number_of_points - - results = dict(npoints=mesh.number_of_points, - ncompcells=mesh.number_of_cells, - ecs_volume=ecs_volume, ecs_surface=ecs_surface, - cell_surface=cell_surface, cell_volume=cell_volume, - ecs_share=ecs_share, - npoints_membrane=ecs_surf_mesh.number_of_points - n_ecs_boundary_points, - npoints_boundary=boundary_mesh.number_of_points, - npoints_ecs=ecs_mesh.number_of_points, - ntets_ecs=ecs_mesh.number_of_cells, - ntets_ics=mesh.number_of_cells - ecs_mesh.number_of_cells, - nfacets_membrane = ecs_surf_mesh.number_of_cells - ecs_boundary_mesh.number_of_cells, - #ecs_width=ecs_width, ecs_cell_volume=ecs_cell_volume, - ) - if "runtime" in mesh.array_names: - results["runtime"] = mesh["runtime"] - if "threads" in mesh.array_names: - results["threads"] = mesh["threads"] - - with open(args.output, "w") as outfile: - yaml.dump(results, outfile) - - #plot_local_width(width, volume, Path(args.infile).parent / "local_width.png") \ No newline at end of file diff --git a/src/emimesh/extract_surfaces.py b/src/emimesh/extract_surfaces.py index 4dd0f01..e0f4fa4 100644 --- a/src/emimesh/extract_surfaces.py +++ b/src/emimesh/extract_surfaces.py @@ -1,12 +1,10 @@ -import os + import numpy as np import pyvista as pv -import json + import pyacvd -import argparse + from pathlib import Path -from utils import get_bounding_box -import fastremap import sys import shutil import itertools @@ -156,60 +154,3 @@ def create_balanced_csg_tree(surface_files): , "right": create_balanced_csg_tree(surface_files[int(n/2):])} return surface_files[0] -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--infile", - help="path to image data", - type=str, - ) - parser.add_argument( - "--outdir", help="directory for output", type=str, default="output" - ) - parser.add_argument( - "--ncpus", help="number oc cores, default 1", type=int, default=1 - ) - args = parser.parse_args() - outdir = Path(args.outdir) - print(f"reading file: {args.infile}") - img_grid = pv.read(args.infile) - dims = img_grid.dimensions - resolution = img_grid.spacing - img = img_grid["data"].reshape(dims - np.array([1, 1, 1]), order="F") - cell_labels, cell_counts = fastremap.unique(img, return_counts=True) - cell_labels = list(cell_labels[np.argsort(cell_counts)]) - cell_labels.remove(0) - - outerbox = get_bounding_box(img_grid, resolution[0]*5 + img_grid.length*0.002) - - if "roimask" in img_grid.array_names: - roimask = img_grid["roimask"].reshape(dims - np.array([1, 1, 1]), - order="F") - roipadded = np.pad(roimask, 1) - grid = pv.ImageData(dimensions=roipadded.shape, spacing=resolution, - origin=(0, 0, 0)) - roisurf = extract_surface(roipadded, grid, mesh_reduction_factor=10, - taubin_smooth_iter=5) - - clip_closed_box(roisurf, outerbox) - else: - roisurf = outerbox - roi_file = outdir / "roi.ply" - - surfs = extract_cell_meshes( - img, - cell_labels[::-1], - resolution, - mesh_reduction_factor=10, - taubin_smooth_iter=5, - write_dir=outdir, - ncpus=args.ncpus - ) - - mesh_files = [outdir / f"{cid}.ply" for cid in surfs if cid] - roisurf.save(roi_file) - csg_tree = create_balanced_csg_tree([str(f) for f in mesh_files]) - csg_tree = create_balanced_csg_tree([str(roi_file), csg_tree]) - csg_tree = {"operation":"intersection","right":csg_tree, "left":str(roi_file)} - with open(outdir / "csgtree.json", "w") as outfile: - outfile.write(json.dumps(csg_tree)) diff --git a/src/emimesh/generate_analysis_plots.py b/src/emimesh/generate_analysis_plots.py index cad93ec..489d3a4 100644 --- a/src/emimesh/generate_analysis_plots.py +++ b/src/emimesh/generate_analysis_plots.py @@ -1,6 +1,5 @@ import numpy as np -import argparse -import yaml + import matplotlib.pyplot as plt from matplotlib.ticker import PercentFormatter import seaborn as sns @@ -95,22 +94,3 @@ def plot_cell_sizes(mesh_statistics, filename): plt.tight_layout() plt.savefig(filename) - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument( - "--infile", - help="path to mesh statistics infile", - type=str, - ) - parser.add_argument( - "--output", - help="outfile name", - type=str, - ) - args = parser.parse_args() - - with open(args.infile) as infile: - mesh_statistic = yaml.load(infile, Loader=yaml.FullLoader) - - plot_cell_sizes(mesh_statistic, args.output) diff --git a/workflow/Snakefile b/workflow/Snakefile index f4874a1..2e60d04 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -77,7 +77,7 @@ rule downloadImageData: fopt=lambda wildcards: name2options(wildcards.name, "raw") shell: """ - python workflow/scripts/download_data.py \ + emi-download_data \ {params.options} {params.fopt} \ --output {output.rawdata} """ @@ -117,7 +117,7 @@ rule extractSurfaces: conda_env shell: """ - python workflow/scripts/extract_surfaces.py --infile {input.processeddata} --ncpus {resources.ntasks} \ + emi-extract_surfaces --infile {input.processeddata} --ncpus {resources.ntasks} \ --outdir {output.outdir} """ @@ -198,7 +198,7 @@ rule generateAnalysisPlot: conda_env shell: """ - python workflow/scripts/generate_analysis_plots.py \ + emi-plot-analysis \ --infile {input.infile} --output {output.plotfile} """ From f80f432092e00b79ee17a5f91b8035f64a225b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20S=2E=20Dokken?= Date: Mon, 2 Feb 2026 10:51:19 +0100 Subject: [PATCH 04/14] Move remaining modules --- pyproject.toml | 2 + .../{ => clients}/generate_screenshot.py | 7 +- .../{ => clients}/generate_surface_tags.py | 46 +++--- src/emimesh/clients/process_image_data.py | 134 ++++++++++++++++++ src/emimesh/process_image_data.py | 127 +---------------- workflow/Snakefile | 8 +- 6 files changed, 171 insertions(+), 153 deletions(-) rename src/emimesh/{ => clients}/generate_screenshot.py (98%) rename src/emimesh/{ => clients}/generate_surface_tags.py (93%) create mode 100644 src/emimesh/clients/process_image_data.py diff --git a/pyproject.toml b/pyproject.toml index d19cdf7..87d5743 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,8 @@ emi-download = "emimesh.clients.download_data:main" emi-evaluate-mesh = "emimesh.clients.evaluate_mesh:main" emi-extract-surfaces = "emimesh.clients.extract_surfaces:main" emi-plot-analysis = "emimesh.clients.analysis_plot:main" +emi-process-image-data = "emimesh.clients.process-image-data:main" +emi-generate-screenshot = "emimesh.clients.generate_screenshot:main" [project.optional-dependencies] test = [] diff --git a/src/emimesh/generate_screenshot.py b/src/emimesh/clients/generate_screenshot.py similarity index 98% rename from src/emimesh/generate_screenshot.py rename to src/emimesh/clients/generate_screenshot.py index 5f4479d..22023c7 100644 --- a/src/emimesh/generate_screenshot.py +++ b/src/emimesh/clients/generate_screenshot.py @@ -8,7 +8,7 @@ hexcolor = lambda c: int(matplotlib.colors.to_hex(c)[1:], base=16) -if __name__ == "__main__": +def main(): parser = argparse.ArgumentParser() parser.add_argument( "--infile", @@ -65,4 +65,7 @@ #generate_screenshots([cells_k3d, ecs_k3d], args.output, fov=32) - \ No newline at end of file + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/emimesh/generate_surface_tags.py b/src/emimesh/clients/generate_surface_tags.py similarity index 93% rename from src/emimesh/generate_surface_tags.py rename to src/emimesh/clients/generate_surface_tags.py index 0082bfe..79ed7a4 100644 --- a/src/emimesh/generate_surface_tags.py +++ b/src/emimesh/clients/generate_surface_tags.py @@ -1,27 +1,9 @@ -from fenics import * + +from fenics import MeshFunction, facets, cells, Mesh, XDMFFile import argparse import numpy as np - -def mark_interfaces(mesh, subdomains, outer_offset): - bm = MeshFunction("size_t", mesh, 2, 0) - bm.rename("boundaries", "") - for f in facets(mesh): - domains = [] - for c in cells(f): - domains.append(subdomains[c]) - domains = list(set(domains)) - domains.sort() - if f.exterior(): - bm[f] = domains[0] + outer_offset - continue - if len(domains) < 2: - continue - bm[f] = domains[1] - return bm - - -if __name__ == "__main__": +def main(): parser = argparse.ArgumentParser() parser.add_argument( "--infile", @@ -46,3 +28,25 @@ def mark_interfaces(mesh, subdomains, outer_offset): with XDMFFile(args.output) as outfile: outfile.write(bm) + + +def mark_interfaces(mesh, subdomains, outer_offset): + bm = MeshFunction("size_t", mesh, 2, 0) + bm.rename("boundaries", "") + for f in facets(mesh): + domains = [] + for c in cells(f): + domains.append(subdomains[c]) + domains = list(set(domains)) + domains.sort() + if f.exterior(): + bm[f] = domains[0] + outer_offset + continue + if len(domains) < 2: + continue + bm[f] = domains[1] + return bm + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/emimesh/clients/process_image_data.py b/src/emimesh/clients/process_image_data.py new file mode 100644 index 0000000..edc5912 --- /dev/null +++ b/src/emimesh/clients/process_image_data.py @@ -0,0 +1,134 @@ +import argparse +import time +import pyvista as pv +from pathlib import Path +from utils import np2pv +from dask_image.ndinterp import affine_transform +import dask.array as da +from functools import partial +import numba +import numpy as np +import fastremap +import dask +import yaml +from emimesh.process_image_data import opdict, parse_operations + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--infile", help="input data", type=str) + parser.add_argument( + "--output", help="output filename", type=str, default="processeddata.vtk" + ) + parser.add_argument("--nworkers", help="number of workers", type=int, default=1) + parser.add_argument("--dx", help="target resolution", type=int, default=None) + parser.add_argument("--ncells", help="number of cells", type=int, default=None) + parser.add_argument('-o','--operation', nargs='+', action='append', help="operations to be performed on the segmented image") + + args = parser.parse_args() + n_parallel = args.nworkers + numba.set_num_threads(n_parallel) + print(f"Using {n_parallel} workers...") + start = time.time() + + # read image file + imggrid = pv.read(args.infile) + img = imggrid["data"] + dims = imggrid.dimensions + resolution = imggrid.spacing + img = img.reshape(dims - np.array([1, 1, 1]), order="F") + img = da.from_array(img) + + # get cells labels, and filter by n-largest (if requested) + cell_labels, cell_counts = fastremap.unique(img, return_counts=True) + if args.ncells: + cell_labels = list(cell_labels[np.argsort(cell_counts)]) + cell_labels.remove(0) + cois = list(cell_labels[-args.ncells :]) + img = da.where(da.isin(img, cois), img, 0) + else: + cell_labels = list(cell_labels) + if 0 in cell_labels: cell_labels.remove(0) + + # remap labels to smaller, sequential ints + remapping = {int(c):i for i,c in enumerate([0] + cell_labels)} + remap = lambda ids: [remapping[int(i)] for i in ids if i in remapping.keys()] + img = img.map_blocks(partial(fastremap.remap, table=remapping), dtype=img.dtype) + img = img.map_blocks(partial(fastremap.refit, value=len(cell_labels))) + + # interpolate into the specified, isotropic grid with size dx + dx = args.dx + scale = np.diag([dx / r for r in resolution] + [1]) + new_dims = [int(d * r / dx) for d,r in zip(dims, resolution)] + img = affine_transform(img, scale, output_shape=new_dims, order=0, + output_chunks=500) + img = dask.compute(img, num_workers= args.nworkers)[0] + print(f"image size: {img.shape}") + resolution = [dx]*3 + roi = None + + # parse user specified operations, and iterate over them: + operations = parse_operations(args.operation) + for op, kwargs in operations: + print(op, kwargs) + for k in kwargs.keys(): + if "label" in k: + labels = kwargs[k] + if kwargs.get("allexcept", False): + kwargs[k] = list(set(remapping.values()) - set(remap(labels))) + else: + kwargs[k] = remap(labels) + + if op=="roigenerate": + roi = np.isin(img, kwargs["labels"]) + continue + if op=="roiapply": + img = np.where(roi, img, 0) + continue + if op.startswith("roi"): + roiop = op[3:] + roi = opdict[roiop](roi, **kwargs) + else: + img =opdict[op](img, **kwargs) + + print(f"processed! {img.shape}") + proc_time = time.time() + + # remap labels to smaller, sequential ints again, since many labels might have disappeared... + cell_labels, cell_counts = fastremap.unique(img, return_counts=True) + cell_labels = list(cell_labels[np.argsort(cell_counts)]) + cell_labels.remove(0) + + img = da.array(img) + remapping2 = {0:0} + remapping2.update({int(c):i + 2 for i,c in enumerate(cell_labels)}) + img = img.map_blocks(partial(fastremap.remap, table=remapping2), dtype=img.dtype) + img = img.map_blocks(partial(fastremap.refit, value=max(remapping2.values()))) + img = dask.compute(img, num_workers= args.nworkers)[0] + + imggrid = np2pv(img, resolution) + if roi is not None: + imggrid["roimask"] = np.array(roi).flatten(order="F") + + resdir = Path(args.output).parent + + resdir.mkdir(parents=True, exist_ok=True) + imggrid.save(args.output) + save_time = time.time() + print(f"saving time: {save_time - proc_time} s") + + + combinedmap = {k:remapping2[v] for k,v in remapping.items() if v in remapping2.keys()} + mesh_statistics = dict() + mesh_statistics["cell_labels"] = cell_labels + mesh_statistics["cell_counts"] = cell_counts + mesh_statistics["mapping"] = combinedmap + + for k, v in mesh_statistics.items(): + mesh_statistics[k] = np.array(v).tolist() + + with open(resdir / "imagestatistic.yml", "w") as mesh_stat_file: + yaml.dump(mesh_statistics, mesh_stat_file) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/emimesh/process_image_data.py b/src/emimesh/process_image_data.py index a96ecbf..e7d106a 100644 --- a/src/emimesh/process_image_data.py +++ b/src/emimesh/process_image_data.py @@ -1,17 +1,8 @@ import numpy as np -import argparse import yaml -import time -import pyvista as pv -from pathlib import Path -from utils import np2pv -from dask_image.ndinterp import affine_transform import fastremap -import dask.array as da import dask -from functools import partial import cc3d -import numba import nbmorph dask.config.set({"array.chunk-size": "1024 MiB"}) @@ -86,120 +77,4 @@ def parse_operations(ops): subargs = _parse_to_dict(op[1:]) parsed.append((op[0], subargs)) return parsed - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument("--infile", help="input data", type=str) - parser.add_argument( - "--output", help="output filename", type=str, default="processeddata.vtk" - ) - parser.add_argument("--nworkers", help="number of workers", type=int, default=1) - parser.add_argument("--dx", help="target resolution", type=int, default=None) - parser.add_argument("--ncells", help="number of cells", type=int, default=None) - parser.add_argument('-o','--operation', nargs='+', action='append', help="operations to be performed on the segmented image") - - args = parser.parse_args() - n_parallel = args.nworkers - numba.set_num_threads(n_parallel) - print(f"Using {n_parallel} workers...") - start = time.time() - - # read image file - imggrid = pv.read(args.infile) - img = imggrid["data"] - dims = imggrid.dimensions - resolution = imggrid.spacing - img = img.reshape(dims - np.array([1, 1, 1]), order="F") - img = da.from_array(img) - - # get cells labels, and filter by n-largest (if requested) - cell_labels, cell_counts = fastremap.unique(img, return_counts=True) - if args.ncells: - cell_labels = list(cell_labels[np.argsort(cell_counts)]) - cell_labels.remove(0) - cois = list(cell_labels[-args.ncells :]) - img = da.where(da.isin(img, cois), img, 0) - else: - cell_labels = list(cell_labels) - if 0 in cell_labels: cell_labels.remove(0) - - # remap labels to smaller, sequential ints - remapping = {int(c):i for i,c in enumerate([0] + cell_labels)} - remap = lambda ids: [remapping[int(i)] for i in ids if i in remapping.keys()] - img = img.map_blocks(partial(fastremap.remap, table=remapping), dtype=img.dtype) - img = img.map_blocks(partial(fastremap.refit, value=len(cell_labels))) - - # interpolate into the specified, isotropic grid with size dx - dx = args.dx - scale = np.diag([dx / r for r in resolution] + [1]) - new_dims = [int(d * r / dx) for d,r in zip(dims, resolution)] - img = affine_transform(img, scale, output_shape=new_dims, order=0, - output_chunks=500) - img = dask.compute(img, num_workers= args.nworkers)[0] - print(f"image size: {img.shape}") - resolution = [dx]*3 - roi = None - - # parse user specified operations, and iterate over them: - operations = parse_operations(args.operation) - for op, kwargs in operations: - print(op, kwargs) - for k in kwargs.keys(): - if "label" in k: - labels = kwargs[k] - if kwargs.get("allexcept", False): - kwargs[k] = list(set(remapping.values()) - set(remap(labels))) - else: - kwargs[k] = remap(labels) - - if op=="roigenerate": - roi = np.isin(img, kwargs["labels"]) - continue - if op=="roiapply": - img = np.where(roi, img, 0) - continue - if op.startswith("roi"): - roiop = op[3:] - roi = opdict[roiop](roi, **kwargs) - else: - img =opdict[op](img, **kwargs) - - print(f"processed! {img.shape}") - proc_time = time.time() - - # remap labels to smaller, sequential ints again, since many labels might have disappeared... - cell_labels, cell_counts = fastremap.unique(img, return_counts=True) - cell_labels = list(cell_labels[np.argsort(cell_counts)]) - cell_labels.remove(0) - - img = da.array(img) - remapping2 = {0:0} - remapping2.update({int(c):i + 2 for i,c in enumerate(cell_labels)}) - img = img.map_blocks(partial(fastremap.remap, table=remapping2), dtype=img.dtype) - img = img.map_blocks(partial(fastremap.refit, value=max(remapping2.values()))) - img = dask.compute(img, num_workers= args.nworkers)[0] - - imggrid = np2pv(img, resolution) - if roi is not None: - imggrid["roimask"] = np.array(roi).flatten(order="F") - - resdir = Path(args.output).parent - - resdir.mkdir(parents=True, exist_ok=True) - imggrid.save(args.output) - save_time = time.time() - print(f"saving time: {save_time - proc_time} s") - - - combinedmap = {k:remapping2[v] for k,v in remapping.items() if v in remapping2.keys()} - mesh_statistics = dict() - mesh_statistics["cell_labels"] = cell_labels - mesh_statistics["cell_counts"] = cell_counts - mesh_statistics["mapping"] = combinedmap - - for k, v in mesh_statistics.items(): - mesh_statistics[k] = np.array(v).tolist() - - with open(resdir / "imagestatistic.yml", "w") as mesh_stat_file: - yaml.dump(mesh_statistics, mesh_stat_file) + \ No newline at end of file diff --git a/workflow/Snakefile b/workflow/Snakefile index 2e60d04..7685644 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -98,7 +98,7 @@ rule processImageData: fopt=lambda wildcards: name2options(wildcards.name, "processing") shell: """ - python workflow/scripts/process_image_data.py --infile {input.rawdata} \ + emi-process-image-data --infile {input.rawdata} \ {params.options} {params.fopt} \ --output {output.outfile} \ --nworkers {resources.ntasks} @@ -157,7 +157,7 @@ rule generateSurfaceTags: time="120:00" shell: """ - python workflow/scripts/generate_surface_tags.py \ + emi-generate-surface-tags \ --infile {input.meshfile} --output {output} """ @@ -171,7 +171,7 @@ rule evaluateMesh: threads: 1 shell: """ - python workflow/scripts/evaluate_mesh.py \ + emi-evaluate-mesh \ --infile {input} --output {output} """ @@ -185,7 +185,7 @@ rule takeScreenshot: threads: 1 shell: """ - python workflow/scripts/generate_screenshot.py \ + emi-generate-screenshot \ --infile {input} --output {output} """ From 175f3016acdcb304792548e27d313d2cc63ff0ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20S=2E=20Dokken?= Date: Mon, 2 Feb 2026 10:57:59 +0100 Subject: [PATCH 05/14] Add to environements self package --- workflow/envs/environment.yml | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/workflow/envs/environment.yml b/workflow/envs/environment.yml index b540a9f..8991cf6 100644 --- a/workflow/envs/environment.yml +++ b/workflow/envs/environment.yml @@ -11,10 +11,11 @@ dependencies: - pyacvd - pip - pip: - - pytetwild - - fastremap - - cloud-volume - - webknossos - - connected-components-3d - - cmocean - - nbmorph + - pytetwild + - fastremap + - cloud-volume + - webknossos + - connected-components-3d + - cmocean + - nbmorph + - -e ../.. From 87a348c9ac38311bba2c4034b030d5362c051b12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20S=2E=20Dokken?= Date: Mon, 2 Feb 2026 11:01:05 +0100 Subject: [PATCH 06/14] Minor improvements --- src/emimesh/seperate_touching_cells.py | 4 ++-- src/emimesh/utils.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/emimesh/seperate_touching_cells.py b/src/emimesh/seperate_touching_cells.py index 34ce5c2..8674dae 100644 --- a/src/emimesh/seperate_touching_cells.py +++ b/src/emimesh/seperate_touching_cells.py @@ -1,6 +1,6 @@ -from fenics import * +from fenics import vertices, cells, Mesh, MeshFunction, XDMFFile + import argparse -import numpy as np def seperate_touching_cells(sm): cellids = [] diff --git a/src/emimesh/utils.py b/src/emimesh/utils.py index dcf9953..0678b7b 100644 --- a/src/emimesh/utils.py +++ b/src/emimesh/utils.py @@ -1,7 +1,5 @@ import pyvista as pv import numpy as np -import dask -import dask.array as da import fastremap From a7a0d9d864b45beb9c14ebec8f1c6fbd17b73619 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20S=2E=20Dokken?= Date: Mon, 2 Feb 2026 11:03:51 +0100 Subject: [PATCH 07/14] Fix cli --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 87d5743..770eda6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ emi-download = "emimesh.clients.download_data:main" emi-evaluate-mesh = "emimesh.clients.evaluate_mesh:main" emi-extract-surfaces = "emimesh.clients.extract_surfaces:main" emi-plot-analysis = "emimesh.clients.analysis_plot:main" -emi-process-image-data = "emimesh.clients.process-image-data:main" +emi-process-image-data = "emimesh.clients.process_image_data:main" emi-generate-screenshot = "emimesh.clients.generate_screenshot:main" [project.optional-dependencies] From 9431e9a718824cc12f3520d1772e706cb969860a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20S=2E=20Dokken?= Date: Mon, 2 Feb 2026 11:37:32 +0100 Subject: [PATCH 08/14] Set webknossos as optional --- pyproject.toml | 3 +-- workflow/envs/environment.yml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 770eda6..5903220 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,6 @@ dependencies = [ "pytetwild", "fastremap", "cloud-volume", - "webknossos", "connected-components-3d", "cmocean", "nbmorph", @@ -38,7 +37,7 @@ emi-generate-screenshot = "emimesh.clients.generate_screenshot:main" [project.optional-dependencies] test = [] dev = ["pdbpp", "ipython", "mypy", "ruff"] -h5py = ["h5py"] +webknossos = ["webknossos"] docs = [ "jupyter-book<2.0.0", "jupytext", diff --git a/workflow/envs/environment.yml b/workflow/envs/environment.yml index 8991cf6..964ec23 100644 --- a/workflow/envs/environment.yml +++ b/workflow/envs/environment.yml @@ -14,7 +14,7 @@ dependencies: - pytetwild - fastremap - cloud-volume - - webknossos + # - webknossos # Needs to unpin it's zipp versioning, ref: https://github.com/scalableminds/webknossos-libs/issues/1421 - connected-components-3d - cmocean - nbmorph From 9cfee6ef0c750b2a3d0d10a181377c25b944cab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20S=2E=20Dokken?= Date: Mon, 2 Feb 2026 11:54:17 +0100 Subject: [PATCH 09/14] Fix command name --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5903220..00c20e7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ readme = "readme.md" [project.scripts] emi-generate-mesh = "emimesh.clients.generate_mesh:main" -emi-download = "emimesh.clients.download_data:main" +emi-download-data = "emimesh.clients.download_data:main" emi-evaluate-mesh = "emimesh.clients.evaluate_mesh:main" emi-extract-surfaces = "emimesh.clients.extract_surfaces:main" emi-plot-analysis = "emimesh.clients.analysis_plot:main" From 4dec1e435586dde26c36dec2ace3eeaa39bf97fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B8rgen=20S=2E=20Dokken?= Date: Mon, 2 Feb 2026 11:59:33 +0100 Subject: [PATCH 10/14] Fix --- src/emimesh/clients/download_data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/emimesh/clients/download_data.py b/src/emimesh/clients/download_data.py index 1935ab8..3331b85 100644 --- a/src/emimesh/clients/download_data.py +++ b/src/emimesh/clients/download_data.py @@ -3,6 +3,7 @@ from emimesh.utils import np2pv from pathlib import Path import argparse +from emimesh.download_data import download_webknossos, download_cloudvolume def main(): parser = argparse.ArgumentParser() From a384834c3542e2a2de194f57dc032ad515d560a8 Mon Sep 17 00:00:00 2001 From: MariusCausemann Date: Mon, 2 Feb 2026 12:10:31 +0100 Subject: [PATCH 11/14] change test case --- .github/workflows/test_conda.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_conda.yml b/.github/workflows/test_conda.yml index a4c2f65..c5013bf 100644 --- a/.github/workflows/test_conda.yml +++ b/.github/workflows/test_conda.yml @@ -57,4 +57,4 @@ jobs: - name: Run snakemake run: | - snakemake --cores 2 -p --configfile ./config_files/motta2019.yml + snakemake --cores 2 -p --configfile ./config_files/test.yml From 12e2c5b8dffb2f9c5d29ee084b9ea67253f0c108 Mon Sep 17 00:00:00 2001 From: Marius Causemann Date: Mon, 2 Feb 2026 13:08:52 +0100 Subject: [PATCH 12/14] fix import issues --- src/emimesh/clients/extract_surfaces.py | 8 ++++++-- src/emimesh/clients/generate_mesh.py | 2 +- src/emimesh/clients/process_image_data.py | 2 +- src/emimesh/extract_surfaces.py | 6 +++--- workflow/Snakefile | 2 +- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/emimesh/clients/extract_surfaces.py b/src/emimesh/clients/extract_surfaces.py index 029a85b..913f21e 100644 --- a/src/emimesh/clients/extract_surfaces.py +++ b/src/emimesh/clients/extract_surfaces.py @@ -1,5 +1,5 @@ import argparse -import pathlib as Path +from pathlib import Path import pyvista as pv from emimesh.extract_surfaces import extract_surface, clip_closed_box, extract_cell_meshes,create_balanced_csg_tree @@ -9,7 +9,8 @@ from emimesh.utils import get_bounding_box import argparse -if __name__ == "__main__": + +def main(): parser = argparse.ArgumentParser() parser.add_argument( "--infile", @@ -66,3 +67,6 @@ csg_tree = {"operation":"intersection","right":csg_tree, "left":str(roi_file)} with open(outdir / "csgtree.json", "w") as outfile: outfile.write(json.dumps(csg_tree)) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/emimesh/clients/generate_mesh.py b/src/emimesh/clients/generate_mesh.py index d98d3b6..2746230 100644 --- a/src/emimesh/clients/generate_mesh.py +++ b/src/emimesh/clients/generate_mesh.py @@ -2,7 +2,7 @@ import pyvista as pv import argparse import numpy as np -from emimesh import mesh_surfaces +from emimesh.generate_mesh import mesh_surfaces def get_values(d): for v in d.values(): diff --git a/src/emimesh/clients/process_image_data.py b/src/emimesh/clients/process_image_data.py index 7bf57d5..f4195f3 100644 --- a/src/emimesh/clients/process_image_data.py +++ b/src/emimesh/clients/process_image_data.py @@ -2,7 +2,7 @@ import time import pyvista as pv from pathlib import Path -from utils import np2pv +from emimesh.utils import np2pv from dask_image.ndinterp import affine_transform import dask.array as da from functools import partial diff --git a/src/emimesh/extract_surfaces.py b/src/emimesh/extract_surfaces.py index 2d82b00..e0f4fa4 100644 --- a/src/emimesh/extract_surfaces.py +++ b/src/emimesh/extract_surfaces.py @@ -9,7 +9,7 @@ import shutil import itertools -from pyvista.core import _vtk_core as _vti +from pyvista.core import _vtk_core as _vtk from pyvista.core.filters import _get_output, _update_alg from pyvista.core.utilities.helpers import generate_plane @@ -19,9 +19,9 @@ def clip_closed_surface(surf, normal='x', origin=None, tolerance=1e-06, inplace=False, progress_bar=False): plane = generate_plane(normal, origin) - collection = _vti.vtiPlaneCollection() + collection = _vtk.vtkPlaneCollection() collection.AddItem(plane) - alg = _vti.vtiClipClosedSurface() + alg = _vtk.vtkClipClosedSurface() alg.SetGenerateFaces(True) alg.SetInputDataObject(surf) alg.SetTolerance(tolerance) diff --git a/workflow/Snakefile b/workflow/Snakefile index e362ffe..3688446 100644 --- a/workflow/Snakefile +++ b/workflow/Snakefile @@ -91,7 +91,7 @@ rule extractSurfaces: conda_env shell: """ - emi-extract_surfaces --infile {input.processeddata} --ncpus {resources.ntasks} \ + emi-extract-surfaces --infile {input.processeddata} --ncpus {resources.ntasks} \ --outdir {output.outdir} """ From d5eace9ec560bacb9cfb13e01fea217f12eb2857 Mon Sep 17 00:00:00 2001 From: Marius Causemann Date: Mon, 2 Feb 2026 14:33:53 +0100 Subject: [PATCH 13/14] fix import issues --- src/emimesh/clients/generate_screenshot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/emimesh/clients/generate_screenshot.py b/src/emimesh/clients/generate_screenshot.py index 22023c7..47178ac 100644 --- a/src/emimesh/clients/generate_screenshot.py +++ b/src/emimesh/clients/generate_screenshot.py @@ -1,6 +1,6 @@ import argparse import pyvista as pv -from plot_utils import get_screenshot +from emimesh.plot_utils import get_screenshot import numpy as np import matplotlib import cmocean From dde417158df83a5d66d9e1c336057ccf1895f5d8 Mon Sep 17 00:00:00 2001 From: Marius Causemann Date: Mon, 2 Feb 2026 14:48:49 +0100 Subject: [PATCH 14/14] remove intel mac from CI --- .github/workflows/test_conda.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_conda.yml b/.github/workflows/test_conda.yml index c5013bf..127ac9a 100644 --- a/.github/workflows/test_conda.yml +++ b/.github/workflows/test_conda.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-15-intel, macos-15] + os: [ubuntu-latest, windows-latest, macos-15] steps: