public override INifti <float> AddOverlay(INifti <float> overlay) { NiftiFloat32 output = (NiftiFloat32)(this.DeepCopy()); // Caclulate conversion to colour map. var range = Header.cal_max - Header.cal_min; var scale = ColorMap.Length / range; var bias = -(Header.cal_min); // Caclulate conversion to colour map. var rangeOverlay = overlay.Header.cal_max - overlay.Header.cal_min; var scaleOverlay = overlay.ColorMap.Length / rangeOverlay; var biasOverlay = -(overlay.Header.cal_min); for (int i = 0; i < Voxels.Length; ++i) { // Get the colour map index for our value. int idx = (int)((Voxels[i] + bias) * scale); if (idx < 0) { idx = 0; } else if (idx > ColorMap.Length - 1) { idx = ColorMap.Length - 1; } // Get the colour map value for the overlay int idxOverlay = (int)((overlay.Voxels[i] + biasOverlay) * scaleOverlay); if (idxOverlay < 0) { idxOverlay = 0; } else if (idxOverlay > overlay.ColorMap.Length - 1) { idxOverlay = overlay.ColorMap.Length - 1; } var red = (overlay.ColorMap[idxOverlay].R * overlay.ColorMap[idxOverlay].A + ColorMap[idx].R * (255 - overlay.ColorMap[idxOverlay].A)) / 255; var green = (overlay.ColorMap[idxOverlay].G * overlay.ColorMap[idxOverlay].A + ColorMap[idx].G * (255 - overlay.ColorMap[idxOverlay].A)) / 255; var blue = (overlay.ColorMap[idxOverlay].B * overlay.ColorMap[idxOverlay].A + ColorMap[idx].B * (255 - overlay.ColorMap[idxOverlay].A)) / 255; output.Voxels[i] = Convert.ToUInt32((byte)red << 16 | (byte)green << 8 | (byte)blue); } output.ConvertHeaderToRgb(); output.RecalcHeaderMinMax(); return(output); }
/// <summary> /// Compares the decrease in values from the reference Nifti (prior) to the input Nifti (current). /// The inputs should be pre-registered and normalised. /// </summary> /// <param name="input">Current example</param> /// <param name="reference">Prior example</param> /// <returns>Nifti who's values are the meaningful decrease (less than 0) between prior and current.</returns> public static INifti <float> CompareMSLesionDecrease(INifti <float> input, INifti <float> reference) { INifti <float> output = Compare.GatedSubract(input, reference, backgroundThreshold: 10, minRelevantStd: -1, maxRelevantStd: 5, minChange: 0.8f, maxChange: 5); for (int i = 0; i < output.Voxels.Length; ++i) { if (output.Voxels[i] > 0) { output.Voxels[i] = 0; } } output.RecalcHeaderMinMax(); // This will update the header range. output.ColorMap = ColorMaps.ReverseGreenScale(); return(output); }
/// <summary> /// Normalizes the data to a Z-Value /// </summary> /// <param name="input"></param> /// <param name="backgroundThreshold"></param> /// <returns></returns> public static INifti <float> ZNormalize(INifti <float> input, float backgroundThreshold = 10) { dynamic output = input.DeepCopy(); // We take the mean and standard deviation ignoring background. var currentMean = input.Voxels.Where(val => val > backgroundThreshold).Mean(); var currentStdDev = input.Voxels.Where(val => val > backgroundThreshold).StandardDeviation(); for (var i = 0; i < output.Voxels.Length; i++) { output.Voxels[i] = (float)((output.Voxels[i] - currentMean) / currentStdDev); } output.RecalcHeaderMinMax(); //update display range return(output); }
private void EstimateEdgeRatio(INifti <float> increaseNifti, INifti <float> decreaseNifti, MSMetrics qaResults) { var varianceIncrease = 0d; var totalIncrease = 0d; var varianceDecrease = 0d; var totalDecrease = 0d; for (int i = 0; i < increaseNifti.Voxels.Length - 1; ++i) { if ((increaseNifti.Voxels[i] > 0 != increaseNifti.Voxels[i + 1] > 0)) { varianceIncrease++; } if (increaseNifti.Voxels[i] > 0) { totalIncrease++; // Note, we'll miss the last voxel but it's just a rough estimate. } if ((decreaseNifti.Voxels[i] < 0 != decreaseNifti.Voxels[i + 1] < 0)) { varianceDecrease++; } if (decreaseNifti.Voxels[i] < 0) { totalDecrease++; } if (i == increaseNifti.Voxels.Length - 2) { if (increaseNifti.Voxels[i + 1] > 0) { totalIncrease++; } if (decreaseNifti.Voxels[i + 1] < 0) { totalDecrease++; } } } _log.Info($@"Edge ratio for increase: {varianceIncrease / totalIncrease}"); _log.Info($@"Edge ratio for decrease: {varianceDecrease / totalDecrease}"); if (totalIncrease == 0 || totalDecrease == 0 || double.IsNaN(varianceIncrease / totalIncrease)) { qaResults.Passed = false; } }
public static INifti <float> ANTSRegistration(INifti <float> floating, INifti <float> reference, DataReceivedEventHandler updates = null) { // Setup our temp file names. string niftiInPath = Path.GetFullPath(Tools.TEMPDIR + floating.GetHashCode() + ".antsrego.in.nii"); string niftiRefPath = Path.GetFullPath(Tools.TEMPDIR + floating.GetHashCode() + ".antsrego.ref.nii"); floating.WriteNifti(niftiInPath); reference.WriteNifti(niftiRefPath); string niftiOutPath = Path.GetFullPath(ANTSRegistration(niftiInPath, niftiRefPath, updates)); var output = floating.DeepCopy(); output = output.ReadNifti(niftiOutPath); return(output); }
/// <summary> /// Uses the CMTK registration and reformatx tools to register and reslice the floating nifti to match the reference nifti. /// </summary> /// <param name="floating">Nifti to be registered</param> /// <param name="reference">Reference nifti</param> /// <param name="updates">Event handler for updates from the toolchain...</param> /// <returns></returns> public static INifti <float> CMTKRegistration(INifti <float> floating, INifti <float> reference, DataReceivedEventHandler updates = null) { // Setup our temp file names. string niftiInPath = Tools.TEMPDIR + floating.GetHashCode() + ".cmtkrego.in.nii"; string niftiRefPath = Tools.TEMPDIR + floating.GetHashCode() + ".cmtkrego.ref.nii"; string niftiOutPath = Tools.TEMPDIR + floating.GetHashCode() + ".cmtkrego.out.nii"; // Write nifti to temp directory. floating.WriteNifti(niftiInPath); reference.WriteNifti(niftiRefPath); niftiOutPath = CMTKRegistration(niftiInPath, niftiRefPath, updates); //INifti output = floating.DeepCopy(); floating.ReadNifti(niftiOutPath); return(floating); }
/// <summary> /// Shifts the ditribution to be within the given range. Default is 0-1. /// </summary> /// <param name="input"></param> /// <param name="rangeStart"></param> /// <param name="rangeEnd"></param> /// <returns></returns> public static INifti <float> RangeNormalize(INifti <float> input, float rangeStart = 0, float rangeEnd = 1) { if (rangeEnd <= rangeStart) { throw new ArgumentException("Start of range cannot be greater than end of range."); } var min = input.Voxels.Min(); var range = input.Voxels.Max() - input.Voxels.Min(); var output = input.DeepCopy(); for (int i = 0; i < output.Voxels.Length; ++i) { output.Voxels[i] = ((output.Voxels[i] - min) / range) * (rangeEnd - rangeStart) + rangeStart; } return(output); }
/// <summary> /// Uses the BrainSuite BSE tool to extract the brain from a given INifti. /// </summary> /// <param name="input">Nifti which contains the brain to be extracted</param> /// <param name="updates">Data handler for updates from the BSE tool.</param> /// <returns>The INifti containing the extracted brain.</returns> public static INifti <float> BrainSuiteBSE(INifti <float> input, DataReceivedEventHandler updates = null) { // Setup our temp file names. string niftiInPath = Path.GetFullPath(Tools.TEMPDIR + input.GetHashCode() + ".bse.in.nii"); string niftiOutPath = Path.GetFullPath(Tools.TEMPDIR + input.GetHashCode() + ".bse.out.nii"); // Write nifti to temp directory. input.WriteNifti(niftiInPath); var args = $"--auto --trim -i \"{niftiInPath}\" -o \"{niftiOutPath}\""; ProcessBuilder.CallExecutableFile(CapiConfig.GetConfig().Binaries.bse, args, outputDataReceived: updates); var output = input.DeepCopy(); // Sometimes this messes with the header and gives us a 4-up??? output.ReadNifti(niftiOutPath); return(output); }
/// <summary> /// Uses the ANTS implementation of the N4 bias correction algorithm. /// </summary> /// <param name="input">The input nifti to be corrected</param> /// <param name="updates">Event handler for updates from the process</param> /// <returns>New, corrected nifti</returns> public static INifti <float> AntsN4(INifti <float> input, DataReceivedEventHandler updates = null) { // Setup our temp file names. string niftiInPath = Path.GetFullPath(Tools.TEMPDIR + input.GetHashCode() + ".antsN4.in.nii"); string niftiOutPath = Path.GetFullPath(Tools.TEMPDIR + input.GetHashCode() + ".antsN4.out.nii"); // Write nifti to temp directory. input.WriteNifti(niftiInPath); var args = $"-i \"{niftiInPath}\" -o \"{niftiOutPath}\""; ProcessBuilder.CallExecutableFile(CapiConfig.GetConfig().Binaries.N4BiasFieldCorrection, args, outputDataReceived: updates); var output = input.DeepCopy(); output.ReadNifti(niftiOutPath); output.RecalcHeaderMinMax(); return(output); }
private Histogram DoCompare(INifti <float> currentnii, INifti <float> priornii) { _log.Info("Starting normalization..."); currentnii = Normalization.ZNormalize(currentnii, priornii); _log.Info($@"..done."); currentnii.RecalcHeaderMinMax(); priornii.RecalcHeaderMinMax(); var tasks = new List <Task>(); var histogram = new Histogram { Prior = priornii, Current = currentnii }; var cs = _recipe.CompareSettings; if (cs.CompareIncrease) { var t = Task.Run(() => { _log.Info("Comparing increased signal..."); var increase = Compare.GatedSubract(currentnii, priornii, cs.BackgroundThreshold, cs.MinRelevantStd, cs.MaxRelevantStd, cs.MinChange, cs.MaxChange); for (int i = 0; i < increase.Voxels.Length; ++i) { increase.Voxels[i] = increase.Voxels[i] > 0 ? increase.Voxels[i] : 0; } increase.RecalcHeaderMinMax(); increase.ColorMap = ColorMaps.RedScale(); histogram.Increase = increase; var increaseOut = currentnii.AddOverlay(increase); var outpath = _currentPath + ".increase.nii"; increaseOut.WriteNifti(outpath); Metrics.ResultFiles.Add(new ResultFile() { FilePath = outpath, Description = "Increased Signal", Type = ResultType.CURRENT_PROCESSED }); }); tasks.Add(t); } if (cs.CompareDecrease) { _log.Info("Comparing decreased signal..."); // I know the code in these two branches looks similar but there's too many inputs to make a function that much simpler... var t = Task.Run(() => { var decrease = Compare.GatedSubract(currentnii, priornii, cs.BackgroundThreshold, cs.MinRelevantStd, cs.MaxRelevantStd, cs.MinChange, cs.MaxChange); for (int i = 0; i < decrease.Voxels.Length; ++i) { decrease.Voxels[i] = decrease.Voxels[i] < 0 ? decrease.Voxels[i] : 0; } decrease.RecalcHeaderMinMax(); decrease.ColorMap = ColorMaps.ReverseGreenScale(); histogram.Decrease = decrease; var decreaseOut = currentnii.AddOverlay(decrease); var outpath = _currentPath + ".decrease.nii"; decreaseOut.WriteNifti(outpath); Metrics.ResultFiles.Add(new ResultFile() { FilePath = outpath, Description = "Decreased Signal", Type = ResultType.CURRENT_PROCESSED }); }); tasks.Add(t); } Task.WaitAll(tasks.ToArray()); _log.Info("...done."); return(histogram); }
private void CheckBrainExtractionMatch( INifti <float> currentNifti, INifti <float> priorNifti, INifti <float> currentNiftiWithSkull, INifti <float> priorNiftiWithSkull, MSMetrics qaResults) { var volCurrent = 0d; var volPrior = 0d; var volWSkullCurrent = 0; var volWSkullPrior = 0; for (int i = 0; i < currentNifti.Voxels.Length; ++i) { if (currentNifti.Voxels[i] > 0) { volCurrent++; } if (currentNiftiWithSkull.Voxels[i] > 30) { volWSkullCurrent++; } if (priorNifti.Voxels[i] > 0) { volPrior++; } if (priorNiftiWithSkull.Voxels[i] > 30) { volWSkullPrior++; } } var match = Math.Min(volPrior, volCurrent) / Math.Max(volPrior, volCurrent); // Add results to QA list qaResults.VoxelVolPrior = volPrior; qaResults.VoxelVolCurrent = volCurrent; qaResults.BrainMatch = match; _log.Info($@"Percentage of current volume that's brain: {(int)(volCurrent / volWSkullCurrent * 100d)}%"); _log.Info($@"Percentage of prior volume that's brain: {(int)(volPrior / volWSkullPrior * 100d)}%"); _log.Info($@"Brain extraction match: {(int)(match * 100)}%"); // If the match is sub-80% it's probably because one of the brains didn't extract so // the compare operation will automatically use the intersection of the two. On the // other hand if we're above 80% but less than 95% one of the extractions probably cut // out a chunk of brain. So we'll make a mask of the union and apply it to both sides. if (match > 0.7 && match < 0.95) { _log.Info($@"Brain extraction match not good enough, taking the union..."); // Let's try to make the brain mask an OR of the two. var mask = currentNifti.DeepCopy(); for (int i = 0; i < mask.Voxels.Length; ++i) { if (currentNifti.Voxels[i] != 0 || priorNifti.Voxels[i] != 0) { mask.Voxels[i] = 1; } else { mask.Voxels[i] = 0; } } currentNifti = currentNiftiWithSkull.DeepCopy(); priorNifti = priorNiftiWithSkull.DeepCopy(); for (int i = 0; i < currentNifti.Voxels.Length; ++i) { currentNifti.Voxels[i] = currentNifti.Voxels[i] * mask.Voxels[i]; priorNifti.Voxels[i] = priorNifti.Voxels[i] * mask.Voxels[i]; } currentNifti.RecalcHeaderMinMax(); priorNifti.RecalcHeaderMinMax(); // Check brain extraction match again... volCurrent = 0d; volPrior = 0d; for (int i = 0; i < currentNifti.Voxels.Length; ++i) { if (currentNifti.Voxels[i] > 0) { volCurrent++; } if (priorNifti.Voxels[i] > 0) { volPrior++; } } match = Math.Min(volPrior, volCurrent) / Math.Max(volPrior, volCurrent); _log.Info($@"Brain extraction match after mask: {(int)(match * 100)}%"); } else if (match < 0.7) { qaResults.Passed = false; } // In theory this should stop the skull highlights darkening the output images... priorNifti.RecalcHeaderMinMax(); currentNifti.RecalcHeaderMinMax(); priorNiftiWithSkull.Header.cal_max = priorNifti.Header.cal_max; currentNiftiWithSkull.Header.cal_max = currentNifti.Header.cal_max; }
public INifti <float> Classify(INifti <float> prior, INifti <float> current) { var output = MSCompare.CompareMSLesionIncrease(prior, current); return(output); }
/// <summary> /// Compares the meaningful change in value between the reference Nifti (prior) and the input Nifti (current). /// This function also allows significant fine-tuning of the cut-off values. /// </summary> /// <param name="input">Current Nifti</param> /// <param name="reference">Prior Nifti</param> /// <param name="backgroundThreshold">Absolute value of background threashold. Any voxels with a value less than this are considered background and ignored.</param> /// <param name="minRelevantStd">Minimum relevant value in number of standard deviations from the mean. e.g. a value of -1 will mean that the minimum relevant value will be the mean - 1 standard deviation. Voxels below this threashold are ignored.</param> /// <param name="maxRelevantStd">Maximum relevant value in number of standard deviations from the mean. e.g. a value of 3 will mean that the maximum relevant value will be the mean + 3 standard deviations. Voxels above this threashold are ignored.</param> /// <param name="minChange">Minimum difference to be considered significant (e.g. noise threshold). Value is given in multiples of the standard deviation for the input voxels (ignoring background).</param> /// <param name="maxChange">Maximum difference to be considered significant. Value is given in multiples of the standard deviation for the input voxels (ignoring background).</param> /// <returns>INifti object which contains the relevant difference between the reference nifti and the input nifti.</returns> public static INifti <float> GatedSubract(INifti <float> input, INifti <float> reference, float backgroundThreshold = 10, float minRelevantStd = -1, float maxRelevantStd = 5, float minChange = 0.8f, float maxChange = 5) { INifti <float> output = input.DeepCopy(); //var mean = (float)input.Voxels.Where(val => val > backgroundThreshold).MeanStandardDeviation(); var meanstddev = input.Voxels.Where(val => val > backgroundThreshold).MeanStandardDeviation(); var mean = meanstddev.Item1; var stdDev = meanstddev.Item2; //(Not sure why decompose stopped working here). //float range = input.voxels.Max() - input.voxels.Min(); // Values from trial and error.... float minRelevantValue = (float)(mean + (minRelevantStd * stdDev)); float maxRelevantValue = (float)(mean + (maxRelevantStd * stdDev)); if (input.Voxels.Length != reference.Voxels.Length) { throw new Exception("Input and reference don't match size"); } for (int i = 0; i < input.Voxels.Length; ++i) { output.Voxels[i] = input.Voxels[i] - reference.Voxels[i]; // We want to ignore changes below the minimum relevant value. if (input.Voxels[i] < minRelevantValue) { output.Voxels[i] = 0; } if (reference.Voxels[i] < minRelevantValue) { output.Voxels[i] = 0; } // And above the maximum relevant value. if (input.Voxels[i] > maxRelevantValue) { output.Voxels[i] = 0; } if (reference.Voxels[i] > maxRelevantValue) { output.Voxels[i] = 0; } // If we haven't changed by at least 1 stdDev we're not significant if (Math.Abs(output.Voxels[i]) < Math.Abs(minChange * stdDev)) { output.Voxels[i] = 0; } if (Math.Abs(output.Voxels[i]) > Math.Abs(maxChange * stdDev)) { output.Voxels[i] = 0; } if (reference.Voxels[i] < backgroundThreshold) { output.Voxels[i] = 0; } if (input.Voxels[i] < backgroundThreshold) { output.Voxels[i] = 0; } } for (int i = 1; i < output.Voxels.Length - 1; ++i) { if (output.Voxels[i - 1] == 0 && output.Voxels[i + 1] == 0) { output.Voxels[i] = 0; } } output.RecalcHeaderMinMax(); // Update header range. var stdDv = output.Voxels.StandardDeviation(); var mean2 = output.Voxels.Where(val => val > 0).Mean(); System.Console.WriteLine($"Compared. Mean={mean2}, stdDv={stdDv}, size={output.Voxels.Where(val => val > 0).Count()}"); return(output); }
public abstract INifti <T> AddOverlay(INifti <T> overlay);