# -*- coding: utf-8 -*-
"""
stitching_wobbly
================
Wobbly stitching module handles the alignment of large volumetric data sets.
The module aligns stacks allowing them to wobble around a wobble axis, i.e.
due to oscillatory movements during image acquisition.
"""
__author__ = 'Christoph Kirst <christoph.kirst.ck@gmail.com>'
__license__ = 'GPLv3 - GNU General Public License v3 (see LICENSE)'
__copyright__ = 'Copyright © 2020 by Christoph Kirst'
__webpage__ = 'https://idisco.info'
__download__ = 'https://www.github.com/ChristophKirst/ClearMap2'
import warnings
import numpy as np
import functools as ft
import multiprocessing as mp
import ClearMap.IO.IO as io
import ClearMap.IO.Slice as slc
import ClearMap.Alignment.Stitching.stitching_rigid as strg
import ClearMap.Alignment.Stitching.Tracking as trk
import ClearMap.ParallelProcessing.ParallelTraceback as ptb
import ClearMap.Utils.Timer as tmr
import ClearMap.Utils.tag_expression as te
from ClearMap.Utils.utilities import CancelableProcessPoolExecutor
from ClearMap.Alignment.Stitching.layout_graph_utils import cluster_components
###############################################################################
### Layout
###############################################################################
[docs]
class WobblySource(strg.Source):
"""Class to handle source data and positions of wobbly stacks."""
ISOLATED = -2
INVALID = -1
VALID = 0
FIXED = 2
status_to_description = {ISOLATED : 'isolated',
INVALID : 'invalid',
VALID : 'valid',
FIXED : 'fixed'}
def __init__(self, source, wobble = None, status = None, axis = 2, position = None, tile_position = None):
"""Source class constructor.
Arguments
---------
source: string, array or Source class
The image source.
position : list of tuple of ints or None
The positions of the source's 'lower' corner of the source.
wobble : list of list of ints or None
The positions of the individual planes in this wobbly source.
"""
strg.Source.__init__(self, source = source, position = position, tile_position = tile_position)
self._axis = int(axis)
if wobble is None:
shape = super(WobblySource, self).shape
self._wobble = np.zeros((shape[self._axis], len(shape) - 1), dtype = int)
else:
self._wobble = np.array(wobble, dtype = int)
if status is None:
shape = super(WobblySource, self).shape
self._status = np.full(shape[self._axis], self.VALID, dtype = int)
else:
self._status = np.array(status, dtype = int)
@property
def name(self):
return 'Wobbly-' + self.source.name
@property
def axis(self):
"""The axis along which the source is assumed to be wobbly.
Returns
-------
axis : int
The wobble axes.
"""
return self._axis
@property
def coordinate(self):
return self._position[self.axis]
@property
def height(self):
return self._source.shape[self.axis]
@property
def wobble(self):
"""The wobblyness of this source.
Returns
-------
wobble : array of ints
The deviations from the source position along the wobble axes.
"""
return self._wobble
@property
def wdim(self):
return self._source.ndim - 1
@wobble.setter
def wobble(self, wobble):
if wobble.shape[0] != self.height or wobble.ndim != self.wdim:
raise ValueError(f'Number of wobbles {wobble.shape[0]:d} is not equal the number of planes={self.height:d}'
f' along the wobble axis {self.axis:d}!')
self._wobble = np.array(wobble, dtype = int)
[docs]
def wobble_from_positions(self, positions):
start = self.coordinate
stop = start + self.height
self._wobble[:] = positions[start:stop]
#set status
finite = np.all(np.isfinite(positions[start:stop]), axis=1)
non_finite = np.logical_not(finite)
self._status[non_finite] = self.INVALID
#self._status[finite] = self.VALID;
@property
def status(self):
"""The status of each slice of this source.
Returns
-------
status : array of ints
The status for each position along the wobble axes.
"""
return self._status
@property
def valids(self):
return self._status >= self.VALID
### Geometry
@property
def lower_wobbly(self):
"""The lower corner of the wobbly source.
Returns
-------
lower : tuple of int
The coordinates of the lower corner of the source.
"""
wobble_min = np.min(self._wobble, axis=0)
return self._wobble_to_position(wobble_min, self.coordinate)
@property
def upper_wobbly(self):
"""The upper corner of the source.
Returns
-------
upper : tuple of int
The coordinates of the upper corner of the source.
"""
wobble_max = np.max(self._wobble, axis=0)
position = self._wobble_to_position(wobble_max, self.coordinate)
shape = self.source.shape
return tuple(p + s for p,s in zip(position, shape))
@property
def positions(self):
"""The positions of the lower corners of all slices along the wobble axis.
Returns
-------
positions : array
The coordinates of the lower corner of the slices along the wobble axis.
"""
wobble = self.wobble
axis = self.axis
coordinate = self.coordinate
positions = np.concatenate([wobble[:,:axis],
np.arange(len(wobble))[:,np.newaxis] + coordinate,
wobble[:,axis:]], axis = 1)
return positions
[docs]
def coordinate_to_local(self, coordinate):
"""Converts a wobble axis coordinate to a local coordinate wrt to the sources' origin.
Arguments
---------
coordinate : int
The non-local coordinate.
Returns
-------
local_coordinate : int
The local coordinate within this source.
"""
position = self.coordinate
shape = self.height
if coordinate < position or coordinate >= position + shape:
raise RuntimeError(f'Coordinate {coordinate:d} out of range ({position:d},{position + shape:d})!')
return coordinate - position
[docs]
def coordinate_from_local(self, local_coordinate):
"""Converts a local wobble axis coordinate to the non-local coordinate.
Arguments
---------
local_coordinate : int
The local coordinate within the source.
Returns
-------
coordinate : int
The non-local coordinate.
"""
position = self.coordainte
shape = self.height
if local_coordinate < 0 or local_coordinate >= shape:
raise RuntimeError(f'Coordinate {local_coordinate:d} out of range ({position:d},{position + shape:d})!')
return local_coordinate + position
[docs]
def position_at_coordinate(self, coordinate):
"""Returns the wobbly position of the source at the specified coordinate along the wobble axis.
Arguments
---------
coordinate : int
The coordinate along the wobble axis.
Returns
-------
position : tuple of int.
The non-local position of the specified coordinate slice.
"""
local_coordinate = self.coordinate_to_local(coordinate)
wobble = self._wobble[local_coordinate]
return self._wobble_to_position(wobble, coordinate)
[docs]
def wobble_at_coordinate(self, coordinate):
"""Returns the wobbly position of the source at the specified coordinate along the wobble axis.
Arguments
---------
coordinate : int
The coordinate along the wobble axis.
Returns
-------
position : tuple of int.
The non-local position of the specified coordinate slice.
"""
local_coordinate = self.coordinate_to_local(coordinate)
return self._wobble[local_coordinate]
#status
[docs]
def status_at_coordinate(self, coordinate):
local_coordinate = self.coordinate_to_local(coordinate)
return self._status[local_coordinate]
[docs]
def set_status_at_coordinate(self, coordinate, status):
local_coordinate = self.coordinate_to_local(coordinate)
self._status[local_coordinate] = status
[docs]
def is_valid(self, coordinate):
return 0 <= coordinate - self.coordinate < self.height and self.status_at_coordinate(coordinate) >= self.VALID
[docs]
def set_invalid(self, coordinate):
if 0 <= coordinate - self.coordinate < self.height:
self.set_status_at_coordinate(coordinate, self.INVALID)
[docs]
def set_isolated(self, coordinate):
if 0 <= coordinate - self.coordinate < self.height:
self.set_status_at_coordinate(coordinate, self.ISOLATED)
[docs]
def fix_isolated(self, exclude_borders = False):
"""Fix the positons of isolated slices."""
status = self.status
wobble = self.wobble
n_status = len(status)
isolated = np.array(status == self.ISOLATED, dtype=int)
isolated = np.pad(isolated, (1,1), 'constant')
delta = np.diff(isolated)
starts = np.where(delta > 0)[0]
ends = np.where(delta < 0)[0]
#whole stack has no isolated slices
if len(starts) == 0:
return
#if whole stack is isolated
if len(starts) == 1 and starts[0] == 0 and len(ends) == 1 and ends[0] == n_status:
status[:] = self.ISOLATED
return
#find left and right bounds for isolated stretches
for s,e in zip(starts, ends):
#exclude borders
if exclude_borders:
if s == 0 or e == n_status:
status[s:e] = self.ISOLATED
continue
#find next valid in each direction
if s > 0 and status[s-1] >= self.VALID:
left = wobble[[s-1]]
else:
left = None
if e < n_status and status[e] >= self.VALID:
right = wobble[[e]]
else:
right = None
if left is None and right is None:
status[s:e] = self.ISOLATED
else:
if left is None:
wobble[s:e] = right
elif right is None:
wobble[s:e] = left
else: # linearly interpolate
wobble[s:e] = np.array(np.round((right-left) * 1.0 / (e-s+1) * np.arange(1, e-s+1)[:, np.newaxis] + left), dtype = int)
status[s:e] = self.FIXED
[docs]
def smooth_positions(self, smooth = dict(method = 'window', window = 'bartlett', window_length = 10)):
positions = smooth_positions(self.positions, self.valids, smooth=smooth)
return positions
### Helper
def _wobble_to_position(self, wobble, coordinate):
"""Transform a wobble and axis coordinate to the full position.
Arguments
---------
wobble : tuple
The wobble to add.
coordinate : int
The coordinate along the axis
Returns
-------
position : tuple
The position with added wobble.
"""
axis = self.axis
return tuple(wobble[:axis]) + (coordinate,) + tuple(wobble[axis:])
### Other
[docs]
def array_wobbly(self):
"""Returns the array in the wobbly form with zeros at empty positions.
Returns
-------
array : array
The data of the array.
"""
axis = self.axis
extent = self.extent
ndim = len(extent)
array = np.zeros(extent, dtype=self.dtype, order=self.order)
lower_wobble = np.min(self.wobble, axis = 0)
slicing = (slice(None),) * (ndim - 1)
for c in range(extent[axis]):
slicing_source = slicing[:axis] + (c,) + slicing[axis:]
data = self.source[slicing_source]
shape = data.shape
position = self.wobble[c] - lower_wobble
slicing_data = tuple(slice(p,p+s) for p,s in zip(position, shape))
slicing_data = slicing_data[:axis] + (c,) + slicing_data[axis:]
array[slicing_data] = data
return array
def __copy__(self):
cls = self.__class__
new = cls.__new__(cls)
new.__dict__.update(self.__dict__)
new._wobble = self._wobble.copy()
return new
[docs]
class WobblyAlignment(strg.Alignment):
NOSIGNAL = -5
NOMINIMA = -4
UNALIGNED = -3
UNTRACED = -2
INVALID = -1
VALID = 0
MEASURED = 1
ALIGNED = 2
FIXED = 3
status_to_description = {NOSIGNAL : 'no signal',
NOMINIMA : 'no minima',
UNALIGNED : 'unaligned',
UNTRACED : 'untraced',
INVALID : 'invalid',
VALID : 'valid',
MEASURED : 'measured',
ALIGNED : 'aligned',
FIXED : 'fixed'}
def __init__(self, pre = None, post = None, shifts = None, displacements = None, qualities = None, status = None, axis = 2, shift = None, displacement = None, quality = None):
strg.Alignment.__init__(self, pre=pre, post=post, shift=shift, displacement=displacement, quality=quality)
#overlap region
overlap = strg.overlap(strg.Region(position = pre.position[axis:axis+1], shape = pre.shape[axis:axis+1]),
strg.Region(position = post.position[axis:axis+1], shape = post.shape[axis:axis+1]))
if overlap == None:
raise ValueError('The two sources do not overlap along the wobble axis!')
n = overlap.shape[0]
ndim = pre.ndim
if displacements is None:
d = tuple(p - q for p,q,d in zip(post.position, pre.position, range(ndim)) if d != axis)
if shifts is None:
displacements = np.ones((n, pre.ndim-1), dtype = int) * d
else:
displacements = np.array(shifts, dtype = int) + d
self._displacements = displacements
if qualities is None:
qualities = np.ones(n) * (-np.inf)
self.qualities = qualities
if status is None:
status = np.full(n, self.VALID, dtype = int)
self.status = status
self.axis = axis
@property
def displacements(self):
return self._displacements
@displacements.setter
def displacements(self, value):
if len(value) != self.upper_coordinate - self.lower_coordinate:
raise ValueError(f'Dimension mismatch {len(value):d} != {self.upper_coordinate - self.lower_coordinate:d}')
self._displacements = value
@property
def lower_coordinate(self):
return max(self.pre.coordinate, self.post.coordinate)
@property
def upper_coordinate(self):
return min(self.pre.coordinate + self.pre.height, self.post.coordinate + self.post.height)
[docs]
def coordinate_to_local(self, coordinate):
lower, upper = self.lower_coordinate, self.upper_coordinate
if not (lower <= coordinate < upper):
raise ValueError('Invalid coordinate!')
else:
return coordinate - lower
@property
def shifts(self):
axis = self.axis
displacements = self._displacements
pre_pos = self.pre.position
post_pos = self.post.position
pre_pos = pre_pos[:axis] + pre_pos[axis+1:]
post_pos = post_pos[:axis] + post_pos[axis+1:]
shifts = displacements - post_pos + pre_pos
return shifts
@shifts.setter
def shifts(self, value):
if len(value) != self.upper_coordinate - self.lower_coordinate:
raise ValueError(f'Dimension mismatch {len(value):d} != {self.upper_coordinate - self.lower_coordinate:d}')
axis = self.axis
pre_pos = self.pre.position
post_pos = self.post.position
pre_pos = pre_pos[:axis] + pre_pos[axis+1:]
post_pos = post_pos[:axis] + post_pos[axis+1:]
self._displacements = np.array(value) + post_pos - pre_pos
[docs]
def align_wobbly_axis(self, **kwargs):
shifts, qualities = align_wobbly_axis(self.pre, self.post, axis=self.axis, **kwargs)
self.shifts = shifts
self.qualities = qualities
[docs]
def displacement_at_coordinate(self, coordinate):
return self._displacements[self.coordinate_to_local(coordinate)]
[docs]
def quality_at_coordinate(self, coordinate):
return self.qualities[self.coordinate_to_local(coordinate)]
[docs]
def status_at_coordinate(self, coordinate):
return self.status[self.coordinate_to_local(coordinate)]
[docs]
def set_status_at_coordinate(self, coordinate, status):
self.status[self.coordinate_to_local(coordinate)]
[docs]
def valids(self, min_quality = -np.inf):
valids = self.status >= self.VALID
if min_quality:
valids = np.logical_and(valids, self.qualities > min_quality)
return valids
[docs]
def smooth_displacements(self, min_quality = -np.inf, **kwargs):
displacements = smooth_displacements(self.displacements, self.valids(min_quality=min_quality), **kwargs)
#self.displacements = displacements
return displacements
[docs]
def fix_unaligned(self):
"""Linearly interpolate between unaligned coordinates"""
status = self.status
displacements = self.displacements
qualities = self.qualities
n_status = len(status)
unaligned = np.array(status == self.UNALIGNED, dtype=int)
unaligned = np.pad(unaligned, (1,1), 'constant')
delta = np.diff(unaligned)
starts = np.where(delta > 0)[0]
ends = np.where(delta < 0)[0]
# whole stack is aligned
if len(starts) == 0:
return
# whole stack is unaligned
if len(starts) == 1 and starts[0] == 0 and len(ends) == 1 and ends[0] == n_status:
status[:] = self.INVALID
return
# find left and right bounds for isolated stretches
for s,e in zip(starts, ends):
# find next valid in each direction
if s > 0 and status[s-1] >= self.VALID:
left = displacements[[s-1]]
else:
left = None
if e < n_status and status[e] >= self.VALID:
right = displacements[[e]]
else:
right = None
if left is None and right is None:
status[s:e] = self.INVALID
else:
if left is None:
displacements[s:e] = right
qualities[s:e] = qualities[e]
elif right is None:
displacements[s:e] = left
qualities[s:e] = qualities[s-1]
else: # linearly interpolate
displacements[s:e] = np.array(np.round((right-left) * 1.0 / (e-s+1) * np.arange(1, e-s+1)[:, np.newaxis] + left), dtype = int)
qs = qualities[s-1]
qe = qualities[e]
if np.isfinite(qs) and np.isfinite(qe):
qualities[s:e] = (qe - qs) / (e-s+1) * np.arange(1, e-s+1) + qs
elif np.isfinite(qe):
qualities[s:e] = qe
else:
qualities[s:e] = qs
status[s:e] = self.FIXED
[docs]
def overlay_wobbly(self, overlap = True):
axis = self.axis
shifts = self.shifts
n_slices = len(shifts)
shifts = shifts[self.status >= self.VALID]
min_shifts = np.min(np.array(shifts), axis = 0)
max_shifts = np.max(np.array(shifts), axis = 0)
min_shifts = tuple(min_shifts[:axis]) + (0,) + tuple(min_shifts[axis:])
max_shifts = tuple(max_shifts[:axis]) + (0,) + tuple(max_shifts[axis:])
ndim = len(min_shifts)
if overlap:
o1,o2 = strg._overlap_with_shifts(self.pre, self.post, max_shifts=[(m,n) for m,n in zip(min_shifts, max_shifts)])
i1 = self.pre[o1.local_slicing(self.pre)]
i2 = self.post[o2.local_slicing(self.post)]
p1 = o1.lower
p2 = o2.lower
s1 = o1.shape
s2 = o2.shape
else:
i1 = self.pre
i2 = self.post
p1 = self.pre.position
p2 = self.post.position
s1 = self.pre.shape
s2 = self.post.shape
#paddings
pad1 = ()
off2 = ()
shape = ()
for d in range(ndim):
pad1 += ((max(0, p1[d] - (p2[d] + min_shifts[d])), max(0, p2[d] + s2[d] + max_shifts[d] - (p1[d] + s1[d]))),)
off2 += (max(0, p2[d] + min_shifts[d] - p1[d]),)
shape += (max(s1[d] + pad1[d][0] + pad1[d][1], off2[d] + max_shifts[d] - min_shifts[d] + s2[d]),)
ovl = [np.zeros(shape, dtype=self.pre.dtype), np.zeros(shape, dtype=self.post.dtype)]
slice_i = [slice(None)] * ndim
pad1i = pad1[:axis] + pad1[axis+1:]
for i in range(n_slices):
if i % 100 == 0:
print(f'Generating overlay slice {i:d}/{n_slices:d}!')
if self.status[i] >= self.VALID:
slice_i[axis] = i
shift = tuple(self.shifts[i])
shift = shift[:axis] + (0,) + shift[axis:]
pad2 = tuple((o + s - m, sh - (o + s - m) - sp) for o,s,m,sh,sp in zip(off2, shift, min_shifts, shape, s2))
pad2i = pad2[:axis] + pad2[axis+1:]
ovl[0][slice_i] = np.pad(i1[slice_i], pad1i, 'constant')
ovl[1][slice_i] = np.pad(i2[slice_i], pad2i, 'constant')
return ovl
[docs]
class WobblyLayout(strg.TiledLayout):
"""Layout to handle stitching of wobbly sources."""
def __init__(self, sources = None, expression = None, tile_axes = None, tile_shape = None, tile_positions = None, positions = None, overlaps = None, alignments = None, axis = 2, position = None, shape = None, dtype = None, order = None):
"""WobblyStackLayout constructor.
Arguments
---------
expression : str
Regular expression of source names.
tile_axes : tuple of strings
The names and ordering of the grid axes of the named groups in the regular expression. If None use the names and order as they appear in expression.
tile_shape : tuple of ints or None
Shape of the grid. If None determine automatically.
tile_positions : list of tuple of ints or None
List of grid positions to consider, if None use all available.
positions : list of tuples of ints
The positions of the individual sources, if None use overlaps to position sources.
overlaps : tuple of ints or None
Overlaps of the individual sources in each grid dimension. If None assume overlap is zero.
shape : tuple of int or None
The fixed shape of this Layout, if None the minimal size to fit all sources will be used.
position : tuple of int or None
The fixed position of this layout, if None the lower corner to fit all sources will be used.
dtype: dtype or None
The data type to use for this layout, if None use the dtype of the first source.
axis : int
The wobbly axis of the sources.
"""
# initialize classes
strg.TiledLayout.__init__(self, sources = sources, expression = expression, tile_axes = tile_axes, tile_shape = tile_shape, tile_positions = tile_positions, positions = positions, overlaps = overlaps, alignments = alignments, position = position, shape = shape, dtype = dtype, order = order)
# convert sources to WobblySources
sources = self.sources
self.sources = [WobblySource(source = s, axis = axis) for s in sources]
alignments = []
sources_to_wobbly_sources = {s : w for s,w in zip(sources, self.sources)}
for a in self.alignments:
pre = sources_to_wobbly_sources[a.pre]
post = sources_to_wobbly_sources[a.post]
displacement = a.displacement
quality = a.quality
alignments.append(WobblyAlignment(pre=pre, post=post, axis=axis, displacement=displacement, quality=quality))
self.alignments = alignments
self.axis = int(axis)
@property
def lower_wobbly(self):
"""Calculates the lower position of the entire layout.
Returns
-------
lower : tuple of ints
The lower position of the full layout.
"""
return tuple(np.min([s.lower_wobbly for s in self.sources], axis = 0))
@property
def upper_wobbly(self):
"""Calculates the upper position of the entire layout.
Returns
-------
upper : tuple of ints
The upper position of the full layout.
"""
return tuple(np.max([s.upper_wobbly for s in self.sources], axis = 0))
@property
def origin_wobbly(self):
return tuple(min(p,0) for p in self.lower_wobbly)
@property
def shape_wobbly(self):
return tuple(u - o for u,o in zip(self.upper_wobbly, self.origin_wobbly))
[docs]
def set_positions(self, positions):
"""Set the positions of all wobbly slices and sources."""
for s,p in zip(self.sources, positions):
s.wobble_from_positions(p)
[docs]
def slice_along_axis_wobbly(self, coordinate):
"""Returns a layout corresponding to a slice along the wobble axis in this layout.
Arguments
---------
coordinate : int
The coordinate at which to take the slice.
axis : int
The axis to take the slice in.
Returns
-------
layout : Layout class
The sliced layout.
Note
----
The underlying sources are converted to virtual for parallel stitching.
"""
axis = self.axis
ndim = self.ndim
#filter sources in slice
sources = [source for source in self.sources if source.is_valid(coordinate)]
#slice sources
sliced_sources = []
for source in sources:
position = source.wobble_at_coordinate(coordinate)
slicing = (slice(None),) * axis + (coordinate - source.coordinate,) + (slice(None),) * (ndim-1-axis)
sliced_sources.append(strg.Source(source = slc.Slice(source=source.source.as_virtual(), slicing=slicing), position=position, tile_position=source.tile_position))
if self._shape is not None:
shape = self._shape[:axis] + self._shape[axis+1]
else:
shape = None
if self._position is not None:
position = self._position[:axis] + self._position[axis+1]
else:
position = None
return strg.Layout(sources = sliced_sources, shape = shape, position = position, dtype = self._dtype, order = self._order)
[docs]
def layouts_along_axis_wobbly(self, coordinates = None):
"""Returns a list of Layouts representing the placed wobbly sources in each wobbly-axis slice of this layout.
Arguments
---------
coordinates : list of ints, all, or None
The positions of the slices along the wobble axis. If all or None take all possible slices.
Returns
-------
slices : list of SlicedLayout classes
The layouts in each wobble-axis-plane.
Note
----
The slices layouts can be used for stitching of the wobbly stacks.
"""
# create stitching planes
if coordinates is all or coordinates is None:
coordinates = range(self.lower[self.axis], self.upper[self.axis])
return [self.slice_along_axis_wobbly(c) for c in coordinates]
###############################################################################
### Alignment
###############################################################################
[docs]
class Verbose:
flags = {
'save' : 0b010,
'figure' : 0b100
}
def __init__(self, verbose = True, save = None, directory = None):
if isinstance(verbose, Verbose):
self.verbose = verbose.verbose
self.save = verbose.save
self.directory = verbose.directory
else:
self.verbose = verbose
self.save = save
self.directory = directory
[docs]
def has_flag(self, flag):
if flag is None:
if isinstance(self.verbose, bool):
return self.verbose
else:
return self.verbose > 0
if isinstance(self.verbose, bool):
return False
else:
return self.verbose & self.flags[flag] > 0
[docs]
def copy(self):
new = type(self)()
new.__dict__.update(self.__dict__)
return new
def __eq__(self, other):
return self.verbose == other
[docs]
def full_filename(self, filename):
if self.directory is None:
return filename
else:
return io.join(self.directory, filename)
[docs]
def create_directory(self, prefix = None):
if self.directory is None:
import datetime
directory = datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S/')
if prefix is not None:
directory = f'{prefix}_{directory}'
self.directory = directory
if not io.is_directory(self.directory):
io.create_directory(self.directory)
return self.directory
[docs]
def verbose_has_flag(verbose, flag):
return Verbose(verbose).has_flag(flag)
#TODO: use global plane wise coordinates if subsampling !
[docs]
def align_layout(layout, axis_range=None, max_shifts=10, axis_mip=None,
stack_validation_params=None, validate=None, prepare='normalization',
slice_validation_params=None, validate_slice=None, prepare_slice=None,
find_shifts='minimization',
verbose=False, processes=None, workspace= None):
if validate is not None: # FIXME: handle_deprecated_parameters
if not stack_validation_params:
stack_validation_params = validate
warnings.warn('Parameter validate is deprecated, please use stack_validation_params instead',
DeprecationWarning, stacklevel=2)
if validate_slice is not None:
if not slice_validation_params:
slice_validation_params = validate_slice
warnings.warn('Parameter validate_slice is deprecated, please use slice_validation_params instead',
DeprecationWarning, stacklevel=2)
axis = layout.axis
alignments = layout.alignments
if verbose:
timer = tmr.Timer()
print(f'Alignment: aligning {len(alignments):d} pairs of wobbly sources.')
verbose = Verbose(verbose)
if verbose.has_flag('save'):
verbose.create_directory(prefix='WobblyAlignment')
_align = ft.partial(align_wobbly_axis, axis=axis, axis_range=axis_range, axis_mip=axis_mip, max_shifts=max_shifts,
prepare=prepare, stack_validation_params=stack_validation_params,
prepare_slice=prepare_slice, slice_validation_params=slice_validation_params,
find_shifts=find_shifts,
verbose=verbose)
if not isinstance(processes, int) and processes != 'serial':
processes = mp.cpu_count()
if processes == 'serial':
results = [_align(a.pre, a.post) for a in alignments]
else:
layout.sources_as_virtual()
with CancelableProcessPoolExecutor(processes) as executor:
results = executor.map(_align, [a.pre for a in alignments], [a.post for a in alignments])
if workspace is not None:
workspace.executor = executor
if workspace is not None:
workspace.executor = None
results = list(results)
for a,r in zip(layout.alignments, results):
a.shifts = r[0]
a.qualities = r[1]
a.status = r[2]
if verbose:
timer.print_elapsed_time(f'Alignment: aligning {len(alignments):d} pairs of wobbly sources')
[docs]
@ptb.parallel_traceback
def align_wobbly_axis(source1, source2, axis=2, axis_range=None, max_shifts=10, axis_mip=None,
stack_validation_params=None, prepare='normalization', slice_validation_params=None,
prepare_slice=None, find_shifts='minimization', with_errors=False, with_overlaps=False,
verbose=True):
"""Create shifts along the wobble axis, estimate smooth shifts and mark invalid slices, accounts for jumps in minima using multiple minima."""
if verbose:
timer = tmr.Timer()
print(f'Alignment: wobbly alignment {source1.identifier!r}->{source2.identifier!r} along {axis=:d}')
# prepare methods dicts
stack_validation_params, prepare, slice_validation_params, prepare_slice, find_shifts = \
[dict(method=m) if isinstance(m, str) else m for m in (stack_validation_params, prepare, slice_validation_params, prepare_slice, find_shifts)]
if axis_mip:
if not isinstance(axis_mip, tuple):
axis_mip = (axis_mip, axis_mip)
#overlap etc
ndim = source1.ndim
p1 = source1.position
p2 = source2.position
s1 = source1.shape
s2 = source2.shape
p1a = p1[axis]
p2a = p2[axis]
start = max(p1a, p2a)
stop = min(p1a + s1[axis], p2a + s2[axis])
if start > stop:
raise ValueError(f'The sources do not overlap along {axis=:d}!')
n_slices = stop - start
# sampling
if not isinstance(axis_range, tuple):
axis_range = (axis_range,)
if len(axis_range) < 3:
axis_range += (None,) * (3-len(axis_range))
a_start, a_stop, a_step = axis_range if axis_range else (None,None,None)
a_start = start if a_start is None else a_start
a_stop = stop if a_stop is None else a_stop
a_step = 1 if a_step is None else a_step
a_start = max(start, a_start)
a_stop = min(stop, a_stop)
# max shifts formatting
max_shifts = strg._format_max_shifts(max_shifts, ndim)
max_shifts = max_shifts[:axis] + max_shifts[axis+1:]
# slices for fft
sl1 = strg.Region(position = p1[:axis] + p1[axis+1:], shape = s1[:axis] + s1[axis+1:])
sl2 = strg.Region(position = p2[:axis] + p2[axis+1:], shape = s2[:axis] + s2[axis+1:])
slice1,slice2, pad1,pad2, slice_no_pad1,slice_no_pad2, shift_min,shift_max, fft_roi = strg._slicing_and_padding_for_fft(sl1, sl2, max_shifts)
# sdim = len(shift_min);
# full slicings
slice1_full = slice1[:axis] + (slice(a_start - p1a, a_stop - p1a),) + slice1[axis:]
slice2_full = slice2[:axis] + (slice(a_start - p2a, a_stop - p2a),) + slice2[axis:]
# pad1_full = pad1[:axis] + [(0,0)] + pad1[axis:];
# pad2_full = pad2[:axis] + [(0,0)] + pad2[axis:];
i1 = np.array(source1[slice1_full], dtype=float)
i2 = np.array(source2[slice2_full], dtype=float)
# initialize the error and status results
status = WobblyAlignment.INVALID * np.ones(n_slices, dtype=int)
error_shape = (n_slices,) + tuple(-s.start if s.start is not None else s.stop for s in fft_roi)
errors = np.zeros(error_shape)
# validate entire stacks
if stack_validation_params:
valid = _validate(i1, **stack_validation_params)
if verbose and not valid:
print(f'Alignment: Source {source1.identifier!r} is not valid!')
if valid:
valid = _validate(i2, **stack_validation_params)
if verbose and not valid:
print(f'Alignment: Source {source1.identifier!r} is not valid!')
if not valid:
status[:] = WobblyAlignment.NOSIGNAL
results = _shifts_qualities_status(errors, status, correct_shift=shift_min, **find_shifts)
if with_errors:
results += (errors,)
if with_overlaps:
results += ((i1,i2),)
return results
#keep original copy for validation
if slice_validation_params:
i1raw = i1.copy()
i2raw = i2.copy()
#prepare
if prepare:
i1 = _prepare(i1, **prepare)
i2 = _prepare(i2, **prepare)
#weights
shape1 = i1.shape[:axis] + i1.shape[axis+1:]
w1 = np.pad(np.zeros(shape1), pad1, 'constant')
w1[slice_no_pad1] = 1
w1fft = np.fft.fftn(w1)
w2 = np.pad(np.zeros(shape1), pad1, 'constant') # FIXME: check pad1
w2[slice_no_pad2] = 1
w2fft = np.fft.fftn(w2)
#norm
nrm = np.fft.ifftn(w1fft * np.conj(w2fft))
nrm = np.abs(nrm[fft_roi])
eps = 2.2204e-16
nrm[nrm < eps] = eps
#align slices
for i, a in enumerate(range(start, stop)):
if verbose and i % 100 == 0:
print(f'Alignment: Wobbly alignment {source1.identifier!r}->{source2.identifier!r} '
f'along {axis=:d}: slice {i:d} / {a_stop - a_start:d}')
if a < a_start or a >= a_stop or (a-a_start) % a_step != 0:
status[i] = WobblyAlignment.UNALIGNED
continue
if axis_mip:
mip_start = max(0, a - a_start - axis_mip[0])
mip_end = max(0, a - a_start + axis_mip[1])
slice1_a = (slice(None),) * axis + (slice(mip_start,mip_end),) + (slice(None),) * (ndim-1-axis)
slice2_a = (slice(None),) * axis + (slice(mip_start,mip_end),) + (slice(None),) * (ndim-1-axis)
i1a = np.max(i1[slice1_a], axis=axis)
i2a = np.max(i2[slice2_a], axis=axis)
else:
slice1_a = (slice(None),) * axis + (a - a_start,) + (slice(None),) * (ndim-1-axis)
slice2_a = (slice(None),) * axis + (a - a_start,) + (slice(None),) * (ndim-1-axis)
i1a = i1[slice1_a]
i2a = i2[slice2_a]
if slice_validation_params:
i1rawa = i1raw[slice1_a]
valid = _validate(i1rawa, **slice_validation_params)
if verbose and not valid:
print(f'Alignment: Slice {a - a_start:d} with coordinate {a:d} in '
f'source {source1.identifier!r} is not valid!')
if valid:
i2rawa = i2raw[slice2_a]
valid = _validate(i2rawa, **slice_validation_params)
if verbose and not valid:
print(f'Alignment: Slice {a - a_start:d} with coordinate {a:d} in'
f' source {source2.identifier!r} is not valid!')
if not valid:
status[i] = WobblyAlignment.NOSIGNAL
continue
if prepare_slice:
i1a = _prepare(i1a, **prepare_slice)
i2a = _prepare(i2a, **prepare_slice)
i1a = np.pad(i1a, pad1, 'constant')
i2a = np.pad(i2a, pad2, 'constant')
# fft
i1fft = np.fft.fftn(i1a)
i2fft = np.fft.fftn(i2a)
s1fft = np.fft.fftn(i1a * i1a)
s2fft = np.fft.fftn(i2a * i2a)
wssd = w1fft * np.conj(s2fft) + s1fft * np.conj(w2fft) - 2 * i1fft * np.conj(i2fft)
wssd = np.fft.ifftn(wssd)
wssd = wssd[fft_roi]
# normalize
wssd = np.abs(wssd)
wssd = wssd / nrm
# save least square errors
errors[i] = wssd
status[i] = WobblyAlignment.MEASURED
if verbose:
timer.print_elapsed_time(f'Alignment: Wobbly slice alignment {source1.identifier!r}->{source2.identifier!r}'
f' along {axis=:d} done')
if verbose_has_flag(verbose, 'save'):
filename = verbose.full_filename(f'errors_{source1.identifier!r}_{source2.identifier!r}.npy')
np.save(filename, errors)
verbose.save = f'{source1.identifier!r}_{source2.identifier!r}'
results = _shifts_qualities_status(errors, status, add_shift=shift_min, verbose=verbose, **find_shifts)
if verbose:
timer.print_elapsed_time(f'Alignment: Wobbly alignment {source1.identifier!r}->{source2.identifier!r}'
f' along {axis=:d} done')
if with_errors:
results += (errors,)
if with_overlaps:
results += ((i1raw,i2raw),)
return results
[docs]
def prepare_normalization(array, clip = None, normalize = True):
# clip images for better alignment performance
if clip is not None:
#clip
if isinstance(clip, (list, tuple)):
if clip[0] is not None:
array[array < clip[0]] = clip[0]
if clip[1] is not None:
array[array > clip[1]] = clip[1]
else:
array[array > clip] = clip
#normalize the full image
if normalize:
array -= np.mean(array)
array *= 1.0/np.sqrt(np.sum(array*array))
return array
def _prepare(array, method='normalization', **kwargs):
if method == 'normalization':
return prepare_normalization(array, **kwargs)
else:
raise ValueError(f'Preparation method {method!r} not valid!')
[docs]
def validate_foreground(array, valid_range = (800,None), size = None, fraction = None, verbose = True):
# check if overlaps are background
if valid_range is None:
return True
low, high = valid_range
if low is None and high is None:
return True
if low is not None and high is not None:
foreground = np.sum(np.logical_and(low < array, array < high))
elif low is not None:
foreground = np.sum(low <= array)
else:
foreground = np.sum(array <= high)
if fraction is not None:
size = fraction * array.size
if size is None:
valid = foreground > 0
if verbose and not valid:
print(f'Alignment: All {array.size:d} pixels are background in range {valid_range!r}!')
else:
valid = foreground >= size
if verbose and not valid:
print(f'Alignment: Not enough foreground pixels {foreground:d} < {size:d} in range {valid_range!r}!')
return valid
def _validate(array, method='foreground', **kwargs):
if method == 'foreground':
return validate_foreground(array, **kwargs)
else:
raise ValueError(f'Validation method {method!r} not valid!')
import skimage.feature as sk_image_feature
[docs]
def detect_local_minima(error, distance=1):
minima = sk_image_feature.peak_local_max(-error, min_distance=distance, exclude_border=True)
if len(minima) > 0:
shifts = [tuple(m) for m in minima]
qualities = [error[s] for s in shifts]
else:
shifts = [(0,) * error.ndim]
qualities = [-np.inf]
return shifts, qualities
def _detect_minima(array, method='local_minima', **kwargs):
if method == 'local_minima':
return detect_local_minima(array, **kwargs)
else:
raise ValueError(f'Method {method!r} not valid for minima detection!')
[docs]
def shifts_from_minimization(errors, status):
n = len(status)
qualities = -np.inf * np.ones(n)
shifts = np.zeros((n,errors.ndim-1),dtype=int)
# find minimal shifts
for e,s,i in zip(errors, status, range(n)):
if s == WobblyAlignment.MEASURED:
shift = np.argmin(e)
shift = tuple(np.unravel_index(shift, e.shape))
shifts[i] = shift
qualities[i] = -(e[shift])
status[i] = WobblyAlignment.ALIGNED
return shifts, qualities, status
[docs]
def shifts_from_tracing(errors, status, cutoff=None, new_trajectory_cost=None, minima='local_minima', verbose=False, **kwargs):
verbose = Verbose(verbose)
#defaults
n = len(status)
qualities = -np.inf * np.ones(n)
shifts = np.zeros((n,errors.ndim-1),dtype=int)
#measured entries
measured = np.where(status == WobblyAlignment.MEASURED)[0]
if len(measured) == 0:
return shifts, qualities, status
#minima detection
mins = [_detect_minima(error, method=minima, **kwargs) for error in errors[measured]]
#invalid minima
for i,m in zip(measured, mins):
if len(m[1]) == 1 and not np.isfinite(m[1][0]):
status[i] = WobblyAlignment.NOMINIMA
#print('no min')
mins = [m for m in mins if np.isfinite(m[1][0])]
#valid regions
measured = status == WobblyAlignment.MEASURED
valids = np.logical_or(measured, status == WobblyAlignment.UNALIGNED)
valids = np.array(valids, dtype=int)
valids = np.asarray(np.pad(valids, (1,1), 'constant'))
starts = np.where(np.diff(valids) > 0)[0]
ends = np.where(np.diff(valids) < 0)[0]
#print starts, ends
if len(starts) == 0:
return shifts, qualities, status
if new_trajectory_cost is None:
new_trajectory_cost = np.sqrt(np.sum(np.power(errors[0].shape, 2)))
n_measured = 0
for s,e in zip(starts, ends):
#account for subsampling
measured_se = np.where(measured[s:e])[0]
n_measured_se = len(measured_se)
if n_measured_se == 0:
continue
positions = [mins[i][0] for i in range(n_measured, n_measured + n_measured_se)]
n_measured += n_measured_se
trajectories = trk.track_positions(positions, new_trajectory_cost=new_trajectory_cost, cutoff=cutoff)
if verbose.has_flag('figure'):
import matplotlib.pyplot as plt
fig = plt.figure(200); plt.clf()
fig.gca(projection='3d')
for t in trajectories:
plt.plot([positions[p[0]][p[1]][0] for p in t], [positions[p[0]][p[1]][1] for p in t], [p[0] for p in t])
plt.title('Tracked trajectories')
if verbose.has_flag('save'):
filename = verbose.full_filename(f'trajectories_{verbose.save}_{s:d}_{e:d}.npy')
np.save(filename, trajectories)
filename = verbose.full_filename(f'positions_{verbose.save}_{s:d}_{e:d}.npy')
np.save(filename, positions)
# successively add longer trajectories
#TODO: could search local error landscape for best error, etc
n_opt = 0
t_opt = []
while n_opt < n_measured_se:
#find longest
lens = np.array([len(t) for t in trajectories])
iopt = np.where(lens == np.max(lens))[0]
if len(iopt) > 1:
q = [np.sum([errors[t[0]][tuple(positions[t[0]][t[1]])] for t in trajectories[i]]) for i in iopt]
iopt = iopt[np.argmin(q)]
else:
iopt = iopt[0]
t_opt.append(trajectories[iopt])
n_opt += len(t_opt[-1])
# remove non-relevant trajectories
ts = t_opt[-1][0][0]
te = t_opt[-1][-1][0]
trajectories = [t for t in trajectories if (t[0][0] < ts and t[-1][0] < ts) or (t[0][0] > te and t[-1][0] > te)]
if len(trajectories) == 0:
break
if verbose.has_flag('figure'):
fig = plt.figure(201); plt.clf()
fig.gca(projection='3d')
for t in t_opt:
plt.plot([positions[p[0]][p[1]][0] for p in t], [positions[p[0]][p[1]][1] for p in t], [p[0] for p in t])
plt.title('Optimal trajectory')
if verbose.has_flag('save'):
#print(verbose.save, (s,e));
filename = verbose.full_filename(f'trajectory_opt_{verbose.save}_{s:d}_{e:d}.npy')
np.save(filename, t_opt)
#update results
measured_se += s
status[measured_se] = WobblyAlignment.UNTRACED
for t in t_opt:
for p in t:
l, m = p
i = measured_se[l]
shifts[i] = positions[l][m]
qualities[i] = -errors[i][tuple(shifts[i])]
status[i] = WobblyAlignment.ALIGNED
return shifts, qualities, status
def _shifts_qualities_status(errors, status, method='minimization', add_shift=None, **kwargs):
"""Helper to calculate shifts, qualities and status from alignment errors."""
if method is None:
method = 'minimization'
if method == 'minimization':
method = shifts_from_minimization
elif method == 'tracing':
method = shifts_from_tracing
else:
raise ValueError(f'Method {method!r} not a valid for shift detection!')
#strg.dv.plot(errors)
shifts, qualities, status = method(errors, status, **kwargs)
#print shifts, qualities, status
if add_shift is not None:
shifts = [tuple(s + m for s,m in zip(shift, add_shift)) for shift in shifts]
return shifts, qualities, status
[docs]
def inspect_align_layout(alignment, verbose):
"""Parse the information saved during an align_layout.
Returns
-------
errors : array
The error landscape for each slice.
minima : array
Coordinates of the detected minima
trajectories : list
List of coordinates of the detected trajectories.
trajectories_optimal : list
List of the optimal trajectories.
"""
verbose = Verbose(verbose)
verbose.save = f'{alignment.pre.identifier!r}_{alignment.post.identifier!r}'
#error
error_file = verbose.full_filename(f'errors_{verbose.save}.npy')
error = np.load(error_file)
#p3d.plot(error_file)
#minima
positions_expression = verbose.full_filename(te.Expression(f'positions_{verbose.save}_<s>_<e>.npy'))
positions_files = io.file_list(positions_expression)
minima = []
for p in positions_files:
values = positions_expression.values(p)
s = values['s']; e = values['e']
positions = np.load(p)
pp = np.vstack([np.array([np.array(m + (z,), dtype=int) for m in mm], dtype=int) for z,mm in zip(range(s,e),positions)])
minima.append(pp)
minima = np.vstack(minima)
#potential trajectories
trajectory_expression = verbose.full_filename(te.Expression(f'trajectories_{verbose.save}_<s>_<e>.npy'))
trajectory_files = io.file_list(trajectory_expression)
paths = []
for t in trajectory_files:
values = trajectory_expression.values(t)
s = values['s']; e = values['e']
trajectories = np.load(t)
positions = np.load(positions_expression.string(values))
z_positions = np.arange(s,e)
for trajectory in trajectories:
paths.append(np.array([np.array(positions[p[0]][p[1]] +(z_positions[p[0]],)) for p in trajectory]))
#p3d.list_line_plot_3d(paths[1])
#optimal trajectory
trajectory_expression = verbose.full_filename(te.Expression(f'trajectory_opt_{verbose.save}_<s>_<e>.npy'))
trajectory_files = io.file_list(trajectory_expression)
opt_paths = []
for t in trajectory_files:
values = trajectory_expression.values(t)
s = values['s']; e = values['e']
trajectories = np.load(t)
positions = np.load(positions_expression.string(values))
z_positions = np.arange(s,e)
for trajectory in trajectories:
opt_paths.append(np.array([np.array(positions[p[0]][p[1]] +(z_positions[p[0]],)) for p in trajectory]))
return error, minima, paths, opt_paths
###############################################################################
### Placement
###############################################################################
[docs]
def place_layout(layout, min_quality = None, method = 'optimization',
smooth = None, smooth_optimized = None, fix_isolated = True,
lower_to_origin = True, processes = None, verbose = False, workspace=None):
"""Place a layout with the WobblyAlignments."""
#prepare methods dicts
smooth, smooth_optimized = [dict(method=m) if isinstance(m, str) else m for m in (smooth, smooth_optimized)]
#place tiles in each slice first
sources = layout.sources
alignments = layout.alignments
axis = layout.axis
#TODO: fix all the upper lower etc defs to not only work with lower_to_origin layout ?
n_slices = layout.extent[axis]
n_sources = len(sources)
if n_sources == 0 or n_slices == 0:
return
if verbose:
timer = tmr.Timer()
print(f'Placement: placing positions in {n_slices:d} slices!')
# compose the slice info
source_to_index = {s : i for i,s in enumerate(sources)}
positions = np.array([s.position[:axis] + s.position[axis+1:] for s in sources])
alignment_pairs = np.array([(source_to_index[a.pre], source_to_index[a.post]) for a in alignments])
n_alignments = len(alignment_pairs)
ndim = len(positions[0])
# displacements and qualities
displacements = np.full((n_slices, n_alignments, ndim), np.nan)
qualities = np.full((n_slices, n_alignments), -np.inf)
status = np.full((n_slices, n_alignments), WobblyAlignment.INVALID, dtype = int)
for i,a in enumerate(alignments):
# fill in undersampled gaps
a.fix_unaligned()
# smooth
l = a.lower_coordinate
u = a.upper_coordinate
if smooth:
displacements[l:u,i] = a.smooth_displacements(min_quality=min_quality, **smooth)
else:
displacements[l:u,i] = a.displacements
qualities[l:u,i] = a.qualities
status[l:u,i] = a.status
#np.save('displacements.npy', displacements);
#np.save('qualities.npy', qualities);
#np.save('status.npy', status);
#place each slice
_place = ft.partial(_place_slice, positions=positions, alignment_pairs=alignment_pairs, min_quality=min_quality)
if not isinstance(processes, int) and processes != 'serial':
processes = mp.cpu_count()
if processes == 'serial':
results = [_place(d,q,s) for d,q,s in zip(displacements, qualities, status)]
else:
with CancelableProcessPoolExecutor(processes) as executor:
results = executor.map(_place, displacements, qualities, status)
if workspace is not None:
workspace.executor = executor
if workspace is not None:
workspace.executor = None
results = list(results)
positions_new = np.array([r[0] for r in results])
components = [r[1] for r in results]
#np.save('positions_new.npy', positions_new.swapaxes(0,1));
#TODO: transform status from alignments to source staus ?
if verbose:
timer.print_elapsed_time(f'Placement: placing positions in {n_slices:d} slices done!')
#mark and remove isolated tiles
for s,components_slice in enumerate(components):
for c in components_slice:
if len(c) == 1:
layout.sources[c[0]].set_isolated(coordinate = s)
components = [[c for c in components_slice if len(c) > 1] for components_slice in components]
#optimize positions
if method == 'optimization':
if verbose:
print('Placement: optimizing wobbly positions!')
positions_optimized = _optimize_slice_positions(positions_new, components, processes=processes,
workspace=workspace, verbose=verbose)
else:
if verbose:
print('Placement: combining wobbly positions!')
positions_optimized = _straighten_slice_positions(positions_new, components, layout.tile_positions)
positions_optimized = positions_optimized.swapaxes(0,1)
#np.save('positions_optimized_1.npy', positions_optimized);
#TODO: after fixing isolated !!!! or including status !!!
#smooth optimized positions
if smooth_optimized:
for p in positions_optimized:
valids = np.all(np.isfinite(p), axis=1)
#include status validation here !
p[:] = smooth_positions(p, valids=valids, **smooth_optimized)
#zero origin
if lower_to_origin:
positions_optimized_valid = np.ma.masked_invalid(positions_optimized)
min_pos = np.array(np.min(np.min(positions_optimized_valid, axis = 0), axis = 0))
positions_optimized -= min_pos
if verbose:
timer.print_elapsed_time('Placement: placing wobbly layout done!')
#np.save('positions_optimized_2.npy', positions_optimized);
layout.set_positions(positions_optimized)
if fix_isolated:
for source in layout.sources:
source.fix_isolated()
#return positions_optimized;
@ptb.parallel_traceback
def _place_slice(displacements, qualities, status, positions, alignment_pairs, min_quality=-np.inf):
positions = positions.copy()
#filter alignments by quality
valid = status >= WobblyAlignment.VALID
if min_quality:
valid = np.logical_and(valid, qualities > min_quality)
alignment_pairs = alignment_pairs[valid]
displacements = displacements[valid]
qualities = qualities[valid]
# connected components
component_ids, component_pairs, component_displacements = _connected_components(positions, alignment_pairs, displacements)
for pairs, displacement in zip(component_pairs, component_displacements):
_place_slice_component(positions, pairs, displacement)
return positions, component_ids
def _connected_components(positions, alignment_pairs, displacements):
"""Returns the connected components of the alignments."""
n_sources = len(positions)
connected_components, n_components = strg.get_connected_components(alignment_pairs, n_sources)
# create components
component_pairs = []
component_displacements = []
component_ids = []
for i in range(n_components):
ids = np.where(connected_components == i)[0]
pairs = []
displacements = []
for a,d in zip(alignment_pairs, displacements):
if a[0] in ids:
pairs.append(a)
displacements.append(d)
component_pairs.append(pairs)
component_displacements.append(displacements)
component_ids.append(ids)
return component_ids, component_pairs, component_displacements
def _place_slice_component(positions, alignment_pairs, displacements, fixed = None):
"""Optimize positions for a connected component."""
n_alignments = len(alignment_pairs)
if n_alignments == 0:
return positions
# construct the mappings between node ids and index 1:n_images
pre_indices = np.unique([p[0] for p in alignment_pairs])
post_indices = np.unique([p[1] for p in alignment_pairs])
node_to_index = np.unique(np.hstack([pre_indices, post_indices]))
index_to_node = { i : n for n,i in enumerate(node_to_index)}
nnodes = len(node_to_index)
ndim = len(positions[0])
n = ndim * n_alignments
m = ndim * (nnodes - 1) # first image is assumed to be fixed at zero
# derivative of the error gives constraints s - M x == 0
# s are the displacements, x the centers of the images, M is derived from the error terms
# s
s = np.zeros(n)
k = 0
for a, sh in zip(alignment_pairs, displacements):
for d in range(ndim):
s[k] = sh[d]
k = k + 1
# M
M = np.zeros((n,m))
k = 0
for a in alignment_pairs:
pre_node = index_to_node[a[0]]
post_node = index_to_node[a[1]]
for d in range(ndim):
if pre_node > 0:
M[k, (pre_node - 1) * ndim + d] = -1
if post_node > 0:
M[k, (post_node - 1) * ndim + d] = 1
k = k + 1
# find the centers of the images via pseudo inverse
positions_optimized = np.dot(np.linalg.pinv(M), s)
positions_optimized = np.hstack([np.zeros(ndim), positions_optimized])
positions_optimized = np.reshape(positions_optimized, (-1, ndim))
positions_optimized = np.asarray(np.round(positions_optimized), dtype = int)
# correct for origin and fixed source
if fixed is not None:
fixed_id = fixed
else:
fixed_id = np.min(alignment_pairs)
fixed_position = positions[fixed_id]
positions_optimized = positions_optimized - positions_optimized[index_to_node[fixed_id]] + fixed_position
# update positions
positions[node_to_index] = positions_optimized
def _optimize_slice_positions(positions, components, processes = None, workspace=None, verbose = False):
"""Helper to optimize the positions of the slices on top of each other"""
# Setting:
# refer to the slice components as 'clusters'
# positions is a list of the tile positions in each slice
# positions[slice, tile] is a ndim array of the tile position in slice s
# positions of non-existent tiles are set to [npinf] * ndim
# components is a list of lists indicating the clusters in each slice
# components[slice] = [cluster1, cluster2, ...]
# each cluster is a list of tile ids. tiles not in a slice are not listed.
# Optimization:
# minimize displacements of all sources between the slices
n_slices = len(components)
ndim = len(positions[0,0])
# compute connected components of the clusters
cluster_components_, si_to_c, c_to_si = cluster_components(components)
# cluster_components_ is a list of lists of ints indicating the cluster ids
# that belong to the connected components of the clusters
# cluster_components_[0] = [c1, c2, ...] with cluster ids c1,c2,...
n_components = len(cluster_components_)
if verbose:
print(f'Placement: found {n_components} components to optimize!')
# optimize positions for each cluster component
for cci, cluster_component in enumerate(cluster_components_):
#Error functon:
# E = \sum_s \sum_{i \in C_s} \sum_{j \in C_{s+1}} \sum_{k\in C_{s,i} \cup C_{s+1,j}} (x_{s, k} + s_{s,i} - (x_{s+1,k} + s_{s+1,j}))^2
# x_{s, k} is the position of the k-th tile in the s-th slice
# s_{s,i} is the shift of the i-th cluster C_{s,i} in slice s
# C_s is the set of clusters in slice s
# s0 = argmin_s(|C_s|>0), i0 = argmin(C_\bar{s}) is the first cluster
# s_{s0,i0} = 0 is fixed as the overall shift is arbitrary otherwise.
# derivative of error gives constraints x - M s == 0
# and cluster shifts are given as the pseudo inverse: s = M^\dagger x
#Notation:
# slice indices: t = s+1, r = s-1
# C_{s,i} has an id c, c_to_si and si_to_c convert between s,i and c
# The clusters in this connected component of clusters are enumerated by d
# starting at the second cluster as the first cluster's shift is fixed
# d_to_si, si_to_d convert between them.
n_clusters = len(cluster_component)
n_s = (n_clusters - 1) # first s == 0
if verbose:
print(f'Placement: optimizing component {cci:d}/{n_components:d} with {n_clusters:d} clusters!')
#construct map : slice -> cluster ids
slice_to_cluster_ids = [()] * n_slices
for c in cluster_component:
s,i = c_to_si(c)
slice_to_cluster_ids[s] += (i,)
#construct generic id d to si maps
si_to_d = {}
d_to_si = {}
for d,c in enumerate(cluster_component[1:]):
s,i = c_to_si(c)
si_to_d[(s,i)] = d
d_to_si[d] = (s,i)
s0,i0 = c_to_si(cluster_component[0])
# construct x, M
X = [io.sma.zeros(n_s) for d in range(ndim)]
M = [io.sma.zeros((n_s, n_s)) for d in range(ndim)]
for ci, c in enumerate(cluster_component[1:]):
#if verbose and ci % 100 == 0:
# print('Placement: constructing constraints %d/%d!' % (ci, n_clusters))
s,i = c_to_si(c)
C_si = components[s][i]
d = si_to_d[(s,i)]
#print s,i,C_si,d
#if s < n_slices - 1:
#for c2 in cluster_component:
t = s + 1
if t < n_slices:
for j in slice_to_cluster_ids[t]:
C_tj = components[t][j]
is_first = s0 == t and i0 == j
if not is_first:
f = si_to_d[(t,j)]
#print t,j,C_tj,is_first,f
for k in C_si:
if k in C_tj:
for e in range(ndim):
X[e][d] += positions[s,k,e] - positions[t,k,e]
M[e][d,d] += 1
if not is_first:
M[e][d,f] -= 1
r = s - 1
if r >= 0:
for j in slice_to_cluster_ids[r]:
C_rj = components[r][j]
is_first = s0 == r and i0 == j
if not is_first:
f = si_to_d[(r,j)]
for k in C_si:
if k in C_rj:
for e in range(ndim):
X[e][d] -= positions[r,k,e] - positions[s,k,e]
M[e][d,d] += 1
if not is_first:
M[e][d,f] -= 1
if verbose:
print(f'Placement: done constructing constraints for component {cci:d}/{n_components:d}!')
# find the shifts of the clusters via pseudo inverse
#print X
#print M
#print np.linalg.pinv(-M)
if isinstance(processes, int) and processes > 1:
M = [io.sma.smm.insert(m) for m in M]
X = [io.sma.smm.insert(x) for x in X]
with CancelableProcessPoolExecutor(min(processes, ndim)) as executor:
shifts = executor.map(_optimize_shifts, M, X)
if workspace is not None:
workspace.executor = executor
if workspace is not None:
workspace.executor = None
shifts = list(shifts)
shifts = np.array(shifts).T
else:
shifts = [np.linalg.lstsq(-M[e], X[e], rcond=None)[0] for e in range(ndim)]
shifts = np.asarray(np.round(shifts), dtype=int).T
#update positions of the tiles
for c in cluster_component[1:]:
s,i = c_to_si(c)
C_si = components[s][i]
d = si_to_d[(s,i)]
#print s,i,d,C_si, shifts[d]
for k in C_si:
positions[s,k] += shifts[d]
if verbose:
print(f'Placement: component {cci:d}/{n_components:d} optimized!')
# note overall shifts between components is not touched but might be based on
# keeping overall distance.
return positions
def _optimize_shifts(MM, XX):
M = io.sma.smm.get(MM)
X = io.sma.smm.get(XX)
#ss = np.dot(np.linalg.pinv(-M), X);
ss = np.linalg.lstsq(-M, X, rcond=None)[0]
#ss = scipy.sparse.linalg.lsqr(-M, X)[0];
io.sma.smm.free(MM)
io.sma.smm.free(XX)
return np.asarray(np.round(ss), dtype=int)
def _straighten_slice_positions(positions, components, tile_positions):
"""Straighten the center tiles in each connected cluster component"""
#The cluster components always split between different tiles
#so we can straighten the center tile in each cluster component.
n_slices = len(components)
#compute connected components of the clusters
cluster_components_, si_to_c, c_to_si = cluster_components(components)
for cluster_component in cluster_components_:
slice_ids = []
for c in cluster_component:
s,i = c_to_si(c)
if s not in slice_ids:
slice_ids.append(s)
C_si = components[s][i]
tile_ids = []
tile_pos = []
for k in C_si:
if k not in tile_ids:
tile_pos.append(tile_positions[k])
tile_ids.append(k)
center_tile_position = strg._center_tile(tile_pos)
for i,t in enumerate(tile_pos):
if center_tile_position == t:
center_tile = i
break
center_slice = slice_ids[(len(slice_ids)-1)//2]
center_position = positions[center_slice, center_tile]
for k in tile_ids:
for s in range(n_slices):
positions[s,k] += center_position - positions[s,center_tile]
return positions
[docs]
def smooth_binary(x, width=1):
"""Remove displacements smaller than a certain width."""
width = width + 1 #width -> range
if len(x) < width:
width = len(x)
x = x.copy()
n = len(x)
#smooth open border
x[:width] = np.median(x[:width])
x[-width:] = np.median(x[-width:])
for w in range(width,1,-1):
starts = range(n-w)
ends = range(w,n)
for s,e in zip(starts, ends):
if x[s] == x[e]:
x[s:e] = x[s]
return x
[docs]
def smooth_window(x, window_length = 10, window = 'bartlett', binary = None):
"""Convolutional smoothing filter"""
if window_length > len(x):
window_length = len(x)
if window:
windows = ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']
if not window in windows:
raise ValueError(f'Window not in {windows!r}!')
if window == 'flat': #moving average
w = np.ones(window_length)
else:
w = getattr(np, window)(window_length)
w /= w.sum()
x = np.pad(x, (window_length, window_length), 'edge')
y = np.convolve(w, x, mode='same')[window_length:-window_length]
y = np.array(np.round(y), dtype = int)
else:
y = x.copy()
if binary:
y = smooth_binary(y, width=binary)
return y
[docs]
def smooth_positions(positions, valids, method = 'window', **kwargs):
"""Smooth positions in valid regions."""
return smooth_displacements(positions, valids=valids, method=method, **kwargs)
[docs]
def smooth_displacements(displacements, valids, method = 'window', **kwargs):
"""Smooth displacements in valid regions."""
displacements_smooth = displacements.copy()
if method is None:
return displacements_smooth
#find valid slices
valids = np.asarray(np.pad(valids, (1,1), 'constant'), dtype = int)
starts = np.where(np.diff(valids) > 0)[0]
ends = np.where(np.diff(valids) < 0)[0]
if method == 'window':
smooth = ft.partial(smooth_window, **kwargs)
else:
raise ValueError(f'Smoothing {method=!r} not valid!')
#smooth each interval
ndim = displacements.ndim
for s,e in zip(starts, ends):
for d in range(ndim):
smooth_displacements = smooth(displacements[s:e,d])
displacements_smooth[s:e,d] = smooth_displacements
return displacements_smooth
[docs]
def fix_unaligned(displacements, status, qualities):
"""Linearly interpolate between unaligned coordinates"""
n_status = len(status)
unaligned = np.array(status == WobblyAlignment.UNALIGNED, dtype=int)
unaligned = np.pad(unaligned, (1,1), 'constant')
delta = np.diff(unaligned)
starts = np.where(delta > 0)[0]
ends = np.where(delta < 0)[0]
# whole stack is aligned
if len(starts) == 0:
return displacements, status
# whole stack is unaligned
if len(starts) == 1 and starts[0] == 0 and len(ends) == 1 and ends[0] == n_status:
status[:] = WobblyAlignment.INVALID
return displacements, status
#find left and right bounds for isolated stretches
for s,e in zip(starts, ends):
#find next valid in each direction
if s > 0 and status[s-1] >= WobblyAlignment.VALID:
left = displacements[[s-1]]
else:
left = None
if e < n_status and status[e] >= WobblyAlignment.VALID:
right = displacements[[e]]
else:
right = None
if left is None and right is None:
status[s:e] = WobblyAlignment.INVALID
else:
if left is None:
displacements[s:e] = right
qualities[s:e] = qualities[e]
elif right is None:
displacements[s:e] = left
qualities[s:e] = qualities[s-1]
else: # linearly interpolate
displacements[s:e] = np.array(np.round((right-left) * 1.0 / (e-s+1) * np.arange(1, e-s+1)[:, np.newaxis] + left), dtype = int)
qs = qualities[s-1]
qe = qualities[e]
if np.isfinite(qs) and np.isfinite(qe):
qualities[s:e] = (qe - qs) / (e-s+1) * np.arange(1, e-s+1) + qs
elif np.isfinite(qe):
qualities[s:e] = qe
else:
qualities[s:e] = qs
status[s:e] = WobblyAlignment.FIXED
return displacements, status
#TODO: fix this clean up placement in total including status info
[docs]
def fix_isolated(self, exclude_borders=False):
"""Fix the positons of isolated slices."""
status = self.status
wobble = self.wobble
n_status = len(status)
isolated = np.array(status == self.ISOLATED, dtype=int)
isolated = np.pad(isolated, (1,1), 'constant')
delta = np.diff(isolated)
starts = np.where(delta > 0)[0]
ends = np.where(delta < 0)[0]
#whole stack has no isolated slices
if len(starts) == 0:
return
#if whole stack is isolated
if len(starts) == 1 and starts[0] == 0 and len(ends) == 1 and ends[0] == n_status:
status[:] = self.ISOLATED
return
#find left and right bounds for isolated stretches
for s,e in zip(starts, ends):
#exclude borders
if exclude_borders:
if s == 0 or e == n_status:
status[s:e] = self.ISOLATED
continue
#find next valid in each direction
if s > 0 and status[s-1] >= self.VALID:
left = wobble[[s-1]]
else:
left = None
if e < n_status and status[e] >= self.VALID:
right = wobble[[e]]
else:
right = None
if left is None and right is None:
status[s:e] = self.ISOLATED
else:
if left is None:
wobble[s:e] = right
elif right is None:
wobble[s:e] = left
else: # linearly interpolate
wobble[s:e] = np.array(np.round((right-left) * 1.0 / (e-s+1) * np.arange(1, e-s+1)[:, np.newaxis] + left), dtype = int)
status[s:e] = self.FIXED
###############################################################################
### Stitching
###############################################################################
[docs]
def stitch_layout(layout, sink, method = 'interpolation', processes = None, verbose = True, workspace=None):
"""Stitches the wobbly sources in a wobbly layout.
Arguments
---------
layout: WobblyLayout class
The layout of the stacks to stitch.
method : 'interpolation', 'max', 'min', 'mean'
The method to use to stitch the sources.
processes : int or 'serial'
Number of processor to use for parallel processing, if 'serial' process in serial.
verbose : bool
If True, print progress information.
Returns
-------
layout : Layout
The layout with updated z-alignments.
"""
#if not isinstance(layout, WobblyLayout):
# raise ValueError('Expecting a WobblyLayout instance as first argument!');
if verbose:
timer = tmr.Timer()
print('Stitching: stitching wobbly layout.')
#overall shape
axis = layout.axis
origin = layout.origin_wobbly
shape = layout.shape_wobbly
# create sink
#TODO: make layout a sink ! use io.create
io.mmp.create(sink, shape=shape, dtype=layout.dtype, order=layout.order)
# create slices
coordinates = np.arange(origin[axis], origin[axis] + shape[axis])
layout_slices = layout.layouts_along_axis_wobbly(coordinates)
n_slices = len(layout_slices)
if verbose:
timer = tmr.Timer()
print(f'Stitching: stitching {n_slices:d} sliced layouts.')
#sliced origin and shape
full_region = strg.Region(position = origin[:axis] + origin[axis+1:], shape = shape[:axis] + shape[axis+1:])
_stitch = ft.partial(_stitch_slice, n_slices=n_slices, sink=sink, method=method,
axis=axis, full_region=full_region, verbose=verbose)
# stitch the data
if not isinstance(processes, int) and processes != 'serial':
processes = mp.cpu_count()
if processes == 'serial':
[_stitch(l,i) for i,l in enumerate(layout_slices)]
else:
#for l in layout_slices:
# l.sources_as_virtual();
with CancelableProcessPoolExecutor(processes) as executor:
results = executor.map(_stitch, layout_slices, range(n_slices))
if workspace is not None:
workspace.executor = executor
if workspace is not None:
workspace.executor = None
if verbose:
timer.print_elapsed_time('Stitching: parallel stitching wobbly layout done. Gathering results!')
_ = list(results)
if verbose:
timer.print_elapsed_time('Stitching: stitching wobbly layout done!')
return sink
@ptb.parallel_traceback
def _stitch_slice(slice_layout, slice_id, n_slices, sink, method, axis, full_region, verbose):
if verbose:
print(f'Stitching: stitching wobbly slice {slice_id:d}/{n_slices:d}')
if len(slice_layout.sources) == 0:
return
# slicings
slice_region = strg.Region(lower = slice_layout.origin, upper = slice_layout.upper)
overlap = strg._overlap(slice_region, full_region)
if overlap is None:
return
overlap.sources = [slice_region, full_region]
slice_slicing, full_slicing = overlap.source_slicings()
full_slicing = full_slicing[:axis] + (slice_id,) + full_slicing[axis:]
#stitch
stitched = strg.stitch_layout(slice_layout, method = method)
io.write(sink, stitched[slice_slicing], slicing = full_slicing)
#############################################################################################################
### Tests
#############################################################################################################
def _test():
import ClearMap.Alignment.Stitching.stitching_wobbly as stw
from importlib import reload
reload(stw)
# create some wobbly tiles
import numpy as np
import ClearMap.Tests.Files as tfs
data = np.load(tfs.vasculature_pre)[:,:100,:100]
# linear wobble
nz = 3
data1 = data[:120,:,:nz]
data2 = np.zeros((100,100,nz), dtype = data.dtype)
wobble = []
for s in range(nz):
x = 2 * s
data2[:,:,s] = data[100+x:200+x,:,s]
wobble.append((x,0))
wobble = np.array(wobble)
import matplotlib.pyplot as plt
plt.figure(1); plt.clf()
plt.plot(wobble[:,0])
l = stw.WobblyLayout([data1, data2], overlaps = 20)
stw.align_layout(l, max_shifts = 20, verbose = True, processes = 'serial', stack_validation_params= True, find_shifts ='tracing')
a = l.alignments[0]
a.plot_overlay_wobbly()
stw.place_layout(l, method='!optimization', smooth = None, lower_to_origin=True, verbose = True, processes = 'serial')
s = stw.stitch_layout(l, sink = 'test.npy', method='max', processes = 'serial')
stw.strg.p3d.plot(s)
#true if not optimized
np.all(stw.io.open_ro(s)[:190,:,:] == data[:190,:,:nz])
plt.figure(2); plt.clf()
for s in l.sources:
plt.plot(s.wobble[:,0] - np.min(s.wobble[:,0]))
plt.plot(wobble[:,0] - np.min(wobble[:,0]))
np.all(l.sources[1].wobble[:,0] - 100 == wobble[:,0] )
stw.strg.p3d.plot(s)
# Sin wobble
import numpy as np
import ClearMap.Tests.Files as tfs
data = np.load(tfs.vasculature_pre)[:,:100,:100]
nz = 30
data1 = data[:120,:,:nz]
data2 = np.zeros((100,100,nz), dtype = data.dtype)
wobble = []
for s in range(nz):
x = int(10 * np.sin(s * 2 * np.pi/30))
data2[:,:,s] = data[100+x:200+x,:,s]
wobble.append((x,0))
wobble = np.array(wobble)
import matplotlib.pyplot as plt
plt.figure(1); plt.clf()
plt.plot(wobble[:,0])
reload(stw.strg)
reload(stw)
l = stw.WobblyLayout([data1, data2], overlaps = 20)
stw.align_layout(l, max_shifts = 20, verbose = True, processes = 'serial', stack_validation_params= False)
stw.place_layout(l, method='!optimization', lower_to_origin=True, smooth = None, verbose = True, processes = 'serial')
s = stw.stitch_layout(l, sink = 'test.npy', method='max', processes = 'serial')
plt.figure(2); plt.clf()
for s in l.sources:
plt.plot(s.wobble[:,0] - np.min(s.wobble[:,0]))
plt.plot(wobble[:,0] - np.min(wobble[:,0]))
# True for non-optimized placements
np.all(stw.io.open_ro(s)[:190,:,:] == data[:190,:,:nz])
# wobble + axis alignment
import ClearMap.Alignment.Stitching.stitching_wobbly as stw
reload(stw.strg)
reload(stw)
import numpy as np
import ClearMap.Tests.Files as tfs
data = np.load(tfs.vasculature_pre)[:,:100,:100]
nz = 30; sh = 5
data1 = data[:120,:,:nz]
data2 = np.zeros((100,100,nz), dtype = data.dtype)
wobble = []
for s in range(nz):
x = int(10 * np.sin(s * 2 * np.pi/30))
data2[:,:,s] = data[100+x:200+x,:,s+sh]
wobble.append((x,0))
wobble = np.array(wobble)
import matplotlib.pyplot as plt
plt.figure(1); plt.clf()
plt.plot(wobble[:,0])
reload(stw.strg)
reload(stw)
l = stw.WobblyLayout([data1, data2], overlaps = 20)
stw.strg.align_layout_axis(l, axis=2, depth=25, max_shifts=10, clip=None, background=None, processes=None, verbose=True)
l.alignments
plt.figure(10); plt.clf()
l.alignments[0].plot_mip(depth = 10, max_shifts = [(-30,30),(-30,30),(-20,20)])
stw.strg.place_layout_axis(l, axis = 2, method = 'optimization', min_quality = -np.inf, lower_to_origin = True, verbose = True)
l.sources
stw.align_layout(l, max_shifts = 20, verbose = True, processes = '!serial', stack_validation_params= False, axis_range = (None, None, 3))
a = l.alignments[0]
a.plot_overlay_wobbly()
plt.figure(10); plt.clf()
plt.plot(l.alignments[0].displacements[:,0])
stw.place_layout(l, method = 'optimization', lower_to_origin=True, smooth = None, min_quality=-np.inf, processes = '!serial', verbose = True)
s = stw.stitch_layout(l, sink = 'test.npy', method='max', processes = 'serial')
stw.strg.dv.plot(s)
# True for non-optimized placements
np.all(stw.io.open_ro(s)[:190,:,sh:nz] == data[:190,:,sh:nz])
plt.figure(2); plt.clf()
for s in l.sources:
plt.plot(s.wobble[:,0] - np.min(s.wobble[:,0]))
plt.plot(wobble[:,0] - np.min(wobble[:,0]))
# wobble + axis alignment + status
import ClearMap.Alignment.Stitching.stitching_wobbly as stw
reload(stw.strg)
reload(stw)
import numpy as np
import ClearMap.Tests.Files as tfs
data = np.load(tfs.vasculature_pre)[:,:100,:100]
nz = 50; sh = 5
data1 = data[:120,:,:nz]
data2 = np.zeros((100,100,nz), dtype = data.dtype)
wobble = np.zeros((nz+sh,2), dtype=int)
for s in range(nz):
x = int(10 * np.sin(s * 2 * np.pi/40))
data2[:,:,s] = data[100+x:200+x,:,s+sh]
wobble[s+sh] = (x,0)
invalid = [13,14,15,16,47,48,49]
for s in invalid:
data2[:,:,s] = 0
import matplotlib.pyplot as plt
plt.figure(1); plt.clf()
plt.plot(wobble[:,0])
plt.plot(invalid, np.zeros(len(invalid)), '*', c = 'r')
reload(stw.strg)
reload(stw)
l = stw.WobblyLayout([data1, data2], overlaps = 20); l.sources[1].position = (100,0,sh)
def plot_status(a, fig = 2):
sm = a.smooth_displacements(min_quality = -np.inf, method='window', window='bartlett', window_length=10)
plt.figure(fig); plt.clf()
ax = plt.subplot(2,2,1)
arange = np.arange(a.lower_coordinate, a.upper_coordinate)
for i,d in enumerate(([a.status], [a.qualities], [wobble[arange,0], a.shifts[:,0], sm[:,0]], [wobble[arange,1], a.shifts[:,1], sm[:,1]])):
plt.subplot(2,2,i+1, sharex = ax)
for dd in d:
#plt.plot(arange, dd)
plt.plot(dd)
stw._validate(data2[:,:,13], **dict(method='foreground', valid_range = (1, None), size = None) )
reload(stw.strg)
reload(stw)
l = stw.WobblyLayout([data1, data2], overlaps = 20); l.sources[1].position = (100,0,sh)
stw.align_layout(l, max_shifts = 15, axis_range = (None, None, 1), axis_mip = 1,
stack_validation_params= None,
prepare = 'normalization',
slice_validation_params= dict(method='foreground', valid_range = (1, None), size = None),
find_shifts = dict(method='tracing', cutoff=np.sqrt(2 * 3**2), debug = True),
verbose = True, processes = 'serial')
a = l.alignments[0]
plot_status(a, fig=2)
a.status[a.status < 0] = stw.WobblyAlignment.UNALIGNED
a.fix_unaligned()
plot_status(a, fig=3)
a = l.alignments[0]
results = stw.align_wobbly_axis(a.pre, a.post, axis_range=(None, None, 1), max_shifts=20, axis_mip=None,
stack_validation_params=None, prepare='normalization',
slice_validation_params=dict(method='foreground', valid_range=(1, None), size=None),
find_shifts='minimization', with_errors=True, with_overlaps=True, verbose=True)
shifts, qualities, status, errors, overlaps = results
stw.strg.dv.plot((errors.transpose([1,2,0]),) + overlaps)
a.plot_overlay_wobbly()
stw.place_layout(l, method = '!optimization', lower_to_origin=True, min_quality=-np.inf,
smooth = None, smooth_optimized = None,
processes = '!serial', verbose = True)
# plot the positions of the stacks
import matplotlib.pyplot as plt
fig = plt.figure(200); plt.clf()
fig.gca(projection='3d')
for s in l.sources:
plt.plot(s.wobble[:,0], s.wobble[:,1], np.arange(s.coordinate, s.coordinate + s.height))
plt.title('Source positions')
plt.figure(300); plt.clf()
plt.plot(wobble[:,0])
for i,s in enumerate(l.sources):
plt.plot(s.wobble[:,0], label=f'{i:d}')
plt.legend()
#non alignable planes
flat = [28,29,30]
for s in flat:
data1[:,:,s] = 10
data2[:,:,s] = 10
stw.strg.dv.plot([data1[:,:,sh:], data2[:,:,:-sh]])
reload(stw.strg)
reload(stw)
l = stw.WobblyLayout([data1, data2], overlaps = 20); l.sources[1].position = (100,0,sh)
stw.align_layout(l, max_shifts=20, axis_range=(None, None, 1),
stack_validation_params=None,
prepare='normalization',
slice_validation_params=dict(method='foreground', valid_range=(1, None), size=None),
find_shifts = dict(method='tracing', cutoff=np.sqrt(2 * 3**2), debug = False),
verbose = True, processes = 'serial')
stw.place_layout(l, method = '!optimization', lower_to_origin=True, min_quality=-np.inf,
smooth = None, smooth_optimized = dict(method='window', window_length=10, binary = 2),
processes = '!serial', verbose = True)
s = stw.stitch_layout(l, sink = 'test.npy', method='max', processes = 'serial')
stw.strg.dv.plot(s)
# True for non-optimized placements
np.all(stw.io.open_ro(s)[:190,:,sh:nz] == data[:190,:,sh:nz])
s = l.slice_along_axis_wobbly(32)
t = stw.strg.stitch_layout(s, sink = None, method = 'max')
stw.strg.dv.plot(t)
plt.figure(10); plt.clf()
plt.imshow(t.T, origin='lower')
plt.figure(2); plt.clf()
for s in l.sources:
plt.plot(np.arange(s.coordinate, s.coordinate + s.height), s.wobble[:,0] - np.min(s.wobble[:,0]))
plt.plot(wobble[:,0] - np.min(wobble[:,0]))
#TODO: min_overlap parameter in alignment to avoid boundary effects
#TODO: option to reduce the shape of the overlaps used for alignment to speed things up
### Test on real data
import numpy as np
import ClearMap.IO.IO as io
import ClearMap.Alignment.Stitching.stitching_rigid as stg
import ClearMap.Alignment.Stitching.stitching_wobbly as stw
import ClearMap.IO.Workspace as wsp
directory = '/home/ckirst/Science/Projects/WholeBrainClearing/Vasculature/Experiment/Stitching_2018_06'
expression = 'tiny_[<Y,2> x <X,2>]_C00.ome.npy'
ws = wsp.Workspace(name = 'test', directory = directory, expression=expression)
io.file_list(ws.filename('expression'))
l = stw.WobblyLayout(expression = ws.filename('expression'), tile_axes = ['X', 'Y'], overlaps = (25, 155))
# rigid alignment
lr = stg.TiledLayout(expression = ws.filename('expression'), tile_axes = ['X', 'Y'], overlaps = (45, 155))
lr.alignments[0].plot_overlap()
stg.align_layout_rigid_mip(lr, depth=[55, 165, None], max_shifts=[(-30,30),(-30,30),(-20,20)],
ranges = [None,None,None], background=(1000, 100), clip = 25000,
verbose=True, processes='!serial')
lr.alignments[0].plot_overlay()
stg.place_layout(lr, method='optimization', min_quality=-np.inf, lower_to_origin=True, verbose=True)
lr.alignments[0].plot_overlay()
# plot result
lr.plot_alignments()