"""
Vector
======
Module to compute filters on vector/direction fields
Used for filtering orientation and fiber data.
"""
__author__ = 'Christoph Kirst <christoph.kirst.ck@gmail.com>'
__license__ = 'GPLv3 - GNU General Public License v3 (see LICENSE.txt)'
__copyright__ = 'Copyright © 2023 by Christoph Kirst'
import numpy as np
from ..StructureElement import sphere
from .VectorStructureElement import vectors_to_neighbours
import pyximport
pyximport.install(setup_args={"include_dirs": np.get_include()}, reload_support=True)
from . import VectorCode as code
########################################################################################################################
# Vector filters
########################################################################################################################
[docs]
def similarity(vectors, structural_element, structural_element_offset=None, weights=None, sink=None):
"""Compute sum of scalar products of the center vector with neighbouring vectors defined by the structural element.
Arguments
---------
vectors : array
Input vector field array.
structural_element: array or str
Structural element specification.
structural_element_offset : tuple or None
Structural element offset
weights : None or array
Weights for each vector in the vector field.
sink : array
Output, if None, a new array is allocated.
Returns
-------
sink : array
Filtered output.
Algorithm
---------
For each center voxel compute the scalar products of the center vector with the neighbouring vectors defined by the
structural element and sum.
"""
if weights is None:
return _apply_code(code.similarity, vectors, structural_element, structural_element_offset, weights, sink)
else:
return _apply_code(code.similarity_weighted, vectors, structural_element, structural_element_offset, weights,
sink)
[docs]
def alignment(vectors, structural_element, structural_element_offset=None, weights=None, sink=None):
"""Compute alignment of central vector with neighboring vectors.
Arguments
---------
vectors : array
Input vector field array.
structural_element: array or str
Structural element specification.
structural_element_offset : tuple or None
Structural element offset
weights : None or array
Weights for each vector in the vector field.
sink : array
Output, if None, a new array is allocated.
Returns
-------
sink : array
Filtered output.
Algorithm
---------
For each central voxel
1. compute a set of weights to neighboring vectors via the scalar product of the
central vector with the structural element vectors
2. compute the scalar product of the central vector with the neighbouring vectors
3. from the weighed sum
"""
if weights is None:
return _apply_code(code.alignment, vectors,
structural_element, structural_element_offset,
weights, sink)
else:
return _apply_code(code.alignment_weighted, vectors,
structural_element, structural_element_offset,
weights, sink)
[docs]
def orientation(vectors, structural_element, structural_element_offset=None, weights=None, sink=None):
"""Compute orientation alignment of central vector with neighboring vectors.
Arguments
---------
vectors : array
Input vector field array.
structural_element: array or str
Structural element specification.
structural_element_offset : tuple or None
Structural element offset
weights : None or array
Weights for each vector in the vector field.
sink : array
Output, if None, a new array is allocated.
Returns
-------
sink : array
Filtered output.
Algorithm
---------
For each central voxel
1. compute a set of matched orientation weights to neighboring vectors via the absolute value scalar product
of the central vector with the structural element vectors (e.g. direction vectors from center), and further
multiply by the input weights for the vectors.
2. compute the absolute value of the scalar product of the central vector with the neighbouring vectors
3. from the weighed sum
"""
if weights is None:
return _apply_code(code.orientation, vectors,
structural_element, structural_element_offset,
weights, sink)
else:
return _apply_code(code.orientation_weighted, vectors,
structural_element, structural_element_offset,
weights,
sink)
[docs]
def orientation_ellipsoid(vectors, weights=None, max_radius=5, scales=(2,1), sink=None):
"""Compute orientation alignment of central vector with neighboring vectors.
Arguments
---------
vectors : array
Input vector field array.
weights : None or array
Weights for each vector in the vector field.
max_radius : float
The maximal radius of the ellipse
scales : tuple or list
A tuple (s1,s2) with s1 for the scale of the mayor axis and s2 the scale for the 2 minor axes of the ellipsoid.
sink : array
Output, if None, a new array is allocated.
Returns
-------
sink : array
Filtered output.
Algorithm
---------
For each central voxel
1. compute a set of matched orientation weights to neighboring vectors via the absolute value scalar product
of the central vector with the structural element vectors (e.g. direction vectors from center), and further
multiply by the input weights for the vectors.
2. compute the absolute value of the scalar product of the central vector with the neighbouring vectors
3. from the weighed sum
"""
if not hasattr(scales, '__len__') or len(scales) != 2:
raise ValueError('scales must be a tuple or list of two elements, found %r' % (scales,))
radius = max_radius / np.max(scales)
parameter = tuple(scales) + (radius,)
# compute structural element that fits ellipsoid
shape = (int(radius * np.max(scales))-1,) * 3
structural_element = sphere(shape)
structural_element_offset = None
if weights is None:
return _apply_code(code.orientation_ellipsoid, vectors,
structural_element, structural_element_offset,
weights, sink, parameter=parameter)
else:
return _apply_code(code.orientation_ellipsoid_weighted, vectors, structural_element, structural_element_offset,
weights, sink, parameter=parameter)
########################################################################################################################
# Helpers
########################################################################################################################
def _apply_code(function, vectors, structural_element, structural_element_offset, weights,
sink=None, sink_dtype=None, sink_shape_per_pixel=None, parameter=None):
"""Helper to apply the core functions"""
if vectors.ndim != 4:
raise ValueError('The vector field is assumed to be 4d array, found %dd!' % (vectors.ndim,))
shape = vectors.shape[:-1]
if isinstance(structural_element, tuple) and len(structural_element) == 2 and \
structural_element[0] == 'relative_distance':
structural_element_shape = (structural_element[1],) * 3
structural_element, _, _ = vectors_to_neighbours(structural_element_shape, reshape=False,
rescale=True, max_radius='sphere')
if structural_element.ndim == 3:
structural_element = structural_element[..., np.newaxis]
structural_element_index = np.array(np.array(np.where(np.sum(structural_element != 0, axis=-1))).T)
structural_element_vector = structural_element[structural_element_index[:, 0],
structural_element_index[:, 1],
structural_element_index[:, 2]]
if structural_element_offset is None:
structural_element_offset = np.array(structural_element.shape[:3]) // 2
structural_element_offset = np.array(structural_element_offset, dtype=int)
if weights is not None:
if weights.shape != shape:
raise ValueError(
'The weights do not match the shape of the vector field %r != %r!' % (weights.shape, shape))
if sink_shape_per_pixel is None:
shape_per_pixel = (1,)
else:
shape_per_pixel = sink_shape_per_pixel
if sink is None:
if sink_dtype is None:
sink_dtype = float
sink = np.zeros(shape + shape_per_pixel, dtype=sink_dtype, order='F')
else:
if shape_per_pixel != (1,):
if sink.shape != shape + shape_per_pixel:
raise ValueError(
'The sink of shape %r does not have expected shape %r!' % (sink.shape, shape + shape_per_pixel))
if sink.shape != shape + shape_per_pixel:
sink = sink.reshape(shape + shape_per_pixel)
if structural_element_vector.dtype != sink_dtype:
structural_element_vector = np.asarray(structural_element_vector, dtype=sink_dtype)
if sink.dtype == bool:
sink_view = sink.view('uint8')
else:
sink_view = sink
if parameter is None:
parameter = np.zeros(0)
parameter = np.asarray([parameter], dtype=float).flatten()
if weights is None:
function(vectors=vectors,
selem_index=structural_element_index,
selem_vector=structural_element_vector,
selem_offset=structural_element_offset,
sink=sink_view,
parameter=parameter)
else:
function(vectors=vectors,
weights=weights,
selem_index=structural_element_index,
selem_vector=structural_element_vector,
selem_offset=structural_element_offset,
sink=sink_view,
parameter=parameter)
if sink_shape_per_pixel is None:
sink = sink.reshape(sink.shape[:-1])
return sink
########################################################################################################################
# Tests
########################################################################################################################
def _test():
# %gui qt
import numpy as np
import ClearMap.ImageProcessing.Filter.Vector.Vector as vec
import ClearMap.ImageProcessing.Filter.Vector.VectorStructureElement as vse
import ClearMap.ImageProcessing.Filter.StructureElement as se
import ClearMap.Visualization.PyVista.plot as pvp
import ClearMap.Visualization.Plot3d as p3d
print('done')
from importlib import reload
reload(vec.code)
reload(vec)
# axon along x axis
vectors = np.zeros((10, 10, 10, 3))
vectors[:, 4:7, 4:7, 0] = 1
vectors[:5] *= -1
# similarity
selem = se.sphere((3,3,3)) > 0
filtered = vec.similarity(vectors, selem)
#pvp.plot_3d_volume(filtered)
p3d.plot({'source' : filtered, 'vectors': vectors})
# orientation ellipsoid filter
filtered = vec.orientation_ellipsoid(vectors, scales=(5,0.5), max_radius=10)
p3d.plot({'source': filtered, 'vectors': vectors})
p3d.close(all)
# real data
filtered = vec.orientation_ellipsoid(o, scales=(4, 1), max_radius=6, weights=a3)
p3d.plot([a3, filtered, {'source': filtered, 'orientations': o}])
# structural_element
def rescale(x):
return 1.0/x**2
structure, lengths, valid = vse.vectors_to_neighbours((7, 7, 7), reshape=False, rescale=rescale, max_radius='sphere')
p = pvp.plot_volumetric_vector_field(structure)
p.show_grid()
# orientation weights
central_vector = np.array([1,1,0])
weights = np.abs(structure @ central_vector) / np.linalg.norm(central_vector)
nrm = np.linalg.norm(structure, axis=-1)
valid = nrm > 0
weights[valid] = weights[valid] / nrm[valid]
weights = weights**5
weights = weights * nrm
pvp.plot_3d_volume(weights)
p3d.plot(weights)
p3d.plot(nrm)
filtered = vec.alignment(vectors=vectors, selem=selem)
print('done')
# %gui qt
p = p3d.plot_volumetric_vector_field(selem)
p.show_grid()
p = p3d.plot_volumetric_vector_field(vectors)
p.show_grid()
# %gui qt
p3d.plot_3d_volume(filtered)
weights = np.random.rand(*vectors.shape[:-1])
filtered = vec.alignment(vectors=vectors, selem=selem, weights=weights)
print('done')