Created
May 14, 2025 10:07
-
-
Save dstansby/dada2403669d97770c81ca0d445ec453 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """ | |
| Code for generating and working with neuroglancer links. | |
| """ | |
| import json | |
| from typing import TYPE_CHECKING, Any | |
| import neuroglancer | |
| import neuroglancer.random_token | |
| import numpy as np | |
| import numpy.typing as npt | |
| from hipct_data_tools.registration.inventory import RegDataset | |
| if TYPE_CHECKING: | |
| from hipct_data_tools.models.dataset_model import HiPCTDataSet | |
| NEUROGLANCER_INSTANCE = "https://neuroglancer-demo.appspot.com" | |
| class RegistrationNotFoundError(Exception): ... | |
| def dataset_to_layer( | |
| dataset: "HiPCTDataSet", *, opacity: float = 1 | |
| ) -> neuroglancer.ManagedLayer: | |
| """ | |
| Convert a HiP-CT dataset to a neuroglancer image layer. | |
| """ | |
| invlerp_params = neuroglancer.InvlerpParameters( | |
| range=(dataset.contrast_low, dataset.contrast_high) | |
| ) | |
| shader_controls = {"normalized": invlerp_params.to_json()} | |
| source = neuroglancer.LayerDataSource(url=dataset.gcs_url) | |
| layer = neuroglancer.ManagedLayer( | |
| name=dataset.name, | |
| layer=neuroglancer.ImageLayer( | |
| shader_controls=shader_controls, | |
| source=source, | |
| opacity=opacity, | |
| volume_rendering="max", | |
| volume_rendering_depth_samples=256, | |
| ), | |
| ) | |
| layer.visible = True | |
| return layer | |
| def _get_ng_matrix( | |
| reg_dataset: RegDataset, *, parent_res_um: float, voi_res_um: float | |
| ) -> npt.NDArray[Any]: | |
| translation = np.array([[reg_dataset.tx, reg_dataset.ty, reg_dataset.tz]]).T | |
| # The transform maps from child to parent, but we want the other way around | |
| rotation_matrix = reg_dataset.rotation_obj.inv().as_matrix() | |
| translation = np.dot(rotation_matrix, -translation) | |
| return ( | |
| parent_res_um | |
| / voi_res_um | |
| * np.column_stack( | |
| ( | |
| reg_dataset.scale * rotation_matrix, | |
| translation, | |
| ) | |
| ) | |
| ) | |
| def add_registration_transform( | |
| *, | |
| voi_dataset: "HiPCTDataSet", | |
| parent_dataset: "HiPCTDataSet", | |
| parent_layer: neuroglancer.ManagedLayer, | |
| ) -> None: | |
| """ | |
| Add a registration transform to a neuroglancer layer. | |
| Modifies the layer in place. | |
| Parameters | |
| ---------- | |
| voi_dataset : | |
| VOI dataset to add transform from. | |
| parent_dataset : | |
| Parent dataset that will be transformed in neuroglancer. | |
| parent_layer : | |
| Neuroglancer layer of the parent dataset to add transform to. | |
| """ | |
| reg_dataset = voi_dataset.registration_dataset | |
| if reg_dataset is None: | |
| raise RegistrationNotFoundError(f"No registration found for {voi_dataset.name}") | |
| if not ( | |
| reg_dataset.tx is not None | |
| and reg_dataset.ty is not None | |
| and reg_dataset.tz is not None | |
| and reg_dataset.rotation is not None | |
| and reg_dataset.scale is not None | |
| ): | |
| raise RegistrationNotFoundError( | |
| f"At least one registration parameter missing for {reg_dataset.name}" | |
| ) | |
| ng_matrix = _get_ng_matrix( | |
| reg_dataset, | |
| parent_res_um=parent_dataset.resolution_um, | |
| voi_res_um=voi_dataset.resolution_um, | |
| ) | |
| dimensions = neuroglancer.CoordinateSpace( | |
| names=["x", "y", "z"], units=["um"] * 3, scales=[voi_dataset.resolution_um] * 3 | |
| ) | |
| parent_layer.source[0].transform = neuroglancer.CoordinateSpaceTransform( | |
| output_dimensions=dimensions, matrix=ng_matrix | |
| ) | |
| def neuroglancer_state( | |
| dataset: "HiPCTDataSet", | |
| *, | |
| with_parent: bool = False, | |
| with_child_boxes: bool = False, | |
| ) -> neuroglancer.ViewerState: | |
| """ | |
| Get the viewer state for an individual dataset. | |
| Parameters | |
| ---------- | |
| dataset : | |
| Dataset to create state for. | |
| with_parent : | |
| If `True`, put this dataset inside the parent dataset it is registered to. | |
| Only works for VOI datasets that are registered. | |
| with_child_boxes : | |
| If `True`, include any registered child datasets as bounding box annotations. | |
| """ | |
| layer = dataset_to_layer(dataset, opacity=1) | |
| dimensions = neuroglancer.CoordinateSpace( | |
| names=["x", "y", "z"], units=["um"] * 3, scales=[dataset.resolution_um] * 3 | |
| ) | |
| selected_layer = neuroglancer.SelectedLayerState(layer=layer.name, visible=True) | |
| viewer_state = neuroglancer.ViewerState( | |
| dimensions=dimensions, | |
| layout="4panel", | |
| projection_orientation=(0.3, 0.2, 0, -0.9), | |
| projectionScale=4096, | |
| selected_layer=selected_layer.to_json(), | |
| position=[dataset.nx / 2, dataset.ny / 2, dataset.nz / 2], | |
| cross_section_scale=10, | |
| show_slices=False, | |
| ) | |
| if with_parent: | |
| # Get parent dataset as a neuroglancer layer, transformed | |
| # into the coordinate system of the VOI dataset | |
| reg_dataset = dataset.registration_dataset | |
| assert reg_dataset is not None | |
| parents = {d.name: d for d in dataset.parent_datasets()} | |
| parent = parents[reg_dataset.parent_name] | |
| parent_layer = dataset_to_layer(parent, opacity=0.5) | |
| # Add transform to this dataset | |
| add_registration_transform( | |
| voi_dataset=dataset, | |
| parent_dataset=parent, | |
| parent_layer=parent_layer, | |
| ) | |
| # Make sure we append parent layer first, so the main | |
| # layer appears on top of this layer in the slice view | |
| viewer_state.layers.append(parent_layer) | |
| viewer_state.layers.append(layer) | |
| if with_child_boxes: | |
| # Get all children that are registered to this dataset | |
| children = [ | |
| child | |
| for child in dataset.child_datasets() | |
| if ( | |
| (child.registration_dataset is not None) | |
| and child.registration_dataset.is_registered | |
| and (child.registration_dataset.parent_name == dataset.name) | |
| ) | |
| ] | |
| if len(children): | |
| new_layer = neuroglancer.LocalAnnotationLayer( | |
| dimensions=viewer_state.dimensions | |
| ) | |
| annotations = [] | |
| for child in children: | |
| reg_dataset = child.registration_dataset | |
| assert reg_dataset is not None | |
| assert reg_dataset.scale is not None | |
| center = np.array([child.nx, child.ny, child.nz]) / 2 | |
| center_transformed = reg_dataset.scale * np.dot( | |
| reg_dataset.rotation_obj.as_matrix(), center | |
| ) + np.array([reg_dataset.tx, reg_dataset.ty, reg_dataset.tz]) | |
| lower_corner = center_transformed - reg_dataset.scale * center | |
| upper_corner = center_transformed + reg_dataset.scale * center | |
| annotation = neuroglancer.AxisAlignedBoundingBoxAnnotation( | |
| id=f"{child.roi}", | |
| pointA=lower_corner.tolist(), | |
| pointB=upper_corner.tolist(), | |
| description=f"{child.roi} ({child._resolution_str()} um)", | |
| ) | |
| annotations.append(annotation) | |
| # new_layer.annotations = annotations | |
| viewer_state.layers["Zoom datasets"] = new_layer | |
| viewer_state.layers["Zoom datasets"].annotations = annotations | |
| viewer_state.layers["Zoom datasets"].tab = "annotations" | |
| viewer_state.selected_layer.layer = "Zoom datasets" | |
| return viewer_state | |
| def neuroglancer_link( | |
| dataset: "HiPCTDataSet", | |
| *, | |
| with_parent: bool = False, | |
| with_child_boxes: bool = False, | |
| ) -> str: | |
| """ | |
| Nueroglancer link for an individual dataset. | |
| Parameters | |
| ---------- | |
| dataset : | |
| Dataset to create link for. | |
| with_parent : | |
| If `True`, put this dataset inside the parent dataset it is registered to. | |
| Only works for VOI datasets that are registered. | |
| """ | |
| state = neuroglancer_state( | |
| dataset, with_parent=with_parent, with_child_boxes=with_child_boxes | |
| ) | |
| return f"{NEUROGLANCER_INSTANCE}/#!{json.dumps(state.to_json(), separators=(',', ':'))}" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment