Skip to content

Commit

Permalink
Merge branch 'main' into feature/oriented-points-layer
Browse files Browse the repository at this point in the history
  • Loading branch information
seankmartin committed Jul 10, 2024
2 parents cd27d0c + 5d76c9c commit 7f12df7
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 37 deletions.
52 changes: 27 additions & 25 deletions cryoet_data_portal_neuroglancer/models/json_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@
NonOrientedPointShaderBuilder,
OrientedPointShaderBuilder,
)
from cryoet_data_portal_neuroglancer.shaders.image import (
ImageShaderBuilder,
ImageWithVolumeRenderingShaderBuilder,
)


def create_source(
Expand Down Expand Up @@ -79,36 +83,33 @@ class ImageJSONGenerator(RenderingJSONGenerator):
mean: float = None
rms: float = None
is_visible: bool = True
has_volume_rendering_shader: bool = False
volume_rendering_depth_samples: int = 256 # Ideally, this should be a power of 2

def __post_init__(self):
self._type = RenderingTypes.IMAGE

def _create_shader_and_controls(self) -> dict[str, Any]:
def _compute_contrast_limits(self) -> tuple[float, float]:
if self.mean is None or self.rms is None:
distance = self.contrast_limits[1] - self.contrast_limits[0]
window_start = self.contrast_limits[0] - (distance / 10)
window_end = self.contrast_limits[1] + (distance / 10)
shader = (
f"#uicontrol invlerp contrast(range=[{self.contrast_limits[0]}, {self.contrast_limits[1]}], "
f"window=[{window_start}, {window_end}])\nvoid main() {{\n emitGrayscale(contrast());\n}}"
)
return {"shader": shader}

return self.contrast_limits
width = 3 * self.rms
start = self.mean - width
end = self.mean + width
window_width_factor = width * 0.1
window_start = start - window_width_factor
window_end = end + window_width_factor
return {
"shader": "#uicontrol invlerp normalized\n\nvoid main() {\n emitGrayscale(normalized());\n}\n",
"shaderControls": {
"normalized": {
"range": [start, end],
"window": [window_start, window_end],
},
},
}
return (self.mean - width, self.mean + width)

def _create_shader_and_controls(self) -> dict[str, Any]:
contrast_limits = self._compute_contrast_limits()
if self.has_volume_rendering_shader:
# At the moment these are the same limits,
# but in the future the calculation might change for 3D rendering
threedee_contrast_limits = contrast_limits
shader_builder = ImageWithVolumeRenderingShaderBuilder(
contrast_limits=contrast_limits,
threedee_contrast_limits=threedee_contrast_limits,
)
else:
shader_builder = ImageShaderBuilder(
contrast_limits=contrast_limits,
)
return shader_builder.build()

def _get_computed_values(self) -> dict[str, Any]:
nstart = self.start or {k: 0 for k in "xyz"}
Expand All @@ -129,6 +130,8 @@ def generate_json(self) -> dict:
"tab": "rendering",
"visible": self.is_visible,
}
if self.has_volume_rendering_shader:
config["volumeRenderingDepthSamples"] = self.volume_rendering_depth_samples
return {**config, **self._create_shader_and_controls(), **self._get_computed_values()}


Expand Down Expand Up @@ -211,7 +214,6 @@ class ImageVolumeJSONGenerator(RenderingJSONGenerator):

color: str
rendering_depth: int
is_visible: bool = True

def __post_init__(self):
self._type = RenderingTypes.IMAGE
Expand Down
Empty file.
149 changes: 149 additions & 0 deletions cryoet_data_portal_neuroglancer/shaders/image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
from typing import Optional

from cryoet_data_portal_neuroglancer.shaders.shader_builder import ShaderBuilder
from cryoet_data_portal_neuroglancer.utils import get_window_limits_from_contrast_limits


class ImageShaderBuilder(ShaderBuilder):
"""Create a shader for Neuroglancer to display an image.
The shader will have a contrast control that can be adjusted by the user.
The contrast control is invertible.
There is no separate volume rendering control for contrast.
"""

def __init__(
self,
contrast_limits: tuple[float, float],
window_limits: Optional[tuple[float, float]] = None,
contrast_name="contrast",
create_default_shader=True,
):
"""Create a shader for Neuroglancer to display an image.
Parameters
----------
contrast_limits : tuple[float, float]
The minimum and maximum values for the contrast control.
window_limits : tuple[float, float], optional
The minimum and maximum values for the window control, by default None.
If None, the window limits will be calculated from the contrast limits.
contrast_name : str, optional
The name of the contrast control, by default "contrast".
create_default_shader : bool, optional
Whether to create the default shader, by default True.
This is primarily turned off by subclasses.
A subclass will call the _make_default_shader method to create the shader.
But when initializing the base class, the default shader is usually intended to be created.
"""
super().__init__()
self._contrast_limits = contrast_limits
self._window_limits = (
window_limits if window_limits is not None else get_window_limits_from_contrast_limits(contrast_limits)
)
self._contrast_name = contrast_name

