Skip to content

Orthogonal views #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 12 commits into
base: main
Choose a base branch
from
45 changes: 44 additions & 1 deletion finn/track_application_menus/main_app.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,68 @@
from qtpy.QtCore import Qt
from qtpy.QtWidgets import (
QPushButton,
QSplitter,
QVBoxLayout,
QWidget,
)

import finn
from finn.track_application_menus.menu_widget import MenuWidget
from finn.track_data_views.views.tree_view.tree_widget import TreeWidget
from finn.track_data_views.views.view_3d.orthogonal_views import OrthogonalViews


class MainApp(QWidget):
"""Combines the different tracker widgets for faster dock arrangement"""

def __init__(self, viewer: finn.Viewer):
super().__init__()
self.viewer = viewer

self.viewer.dims.events.ndim.connect(self.dims_changed)

self.menu_widget = MenuWidget(viewer)
self.orth_views = OrthogonalViews(viewer)
tree_widget = TreeWidget(viewer)

self.splitter = QSplitter(Qt.Horizontal) # Set orientation to horizontal
self.splitter.addWidget(self.orth_views) # Add the orthogonal views
self.splitter.addWidget(self.menu_widget) # Add the menu widget
self.splitter.setSizes([0, 300])
viewer.window.add_dock_widget(tree_widget, area="bottom", name="Tree View")

layout = QVBoxLayout()
layout.addWidget(self.menu_widget)

self.collapse_btn = QPushButton("Show/hide orthogonal views")
self.collapse_btn.clicked.connect(self.toggle_orth_views)

if self.viewer.dims.ndim < 3:
self.collapse_btn.hide()
layout.addWidget(self.collapse_btn)
layout.addWidget(self.splitter)

self.setLayout(layout)

def toggle_orth_views(self):
"""Show/Hide the orthogonal views depending on their current state"""

if self.splitter.sizes()[0] > 0:
self.splitter.setSizes([0, 300])
self.orth_views.hide()
else:
self.splitter.setSizes([300, 300])
self.orth_views.show()
self.adjustSize()

def dims_changed(self):
"""Show/Hide the orthogonal views depending on the amount of dimensions of the viewer"""

if self.viewer.dims.ndim > 2:
self.collapse_btn.show()
self.splitter.setSizes([300, 300])
self.orth_views.show()
self.adjustSize()
else:
self.collapse_btn.hide()
self.splitter.setSizes([0, 300])
self.orth_views.hide()
1 change: 1 addition & 0 deletions finn/track_application_menus/menu_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ def __init__(self, viewer: finn.Viewer):
self.setWidgetResizable(True)

self.setLayout(layout)
self.setMinimumWidth(300)
123 changes: 123 additions & 0 deletions finn/track_data_views/views/layers/contour_labels.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from __future__ import annotations

import numpy as np
from scipy import ndimage as ndi

import finn
from finn.layers.labels._labels_utils import (
expand_slice,
)
from finn.utils import DirectLabelColormap


def get_contours(
labels: np.ndarray,
thickness: int,
background_label: int,
group_labels: list[int] | None = None,
):
"""Computes the contours of a 2D label image.

Parameters
----------
labels : array of integers
An input labels image.
thickness : int
It controls the thickness of the inner boundaries. The outside thickness is always 1.
The final thickness of the contours will be `thickness + 1`.
background_label : int
That label is used to fill everything outside the boundaries.

Returns
-------
A new label image in which only the boundaries of the input image are kept.
"""
struct_elem = ndi.generate_binary_structure(labels.ndim, 1)

thick_struct_elem = ndi.iterate_structure(struct_elem, thickness).astype(bool)

dilated_labels = ndi.grey_dilation(labels, footprint=struct_elem)
eroded_labels = ndi.grey_erosion(labels, footprint=thick_struct_elem)
not_boundaries = dilated_labels == eroded_labels

contours = labels.copy()
contours[not_boundaries] = background_label

# instead of filling with background label, fill the group label with their normal color
if group_labels is not None and len(group_labels) > 0:
group_mask = np.isin(labels, group_labels)
combined_mask = not_boundaries & group_mask
contours = np.where(combined_mask, labels, contours)

return contours


class ContourLabels(finn.layers.Labels):
"""Extended labels layer that allows to show contours and filled labels simultaneously"""

@property
def _type_string(self) -> str:
return (
"labels" # to make sure that the layer is treated as labels layer for saving
)

def __init__(
self,
data: np.array,
name: str,
opacity: float,
scale: tuple,
colormap: DirectLabelColormap,
):
super().__init__(
data=data,
name=name,
opacity=opacity,
scale=scale,
colormap=colormap,
)

self.group_labels = None

def _calculate_contour(
self, labels: np.ndarray, data_slice: tuple[slice, ...]
) -> np.ndarray | None:
"""Calculate the contour of a given label array within the specified data slice.

Parameters
----------
labels : np.ndarray
The label array.
data_slice : Tuple[slice, ...]
The slice of the label array on which to calculate the contour.

Returns
-------
Optional[np.ndarray]
The calculated contour as a boolean mask array.
Returns None if the contour parameter is less than 1,
or if the label array has more than 2 dimensions.
"""

if self.contour < 1:
return None
if labels.ndim > 2:
return None

expanded_slice = expand_slice(data_slice, labels.shape, 1)
sliced_labels = get_contours(
labels[expanded_slice],
self.contour,
self.colormap.background_value,
self.group_labels,
)

# Remove the latest one-pixel border from the result
delta_slice = tuple(
slice(s1.start - s2.start, s1.stop - s2.start)
for s1, s2 in zip(data_slice, expanded_slice, strict=False)
)
return sliced_labels[delta_slice]

def set_group_labels(self, labels: list[int] | None = None):
self.group_labels = labels
Loading
Loading