Coverage for /usr/local/lib/python3.8/site-packages/spam/DIC/registration.py: 83%
242 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-11-22 13:26 +0000
« prev ^ index » next coverage.py v7.2.3, created at 2023-11-22 13:26 +0000
1"""
2Library of SPAM image correlation functions.
3Copyright (C) 2020 SPAM Contributors
5This program is free software: you can redistribute it and/or modify it
6under the terms of the GNU General Public License as published by the Free
7Software Foundation, either version 3 of the License, or (at your option)
8any later version.
10This program is distributed in the hope that it will be useful, but WITHOUT
11ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13more details.
15You should have received a copy of the GNU General Public License along with
16this program. If not, see <http://www.gnu.org/licenses/>.
17"""
19import numpy
20import progressbar
21import spam.deformation
22import spam.DIC
23import spam.label # for im1mask
25# 2017-05-29 ER and EA
26# This is spam's C++ DIC toolkit, but since we're in the tools/ directory we can import it directly
27from spam.DIC.DICToolkit import computeDICjacobian, computeDICoperators
29# numpy.set_printoptions(precision=3, suppress=True)
32def _errorCalc(im1, im2):
33 return numpy.nansum(numpy.square(numpy.subtract(im1, im2))) / numpy.nansum(im1)
36def register(
37 im1,
38 im2,
39 im1mask=None,
40 PhiInit=None,
41 PhiRigid=False,
42 PhiInitBinRatio=1.0,
43 margin=None,
44 maxIterations=25,
45 deltaPhiMin=0.001,
46 updateGradient=False,
47 interpolationOrder=1,
48 interpolator="python",
49 verbose=False,
50 imShowProgress=False,
51 imShowProgressNewFig=False,
52):
53 r"""
54 Perform subpixel image correlation between im1 and im2.
56 The result of register(im1, im2) will give a deformation function :math:`\Phi` which maps im1 into im2.
57 The Phi function used here allows the measurement of sub-pixel displacements, rotation, and linear straining of the whole image.
58 However, this function will numerically deform im2 until it best matches im1.
60 :math:`im1(x) = im2(\Phi.x)`
62 If im1 and im2 follow each other in time, then the resulting Phi is im1 -> im2 which makes sense in most cases.
63 "Discrete correlation" can be performed by masking im1.
65 im1 and im2 do not necessarily have to be the same size (`i.e.`, im2 can be bigger) -- this is good since there
66 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.
68 Parameters
69 ----------
70 im1 : 3D numpy array
71 The greyscale image that will not move -- must not contain NaNs
73 im2 : 3D numpy array
74 The greyscale image that will be deformed -- must not contain NaNs
76 im1mask : 3D boolean numpy array, optional
77 A mask for the zone to correlate in im1 with `False` in the zone to not correlate.
78 Default = None, `i.e.`, correlate all of im1 minus the margin.
79 If this is defined, the Phi returned is in the centre of mass of the mask
81 PhiInit : 4x4 numpy array, optional
82 Initial deformation to apply to im1.
83 Default = numpy.eye(4), `i.e.`, no transformation
85 PhiRigid : bool, optional
86 Run a rigid correlation? Only the rigid part of your PhiInit will be kept.
87 Default = False
89 PhiInitBinRatio : float, optional
90 Change translations in PhiInit, if it's been calculated on a differently-binned image. Default = 1
92 margin : int, optional
93 Margin, in pixels, to take in im1.
94 Can also be a N-component list of ints, representing the margin in ND.
95 If im2 has the same size as im1 this is strictly necessary to allow space for interpolation and movement
96 Default = None (`i.e.`, 10% of max dimension of im1)
98 maxIterations : int, optional
99 Maximum number of quasi-Newton iterations to perform before stopping. Default = 25
101 deltaPhiMin : float, optional
102 Smallest change in the norm of Phi (the transformation operator) before stopping. Default = 0.001
104 updateGradient : bool, optional
105 Should the gradient of the image be computed (and updated) on the deforming im2?
106 Default = False (it is computed once on im1)
108 interpolationOrder : int, optional
109 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
111 interpolator : string, optional
112 Which interpolation function to use from `spam`.
113 Default = 'python'. 'C' is also an option
115 verbose : bool, optional
116 Get to know what the function is really thinking, recommended for debugging only.
117 Default = False
119 imShowProgress : bool, optional
120 Pop up a window showing a ``imShowProgress`` slice of the image differences (im1-im2) as im1 is progressively deformed.
121 Default = False
123 imShowProgressNewFig : bool, optional (defaul = False)
124 Make a new plt.figure for each iteration, useful for examples gallery
126 Returns
127 -------
128 Dictionary :
130 'Phi' : 4x4 float array
131 Deformation function defined at the centre of the image
133 'returnStatus' : signed int
134 Return status from the correlation:
136 2 : Achieved desired precision in the norm of delta Phi
138 1 : Hit maximum number of iterations while iterating
140 -1 : Error is more than 80% of previous error, we're probably diverging
142 -2 : Singular matrix M cannot be inverted
144 -3 : Displacement > 5*margin
146 'error' : float
147 Error float describing mismatch between images, it's the sum of the squared difference divided by the sum of im1
149 'iterations' : int
150 Number of iterations
152 'deltaPhiNorm' : float
153 Norm of deltaPhi
155 Note
156 ----
157 This correlation was written in the style of S. Roux (especially "An extension of Digital Image Correlation for intermodality image registration")
158 especially equations 12 and 13.
159 """
160 # Explicitly set input images to floats
161 im1 = im1.astype("<f4")
162 im2 = im2.astype("<f4")
164 # initialise exit clause for singular "M" matrices
165 singular = False
167 # 2022-06-03 GP: Setting deltaPhiMin to only positive values
168 deltaPhiMin = numpy.abs(deltaPhiMin)
170 # Detect unpadded 2D image first:
171 if im1.ndim == 2:
172 # pad them
173 im1 = im1[numpy.newaxis, ...]
174 im2 = im2[numpy.newaxis, ...]
175 if im1mask is not None:
176 im1mask = im1mask[numpy.newaxis, ...]
178 # Detect 2D images
179 if im1.shape[0] == 1:
180 twoD = True
182 # Override interpolator for python in 2D
183 interpolator = "python"
185 # Define masks for M and A in 2D since we'll ignore the Z components
186 # Components of M and A which don't include Z
187 twoDmaskA = numpy.zeros((12), dtype=bool)
188 for i in [5, 6, 7, 9, 10, 11]:
189 twoDmaskA[i] = True
191 twoDmaskM = numpy.zeros((12, 12), dtype=bool)
192 for y in range(12):
193 for x in range(12):
194 if twoDmaskA[y] and twoDmaskA[x]:
195 twoDmaskM[y, x] = True
197 else:
198 twoD = False
200 if interpolationOrder > 1:
201 # Override interpolator for python for higher than linear
202 interpolator = "python"
204 # Automatically calculate margin if none is passed
205 # Detect default case and calculate maring necessary for a 45deg rotation with no displacement
206 if margin is None:
207 if twoD:
208 # z-margin will be overwritten below
209 margin = [1 + int(0.1 * min(im1.shape[1:]))] * 3
210 else:
211 margin = [1 + int(0.1 * min(im1.shape))] * 3
212 elif type(margin) == list:
213 pass
214 else:
215 # Make sure margin is an int
216 margin = int(margin)
217 margin = [margin] * 3
219 # Make sure im2 is bigger than im1 and check difference in size
220 # Get difference in image sizes. This should be positive, since we must always have enough data for im2 interpolation
221 im1im2sizeDiff = numpy.array(im2.shape) - numpy.array(im1.shape)
223 # Check im2 is bigger or same size
224 if (im1im2sizeDiff < 0).any():
225 print("\tcorrelate.register(): im2 is smaller than im1 in at least one dimension: im2.shape: {}, im1.shape: {}".format(im2.shape, im1.shape))
226 raise ValueError("correlate.register():DimProblem")
228 # Make sure margin is at least 1 for the gradient calculation
229 if twoD:
230 margin[0] = 0
231 elif min(margin) < 1 and min(im1im2sizeDiff) == 0:
232 margin = [1] * 3
234 # Calculate crops -- margin for im2 and more for im1 if it is bigger
235 # Margin + half the difference in size for im2 -- im1 will start in the middle.
236 crop2 = (
237 slice(
238 int(im1im2sizeDiff[0] / 2 + margin[0]),
239 int(im1im2sizeDiff[0] / 2 + im1.shape[0] - margin[0]),
240 ),
241 slice(
242 int(im1im2sizeDiff[1] / 2 + margin[1]),
243 int(im1im2sizeDiff[1] / 2 + im1.shape[1] - margin[1]),
244 ),
245 slice(
246 int(im1im2sizeDiff[2] / 2 + margin[2]),
247 int(im1im2sizeDiff[2] / 2 + im1.shape[2] - margin[2]),
248 ),
249 )
251 # Get subvolume crops from both images -- just the margin for im1
252 crop1 = (
253 slice(int(margin[0]), int(im1.shape[0] - margin[0])),
254 slice(int(margin[1]), int(im1.shape[1] - margin[1])),
255 slice(int(margin[2]), int(im1.shape[2] - margin[2])),
256 )
258 # Create im1 crop to shift less data
259 im1crop = im1[crop1].copy()
261 # Calculate effective margin
262 # to calculate displacement divergence
263 # using max for the margin -- subjective choice
264 max(margin) + min(im1im2sizeDiff) / 2
265 # print( "\tcorrelate.register(): realMargin is:", realMargin)
267 # If live plot is asked for, initialise canvas
268 if imShowProgress:
269 import matplotlib.pyplot as plt
271 # Plot ranges for signed residual
272 vmin = -im1crop.max()
273 vmax = im1crop.max()
274 if not imShowProgressNewFig:
275 # if imShowProgress == "Z" or imShowProgress == "z":
276 plt.subplot(3, 3, 1)
277 plt.axis([im1crop.shape[2], 0, im1crop.shape[1], 0])
278 plt.subplot(3, 3, 2)
279 plt.axis([im1crop.shape[2], 0, im1crop.shape[1], 0])
280 plt.subplot(3, 3, 3)
281 plt.axis([im1crop.shape[2], 0, im1crop.shape[1], 0])
282 # if imShowProgress == "Y" or imShowProgress == "y":
283 plt.subplot(3, 3, 4)
284 plt.axis([im1crop.shape[2], 0, im1crop.shape[0], 0])
285 plt.subplot(3, 3, 5)
286 plt.axis([im1crop.shape[2], 0, im1crop.shape[0], 0])
287 plt.subplot(3, 3, 6)
288 plt.axis([im1crop.shape[2], 0, im1crop.shape[0], 0])
289 # if imShowProgress == "X" or imShowProgress == "x":
290 plt.subplot(3, 3, 7)
291 plt.axis([im1crop.shape[1], 0, im1crop.shape[0], 0])
292 plt.subplot(3, 3, 8)
293 plt.axis([im1crop.shape[1], 0, im1crop.shape[0], 0])
294 plt.subplot(3, 3, 9)
295 plt.axis([im1crop.shape[1], 0, im1crop.shape[0], 0])
296 plt.ion()
298 ###########################################################
299 # Important -- since we're moving im2, initial Phis will be
300 # pointing the wrong way, they need to be inversed
301 ###########################################################
302 # If there is no initial Phi, initalise it and im1defCrop to zero.
303 if PhiInit is None:
304 Phi = numpy.eye(4)
305 im2def = im2.copy()
307 else:
308 # 2020-03-17 in isolation from COVID-19 EA and OS: Apparently this changes the PhiInit outside this function,
309 # Copying into different variable
310 # Apply binning on displacement
311 Phi = PhiInit.copy()
313 # If we're in rigid mode, keep only translations and rotations for this guess
314 # If you don't do this it goes mad (i.e., rigid updates to non-rigid guess don't seem to work)
315 if PhiRigid:
316 Phi = spam.deformation.computeRigidPhi(Phi.copy())
317 Phi[0:3, -1] *= PhiInitBinRatio
319 # invert PhiInit to apply it to im2
320 try:
321 PhiInv = numpy.linalg.inv(Phi.copy())
322 except numpy.linalg.linalg.LinAlgError:
323 PhiInv = numpy.eye(4)
325 # Since we are now using Fcentred for iterations, do nothing
326 # call decomposePhi to apply PhiInit (calculated on the centre of the image) to the origin (0,0,0)
327 if interpolator == "C":
328 im2def = spam.DIC.applyPhi(im2, Phi=PhiInv, interpolationOrder=interpolationOrder)
330 elif interpolator == "python":
331 im2def = spam.DIC.applyPhiPython(im2, Phi=PhiInv, interpolationOrder=interpolationOrder)
333 def computeGradient(im, twoD):
334 # Function to compute gradients
335 if twoD:
336 # If 2D image we have no gradients in the 1st direction
337 # if verbose: print("Calculating gradients...", end="")
338 imGradY, imGradX = numpy.gradient(im[0])
339 imGradX = imGradX[numpy.newaxis, ...]
340 imGradY = imGradY[numpy.newaxis, ...]
341 imGradZ = numpy.zeros_like(imGradX)
342 # if verbose: print("done")
343 else:
344 # if verbose: print("Calculating gradients...", end="")
345 imGradZ, imGradY, imGradX = numpy.gradient(im)
346 # if verbose: print("done ")
347 return imGradZ, imGradY, imGradX
349 # Apply stationary im1 mask
350 if im1mask is not None:
351 im1crop[im1mask[crop1] is False] = numpy.nan
353 # Initialise iteration variables
354 iterations = 0
355 returnStatus = 0
356 # Big value to start with to ensure the first iteration
357 deltaPhiNorm = 100.0
358 error = _errorCalc(im1crop, im2def[crop2])
360 if verbose:
361 print("Start correlation with Error = {:0.2f}".format(error))
363 widgets = [
364 " Iteration Number:",
365 progressbar.Counter(),
366 " ",
367 progressbar.FormatLabel(""),
368 " (",
369 progressbar.Timer(),
370 ")",
371 ]
372 pbar = progressbar.ProgressBar(widgets=widgets, maxval=maxIterations)
373 # widgets = [progressbar.FormatLabel(''), ' ', progressbar.Bar(), ' ', progressbar.AdaptiveETA()]
374 # pbar = progressbar.ProgressBar(widgets=widgets, maxval=numberOfNodes)
375 pbar.start()
377 # --- Start Iterations ---
378 while iterations < maxIterations and deltaPhiNorm > deltaPhiMin:
379 errorPrev = error
381 # On first iteration, compute hessian and jacobian in any case
382 # ...or if we've been asked to update Gradient
383 if iterations == 0 or updateGradient:
384 # If we've been asked to update gradient, compute it on im2, which is moving
385 if updateGradient:
386 imGradZ, imGradY, imGradX = computeGradient(im2def, twoD)
387 crop = crop2
388 # Otherwise compute it once and for all on the non-moving im1
389 else:
390 imGradZ, imGradY, imGradX = computeGradient(im1, twoD)
391 crop = crop1
393 M = numpy.zeros((12, 12), dtype="<f8")
394 A = numpy.zeros((12), dtype="<f8")
396 # Compute both DIC operators A and M with C library
397 computeDICoperators(
398 im1crop.astype("<f4"),
399 im2def[crop2].astype("<f4"),
400 imGradZ[crop].astype("<f4"),
401 imGradY[crop].astype("<f4"),
402 imGradX[crop].astype("<f4"),
403 M,
404 A,
405 )
406 else:
407 # just update jacobian A
408 A = numpy.zeros((12), dtype="<f8")
409 computeDICjacobian(
410 im1crop.astype("<f4"),
411 im2def[crop2].copy().astype("<f4"),
412 imGradZ[crop].copy().astype("<f4"),
413 imGradY[crop].copy().astype("<f4"),
414 imGradX[crop].copy().astype("<f4"),
415 A,
416 )
418 # Solve for delta Phi
419 if twoD:
420 # If a twoD image, cut out the bits of the M and A matrices that interest us
421 # This is necessary since the rest is super singular
422 # Solve for delta Phi
423 try:
424 deltaPhi = numpy.dot(numpy.linalg.inv(M[twoDmaskM].reshape(6, 6)), A[twoDmaskA])
425 except numpy.linalg.linalg.LinAlgError:
426 singular = True
427 break
428 # ...and now put deltaPhi components back in place for a 3D deltaPhi
429 deltaPhinew = numpy.zeros((12), dtype=float)
430 deltaPhinew[twoDmaskA] = deltaPhi
431 del deltaPhi
432 deltaPhi = deltaPhinew
433 else:
434 # Solve for delta Phi
435 try:
436 deltaPhi = numpy.dot(numpy.linalg.inv(M), A)
437 except numpy.linalg.linalg.LinAlgError:
438 singular = True
439 break
441 # If we're doing a rigid registration...
442 if PhiRigid:
443 # Add padding zeros
444 deltaPhi = numpy.hstack([deltaPhi, numpy.zeros(4)]).reshape((4, 4))
446 deltaPhiPlusI = numpy.eye(4) + deltaPhi
447 # Keep only rigid part of deltaPhi
448 deltaPhiPlusIrigid = spam.deformation.computeRigidPhi(deltaPhiPlusI.copy())
450 # Subtract I from the rigid dPhi+1, and compute norm only on first 3 rows
451 # ...basically recompute deltaPhiNorm only on rigid part
452 deltaPhiNorm = numpy.linalg.norm((deltaPhiPlusIrigid - numpy.eye(4))[0:3].ravel())
454 # Apply Delta Phi correction to Phi In Roux X-N paper equation number 11
455 Phi = numpy.dot(Phi, deltaPhiPlusIrigid)
457 else:
458 # The general, non-rigid case
459 deltaPhiNorm = numpy.linalg.norm(deltaPhi)
461 # Add padding zeros
462 deltaPhi = numpy.hstack([deltaPhi, numpy.zeros(4)]).reshape((4, 4))
464 # Update Phi
465 Phi = numpy.dot(Phi, (numpy.eye(4) + deltaPhi))
467 try:
468 PhiInv = numpy.linalg.inv(Phi.copy())
469 except numpy.linalg.linalg.LinAlgError:
470 singular = True
471 break
473 # reset im1def as emtpy matrix for deformed image
474 if interpolator == "C":
475 im2def = spam.DIC.applyPhi(im2, Phi=PhiInv, interpolationOrder=interpolationOrder)
476 elif interpolator == "python":
477 im2def = spam.DIC.applyPhiPython(im2, Phi=PhiInv, interpolationOrder=interpolationOrder)
479 # Error calculation
480 error = _errorCalc(im1crop, im2def[crop2])
482 # Keep interested people up to date with what's happening
483 # if verbose:
484 # print("Error = {:0.2f}".format(error)),
485 # print("deltaPhiNorm = {:0.4f}".format(deltaPhiNorm))
487 # Catch divergence condition after half of the max iterations
488 if errorPrev < error * 0.8 and iterations > maxIterations / 2:
489 # undo this bad Phi which has increased the error:
490 # Phi = numpy.dot((numpy.eye(4) + deltaPhi), Phi)
491 returnStatus = -1
492 if verbose:
493 print("\t -> diverging on error condition")
494 break
496 # Second divergence criterion on displacement (Issue #62)
497 # If any displcement is bigger than 5* the margin...
498 # if (numpy.abs(spam.deformation.decomposePhi(Phi.copy())['t']) > 5 * realMargin).any():
499 # if verbose: print("\t -> diverging on displacement condition")
500 # returnStatus = -3
501 # break
503 # 2018-10-02 - EA: Add divergence condition on U
504 trans = spam.deformation.decomposePhi(Phi.copy())
505 try:
506 volumeChange = numpy.linalg.det(trans["U"])
507 if volumeChange > 3 or volumeChange < 0.2:
508 if verbose:
509 print("\t -> diverging on volumetric change condition")
510 returnStatus = -3
511 break
512 except Exception:
513 returnStatus = -3
514 break
516 if imShowProgress:
517 # if imShowProgress == "Z" or imShowProgress == "z":
518 if imShowProgressNewFig:
519 plt.figure()
520 else:
521 plt.clf()
522 plt.suptitle("Iteration Number = {}".format(iterations), fontsize=10)
523 plt.subplot(3, 3, 1)
524 plt.title("im1 Z-slice")
525 plt.imshow(im1crop[im1crop.shape[0] // 2, :, :], cmap="Greys_r", vmin=0, vmax=vmax)
526 plt.subplot(3, 3, 2)
527 plt.title("im2def Z-slice")
528 plt.imshow(
529 im2def[crop2][im1crop.shape[0] // 2, :, :],
530 cmap="Greys_r",
531 vmin=0,
532 vmax=vmax,
533 )
534 plt.subplot(3, 3, 3)
535 plt.title("im1-im2def Z-slice")
536 plt.imshow(
537 numpy.subtract(im1crop, im2def[crop2])[im1crop.shape[0] // 2, :, :],
538 cmap="coolwarm",
539 vmin=vmin,
540 vmax=vmax,
541 )
542 # if imShowProgress == "Y" or imShowProgress == "y":
543 # if imShowProgressNewFig: plt.figure()
544 # else: plt.clf()
545 plt.subplot(3, 3, 4)
546 plt.title("im1 Y-slice")
547 plt.imshow(im1crop[:, im1crop.shape[1] // 2, :], cmap="Greys_r", vmin=0, vmax=vmax)
548 plt.subplot(3, 3, 5)
549 plt.title("im2def Y-slice")
550 plt.imshow(
551 im2def[crop2][:, im1crop.shape[1] // 2, :],
552 cmap="Greys_r",
553 vmin=0,
554 vmax=vmax,
555 )
556 plt.subplot(3, 3, 6)
557 plt.title("im1-im2def Y-slice")
558 plt.imshow(
559 numpy.subtract(im1crop, im2def[crop2])[:, im1crop.shape[1] // 2, :],
560 cmap="coolwarm",
561 vmin=vmin,
562 vmax=vmax,
563 )
564 # if imShowProgress == "X" or imShowProgress == "x":
565 # if imShowProgressNewFig: plt.figure()
566 # else: plt.clf()
567 plt.subplot(3, 3, 7)
568 plt.title("im1 X-slice")
569 plt.imshow(im1crop[:, :, im1crop.shape[2] // 2], cmap="Greys_r", vmin=0, vmax=vmax)
570 plt.subplot(3, 3, 8)
571 plt.title("im2def X-slice")
572 plt.imshow(
573 im2def[crop2][:, :, im1crop.shape[2] // 2],
574 cmap="Greys_r",
575 vmin=0,
576 vmax=vmax,
577 )
578 plt.subplot(3, 3, 9)
579 plt.title("im1-im2def X-slice")
580 plt.imshow(
581 numpy.subtract(im1crop, im2def[crop2])[:, :, im1crop.shape[2] // 2],
582 cmap="coolwarm",
583 vmin=vmin,
584 vmax=vmax,
585 )
586 plt.pause(0.01)
588 iterations += 1
590 if verbose:
591 decomposedPhi = spam.deformation.decomposePhi(Phi.copy())
592 widgets[3] = progressbar.FormatLabel(
593 " 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(
594 deltaPhiNorm,
595 error,
596 decomposedPhi["t"][0],
597 decomposedPhi["t"][1],
598 decomposedPhi["t"][2],
599 decomposedPhi["r"][0],
600 decomposedPhi["r"][1],
601 decomposedPhi["r"][2],
602 decomposedPhi["z"][0],
603 decomposedPhi["z"][1],
604 decomposedPhi["z"][2],
605 )
606 )
607 pbar.update(iterations)
609 # Positive return status is a healthy end of while loop:
610 if iterations >= maxIterations:
611 returnStatus = 1
612 if deltaPhiNorm <= deltaPhiMin:
613 returnStatus = 2
614 if singular:
615 returnStatus = -2
617 if verbose:
618 print()
619 # pbar.finish()
620 if iterations > maxIterations:
621 print("\t -> No convergence before max iterations")
622 if deltaPhiNorm <= deltaPhiMin:
623 print("\t -> Converged")
624 if singular:
625 print("\t -> Singular")
627 if im1mask is not None:
628 # If a mask on im1 is defined, return an Phi at the centre of the mass
629 maskCOM = spam.label.centresOfMass(im1mask[crop1])[-1]
630 # print("Mask COM", maskCOM)
631 # print( "\nNormal Phi:\n", Phi)
632 Phi[0:3, -1] = spam.deformation.decomposePhi(
633 Phi.copy(),
634 PhiCentre=(numpy.array(im1crop.shape) - 1) / 2.0,
635 PhiPoint=maskCOM,
636 )["t"]
637 # print( "\nF in mask:\n", F)
639 return {
640 "error": error,
641 "Phi": Phi,
642 "returnStatus": returnStatus,
643 "iterations": iterations,
644 "deltaPhiNorm": deltaPhiNorm,
645 }
648def registerMultiscale(
649 im1,
650 im2,
651 binStart,
652 binStop=1,
653 im1mask=None,
654 PhiInit=None,
655 PhiRigid=False,
656 PhiInitBinRatio=1.0,
657 margin=None,
658 maxIterations=100,
659 deltaPhiMin=0.0001,
660 updateGradient=False,
661 interpolationOrder=1,
662 interpolator="C",
663 verbose=False,
664 imShowProgress=False,
665 forceChangeScale=False,
666):
667 """
668 Perform multiscale subpixel image correlation between im1 and im2.
670 This means applying a downscale (binning) to the images, performing a Lucas and Kanade at that level,
671 and then improving it on a 2* less downscaled image, all the way back to the full scale image.
673 If your input images have multiple scales of texture, this should save significant time.
675 Please see the documentation for `register` for the rest of the documentation.
677 Parameters
678 ----------
679 im1 : 3D numpy array
680 The greyscale image that will not move -- must not contain NaNs
682 im2 : 3D numpy array
683 The greyscale image that will be deformed -- must not contain NaNs
685 binStart : int
686 Maximum amount of binning to apply, please input a number which is 2^int
688 binStop : int, optional
689 Which binning level to stop upscaling at.
690 The value of 1 (full image resolution) is almost always recommended (unless memory/time problems).
691 Default = 1
693 im1mask : 3D boolean numpy array, optional
694 A mask for the zone to correlate in im1 with `False` in the zone to not correlate.
695 Default = None, `i.e.`, correlate all of im1 minus the margin.
696 If this is defined, the Phi returned is in the centre of mass of the mask
698 PhiInit : 4x4 numpy array, optional
699 Initial deformation to apply to im1, by default at bin1 scale
700 Default = numpy.eye(4), `i.e.`, no transformation
702 PhiRigid : bool, optional
703 Run a rigid correlation? Only the rigid part of your PhiInit will be kept.
704 Default = False
706 PhiInitBinRatio : float, optional
707 Change translations in PhiInit, if it's been calculated on a differently-binned image. Default = 1
709 margin : int, optional
710 Margin, in pixels, to take in im1.
711 Can also be a N-component list of ints, representing the margin in ND.
712 If im2 has the same size as im1 this is strictly necessary to allow space for interpolation and movement
713 Default = 0 (`i.e.`, 10% of max dimension of im1)
715 maxIterations : int, optional
716 Maximum number of quasi-Newton iterations to perform before stopping. Default = 25
718 deltaPhiMin : float, optional
719 Smallest change in the norm of Phi (the transformation operator) before stopping. Default = 0.001
721 updateGradient : bool, optional
722 Should the gradient of the image be computed (and updated) on the deforming im2?
723 Default = False (it is computed once on im1)
725 interpolationOrder : int, optional
726 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
728 interpolator : string, optional
729 Which interpolation function to use from `spam`.
730 Default = 'python'. 'C' is also an option
732 verbose : bool, optional
733 Get to know what the function is really thinking, recommended for debugging only. Default = False
735 imShowProgress : bool, optional
736 Pop up a window showing a ``imShowProgress`` slice of the image differences (im1-im2) as im1 is progressively deformed.
737 Default = False
739 forceChangeScale : bool, optional
740 Change up a scale even if not converged?
741 Default = False
743 Returns
744 -------
745 Dictionary:
747 'Phi': 4x4 float array
748 Deformation function defined at the centre of the image
750 'returnStatus': signed int
751 Return status from the correlation:
753 2 : Achieved desired precision in the norm of delta Phi
755 1 : Hit maximum number of iterations while iterating
757 -1 : Error is more than 80% of previous error, we're probably diverging
759 -2 : Singular matrix M cannot be inverted
761 -3 : Displacement > 5*margin
763 'error': float
764 Error float describing mismatch between images, it's the sum of the squared difference divided by the sum of im1
766 'iterations': int
767 Number of iterations
768 """
769 # Detect unpadded 2D image first:
770 if len(im1.shape) == 2:
771 # pad them
772 im1 = im1[numpy.newaxis, ...]
773 im2 = im2[numpy.newaxis, ...]
774 if im1mask is not None:
775 im1mask = im1mask[numpy.newaxis, ...]
777 # Detect 2D images
778 if im1.shape[0] == 1:
779 twoD = True
780 else:
781 twoD = False
783 import math
785 logbinstart = math.log(binStart, 2)
786 if not logbinstart.is_integer():
787 print(
788 "spam.DIC.registerMultiscale(): You asked for an initial binning of",
789 binStart,
790 ",rounding it to ",
791 end="",
792 )
793 binStart = 2 ** numpy.round(logbinstart)
794 print(binStart)
796 logbinstop = math.log(binStop, 2)
797 if not logbinstop.is_integer():
798 print(
799 "spam.DIC.registerMultiscale(): You asked for a final binning of",
800 binStop,
801 ",rounding it to ",
802 end="",
803 )
804 binStop = 2 ** numpy.round(logbinstop)
805 print(binStop)
807 # If there is no initial Phi, initalise it and im1defCrop to zero.
808 if PhiInit is None:
809 reg = {"Phi": numpy.eye(4)}
810 else:
811 # Apply binning on displacement -- the /2 is to be able to *2 it in the LK call
812 tmp = PhiInit.copy()
813 tmp[0:3, -1] *= PhiInitBinRatio / 2.0 / float(binStart)
814 reg = {"Phi": tmp}
816 if im1mask is not None:
817 # Multiply up to 100 so we can apply a threshold below on binning in %
818 im1mask = im1mask.astype("<u1") * 100
820 # Sorry... This generates a list of binning levels, if binStart=8 and binStop=2 this will be [8, 4 ,2]
821 binLevels = 2 ** numpy.arange(math.log(binStart, 2), math.log(binStop, 2) - 1, -1).astype(int)
822 for binLevel in binLevels:
823 if verbose:
824 print(
825 "spam.DIC.registerMultiscale(): working on binning: ",
826 binLevel,
827 )
828 if binLevel > 1:
829 if twoD:
830 import scipy.ndimage
832 im1b = scipy.ndimage.zoom(im1[0], 1 / binLevel, order=1)
833 im2b = scipy.ndimage.zoom(im2[0], 1 / binLevel, order=1)
834 # repad them
835 im1b = im1b[numpy.newaxis, ...]
836 im2b = im2b[numpy.newaxis, ...]
837 if im1mask is not None:
838 im1maskb = scipy.ndimage.zoom(im1mask[0], 1 / binLevel, order=1)
839 im1maskb = im1maskb[numpy.newaxis, ...]
840 else:
841 im1maskb = None
842 else:
843 im1b = spam.DIC.binning(im1, binLevel)
844 im2b = spam.DIC.binning(im2, binLevel)
845 if im1mask is not None:
846 im1maskb = spam.DIC.binning(im1mask, binLevel) > 0
847 else:
848 im1maskb = None
849 else:
850 im1b = im1
851 im2b = im2
852 if im1mask is not None:
853 im1maskb = im1mask > 0
854 else:
855 im1maskb = None
857 # Automatically calculate margin if none is passed
858 # Detect default case and calculate margin necessary for a 45deg rotation with no displacement
859 if margin is None:
860 if twoD:
861 # z-margin will be overwritten below
862 marginB = [1 + int(0.1 * min(im1b.shape[1:]))] * 3
863 else:
864 marginB = [1 + int(0.1 * min(im1b.shape))] * 3
866 elif type(margin) == list:
867 marginB = (numpy.array(margin) // binLevel).tolist()
869 else:
870 # Make sure margin is an int
871 margin = int(margin)
872 margin = [margin] * 3
873 marginB = (numpy.array(margin) // binLevel).tolist()
875 reg = spam.DIC.register(
876 im1b,
877 im2b,
878 im1mask=im1maskb,
879 PhiInit=reg["Phi"],
880 PhiRigid=PhiRigid,
881 PhiInitBinRatio=2.0,
882 margin=marginB,
883 maxIterations=maxIterations,
884 deltaPhiMin=deltaPhiMin,
885 updateGradient=updateGradient,
886 interpolationOrder=interpolationOrder,
887 interpolator=interpolator,
888 verbose=verbose,
889 imShowProgress=imShowProgress,
890 )
892 if reg["returnStatus"] != 2 and not forceChangeScale:
893 if verbose:
894 print("spam.DIC.registerMultiscale(): binning {} did not converge (return Status = {}), not continuing".format(binLevel, reg["returnStatus"]))
895 # Multiply up displacement and return bad result
896 reg["Phi"][0:3, -1] *= float(binLevel)
897 return reg
899 binLevel = int(binLevel / 2)
901 # Return displacments at bin1 scale
902 reg["Phi"][0:3, -1] *= float(binStop)
903 return reg