Skip to content

Commit

Permalink
Added exclusion masks
Browse files Browse the repository at this point in the history
  • Loading branch information
StephenThornquist committed Dec 27, 2024
1 parent 4bd1385 commit 868ea5a
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 15 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ packages = ["siffroi"]

[project]
name = "siffroi"
version = "0.0.2"
version = "0.0.3"
#dynamic = ["version"]
readme = "README.md"
description = "A package for annotating central complex ROIs"
Expand Down
Empty file removed setup.cfg
Empty file.
21 changes: 16 additions & 5 deletions siffroi/ellipsoid_body/protocols/use_ellipse.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Optional, TYPE_CHECKING
from typing import Optional, TYPE_CHECKING
import numpy as np

from .extra_rois import ExtraRois
Expand All @@ -7,18 +7,20 @@
from ..rois.ellipse import Ellipse
from ...utils import nth_largest_shape_in_list
from ...utils.mixins import (
UsesAnatomyReferenceMixin, UsesReferenceFramesMixin, ExpectsShapesMixin
UsesAnatomyReferenceMixin, UsesReferenceFramesMixin, ExpectsShapesMixin,
AllowsExclusionsMixin
)

if TYPE_CHECKING:
from ...utils.types import (
MaskLike, PolygonLike, ImageShapeLike, AnatomyReference, ReferenceFrames
AnatomyReference, ReferenceFrames
)

class UseEllipse(
ExpectsShapesMixin,
UsesAnatomyReferenceMixin,
UsesReferenceFramesMixin,
AllowsExclusionsMixin,
ROIProtocol
):

