Source code for spam.DIC.multimodal

"""
Library of SPAM multimodal image correlation functions using a Gaussian Mixture model from Tudisco et al. 2017.
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/>.
"""

# 2017-05-29 ER and EA

import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy
import scipy.ndimage
import spam.deformation
import spam.DIC
import spam.helpers
from spam.DIC.DICToolkit import computeDICoperatorsGM, computeGMresidualAndPhase

numpy.set_printoptions(precision=3, suppress=True)
mpl.rc("font", size=6)
cmapPhases = "Set1_r"


[docs] def multimodalRegistration( im1, im2, phaseDiagram, gaussianParameters, im1mask=None, PhiInit=None, margin=None, maxIterations=50, deltaPhiMin=0.005, interpolationOrder=1, verbose=False, GRAPHS=False, INTERACTIVE=False, sliceAxis=0, suffix="", rootPath=".", BINS=64, ): """ Perform subpixel image correlation between im1 and im2 -- images of the same object acquired with different modalities. This function will deform im2 until it best matches im1. The matching includes sub-pixel displacements, rotation, and linear straining of the whole image. The correlation of im1, im2 will give a deformationFunction F which maps im1 into im2. Phi(im1(x),im2(F.x)) = 0 As per Equation (3) of Tudisco `et al.` "Discrete correlation" can be performed by masking im1. Parameters ---------- im1 : 3D numpy array The greyscale image that will not move im2 : 3D numpy array The greyscale image that will be deformed phaseDiagram : Nbins x Nbins numpy array of ints Pre-labelled phase diagram, which is a joint histogram with the phases labelled gaussianParameters : 2D numpy array Nx6 Key parameter which is the result of a 2D gaussian fit of the first N peaks in the joint histograms of im1 and im2. The 6 parameters of the fit are: φ, μ(im1), μ(im2), and a, b, c, where {a,b,c} are the parameter that can be found for `two-dimensional elliptical Gaussian function` here: https://en.wikipedia.org/wiki/Gaussian_function, `i.e.`, coupled with im1², im1*im2 and im2² respectively im1mask : 3D numpy array, float, optional A mask for the zone to correlate in im1 with NaNs in the zone to not correlate. Default = None, `i.e.`, correlate all of im1 minus the margin PhiInit : 4x4 numpy array, optional Initial transformation to apply. Default = numpy.eye(4), `i.e.`, no transformation margin : int, optional Margin, in pixels, to take in im1 and im2 to allow space for interpolation and movement. Default = None (`i.e.`, enough margin for a worse-case 45degree rotation with no displacement) maxIterations : int, optional Maximum number of quasi-Newton interations to perform before stopping. Default = 25 deltaPhiMin : float, optional Smallest change in the norm of F (the transformation operator) before stopping. Default = 0.001 interpolationOrder : int, optional Order of the greylevel interpolation for applying F to im1 when correlating. Reccommended value is 3, but you can get away with 1 for faster calculations. Default = 3 verbose : bool, optional Get to know what the function is really thinking, recommended for debugging only. Default = False GRAPHS : bool, optional Pop up a window showing the image differences (im1-im2) as im1 is progressively deformed. Returns -------- Dictionary: 'transformation' Dictionary containing: 't' : 3-component vector Z, Y, X displacements in pixels 'r' : 3-component vector Z, Y, X components of rotation vector 'Phi': 4 x 4 numpy array of floats Deformation function, Phi 'returnStatus': 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 cannot be inverted -3 : Displacement > 5*margin 'iterations': int Number of iterations 'logLikelyhood' : float Number indicating quality of match 'deltaPhiNorm' : float Size of last Phi step 'residualField': 3D numpy array of floats Same size as input image, residual field 'phaseField': phaseField Same size as input image, labelled phases Note ---- This correlation is what is proposed in Tudisco et al. "An extension of Digital Image Correlation for intermodality image registration", section 4.3. """ # if verbose: # print("Enter registration") # for interactive graphs if INTERACTIVE: GRAPHS = True plt.ion() # Detect default case and calculate maring necessary for a 45deg rotation with no displacement if margin is None: # sqrt margin = int((3**0.5 - 1.0) * max(im1.shape) * 0.5) else: # Make sure margin is an int margin = int(margin) # Exit clause for singular matrices singular = False crop = ( slice(margin, im1.shape[0] - margin), slice(margin, im1.shape[1] - margin), slice(margin, im1.shape[2] - margin), ) # Figure out coordinates on which the correlation should happen, i.e., the non NaN ones # NOTE: these coordinates are within the cropped margin for interpolation # cropImCoordsFG = numpy.where( numpy.isfinite( im1[crop] ) ) if im1mask is not None: # TODO: This could just be directly = mask and equals inv of mask # i.e., arry a boolean mask and not a series of coord array # cropImCoordsFG = numpy.where(im1mask[crop] is True) # cropImCoordsBG = numpy.where(im1mask[crop] is False) pass else: # Everything regular, except that we might have NaNs in the CW... # cropImCoordsFG = numpy.ones_like( im1[crop], dtype='bool' ) # cropImCoordsBG = numpy.zeros_like( im1[crop], dtype='bool' ) # cropImCoordsFG = numpy.isfinite(im1[crop], dtype="bool") # cropImCoordsBG = numpy.isnan(im1[crop], dtype="bool") # HACK # print cropImCoordsFG # print cropImCoordsBG # HACK: set nans in im2 to zero im2[numpy.isnan(im2)] = 0.0 # if numpy.isnan( im1[crop] ).sum() > 0 numpy.isnan( im2[crop] ).sum() > 0: # compute image centre # im1centre = (numpy.array(im1.shape)-1)/2.0 # If there is no initial Phi, initalise it to zero. if PhiInit is None: Phi = numpy.eye(4) im2def = im2.copy() # If there is an initial guess... else: Phi = PhiInit.copy() # invert PhiInit to apply it to im2 try: PhiInv = numpy.linalg.inv(Phi.copy()) except numpy.linalg.linalg.LinAlgError: PhiInv = numpy.eye(4) im2def = spam.DIC.applyPhi(im2, Phi=PhiInv, interpolationOrder=interpolationOrder).astype(im2.dtype) # At this stage we've computed gradients which we are not going to update, im2 and it's gradients will be set equal to # their cropped versions: # im2def = im2def[crop] # im2defGradZ = im2defGradZ[crop] # im2defGradY = im2defGradY[crop] # im2defGradX = im2defGradX[crop] # Mask nans in gradient (but not before or there are jumps when the grey goes to zero # beacuse of this a label dilate of at least 1 is recommended) # im2defGradZ[ numpy.where( numpy.isnan( im1gradZ ) ) ] = 0 # im2defGradY[ numpy.where( numpy.isnan( im1gradY ) ) ] = 0 # im2defGradX[ numpy.where( numpy.isnan( im1gradX ) ) ] = 0 iterations = 0 returnStatus = 0 deltaPhiNorm = 0.0 # compute initial Log likelyhood # p, _, _ = numpy.histogram2d(im1[crop].ravel(), im2def[crop].ravel(), bins=BINS, range=[[0.0, BINS], [0.0, BINS]], normed=False, weights=None) # LL = 0.0 # for v in p.ravel(): # if v: # LL += numpy.log(v) # LL = numpy.prod(p[numpy.where(p > 0)]) p, _, _ = numpy.histogram2d( im1[crop].ravel(), im2def[crop].ravel(), bins=BINS, range=[[0.0, BINS], [0.0, BINS]], # normed=False, weights=None, ) LL = numpy.sum(numpy.log(p[numpy.where(p > 0)])) if verbose: print("\tInitial state LL = {:0.2f}".format(LL)) print("\tIteration Number {:03d} ".format(iterations), end="") print("LL = {:0.2f} ".format(LL), end="") print("dPhi = {:0.4f} ".format(deltaPhiNorm), end="") # print("\nPhi", Phi ) # currentTransformation = spam.deformation.decomposePhi(Phi, PhiPoint=im1centre) currentTransformation = spam.deformation.decomposePhi(Phi) print( "Tr = {: .3f}, {: .3f}, {: .3f} ".format(*currentTransformation["t"]), end="", ) print( "Ro = {: .3f}, {: .3f}, {: .3f} ".format(*currentTransformation["r"]), end="", ) print("Zo = {: .3f}, {: .3f}, {: .3f} ".format(*currentTransformation["z"])) # Use gradient of image 1 which does not move # im1GradZ, im1GradY, im1GradX = [g.astype('<f4') for g in numpy.gradient(im1)] # im2GradZ, im2GradY, im2GradX = [g.astype('<f4') for g in numpy.gradient(im2def)] while (iterations <= maxIterations and deltaPhiNorm > deltaPhiMin) or iterations == 0: previousLL = LL if verbose: print("\tIteration Number {:03d} ".format(iterations + 1), end="") im2defGradZ, im2defGradY, im2defGradX = (g.astype("<f4") for g in numpy.gradient(im2def)) M = numpy.zeros((12, 12), dtype="<f8") A = numpy.zeros((12), dtype="<f8") # im2 updated computeDICoperatorsGM( im1[crop].astype("<f4"), im2def[crop].astype("<f4"), im2defGradZ[crop].astype("<f4"), im2defGradY[crop].astype("<f4"), im2defGradX[crop].astype("<f4"), phaseDiagram.astype("<u1"), gaussianParameters.astype("<f8"), M, A, ) # im2 with guess # computeDICoperatorsGM(im1[crop].astype("<f4"), # im2def[crop].astype("<f4"), # im2GradZ[crop].astype("<f4"), # im2GradY[crop].astype("<f4"), # im2GradX[crop].astype("<f4"), # phaseDiagram.astype("<u1"), # gaussianParameters.astype("<f8"), # M, A) # im1 # computeDICoperatorsGM(im1[crop].astype("<f4"), # im2def[crop].astype("<f4"), # im1GradZ[crop].astype("<f4"), # im1GradY[crop].astype("<f4"), # im1GradX[crop].astype("<f4"), # phaseDiagram.astype("<u1"), # gaussianParameters.astype("<f8"), # M, A) try: deltaPhi = numpy.dot(numpy.linalg.inv(M), A) except numpy.linalg.linalg.LinAlgError: # no cover singular = True # TODO: Calculate error for clean break. print("\tsingular M matrix") print("exiting") exit() # break deltaPhiNorm = numpy.linalg.norm(deltaPhi) # Add padding zeros deltaPhi = numpy.hstack([deltaPhi, numpy.zeros(4)]).reshape((4, 4)) # In Roux X-N paper equation number 11 # Phi = numpy.dot((numpy.eye(4) - deltaPhi), Phi) Phi = numpy.dot(Phi, (numpy.eye(4) + deltaPhi)) # currentTransformation = spam.deformation.decomposePhi(Phi, PhiPoint=im1centre) currentTransformation = spam.deformation.decomposePhi(Phi) # Solve for delta Phi try: PhiInv = numpy.linalg.inv(Phi.copy()) except numpy.linalg.linalg.LinAlgError: singular = True break im2def = spam.DIC.applyPhi(im2, Phi=PhiInv, interpolationOrder=interpolationOrder) residualField = numpy.zeros_like(im2[crop], dtype="<f4") phaseField = numpy.zeros_like(im2[crop], dtype="<u1") computeGMresidualAndPhase( im1[crop].astype("<f4"), im2def[crop].astype("<f4"), phaseDiagram.astype("<u1"), gaussianParameters.astype("<f8"), residualField, phaseField, ) # computeGMresidualAndPhase(im1[crop].astype("<f4"), # im2def[crop].astype("<f4"), # phaseDiagram.copy().astype("<u1"), # gaussianParameters.copy().astype("<f8"), # residualField, # phaseField) # compute current log likelyhood # p, _, _ = numpy.histogram2d(im1[crop].ravel(), im2def[crop].ravel(), bins=BINS, range=[[0.0, BINS], [0.0, BINS]], normed=False, weights=None) # LL = 0.0 # for v in p.ravel(): # if v: # LL += numpy.log(v) p, _, _ = numpy.histogram2d( im1[crop].ravel(), im2def[crop].ravel(), bins=BINS, range=[[0.0, BINS], [0.0, BINS]], # normed=False, weights=None, ) LL = numpy.sum(numpy.log(p[numpy.where(p > 0)])) if verbose: print("LL = {:0.2f} ".format(LL), end="") print("dPhi = {:0.4f} ".format(deltaPhiNorm), end="") # print("\nPhi", Phi ) print( "Tr = {: .3f}, {: .3f}, {: .3f} ".format(*currentTransformation["t"]), end="", ) print( "Ro = {: .3f}, {: .3f}, {: .3f} ".format(*currentTransformation["r"]), end="", ) print("Zo = {: .3f}, {: .3f}, {: .3f} ".format(*currentTransformation["z"])) if previousLL < LL * 0.6: # undo this bad Phi which has increased the LL: Phi = numpy.dot((numpy.eye(4) + deltaPhi), Phi) returnStatus = -1 print("Log-likelyhood increasing, divergence condition detected, exiting.") # break print("...no actually continuing...") if GRAPHS: NPHASES = gaussianParameters.shape[0] grid = plt.GridSpec(2, 4) plt.clf() plt.suptitle( "Gaussian Mixture {} iteration number {} " "|deltaPhi|={:.5f} \nT = [{: 2.4f} {: 2.4f} {:.4f}]\n" "R = [{: 2.4f} {: 2.4f} {: 2.4f}]\n" "Z = [{: 2.4f} {: 2.4f} {: 2.4f}]".format( suffix, iterations, deltaPhiNorm, currentTransformation["t"][0], currentTransformation["t"][1], currentTransformation["t"][2], currentTransformation["r"][0], currentTransformation["r"][1], currentTransformation["r"][2], currentTransformation["z"][0], currentTransformation["z"][1], currentTransformation["z"][2], ) ) plt.subplot(grid[0, 0]) plt.axis("off") plt.title("Residual field") if sliceAxis == 0: plt.imshow(residualField[residualField.shape[0] // 2, :, :]) elif sliceAxis == 1: plt.imshow(residualField[:, residualField.shape[1] // 2, :]) elif sliceAxis == 2: plt.imshow(residualField[:, :, residualField.shape[2] // 2]) # plt.colorbar() plt.subplot(grid[0, 1]) plt.axis("off") plt.title("Phase field") if sliceAxis == 0: plt.imshow( phaseField[phaseField.shape[0] // 2, :, :], vmin=-0.5, vmax=NPHASES + 0.5, cmap=mpl.cm.get_cmap(cmapPhases, NPHASES + 1), ) elif sliceAxis == 1: plt.imshow( phaseField[:, phaseField.shape[1] // 2, :], vmin=-0.5, vmax=NPHASES + 0.5, cmap=mpl.cm.get_cmap(cmapPhases, NPHASES + 1), ) elif sliceAxis == 2: plt.imshow( phaseField[:, :, phaseField.shape[2] // 2], vmin=-0.5, vmax=NPHASES + 0.5, cmap=mpl.cm.get_cmap(cmapPhases, NPHASES + 1), ) # plt.colorbar(ticks=numpy.arange(0, NPHASES+1)) plt.subplot(grid[1, 0]) plt.axis("off") plt.title("im1") if sliceAxis == 0: plt.imshow(im1[crop][im1[crop].shape[0] // 2, :, :]) elif sliceAxis == 1: plt.imshow(im1[crop][:, im1[crop].shape[1] // 2, :]) elif sliceAxis == 2: plt.imshow(im1[crop][:, :, im1[crop].shape[2] // 2]) # plt.colorbar() plt.subplot(grid[1, 1]) plt.axis("off") plt.title("im2def") if sliceAxis == 0: plt.imshow(im2def[crop][im2def[crop].shape[0] // 2, :, :]) elif sliceAxis == 1: plt.imshow(im2def[crop][:, im2def[crop].shape[1] // 2, :]) elif sliceAxis == 2: plt.imshow(im2def[crop][:, :, im2def[crop].shape[2] // 2]) # plt.colorbar() plt.subplot(grid[:, 2:]) plt.axis("off") plt.title("Checker Board") if sliceAxis == 0: plt.imshow( spam.helpers.checkerBoard( im1[crop][im2def[crop].shape[0] // 2, :, :], im2def[crop][im2def[crop].shape[0] // 2, :, :], ) ) elif sliceAxis == 1: plt.imshow( spam.helpers.checkerBoard( im1[crop][:, im2def[crop].shape[1] // 2, :], im2def[crop][:, im2def[crop].shape[1] // 2, :], ) ) elif sliceAxis == 2: plt.imshow( spam.helpers.checkerBoard( im1[crop][:, :, im2def[crop].shape[2] // 2], im2def[crop][:, :, im2def[crop].shape[2] // 2], ) ) if INTERACTIVE: plt.show() plt.pause(1.0) else: plt.savefig( "{}/GaussianMixture_Iteration-{}-{}.png".format(rootPath, iterations, suffix), dpi=600, ) iterations += 1 if INTERACTIVE: plt.ioff() plt.close() # Positive return status is a healthy end of while loop: if iterations >= maxIterations: returnStatus = 1 if deltaPhiNorm <= deltaPhiMin: returnStatus = 2 if singular: returnStatus = -2 if verbose: print() # pbar.finish() if iterations > maxIterations: print("\t -> No convergence before max iterations") if deltaPhiNorm <= deltaPhiMin: print("\t -> Converged") if singular: print("\t -> Singular") return { "transformation": currentTransformation, "Phi": Phi, "returnStatus": returnStatus, "iterations": iterations, "residualField": residualField, "phaseField": phaseField, "logLikelyhood": LL, "deltaPhiNorm": deltaPhiNorm, }
################################################################ # helper functions for correlation with different modalities ################################################################
[docs] def gaussianMixtureParameters( im1, im2, BINS=64, NPHASES=2, im1threshold=0, im2threshold=0, distanceMaxima=None, excludeBorder=False, fitDistance=None, GRAPHS=False, INTERACTIVE=False, sliceAxis=0, rootPath=".", suffix="", ): """ This function, given two different modality images, builds the joint histogram in BINS bins, and fits NPHASES peaks with bivariate Gaussian distributions. Parameters ---------- im1 : 3D numpy array of floats One image, values should be rescaled to 0:BIN-1 im2 : 3D numpy array of floats Another image, values should be rescaled to 0:BIN-1 BINS : int, optional Number of bins for the joint histogram, recommend 2^x Default = 64 NPHASES : int, optional Number of phases to automatically fit Default = 2 im1threshold : float, optional im2threshold : float, optional distanceMaxima : float, optional excludeBorder : False, optional Exclude edges in peak finding? This is passed to skimage.feature.peak_local_max() fitDistance : float, optional GRAPHS : bool, optional INTERACTIVE : bool, optional sliceAxis=0 rootPath="." suffix="" Returns ------- gaussianParameters : list of lists List of NPHASES components, containing the parameters of the bivariate Gaussian fit for each phase: [0] = GLOBALphi -- Normlised "Height" of the fit [1] = GLOBALmu1 -- Mean in F of bivairate Gaussian [2] = GLOBALmu2 -- Mean in G of bivairate Gaussian [3] = a -- [4] = b -- [5] = c -- p : BINSxBINS 2D numpy array of floats The normalised joint histogram, the sum of which will be =1 if all of your image values are 0:BIN-1 """ # To fit 2D likelyhood with gaussian ellipsoids # To find maxima in likelyhood which correspond to phases import skimage.feature from scipy.optimize import curve_fit # for interactive graphs if INTERACTIVE: GRAPHS = True plt.ion() # DEFINE the global variables needed for curve fitting GLOBALmu1 = 0.0 # mean of the f image GLOBALmu2 = 0.0 # mean of the g image GLOBALphi = 0.0 # number of hits (value of the maxima) # DEFINE fitting functions # https://en.wikipedia.org/wiki/Gaussian_function#Two-dimensional_Gaussian_function # def gaussian2Delliptical( XY, GLOBALphi, GLOBALmu2, GLOBALmu1, a, b, c ): # Thsi needs to be in here in order to pass GLOBALphi as a global variable. # Perhaps it should be optimised? def computeLambda(a, b, c, x, GLOBALmu1, y, GLOBALmu2): return numpy.longfloat(0.5 * (a * (x - GLOBALmu1) ** 2 + 2.0 * b * (x - GLOBALmu1) * (y - GLOBALmu2) + c * (y - GLOBALmu2) ** 2)) def gaussian2Delliptical(XY, a, b, c): # invert x and y on purpose to be consistent with H grid = numpy.array(numpy.meshgrid(XY[1], XY[0])) field = numpy.zeros(grid.shape[1:3]) for ny in range(grid.shape[2]): y = grid[0, 0, ny] for nx in range(grid.shape[1]): x = grid[1, nx, 0] field[nx, ny] = float(GLOBALphi) * numpy.exp(-computeLambda(a, b, c, x, GLOBALmu1, y, GLOBALmu2)) return field.ravel() # START function im1min = im1.min() im1max = im1.max() im2min = im2.min() im2max = im2.max() # f,g discretisation X = numpy.linspace(0, BINS - 1, BINS) Y = numpy.linspace(0, BINS - 1, BINS) print("\tim1 from {:.2f} to {:.2f}".format(im1min, im1max)) print("\tim2 from {:.2f} to {:.2f}".format(im2min, im2max)) # Calculate probability distribution p(f,g) p, _, _ = numpy.histogram2d( im1.ravel(), im2.ravel(), bins=BINS, range=[[0.0, BINS], [0.0, BINS]], # normed=False, weights=None, ) p /= float(len(im1.ravel())) p_sum = p.sum() print(f"\tp normalisation: {p_sum:.2f}") # write joint histogram in a dat file for tikz # if DATA: # tmp = p.copy() # with open("GaussianMixture_jointHistogram-{}-{}.dat".format(0, suffix), "w") as f: # string = "{}\t {}\t {}\n".format("x", "y", "c") # f.write(string) # for xbin in range(tmp.shape[0]): # x = (xbin+0.5)/tmp.shape[0] # for ybin in range(tmp.shape[1]): # y = (ybin+0.5)/tmp.shape[1] # if tmp[xbin, ybin]: # string = "{}\t {}\t {}\n".format(x, y, tmp[xbin, ybin]) # f.write(string) # Get disordered maxima of the joint histogram print("\tFind maxima") if distanceMaxima is None: distanceMaxima = int(BINS / 25.0) distanceMaxima = max(1, int(distanceMaxima)) print(f"\t\tMin distance between maxima: {distanceMaxima:.0f}") maxima = skimage.feature.peak_local_max(p, min_distance=int(distanceMaxima), exclude_border=excludeBorder) nMax = len(maxima) if nMax < NPHASES: print(f"\t\t{NPHASES} phases asked but only {nMax} maxima...") NPHASES = min(NPHASES, nMax) # print "\t\t Number of maxima: {:2}".format(nMax) if nMax == 0: # no cover print("In gaussian fit: no maxium found... Stoping...") exit() # Organise maxima into muF, muG, p(f,g) the sort at take the maximum maxTmp = numpy.zeros((nMax, 3)) for i, (f, g) in enumerate(maxima): if f > im1threshold or g > im2threshold: maxTmp[i] = [f, g, p[f, g]] # print("\t\t max {:2} --- f: {:.2f} g: {:.2f} hits: {}".format(i+1,*maxTmp[i])) nMax = 0 percentage = 0.1 while nMax < NPHASES: maxSorted = [m for m in maxTmp[maxTmp[:, 2].argsort()[::-1]] if float(m[2]) > percentage * float(p_sum)] nMax = len(maxSorted) print(f"\t\t{nMax:02d} maxima found over the {NPHASES} needed with {percentage:.2e} times of the total count") percentage /= 10.0 for i, (GLOBALmu1, GLOBALmu2, GLOBALphi) in enumerate(maxSorted): print(f"\t\tMaximum {i + 1}:\t mu1={GLOBALmu1:.2f}\t mu2={GLOBALmu2:.2f}\t Phi={GLOBALphi:.2e}") print("") # output of the function: list of fitting gaussian parameters # size NPHASES x 6, the 6 parameters being GLOBALlogPhi, GLOBALmu1, GLOBALmu2, a, b, c gaussianParameters = numpy.zeros((NPHASES, 6)).astype("<f4") # loop over phases maxEllipsoid = numpy.zeros_like(p) for iPhase in range(NPHASES): GLOBALmu1, GLOBALmu2, GLOBALphi = maxSorted[iPhase] print(f"\tPhase {iPhase + 1:2}:\t\t mu1={GLOBALmu1:.2f}\t mu2={GLOBALmu2:.2f}\t Phi={GLOBALphi:.2e}") if fitDistance is None: fitDistance = BINS / 2.0 # fit the probability distribution p(f,g) with an gaussian ellipsoids pFit = p.copy() for nf in range(pFit.shape[0]): for ng in range(pFit.shape[1]): posF = nf posG = ng dist = numpy.sqrt((posF - GLOBALmu1) ** 2.0 + (posG - GLOBALmu2) ** 2.0) # cicrle # dist = abs(posF-GLOBALmu1)+abs(posG-GLOBALmu2) # square if dist > fitDistance: pFit[nf, ng] = 0.0 (a, b, c), _ = curve_fit(gaussian2Delliptical, (X, Y), pFit.ravel(), p0=(1, 1, 1)) print(f"\t\tFit:\t\t a={a:.2f}\t b={b:.2f}\t c={c:.2f}\t Hessian: {a * c - b**2:.2f}") while a * c - b**2 < 0: print("\t\t\tWarning: Hessian < 0") print("\t\t\t The gaussian doesn't have a local maximum.") fitDistance /= 2.0 print(f"\t\t\t Try with fit distance = {fitDistance} ") for nf in range(pFit.shape[0]): for ng in range(pFit.shape[1]): posF = nf / float(pFit.shape[0] - 1) posG = ng / float(pFit.shape[1] - 1) dist = numpy.sqrt((posF - GLOBALmu1) ** 2.0 + (posG - GLOBALmu2) ** 2.0) # cicrle # dist = abs(posF-GLOBALmu1)+abs(posG-GLOBALmu2) # square if dist > fitDistance: pFit[nf, ng] = 0.0 (a, b, c), _ = curve_fit(gaussian2Delliptical, (X, Y), pFit.ravel(), p0=(1, 1, 1)) print(f"\t\tFit:\t\t a={a:.2f}\t b={b:.2f}\t c={c:.2f}\t Hessian: {a * c - b**2:.2f}") # compute covariance function cov = scipy.linalg.inv(numpy.array([[a, b], [b, c]])) print(f"\t\tCov:\t\t 1,1={cov[0, 0]:.4f}\t 1,2={cov[1, 0]:.4f}\t 2,2={cov[1, 1]:.4f}") gaussianParameters[iPhase, 0] = GLOBALphi gaussianParameters[iPhase, 1] = GLOBALmu1 gaussianParameters[iPhase, 2] = GLOBALmu2 gaussianParameters[iPhase, 3] = a gaussianParameters[iPhase, 4] = b gaussianParameters[iPhase, 5] = c currentEllipsoid = gaussian2Delliptical((X, Y), a, b, c).reshape((len(X), len(Y))) maxEllipsoid = numpy.maximum(maxEllipsoid, currentEllipsoid) if GRAPHS: plt.clf() plt.suptitle(f"Gaussian Mixture fitting Phase number {iPhase + 1}") plt.subplot(221) plt.title(f"im1 (from {im1.min():.2f} to {im1.max():.2f})") plt.axis("off") if sliceAxis == 0: plt.imshow(im1[im1.shape[0] // 2, :, :], vmin=0.0, vmax=BINS) elif sliceAxis == 1: plt.imshow(im1[:, im1.shape[1] // 2, :], vmin=0.0, vmax=BINS) elif sliceAxis == 2: plt.imshow(im1[:, :, im1.shape[2] // 2], vmin=0.0, vmax=BINS) plt.colorbar() plt.subplot(222) plt.title(f"im2 (from {im2.min():.2f} to {im2.max():.2f})") plt.axis("off") if sliceAxis == 0: plt.imshow(im2[im2.shape[0] // 2, :, :], vmin=0.0, vmax=BINS) elif sliceAxis == 1: plt.imshow(im2[:, im2.shape[1] // 2, :], vmin=0.0, vmax=BINS) elif sliceAxis == 2: plt.imshow(im2[:, :, im2.shape[2] // 2], vmin=0.0, vmax=BINS) plt.colorbar() plt.subplot(223) tmp = p.copy() tmp[p <= 0] = numpy.nan tmp = numpy.log(tmp) plt.title("Log Probability log(p(im1,im2))") plt.imshow(tmp.T, origin="lower", extent=[0.0, BINS, 0.0, BINS]) for gp in maxSorted: plt.plot(gp[0], gp[1], "b*") plt.plot(GLOBALmu1, GLOBALmu2, "r*") fig = plt.gcf() ax = fig.gca() ax.add_artist(plt.Circle((GLOBALmu1, GLOBALmu2), fitDistance, color="r", fill=False)) plt.xlabel("im1") plt.ylabel("im2") plt.colorbar() plt.subplot(224) tmp = currentEllipsoid.copy() tmp[currentEllipsoid <= 0] = numpy.nan tmp = numpy.log(tmp) plt.title("Gaussian ellipsoid") plt.imshow(tmp.T, origin="lower", extent=[0.0, BINS, 0.0, BINS]) plt.plot(GLOBALmu1, GLOBALmu2, "r*") plt.xlabel("im1") plt.ylabel("im2") plt.colorbar() if INTERACTIVE: plt.show() plt.pause(1.0) else: plt.savefig( f"{rootPath}/GaussianMixture_jointHistogram-{iPhase + 1}-{suffix}.png", dpi=600, ) # p -= currentEllipsoid # if DATA: # write joint histogram in a dat file for tikz # tmp = p.copy() # # tmp_sum = tmp.sum() # with open("GaussianMixture_jointHistogram-{}-{}.dat".format(iPhase+1, suffix), "w") as f: # string = "{}\t {}\t {}\n".format("x", "y", "c") # f.write(string) # for xbin in range(tmp.shape[0]): # x = (xbin+0.5)/tmp.shape[0] # for ybin in range(tmp.shape[1]): # y = (ybin+0.5)/tmp.shape[1] # if tmp[xbin, ybin]: # string = "{}\t {}\t {}\n".format(x, y, float(tmp[xbin, ybin])) # f.write(string) print("") # end of phase loop if INTERACTIVE: plt.ioff() plt.close() # write phase histogram in a dat file for tikz # if DATA: # print("\tSave phase repartition") # levels = [10**float(e) for e in numpy.arange(-8,0) ] # # contour # plt.clf() # tmp = maxEllipsoid.copy() # plt.title("Maximum gaussian ellispoid") # X = numpy.linspace(0, 1, BINS) # Y = numpy.linspace(0, 1, BINS) # CS = plt.contour(X, Y, tmp.T, levels=levels) # for gp in maxSorted: # plt.plot(gp[0], gp[1], 'b*') # plt.xlabel("f") # plt.ylabel("g") # plt.colorbar() # plt.savefig('GaussianMixture_phaseContour-{}.png'.format(suffix), dpi=600) return gaussianParameters, p
[docs] def phaseDiagram( gaussianParameters, jointHistogram, voxelCoverage=None, sigmaMax=9, BINS=64, GRAPHS=False, INTERACTIVE=False, suffix="", rootPath=".", ): """ To be commented too """ if INTERACTIVE: GRAPHS = True plt.ion() def distanceMax(gaussianParameter): phi, muf, mug, a, b, c = gaussianParameter return (a * (x - muf) ** 2 + 2.0 * b * (x - muf) * (y - mug) + c * (y - mug) ** 2) - numpy.log(phi) def distanceMahalanobis(gaussianParameter): phi, muf, mug, a, b, c = gaussianParameter return numpy.sqrt(a * (x - muf) ** 2 + 2.0 * b * (x - muf) * (y - mug) + c * (y - mug) ** 2) if voxelCoverage == 1.0 or voxelCoverage is None: coverage = 1.0 phase = numpy.zeros((BINS, BINS), dtype="<u1") # define corresponding level for xbin in range(BINS): x = xbin + 0.5 for ybin in range(BINS): y = ybin + 0.5 distances = numpy.array([distanceMax(gp) for gp in gaussianParameters]) i = numpy.argmin(distances) distanceMin = distances[i] phase[xbin, ybin] = i + 1 print("\tVoxel coverage = 100 %") if GRAPHS: NPHASES = len(gaussianParameters) plt.clf() plt.title("Phase diagram: voxel coverage = 100%") plt.imshow( phase.T, origin="lower", extent=[0.0, BINS, 0.0, BINS], vmin=-0.5, vmax=NPHASES + 0.5, cmap=mpl.cm.get_cmap(cmapPhases, NPHASES + 1), ) plt.colorbar(ticks=numpy.arange(0, NPHASES + 1)) for gp in gaussianParameters: plt.plot(gp[1], gp[2], "b*") plt.xlabel("f") plt.ylabel("g") if INTERACTIVE: plt.show() plt.pause(1.0) else: sigma = numpy.arange(1, sigmaMax + 1, 1)[::-1] # phases = numpy.zeros((len(sigma), BINS, BINS), dtype='<u1') for n, sig in enumerate(sigma): phase = numpy.zeros((BINS, BINS), dtype="<u1") # define corresponding level for xbin in range(BINS): x = xbin + 0.5 for ybin in range(BINS): y = ybin + 0.5 distancesMax = numpy.array([distanceMax(gp) for gp in gaussianParameters]) distancesMah = numpy.array([distanceMahalanobis(gp) for gp in gaussianParameters]) i = numpy.argmin(distancesMax) distanceMin = distancesMah[i] if distanceMin < sig: # phases[n, xbin, ybin] = i+1 phase[xbin, ybin] = i + 1 coverage = jointHistogram[phase > 0].sum() if GRAPHS: NPHASES = len(gaussianParameters) plt.clf() plt.title("Phase diagram for {:.0f}-sigma: voxel coverage = {:.2f}%".format(sig, 100 * coverage)) plt.imshow( phase.T, origin="lower", extent=[0.0, BINS, 0.0, BINS], vmin=-0.5, vmax=NPHASES + 0.5, cmap=mpl.cm.get_cmap(cmapPhases, NPHASES + 1), ) plt.colorbar(ticks=numpy.arange(0, NPHASES + 1)) for gp in gaussianParameters: plt.plot(gp[1], gp[2], "b*") plt.xlabel("f") plt.ylabel("g") if INTERACTIVE: plt.show() plt.pause(1.0) else: plt.savefig( "{}/GaussianMixture_phaseDiagram-{:.0f}sig-{}.png".format(rootPath, sig, suffix), dpi=600, ) print( "\t{:.0f}-sigma: voxel coverage = {:.2f}".format(sig, 100 * coverage), end="", ) if coverage > voxelCoverage: print(" (> {:.2f}%)".format(100 * voxelCoverage)) else: print(" (< {:.2f}%) -> Returning this phase diagram.".format(100 * voxelCoverage)) break if GRAPHS and not INTERACTIVE: plt.savefig("{}/GaussianMixture_phaseDiagram-{}.png".format(rootPath, suffix), dpi=600) if INTERACTIVE: plt.ioff() plt.close() # phase diagram for different levels # for n, sig in enumerate(sigma): # plt.clf() # tmp = phases[n].astype('<f4') # tmp[tmp == 0] = numpy.nan # plt.title("Phases repartition for sigma {}".format(sigma)) # plt.imshow(tmp.T, origin='lower', extent=[0.0, 1.0, 0.0, 1.0], vmin=-0.5, vmax=NPHASES+0.5, cmap=mpl.cm.get_cmap(cmapPhases, NPHASES+1)) # plt.colorbar(ticks=numpy.arange(0, NPHASES+1)) # for gp in gaussianParameters: # plt.plot(gp[1], gp[2], 'b*') # plt.xlabel("f") # plt.ylabel("g") # plt.savefig('GaussianMixture_phaseDiagram-level{:02d}-{}.png'.format(n, suffix), dpi=600) # import spam.helpers # spam.helpers.writeStructuredVTK(cellData={"phases": phases}, # aspectRatio=(1.0, 1.0, 1.0), fileName="GaussianMixture_phaseDiagram-{}.vtk".format(suffix)) # tifffile.imwrite("{}/GaussianMixture_phaseDiagram-{}.tif".format(rootPath, suffix), phase) return phase, coverage