def _test_similarity_measure(simi, val):
    I = AffineImage(make_data_int16(), dummy_affine, 'ijk')
    J = AffineImage(I.get_data().copy(), dummy_affine, 'ijk')
    R = HistogramRegistration(I, J)
    R.subsample(spacing=[2,1,3])
    R.similarity = simi
    assert_almost_equal(R.eval(Affine()), val)
def test_histogram_registration():
    """ Test the histogram registration class.
    """
    I = AffineImage(make_data_int16(), dummy_affine, 'ijk')
    J = AffineImage(I.get_data().copy(), dummy_affine, 'ijk')
    R = HistogramRegistration(I, J)
    assert_raises(ValueError, R.subsample, spacing=[0,1,3])
def test_joint_hist_eval():
    I = AffineImage(make_data_int16(), dummy_affine, 'ijk')
    J = AffineImage(I.get_data().copy(), dummy_affine, 'ijk')
    # Obviously the data should be the same
    assert_array_equal(I.get_data(), J.get_data())
    # Instantiate default thing
    R = HistogramRegistration(I, J)
    R.similarity = 'cc'
    null_affine = Affine()
    val = R.eval(null_affine)
    assert_almost_equal(val, 1.0)
    # Try with what should be identity
    R.subsample(spacing=[1,1,1])
    assert_array_equal(R._from_data.shape, I.shape)
    val = R.eval(null_affine)
    assert_almost_equal(val, 1.0)
Exemplo n.º 4
0
    def __init__(
        self,
        from_img,
        to_img,
        from_bins=BINS,
        to_bins=None,
        from_mask=None,
        to_mask=None,
        similarity=SIMILARITY,
        interp=INTERP,
        **kwargs
    ):
        """
        Creates a new histogram registration object.

        Parameters
        ----------
        from_img : nipy-like image
          `From` image 
        to_img : nipy-like image
          `To` image 
        from_bins : integer
          Number of histogram bins to represent the `from` image
        to_bins : integer
          Number of histogram bins to represent the `to` image
        from_mask : nipy image-like
          Mask to apply to the `from` image 
        to_mask : nipy image-like
          Mask to apply to the `to` image
        similarity : str or callable
          Cost-function for assessing image similarity. If a string,
          one of 'cc': correlation coefficient, 'cr': correlation
          ratio, 'crl1': L1-norm based correlation ratio, 'mi': mutual
          information, 'nmi': normalized mutual information, 'slr':
          supervised log-likelihood ratio. If a callable, it should
          take a two-dimensional array representing the image joint
          histogram as an input and return a float.
       interp : str
         Interpolation method.  One of 'pv': Partial volume, 'tri':
         Trilinear, 'rand': Random interpolation.  See ``joint_histogram.c``
        """

        # Binning sizes
        if to_bins == None:
            to_bins = from_bins

        # Clamping of the `from` image. The number of bins may be
        # overriden if unnecessarily large.
        mask = None
        if not from_mask == None:
            mask = from_mask.get_data()
        data, from_bins = clamp(from_img.get_data(), bins=from_bins, mask=mask)
        self._from_img = AffineImage(data, from_img.affine, "scanner")
        # Set the subsampling.  This also sets the _from_data and _vox_coords
        # attributes
        self.subsample()

        # Clamping of the `to` image including padding with -1
        mask = None
        if not to_mask == None:
            mask = to_mask.get_data()
        data, to_bins = clamp(to_img.get_data(), bins=to_bins, mask=mask)
        self._to_data = -np.ones(np.array(to_img.shape) + 2, dtype=CLAMP_DTYPE)
        self._to_data[1:-1, 1:-1, 1:-1] = data
        self._to_inv_affine = inverse_affine(to_img.affine)

        # Joint histogram: must be double contiguous as it will be
        # passed to C routines which assume so
        self._joint_hist = np.zeros([from_bins, to_bins], dtype="double")

        # Set default registration parameters
        self._set_interp(interp)
        self._set_similarity(similarity, **kwargs)
