#!/usr/bin/env python """ This script manipulates a Phi field: Gridded Phi values: - spam-pixelSearch - spam-pixelSearchPropagate - spam-ldic Phis defined at labels centres: - spam-pixelSearch - spam-pixelSearchPropagate - spam-ddic This script allows you to: - correct bad points inside a PhiField based on RS, or CC - correct incoherent points inside a PhiField based on LQC - apply a median filter to the PhiField Outputs are: - TSV files - (optional) VTK files for visualisation - (optional) TIF files in the case of gridded data 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 . """ import os os.environ['OPENBLAS_NUM_THREADS'] = '1' import spam.DIC import spam.deformation import spam.helpers #import spam.mesh import spam.label import numpy import multiprocessing import scipy.spatial import scipy.ndimage import progressbar import argparse import tifffile tol = 1e-6 # Define argument parser object parser = argparse.ArgumentParser(description="spam-filterPhiField "+spam.helpers.optionsParser.GLPv3descriptionHeader +\ "This script process Phi fields by\n"+ "correcting bad or incoherent points or filtering", formatter_class=argparse.RawTextHelpFormatter) # Parse arguments with external helper function args = spam.helpers.optionsParser.filterPhiField(parser) if args.PROCESSES is None: args.PROCESSES = multiprocessing.cpu_count() print("spam-filterPhiField -- Current Settings:") argsDict = vars(args) for key in sorted(argsDict): print("\t{}: {}".format(key, argsDict[key])) ############################################################### ### Step 1 (mandatory) read input Phi File ############################################################### PhiFromFile = spam.helpers.readCorrelationTSV(args.PHIFILE.name, readConvergence=True, readPixelSearchCC=True, readError=True) if PhiFromFile is None: print(f"\tFailed to read your TSV file passed with -pf {args.PHIFILE.name}") exit() # If the read Phi-file has only one line -- it's a single point registration! # We can either apply it to a grid or to labels if PhiFromFile['fieldCoords'].shape[0] == 1: print(f"\tYour TSV passed with -pf {args.PHIFILE.name} is single line file (a registration). A field is required") exit() # Check if it is a discrete or gridded field grid = True discrete = False if PhiFromFile["numberOfLabels"] != 0: discrete = True grid = False ############################################################### ### Input Phi file is a Phi FIELD ############################################################### inputNodesDim = PhiFromFile["fieldDims"] inputNodePositions = PhiFromFile["fieldCoords"] inputPhiField = PhiFromFile["PhiField"] inputDisplacements = PhiFromFile["PhiField"][:, 0:3, -1] inputReturnStatus = PhiFromFile["returnStatus"] inputPixelSearchCC = PhiFromFile["pixelSearchCC"] inputDeltaPhiNorm = PhiFromFile["deltaPhiNorm"] inputIterations = PhiFromFile["iterations"] inputError = PhiFromFile["error"] ### Empty arrays for masking points inputGood = numpy.zeros(inputNodePositions.shape[0], dtype=bool) inputBad = numpy.zeros(inputNodePositions.shape[0], dtype=bool) inputIgnore = numpy.zeros(inputNodePositions.shape[0], dtype=bool) # output arrays outputPhiField = numpy.zeros((inputNodePositions.shape[0], 4, 4)) outputReturnStatus = numpy.ones( (inputNodePositions.shape[0]), dtype=float) outputDeltaPhiNorm = numpy.ones( (inputNodePositions.shape[0]), dtype=float)*100 outputIterations = numpy.zeros((inputNodePositions.shape[0]), dtype=float) outputError = numpy.ones( (inputNodePositions.shape[0]), dtype=float)*100 outputPixelSearchCC = numpy.zeros((inputNodePositions.shape[0]), dtype=float) # Check neighbour inputs, either args.NEIGHBOUR_RADIUS or args.NUMBER_OF_NEIGHBOURS should be set. if args.NEIGHBOUR_RADIUS is not None and args.NUMBER_OF_NEIGHBOURS is not None: print("Both number of neighbours and neighbour radius are set, I'm taking the radius and ignoring the number of neighbours") args.NUMBER_OF_NEIGHBOURS = None if args.NEIGHBOUR_RADIUS is None and args.NUMBER_OF_NEIGHBOURS is None: if grid: # Gridded input field nodeSpacing = numpy.array([numpy.unique(inputNodePositions[:, i])[1] - numpy.unique(inputNodePositions[:, i])[0] if len(numpy.unique(inputNodePositions[:, i])) > 1 else numpy.unique(inputNodePositions[:, i])[0] for i in range(3)]) args.NEIGHBOUR_RADIUS = 4*int(numpy.mean(nodeSpacing)) print(f"Neither number of neighbours nor neighbour distance set, using default distance of 4*mean(nodeSpacing) = {args.NEIGHBOUR_RADIUS}") else: # Discrete input field args.NUMBER_OF_NEIGHBOURS = 27 print("Neither number of neighbours nor neighbour distance set, using default 27 neighbours") ############################################################### ### Define IGNORE points: ############################################################### if args.MASK: inputIgnore = inputReturnStatus < -4 ############################################################### ### Apply threshold to select good and bad points ############################################################### if args.SRS: print(f"\n\nSelecting bad points as Return Status <= {args.SRST}") inputGood = numpy.logical_and(inputReturnStatus > args.SRST, ~inputIgnore) inputBad = numpy.logical_and(inputReturnStatus <= args.SRST, ~inputIgnore) if args.SLQC: print("\tYou passed -slqc but you can only have one selection at a time") if args.SCC: print("\tYou passed -scc but you can only have one selection at a time") elif args.SCC: print(f"\n\nSelecting bad points with Pixel Search CC <= {args.SCCT}") inputGood = numpy.logical_and(inputPixelSearchCC > args.SCCT, ~inputIgnore) inputBad = numpy.logical_and(inputPixelSearchCC <= args.SCCT, ~inputIgnore) if args.SLQC: print("\tYou passed -slqc but you can only have one selection at a time") elif args.SLQC: print("\n\nCalculate coherency") LQC = spam.DIC.estimateLocalQuadraticCoherency(inputNodePositions[~inputIgnore], inputDisplacements[~inputIgnore], neighbourRadius=args.NEIGHBOUR_RADIUS, nNeighbours=args.NUMBER_OF_NEIGHBOURS, nProcesses=args.PROCESSES, verbose=True) #print(LQC.shape) #print(inputGood[~inputIgnore].shape) inputGood[~inputIgnore] = LQC < 0.1 inputBad[~inputIgnore] = LQC >= 0.1 ############################################################### ### Copy over the values for good AND ignore to output ############################################################### gandi = numpy.logical_or(inputGood, inputIgnore) outputPhiField[gandi] = inputPhiField[gandi] outputReturnStatus[gandi] = inputReturnStatus[gandi] outputDeltaPhiNorm[gandi] = inputDeltaPhiNorm[gandi] outputIterations[gandi] = inputIterations[gandi] outputError[gandi] = inputError[gandi] outputPixelSearchCC[gandi] = inputPixelSearchCC[gandi] if (args.CINT + args.CLQF) > 0 and numpy.sum(inputBad) == 0: print("No points to correct, exiting") exit() else: print(f"\n\nCorrecting {numpy.sum(inputBad)} points ({100*numpy.sum(inputBad)/numpy.sum(inputGood):03.1f}%)") ############################################################### ### Correct those bad points ############################################################### if args.CINT: print(f"\n\nCorrection based on local interpolation (filterF = {args.FILTER_F})") PhiFieldCorrected = spam.DIC.interpolatePhiField(inputNodePositions[inputGood], inputPhiField[inputGood], inputNodePositions[inputBad], nNeighbours=args.NUMBER_OF_NEIGHBOURS, neighbourRadius=args.NEIGHBOUR_RADIUS, interpolateF=args.FILTER_F, nProcesses=args.PROCESSES, verbose=True) outputPhiField[inputBad] = PhiFieldCorrected outputReturnStatus[inputBad] = 1 if args.CLQF: print("\tYou asked to correct with local QC fitting with -clqf, but only one correciton mode is supported") elif args.CLQF: if args.FILTER_F != 'no': print("WARNING: non-displacement quadratic coherency correction not implemented, only doing displacements, and returning F=eye(3)\n") print("\n\nCorrection based on local quadratic coherency") dispLQC = spam.DIC.estimateDisplacementFromQuadraticFit(inputNodePositions[inputGood], inputDisplacements[inputGood], inputNodePositions[inputBad], neighbourRadius=args.NEIGHBOUR_RADIUS, nNeighbours=args.NUMBER_OF_NEIGHBOURS, nProcesses=args.PROCESSES, verbose=True) # pass the displacements outputPhiField[inputBad, 0:3, 0:3] = numpy.eye(3) outputPhiField[inputBad, 0:3, -1] = dispLQC outputReturnStatus[inputBad] = 1 if args.FILTER_MEDIAN: if discrete: print("Median filter for discrete mode not implemented... does it even make sense?") else: # Filter ALL POINTS # if asked, apply a median filter of a specific size in the Phi field print("\nApplying median filter...") filterPointsRadius = int(args.FILTER_MEDIAN_RADIUS) if args.MASK: inputPhiField[inputIgnore] = numpy.nan if args.FILTER_F == 'rigid': print("Rigid mode not well defined for overall median filtering, exiting") exit() if args.FILTER_F == 'all': # Filter F components print("Filtering F components...") print("\t1/9") outputPhiField[:, 0, 0] = scipy.ndimage.generic_filter(inputPhiField[:, 0, 0].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() print("\t2/9") outputPhiField[:, 1, 0] = scipy.ndimage.generic_filter(inputPhiField[:, 1, 0].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() print("\t3/9") outputPhiField[:, 2, 0] = scipy.ndimage.generic_filter(inputPhiField[:, 2, 0].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() print("\t4/9") outputPhiField[:, 0, 1] = scipy.ndimage.generic_filter(inputPhiField[:, 0, 1].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() print("\t5/9") outputPhiField[:, 1, 1] = scipy.ndimage.generic_filter(inputPhiField[:, 1, 1].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() print("\t6/9") outputPhiField[:, 2, 1] = scipy.ndimage.generic_filter(inputPhiField[:, 2, 1].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() print("\t7/9") outputPhiField[:, 0, 2] = scipy.ndimage.generic_filter(inputPhiField[:, 0, 2].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() print("\t8/9") outputPhiField[:, 1, 2] = scipy.ndimage.generic_filter(inputPhiField[:, 1, 2].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() print("\t9/9") outputPhiField[:, 2, 2] = scipy.ndimage.generic_filter(inputPhiField[:, 2, 2].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() if args.FILTER_F == 'no': for n in range(inputNodePositions.shape[0]): outputPhiField[n] = numpy.eye(4) print("Filtering displacements...") print("\t1/3") outputPhiField[:, 0, -1] = scipy.ndimage.generic_filter(inputPhiField[:, 0, -1].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() print("\t2/3") outputPhiField[:, 1, -1] = scipy.ndimage.generic_filter(inputPhiField[:, 1, -1].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() print("\t3/3") outputPhiField[:, 2, -1] = scipy.ndimage.generic_filter(inputPhiField[:, 2, -1].reshape(inputNodesDim), numpy.nanmedian, size=(2 * filterPointsRadius + 1)).ravel() if args.MASK: outputPhiField[inputIgnore] = numpy.nan #Outputs are: #- TSV files #- (optional) VTK files for visualisation #- (optional) TIF files in the case of gridded data if args.TSV: if discrete: TSVheader = "Label\tZpos\tYpos\tXpos\tFzz\tFzy\tFzx\tZdisp\tFyz\tFyy\tFyx\tYdisp\tFxz\tFxy\tFxx\tXdisp\tpixelSearchCC\treturnStatus\terror\tdeltaPhiNorm\titerations" else: TSVheader = "NodeNumber\tZpos\tYpos\tXpos\tFzz\tFzy\tFzx\tZdisp\tFyz\tFyy\tFyx\tYdisp\tFxz\tFxy\tFxx\tXdisp\tpixelSearchCC\treturnStatus\terror\tdeltaPhiNorm\titerations" outMatrix = numpy.array([numpy.arange(inputNodePositions.shape[0]), inputNodePositions[:, 0], inputNodePositions[:, 1], inputNodePositions[:, 2], outputPhiField[:, 0, 0], outputPhiField[:, 0, 1], outputPhiField[:, 0, 2], outputPhiField[:, 0, 3], outputPhiField[:, 1, 0], outputPhiField[:, 1, 1], outputPhiField[:, 1, 2], outputPhiField[:, 1, 3], outputPhiField[:, 2, 0], outputPhiField[:, 2, 1], outputPhiField[:, 2, 2], outputPhiField[:, 2, 3], outputPixelSearchCC, outputReturnStatus, outputError, outputDeltaPhiNorm, outputIterations]).T numpy.savetxt(args.OUT_DIR+"/"+args.PREFIX+".tsv", outMatrix, fmt='%.7f', delimiter='\t', newline='\n', comments='', header=TSVheader) if args.TIFF: if grid: if inputNodesDim[0] != 1: tifffile.imsave(args.OUT_DIR+"/"+args.PREFIX+"-Zdisp.tif", outputPhiField[:, 0, -1].astype(' 1 else numpy.unique(inputNodePositions[:, i])[0] for i in range(3)]) HWS = nodeSpacing/2 spam.helpers.writeStructuredVTK(origin=inputNodePositions[0]-HWS, aspectRatio=nodeSpacing, cellData=cellData, fileName=args.OUT_DIR+"/"+args.PREFIX+".vtk") else: disp = outputPhiField[:, 0:3, -1] disp[numpy.logical_not(numpy.isfinite(disp))] = 0 magDisp = numpy.linalg.norm(disp, axis=1) VTKglyphDict = {'displacements': outputPhiField[:, 0:3, -1], 'mag(displacements)': magDisp } spam.helpers.writeGlyphsVTK(inputNodePositions, VTKglyphDict, fileName=args.OUT_DIR + "/" + args.PREFIX + ".vtk")