Skip to content

Commit 9427b79

Browse files
Merge pull request #193 from scverse/fix/modules
moved 2 functions in wrong module
2 parents 7288fdc + 7ded073 commit 9427b79

File tree

4 files changed

+168
-165
lines changed

4 files changed

+168
-165
lines changed

src/spatialdata/_core/operations/transform.py

Lines changed: 2 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import itertools
44
from functools import singledispatch
5-
from typing import TYPE_CHECKING, Any, Optional, Union
5+
from typing import TYPE_CHECKING, Any, Optional
66

77
import dask.array as da
88
import dask_image.ndinterp
@@ -11,7 +11,6 @@
1111
from dask.dataframe.core import DataFrame as DaskDataFrame
1212
from geopandas import GeoDataFrame
1313
from multiscale_spatial_image import MultiscaleSpatialImage
14-
from skimage.transform import estimate_transform
1514
from spatial_image import SpatialImage
1615
from xarray import DataArray
1716

@@ -21,14 +20,10 @@
2120
from spatialdata.models import SpatialElement, get_axis_names, get_model
2221
from spatialdata.models._utils import DEFAULT_COORDINATE_SYSTEM
2322
from spatialdata.transformations._utils import _get_scale, compute_coordinates
24-
from spatialdata.transformations.operations import (
25-
get_transformation,
26-
set_transformation,
27-
)
23+
from spatialdata.transformations.operations import set_transformation
2824