# This is a hack to suppress the call to _make_default_shader in the super class
if create_default_shader:
self._make_default_shader()

def _make_default_shader(self, suppress_emission=False):
self.add_to_shader_controls(
self.make_invertible_invlerp_component(
self._contrast_name,
self._contrast_limits,
self._window_limits,
),
)
self.add_to_shader_main("float outputValue;")

if not suppress_emission:
self.add_to_shader_main(f"outputValue = get_{self._contrast_name}();")
self.add_to_shader_main("emitGrayscale(outputValue);")


class ImageWithVolumeRenderingShaderBuilder(ImageShaderBuilder):
"""Create a shader for Neuroglancer to display an image.
The shader will have a contrast control that can be adjusted by the user.
The contrast control is invertible.
There is a separate volume rendering control for contrast.
"""

def __init__(
self,
contrast_limits: tuple[float, float],
threedee_contrast_limits: tuple[float, float],
contrast_name="contrast",
window_limits: Optional[tuple[float, float]] = None,
threedee_window_limits: Optional[tuple[float, float]] = None,
threedee_contrast_name="contrast3D",
):
"""Create a shader for Neuroglancer to display an image.
Parameters
----------
contrast_limits : tuple[float, float]
The minimum and maximum values for the contrast control.
threedee_contrast_limits : tuple[float, float]
The minimum and maximum values for the contrast control for volume rendering.
contrast_name : str, optional
The name of the contrast control, by default "contrast".
window_limits : tuple[float, float], optional
The minimum and maximum values for the window control, by default None.
If None, the window limits will be calculated from the contrast limits.
threedee_window_limits : tuple[float, float], optional
The minimum and maximum values for the window control for volume rendering, by default None.
If None, the window limits will be calculated from the contrast limits.
threedee_contrast_name : str, optional
The name of the contrast control for volume rendering, by default "contrast3D".
"""
super().__init__(
contrast_limits=contrast_limits,
window_limits=window_limits,
contrast_name=contrast_name,
create_default_shader=False,
)
self._threedee_contrast_limits = threedee_contrast_limits
self._threedee_window_limits = (
threedee_window_limits
if threedee_window_limits is not None
else get_window_limits_from_contrast_limits(threedee_contrast_limits)
)
self._threedee_contrast_name = threedee_contrast_name

self._make_default_shader()

def _make_default_shader(self):
super()._make_default_shader(suppress_emission=True)
self.add_to_shader_controls(
self.make_invertible_invlerp_component(
self._threedee_contrast_name,
self._threedee_contrast_limits,
self._threedee_window_limits,
),
)

self._add_cross_section_and_vr_code(
[
f"outputValue = get_{self._threedee_contrast_name}();",
"emitIntensity(outputValue);",
],
[
f"outputValue = get_{self._contrast_name}();",
],
)

self.add_to_shader_main("emitGrayscale(outputValue);")

def _add_cross_section_and_vr_code(
self,
volume_rendering_code: str | list[str],
cross_section_code: str | list[str],
):
self.add_to_shader_main("if (VOLUME_RENDERING) {")
self.add_to_shader_main(volume_rendering_code, indent=2)
self.add_to_shader_main("} else {")
self.add_to_shader_main(cross_section_code, indent=2)
self.add_to_shader_main("}")
35 changes: 27 additions & 8 deletions cryoet_data_portal_neuroglancer/shaders/shader_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,30 @@ def add_to_shader_main(self, code: str | list[str], indent: int = 1):
def _make_main(self) -> str:
return f"void main() {{\n{self._shader_main_function}}}"

def build_shader(self) -> dict[str, str | dict[str, Any]]:
def sort_shader_preamble(self, sorting: lambda x: x):
self._shader_pre_main = "\n".join(sorted(self._shader_pre_main.split("\n"), key=sorting))

