Skip to content

align

Source code in navis/transforms/moving_least_squares.py
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
class MovingLeastSquaresTransform(BaseTransform):
    def __init__(
        self,
        landmarks_source: np.ndarray,
        landmarks_target: np.ndarray,
        direction: str = 'forward',
    ) -> None:
        """Moving Least Squares transforms of 3D spatial data.

        Uses the [molesq](https://github.com/clbarnes/molesq) library, which packages the
        [implementation](https://github.com/ceesem/catalysis/blob/master/catalysis/transform.py)
        by Casey Schneider-Mizell of the affine algorithm published in
        [Schaefer et al. 2006](https://dl.acm.org/doi/pdf/10.1145/1179352.1141920).

        Parameters
        ----------
        landmarks_source : np.ndarray
            Source landmarks as x/y/z coordinates.
        landmarks_target : np.ndarray
            Target landmarks as x/y/z coordinates.
        direction : str
            'forward' (default) or 'inverse' (treat the target as the source and vice versa)

        Examples
        --------
        >>> from navis import transforms
        >>> import numpy as np
        >>> # Generate some mock landmarks
        >>> src = np.array([[0, 0, 0], [10, 10, 10], [100, 100, 100], [80, 10, 30]])
        >>> trg = np.array([[1, 15, 5], [9, 18, 21], [80, 99, 120], [5, 10, 80]])
        >>> tr = transforms.MovingLeastSquaresTransform(src, trg)
        >>> points = np.array([[0, 0, 0], [50, 50, 50]])
        >>> tr.xform(points)                                        # doctest: +SKIP
        array([[  1.        ,  15.        ,   5.        ],
               [ 81.56361725, 155.32071504, 187.3147564 ]])

        """
        assert direction in ('forward', 'inverse')
        self.transformer = Transformer(landmarks_source, landmarks_target)
        self.reverse = direction == 'inverse'

    def xform(self, points: np.ndarray) -> np.ndarray:
        """Transform points.

        Parameters
        ----------
        points :    (N, 3) array
                    Points to transform.

        Returns
        -------
        pointsxf :  (N, 3) array
                    Transformed points.

        """
        if isinstance(points, pd.DataFrame):
            if any([c not in points for c in ['x', 'y', 'z']]):
                raise ValueError('DataFrame must have x/y/z columns.')
            points = points[['x', 'y', 'z']].values

        return self.transformer.transform(points, reverse=self.reverse)

    def __neg__(self) -> 'MovingLeastSquaresTransform':
        """Invert direction"""
        out = self.copy()
        out.reverse = not self.reverse
        return out

    def __eq__(self, o: object) -> bool:
        if not isinstance(o, MovingLeastSquaresTransform):
            return False
        for cp_this, cp_that in zip(
            self._control_points(), o._control_points()
        ):
            if not np.array_equal(cp_this, cp_that):
                return False
        return True

    def _control_points(self):
        cp1 = self.transformer.control_points
        cp2 = self.transformer.deformed_control_points
        if self.reverse:
            cp2, cp1 = cp1, cp2
        return cp1, cp2

    def copy(self):
        """Make a copy"""
        return deepcopy(self)

Moving Least Squares transforms of 3D spatial data.

Uses the molesq library, which packages the implementation by Casey Schneider-Mizell of the affine algorithm published in Schaefer et al. 2006.

PARAMETER DESCRIPTION
landmarks_source

Source landmarks as x/y/z coordinates.

TYPE: np.ndarray

landmarks_target

Target landmarks as x/y/z coordinates.

TYPE: np.ndarray

direction

'forward' (default) or 'inverse' (treat the target as the source and vice versa)

TYPE: str DEFAULT: 'forward'

Examples:

>>> from navis import transforms
>>> import numpy as np
>>> # Generate some mock landmarks
>>> src = np.array([[0, 0, 0], [10, 10, 10], [100, 100, 100], [80, 10, 30]])
>>> trg = np.array([[1, 15, 5], [9, 18, 21], [80, 99, 120], [5, 10, 80]])
>>> tr = transforms.MovingLeastSquaresTransform(src, trg)
>>> points = np.array([[0, 0, 0], [50, 50, 50]])
>>> tr.xform(points)
array([[  1.        ,  15.        ,   5.        ],
       [ 81.56361725, 155.32071504, 187.3147564 ]])