Expand All @@ -43,6 +45,7 @@ def extract(
slice_idx : Optional[int] = None,
extra_rois : 'ExtraRois' = ExtraRois.CENTER,
view_direction : 'ViewDirection' = ViewDirection.ANTERIOR,
exclusion_layer : np.ndarray = None,
)->Ellipse:
image_shape = reference_frames.shape
return use_ellipse(
Expand All @@ -53,7 +56,8 @@ def extract(
view_direction=view_direction,
slice_idx=slice_idx,
extra_rois=extra_rois,
mirrored=mirrored
mirrored=mirrored,
exclusion_layer = exclusion_layer,
)

def use_ellipse(
Expand All @@ -65,6 +69,7 @@ def use_ellipse(
slice_idx : Optional[int] = -1,
extra_rois : 'ExtraRois' = ExtraRois.CENTER,
mirrored : bool = True,
exclusion_layer : np.ndarray = None,
**kwargs) -> 'Ellipse':
"""
Simply takes the largest ellipse type shape in a viewer
Expand Down Expand Up @@ -130,7 +135,7 @@ def use_ellipse(

orientation = 0.0

if not (anatomy_reference is None) and (len(anatomy_reference) > 0):
if anatomy_reference is not None and (len(anatomy_reference) > 0):
if isinstance(anatomy_reference, (tuple,list)):
anatomy_reference = anatomy_reference[0]
# Goes postero-dorsal to antero-ventral
Expand All @@ -141,6 +146,12 @@ def use_ellipse(
orientation += np.angle(1j*start_to_end)
# I always find geometry with complex numbers much easier than using tangents etc.

if exclusion_layer is not None:
main_ellip = np.logical_and(
main_ellip,
np.logical_not(exclusion_layer)
)

return Ellipse(
mask = main_ellip if FROM_MASK else None,
polygon = None if FROM_MASK else main_ellip,
Expand Down
1 change: 0 additions & 1 deletion siffroi/ellipsoid_body/protocols/von_mises.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
# Feels like I use too much interfacing with the CorrelationWindow class,
# which seems like it should do as much as possible without interacting with
# this stuff...
from typing import Any

import numpy as np
from fourcorr import FourCorrAnalysis
Expand Down
6 changes: 3 additions & 3 deletions siffroi/ellipsoid_body/rois/ellipse.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, TYPE_CHECKING, Optional
from typing import TYPE_CHECKING, Optional
import numpy as np
from scipy.ndimage import center_of_mass

Expand Down Expand Up @@ -77,7 +77,7 @@ def __init__(
the line from posterodorsal to anteroventral is rotated clockwise
IN THE IMAGE. So ventral at the bottom = orientation = 3/2 * pi
"""
if not "name" in kwargs:
if "name" not in kwargs:
kwargs["name"] = "Ellipse"
super().__init__(
mask = mask,
Expand All @@ -99,7 +99,7 @@ def wedges(self)->list['WedgeROI']:
@property
def mask(self)->np.ndarray:
""" Returns the mask of the Ellipse """
if not (self._mask is None):
if self._mask is not None:
return self._mask

raise NotImplementedError("Mask from polygon not yet implemented for Ellipse")
Expand Down
13 changes: 12 additions & 1 deletion siffroi/fan_shaped_body/protocols/outline_fan.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
from ...roi_protocol import ROIProtocol
from ...utils import nth_largest_shape_in_list
from ...utils.mixins import (
UsesReferenceFramesMixin, UsesAnatomyReferenceMixin, ExpectsShapesMixin
UsesReferenceFramesMixin, UsesAnatomyReferenceMixin, ExpectsShapesMixin,
AllowsExclusionsMixin
)
from ...utils.types import (
MaskLike, PolygonLike, ImageShapeLike, AnatomyReference, ReferenceFrames
Expand All @@ -18,6 +19,7 @@ class OutlineFan(
ExpectsShapesMixin,
UsesAnatomyReferenceMixin,
UsesReferenceFramesMixin,
AllowsExclusionsMixin,
ROIProtocol
):

Expand All @@ -40,6 +42,7 @@ def extract(
mirrored : bool = True,
slice_idx : Optional[int] = None,
view_direction : ViewDirection = ViewDirection.ANTERIOR,
exclusion_layer : np.ndarray = None,
)-> Fan:
image_shape = reference_frames.shape
return outline_fan(
Expand All @@ -50,6 +53,7 @@ def extract(
roi_name = roi_name,
view_direction=view_direction,
slice_idx=slice_idx,
exclusion_layer = exclusion_layer,
)

def outline_fan(
Expand All @@ -60,6 +64,7 @@ def outline_fan(
mirrored : bool = True,
view_direction : ViewDirection = ViewDirection.ANTERIOR,
slice_idx : Optional[int] = -1,
exclusion_layer : np.ndarray = None,
**kwargs
)-> Fan:
"""
Expand Down Expand Up @@ -94,6 +99,12 @@ def outline_fan(
orientation += np.angle(1j*start_to_end)
# I always find geometry with complex numbers much easier than using tangents etc.

if exclusion_layer is not None:
main_fan = np.logical_and(
main_fan,
np.logical_not(exclusion_layer)
)

return Fan(
mask = main_fan if FROM_MASK else None,
polygon = None if FROM_MASK else main_fan,
Expand Down
10 changes: 8 additions & 2 deletions siffroi/noduli/protocols/noduli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from ...utils import n_largest_shapes_in_list
from ...utils.mixins import (
UsesFrameDataMixin, ExpectsShapesMixin, UsesAnatomyReferenceMixin,
UsesReferenceFramesMixin,
UsesReferenceFramesMixin, AllowsExclusionsMixin
)
from ...utils.types import (
FrameData, ReferenceFrames, AnatomyReference,
Expand All @@ -20,6 +20,7 @@ class DrawROI(
ExpectsShapesMixin,
UsesAnatomyReferenceMixin,
UsesReferenceFramesMixin,
AllowsExclusionsMixin,
ROIProtocol
):

Expand All @@ -38,6 +39,7 @@ def extract(
roi_name : str = "Noduli",
slice_idx : Optional[int] = None,
view_direction : ViewDirection = ViewDirection.POSTERIOR,
exclusion_layer : np.ndarray = None,
)-> Blobs:

image_shape = reference_frames.shape
Expand All @@ -64,7 +66,11 @@ def extract(
orientation += np.angle(1j*start_to_end)
# I always find geometry with complex numbers much easier than using tangents etc.


if exclusion_layer is not None:
blobs = np.logical_and(
blobs,
np.logical_not(exclusion_layer)
)

return Blobs(
mask = blobs if FROM_MASK else None,
Expand Down
2 changes: 1 addition & 1 deletion siffroi/noduli/rois/blob.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def __init__(

view_direction = ViewDirection(view_direction)

if not "name" in kwargs:
if "name" not in kwargs:
kwargs["name"] = "Fan"
super().__init__(
mask=mask,
Expand Down
46 changes: 46 additions & 0 deletions siffroi/protocerebral_bridge/rois/mustache.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,52 @@ def sort_glomeruli_by_center(self, axis : int = -1, increasing : bool = True):
reverse = not increasing,
)

def sort_glomeruli_by_snake(self):
"""
Uses the shortest-path algorithm to sort glomeruli into a snake.
May start at either end of the mustache, so use some analysis to
identify which end should go first (e.g. might need to use the direction
you're imaging from or whether the ROI is mirrored...)
"""
try:
from python_tsp.exact import solve_tsp_dynamic_programming
except ImportError:
print("Please install python-tsp to use this function"
" (pip install python-tsp)"
)
return

# Get the centers of the glomeruli
centers = [glom.center() for glom in self.subROIs]

# Get the distance matrix
dists = np.linalg.norm(
np.array(centers)[:, None] - np.array(centers)[None],
axis = -1
)

# Add a "depot" that is distance 0 to all other points
dists = np.vstack([dists, np.zeros(len(dists))])
dists = np.hstack([dists, np.zeros((len(dists), 1))])

# solve the traveling salesman problem!

path, _ = solve_tsp_dynamic_programming(dists)

# Reorder to start at the depot

start_idx = np.argmax(path)
path = path[start_idx:] + path[:start_idx]

# now remove the deport

path = [p for p in path if p != len(self.subROIs)]

# Reorder the glomeruli
self.subROIs = [self.subROIs[p] for p in path]


class GlomerulusROI(subROI):
"""
A single glomerulus
Expand Down
16 changes: 15 additions & 1 deletion siffroi/utils/mixins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,18 @@ class ExpectsShapesMixin():
def shape_arg_num(self)->int:
""" Returns the position of the shapes argument in the extraction function """
return list(self.extraction_args.keys()).index("shapes")


class AllowsExclusionsMixin():
"""
`AllowsExclusions` means that the mask can be intersected with the negation of
an "exclusion mask" before extraction to ensure those pixels are ignored.
"""

extraction_args : dict[str, Any]
ANATOMY_REFERENCE_SHAPE_TYPE : str = "any"
LAYER_NAME : str = "exclusion_layer"

@property
def exclusion_mask_arg_num(self)->int:
""" Returns the position of the exclusion_mask argument in the extraction function """
return list(self.extraction_args.keys()).index("exclusion_mask")

0 comments on commit 868ea5a

Please sign in to comment.