def _make_pre_main(self) -> str:
"""Sort the preamble for more visually appealing code"""
# Extract all the #uicontrol lines and put them at the top
uicontrol_lines = []
pre_main_lines = []
for line in self._shader_pre_main.split("\n"):
if line.startswith("#uicontrol"):
uicontrol_lines.append(line)
else:
pre_main_lines.append(line)
# Create a blank line between the uicontrols and the rest of the code
if len(pre_main_lines) > 1:
pre_main_lines.insert(0, "")
return "\n".join(uicontrol_lines + pre_main_lines)

def build(self) -> dict[str, str | dict[str, Any]]:
return {
"shader": self._shader_pre_main + "\n" + self._make_main(),
"shader": self._make_pre_main() + "\n" + self._make_main(),
"shaderControls": self._shader_controls,
}

def sort_shader_preamble(self, sorting: lambda x: x):
self._shader_pre_main = "\n".join(sorted(self._shader_pre_main.split("\n"), key=sorting))

def make_invlerp_component(
self,
name: str,
Expand All @@ -57,9 +72,13 @@ def make_invertible_invlerp_component(
window_limits: tuple[float, float],
) -> list[str]:
invlerp_component = self.make_invlerp_component(name, contrast_limits, window_limits)
checkbox_part = f"#uicontrol bool invert_{name} checkbox(default=false)"
data_value_getter = f"float {name}_get() {{ return invert_{name} ? 1.0 - {name}() : {name}(); }}"
return [invlerp_component, checkbox_part, data_value_getter]
checkbox_part = f"#uicontrol bool invert_{name} checkbox"
data_value_getter = [
f"float get_{name}()" + " {",
f"{TAB}return invert_{name} ? 1.0 - {name}() : {name}();",
"}",
]
return [invlerp_component, checkbox_part, *data_value_getter]

def make_slider_component(
self,
Expand Down
9 changes: 5 additions & 4 deletions cryoet_data_portal_neuroglancer/state_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ def generate_image_layer(
mean: float = None,
rms: float = None,
is_visible: bool = True,
has_volume_rendering_shader: bool = False,
) -> dict[str, Any]:
source, name, url, _, scale = _setup_creation(source, name, url, scale=scale)
return ImageJSONGenerator(
Expand All @@ -122,6 +123,7 @@ def generate_image_layer(
mean=mean,
rms=rms,
is_visible=is_visible,
has_volume_rendering_shader=has_volume_rendering_shader,
).to_json()


Expand All @@ -132,7 +134,7 @@ def generate_image_volume_layer(
color: str = "#FFFFFF",
scale: tuple[float, float, float] = (1.0, 1.0, 1.0),
is_visible: bool = True,
rendering_depth: int = 10000,
rendering_depth: int = 1024,
) -> dict[str, Any]:
source, name, url, _, scale = _setup_creation(source, name, url, scale=scale)
_validate_color(color)
Expand All @@ -148,7 +150,7 @@ def generate_image_volume_layer(

def combine_json_layers(
layers: list[dict[str, Any]],
scale: Optional[tuple[float, float, float] | list[float]],
scale: tuple[float, float, float] | list[float] | float,
units: str = "m",
projection_quaternion: list[float] = None,
) -> dict[str, Any]:
Expand All @@ -168,8 +170,7 @@ def combine_json_layers(
"crossSectionBackgroundColor": "#000000",
"layout": "4panel",
}
# TODO temp, remove this fix
if len(image_layers) > 0:
if len(image_layers) > 0 and "_position" in image_layers[0]:
combined_json["position"] = image_layers[0]["_position"]
combined_json["crossSectionScale"] = image_layers[0]["_crossSectionScale"]
combined_json["projectionScale"] = image_layers[0]["_projectionScale"]
Expand Down
28 changes: 28 additions & 0 deletions cryoet_data_portal_neuroglancer/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,31 @@ def rotate_xyz_via_matrix(matrix: np.ndarray) -> np.ndarray:
The rotated XYZ axes
"""
return np.dot(matrix, np.eye(3)).T


def get_window_limits_from_contrast_limits(
contrast_limits: tuple[float, float],
distance_scale: float = 0.1,
) -> tuple[float, float]:
"""
Create default window limits from contrast limits, 10% padding
Parameters
----------
contrast_limits : tuple[float, float]
The contrast limits
Returns
-------
tuple[float, float]
The window limits
"""
lower_contrast, higher_contrast = contrast_limits
# First check if the contrast limits are inverted
if lower_contrast > higher_contrast:
lower_contrast, higher_contrast = higher_contrast, lower_contrast

distance = higher_contrast - lower_contrast
window_start = lower_contrast - (distance * distance_scale)
window_end = higher_contrast + (distance * distance_scale)
return window_start, window_end
Loading

0 comments on commit 7f12df7

Please sign in to comment.