Skip to content

Implement PolygonSelector #837

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 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/source/api/graphics/ImageGraphic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ Methods
ImageGraphic.add_linear_region_selector
ImageGraphic.add_linear_selector
ImageGraphic.add_rectangle_selector
ImageGraphic.add_polygon_selector
ImageGraphic.clear_event_handlers
ImageGraphic.remove_event_handler
ImageGraphic.reset_vmin_vmax
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/graphics/LineCollection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Methods
LineCollection.add_linear_region_selector
LineCollection.add_linear_selector
LineCollection.add_rectangle_selector
LineCollection.add_polygon_selector
LineCollection.clear_event_handlers
LineCollection.remove_event_handler
LineCollection.remove_graphic
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/graphics/LineGraphic.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Methods
LineGraphic.add_linear_region_selector
LineGraphic.add_linear_selector
LineGraphic.add_rectangle_selector
LineGraphic.add_polygon_selector
LineGraphic.clear_event_handlers
LineGraphic.remove_event_handler
LineGraphic.rotate
Expand Down
1 change: 1 addition & 0 deletions docs/source/api/graphics/LineStack.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ Methods
LineStack.add_linear_region_selector
LineStack.add_linear_selector
LineStack.add_rectangle_selector
LineStack.add_polygon_selector
LineStack.clear_event_handlers
LineStack.remove_event_handler
LineStack.remove_graphic
Expand Down
66 changes: 66 additions & 0 deletions examples/selection_tools/polygon_selector.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
"""
Polygon Selectors
=================

Example showing how to use a `PolygonSelector` (a.k.a. lasso selector) with line collections
"""

# test_example = false
# sphinx_gallery_pygfx_docs = 'screenshot'

import numpy as np
import fastplotlib as fpl
from itertools import product

# create a figure
figure = fpl.Figure(
size=(700, 560)
)


# generate some data
def make_circle(center, radius: float, n_points: int = 75) -> np.ndarray:
theta = np.linspace(0, 2 * np.pi, n_points)
xs = radius * np.sin(theta)
ys = radius * np.cos(theta)

return np.column_stack([xs, ys]) + center


spatial_dims = (50, 50)

circles = list()
for center in product(range(0, spatial_dims[0], 9), range(0, spatial_dims[1], 9)):
circles.append(make_circle(center, 3, n_points=75))

pos_xy = np.vstack(circles)

# add image
line_collection = figure[0, 0].add_line_collection(circles, cmap="jet", thickness=5)

# add polygon selector to image graphic
polygon_selector = line_collection.add_polygon_selector(fill_color="#ff00ff22", edge_color="#FFF", vertex_color="#FFF")


# add event handler to highlight selected indices
@polygon_selector.add_event_handler("selection")
def color_indices(ev):
line_collection.cmap = "jet"
ixs = ev.get_selected_indices()

# iterate through each of the selected indices, if the array size > 0 that mean it's under the selection
selected_line_ixs = [i for i in range(len(ixs)) if ixs[i].size > 0]
line_collection[selected_line_ixs].colors = "w"


# # manually move selector to make a nice gallery image :D
# polygon_selector.selection = (15, 30, 15, 30)


figure.show()

# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
# please see our docs for using fastplotlib interactively in ipython and jupyter
if __name__ == "__main__":
print(__doc__)
fpl.loop.run()
108 changes: 108 additions & 0 deletions fastplotlib/graphics/features/_selection_features.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from typing import Sequence

import numpy as np
import pygfx as gfx

from ...utils import mesh_masks
from ._base import GraphicFeature, GraphicFeatureEvent, block_reentrance
from ...utils.triangulation import triangulate


class LinearSelectionFeature(GraphicFeature):
Expand Down Expand Up @@ -340,3 +342,109 @@ def set_value(self, selector, value: Sequence[float]):

# calls any events
self._call_event_handlers(event)


class PolygonSelectionFeature(GraphicFeature):
event_info_spec = [
{
"dict key": "value",
"type": "np.ndarray",
"description": "new array of points that represents the polygon selection",
},
]

event_extra_attrs = [
{
"attribute": "get_selected_indices",
"type": "callable",
"description": "returns indices under the selector",
},
{
"attribute": "get_selected_data",
"type": "callable",
"description": "returns data under the selector",
},
]

def __init__(
self,
value: Sequence[tuple[float]],
limits: tuple[float, float, float, float],
):
super().__init__()

self._limits = limits
self._value = np.asarray(value).reshape(-1, 3).astype(float)

@property
def value(self) -> np.ndarray[float]:
"""
The array of the polygon, in data space
"""
return self._value

@block_reentrance
def set_value(self, selector, value: Sequence[tuple[float]]):
"""
Set the selection of the rectangle selector.

Parameters
----------
selector: PolygonSelector

value: array
new values (3D points) of the selection
"""

value = np.asarray(value, dtype=np.float32)

if not value.shape[1] == 3:
raise TypeError(
"Selection must be an array, tuple, list, or sequence of the shape Nx3."
)

