#
# selection.py - The Selection class.
#
# Author: Paul McCarthy <pauldmccarthy@gmail.com>
#
"""This module provides the :class:`Selection` class, which represents a
selection of voxels in a 3D :class:`.Image`.
"""
import logging
import collections.abc as abc
import numpy as np
import scipy.ndimage.measurements as ndimeas
import fsl.utils.notifier as notifier
import fsleyes.gl.routines as glroutines
log = logging.getLogger(__name__)
[docs]class Selection(notifier.Notifier):
"""The ``Selection`` class represents a selection of voxels in a 3D
:class:`.Image`. The selection is stored as a ``numpy`` mask array,
the same shape as the image. Methods are available to query and update
the selection.
Changes to a ``Selection`` can be made through *blocks*, which are 3D
cuboid regions. The following methods allow a block to be
selected/deselected, where the block is specified by a voxel coordinate,
and a block size:
.. autosummary::
:nosignatures:
selectBlock
deselectBlock
The following methods offer more fine grained control over selection
blocks - with these methods, you pass in a block that you have created
yourself, and an offset into the selection, specifying its location:
.. autosummary::
:nosignatures:
setSelection
replaceSelection
addToSelection
removeFromSelection
A third approach to making a selection is provided by the
:meth:`selectByValue` method, which allows a selection to be made
in a manner similar to a *bucket fill* technique found in any image
editor.
The related :meth:`invertRegion` method, given a seed location, will
invert the selected state of all voxels adjacent to that location. This
approach allows a *fill holes* type approach, where a region outline is
delineated, and then the interior inverted to select it.
A ``Selection`` object keeps track of the most recent change made through
any of the above methods. The most recent change can be retrieved through
the :meth:`getLastChange` method. The ``Selection`` class inherits from
the :class:`.Notifier` class - you can be notified whenever the selection
changes by registering as a listener.
Finally, the ``Selection`` class offers a few other methods for
convenience:
.. autosummary::
:nosignatures:
getSelection
getSelectionSize
clearSelection
getBoundedSelection
getIndices
"""
[docs] def __init__(self, image, display, selection=None):
"""Create a ``Selection`` instance.
:arg image: The :class:`.Image` instance associated with this
``Selection``.
:arg display: The :class:`.Display` instance for the ``image``.
:arg selection: Selection array. If not provided, one is created.
Must be a ``numpy.uint8`` array with the same shape
as ``image``. This array is *not* copied.
"""
self.__image = image
self.__display = display
self.__opts = display.opts
self.__clear = True
self.__lastChangeOffset = None
self.__lastChangeOldBlock = None
self.__lastChangeNewBlock = None
if selection is None:
selection = np.zeros(image.shape[:3], dtype=np.uint8)
elif selection.shape != image.shape[:3] or \
selection.dtype != np.uint8:
raise ValueError('Incompatible selection array: {} ({})'.format(
selection.shape, selection.dtype))
self.__selection = selection
log.debug('{}.init ({})'.format(type(self).__name__, id(self)))
[docs] def __del__(self):
"""Prints a log message."""
if log:
log.debug('{}.del ({})'.format(type(self).__name__, id(self)))
@property
def shape(self):
"""Returns the selection shape. """
return self.__selection.shape
[docs] def getSelection(self):
"""Returns the selection array.
.. warning:: Do not modify the selection array directly - use the
``Selection`` instance methods
(e.g. :meth:`setSelection`) instead. If you modify the
selection directly through this attribute, the
:meth:`getLastChange` method, and selection notification,
will break.
"""
return self.__selection
[docs] def selectBlock(self,
voxel,
boxSize,
axes=(0, 1, 2),
bias=None,
combine=False):
"""Selects the block (sets all voxels to 1) specified by the given
voxel and block size. See the :func:`.routines.voxelBlock` function
for details on the arguments.
:arg combine: Combine this change with the previous stored change
(see :meth:`__storeChange`).
"""
block, offset = glroutines.voxelBlock(
voxel,
self.__selection.shape,
boxSize,
bias=bias,
axes=axes)
self.addToSelection(block, offset, combine)
[docs] def deselectBlock(self,
voxel,
boxSize,
axes=(0, 1, 2),
bias=None,
combine=False):
"""De-selects the block (sets all voxels to 0) specified by the given
voxel and box size. See the :func:`.routines.voxelBlock` function
for details on the arguments.
:arg combine: Combine this change with the previous stored change
(see :meth:`__storeChange`).
"""
block, offset = glroutines.voxelBlock(
voxel,
self.__selection.shape,
boxSize,
bias=bias,
axes=axes)
self.removeFromSelection(block, offset, combine)
[docs] def setSelection(self, block, offset, combine=False):
"""Copies the given ``block`` into the selection, starting at
``offset``.
:arg block: A ``numpy.uint8`` array containing a selection.
:arg offset: Voxel coordinates specifying the block location.
:arg combine: Combine this change with the previous stored change (see
:meth:`__storeChange`).
"""
self.__updateSelectionBlock(block, offset, combine)
[docs] def replaceSelection(self, block, offset, combine=False):
"""Copies the given ``block`` into the selection, starting at
``offset``.
:arg block: A ``numpy.uint8`` array containing a selection.
:arg offset: Voxel coordinates specifying the block location.
:arg combine: Combine this change with the previous stored change (see
:meth:`__storeChange`).
"""
self.__updateSelectionBlock(block, offset, combine)
[docs] def addToSelection(self, block, offset, combine=False):
"""Adds the selection (via a boolean OR operation) in the given
``block`` to the current selection, starting at ``offset``.
:arg block: A ``numpy.uint8`` array containing a selection.
:arg offset: Voxel coordinates specifying the block location.
:arg combine: Combine this change with the previous stored change (see
:meth:`__storeChange`).
"""
existing = self.__getSelectionBlock(block.shape, offset)
block = np.logical_or(block, existing)
self.__updateSelectionBlock(block, offset, combine)
[docs] def removeFromSelection(self, block, offset, combine=False):
"""Clears all voxels in the selection where the values in ``block``
are non-zero.
:arg block: A ``numpy.uint8`` array containing a selection.
:arg offset: Voxel coordinates specifying the block location.
:arg combine: Combine this change with the previous stored change (see
:meth:`__storeChange`).
"""
existing = self.__getSelectionBlock(block.shape, offset)
existing[block != 0] = False
self.__updateSelectionBlock(existing, offset, combine)
[docs] def getSelectionSize(self):
"""Returns the number of voxels that are currently selected. """
return self.__selection.sum()
[docs] def getBoundedSelection(self):
"""Extracts the smallest region from the :attr:`selection` which
contains all selected voxels.
Returns a tuple containing the region, as a ``numpy.uint8`` array, and
the coordinates specifying its location in the full :attr:`selection`
array.
.. warning:: This method is slow, and in many cases it may be
faster simply to access the full selection array.
"""
xs, ys, zs = np.where(self.__selection > 0)
if len(xs) == 0:
return np.array([]).reshape((0, 0, 0)), (0, 0, 0)
xlo = int(xs.min())
ylo = int(ys.min())
zlo = int(zs.min())
xhi = int(xs.max() + 1)
yhi = int(ys.max() + 1)
zhi = int(zs.max() + 1)
selection = self.__selection[xlo:xhi, ylo:yhi, zlo:zhi]
return selection, (xlo, ylo, zlo)
[docs] def clearSelection(self, restrict=None, combine=False):
"""Clears (sets to 0) the entire selection, or the selection specified
by the ``restrict`` parameter, if it is given.
.. note:: Calling this method when the selection is already empty
will clear the most recently stored change - see
:meth:`getLastChange`.
:arg restrict: An optional sequence of three ``slice`` objects,
specifying the portion of the selection to clear.
:arg combine: Combine this change with the previous stored change (see
:meth:`__storeChange`).
"""
if self.__clear:
self.setChange(None, None)
return
fRestrict = fixSlices(restrict)
offset = [r.start if r.start is not None else 0 for r in fRestrict]
log.debug('Clearing selection ({}): {}'.format(id(self), fRestrict))
block = np.array(self.__selection[fRestrict])
self.__selection[fRestrict] = False
self.__storeChange(block,
np.array(self.__selection[fRestrict]),
offset,
combine)
# Set the internal clear flag to True,
# when the entire selection has been
# cleared, so we can skip subsequent
# redundant clears.
if restrict is None:
self.__clear = True
self.notify()
[docs] def getLastChange(self):
"""Returns the most recent change made to this ``Selection``.
A tuple is returned, containing the following:
- A ``numpy.uint8`` array containing the old block value
- A ``numpy.uint8`` array containing the new block value
- Voxel coordinates denoting the block location in the full
:attr:`selection` array.
If there is no stored change this method will return ``(None, None,
None)`` (see also the note in :meth:`clearSelection`).
"""
return (self.__lastChangeOldBlock,
self.__lastChangeNewBlock,
self.__lastChangeOffset)
[docs] def setChange(self, block, offset, oldBlock=None):
"""Sets/overwrites the most recently saved change made to this
``Selection``.
"""
self.__lastChangeOldBlock = oldBlock
self.__lastChangeNewBlock = block
self.__lastChangeOffset = offset
def __storeChange(self, old, new, offset, combine=False):
"""Stores the given selection change.
:arg old: A copy of the portion of the :attr:`selection` that
has changed,
:arg new: The new selection values.
:arg offset: Offset into the full :attr:`selection` array
:arg combine: If ``False`` (the default), the previously stored change
will be replaced by the current change. Otherwise the
previous and current changes will be combined.
"""
# Not combining changes (or there
# is no previously stored change).
# We store the change, replacing
# the previous one.
if (not combine) or (self.__lastChangeNewBlock is None):
if log.getEffectiveLevel() == logging.DEBUG:
log.debug('Replacing previously stored change with: '
'[({}, {}), ({}, {}), ({}, {})] ({} selected)'
.format(offset[0], offset[0] + old.shape[0],
offset[1], offset[1] + old.shape[1],
offset[2], offset[2] + old.shape[2],
new.sum()))
self.__lastChangeOldBlock = old
self.__lastChangeNewBlock = new
self.__lastChangeOffset = offset
return
# Otherwise, we combine the old
# change with the new one.
lcOld = self.__lastChangeOldBlock
lcNew = self.__lastChangeNewBlock
lcOffset = self.__lastChangeOffset
# The old block might be None, which
# implies all zeros
if lcOld is None:
lcOld = np.zeros(lcNew.shape, dtype=lcNew.dtype)
# Calculate/organise low/high indices
# for each change set:
#
# - one for the current change (passed
# in to this method call)
#
# - One for the last stored change
#
# - One for the combination of the above
currIdxs = []
lastIdxs = []
cmbIdxs = []
for ax in range(3):
currLo = offset[ ax]
lastLo = lcOffset[ax]
currHi = offset[ ax] + old .shape[ax]
lastHi = lcOffset[ax] + lcOld.shape[ax]
cmbLo = min(currLo, lastLo)
cmbHi = max(currHi, lastHi)
currIdxs.append((int(currLo), int(currHi)))
lastIdxs.append((int(lastLo), int(lastHi)))
cmbIdxs .append((int(cmbLo), int(cmbHi)))
# Make slice objects for each of the indices,
# to make indexing easier. The last/current
# slice objects are defined relative to the
# combined space of both.
cmbSlices = tuple([slice(lo, hi) for lo, hi in cmbIdxs])
lastSlices = tuple([slice(lLo - cmLo, lHi - cmLo)
for ((lLo, lHi), (cmLo, cmHi))
in zip(lastIdxs, cmbIdxs)])
currSlices = tuple([slice(cuLo - cmLo, cuHi - cmLo)
for ((cuLo, cuHi), (cmLo, cmHi))
in zip(currIdxs, cmbIdxs)])
cmbOld = np.array(self.__selection[cmbSlices])
cmbNew = np.array(cmbOld)
cmbOld[lastSlices] = lcOld
cmbNew[lastSlices] = lcNew
cmbNew[currSlices] = new
if log.getEffectiveLevel() == logging.DEBUG:
log.debug('Combining changes: '
'[({}, {}), ({}, {}), ({}, {})] ({} selected) + '
'[({}, {}), ({}, {}), ({}, {})] ({} selected) = '
'[({}, {}), ({}, {}), ({}, {})] ({} selected)'.format(
lastIdxs[0][0], lastIdxs[0][1],
lastIdxs[1][0], lastIdxs[1][1],
lastIdxs[2][0], lastIdxs[2][1],
lcNew.sum(),
currIdxs[0][0], currIdxs[0][1],
currIdxs[1][0], currIdxs[1][1],
currIdxs[2][0], currIdxs[2][1],
new.sum(),
cmbIdxs[0][0], cmbIdxs[0][1],
cmbIdxs[1][0], cmbIdxs[1][1],
cmbIdxs[2][0], cmbIdxs[2][1],
cmbNew.sum()))
self.__lastChangeOldBlock = cmbOld
self.__lastChangeNewBlock = cmbNew
self.__lastChangeOffset = cmbIdxs[0][0], cmbIdxs[1][0], cmbIdxs[2][0]
[docs] def getIndices(self, restrict=None):
"""Returns a :math:`N \\times 3` array which contains the
coordinates of all voxels that are currently selected.
If the ``restrict`` argument is not provided, the entire
selection image is searched.
:arg restrict: A ``slice`` object specifying a sub-set of the
full selection to consider.
"""
restrict = fixSlices(restrict)
xs, ys, zs = np.where(self.__selection[restrict])
result = np.vstack((xs, ys, zs)).T
for ax in range(3):
off = restrict[ax].start
if off is not None:
result[:, ax] += off
return result
[docs] def selectByValue(self,
seedLoc,
precision=None,
searchRadius=None,
local=False,
restrict=None,
combine=False):
"""A *bucket fill* style selection routine.
:arg combine: Combine with the previous stored change (see
:meth:`__storeChange`).
See the :func:`selectByValue` function for details on the other
arguments.
:returns: The generated selection array (a ``numpy`` boolean array),
and offset of this array into the full selection image.
"""
data = self.__image[self.__opts.index()]
block, offset = selectByValue(data,
seedLoc,
precision,
searchRadius,
local,
restrict)
self.replaceSelection(block, offset, combine)
return block, offset
[docs] def invertRegion(self, seedLoc, restrict=None):
"""Inverts the selected state of the region adjacent to ``seedLoc``.
See the :func:`selectByValue` function for details on the other
arguments.
"""
data = self.__selection
val = data[tuple(seedLoc)]
block, offset = selectByValue(data,
seedLoc,
0.5,
local=True,
restrict=restrict)
if val == 0: self.addToSelection( block, offset)
else: self.removeFromSelection(block, offset)
return block, offset
[docs] def selectLine(self,
from_,
to,
boxSize,
axes=(0, 1, 2),
bias=None,
combine=False):
"""Selects a line from ``from_`` to ``to``.
:arg combine: Combine with the previous stored change (see
:meth:`__storeChange`).
See the :func:`selectLine` function for details on the other arguments.
"""
block, offset = selectLine(self.__selection.shape,
self.__image.pixdim[:3],
from_,
to,
boxSize,
axes,
bias)
self.addToSelection(block, offset, combine)
return block, offset
[docs] def deselectLine(self,
from_,
to,
boxSize,
axes=(0, 1, 2),
bias=None,
combine=False):
"""Deselects a line from ``from_`` to ``to``.
:arg combine: Combine with the previous stored change (see
:meth:`__storeChange`).
See the :func:`selectLine` function for details on the other arguments.
"""
block, offset = selectLine(self.__selection.shape,
self.__image.pixdim[:3],
from_,
to,
boxSize,
axes,
bias)
self.removeFromSelection(block, offset, combine)
return block, offset
[docs] def transferSelection(self, destImg, destDisplay):
"""Re-samples the current selection into the destination image
space.
Each ``Selection`` instance is in terms of a specific :class:`.Image`
instance, which has a specific dimensionality. In order to apply
a ``Selection`` which is in terms of one ``Image``, the selection
array needs to be re-sampled.
:arg destImg: The :class:`.Image` that the selection is to be
transferred to.
:arg destDisplay: The :class:`.Display` instance associated with
``destImg``.
:returns: a new ``numpy.uint8`` array, suitable for creating a new
``Selection`` object for use with the given ``destImg``.
"""
raise NotImplementedError('todo')
def __updateSelectionBlock(self, block, offset, combine=False):
"""Replaces the current selection at the specified ``offset`` with the
given ``block``.
The old values for the block are stored, and can be retrieved via the
:meth:`getLastChange` method.
:arg block: A ``numpy.uint8`` array containing the new selection
values.
:arg offset: Voxel coordinates specifying the location of ``block``.
:arg combine: Combine with the previous stored change (see
:meth:`__storeChange`).
"""
if block.size == 0:
return
if offset is None:
offset = (0, 0, 0)
xlo, ylo, zlo = [int(o) for o in offset]
xhi = int(xlo + block.shape[0])
yhi = int(ylo + block.shape[1])
zhi = int(zlo + block.shape[2])
self.__storeChange(
np.array(self.__selection[xlo:xhi, ylo:yhi, zlo:zhi]),
np.array(block, dtype=np.uint8),
offset,
combine)
log.debug('Updating selection ({}) block [{}:{}, {}:{}, {}:{}]'.format(
id(self), xlo, xhi, ylo, yhi, zlo, zhi))
self.__selection[xlo:xhi, ylo:yhi, zlo:zhi] = block
self.__clear = False
self.notify()
def __getSelectionBlock(self, size, offset):
"""Extracts a block from the selection image starting from the
specified ``offset``, and of the specified ``size``.
"""
xlo, ylo, zlo = [int(o) for o in offset]
xhi, yhi, zhi = [int(s) for s in size]
xhi = xlo + size[0]
yhi = ylo + size[1]
zhi = zlo + size[2]
return np.array(self.__selection[xlo:xhi, ylo:yhi, zlo:zhi])
[docs]def fixSlices(slices):
"""A convenience function used by :meth:`selectByValue`,
:meth:`clearSelection` and :meth:`getIndices`, to sanitise their
``restrict`` parameter.
"""
if slices is None:
slices = [None, None, None]
if len(slices) != 3:
raise ValueError('Three slice objects are required')
for i, s in enumerate(slices):
if s is None:
slices[i] = slice(None)
return tuple(slices)
[docs]def selectByValue(data,
seedLoc,
precision=None,
searchRadius=None,
local=False,
restrict=None):
"""A *bucket fill* style selection routine. Given a seed location,
finds all voxels which have a value similar to that of that location.
The current selection is replaced with all voxels that were found.
:arg seedLoc: Voxel coordinates specifying the seed location
:arg precision: Voxels which have a value that is less than
``precision`` from the seed location value will
be selected.
:arg searchRadius: May be either a single value, or a sequence of
three values - one for each axis. If provided, the
search is limited to a sphere (in the voxel
coordinate system), centred on the seed location,
with the specified ``searchRadius`` (in voxels). If
not provided, the search will cover the entire
image space.
:arg local: If ``True``, a voxel will only be selected if it
is adjacent to an already selected voxel (using
8-neighbour connectivity).
:arg restrict: An optional sequence of three ``slice`` object,
specifying a sub-set of the image to search.
:returns: The generated selection array (a ``numpy`` boolean array),
and offset of this array into the data.
"""
if precision is not None and precision < 0:
precision = 0
shape = data.shape
seedLoc = np.array(seedLoc)
value = float(data[seedLoc[0], seedLoc[1], seedLoc[2]])
# Search radius may be either None, a scalar value,
# or a sequence of three values (one for each axis).
# If it is one of the first two options (None/scalar),
# turn it into the third.
if searchRadius is None:
searchRadius = np.array([0, 0, 0])
elif not isinstance(searchRadius, abc.Sequence):
searchRadius = np.array([searchRadius] * 3)
searchRadius = np.ceil(searchRadius)
searchOffset = (0, 0, 0)
# Reduce the data set if
# restrictions have been
# specified
if restrict is not None:
restrict = fixSlices(restrict)
xs, xe = restrict[0].start, restrict[0].step
ys, ye = restrict[1].start, restrict[1].step
zs, ze = restrict[2].start, restrict[2].step
if xs is None: xs = 0
if ys is None: ys = 0
if zs is None: zs = 0
if xe is None: xe = data.shape[0]
if ye is None: ye = data.shape[1]
if ze is None: ze = data.shape[2]
# The seed location has to be in the sub-set
# o the image specified by the restrictions
if seedLoc[0] < xs or seedLoc[0] >= xe or \
seedLoc[1] < ys or seedLoc[1] >= ye or \
seedLoc[2] < zs or seedLoc[2] >= ze:
raise ValueError('Seed location ({}) is outside '
'of restrictions ({})'.format(
seedLoc, ((xs, xe), (ys, ye), (zs, ze))))
data = data[restrict]
shape = data.shape
searchOffset = [xs, ys, zs]
seedLoc = [sl - so for sl, so in zip(seedLoc, searchOffset)]
# No search radius - search
# through the entire image
if np.any(searchRadius == 0):
searchSpace = data
searchMask = None
# Search radius specified - limit
# the search space, and specify
# an ellipsoid mask with the
# specified per-axis radii
else:
ranges = [None, None, None]
slices = [None, None, None]
# Calculate xyz indices
# of the search space
for ax in range(3):
idx = seedLoc[ ax]
rad = searchRadius[ax]
lo = int(round(idx - rad))
hi = int(round(idx + rad + 1))
if lo < 0: lo = 0
if hi > shape[ax] - 1: hi = shape[ax]
ranges[ax] = np.arange(lo, hi)
slices[ax] = slice( lo, hi)
xs, ys, zs = np.meshgrid(*ranges, indexing='ij')
# Centre those indices and the
# seed location at (0, 0, 0)
xs -= seedLoc[0]
ys -= seedLoc[1]
zs -= seedLoc[2]
seedLoc[0] -= ranges[0][0]
seedLoc[1] -= ranges[1][0]
seedLoc[2] -= ranges[2][0]
# Distances from each point in the search
# space to the centre of the search space
dists = ((xs / searchRadius[0]) ** 2 +
(ys / searchRadius[1]) ** 2 +
(zs / searchRadius[2]) ** 2)
# Extract the search space, and
# create the ellipsoid mask
searchSpace = data[tuple(slices)]
searchOffset = [so + r[0] for so, r in zip(searchOffset, ranges)]
searchMask = dists <= 1
if precision is None: hits = searchSpace == value
else: hits = np.abs(searchSpace - value) <= precision
if searchMask is not None:
hits[~searchMask] = False
# If local is true, limit the selection to
# adjacent points with the same/similar value
# (using scipy.ndimage.measurements.label)
#
# The label function defaults to 6 neighbour
# connectivity for 3D, or 4 neighbour
# connectivity for 2D.
#
# If local is not True, any same or similar
# values are part of the selection
if local:
hits, _ = ndimeas.label(hits)
seedLabel = hits[seedLoc[0], seedLoc[1], seedLoc[2]]
hits = hits == seedLabel
return hits, searchOffset
[docs]def selectLine(shape,
dims,
from_,
to,
boxSize,
axes=(0, 1, 2),
bias=None):
"""Selects a continuous "line" in an array of the given ``shape``,
between the points ``from_`` and ``to``.
:arg shape: Shape of the image in which the selection is taking place.
:arg dims: Size of one voxel along each axis (the pixdims).
:arg from_: Start point of the line
:arg to: End point of the line
See the :func:`.routines.voxelBlock` function for details on the other
arguments.
:returns: A tuple containing:
- A 3D boolean ``numpy`` array containing the selected line.
- An offset of this array according to the ``shape`` of the
full image. If the
"""
from_ = np.array(from_)
to = np.array(to)
# The boxSize is specified in scaled
# voxels, so we need to calculate the
# distance between the two voxels,
# and the required number of points,
# in scaled voxels as well.
length = np.sqrt(np.sum((from_ * dims - to * dims) ** 2))
# box size can either be
# a scalar or a sequence.
# We add 2 to the number
# of points for the from_
# and to locations.
if isinstance(boxSize, abc.Sequence):
npoints = int(np.ceil(length / min(boxSize))) + 2
else:
npoints = int(np.ceil(length / boxSize)) + 2
# Create a bunch of interpolated
# points between from_ and to
xs = np.linspace(from_[0], to[0], npoints).round()
ys = np.linspace(from_[1], to[1], npoints).round()
zs = np.linspace(from_[2], to[2], npoints).round()
# We have the minimums and maximums
# of the coordinates to be selected
xmin = xs.min()
ymin = ys.min()
zmin = zs.min()
xmax = xs.max()
ymax = ys.max()
zmax = zs.max()
# But before we can figure out how big
# the cuboid region which encompasses
# the line is, we need to take into
# account the pen size. So we create
# blocks at the start and end points.
minBox = glroutines.voxelBox([xmin, ymin, zmin],
shape,
dims,
boxSize,
axes,
bias)
maxBox = glroutines.voxelBox([xmax, ymax, zmax],
shape,
dims,
boxSize,
axes,
bias)
# And then adjust our overall
# block offset and size by
# these start/end blocks
offset = [int(min((xmin, minBox[:, 0].min()))),
int(min((ymin, minBox[:, 1].min()))),
int(min((zmin, minBox[:, 2].min())))]
size = [int(max((xmax, maxBox[:, 0].max())) - offset[0]),
int(max((ymax, maxBox[:, 1].max())) - offset[1]),
int(max((zmax, maxBox[:, 2].max())) - offset[2])]
# Allocate a selection block
# which will contain the line
block = np.zeros(size, dtype=np.bool)
# Generate a voxel block
# at each point
for i in range(npoints):
point = (xs[i], ys[i], zs[i])
pointBlock, pointOff = glroutines.voxelBlock(
voxel=point,
shape=shape,
dims=dims,
boxSize=boxSize,
axes=axes,
bias=bias)
pointOff = [pointOff[0] - offset[0],
pointOff[1] - offset[1],
pointOff[2] - offset[2]]
# And fill in our line block
sz = pointBlock.shape
block[pointOff[0]:pointOff[0] + sz[0],
pointOff[1]:pointOff[1] + sz[1],
pointOff[2]:pointOff[2] + sz[2]] = pointBlock
return block, offset