Exemplo n.º 5
0
class HistogramRegistration(object):
    """
    A class to reprensent a generic intensity-based image registration
    algorithm.
    """

    def __init__(
        self,
        from_img,
        to_img,
        from_bins=BINS,
        to_bins=None,
        from_mask=None,
        to_mask=None,
        similarity=SIMILARITY,
        interp=INTERP,
        **kwargs
    ):
        """
        Creates a new histogram registration object.

        Parameters
        ----------
        from_img : nipy-like image
          `From` image 
        to_img : nipy-like image
          `To` image 
        from_bins : integer
          Number of histogram bins to represent the `from` image
        to_bins : integer
          Number of histogram bins to represent the `to` image
        from_mask : nipy image-like
          Mask to apply to the `from` image 
        to_mask : nipy image-like
          Mask to apply to the `to` image
        similarity : str or callable
          Cost-function for assessing image similarity. If a string,
          one of 'cc': correlation coefficient, 'cr': correlation
          ratio, 'crl1': L1-norm based correlation ratio, 'mi': mutual
          information, 'nmi': normalized mutual information, 'slr':
          supervised log-likelihood ratio. If a callable, it should
          take a two-dimensional array representing the image joint
          histogram as an input and return a float.
       interp : str
         Interpolation method.  One of 'pv': Partial volume, 'tri':
         Trilinear, 'rand': Random interpolation.  See ``joint_histogram.c``
        """

        # Binning sizes
        if to_bins == None:
            to_bins = from_bins

        # Clamping of the `from` image. The number of bins may be
        # overriden if unnecessarily large.
        mask = None
        if not from_mask == None:
            mask = from_mask.get_data()
        data, from_bins = clamp(from_img.get_data(), bins=from_bins, mask=mask)
        self._from_img = AffineImage(data, from_img.affine, "scanner")
        # Set the subsampling.  This also sets the _from_data and _vox_coords
        # attributes
        self.subsample()

        # Clamping of the `to` image including padding with -1
        mask = None
        if not to_mask == None:
            mask = to_mask.get_data()
        data, to_bins = clamp(to_img.get_data(), bins=to_bins, mask=mask)
        self._to_data = -np.ones(np.array(to_img.shape) + 2, dtype=CLAMP_DTYPE)
        self._to_data[1:-1, 1:-1, 1:-1] = data
        self._to_inv_affine = inverse_affine(to_img.affine)

        # Joint histogram: must be double contiguous as it will be
        # passed to C routines which assume so
        self._joint_hist = np.zeros([from_bins, to_bins], dtype="double")

        # Set default registration parameters
        self._set_interp(interp)
        self._set_similarity(similarity, **kwargs)

    def _get_interp(self):
        return interp_methods.keys()[interp_methods.values().index(self._interp)]

    def _set_interp(self, interp):
        self._interp = interp_methods[interp]

    interp = property(_get_interp, _set_interp)

    def subsample(self, spacing=None, corner=[0, 0, 0], size=None, npoints=NPOINTS):
        """ 
        Defines a subset of the `from` image to restrict joint
        histogram computation.

        Parameters
        ----------
        spacing : sequence (3,) of positive integers
          Subsampling of image in voxels, where None (default) results
          in the subsampling to be automatically adjusted to roughly
          match a cubic grid with `npoints` voxels 
        corner : sequence (3,) of positive integers
          Bounding box origin in voxel coordinates
        size : sequence (3,) of positive integers
          Desired bounding box size 
        npoints : positive integer
          Desired number of voxels in the bounding box. If a `spacing`
          argument is provided, then `npoints` is ignored.
        """
        if spacing == None:
            spacing = [1, 1, 1]
        else:
            npoints = None
        if size == None:
            size = self._from_img.shape
        slicer = lambda: tuple([slice(corner[i], size[i] + corner[i], spacing[i]) for i in range(3)])
        fov_data = self._from_img.get_data()[slicer()]
        # Adjust spacing to match desired field of view size
        if npoints:
            spacing = ideal_spacing(fov_data, npoints=npoints)
            fov_data = self._from_img.get_data()[slicer()]
        self._from_data = fov_data
        self._from_npoints = (fov_data >= 0).sum()
        self._from_affine = subgrid_affine(self._from_img.affine, slicer())
        # We cache the voxel coordinates of the clamped image
        self._vox_coords = np.indices(self._from_data.shape).transpose((1, 2, 3, 0))

    def _set_similarity(self, similarity="cr", **kwargs):
        if similarity in _sms:
            self._similarity = similarity
            self._similarity_call = _sms[similarity](self._joint_hist.shape, **kwargs)
        else:
            if not hasattr(similarity, "__call__"):
                raise ValueError("similarity should be callable")
            self._similarity = "custom"
            self._similarity_call = similarity

    def _get_similarity(self):
        return self._similarity

    similarity = property(_get_similarity, _set_similarity)

    def eval(self, T):
        """ 
        Evaluate similarity function given a world-to-world transform. 

        Parameters
        ----------
        T : Transform
            Transform object implementing ``apply`` method
        """
        Tv = ChainTransform(T, pre=self._from_affine, post=self._to_inv_affine)
        return self._eval(Tv)

    def _eval(self, Tv):
        """ 
        Evaluate similarity function given a voxel-to-voxel transform. 

        Parameters
        ----------
        Tv : Transform
             Transform object implementing ``apply`` method
             Should map voxel space to voxel space
        """
        # trans_vox_coords needs be C-contiguous
        trans_vox_coords = Tv.apply(self._vox_coords)
        interp = self._interp
        if self._interp < 0:
            interp = -np.random.randint(maxint)
        _joint_histogram(
            self._joint_hist, self._from_data.flat, self._to_data, trans_vox_coords, interp  ## array iterator
        )
        return self._similarity_call(self._joint_hist)

    def optimize(self, T, optimizer=OPTIMIZER, **kwargs):
        """ Optimize transform `T` with respect to similarity measure. 

        The input object `T` will change as a result of the optimization.

        Parameters
        ----------
        T : object or str
          An object representing a transformation that should
          implement ``apply`` method and ``param`` attribute or
          property. If a string, one of 'rigid', 'similarity', or
          'affine'. The corresponding transformation class is then
          initialized by default.
        optimizer : str
          Name of optimization function (one of 'powell', 'steepest',
          'cg', 'bfgs', 'simplex')
        **kwargs : dict
          keyword arguments to pass to optimizer
        """
        # Replace T if a string is passed
        if T in affine_transforms:
            T = affine_transforms[T]()

        # Pull callback out of keyword arguments, if present
        callback = kwargs.pop("callback", None)

        # Create transform chain object with T generating params
        Tv = ChainTransform(T, pre=self._from_affine, post=self._to_inv_affine)
        tc0 = Tv.param

        # Cost function to minimize
        def cost(tc):
            # This is where the similarity function is calculcated
            Tv.param = tc
            return -self._eval(Tv)

        # Callback during optimization
        if callback == None and VERBOSE:

            def callback(tc):
                Tv.param = tc
                print(Tv.optimizable)
                print(str(self.similarity) + " = %s" % self._eval(Tv))
                print("")

        # Switching to the appropriate optimizer
        if VERBOSE:
            print("Initial guess...")
            print(Tv.optimizable)
        if optimizer == "powell":
            fmin = fmin_powell
            kwargs.setdefault("xtol", XTOL)
            kwargs.setdefault("ftol", FTOL)
        elif optimizer == "steepest":
            fmin = fmin_steepest
            kwargs.setdefault("xtol", XTOL)
            kwargs.setdefault("ftol", FTOL)
            kwargs.setdefault("step", STEPSIZE)
        elif optimizer == "cg":
            fmin = fmin_cg
            kwargs.setdefault("gtol", GTOL)
            kwargs.setdefault("maxiter", MAXITER)
        elif optimizer == "bfgs":
            fmin = fmin_bfgs
            kwargs.setdefault("gtol", GTOL)
            kwargs.setdefault("maxiter", MAXITER)
        elif optimizer == "simplex":
            fmin = fmin_simplex
            kwargs.setdefault("xtol", XTOL)
            kwargs.setdefault("ftol", FTOL)
        else:
            raise ValueError("Unknown optimizer name: %s???" % optimizer)
        # Output
        if VERBOSE:
            print("Optimizing using %s" % fmin.__name__)
        Tv.param = fmin(cost, tc0, callback=callback, **kwargs)
        return Tv.optimizable

    def explore(self, T0, *args):
        """
        Evaluate the similarity at the transformations specified by
        sequences of parameter values.

        For instance: 

        explore(T0, (0, [-1,0,1]), (4, [-2.,2]))
        """
        nparams = T0.param.size
        sizes = np.ones(nparams)
        deltas = [[0] for i in range(nparams)]
        for a in args:
            deltas[a[0]] = a[1]
        grids = np.mgrid[[slice(0, len(d)) for d in deltas]]
        ntrials = np.prod(grids.shape[1:])
        Deltas = [np.asarray(deltas[i])[grids[i, :]].ravel() for i in range(nparams)]
        simis = np.zeros(ntrials)
        params = np.zeros([nparams, ntrials])

        Tv = ChainTransform(T0, pre=self._from_affine, post=self._to_inv_affine)
        param0 = Tv.param
        for i in range(ntrials):
            param = param0 + np.array([D[i] for D in Deltas])
            Tv.param = param
            simis[i] = self._eval(Tv)
            params[:, i] = param

        return simis, params