Source code in navis/transforms/moving_least_squares.py
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(
    self,
    landmarks_source: np.ndarray,
    landmarks_target: np.ndarray,
    direction: str = 'forward',
) -> None:
    """Moving Least Squares transforms of 3D spatial data.

    Uses the [molesq](https://github.com/clbarnes/molesq) library, which packages the
    [implementation](https://github.com/ceesem/catalysis/blob/master/catalysis/transform.py)
    by Casey Schneider-Mizell of the affine algorithm published in
    [Schaefer et al. 2006](https://dl.acm.org/doi/pdf/10.1145/1179352.1141920).

    Parameters
    ----------
    landmarks_source : np.ndarray
        Source landmarks as x/y/z coordinates.
    landmarks_target : np.ndarray
        Target landmarks as x/y/z coordinates.
    direction : str
        'forward' (default) or 'inverse' (treat the target as the source and vice versa)

    Examples
    --------
    >>> from navis import transforms
    >>> import numpy as np
    >>> # Generate some mock landmarks
    >>> src = np.array([[0, 0, 0], [10, 10, 10], [100, 100, 100], [80, 10, 30]])
    >>> trg = np.array([[1, 15, 5], [9, 18, 21], [80, 99, 120], [5, 10, 80]])
    >>> tr = transforms.MovingLeastSquaresTransform(src, trg)
    >>> points = np.array([[0, 0, 0], [50, 50, 50]])
    >>> tr.xform(points)                                        # doctest: +SKIP
    array([[  1.        ,  15.        ,   5.        ],
           [ 81.56361725, 155.32071504, 187.3147564 ]])

    """
    assert direction in ('forward', 'inverse')
    self.transformer = Transformer(landmarks_source, landmarks_target)
    self.reverse = direction == 'inverse'

Make a copy

Source code in navis/transforms/moving_least_squares.py
108
109
110
def copy(self):
    """Make a copy"""
    return deepcopy(self)

Transform points.

PARAMETER DESCRIPTION
points
    Points to transform.

TYPE: (N, 3) array

RETURNS DESCRIPTION
pointsxf

Transformed points.

TYPE: (N, 3) array

Source code in navis/transforms/moving_least_squares.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
def xform(self, points: np.ndarray) -> np.ndarray:
    """Transform points.

    Parameters
    ----------
    points :    (N, 3) array
                Points to transform.

    Returns
    -------
    pointsxf :  (N, 3) array
                Transformed points.

    """
    if isinstance(points, pd.DataFrame):
        if any([c not in points for c in ['x', 'y', 'z']]):
            raise ValueError('DataFrame must have x/y/z columns.')
        points = points[['x', 'y', 'z']].values

    return self.transformer.transform(points, reverse=self.reverse)

Align neurons using a deformable registration.

Requires the pycpd library. Note that it's often beneficial to first run a rough affine alignment via rigid_align. Anecdotally, this works well to align backbones but tends to pull denser parts (e.g. dendrites) into a tight ball.

PARAMETER DESCRIPTION
x
        Neurons to align.

TYPE: navis.NeuronList

target
        The neuron that all neurons in `x` will be aligned to.
        If `None`, neurons will be aligned to the first neuron in `x`!

TYPE: navis.Neuron | np.ndarray DEFAULT: None

sample
        If provided, will calculate an initial registration on only
        the given fraction of points followed by a landmark transform
        to transform the rest. Use this to speed things up.

TYPE: float [0-1] DEFAULT: None

progress
        Whether to show a progress bar.

TYPE: bool DEFAULT: True

**kwargs
        Additional keyword-argumens are passed through to
        pycpd.DeformableRegistration. In brief: lower `alpha` and
        higher `beta` typically make for more fitting deform. I have
        gone as far as alpha=.01 and beta=10000.

DEFAULT: {}

RETURNS DESCRIPTION
xf

The aligned neurons.

TYPE: navis.NeuronList

regs

The pycpd registration objects.

TYPE: list

Examples:

>>> import navis
>>> n1, n2 = navis.example_neurons(2, kind='skeleton')
>>> n1_aligned, regs = navis.align.align_deform(n1, n2, sample=.2)
Source code in navis/transforms/align.py
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
def align_deform(x, target=None, sample=None, progress=True, **kwargs):
    """Align neurons using a deformable registration.

    Requires the `pycpd` library. Note that it's often beneficial to first
    run a rough affine alignment via `rigid_align`. Anecdotally, this works
    well to align backbones but tends to pull denser parts (e.g. dendrites)
    into a tight ball.

    Parameters
    ----------
    x :             navis.NeuronList
                    Neurons to align.
    target :        navis.Neuron | np.ndarray
                    The neuron that all neurons in `x` will be aligned to.
                    If `None`, neurons will be aligned to the first neuron in `x`!
    sample :        float [0-1], optional
                    If provided, will calculate an initial registration on only
                    the given fraction of points followed by a landmark transform
                    to transform the rest. Use this to speed things up.
    progress :      bool
                    Whether to show a progress bar.
    **kwargs
                    Additional keyword-argumens are passed through to
                    pycpd.DeformableRegistration. In brief: lower `alpha` and
                    higher `beta` typically make for more fitting deform. I have
                    gone as far as alpha=.01 and beta=10000.

    Returns
    -------
    xf :    navis.NeuronList
            The aligned neurons.
    regs :  list
            The pycpd registration objects.

    Examples
    --------
    >>> import navis
    >>> n1, n2 = navis.example_neurons(2, kind='skeleton')
    >>> n1_aligned, regs = navis.align.align_deform(n1, n2, sample=.2)

    """
    try:
        from pycpd import DeformableRegistration as Registration
    except ModuleNotFoundError:
        raise ModuleNotFoundError(
            '`align_deform()` requires the `pycpd` library:\n'
            '  pip3 install git+https://github.com/siavashk/pycpd@master -U'
            )

    if isinstance(x, core.BaseNeuron):
        x = core.NeuronList(x)

    assert isinstance(x, core.NeuronList), f"Expected NeuronList, got {type(x)}"

    if target is None:
        target = x[0]

    target_co = _extract_coords(target)

    # This wraps the registration process
    register = _reg_subsample(Registration, sample=sample)

    # pycpd's deformable registration is very sensitive to the scale of the
    # data. We will hence normalize the neurons to be within the -1 to 1 range
    scale_factor = 0
    for n in x:
        co = _extract_coords(n)
        mx = np.abs(co).max()
        scale_factor = mx if mx > scale_factor else scale_factor

    xf = x / scale_factor
    target_co = target_co / scale_factor
    regs = []
    for n in config.tqdm(xf,
                         disable=(not progress) or (len(xf) == 1),
                         desc='Aligning'):
        if n is target:
            continue
        TY, params, reg = register(X=target_co, Y=_extract_coords(n), **kwargs)
        _set_coords(n, TY)
        regs.append(reg)

    return xf * scale_factor, regs

Run a pairwise alignment between given neurons.

Requires the pycpd library.

PARAMETER DESCRIPTION
x
    Neurons to align to other neurons.

TYPE: navis.NeuronList

y
    The neurons to align to. If `None`, will run pairwise
    alignment of `x` vs `x`.

TYPE: navis.NeuronList DEFAULT: None

method
    Which method to use for alignment. Maps to the respective
    `navis.align_{method}` function. "rigid+deform" performs a
    rigid followed by a warping alignment.

TYPE: "rigid" | "deform" | "pca" | "rigid+deform" DEFAULT: 'rigid'

sample
    If provided, will calculate an initial registration on only
    the given fraction of points followed by a landmark transform
    to transform the rest. Use this to speed things up.

TYPE: float [0-1] DEFAULT: None

**kwargs
    Keyword arguments are passed through to the respective
    alignment function.

DEFAULT: {}

RETURNS DESCRIPTION
np.ndarray

Array of shape (x, y) with the pairwise-aligned neurons.

See Also

navis.nblast_align Runs an NBLAST where neurons are first aligned pairwise.

Examples:

