Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ ci:

repos:
- repo: https://github.com/crate-ci/typos
rev: v1
rev: v1.35.4
hooks:
- id: typos
args: [--force-exclude] # omitting --write-changes
Expand All @@ -29,4 +29,4 @@ repos:
files: "^src/"
additional_dependencies:
- pymmcore-plus >=0.15.0
- useq-schema >=0.5.0
- useq-schema >=0.8.0
37 changes: 37 additions & 0 deletions examples/temp/grid_from_polygon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import useq
from pymmcore_plus import CMMCorePlus
from qtpy.QtWidgets import QApplication

from pymmcore_widgets import MDAWidget

mmc = CMMCorePlus.instance()
mmc.loadSystemConfiguration()

app = QApplication([])

poly1 = useq.GridFromPolygon(
vertices=[(-400, 0), (1000, -500), (500, 1200), (0, 100)],
fov_height=100,
fov_width=100,
overlap=(10, 10),
)
poly2 = useq.GridFromPolygon(
vertices=[(0, 0), (300, 0), (300, 100), (100, 100), (100, 300), (0, 300)],
fov_height=100,
fov_width=100,
overlap=(10, 10),
)
pos1 = useq.AbsolutePosition(
x=1, y=2, z=3, name="pos1", sequence=useq.MDASequence(grid_plan=poly1)
)
pos2 = useq.AbsolutePosition(
x=4, y=5, z=6, name="pos2", sequence=useq.MDASequence(grid_plan=poly2)
)

seq = useq.MDASequence(stage_positions=[pos1, pos2])

m = MDAWidget()
m.setValue(seq)
m.show()

app.exec()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ dependencies = [
'pymmcore-plus[cli] >=0.14.0',
'qtpy >=2.0',
'superqt[quantity,cmap,iconify] >=0.7.1',
'useq-schema >=0.7.3',
'useq-schema >=0.8.0',
'vispy >=0.15.0',
"pyopengl >=3.1.9; platform_system == 'Darwin'",
"shapely>=2.0.7",
Expand Down
15 changes: 0 additions & 15 deletions src/pymmcore_widgets/_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from pathlib import Path
from typing import TYPE_CHECKING

import useq
from psygnal import SignalInstance
from pymmcore_plus import CMMCorePlus
from qtpy.QtCore import QEvent, QMarginsF, QObject, Qt
Expand Down Expand Up @@ -144,20 +143,6 @@ def block_core(obj: Any) -> AbstractContextManager:
raise TypeError(f"Cannot block signals for {obj}")


def cast_grid_plan(
grid: dict | useq.GridRowsColumns | useq.GridWidthHeight | useq.GridFromEdges,
) -> useq.GridRowsColumns | useq.GridWidthHeight | useq.GridFromEdges | None:
"""Get the grid type from the grid_plan."""
if not grid or isinstance(grid, useq.RandomPoints):
return None
if isinstance(grid, dict):
_grid = useq.MDASequence(grid_plan=grid).grid_plan
if isinstance(_grid, useq.RelativePosition): # pragma: no cover
raise ValueError("Grid plan cannot be a single Relative position.")
return None if isinstance(_grid, useq.RandomPoints) else _grid
return grid


def fov_kwargs(core: CMMCorePlus) -> dict:
"""Return image width and height in micron to be used for the grid plan."""
if px := core.getPixelSizeUm():
Expand Down
112 changes: 3 additions & 109 deletions src/pymmcore_widgets/control/_rois/roi_model.py
Original file line number Diff line number Diff line change
@@ -1,118 +1,12 @@
from __future__ import annotations

from dataclasses import dataclass, field
from functools import cached_property
from typing import TYPE_CHECKING, Annotated, Any
from typing import Any
from uuid import UUID, uuid4

import numpy as np
import useq
import useq._grid
from pydantic import Field, PrivateAttr
from shapely import Polygon, box, prepared

if TYPE_CHECKING:
from collections.abc import Iterator


class GridFromPolygon(useq._grid._GridPlan[useq.AbsolutePosition]):
vertices: Annotated[
list[tuple[float, float]],
Field(
min_length=3,
description="List of points that define the polygon",
frozen=True,
),
]

def num_positions(self) -> int:
"""Return the number of positions in the grid."""
if self.fov_width is None or self.fov_height is None:
raise ValueError("fov_width and fov_height must be set")
return len(
self._cached_tiles(
fov=(self.fov_width, self.fov_height), overlap=self.overlap
)
)

def iter_grid_positions(
self,
fov_width: float | None = None,
fov_height: float | None = None,
*,
order: useq.OrderMode | None = None,
) -> Iterator[useq.AbsolutePosition]:
"""Iterate over all grid positions, given a field of view size."""
try:
pos = self._cached_tiles(
fov=(
fov_width or self.fov_width or 1,
fov_height or self.fov_height or 1,
),
overlap=self.overlap,
order=order,
)
except ValueError:
pos = []
for x, y in pos:
yield useq.AbsolutePosition(x=x, y=y)

@cached_property
def poly(self) -> Polygon:
"""Return the polygon vertices as a list of (x, y) tuples."""
return Polygon(self.vertices)

@cached_property
def prepared_poly(self) -> prepared.PreparedGeometry:
"""Return the prepared polygon for faster intersection tests."""
return prepared.prep(self.poly)

_poly_cache: dict[tuple, list[tuple[float, float]]] = PrivateAttr(
default_factory=dict
)

def _cached_tiles(
self,
*,
fov: tuple[float, float],
overlap: tuple[float, float],
order: useq.OrderMode | None = None,
) -> list[tuple[float, float]]:
"""Compute an ordered list of (x, y) stage positions that cover the ROI."""
# Compute grid spacing and half-extents
mode = useq.OrderMode(order) if order is not None else self.mode
key = (fov, overlap, mode)

if key not in self._poly_cache:
w, h = fov
dx = w * (1 - overlap[0])
dy = h * (1 - overlap[1])
half_w, half_h = w / 2, h / 2

# Expand bounds to ensure full coverage
minx, miny, maxx, maxy = self.poly.bounds
minx -= half_w
miny -= half_h
maxx += half_w
maxy += half_h

# Determine grid dimensions
n_cols = int(np.ceil((maxx - minx) / dx))
n_rows = int(np.ceil((maxy - miny) / dy))

# Generate grid positions
positions: list[tuple[float, float]] = []
prepared_poly = self.prepared_poly

for r, c in mode.generate_indices(n_rows, n_cols):
x = c + minx + (c + 0.5) * dx + half_w
y = maxy - (r + 0.5) * dy - half_h
tile = box(x - half_w, y - half_h, x + half_w, y + half_h)
if prepared_poly.intersects(tile):
positions.append((x, y))

self._poly_cache[key] = positions
return self._poly_cache[key]


@dataclass(eq=False)
Expand Down Expand Up @@ -212,8 +106,8 @@ def create_grid_plan(
if type(self) is not RectangleROI:
if len(self.vertices) < 3:
return None
return GridFromPolygon(
vertices=self.vertices,
return useq.GridFromPolygon(
vertices=list(self.vertices),
fov_width=fov_w,
fov_height=fov_h,
)
Expand Down
Loading
Loading