Source code for spam.DIC.registration

# Library of SPAM image correlation functions.
# Copyright (C) 2020 SPAM Contributors
#
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
# FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
# more details.
#
# You should have received a copy of the GNU General Public License along with
# this program.  If not, see <http://www.gnu.org/licenses/>.

import numpy
import progressbar
import spam.deformation
import spam.DIC
import spam.label  # for im1mask

# 2017-05-29 ER and EA
# This is spam's C++ DIC toolkit, but since we're in the tools/ directory we can import it directly
from spam.DIC.DICToolkit import computeDICjacobian, computeDICoperators

# numpy.set_printoptions(precision=3, suppress=True)


def _errorCalc(im1, im2):
    return numpy.nansum(numpy.square(numpy.subtract(im1, im2))) / numpy.nansum(im1)


[docs] def register( im1, im2, im1mask=None, PhiInit=None, PhiRigid=False, PhiInitBinRatio=1.0, margin=None, maxIterations=25, deltaPhiMin=0.001, updateGradient=False, interpolationOrder=1, interpolator="python", verbose=False, returnPhiMaskCentre=True, imShowProgress=False, imShowProgressNewFig=False, imShowProgressPause=0.01, ): r""" Perform subpixel image correlation between im1 and im2. The result of register(im1, im2) will give a deformation function :math:`\Phi` which maps im1 into im2. The Phi function used here allows the measurement of sub-pixel displacements, rotation, and linear straining of the whole image. However, this function will numerically deform im2 until it best matches im1. :math:`im1(x) = im2(\Phi.x)` If im1 and im2 follow each other in time, then the resulting Phi is im1 -> im2 which makes sense in most cases. "Discrete correlation" can be performed by masking im1. im1 and im2 do not necessarily have to be the same size (`i.e.`, im2 can be bigger) -- this is good since there is a zone to accommodate movement. In the case of a bigger im2, im1 and im2 are assumed to be centred with respect to each other. Parameters ---------- im1 : 3D numpy array The greyscale image that will not move -- must not contain NaNs im2 : 3D numpy array The greyscale image that will be deformed -- must not contain NaNs im1mask : 3D boolean numpy array, optional A mask for the zone to correlate in im1 with `False` in the zone to not correlate. Default = None, `i.e.`, correlate all of im1 minus the margin. If this is defined, the Phi returned is in the centre of mass of the mask PhiInit : 4x4 numpy array, optional Initial deformation to apply to im1. Default = numpy.eye(4), `i.e.`, no transformation PhiRigid : bool, optional Run a rigid correlation? Only the rigid part of your PhiInit will be kept. Default = False PhiInitBinRatio : float, optional Change translations in PhiInit, if it's been calculated on a differently-binned image. Default = 1 margin : int, optional Margin, in pixels, to take in im1. Can also be a N-component list of ints, representing the margin in ND. If im2 has the same size as im1 this is strictly necessary to allow space for interpolation and movement Default = None (`i.e.`, 10% of max dimension of im1) maxIterations : int, optional Maximum number of quasi-Newton iterations to perform before stopping. Default = 25 deltaPhiMin : float, optional Smallest change in the norm of Phi (the transformation operator) before stopping. Default = 0.001 updateGradient : bool, optional Should the gradient of the image be computed (and updated) on the deforming im2? Default = False (it is computed once on im1) interpolationOrder : int, optional Order of the greylevel interpolation for applying Phi to im1 when correlating. Recommended value is 3, but you can get away with 1 for faster calculations. Default = 3 interpolator : string, optional Which interpolation function to use from `spam`. Default = 'python'. 'C' is also an option verbose : bool, optional Get to know what the function is really thinking, recommended for debugging only. Default = False returnPhiMaskCentre : bool, optional In case a mask is passed, should the Phi returned by the function be returned at the centre of mass of the mask? If False, it's returned (as per no mask) in the centre of the image. Default = True imShowProgress : bool, optional Pop up a window showing a ``imShowProgress`` slice of the image differences (im1-im2) as im1 is progressively deformed. Default = False imShowProgressNewFig : bool, optional (defaul = False) Make a new plt.figure for each iteration, useful for examples gallery imShowProgressPause : float, optional (defaul = 0.01) Seconds to pause between imShowProgress updates Returns ------- Dictionary : 'Phi' : 4x4 float array Deformation function defined at the centre of the image 'returnStatus' : signed int Return status from the correlation: 2 : Achieved desired precision in the norm of delta Phi 1 : Hit maximum number of iterations while iterating -1 : Error is more than 80% of previous error, we're probably diverging -2 : Singular matrix M (most probably image texture problem) -3 : Displacement > 5*margin -4 : Singular Phi matrix (most probably due to divergence) -5 : Not used in this funciton but reserved for the script (correlation skipped) 'error' : float Error float describing mismatch between images, it's the sum of the squared difference divided by the sum of im1 'iterations' : int Number of iterations 'deltaPhiNorm' : float Norm of deltaPhi Note ---- This correlation was written in the style of S. Roux (especially "An extension of Digital Image Correlation for intermodality image registration") especially equations 12 and 13. """ # Explicitly set input images to floats im1 = im1.astype("<f4") im2 = im2.astype("<f4") # initialise exit clause for singular "M" and Phi matrices singularM = False singularPhi = False # 2022-06-03 GP: Setting deltaPhiMin to only positive values deltaPhiMin = numpy.abs(deltaPhiMin) # Detect unpadded 2D image first: if im1.ndim == 2: # pad them im1 = im1[numpy.newaxis, ...] im2 = im2[numpy.newaxis, ...] if im1mask is not None: im1mask = im1mask[numpy.newaxis, ...] # Detect 2D images if im1.shape[0] == 1: twoD = True # Override interpolator for python in 2D interpolator = "python" # Define masks for M and A in 2D since we'll ignore the Z components # Components of M and A which don't include Z twoDmaskA = numpy.zeros((12), dtype=bool) for i in [5, 6, 7, 9, 10, 11]: twoDmaskA[i] = True twoDmaskM = numpy.zeros((12, 12), dtype=bool) for y in range(12): for x in range(12): if twoDmaskA[y] and twoDmaskA[x]: twoDmaskM[y, x] = True else: twoD = False if interpolationOrder > 1: # Override interpolator for python for higher than linear interpolator = "python" # Automatically calculate margin if none is passed # Detect default case and calculate maring necessary for a 45deg rotation with no displacement if margin is None: if twoD: # z-margin will be overwritten below margin = [1 + int(0.1 * min(im1.shape[1:]))] * 3 else: margin = [1 + int(0.1 * min(im1.shape))] * 3 elif type(margin) == list: pass else: # Make sure margin is an int margin = int(margin) margin = [margin] * 3 # Make sure im2 is bigger than im1 and check difference in size # Get difference in image sizes. This should be positive, since we must always have enough data for im2 interpolation im1im2sizeDiff = numpy.array(im2.shape) - numpy.array(im1.shape) # Check im2 is bigger or same size if (im1im2sizeDiff < 0).any(): print("\tcorrelate.register(): im2 is smaller than im1 in at least one dimension: im2.shape: {}, im1.shape: {}".format(im2.shape, im1.shape)) raise ValueError("correlate.register():DimProblem") # Make sure margin is at least 1 for the gradient calculation if twoD: margin[0] = 0 elif min(margin) < 1 and min(im1im2sizeDiff) == 0: margin = [1] * 3 # Calculate crops -- margin for im2 and more for im1 if it is bigger # Margin + half the difference in size for im2 -- im1 will start in the middle. crop2 = ( slice( int(im1im2sizeDiff[0] / 2 + margin[0]), int(im1im2sizeDiff[0] / 2 + im1.shape[0] - margin[0]), ), slice( int(im1im2sizeDiff[1] / 2 + margin[1]), int(im1im2sizeDiff[1] / 2 + im1.shape[1] - margin[1]), ), slice( int(im1im2sizeDiff[2] / 2 + margin[2]), int(im1im2sizeDiff[2] / 2 + im1.shape[2] - margin[2]), ), ) # Get subvolume crops from both images -- just the margin for im1 crop1 = ( slice(int(margin[0]), int(im1.shape[0] - margin[0])), slice(int(margin[1]), int(im1.shape[1] - margin[1])), slice(int(margin[2]), int(im1.shape[2] - margin[2])), ) # Create im1 crop to shift less data im1crop = im1[crop1].copy() # Calculate effective margin # to calculate displacement divergence # using max for the margin -- subjective choice max(margin) + min(im1im2sizeDiff) / 2 # print( "\tcorrelate.register(): realMargin is:", realMargin) # If live plot is asked for, initialise canvas if imShowProgress: import matplotlib.pyplot as plt # Plot ranges for signed residual vmin = -im1crop.max() vmax = im1crop.max() if not imShowProgressNewFig: if twoD: plt.subplot(1, 3, 1) plt.axis([im1crop.shape[2], 0, im1crop.shape[1], 0]) plt.subplot(1, 3, 2) plt.axis([im1crop.shape[2], 0, im1crop.shape[1], 0]) plt.subplot(1, 3, 3) plt.axis([im1crop.shape[2], 0, im1crop.shape[1], 0]) else: # 3D plt.subplot(3, 3, 1) plt.axis([im1crop.shape[2], 0, im1crop.shape[1], 0]) plt.subplot(3, 3, 2) plt.axis([im1crop.shape[2], 0, im1crop.shape[1], 0]) plt.subplot(3, 3, 3) plt.axis([im1crop.shape[2], 0, im1crop.shape[1], 0]) plt.subplot(3, 3, 4) plt.axis([im1crop.shape[2], 0, im1crop.shape[0], 0]) plt.subplot(3, 3, 5) plt.axis([im1crop.shape[2], 0, im1crop.shape[0], 0]) plt.subplot(3, 3, 6) plt.axis([im1crop.shape[2], 0, im1crop.shape[0], 0]) plt.subplot(3, 3, 7) plt.axis([im1crop.shape[1], 0, im1crop.shape[0], 0]) plt.subplot(3, 3, 8) plt.axis([im1crop.shape[1], 0, im1crop.shape[0], 0]) plt.subplot(3, 3, 9) plt.axis([im1crop.shape[1], 0, im1crop.shape[0], 0]) plt.ion() ########################################################### # Important -- since we're moving im2, initial Phis will be # pointing the wrong way, they need to be inversed ########################################################### # If there is no initial Phi, initalise it and im1defCrop to zero. if PhiInit is None: Phi = numpy.eye(4) im2def = im2.copy() else: # 2020-03-17 in isolation from COVID-19 EA and OS: Apparently this changes the PhiInit outside this function, # Copying into different variable # Apply binning on displacement Phi = PhiInit.copy() # If we're in rigid mode, keep only translations and rotations for this guess # If you don't do this it goes mad (i.e., rigid updates to non-rigid guess don't seem to work) if PhiRigid: Phi = spam.deformation.computeRigidPhi(Phi.copy()) Phi[0:3, -1] *= PhiInitBinRatio # invert PhiInit to apply it to im2 try: PhiInv = numpy.linalg.inv(Phi.copy()) except numpy.linalg.linalg.LinAlgError: PhiInv = numpy.eye(4) # Since we are now using Fcentred for iterations, do nothing # call decomposePhi to apply PhiInit (calculated on the centre of the image) to the origin (0,0,0) if interpolator == "C": im2def = spam.DIC.applyPhi(im2, Phi=PhiInv, interpolationOrder=interpolationOrder) elif interpolator == "python": im2def = spam.DIC.applyPhiPython(im2, Phi=PhiInv, interpolationOrder=interpolationOrder) def computeGradient(im, twoD): # Function to compute gradients if twoD: # If 2D image we have no gradients in the 1st direction # if verbose: print("Calculating gradients...", end="") imGradY, imGradX = numpy.gradient(im[0]) imGradX = imGradX[numpy.newaxis, ...] imGradY = imGradY[numpy.newaxis, ...] imGradZ = numpy.zeros_like(imGradX) # if verbose: print("done") else: # if verbose: print("Calculating gradients...", end="") imGradZ, imGradY, imGradX = numpy.gradient(im) # if verbose: print("done ") return imGradZ, imGradY, imGradX # Apply stationary im1 mask if im1mask is not None: im1crop[im1mask[crop1] == 0] = numpy.nan # Initialise iteration variables iterations = 0 returnStatus = 0 # Big value to start with to ensure the first iteration deltaPhiNorm = 100.0 error = _errorCalc(im1crop, im2def[crop2]) if verbose: print("Start correlation with Error = {:0.2f}".format(error)) widgets = [ " Iteration Number:", progressbar.Counter(), " ", progressbar.FormatLabel(""), " (", progressbar.Timer(), ")", ] pbar = progressbar.ProgressBar(widgets=widgets, maxval=maxIterations) # widgets = [progressbar.FormatLabel(''), ' ', progressbar.Bar(), ' ', progressbar.AdaptiveETA()] # pbar = progressbar.ProgressBar(widgets=widgets, maxval=numberOfNodes) pbar.start() # --- Start Iterations --- while iterations < maxIterations and deltaPhiNorm > deltaPhiMin: errorPrev = error # On first iteration, compute hessian and jacobian in any case # ...or if we've been asked to update Gradient if iterations == 0 or updateGradient: # If we've been asked to update gradient, compute it on im2, which is moving if updateGradient: imGradZ, imGradY, imGradX = computeGradient(im2def, twoD) crop = crop2 # Otherwise compute it once and for all on the non-moving im1 else: imGradZ, imGradY, imGradX = computeGradient(im1, twoD) crop = crop1 M = numpy.zeros((12, 12), dtype="<f8") A = numpy.zeros((12), dtype="<f8") # Compute both DIC operators A and M with C library computeDICoperators( im1crop.astype("<f4"), im2def[crop2].astype("<f4"), imGradZ[crop].astype("<f4"), imGradY[crop].astype("<f4"), imGradX[crop].astype("<f4"), M, A, ) else: # just update jacobian A A = numpy.zeros((12), dtype="<f8") computeDICjacobian( im1crop.astype("<f4"), im2def[crop2].copy().astype("<f4"), imGradZ[crop].copy().astype("<f4"), imGradY[crop].copy().astype("<f4"), imGradX[crop].copy().astype("<f4"), A, ) # Solve for delta Phi if twoD: # If a twoD image, cut out the bits of the M and A matrices that interest us # This is necessary since the rest is super singular # Solve for delta Phi try: deltaPhi = numpy.dot(numpy.linalg.inv(M[twoDmaskM].reshape(6, 6)), A[twoDmaskA]) except numpy.linalg.linalg.LinAlgError: singularM = True break # ...and now put deltaPhi components back in place for a 3D deltaPhi deltaPhinew = numpy.zeros((12), dtype=float) deltaPhinew[twoDmaskA] = deltaPhi del deltaPhi deltaPhi = deltaPhinew else: # Solve for delta Phi try: deltaPhi = numpy.dot(numpy.linalg.inv(M), A) except numpy.linalg.linalg.LinAlgError: singularM = True break # If we're doing a rigid registration... if PhiRigid: # Add padding zeros deltaPhi = numpy.hstack([deltaPhi, numpy.zeros(4)]).reshape((4, 4)) deltaPhiPlusI = numpy.eye(4) + deltaPhi # Keep only rigid part of deltaPhi deltaPhiPlusIrigid = spam.deformation.computeRigidPhi(deltaPhiPlusI.copy()) # Subtract I from the rigid dPhi+1, and compute norm only on first 3 rows # ...basically recompute deltaPhiNorm only on rigid part deltaPhiNorm = numpy.linalg.norm((deltaPhiPlusIrigid - numpy.eye(4))[0:3].ravel()) # Apply Delta Phi correction to Phi In Roux X-N paper equation number 11 Phi = numpy.dot(Phi, deltaPhiPlusIrigid) else: # The general, non-rigid case deltaPhiNorm = numpy.linalg.norm(deltaPhi) # Add padding zeros deltaPhi = numpy.hstack([deltaPhi, numpy.zeros(4)]).reshape((4, 4)) # Update Phi Phi = numpy.dot(Phi, (numpy.eye(4) + deltaPhi)) try: PhiInv = numpy.linalg.inv(Phi.copy()) except numpy.linalg.linalg.LinAlgError: singularPhi = True break # reset im1def as emtpy matrix for deformed image if interpolator == "C": im2def = spam.DIC.applyPhi(im2, Phi=PhiInv, interpolationOrder=interpolationOrder) elif interpolator == "python": im2def = spam.DIC.applyPhiPython(im2, Phi=PhiInv, interpolationOrder=interpolationOrder) # Error calculation error = _errorCalc(im1crop, im2def[crop2]) # Keep interested people up to date with what's happening # if verbose: # print("Error = {:0.2f}".format(error)), # print("deltaPhiNorm = {:0.4f}".format(deltaPhiNorm)) # Catch divergence condition after half of the max iterations if errorPrev < error * 0.8 and iterations > maxIterations / 2: # undo this bad Phi which has increased the error: # Phi = numpy.dot((numpy.eye(4) + deltaPhi), Phi) returnStatus = -1 if verbose: print("\t -> diverging on error condition (returning -1)") break # Second divergence criterion on displacement (Issue #62) # If any displcement is bigger than 5* the margin... # if (numpy.abs(spam.deformation.decomposePhi(Phi.copy())['t']) > 5 * realMargin).any(): # if verbose: print("\t -> diverging on displacement condition") # returnStatus = -3 # break # 2018-10-02 - EA: Add divergence condition on U trans = spam.deformation.decomposePhi(Phi.copy()) try: volumeChange = numpy.linalg.det(trans["U"]) if volumeChange > 3 or volumeChange < 0.2: if verbose: print("\t -> diverging on volumetric change condition (returning -3)") returnStatus = -3 break except Exception: print("\t -> can't compute det(U) (returning -3)") returnStatus = -3 break if imShowProgress: if imShowProgressNewFig: plt.figure() else: plt.clf() if twoD: plt.suptitle("Iteration Number = {}".format(iterations), fontsize=10) plt.subplot(1, 3, 1) plt.title("im1") plt.imshow(im1crop[im1crop.shape[0] // 2, :, :], cmap="Greys_r", vmin=0, vmax=vmax) plt.subplot(1, 3, 2) plt.title("im2def") plt.imshow( im2def[crop2][im1crop.shape[0] // 2, :, :], cmap="Greys_r", vmin=0, vmax=vmax, ) plt.subplot(1, 3, 3) plt.title("im1-im2def") plt.imshow( numpy.subtract(im1crop, im2def[crop2])[im1crop.shape[0] // 2, :, :], cmap="coolwarm", vmin=vmin, vmax=vmax, ) plt.pause(imShowProgressPause) else: # 3D plt.suptitle("Iteration Number = {}".format(iterations), fontsize=10) plt.subplot(3, 3, 1) plt.title("im1 Z-slice") plt.imshow(im1crop[im1crop.shape[0] // 2, :, :], cmap="Greys_r", vmin=0, vmax=vmax) plt.subplot(3, 3, 2) plt.title("im2def Z-slice") plt.imshow( im2def[crop2][im1crop.shape[0] // 2, :, :], cmap="Greys_r", vmin=0, vmax=vmax, ) plt.subplot(3, 3, 3) plt.title("im1-im2def Z-slice") plt.imshow( numpy.subtract(im1crop, im2def[crop2])[im1crop.shape[0] // 2, :, :], cmap="coolwarm", vmin=vmin, vmax=vmax, ) plt.subplot(3, 3, 4) plt.title("im1 Y-slice") plt.imshow(im1crop[:, im1crop.shape[1] // 2, :], cmap="Greys_r", vmin=0, vmax=vmax) plt.subplot(3, 3, 5) plt.title("im2def Y-slice") plt.imshow( im2def[crop2][:, im1crop.shape[1] // 2, :], cmap="Greys_r", vmin=0, vmax=vmax, ) plt.subplot(3, 3, 6) plt.title("im1-im2def Y-slice") plt.imshow( numpy.subtract(im1crop, im2def[crop2])[:, im1crop.shape[1] // 2, :], cmap="coolwarm", vmin=vmin, vmax=vmax, ) # if imShowProgress == "X" or imShowProgress == "x": # if imShowProgressNewFig: plt.figure() # else: plt.clf() plt.subplot(3, 3, 7) plt.title("im1 X-slice") plt.imshow(im1crop[:, :, im1crop.shape[2] // 2], cmap="Greys_r", vmin=0, vmax=vmax) plt.subplot(3, 3, 8) plt.title("im2def X-slice") plt.imshow( im2def[crop2][:, :, im1crop.shape[2] // 2], cmap="Greys_r", vmin=0, vmax=vmax, ) plt.subplot(3, 3, 9) plt.title("im1-im2def X-slice") plt.imshow( numpy.subtract(im1crop, im2def[crop2])[:, :, im1crop.shape[2] // 2], cmap="coolwarm", vmin=vmin, vmax=vmax, ) plt.pause(imShowProgressPause) iterations += 1 if verbose: decomposedPhi = spam.deformation.decomposePhi(Phi.copy()) widgets[3] = progressbar.FormatLabel( " dPhiNorm={:0>7.5f} error={:0>4.2f} t=[{:0>5.3f} {:0>5.3f} {:0>5.3f}] r=[{:0>5.3f} {:0>5.3f} {:0>5.3f}] z=[{:0>5.3f} {:0>5.3f} {:0>5.3f}]".format( deltaPhiNorm, error, decomposedPhi["t"][0], decomposedPhi["t"][1], decomposedPhi["t"][2], decomposedPhi["r"][0], decomposedPhi["r"][1], decomposedPhi["r"][2], decomposedPhi["z"][0], decomposedPhi["z"][1], decomposedPhi["z"][2], ) ) pbar.update(iterations) # Positive return status is a healthy end of while loop: if iterations >= maxIterations: returnStatus = 1 if deltaPhiNorm <= deltaPhiMin: returnStatus = 2 if singularM: returnStatus = -2 elif singularPhi: returnStatus = -4 if verbose: print() # pbar.finish() if iterations > maxIterations: print("\t -> No convergence before max iterations") if deltaPhiNorm <= deltaPhiMin: print("\t -> Converged") if singularM: print("\t -> Singular matrix M (most probably image texture problem)") if singularPhi: print("\t -> Singular Phi matrix (most probably due to divergence)") if im1mask is not None and returnPhiMaskCentre: # If a mask on im1 is defined, return an Phi at the centre of the mass maskCOM = spam.label.centresOfMass(im1mask[crop1])[-1] # print("Mask COM", maskCOM) # print( "\nNormal Phi:\n", Phi) Phi[0:3, -1] = spam.deformation.decomposePhi( Phi.copy(), PhiCentre=(numpy.array(im1crop.shape) - 1) / 2.0, PhiPoint=maskCOM, )["t"] # print( "\nF in mask:\n", F) return { "error": error, "Phi": Phi, "returnStatus": returnStatus, "iterations": iterations, "deltaPhiNorm": deltaPhiNorm, }
[docs] def registerMultiscale( im1, im2, binStart, binStop=1, im1mask=None, PhiInit=None, PhiRigid=False, PhiInitBinRatio=1.0, margin=None, maxIterations=100, deltaPhiMin=0.0001, updateGradient=False, interpolationOrder=1, interpolator="C", verbose=False, returnPhiMaskCentre=True, imShowProgress=False, forceChangeScale=False, ): """ Perform multiscale subpixel image correlation between im1 and im2. This means applying a downscale (binning) to the images, performing a Lucas and Kanade at that level, and then improving it on a 2* less downscaled image, all the way back to the full scale image. If your input images have multiple scales of texture, this should save significant time. Please see the documentation for `register` for the rest of the documentation. Parameters ---------- im1 : 3D numpy array The greyscale image that will not move -- must not contain NaNs im2 : 3D numpy array The greyscale image that will be deformed -- must not contain NaNs binStart : int Maximum amount of binning to apply, please input a number which is 2^int binStop : int, optional Which binning level to stop upscaling at. The value of 1 (full image resolution) is almost always recommended (unless memory/time problems). Default = 1 im1mask : 3D boolean numpy array, optional A mask for the zone to correlate in im1 with `False` in the zone to not correlate. Default = None, `i.e.`, correlate all of im1 minus the margin. If this is defined, the Phi returned is in the centre of mass of the mask PhiInit : 4x4 numpy array, optional Initial deformation to apply to im1, by default at bin1 scale Default = numpy.eye(4), `i.e.`, no transformation PhiRigid : bool, optional Run a rigid correlation? Only the rigid part of your PhiInit will be kept. Default = False PhiInitBinRatio : float, optional Change translations in PhiInit, if it's been calculated on a differently-binned image. Default = 1 margin : int, optional Margin, in pixels, to take in im1. Can also be a N-component list of ints, representing the margin in ND. If im2 has the same size as im1 this is strictly necessary to allow space for interpolation and movement Default = 0 (`i.e.`, 10% of max dimension of im1) maxIterations : int, optional Maximum number of quasi-Newton iterations to perform before stopping. Default = 25 deltaPhiMin : float, optional Smallest change in the norm of Phi (the transformation operator) before stopping. Default = 0.001 updateGradient : bool, optional Should the gradient of the image be computed (and updated) on the deforming im2? Default = False (it is computed once on im1) interpolationOrder : int, optional Order of the greylevel interpolation for applying Phi to im1 when correlating. Recommended value is 3, but you can get away with 1 for faster calculations. Default = 3 interpolator : string, optional Which interpolation function to use from `spam`. Default = 'python'. 'C' is also an option verbose : bool, optional Get to know what the function is really thinking, recommended for debugging only. Default = False returnPhiMaskCentre : bool, optional In case a mask is passed, should the Phi returned by the function be returned at the centre of mass of the mask? If False, it's returned (as per no mask) in the centre of the image. Default = True imShowProgress : bool, optional Pop up a window showing a ``imShowProgress`` slice of the image differences (im1-im2) as im1 is progressively deformed. Default = False forceChangeScale : bool, optional Change up a scale even if not converged? Default = False Returns ------- Dictionary: 'Phi': 4x4 float array Deformation function defined at the centre of the image 'returnStatus': signed int Return status from the correlation: 2 : Achieved desired precision in the norm of delta Phi 1 : Hit maximum number of iterations while iterating -1 : Error is more than 80% of previous error, we're probably diverging -2 : Singular matrix M (most probably image texture problem) -3 : Displacement > 5*margin -4 : Singular Phi matrix (most probably due to divergence) -5 : Not used in this funciton but reserved for the script (correlation skipped) 'error': float Error float describing mismatch between images, it's the sum of the squared difference divided by the sum of im1 'iterations': int Number of iterations """ # Detect unpadded 2D image first: if len(im1.shape) == 2: # pad them im1 = im1[numpy.newaxis, ...] im2 = im2[numpy.newaxis, ...] if im1mask is not None: im1mask = im1mask[numpy.newaxis, ...] # Detect 2D images if im1.shape[0] == 1: twoD = True else: twoD = False import math logbinstart = math.log(binStart, 2) if not logbinstart.is_integer(): print( "spam.DIC.registerMultiscale(): You asked for an initial binning of", binStart, ",rounding it to ", end="", ) binStart = 2 ** numpy.round(logbinstart) print(binStart) logbinstop = math.log(binStop, 2) if not logbinstop.is_integer(): print( "spam.DIC.registerMultiscale(): You asked for a final binning of", binStop, ",rounding it to ", end="", ) binStop = 2 ** numpy.round(logbinstop) print(binStop) # If there is no initial Phi, initalise it and im1defCrop to zero. if PhiInit is None: reg = {"Phi": numpy.eye(4)} else: # Apply binning on displacement -- the /2 is to be able to *2 it in the LK call tmp = PhiInit.copy() tmp[0:3, -1] *= PhiInitBinRatio / 2.0 / float(binStart) reg = {"Phi": tmp} if im1mask is not None: # Multiply up to 100 so we can apply a threshold below on binning in % im1mask = im1mask.astype("<u1") * 100 # Sorry... This generates a list of binning levels, if binStart=8 and binStop=2 this will be [8, 4 ,2] binLevels = 2 ** numpy.arange(math.log(binStart, 2), math.log(binStop, 2) - 1, -1).astype(int) for binLevel in binLevels: if verbose: print( "spam.DIC.registerMultiscale(): working on binning: ", binLevel, ) if binLevel > 1: if twoD: import scipy.ndimage im1b = scipy.ndimage.zoom(im1[0], 1 / binLevel, order=1) im2b = scipy.ndimage.zoom(im2[0], 1 / binLevel, order=1) # repad them im1b = im1b[numpy.newaxis, ...] im2b = im2b[numpy.newaxis, ...] if im1mask is not None: im1maskb = scipy.ndimage.zoom(im1mask[0], 1 / binLevel, order=1) im1maskb = im1maskb[numpy.newaxis, ...] else: im1maskb = None else: im1b = spam.DIC.binning(im1, binLevel) im2b = spam.DIC.binning(im2, binLevel) if im1mask is not None: im1maskb = spam.DIC.binning(im1mask, binLevel) > 0 else: im1maskb = None else: im1b = im1 im2b = im2 if im1mask is not None: im1maskb = im1mask > 0 else: im1maskb = None # Automatically calculate margin if none is passed # Detect default case and calculate margin necessary for a 45deg rotation with no displacement if margin is None: if twoD: # z-margin will be overwritten below marginB = [1 + int(0.1 * min(im1b.shape[1:]))] * 3 else: marginB = [1 + int(0.1 * min(im1b.shape))] * 3 elif type(margin) == list: marginB = (numpy.array(margin) // binLevel).tolist() else: # Make sure margin is an int margin = int(margin) margin = [margin] * 3 marginB = (numpy.array(margin) // binLevel).tolist() reg = spam.DIC.register( im1b, im2b, im1mask=im1maskb, PhiInit=reg["Phi"], PhiRigid=PhiRigid, PhiInitBinRatio=2.0, margin=marginB, maxIterations=maxIterations, deltaPhiMin=deltaPhiMin, updateGradient=updateGradient, interpolationOrder=interpolationOrder, interpolator=interpolator, verbose=verbose, returnPhiMaskCentre=returnPhiMaskCentre, imShowProgress=imShowProgress, ) if reg["returnStatus"] != 2 and not forceChangeScale: if verbose: print("spam.DIC.registerMultiscale(): binning {} did not converge (return Status = {}), not continuing".format(binLevel, reg["returnStatus"])) # Multiply up displacement and return bad result reg["Phi"][0:3, -1] *= float(binLevel) return reg binLevel = int(binLevel / 2) # Return displacments at bin1 scale reg["Phi"][0:3, -1] *= float(binStop) return reg