>>> import navis
>>> nl = navis.example_neurons(2, kind='skeleton')
>>> aligned = navis.align.align_pairwise(nl, method='rigid', sample=.2)
Source code in navis/transforms/align.py
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def align_pairwise(x, y=None, method='rigid', sample=None, progress=True, **kwargs):
    """Run a pairwise alignment between given neurons.

    Requires the `pycpd` library.

    Parameters
    ----------
    x :         navis.NeuronList
                Neurons to align to other neurons.
    y :         navis.NeuronList, optional
                The neurons to align to. If `None`, will run pairwise
                alignment of `x` vs `x`.
    method :    "rigid" | "deform" | "pca" | "rigid+deform"
                Which method to use for alignment. Maps to the respective
                `navis.align_{method}` function. "rigid+deform" performs a
                rigid followed by a warping alignment.
    sample :    float [0-1], optional
                If provided, will calculate an initial registration on only
                the given fraction of points followed by a landmark transform
                to transform the rest. Use this to speed things up.
    **kwargs
                Keyword arguments are passed through to the respective
                alignment function.

    Returns
    -------
    np.ndarray
                Array of shape (x, y) with the pairwise-aligned neurons.

    See Also
    --------
    [`navis.nblast_align`][]
                Runs an NBLAST where neurons are first aligned pairwise.

    Examples
    --------
    >>> import navis
    >>> nl = navis.example_neurons(2, kind='skeleton')
    >>> aligned = navis.align.align_pairwise(nl, method='rigid', sample=.2)

    """
    if y is None:
        y = x

    utils.eval_param(x, name='x', allowed_types=(core.NeuronList, ))
    utils.eval_param(y, name='y', allowed_types=(core.NeuronList, ))
    utils.eval_param(method, name='method',
                     allowed_values=('rigid', 'deform', 'pca'))

    func = {'rigid': align_rigid,
            'deform': align_deform,
            'pca': align_pca,
            'rigid+deform': _align_rigid_deform}[method]

    aligned = []
    for n1 in config.tqdm(x,
                          desc='Aligning',
                          disable=not progress or len(x) == 1):
        aligned.append([])
        for n2 in y:
            if n1 is n2:
                xf = n1
            else:
                xf = func(n1, target=n2, sample=sample, progress=False, **kwargs)[0][0]
            aligned[-1].append(xf)

    return np.array(aligned)

Align neurons along their first principal components.

This will in effect turn the neurons into a 1-dimensional line. Requires the scikit-learn library.

PARAMETER DESCRIPTION
x
        The neurons to align.

TYPE: navis.NeuronList | np.ndarray

individually
        Whether to align neurons along their individual or
        collective first principical component.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
xf

The PCA-aligned neurons.

TYPE: navis.NeuronList

pcas

The scikit-learn PCA object(s)

TYPE: list

Examples:

>>> import navis
>>> n1, n2 = navis.example_neurons(2, kind='skeleton')
>>> n1_aligned, pcas = navis.align.align_pca(n1, n2)
Source code in navis/transforms/align.py
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
def align_pca(x, individually=True):
    """Align neurons along their first principal components.

    This will in effect turn the neurons into a 1-dimensional line.
    Requires the `scikit-learn` library.

    Parameters
    ----------
    x :             navis.NeuronList | np.ndarray
                    The neurons to align.
    individually :  bool
                    Whether to align neurons along their individual or
                    collective first principical component.

    Returns
    -------
    xf :    navis.NeuronList
            The PCA-aligned neurons.
    pcas :  list
            The scikit-learn PCA object(s)

    Examples
    --------
    >>> import navis
    >>> n1, n2 = navis.example_neurons(2, kind='skeleton')
    >>> n1_aligned, pcas = navis.align.align_pca(n1, n2)

    """
    try:
        from sklearn.decomposition import PCA
    except ModuleNotFoundError:
        raise ModuleNotFoundError(
            '`align_pca()` requires the `scikit-learn` library:\n'
            '  pip3 install scikit-learn -U'
            )

    if isinstance(x, core.BaseNeuron):
        x = core.NeuronList(x)

    assert isinstance(x, core.NeuronList)

    pcas = []
    if not individually:
        # Collect coordinates
        co = [_extract_coords(n) for n in x]
        n_points = [len(c) for c in co]  # track how many points per neuron
        co = np.vstack(co)

        pca = PCA(n_components=1)
        co_new = pca.fit_transform(X=co)

        xf = x.copy()
        i = 0
        for n, le in zip(xf, n_points):
            _set_coords(n, co_new[i: i + le])
            i += le
        pcas.append(pca)
    else:
        xf = x.copy()
        for n in xf:
            pca = PCA(n_components=1)
            _set_coords(n, pca.fit_transform(X=_extract_coords(n)))
            pcas.append(pca)
    return xf, pcas

Align neurons using a rigid registration.

Requires the pycpd library.

PARAMETER DESCRIPTION
x
        Neurons to align.

TYPE: navis.NeuronList

target
        The neuron that all neurons in `x` will be aligned to.
        If `None`, neurons will be aligned to the first neuron in `x`!