# clip values if they are beyond the limits
value[:, 0] = value[:, 0].clip(self._limits[0], self._limits[1])
value[:, 1] = value[:, 1].clip(self._limits[2], self._limits[3])

self._value = value

if len(value) >= 3:
indices = triangulate(value)
else:
indices = np.zeros((0, 3), np.int32)

# TODO: Update the fill mesh
# selector.fill.geometry.positions = ...

geometry = selector.geometry

# Need larger buffer?
if len(value) > geometry.positions.nitems:
arr = np.zeros((geometry.positions.nitems * 2, 3), np.float32)
geometry.positions = gfx.Buffer(arr)
if len(indices) > geometry.indices.nitems:
arr = np.zeros((geometry.indices.nitems * 2, 3), np.int32)
geometry.indices = gfx.Buffer(arr)

geometry.positions.data[: len(value)] = value
geometry.positions.data[len(value)] = value[-1] if len(value) else (0, 0, 0)
geometry.positions.draw_range = 0, len(value)
geometry.positions.update_full()

geometry.indices.data[: len(indices)] = indices
geometry.indices.data[len(indices)] = 0
geometry.indices.draw_range = 0, len(indices)
geometry.indices.update_full()

# send event
if len(self._event_handlers) < 1:
return

event = GraphicFeatureEvent("selection", {"value": self.value})

event.get_selected_indices = selector.get_selected_indices
event.get_selected_data = selector.get_selected_data

# calls any events
self._call_event_handlers(event)
52 changes: 50 additions & 2 deletions fastplotlib/graphics/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

from ..utils import quick_min_max
from ._base import Graphic
from .selectors import LinearSelector, LinearRegionSelector, RectangleSelector
from .selectors import (
LinearSelector,
LinearRegionSelector,
RectangleSelector,
PolygonSelector,
)
from .features import (
TextureArray,
ImageCmap,
Expand Down Expand Up @@ -169,7 +174,6 @@ def __init__(
# iterate through each texture chunk and create
# an _ImageTIle, offset the tile using the data indices
for texture, chunk_index, data_slice in self._data:

# create an ImageTile using the texture for this chunk
img = _ImageTile(
geometry=pygfx.Geometry(grid=texture),
Expand Down Expand Up @@ -437,3 +441,47 @@ def add_rectangle_selector(
selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1)

return selector

def add_polygon_selector(
self,
selection: List[tuple[float, float]] = None,
fill_color=(0, 0, 0.35, 0.2),
**kwargs,
) -> PolygonSelector:
"""
Add a :class:`.PolygonSelector`.

Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them
from a plot area just like any other ``Graphic``.

Parameters
----------
selection: List of positions, optional
initial points for the polygon

"""
# default selection is 25% of the diagonal
if selection is None:
diagonal = math.sqrt(
self._data.value.shape[0] ** 2 + self._data.value.shape[1] ** 2
)

selection = (0, int(diagonal / 4), 0, int(diagonal / 4))

# min/max limits are image shape
# rows are ys, columns are xs
limits = (0, self._data.value.shape[1], 0, self._data.value.shape[0])

selector = PolygonSelector(
limits,
fill_color=fill_color,
parent=self,
**kwargs,
)

self._plot_area.add_graphic(selector, center=False)

# place above this graphic
selector.offset = selector.offset + (0.0, 0.0, self.offset[-1] + 1)

return selector
50 changes: 49 additions & 1 deletion fastplotlib/graphics/line.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
import pygfx

from ._positions_base import PositionsGraphic
from .selectors import LinearRegionSelector, LinearSelector, RectangleSelector
from .selectors import (
LinearRegionSelector,
LinearSelector,
RectangleSelector,
PolygonSelector,
)
from .features import (
Thickness,
VertexPositions,
Expand Down Expand Up @@ -288,6 +293,49 @@ def add_rectangle_selector(

return selector

def add_polygon_selector(
self,
selection: List[tuple[float, float]] = None,
**kwargs,
) -> PolygonSelector:
"""
Add a :class:`.PolygonSelector`.

Selectors are just ``Graphic`` objects, so you can manage, remove, or delete them from a
plot area just like any other ``Graphic``.

Parameters
----------
selection: (float, float, float, float), optional
initial (xmin, xmax, ymin, ymax) of the selection
"""

# remove any nans
data = self.data.value[~np.any(np.isnan(self.data.value), axis=1)]

x_axis_vals = data[:, 0]
y_axis_vals = data[:, 1]

ymin = np.floor(y_axis_vals.min()).astype(int)
ymax = np.ceil(y_axis_vals.max()).astype(int)

# default selection is 25% of the image
if selection is None:
selection = []

# min/max limits
limits = (x_axis_vals[0], x_axis_vals[-1], ymin * 1.5, ymax * 1.5)

selector = PolygonSelector(
limits,
parent=self,
**kwargs,
)

self._plot_area.add_graphic(selector, center=False)

return selector

# TODO: this method is a bit of a mess, can refactor later
def _get_linear_selector_init_args(
self, axis: str, padding
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy