transformnd 0.5.0__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {transformnd-0.5.0 → transformnd-0.7.0}/PKG-INFO +6 -7
- {transformnd-0.5.0 → transformnd-0.7.0}/README.md +5 -6
- {transformnd-0.5.0 → transformnd-0.7.0}/pyproject.toml +1 -1
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/__init__.py +6 -1
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/pandas.py +7 -4
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/polars.py +7 -4
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/shapely.py +10 -6
- transformnd-0.7.0/src/transformnd/transforms/__init__.py +31 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/affine.py +2 -2
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/by_dimension.py +22 -1
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/moving_least_squares.py +5 -2
- transformnd-0.7.0/src/transformnd/transforms/project_axis.py +173 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/thinplate.py +9 -4
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/vector_field.py +8 -2
- transformnd-0.5.0/src/transformnd/transforms/__init__.py +0 -19
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/__init__.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/base.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/adapters/bounding_box.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/base.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/constants.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/extents/__init__.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/extents/base.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/extents/bounding_box.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/graph.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/py.typed +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/bijection.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/map_axis.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/reflection.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/transforms/simple.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/types.py +0 -0
- {transformnd-0.5.0 → transformnd-0.7.0}/src/transformnd/util.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: transformnd
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: ND coordinate transformations
|
|
5
5
|
Author: Chris Barnes
|
|
6
6
|
Author-email: Chris Barnes <chris.barnes@gerbi-gmb.de>
|
|
@@ -99,10 +99,10 @@ All transforms are accessed under the `transformnd.transforms` subpackage.
|
|
|
99
99
|
| `MapAxis` | | Rearrange axes of the input coordinates |
|
|
100
100
|
| `Affine` | | Multiply augmented coordinates by an affine transformation matrix. Can represent all of the above transformations. Can be composed with matrix multiplication `aff2 @ aff1`. |
|
|
101
101
|
| `ByDimension` | | Apply different transformations to subsets of the input coordinates' dimensions |
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
102
|
+
| `MovingLeastSquares` | `movingleastsquares` | Landmark-based transformation. |
|
|
103
|
+
| `ThinPlateSplines` | `thinplatesplines` | Landmark-based transformation. |
|
|
104
|
+
| `Coordinates` | `vectorfield` for in-memory, `vectorfield-dask` for chunked | Look up output coordinates in a vector field indexed by the input coordinates |
|
|
105
|
+
| `Displacements` | `vectorfield`, `vectorfield-dask` for chunked | Look up translations in a vector field indexed by the input coordinates, and add them to input coordinates |
|
|
106
106
|
|
|
107
107
|
Arbitrary transforms can be composed into a `TransformSequence` with `transform1 | transform2`.
|
|
108
108
|
A graph of transforms between defined spaces can be traversed using the `TransformGraph`.
|
|
@@ -110,13 +110,12 @@ A graph of transforms between defined spaces can be traversed using the `Transfo
|
|
|
110
110
|
## Implemented adapters
|
|
111
111
|
|
|
112
112
|
- Numpy arrays of shape `(..., D, ...)` (`transformnd.adapters.ReshapeAdapter`)
|
|
113
|
-
- `meshio.Mesh` (`transformnd.adapters.meshio.MeshAdapter`)
|
|
114
113
|
- `pandas.DataFrame` (`transformnd.adapters.pandas.PandasAdapter`)
|
|
115
114
|
- Takes a subset of columns as a coordinate array
|
|
116
115
|
- `polars.DataFrame` (`transformnd.adapters.polars.PolarsAdapter`)
|
|
117
116
|
- Similar to the pandas adapter
|
|
118
117
|
- Currently, only scalar columns are supported (e.g. not a single struct column with fields `x`, `y`, `z`)
|
|
119
|
-
- Geometries from `shapely` (`transformnd.adapters.shapely.
|
|
118
|
+
- Geometries from `shapely` (`transformnd.adapters.shapely.ShapelyAdapter`)
|
|
120
119
|
- Objects composed of transformable attributes (`transformnd.adapters.AttrAdapter`).
|
|
121
120
|
|
|
122
121
|
## Additional transforms and adapters
|
|
@@ -44,10 +44,10 @@ All transforms are accessed under the `transformnd.transforms` subpackage.
|
|
|
44
44
|
| `MapAxis` | | Rearrange axes of the input coordinates |
|
|
45
45
|
| `Affine` | | Multiply augmented coordinates by an affine transformation matrix. Can represent all of the above transformations. Can be composed with matrix multiplication `aff2 @ aff1`. |
|
|
46
46
|
| `ByDimension` | | Apply different transformations to subsets of the input coordinates' dimensions |
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
47
|
+
| `MovingLeastSquares` | `movingleastsquares` | Landmark-based transformation. |
|
|
48
|
+
| `ThinPlateSplines` | `thinplatesplines` | Landmark-based transformation. |
|
|
49
|
+
| `Coordinates` | `vectorfield` for in-memory, `vectorfield-dask` for chunked | Look up output coordinates in a vector field indexed by the input coordinates |
|
|
50
|
+
| `Displacements` | `vectorfield`, `vectorfield-dask` for chunked | Look up translations in a vector field indexed by the input coordinates, and add them to input coordinates |
|
|
51
51
|
|
|
52
52
|
Arbitrary transforms can be composed into a `TransformSequence` with `transform1 | transform2`.
|
|
53
53
|
A graph of transforms between defined spaces can be traversed using the `TransformGraph`.
|
|
@@ -55,13 +55,12 @@ A graph of transforms between defined spaces can be traversed using the `Transfo
|
|
|
55
55
|
## Implemented adapters
|
|
56
56
|
|
|
57
57
|
- Numpy arrays of shape `(..., D, ...)` (`transformnd.adapters.ReshapeAdapter`)
|
|
58
|
-
- `meshio.Mesh` (`transformnd.adapters.meshio.MeshAdapter`)
|
|
59
58
|
- `pandas.DataFrame` (`transformnd.adapters.pandas.PandasAdapter`)
|
|
60
59
|
- Takes a subset of columns as a coordinate array
|
|
61
60
|
- `polars.DataFrame` (`transformnd.adapters.polars.PolarsAdapter`)
|
|
62
61
|
- Similar to the pandas adapter
|
|
63
62
|
- Currently, only scalar columns are supported (e.g. not a single struct column with fields `x`, `y`, `z`)
|
|
64
|
-
- Geometries from `shapely` (`transformnd.adapters.shapely.
|
|
63
|
+
- Geometries from `shapely` (`transformnd.adapters.shapely.ShapelyAdapter`)
|
|
65
64
|
- Objects composed of transformable attributes (`transformnd.adapters.AttrAdapter`).
|
|
66
65
|
|
|
67
66
|
## Additional transforms and adapters
|
|
@@ -12,7 +12,6 @@ Implement your own adapter by inheriting from `BaseAdapter`.
|
|
|
12
12
|
|
|
13
13
|
See `.pandas.DataFrameAdapter` for an example of creating an adapter
|
|
14
14
|
for an external type.
|
|
15
|
-
|
|
16
15
|
"""
|
|
17
16
|
|
|
18
17
|
from .base import (
|
|
@@ -23,6 +22,9 @@ from .base import (
|
|
|
23
22
|
ReshapeAdapter,
|
|
24
23
|
SimpleAdapter,
|
|
25
24
|
)
|
|
25
|
+
from .pandas import PandasAdapter
|
|
26
|
+
from .polars import PolarsAdapter
|
|
27
|
+
from .shapely import ShapelyAdapter
|
|
26
28
|
|
|
27
29
|
__all__ = [
|
|
28
30
|
"BaseAdapter",
|
|
@@ -31,4 +33,7 @@ __all__ = [
|
|
|
31
33
|
"FnAdapter",
|
|
32
34
|
"AttrAdapter",
|
|
33
35
|
"ReshapeAdapter",
|
|
36
|
+
"PandasAdapter",
|
|
37
|
+
"PolarsAdapter",
|
|
38
|
+
"ShapelyAdapter",
|
|
34
39
|
]
|
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
"""Adapt pandas DataFrames for transformation."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Hashable
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
|
-
import pandas as pd
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
8
8
|
from ..base import Transform
|
|
9
9
|
from .base import BaseAdapter
|
|
10
10
|
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
import pandas as pd
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
|
|
15
|
+
class PandasAdapter(BaseAdapter["pd.DataFrame", np.ndarray]):
|
|
13
16
|
def __init__(self, columns: list[Hashable]):
|
|
14
17
|
"""Adapt transformation for coordinates stored in a pandas DataFrame.
|
|
15
18
|
|
|
@@ -21,8 +24,8 @@ class PandasAdapter(BaseAdapter[pd.DataFrame, np.ndarray]):
|
|
|
21
24
|
self.columns = columns
|
|
22
25
|
|
|
23
26
|
def apply(
|
|
24
|
-
self, transform: Transform, df: pd.DataFrame, in_place: bool = False
|
|
25
|
-
) -> pd.DataFrame:
|
|
27
|
+
self, transform: Transform, df: "pd.DataFrame", in_place: bool = False
|
|
28
|
+
) -> "pd.DataFrame":
|
|
26
29
|
"""Transform the dataframe, optionally in-place.
|
|
27
30
|
|
|
28
31
|
Parameters
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"""Adapt polars DataFrames for transformation."""
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
4
|
import numpy as np
|
|
5
5
|
|
|
6
6
|
from ..base import Transform
|
|
7
7
|
from .base import BaseAdapter
|
|
8
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
import polars as pl
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
|
|
13
|
+
class PolarsAdapter(BaseAdapter["pl.DataFrame", np.ndarray]):
|
|
11
14
|
def __init__(self, columns: list[str]):
|
|
12
15
|
"""Adapt transformation for coordinates stored in a polars DataFrame.
|
|
13
16
|
|
|
@@ -19,8 +22,8 @@ class PolarsAdapter(BaseAdapter[pl.DataFrame, np.ndarray]):
|
|
|
19
22
|
self.columns = columns
|
|
20
23
|
|
|
21
24
|
def apply(
|
|
22
|
-
self, transform: Transform, df: pl.DataFrame, in_place: bool = False
|
|
23
|
-
) -> pl.DataFrame:
|
|
25
|
+
self, transform: Transform, df: "pl.DataFrame", in_place: bool = False
|
|
26
|
+
) -> "pl.DataFrame":
|
|
24
27
|
"""Transform the dataframe, optionally in-place.
|
|
25
28
|
|
|
26
29
|
Parameters
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
2
3
|
|
|
3
4
|
import numpy as np
|
|
4
|
-
import shapely
|
|
5
|
-
from shapely.geometry.base import BaseGeometry
|
|
6
|
-
from shapely.coords import CoordinateSequence
|
|
7
5
|
|
|
8
6
|
from ..base import Transform, ArrayT
|
|
9
7
|
from .base import BaseAdapter
|
|
10
8
|
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from shapely.geometry.base import BaseGeometry
|
|
11
|
+
from shapely.coords import CoordinateSequence
|
|
12
|
+
|
|
13
|
+
|
|
11
14
|
logger = logging.getLogger(__name__)
|
|
12
15
|
|
|
13
16
|
|
|
14
|
-
def as_numpy(coords: CoordinateSequence) -> np.ndarray:
|
|
17
|
+
def as_numpy(coords: "CoordinateSequence") -> np.ndarray:
|
|
15
18
|
return np.asarray(coords)
|
|
16
19
|
|
|
17
20
|
|
|
18
|
-
class
|
|
21
|
+
class ShapelyAdapter(BaseAdapter["BaseGeometry", ArrayT]):
|
|
19
22
|
"""Transform shapely geometries.
|
|
20
23
|
|
|
21
24
|
As well as the generic `apply()`,
|
|
@@ -27,7 +30,7 @@ class GeometryAdapter(BaseAdapter[BaseGeometry, ArrayT]):
|
|
|
27
30
|
N.B. shapely geometries' coordinates are in `XY(Z)` order
|
|
28
31
|
"""
|
|
29
32
|
|
|
30
|
-
def apply[T: BaseGeometry](
|
|
33
|
+
def apply[T: "BaseGeometry"](
|
|
31
34
|
self,
|
|
32
35
|
transform: Transform,
|
|
33
36
|
obj: T,
|
|
@@ -51,6 +54,7 @@ class GeometryAdapter(BaseAdapter[BaseGeometry, ArrayT]):
|
|
|
51
54
|
T
|
|
52
55
|
An object of the same type as the input.
|
|
53
56
|
"""
|
|
57
|
+
import shapely
|
|
54
58
|
|
|
55
59
|
def fn(coords: np.ndarray) -> np.ndarray:
|
|
56
60
|
c = coords.copy()
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Implementations of some common transforms."""
|
|
2
|
+
|
|
3
|
+
from .affine import Affine
|
|
4
|
+
from .reflection import Reflect
|
|
5
|
+
from .simple import Identity, Scale, Translate
|
|
6
|
+
from .map_axis import MapAxis
|
|
7
|
+
from .bijection import Bijection
|
|
8
|
+
from .project_axis import ProjectAxis, Insert, Remove
|
|
9
|
+
from .by_dimension import ByDimension, SubTransform
|
|
10
|
+
from .vector_field import Coordinates, Displacements
|
|
11
|
+
from .moving_least_squares import MovingLeastSquares
|
|
12
|
+
from .thinplate import ThinPlateSplines
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"Affine",
|
|
16
|
+
"Identity",
|
|
17
|
+
"ProjectAxis",
|
|
18
|
+
"Insert",
|
|
19
|
+
"Remove",
|
|
20
|
+
"Reflect",
|
|
21
|
+
"Scale",
|
|
22
|
+
"Translate",
|
|
23
|
+
"MapAxis",
|
|
24
|
+
"Bijection",
|
|
25
|
+
"ByDimension",
|
|
26
|
+
"SubTransform",
|
|
27
|
+
"Coordinates",
|
|
28
|
+
"Displacements",
|
|
29
|
+
"MovingLeastSquares",
|
|
30
|
+
"ThinPlateSplines",
|
|
31
|
+
]
|
|
@@ -42,7 +42,7 @@ class Affine(Transform[ArrayT]):
|
|
|
42
42
|
----------
|
|
43
43
|
matrix
|
|
44
44
|
Affine transformation matrix,
|
|
45
|
-
i.e. a 2D array-like with shape `(
|
|
45
|
+
i.e. a 2D array-like with shape `(Do + 1, Di + 1)`,
|
|
46
46
|
where the bottom row is all 0s except in the rightmost column, which is 1.
|
|
47
47
|
spaces
|
|
48
48
|
Optional source and target spaces
|
|
@@ -64,7 +64,7 @@ class Affine(Transform[ArrayT]):
|
|
|
64
64
|
f"Transformation matrix is not affine (expected bottom row {expected}, got {bottom_row})."
|
|
65
65
|
)
|
|
66
66
|
|
|
67
|
-
super().__init__(NDims(m.shape[
|
|
67
|
+
super().__init__(NDims(m.shape[1] - 1, m.shape[0] - 1), spaces=spaces)
|
|
68
68
|
|
|
69
69
|
self.matrix: np.ndarray = m
|
|
70
70
|
|
|
@@ -8,7 +8,10 @@ from ..types import NDims, Spaces
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class SubTransform[ArrayT]:
|
|
11
|
-
"""
|
|
11
|
+
"""Component of the `ByDimension` transformation.
|
|
12
|
+
|
|
13
|
+
Transformation to apply to subsets of the input dimensions and which output dimensions they calculate.
|
|
14
|
+
"""
|
|
12
15
|
|
|
13
16
|
def __init__(
|
|
14
17
|
self,
|
|
@@ -16,6 +19,24 @@ class SubTransform[ArrayT]:
|
|
|
16
19
|
input_axes: list[int],
|
|
17
20
|
output_axes: list[int] | None = None,
|
|
18
21
|
):
|
|
22
|
+
"""
|
|
23
|
+
Parameters
|
|
24
|
+
----------
|
|
25
|
+
transform
|
|
26
|
+
Transformation to apply to the subset of axes.
|
|
27
|
+
input_axes
|
|
28
|
+
Which axes to apply the transformation to, in order.
|
|
29
|
+
The length must match the input dimensionality of `transform`.
|
|
30
|
+
output_axes
|
|
31
|
+
Which axes to apply the transformation to, in order.
|
|
32
|
+
The length must match the input dimensionality of `transform`.
|
|
33
|
+
If None, re-use the input axes.
|
|
34
|
+
|
|
35
|
+
Raises
|
|
36
|
+
------
|
|
37
|
+
ValueError
|
|
38
|
+
`transform`'s dimensionality does not match the input/output axes.
|
|
39
|
+
"""
|
|
19
40
|
|
|
20
41
|
self.input_axes = input_axes
|
|
21
42
|
if output_axes is None:
|
|
@@ -7,7 +7,6 @@ Requires the `movingleastsquares` extra.
|
|
|
7
7
|
from array_api_compat import array_namespace
|
|
8
8
|
import numpy as np
|
|
9
9
|
from typing import Self
|
|
10
|
-
from molesq.transform import Transformer as _Transformer
|
|
11
10
|
|
|
12
11
|
from ..base import Transform
|
|
13
12
|
from ..types import NDims, Spaces
|
|
@@ -18,6 +17,8 @@ class MovingLeastSquares(Transform[np.ndarray]):
|
|
|
18
17
|
"""Moving least squares transformation.
|
|
19
18
|
|
|
20
19
|
Deform based on a matched pairs of source and target control points; see <https://dl.acm.org/doi/10.1145/1141911.1141920>
|
|
20
|
+
|
|
21
|
+
REQUIRES: `movingleastsquares` extra.
|
|
21
22
|
"""
|
|
22
23
|
|
|
23
24
|
def __init__(
|
|
@@ -39,9 +40,11 @@ class MovingLeastSquares(Transform[np.ndarray]):
|
|
|
39
40
|
spaces
|
|
40
41
|
Optional source and target spaces
|
|
41
42
|
"""
|
|
43
|
+
from molesq.transform import Transformer
|
|
44
|
+
|
|
42
45
|
s = as_floats(source_control_points)
|
|
43
46
|
t = as_floats(target_control_points)
|
|
44
|
-
self._transformer =
|
|
47
|
+
self._transformer = Transformer(s, t)
|
|
45
48
|
super().__init__(
|
|
46
49
|
NDims(
|
|
47
50
|
s.shape[1],
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from copy import copy
|
|
4
|
+
from typing import Self, Sequence
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
from array_api_compat import array_namespace
|
|
8
|
+
from transformnd.transforms import Affine
|
|
9
|
+
from transformnd.types import NDims, Spaces
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from ..base import Transform
|
|
12
|
+
from ..types import ArrayT
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True, eq=True)
|
|
16
|
+
class BaseOperation(ABC):
|
|
17
|
+
idx: int
|
|
18
|
+
"""Which axis to apply the operation to."""
|
|
19
|
+
|
|
20
|
+
def __post_init__(self):
|
|
21
|
+
if self.idx < 0:
|
|
22
|
+
raise ValueError("insert/remove idx must be positive")
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def check(self, ndim: int) -> int: ...
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def invert(self) -> BaseOperation: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, eq=True)
|
|
32
|
+
class Insert(BaseOperation):
|
|
33
|
+
"""Component of the `ProjectAxis` transform which inserts a new axis."""
|
|
34
|
+
|
|
35
|
+
def check(self, ndim: int) -> int:
|
|
36
|
+
if self.idx > ndim or self.idx <= -ndim:
|
|
37
|
+
raise ValueError(
|
|
38
|
+
f"Index {self.idx} is out of range for dimensionality {ndim}"
|
|
39
|
+
)
|
|
40
|
+
return ndim + 1
|
|
41
|
+
|
|
42
|
+
def invert(self) -> Remove:
|
|
43
|
+
return Remove(self.idx)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, eq=True)
|
|
47
|
+
class Remove(BaseOperation):
|
|
48
|
+
"""Component of the `ProjectAxis` transform which removes an existing axis."""
|
|
49
|
+
|
|
50
|
+
def check(self, ndim: int) -> int:
|
|
51
|
+
if self.idx >= ndim or self.idx <= -ndim:
|
|
52
|
+
raise ValueError(
|
|
53
|
+
f"Index {self.idx} is out of range for dimensionality {ndim}"
|
|
54
|
+
)
|
|
55
|
+
return ndim - 1
|
|
56
|
+
|
|
57
|
+
def invert(self) -> Insert:
|
|
58
|
+
if self.idx == -1:
|
|
59
|
+
raise ValueError("Removal of the -1th axis is not invertible")
|
|
60
|
+
return Insert(self.idx)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
Operation = Insert | Remove
|
|
64
|
+
"""Insert or remove an axis."""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ProjectAxis(Transform):
|
|
68
|
+
"""Transform for adding and removing axes.
|
|
69
|
+
|
|
70
|
+
WARNING: inverting this transformation may be lossy.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
operations: Sequence[Operation],
|
|
76
|
+
source_ndim: int | None = None,
|
|
77
|
+
target_ndim: int | None = None,
|
|
78
|
+
*,
|
|
79
|
+
spaces: Spaces = Spaces(None, None),
|
|
80
|
+
):
|
|
81
|
+
"""Create a transform for adding and dropping axes.
|
|
82
|
+
|
|
83
|
+
At least one of source_ndim and target_ndim must be given.
|
|
84
|
+
|
|
85
|
+
Parameters
|
|
86
|
+
----------
|
|
87
|
+
operations
|
|
88
|
+
Sequence of operations to apply.
|
|
89
|
+
source_ndim
|
|
90
|
+
If omitted, can be inferred from `target_ndim`.
|
|
91
|
+
target_ndim
|
|
92
|
+
If omitted, can be inferred from `source_ndim`.
|
|
93
|
+
spaces
|
|
94
|
+
Identifiers for source and target spaces, by default Spaces(None, None)
|
|
95
|
+
|
|
96
|
+
Raises
|
|
97
|
+
------
|
|
98
|
+
ValueError
|
|
99
|
+
Operations are inconsistent with given dimensionality,
|
|
100
|
+
or insufficient dimensionality information was given.
|
|
101
|
+
"""
|
|
102
|
+
self.operations = []
|
|
103
|
+
self._has_inserts = False
|
|
104
|
+
|
|
105
|
+
if source_ndim is not None:
|
|
106
|
+
nd = source_ndim
|
|
107
|
+
for op in operations:
|
|
108
|
+
nd = op.check(nd)
|
|
109
|
+
if target_ndim is None:
|
|
110
|
+
target_ndim = nd
|
|
111
|
+
elif target_ndim != nd:
|
|
112
|
+
raise ValueError("Operations do not match expected target ndim")
|
|
113
|
+
|
|
114
|
+
elif target_ndim is not None:
|
|
115
|
+
nd = target_ndim
|
|
116
|
+
for op in reversed(operations):
|
|
117
|
+
nd = op.invert().check(nd)
|
|
118
|
+
if source_ndim is None:
|
|
119
|
+
source_ndim = nd
|
|
120
|
+
elif source_ndim != nd:
|
|
121
|
+
raise ValueError("Operations do not match expected source ndim")
|
|
122
|
+
|
|
123
|
+
else:
|
|
124
|
+
raise ValueError("At least one of source_ndim or target_ndim must be given")
|
|
125
|
+
|
|
126
|
+
idxs: list[int | None] = list(range(source_ndim))
|
|
127
|
+
for op in operations:
|
|
128
|
+
if isinstance(op, Insert):
|
|
129
|
+
self._has_inserts = True
|
|
130
|
+
idxs.insert(op.idx, None)
|
|
131
|
+
elif isinstance(op, Remove):
|
|
132
|
+
idxs.pop(op.idx)
|
|
133
|
+
self.operations.append(op)
|
|
134
|
+
self._idxs = idxs
|
|
135
|
+
|
|
136
|
+
super().__init__(NDims(source_ndim, target_ndim), spaces=spaces)
|
|
137
|
+
|
|
138
|
+
def apply(self, coords: ArrayT) -> ArrayT:
|
|
139
|
+
coords = self._validate_coords(coords)
|
|
140
|
+
if self._has_inserts:
|
|
141
|
+
xp = array_namespace(coords)
|
|
142
|
+
out = xp.zeros_like(coords, shape=(xp.shape(coords)[0], self.ndims.target))
|
|
143
|
+
for idx, orig_idx in enumerate(self._idxs):
|
|
144
|
+
if orig_idx is not None:
|
|
145
|
+
out[:, idx] = coords[:, orig_idx] # type:ignore
|
|
146
|
+
|
|
147
|
+
else:
|
|
148
|
+
out = coords[:, self._idxs] # type:ignore
|
|
149
|
+
return out
|
|
150
|
+
|
|
151
|
+
def is_identity(self) -> bool:
|
|
152
|
+
orig: list[int | None] = list(range(self.ndims.source))
|
|
153
|
+
dims = copy(orig)
|
|
154
|
+
for op in self.operations:
|
|
155
|
+
if isinstance(op, Insert):
|
|
156
|
+
dims.insert(op.idx, None)
|
|
157
|
+
elif isinstance(op, Remove):
|
|
158
|
+
dims.pop(op.idx)
|
|
159
|
+
|
|
160
|
+
return dims == orig
|
|
161
|
+
|
|
162
|
+
def to_affine(self) -> Affine | None:
|
|
163
|
+
m = np.eye(self.ndims.source)
|
|
164
|
+
out_m = self.apply(m)
|
|
165
|
+
return Affine.from_linear_map(out_m.T)
|
|
166
|
+
|
|
167
|
+
def invert(self) -> Self | None:
|
|
168
|
+
return type(self)(
|
|
169
|
+
[op.invert() for op in reversed(self.operations)],
|
|
170
|
+
source_ndim=self.ndims.target,
|
|
171
|
+
target_ndim=self.ndims.source,
|
|
172
|
+
spaces=self.spaces.invert(),
|
|
173
|
+
)
|
|
@@ -6,7 +6,6 @@ Requires the `thinplatesplines` extra.
|
|
|
6
6
|
|
|
7
7
|
import logging
|
|
8
8
|
|
|
9
|
-
import morphops as mops
|
|
10
9
|
import numpy as np
|
|
11
10
|
|
|
12
11
|
from ..base import Transform
|
|
@@ -20,6 +19,8 @@ class ThinPlateSplines(Transform[np.ndarray]):
|
|
|
20
19
|
"""Thin plate splines transforms.
|
|
21
20
|
|
|
22
21
|
Deform based on matched pairs of control points.
|
|
22
|
+
|
|
23
|
+
REQUIRES: `thinplatesplines` extra.
|
|
23
24
|
"""
|
|
24
25
|
|
|
25
26
|
def __init__(
|
|
@@ -48,6 +49,8 @@ class ThinPlateSplines(Transform[np.ndarray]):
|
|
|
48
49
|
ValueError
|
|
49
50
|
Invalid control points.
|
|
50
51
|
"""
|
|
52
|
+
import morphops
|
|
53
|
+
|
|
51
54
|
self.source_control_points = as_floats(source_control_points)
|
|
52
55
|
self.target_control_points = as_floats(target_control_points)
|
|
53
56
|
|
|
@@ -59,7 +62,7 @@ class ThinPlateSplines(Transform[np.ndarray]):
|
|
|
59
62
|
|
|
60
63
|
ndim = self.source_control_points.shape[1]
|
|
61
64
|
|
|
62
|
-
self.W, self.A =
|
|
65
|
+
self.W, self.A = morphops.tps_coefs(
|
|
63
66
|
self.source_control_points,
|
|
64
67
|
self.target_control_points,
|
|
65
68
|
)
|
|
@@ -73,8 +76,10 @@ class ThinPlateSplines(Transform[np.ndarray]):
|
|
|
73
76
|
)
|
|
74
77
|
|
|
75
78
|
def apply(self, coords: np.ndarray) -> np.ndarray:
|
|
79
|
+
import morphops
|
|
80
|
+
|
|
76
81
|
coords = self._validate_coords(coords)
|
|
77
|
-
U =
|
|
78
|
-
P =
|
|
82
|
+
U = morphops.K_matrix(coords, self.source_control_points)
|
|
83
|
+
P = morphops.P_matrix(coords)
|
|
79
84
|
# The warped pts are the affine part + the non-uniform part
|
|
80
85
|
return P @ self.A + U @ self.W
|
|
@@ -10,8 +10,6 @@ from ..types import NDims, Spaces
|
|
|
10
10
|
from ..base import Transform, ArrayT
|
|
11
11
|
from ..util import set_scipy_array_api, as_floats
|
|
12
12
|
|
|
13
|
-
set_scipy_array_api()
|
|
14
|
-
|
|
15
13
|
__all__ = ["Coordinates", "Displacements"]
|
|
16
14
|
|
|
17
15
|
|
|
@@ -95,6 +93,8 @@ class BaseVectorField(Transform[ArrayT], ABC):
|
|
|
95
93
|
|
|
96
94
|
set_scipy_array_api()
|
|
97
95
|
xp = array_namespace(index_coords_t)
|
|
96
|
+
|
|
97
|
+
# make columnar output array so that each dimension can be written contiguously
|
|
98
98
|
out = xp.zeros_like(
|
|
99
99
|
self.vector_field, shape=(self.ndims.target, xp.shape(index_coords_t)[1])
|
|
100
100
|
)
|
|
@@ -133,6 +133,9 @@ class Coordinates(BaseVectorField[ArrayT]):
|
|
|
133
133
|
the output coordinate is `vector_field[a, b, c, :].
|
|
134
134
|
|
|
135
135
|
Input coordinates outside the vector field return NaN.
|
|
136
|
+
|
|
137
|
+
REQUIRES: `vectorfield` extra for in-memory,
|
|
138
|
+
or `vectorfield-dask` extra for lazy chunked vector fields.
|
|
136
139
|
"""
|
|
137
140
|
|
|
138
141
|
def __init__(
|
|
@@ -182,6 +185,9 @@ class Displacements(BaseVectorField[ArrayT]):
|
|
|
182
185
|
the output coordinate is `(a, b, c) + vector_field[a, b, c, :].
|
|
183
186
|
|
|
184
187
|
Input coordinates outside the vector field return NaN.
|
|
188
|
+
|
|
189
|
+
REQUIRES: `vectorfield` extra for in-memory,
|
|
190
|
+
or `vectorfield-dask` extra for lazy chunked vector fields.
|
|
185
191
|
"""
|
|
186
192
|
|
|
187
193
|
def __init__(
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
"""Implementations of some common transforms."""
|
|
2
|
-
|
|
3
|
-
from .affine import Affine
|
|
4
|
-
from .reflection import Reflect
|
|
5
|
-
from .simple import Identity, Scale, Translate
|
|
6
|
-
from .map_axis import MapAxis
|
|
7
|
-
from .bijection import Bijection
|
|
8
|
-
from .by_dimension import ByDimension
|
|
9
|
-
|
|
10
|
-
__all__ = [
|
|
11
|
-
"Affine",
|
|
12
|
-
"Identity",
|
|
13
|
-
"Reflect",
|
|
14
|
-
"Scale",
|
|
15
|
-
"Translate",
|
|
16
|
-
"MapAxis",
|
|
17
|
-
"Bijection",
|
|
18
|
-
"ByDimension",
|
|
19
|
-
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|