Skip to content

Commit

Permalink
Feature/segment properties (#24)
Browse files Browse the repository at this point in the history
Adds ability to tag segmentations with a string label. This is across
segmentation masks and meshes


![image_720](https://github.com/user-attachments/assets/7b9d4216-395d-4df1-8354-110b2be60fae)
  • Loading branch information
seankmartin authored Sep 30, 2024
2 parents 759def1 + b482f6e commit 501240f
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 28 deletions.
27 changes: 27 additions & 0 deletions cryoet_data_portal_neuroglancer/models/json_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,33 @@ def __str__(self):
return self.name.lower()


@dataclass
class SegmentPropertyJSONGenerator:
"""Generates a JSON file for segmentation properties.
Supports a subset of the properties that can be set in Neuroglancer.
See https://github.com/google/neuroglancer/blob/3efc90465e702453916d2b03d472c16378848132/src/datasource/precomputed/segment_properties.md
"""

ids: list[int]
labels: list[str]

def generate_json(self) -> dict:
return {
"inline": {
"ids": [str(val) for val in self.ids],
"properties": [
{
"values": self.labels,
"type": "label",
"id": "label",
},
],
},
"@type": "neuroglancer_segment_properties",
}


@dataclass
class RenderingJSONGenerator:
"""Generates a JSON file for Neuroglancer to read."""
Expand Down
77 changes: 53 additions & 24 deletions cryoet_data_portal_neuroglancer/precompute/mesh.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from tqdm import tqdm

from cryoet_data_portal_neuroglancer.igneous_patch import patched_create_octree_level_from_mesh, patched_process_mesh
from cryoet_data_portal_neuroglancer.precompute.segmentation_properties import write_segment_properties
from cryoet_data_portal_neuroglancer.utils import determine_mesh_shape_from_lods

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -253,6 +254,7 @@ def _generate_standalone_mesh_info(
mesh_dir: str = "mesh",
resolution: tuple[float, float, float] | float = (1.0, 1.0, 1.0),
mesh_chunk_size: tuple[float, float, float] | float = (448, 448, 448),
has_segment_properties: bool = False,
):
outfolder = Path(outfolder)
outfolder.mkdir(exist_ok=True, parents=True)
Expand All @@ -262,32 +264,31 @@ def _generate_standalone_mesh_info(

# offset = bbox.transform[:, 3][:3].tolist()
info = outfolder / "info"
info.write_text(
json.dumps(
output = {
"@type": "neuroglancer_multiscale_volume",
"data_type": "uint32",
"mesh": "mesh",
"num_channels": 1,
"scales": [
{
"@type": "neuroglancer_multiscale_volume",
"data_type": "uint32",
"mesh": "mesh",
"num_channels": 1,
"scales": [
{
"chunk_sizes": [[256, 256, 256]], # information required by neuroglancer but not used
"compressed_segmentation_block_size": [
64,
64,
64,
], # information required by neuroglancer but not used
"encoding": "compressed_segmentation",
"key": "data",
"resolution": resolution_conv,
"size": size,
},
],
"type": "segmentation",
"chunk_sizes": [[256, 256, 256]], # information required by neuroglancer but not used
"compressed_segmentation_block_size": [
64,
64,
64,
], # information required by neuroglancer but not used
"encoding": "compressed_segmentation",
"key": "data",
"resolution": resolution_conv,
"size": size,
},
indent=2,
),
)
],
"type": "segmentation",
}

if has_segment_properties:
output["segment_properties"] = "segment_properties"
info.write_text(json.dumps(output, indent=2))

mesh_info = outfolder / mesh_dir
mesh_info.mkdir(exist_ok=True, parents=True)
Expand Down Expand Up @@ -452,6 +453,7 @@ def generate_mesh_from_lods(
min_mesh_chunk_dim: int = 16,
label: int = 1,
bounding_box_size: tuple[float, float, float] | None = None,
string_label: str | None = None,
):
"""
Generate a sharded mesh from a list of LODs
Expand All @@ -474,6 +476,8 @@ def generate_mesh_from_lods(
This calculation is often not accurate, so it is recommended to
provide the bounding box size. Or turn off the bounding box in the
neuroglancer state.
string_label : str | None, optional
The string label to use, by default None - ie. not used
"""
concatenated_lods: list[trimesh.Trimesh] = cast(list[trimesh.Trimesh], [lod.dump(concatenate=True) for lod in lods])
num_lod = len(concatenated_lods)
Expand All @@ -499,8 +503,12 @@ def _compute_size(bbx):
size=(size_x, size_y, size_z),
resolution=1.0,
mesh_chunk_size=actual_chunk_shape,
has_segment_properties=string_label is not None,
)

if string_label is not None:
write_segment_properties(outfolder, [label], [string_label])

tq = LocalTaskQueue(progress=False)
tasks = _create_sharded_multires_mesh_tasks_from_glb(
f"precomputed://file://{outfolder}",
Expand All @@ -521,6 +529,7 @@ def generate_multiresolution_mesh(
min_mesh_chunk_dim: int = 16,
bounding_box_size: tuple[float, float, float] | None = None,
label: int = 1,
string_label: str | None = None,
):
"""
Generate a standalone sharded multiresolution mesh from a glb file or scene
Expand Down Expand Up @@ -549,6 +558,7 @@ def generate_multiresolution_mesh(
min_mesh_chunk_dim=min_mesh_chunk_dim,
bounding_box_size=bounding_box_size,
label_dict={label: glb},
string_labels={label: string_label} if string_label is not None else None,
)


Expand All @@ -558,6 +568,7 @@ def generate_multilabel_multiresolution_mesh(
max_lod: int = 2,
min_mesh_chunk_dim: int = 16,
bounding_box_size: tuple[float, float, float] | None = None,
string_labels: dict[int, str] | None = None,
):
"""
Generate standalone sharded multiresolution meshes from a mapping of labels to glb files or scenes.
Expand Down Expand Up @@ -626,8 +637,14 @@ def _compute_size():
size=(size_x, size_y, size_z),
resolution=1.0,
mesh_chunk_size=actual_chunk_size,
has_segment_properties=string_labels is not None,
)

if string_labels is not None:
ids = list(string_labels.keys())
string_labels_list = [string_labels[i] for i in ids]
write_segment_properties(outfolder, ids, string_labels_list)

tq = LocalTaskQueue()
tasks = _create_sharded_multires_mesh_tasks_from_glb(
f"precomputed://file://{outfolder}",
Expand All @@ -648,6 +665,7 @@ def generate_multiresolution_mesh_from_segmentation(
mesh_shape: tuple[int, int, int] | np.ndarray,
min_mesh_chunk_dim: int = 16,
max_simplification_error: int = 10,
labels_dict: dict[int, str] | None = None,
) -> None:
"""Generates the meshes for a segmentation stored as a precomputed Neuroglancer format.
Expand All @@ -668,6 +686,9 @@ def generate_multiresolution_mesh_from_segmentation(
The maximal simplification error allowed for the mesh generation from the
segmentation. This is used to simplify the mesh and reduce the number of
vertices and faces in the mesh. The error is in the same unit as the data.
labels_dict: dict[int, str] | None
A dictionary of labels to string labels. This is used to generate the segment properties
for the segmentation. If None, no segment properties are generated.
"""
tq = LocalTaskQueue()

Expand Down Expand Up @@ -709,6 +730,14 @@ def generate_multiresolution_mesh_from_segmentation(
tq.insert(tasks)
tq.execute()

if labels_dict is not None:
LOGGER.debug("Writing segment properties")
write_segment_properties(
Path(mesh_directory).parent,
list(labels_dict.keys()),
list(labels_dict.values()),
)


def generate_single_resolution_mesh(
dask_data: da.Array,
Expand Down
16 changes: 16 additions & 0 deletions cryoet_data_portal_neuroglancer/precompute/segmentation_mask.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
clean_mesh_folder,
generate_multiresolution_mesh_from_segmentation,
)
from cryoet_data_portal_neuroglancer.precompute.segmentation_properties import write_segment_properties
from cryoet_data_portal_neuroglancer.utils import (
determine_size_of_non_zero_bounding_box,
get_grid_size_from_block_shape,
Expand Down Expand Up @@ -245,6 +246,7 @@ def _create_metadata(
data_directory: str,
resolution: tuple[float, float, float] = (1.0, 1.0, 1.0),
mesh_directory: str | None = None,
has_segment_properties: bool = False,
) -> dict[str, Any]:
"""Create the metadata for the segmentation"""
metadata = {
Expand All @@ -263,6 +265,8 @@ def _create_metadata(
],
"type": "segmentation",
}
if has_segment_properties:
metadata["segment_properties"] = "segment_properties"
if mesh_directory:
metadata["mesh"] = mesh_directory
return metadata
Expand Down Expand Up @@ -307,6 +311,7 @@ def encode_segmentation(
min_mesh_chunk_dim: int = 16,
fast_bounding_box: bool = False,
max_simplification_error_in_voxels: int = 2,
labels_dict: dict[int, str] | None = None,
) -> None:
"""Convert the given OME-Zarr file to neuroglancer segmentation format with the given block size
Expand Down Expand Up @@ -364,6 +369,10 @@ def encode_segmentation(
simplified. This parameter sets the maximum error in voxels for
the simplification, by default 2.
For large meshes, this can be increased to reduce memory consumption.
labels_dict : dict[int, str] | None, optional
A dictionary mapping the integer labels in the segmentation to
human-readable names, by default None
This is useful for generating a legend in the viewer
"""
LOGGER.info("Converting %s to neuroglancer compressed segmentation format", filename)
output_path = Path(output_path)
Expand Down Expand Up @@ -400,8 +409,15 @@ def encode_segmentation(
data_directory,
resolution,
mesh_directory=mesh_directory if include_mesh else None,
has_segment_properties=labels_dict is not None,
)
write_metadata(metadata, output_path)
if labels_dict is not None:
write_segment_properties(
output_path,
list(labels_dict.keys()),
list(labels_dict.values()),
)

if include_mesh:
LOGGER.info("Converting %s to neuroglancer mesh format", filename)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import json
from pathlib import Path

from cryoet_data_portal_neuroglancer.models.json_generator import SegmentPropertyJSONGenerator


def write_segment_properties(base_folder: str | Path, ids: list[int], labels: list[str]):
segment_generator = SegmentPropertyJSONGenerator(ids=ids, labels=labels)
segment_properties = segment_generator.generate_json()
segment_properties_path = Path(base_folder) / "segment_properties" / "info"
segment_properties_path.parent.mkdir(exist_ok=True, parents=True)
segment_properties_path.write_text(json.dumps(segment_properties, indent=2))
20 changes: 19 additions & 1 deletion manual_tests/many_mesh_to_precompute.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,25 @@ def main(output_folder="test_glb", port=1030, num_lods=2):
here = Path(__file__).parent
output_folder = here / output_folder
mesh_dict = generate_basic_trimeshes()
generate_multilabel_multiresolution_mesh(mesh_dict, output_folder, num_lods - 1, 16, [500, 500, 500])
string_labels = [
"cube",
"torus",
"sphere",
"cylinder",
"cone",
"capsule",
"annulus",
"icosahedron",
]
string_labels = dict(zip(mesh_dict.keys(), string_labels, strict=False))
generate_multilabel_multiresolution_mesh(
mesh_dict,
output_folder,
num_lods - 1,
16,
[500, 500, 500],
string_labels,
)

print(
f"Go to https://neuroglancer-demo.appspot.com/#!%7B%22dimensions%22:%7B%22x%22:%5B1e-9%2C%22m%22%5D%2C%22y%22:%5B1e-9%2C%22m%22%5D%2C%22z%22:%5B1e-9%2C%22m%22%5D%7D%2C%22position%22:%5B10.890692710876465%2C-2.4535868167877197%2C50.0444221496582%5D%2C%22crossSectionScale%22:1%2C%22projectionOrientation%22:%5B-0.9983203411102295%2C-0.04334384202957153%2C0.009803229942917824%2C-0.037171632051467896%5D%2C%22projectionScale%22:642.7371815420057%2C%22layers%22:%5B%7B%22type%22:%22segmentation%22%2C%22source%22:%7B%22url%22:%22precomputed://http://localhost:{port}%22%2C%22subsources%22:%7B%22default%22:true%2C%22mesh%22:true%7D%2C%22enableDefaultSubsources%22:false%7D%2C%22toolBindings%22:%7B%22B%22:%22selectSegments%22%7D%2C%22tab%22:%22rendering%22%2C%22hoverHighlight%22:false%2C%22ignoreNullVisibleSet%22:false%2C%22meshRenderScale%22:475.1228767657264%2C%22crossSectionRenderScale%22:2%2C%22segments%22:%5B%221%22%2C%222%22%2C%223%22%2C%224%22%2C%225%22%2C%226%22%2C%227%22%2C%228%22%5D%2C%22segmentQuery%22:%228%22%2C%22name%22:%22localhost:{port}%22%7D%5D%2C%22showAxisLines%22:false%2C%22showScaleBar%22:false%2C%22showSlices%22:false%2C%22selectedLayer%22:%7B%22size%22:605%2C%22visible%22:true%2C%22layer%22:%22localhost:{port}%22%7D%2C%22layout%22:%223d%22%7D to view",
Expand Down
9 changes: 6 additions & 3 deletions manual_tests/segmentation_from_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ def make_precomputed_segmentation():
zarr_path,
output_path,
(resolution, resolution, resolution),
include_mesh=False,
include_mesh=True,
convert_non_zero_to=1,
delete_existing=False,
delete_existing=True,
fast_bounding_box=True,
labels_dict={1: "membrane"},
)


Expand All @@ -49,6 +51,7 @@ def make_multi_res_mesh():
max_lod=2,
mesh_shape=mesh_shape,
max_simplification_error=max_simplification_error,
labels_dict={1: "membrane"},
)
clean_mesh_folder(output_path, "mesh")

Expand Down Expand Up @@ -77,7 +80,7 @@ def serve_files():
def main():
grab_annotation()
make_precomputed_segmentation()
make_multi_res_mesh()
# make_multi_res_mesh()
# make_single_res_mesh()
serve_files()

Expand Down

0 comments on commit 501240f

Please sign in to comment.