2925
if TYPE_CHECKING:
3026
from spatialdata.transformations.transformations import (
31-
Affine,
3227
BaseTransformation,
3328
Translation,
3429
)
@@ -384,156 +379,3 @@ def _(data: GeoDataFrame, transformation: BaseTransformation, maintain_positioni
384379
)
385380
ShapesModel.validate(transformed_data)
386381
return transformed_data
387-
388-
389-
def get_transformation_between_landmarks(
390-
references_coords: Union[GeoDataFrame, DaskDataFrame],
391-
moving_coords: Union[GeoDataFrame, DaskDataFrame],
392-
) -> Affine:
393-
"""
394-
Get a similarity transformation between two lists of (n >= 3) landmarks. Landmarks are assumed to be in the same space.
395-
396-
Parameters
397-
----------
398-
references_coords
399-
landmarks annotating the reference element. Must be a valid element describing points or circles.
400-
moving_coords
401-
landmarks annotating the moving element. Must be a valid element describing points or circles.
402-
403-
Returns
404-
-------
405-
The Affine transformation that maps the moving element to the reference element.
406-
407-
Examples
408-
--------
409-
If you save the landmark points using napari_spatialdata, they will be alredy saved as circles. Here is an
410-
example on how to call this function on two sets of numpy arrays describing x, y coordinates.
411-
>>> import numpy as np
412-
>>> from spatialdata.models import PointsModel
413-
>>> from spatialdata.transform import get_transformation_between_landmarks
414-
>>> points_moving = np.array([[0, 0], [1, 1], [2, 2]])
415-
>>> points_reference = np.array([[0, 0], [10, 10], [20, 20]])
416-
>>> moving_coords = PointsModel(points_moving)
417-
>>> references_coords = PointsModel(points_reference)
418-
>>> transformation = get_transformation_between_landmarks(references_coords, moving_coords)
419-
"""
420-
from spatialdata.transformations.transformations import (
421-
Affine,
422-
BaseTransformation,
423-
Sequence,
424-
)
425-
426-
assert get_axis_names(references_coords) == ("x", "y")
427-
assert get_axis_names(moving_coords) == ("x", "y")
428-
429-
if isinstance(references_coords, GeoDataFrame):
430-
references_xy = np.stack([references_coords.geometry.x, references_coords.geometry.y], axis=1)
431-
moving_xy = np.stack([moving_coords.geometry.x, moving_coords.geometry.y], axis=1)
432-
elif isinstance(references_coords, DaskDataFrame):
433-
references_xy = references_coords[["x", "y"]].to_dask_array().compute()
434-
moving_xy = moving_coords[["x", "y"]].to_dask_array().compute()
435-
else:
436-
raise TypeError("references_coords must be either an GeoDataFrame or a DaskDataFrame")
437-
438-
model = estimate_transform("affine", src=moving_xy, dst=references_xy)
439-
transform_matrix = model.params
440-
a = transform_matrix[:2, :2]
441-
d = np.linalg.det(a)
442-
final: BaseTransformation
443-
if d < 0:
444-
m = (moving_xy[:, 0].max() - moving_xy[:, 0].min()) / 2
445-
flip = Affine(
446-
np.array(
447-
[
448-
[-1, 0, 2 * m],
449-
[0, 1, 0],
450-
[0, 0, 1],
451-
]
452-
),
453-
input_axes=("x", "y"),
454-
output_axes=("x", "y"),
455-
)
456-
flipped_moving = transform(moving_coords, flip, maintain_positioning=False)
457-
if isinstance(flipped_moving, GeoDataFrame):
458-
flipped_moving_xy = np.stack([flipped_moving.geometry.x, flipped_moving.geometry.y], axis=1)
459-
elif isinstance(flipped_moving, DaskDataFrame):
460-
flipped_moving_xy = flipped_moving[["x", "y"]].to_dask_array().compute()
461-
else:
462-
raise TypeError("flipped_moving must be either an GeoDataFrame or a DaskDataFrame")
463-
model = estimate_transform("similarity", src=flipped_moving_xy, dst=references_xy)
464-
final = Sequence([flip, Affine(model.params, input_axes=("x", "y"), output_axes=("x", "y"))])
465-
else:
466-
model = estimate_transform("similarity", src=moving_xy, dst=references_xy)
467-
final = Affine(model.params, input_axes=("x", "y"), output_axes=("x", "y"))
468-
469-
affine = Affine(
470-
final.to_affine_matrix(input_axes=("x", "y"), output_axes=("x", "y")),
471-
input_axes=("x", "y"),
472-
output_axes=("x", "y"),
473-
)
474-
return affine
475-
476-
477-
def align_elements_using_landmarks(
478-
references_coords: Union[GeoDataFrame | DaskDataFrame],
479-
moving_coords: Union[GeoDataFrame | DaskDataFrame],
480-
reference_element: SpatialElement,
481-
moving_element: SpatialElement,
482-
reference_coordinate_system: str = "global",
483-
moving_coordinate_system: str = "global",
484-
new_coordinate_system: Optional[str] = None,
485-
write_to_sdata: Optional[SpatialData] = None,
486-
) -> BaseTransformation:
487-
"""
488-
Maps a moving object into a reference object using two lists of (n >= 3) landmarks; returns the transformations that enable this
489-
mapping and optinally saves them, to map to a new shared coordinate system.
490-
491-
Parameters
492-
----------
493-
references_coords
494-
landmarks annotating the reference element. Must be a valid element describing points or circles.
495-
moving_coords
496-
landmarks annotating the moving element. Must be a valid element describing points or circles.
497-
reference_element
498-
the reference element.
499-
moving_element
500-
the moving element.
501-
reference_coordinate_system
502-
the coordinate system of the reference element that have been used to annotate the landmarks.
503-
moving_coordinate_system
504-
the coordinate system of the moving element that have been used to annotate the landmarks.
505-
new_coordinate_system
506-
If provided, both elements will be mapped to this new coordinate system with the new transformations just
507-
computed.
508-
write_to_sdata
509-
If provided, the transformations will be saved to disk in the specified SpatialData object. The SpatialData
510-
object must be backed and must contain both the reference and moving elements.
511-
512-
Returns
513-
-------
514-
A similarity transformation that maps the moving element to the same coordinate of reference element in the
515-
coordinate system specified by reference_coordinate_system.
516-
"""
517-
from spatialdata.transformations.transformations import BaseTransformation, Sequence
518-
519-
affine = get_transformation_between_landmarks(references_coords, moving_coords)
520-
521-
# get the old transformations of the visium and xenium data
522-
old_moving_transformation = get_transformation(moving_element, moving_coordinate_system)
523-
old_reference_transformation = get_transformation(reference_element, reference_coordinate_system)
524-
assert isinstance(old_moving_transformation, BaseTransformation)
525-
assert isinstance(old_reference_transformation, BaseTransformation)
526-
527-
# compute the new transformations
528-
new_moving_transformation = Sequence([old_moving_transformation, affine])
529-
new_reference_transformation = old_reference_transformation
530-
531-
if new_coordinate_system is not None:
532-
# this allows to work on singleton objects, not embedded in a SpatialData object
533-
set_transformation(
534-
moving_element, new_moving_transformation, new_coordinate_system, write_to_sdata=write_to_sdata
535-
)
536-
set_transformation(
537-
reference_element, new_reference_transformation, new_coordinate_system, write_to_sdata=write_to_sdata
538-
)
539-
return new_moving_transformation

src/spatialdata/transformations/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from spatialdata.transformations.operations import (
2+
align_elements_using_landmarks,
23
get_transformation,
34
get_transformation_between_coordinate_systems,
5+
get_transformation_between_landmarks,
46
remove_transformation,
57
set_transformation,
68
)
@@ -26,4 +28,6 @@
2628
"set_transformation",
2729
"remove_transformation",
2830
"get_transformation_between_coordinate_systems",
31+
"get_transformation_between_landmarks",
32+
"align_elements_using_landmarks",
2933
]

src/spatialdata/transformations/operations.py

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
from typing import TYPE_CHECKING, Optional, Union
44

55
import networkx as nx
6+
import numpy
67
import numpy as np
8+
from dask.dataframe import DataFrame as DaskDataFrame
9+
from geopandas import GeoDataFrame
10+
from skimage.transform import estimate_transform
711

812
from spatialdata.transformations._utils import (
913
_get_transformations,
@@ -13,7 +17,7 @@
1317
if TYPE_CHECKING:
1418
from spatialdata import SpatialData
1519
from spatialdata.models import SpatialElement
16-
from spatialdata.transformations import BaseTransformation
20+
from spatialdata.transformations import Affine, BaseTransformation
1721

1822

1923
def set_transformation(
@@ -291,3 +295,158 @@ def _describe_paths(paths: list[list[Union[int, str]]]) -> str:
291295
transformations.append(g[path[i]][path[i + 1]]["transformation"])
292296
sequence = Sequence(transformations)
293297
return sequence
298+
299+
300+
def get_transformation_between_landmarks(
301+
references_coords: Union[GeoDataFrame, DaskDataFrame],
302+
moving_coords: Union[GeoDataFrame, DaskDataFrame],
303+
) -> Affine:
304+
"""
305+
Get a similarity transformation between two lists of (n >= 3) landmarks. Landmarks are assumed to be in the same space.
306+
307+
Parameters
308+
----------
309+
references_coords
310+
landmarks annotating the reference element. Must be a valid element describing points or circles.
311+
moving_coords
312+
landmarks annotating the moving element. Must be a valid element describing points or circles.
313+
314+
Returns
315+
-------
316+
The Affine transformation that maps the moving element to the reference element.
317+
318+
Examples
319+
--------
320+
If you save the landmark points using napari_spatialdata, they will be alredy saved as circles. Here is an
321+
example on how to call this function on two sets of numpy arrays describing x, y coordinates.
322+
>>> import numpy as np
323+
>>> from spatialdata.models import PointsModel
324+
>>> from spatialdata.transform import get_transformation_between_landmarks
325+
>>> points_moving = np.array([[0, 0], [1, 1], [2, 2]])
326+
>>> points_reference = np.array([[0, 0], [10, 10], [20, 20]])
327+
>>> moving_coords = PointsModel(points_moving)
328+
>>> references_coords = PointsModel(points_reference)
329+
>>> transformation = get_transformation_between_landmarks(references_coords, moving_coords)
330+
"""
331+
from spatialdata import transform
332+
from spatialdata.models import get_axis_names
333+
from spatialdata.transformations.transformations import (
334+
Affine,
335+
BaseTransformation,
336+
Sequence,
337+
)
338+
339+
assert get_axis_names(references_coords) == ("x", "y")
340+
assert get_axis_names(moving_coords) == ("x", "y")
341+
342+
if isinstance(references_coords, GeoDataFrame):
343+
references_xy = np.stack([references_coords.geometry.x, references_coords.geometry.y], axis=1)
344+
moving_xy = np.stack([moving_coords.geometry.x, moving_coords.geometry.y], axis=1)
345+
elif isinstance(references_coords, DaskDataFrame):
346+
references_xy = references_coords[["x", "y"]].to_dask_array().compute()
347+
moving_xy = moving_coords[["x", "y"]].to_dask_array().compute()
348+
else:
349+
raise TypeError("references_coords must be either an GeoDataFrame or a DaskDataFrame")
350+
351+
model = estimate_transform("affine", src=moving_xy, dst=references_xy)
352+
transform_matrix = model.params
353+
a = transform_matrix[:2, :2]
354+
d = np.linalg.det(a)
355+
final: BaseTransformation
356+
if d < 0:
357+
m = (moving_xy[:, 0].max() - moving_xy[:, 0].min()) / 2
358+
flip = Affine(
359+
np.array(
360+
[
361+
[-1, 0, 2 * m],
362+
[0, 1, 0],
363+
[0, 0, 1],
364+
]
365+
),
366+
input_axes=("x", "y"),
367+
output_axes=("x", "y"),
368+
)
369+
flipped_moving = transform(moving_coords, flip, maintain_positioning=False)
370+
if isinstance(flipped_moving, GeoDataFrame):
371+
flipped_moving_xy = np.stack([flipped_moving.geometry.x, flipped_moving.geometry.y], axis=1)
372+
elif isinstance(flipped_moving, DaskDataFrame):
373+
flipped_moving_xy = flipped_moving[["x", "y"]].to_dask_array().compute()
374+
else:
375+
raise TypeError("flipped_moving must be either an GeoDataFrame or a DaskDataFrame")
376+
model = estimate_transform("similarity", src=flipped_moving_xy, dst=references_xy)
377+
final = Sequence([flip, Affine(model.params, input_axes=("x", "y"), output_axes=("x", "y"))])
378+
else:
379+
model = estimate_transform("similarity", src=moving_xy, dst=references_xy)
380+
final = Affine(model.params, input_axes=("x", "y"), output_axes=("x", "y"))
381+
382+
affine = Affine(
383+
final.to_affine_matrix(input_axes=("x", "y"), output_axes=("x", "y")),
384+
input_axes=("x", "y"),
385+
output_axes=("x", "y"),
386+
)
387+
return affine
388+
389+
390+
def align_elements_using_landmarks(
391+
references_coords: Union[GeoDataFrame | DaskDataFrame],
392+
moving_coords: Union[GeoDataFrame | DaskDataFrame],
393+
reference_element: SpatialElement,
394+
moving_element: SpatialElement,
395+
reference_coordinate_system: str = "global",
396+
moving_coordinate_system: str = "global",
397+
new_coordinate_system: Optional[str] = None,
398+
write_to_sdata: Optional[SpatialData] = None,
399+
) -> BaseTransformation:
400+
"""
401+
Maps a moving object into a reference object using two lists of (n >= 3) landmarks; returns the transformations that enable this
402+
mapping and optinally saves them, to map to a new shared coordinate system.
403+
404+
Parameters
405+
----------
406+
references_coords
407+
landmarks annotating the reference element. Must be a valid element describing points or circles.
408+
moving_coords
409+
landmarks annotating the moving element. Must be a valid element describing points or circles.
410+
reference_element
411+
the reference element.
412+
moving_element
413+
the moving element.
414+
reference_coordinate_system
415+
the coordinate system of the reference element that have been used to annotate the landmarks.
416+
moving_coordinate_system
417+
the coordinate system of the moving element that have been used to annotate the landmarks.
418+
new_coordinate_system
419+
If provided, both elements will be mapped to this new coordinate system with the new transformations just
420+
computed.
421+
write_to_sdata
422+
If provided, the transformations will be saved to disk in the specified SpatialData object. The SpatialData
423+
object must be backed and must contain both the reference and moving elements.
424+
425+
Returns
426+
-------
427+
A similarity transformation that maps the moving element to the same coordinate of reference element in the
428+
coordinate system specified by reference_coordinate_system.
429+
"""
430+
from spatialdata.transformations.transformations import BaseTransformation, Sequence
431+
432+
affine = get_transformation_between_landmarks(references_coords, moving_coords)
433+
434+
# get the old transformations of the visium and xenium data
435+
old_moving_transformation = get_transformation(moving_element, moving_coordinate_system)
436+
old_reference_transformation = get_transformation(reference_element, reference_coordinate_system)
437+
assert isinstance(old_moving_transformation, BaseTransformation)
438+
assert isinstance(old_reference_transformation, BaseTransformation)
439+
440+
# compute the new transformations
441+
new_moving_transformation = Sequence([old_moving_transformation, affine])
442+
new_reference_transformation = old_reference_transformation
443+
444+
if new_coordinate_system is not None:
445+
# this allows to work on singleton objects, not embedded in a SpatialData object
446+
set_transformation(
447+
moving_element, new_moving_transformation, new_coordinate_system, write_to_sdata=write_to_sdata
448+
)
449+
set_transformation(
450+
reference_element, new_reference_transformation, new_coordinate_system, write_to_sdata=write_to_sdata
451+
)
452+
return new_moving_transformation

0 commit comments

Comments
 (0)