Skip to content

Commit 187e028

Browse files
axel-grcCopilot
andcommitted
ENH: Add rtkspectralsimplexdecomposition python application
Co-authored-by: Copilot <copilot@github.com>
1 parent 37aa96e commit 187e028

3 files changed

Lines changed: 156 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
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()

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ rtkspectralforwardmodel = "itk.rtkspectralforwardmodel:main"
8585
rtkspectraldenoiseprojections = "itk.rtkspectraldenoiseprojections:main"
8686
rtkspectralonestep = "itk.rtkspectralonestep:main"
8787
rtkspectralrooster = "itk.rtkspectralrooster:main"
88+
rtkspectralsimplexdecomposition = "itk.rtkspectralsimplexdecomposition:main"
8889
rtksubselect = "itk.rtksubselect:main"
8990
rtktotalvariationdenoising = "itk.rtktotalvariationdenoising:main"
9091
rtktotalnuclearvariationdenoising = "itk.rtktotalnuclearvariationdenoising:main"

wrapping/__init_rtk__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
"rtkspectraldenoiseprojections",
6565
"rtkspectralonestep",
6666
"rtkspectralrooster",
67+
"rtkspectralsimplexdecomposition",
6768
"rtksubselect",
6869
"rtktotalnuclearvariationdenoising",
6970
"rtktotalvariationdenoising",

0 commit comments

Comments
 (0)