|
| 1 | +#!/usr/bin/env python |
| 2 | +import argparse |
| 3 | +import itk |
| 4 | +import numpy as np |
| 5 | +from itk import RTK as rtk |
| 6 | + |
| 7 | + |
| 8 | +def build_parser(): |
| 9 | + parser = rtk.RTKArgumentParser(description="Decomposes spectral projections into materials") |
| 10 | + parser.add_argument("--output", "-o", required=True, help="Output file name (decomposed projections)") |
| 11 | + parser.add_argument("--input", "-i", help="Decomposed projections for initialization file name") |
| 12 | + parser.add_argument("--spectral", "-s", required=True, help="Spectral projections to be decomposed") |
| 13 | + parser.add_argument("--detector", "-d", required=True, help="Detector response file") |
| 14 | + parser.add_argument("--incident", required=True, help="Incident spectrum file") |
| 15 | + parser.add_argument("--attenuations", "-a", required=True, help="Material attenuations file") |
| 16 | + parser.add_argument("--niterations", "-n", type=int, default=300, help="Number of iterations") |
| 17 | + parser.add_argument("--thresholds", "-t", type=float, nargs='+', required=True, help="Lower threshold of bins, expressed in pulse height") |
| 18 | + parser.add_argument("--weightsmap", "-w", help="File name for the output weights map (inverse noise variance)") |
| 19 | + parser.add_argument("--restarts", "-r", action='store_true', help="Allow random restarts during optimization") |
| 20 | + parser.add_argument("--fischer", "-f", help="File name for the Fischer information matrix") |
| 21 | + parser.add_argument("--log", "-l", action='store_true', help="Log transform each bin, and concatenate the projections with the decomposed ones") |
| 22 | + parser.add_argument("--guess", "-g", action='store_true', help="Ignore values in input and initialize the simplex with a simple heuristic instead") |
| 23 | + return parser |
| 24 | + |
| 25 | + |
| 26 | +def process(args_info: argparse.Namespace): |
| 27 | + PixelValueType = itk.F |
| 28 | + Dimension = 3 |
| 29 | + |
| 30 | + DecomposedProjectionType = itk.VectorImage[PixelValueType, Dimension] |
| 31 | + SpectralProjectionsType = itk.VectorImage[PixelValueType, Dimension] |
| 32 | + IncidentSpectrumImageType = itk.Image[PixelValueType, Dimension] |
| 33 | + DetectorResponseImageType = itk.Image[PixelValueType, Dimension - 1] |
| 34 | + MaterialAttenuationsImageType = itk.Image[PixelValueType, Dimension - 1] |
| 35 | + |
| 36 | + # Read inputs |
| 37 | + # Readers |
| 38 | + if args_info.verbose: |
| 39 | + print(f"Reading decomposed projections from {args_info.input}...") |
| 40 | + if args_info.input: |
| 41 | + reader = itk.ImageFileReader[DecomposedProjectionType].New() |
| 42 | + reader.SetFileName(args_info.input) |
| 43 | + reader.Update() |
| 44 | + decomposedProjection = reader.GetOutput() |
| 45 | + else: |
| 46 | + decomposedProjection = None |
| 47 | + |
| 48 | + if args_info.verbose: |
| 49 | + print(f"Reading spectral projections from {args_info.spectral}...") |
| 50 | + reader = itk.ImageFileReader[SpectralProjectionsType].New() |
| 51 | + reader.SetFileName(args_info.spectral) |
| 52 | + reader.Update() |
| 53 | + spectralProjection = reader.GetOutput() |
| 54 | + |
| 55 | + if args_info.verbose: |
| 56 | + print(f"Reading incident spectrum from {args_info.incident}...") |
| 57 | + reader = itk.ImageFileReader[IncidentSpectrumImageType].New() |
| 58 | + reader.SetFileName(args_info.incident) |
| 59 | + reader.Update() |
| 60 | + incidentSpectrum = reader.GetOutput() |
| 61 | + |
| 62 | + if args_info.verbose: |
| 63 | + print(f"Reading detector response from {args_info.detector}...") |
| 64 | + reader = itk.ImageFileReader[DetectorResponseImageType].New() |
| 65 | + reader.SetFileName(args_info.detector) |
| 66 | + reader.Update() |
| 67 | + detectorResponse = reader.GetOutput() |
| 68 | + |
| 69 | + if args_info.verbose: |
| 70 | + print(f"Reading material attenuations from {args_info.attenuations}...") |
| 71 | + reader = itk.ImageFileReader[MaterialAttenuationsImageType].New() |
| 72 | + reader.SetFileName(args_info.attenuations) |
| 73 | + reader.Update() |
| 74 | + materialAttenuations = reader.GetOutput() |
| 75 | + |
| 76 | + # Parameters |
| 77 | + NumberOfMaterials = int(materialAttenuations.GetLargestPossibleRegion().GetSize()[0]) |
| 78 | + NumberOfSpectralBins = int(spectralProjection.GetVectorLength()) |
| 79 | + MaximumEnergy = int(incidentSpectrum.GetLargestPossibleRegion().GetSize()[0]) |
| 80 | + |
| 81 | + # Thresholds |
| 82 | + if len(args_info.thresholds) != NumberOfSpectralBins: |
| 83 | + raise RuntimeError(f"Number of thresholds {len(args_info.thresholds)} does not match the number of bins {NumberOfSpectralBins}") |
| 84 | + thresholds = itk.VariableLengthVector[itk.D]() |
| 85 | + thresholds.SetSize(NumberOfSpectralBins + 1) |
| 86 | + for i in range(NumberOfSpectralBins): |
| 87 | + thresholds[i] = float(args_info.thresholds[i]) |
| 88 | + MaximumPulseHeight = int(detectorResponse.GetLargestPossibleRegion().GetSize()[1]) |
| 89 | + thresholds[NumberOfSpectralBins] = MaximumPulseHeight |
| 90 | + |
| 91 | + # Sanity checks |
| 92 | + idx = itk.Index[3]() |
| 93 | + idx.Fill(0) |
| 94 | + if decomposedProjection is not None: |
| 95 | + if decomposedProjection.GetPixel(idx).Size() != NumberOfMaterials: |
| 96 | + raise RuntimeError(f"Decomposed projections vector size {decomposedProjection.GetPixel(idx).Size()} != {NumberOfMaterials}") |
| 97 | + |
| 98 | + if spectralProjection.GetPixel(idx).Size() != NumberOfSpectralBins: |
| 99 | + raise RuntimeError(f"Spectral projections vector size {spectralProjection.GetPixel(idx).Size()} != {NumberOfSpectralBins}") |
| 100 | + |
| 101 | + if detectorResponse.GetLargestPossibleRegion().GetSize()[0] != MaximumEnergy: |
| 102 | + raise RuntimeError(f"Detector response energies {detectorResponse.GetLargestPossibleRegion().GetSize()[0]} != {MaximumEnergy}") |
| 103 | + |
| 104 | + # Create and set the filter |
| 105 | + simplex = rtk.SimplexSpectralProjectionsDecompositionImageFilter[ |
| 106 | + DecomposedProjectionType, |
| 107 | + SpectralProjectionsType, |
| 108 | + IncidentSpectrumImageType, |
| 109 | + DetectorResponseImageType, |
| 110 | + MaterialAttenuationsImageType, |
| 111 | + ].New() |
| 112 | + if decomposedProjection is not None: |
| 113 | + simplex.SetInputDecomposedProjections(decomposedProjection) |
| 114 | + simplex.SetGuessInitialization(bool(args_info.guess)) |
| 115 | + simplex.SetInputMeasuredProjections(spectralProjection) |
| 116 | + simplex.SetInputIncidentSpectrum(incidentSpectrum) |
| 117 | + simplex.SetDetectorResponse(detectorResponse) |
| 118 | + simplex.SetMaterialAttenuations(materialAttenuations) |
| 119 | + simplex.SetThresholds(thresholds) |
| 120 | + simplex.SetNumberOfIterations(int(args_info.niterations)) |
| 121 | + simplex.SetOptimizeWithRestarts(bool(args_info.restarts)) |
| 122 | + simplex.SetLogTransformEachBin(bool(args_info.log)) |
| 123 | + |
| 124 | + # Note: The simplex filter is set to perform several searches for each pixel, |
| 125 | + # with different initializations, and keep the best one (SetOptimizeWithRestart(true)). |
| 126 | + # While it may yield better results, these initializations are partially random, |
| 127 | + # which makes the output non-reproducible. |
| 128 | + # The default behavior, used for example in the tests, is not to use this feature |
| 129 | + # (SetOptimizeWithRestart(false)), which makes the output reproducible. |
| 130 | + |
| 131 | + if args_info.weightsmap: |
| 132 | + simplex.SetOutputInverseCramerRaoLowerBound(True) |
| 133 | + if args_info.fischer: |
| 134 | + simplex.SetOutputFischerMatrix(True) |
| 135 | + |
| 136 | + if args_info.verbose: |
| 137 | + print("Running simplex decomposition...") |
| 138 | + simplex.Update() |
| 139 | + |
| 140 | + itk.imwrite(simplex.GetOutput(0), args_info.output) |
| 141 | + if args_info.weightsmap: |
| 142 | + itk.imwrite(simplex.GetOutput(1), args_info.weightsmap) |
| 143 | + if args_info.fischer: |
| 144 | + itk.imwrite(simplex.GetOutput(2), args_info.fischer) |
| 145 | + |
| 146 | + |
| 147 | +def main(argv=None): |
| 148 | + parser = build_parser() |
| 149 | + args_info = parser.parse_args(argv) |
| 150 | + process(args_info) |
| 151 | + |
| 152 | + |
| 153 | +if __name__ == "__main__": |
| 154 | + main() |
0 commit comments