TYPE: navis.Neuron | np.ndarray DEFAULT: None

scale
        If True, will also scale the neuron.

TYPE: bool DEFAULT: False

w
        `w` is used to account for outliers: higher w = more forgiving.
        The default is w=0 which can lead to failure to converge on a
        solution (in particular when scale=False). In that case we
        incrementally increase `w` by a factor of 10 until we find
        a solution. Set `verbose=True` to get detailed feedback
        on the solution.

TYPE: float DEFAULT: 0

sample
        If provided, will calculate an initial registration on only
        the given fraction of points followed by a landmark transform
        to transform the rest. Use this to speed things up.

TYPE: float [0-1] DEFAULT: None

progress
        Whether to show a progress bar.

TYPE: bool DEFAULT: True

RETURNS DESCRIPTION
xf

The aligned neurons.

TYPE: navis.NeuronList

regs

The pycpd registration objects.

TYPE: list

Examples:

>>> import navis
>>> n1, n2 = navis.example_neurons(2, kind='skeleton')
>>> n1_aligned, regs = navis.align.align_rigid(n1, n2, sample=.2)
Source code in navis/transforms/align.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def align_rigid(x, target=None, scale=False, w=0, verbose=False, sample=None, progress=True):
    """Align neurons using a rigid registration.

    Requires the `pycpd` library.

    Parameters
    ----------
    x :             navis.NeuronList
                    Neurons to align.
    target :        navis.Neuron | np.ndarray
                    The neuron that all neurons in `x` will be aligned to.
                    If `None`, neurons will be aligned to the first neuron in `x`!
    scale :         bool
                    If True, will also scale the neuron.
    w :             float
                    `w` is used to account for outliers: higher w = more forgiving.
                    The default is w=0 which can lead to failure to converge on a
                    solution (in particular when scale=False). In that case we
                    incrementally increase `w` by a factor of 10 until we find
                    a solution. Set `verbose=True` to get detailed feedback
                    on the solution.
    sample :        float [0-1], optional
                    If provided, will calculate an initial registration on only
                    the given fraction of points followed by a landmark transform
                    to transform the rest. Use this to speed things up.
    progress :      bool
                    Whether to show a progress bar.

    Returns
    -------
    xf :    navis.NeuronList
            The aligned neurons.
    regs :  list
            The pycpd registration objects.

    Examples
    --------
    >>> import navis
    >>> n1, n2 = navis.example_neurons(2, kind='skeleton')
    >>> n1_aligned, regs = navis.align.align_rigid(n1, n2, sample=.2)

    """
    try:
        from pycpd import RigidRegistration as Registration
    except ModuleNotFoundError:
        raise ModuleNotFoundError(
            '`align_rigid()` requires the `pycpd` library:\n'
            '  pip3 install git+https://github.com/siavashk/pycpd@master -U'
            )

    if isinstance(x, core.BaseNeuron):
        x = core.NeuronList(x)

    assert isinstance(x, core.NeuronList)

    if target is None:
        target = x[0]

    target_co = _extract_coords(target)

    # This wraps the registration process
    register = _reg_subsample(Registration, sample=sample)

    xf = x.copy()
    regs = []
    for n in config.tqdm(xf,
                         disable=(not progress) or (len(xf) == 1),
                         desc='Aligning'):
        if n is target:
            continue
        # `w` is used to account for outliers -> higher w = more forgiving
        # the default is w=0 which can lead to failure to converge on a solution
        # in particular when scale=False
        # Our work-around here is to start at w=0 and incrementally increase w
        # if we fail to converge
        # Also note that pycpd ignores the `scale` in earlier versions. The
        # version on PyPI is currently outdated. From what I understand we need
        # the Github version.
        converged = False
        while w <= 0.001:
            try:
                with warnings.catch_warnings():
                    warnings.simplefilter("ignore")
                    TY, params, reg = register(X=target_co,
                                               Y=_extract_coords(n),
                                               scale=scale,
                                               s=1,
                                               w=w)
                _set_coords(n, TY)
                converged = True
                regs.append(reg)
                break
            except np.linalg.LinAlgError:
                if w == 0:
                    w += 0.000000001
                else:
                    w *= 10

        if verbose:
            if not converged:
                logger.info(f'Registration of {n.id} onto {target.id} did not converge')
            else:
                logger.info(f'Registration of {n.id} onto {target.id} converged for w={w}')

    return xf, regs