Module ctsimu.evaluation.test2D_FB_1
Test 2D-FB-1: Noise
This test scenario consists of two subtests. For a free-beam, projection images with two different noise levels are to be simulated: a signal-to-noise ratio (SNR) of 100, and a second one with an SNR of 250, both of which refer to the point of maximum intensity.
Once simulated, the metadata file for each subtest that shall be evaluated must be passed to the toolbox, identified by the correct argument keyword SNR100
or SNR250
. You don't have to run both test evaluations at once, but it is possible.
from ctsimu.toolbox import Toolbox
Toolbox("2D-FB-1",
SNR100="2D-FB-1_Detektor1_SNR-100_metadata.json",
SNR250="2D-FB-1_Detektor2_SNR-250_metadata.json"
)
The detector is small and very far away from the source, leading to a very homogeneous illumination. The simulation software must provide a noise-free projection image to be used for the flat-field correction of the noisy projection image. Both files must be correctly referenced in the metadata file that is passed to the toolbox. The SNR is then evaluated from the complete pixel ensemble of the flat-field corrected image:
\text{SNR} = \frac{\left< I \right>}{\sigma},
with \left< I \right> being the mean grey value and \sigma the root mean square grey value deviation (RMSD) of the pixel ensemble.
Expand source code
# -*- coding: UTF-8 -*-
"""# Test 2D-FB-1: Noise
.. include:: ./test2D_FB_1.md
"""
from ..test import *
from ..helpers import *
from ..scenario import Scenario
class Test2D_FB_1_results:
""" Results for one sub test of the noise scenario. """
def __init__(self):
self.mean = 0
self.stdDev = 0
self.snr = 0
self.fwhm = 0
# Nominal values from JSON file:
self.SNRmax = None
self.nominalMean = None
self.nominalFWHM = None
self.nominalSigma = None
# Grey Value Histogram:
self.gvHistogram = None # stores the GV histogram
self.gvHistNormalized = None # area-normalized GV histogram
class Test2D_FB_1(generalTest):
""" CTSimU test 2D-FB-1: Noise. """
def __init__(self, resultFileDirectory=".", name=None, rawOutput=False):
generalTest.__init__(
self,
testName="2D-FB-1",
name=name,
nExpectedRuns=2,
resultFileDirectory=resultFileDirectory,
rawOutput=rawOutput)
self.imax = None # maximum (noise-free) grey value in free beam, as required by the scenario
self.gvMax = 70000 # maximum grey value for the histograms
self.xValues = None # x grey values for the histograms
# Results for each sub test:
self.results = []
def prepare(self):
""" Preparations before the test will be run with the images from the pipeline. """
if not isinstance(self.pipe, Pipeline):
self.prepared = False
raise Exception("Step must be part of a processing pipeline before it can prepare. Current pipeline: {}".format(self.pipe))
if not self.prepared:
self.results = []
self.xValues = numpy.linspace(0, self.gvMax, self.gvMax, endpoint=False)
if self.jsonScenarioFile is not None:
self.prepared = True
else:
raise Exception("Test {name}: Please provide a JSON scenario description.".format(name=self.name))
self.prepared = True
def prepareRun(self, i):
if i < len(self.subtests):
if self.subtests[i] == "SNR100":
self.jsonScenarioFile = "2D-FB-1_Detektor1_SNR100_2021-05-25v06r00dp.json"
elif self.subtests[i] == "SNR250":
self.jsonScenarioFile = "2D-FB-1_Detektor2_SNR250_2021-05-25v06r00dp.json"
else:
raise Exception("{key} is not a valid subtest identifier for test scenario {test}".format(key=self.subtests[i], test=self.testName))
if self.jsonScenarioFile is not None:
scenario = Scenario(json_dict=json_from_pkg(pkg_scenario(self.jsonScenarioFile)))
self.imax = scenario.detector.gray_value.imax.get()
snrMax = scenario.detector.noise.snr_at_imax.get()
if self.imax is None:
raise Exception("Test {name}: Cannot find 'imax' value in JSON scenario description: {json}".format(name=self.name, json=self.jsonScenarioFile))
if snrMax is None:
raise Exception("Test {name}: Cannot find 'snr_at_imax' value in JSON scenario description: {json}".format(name=self.name, json=self.jsonScenarioFile))
nominalSigma = self.imax / snrMax
nominalFWHM = 2.0*math.sqrt(2.0*math.log(2.0))*self.imax / snrMax
else:
raise Exception("Test {name}: Cannot open JSON scenario description: {json}".format(name=self.name, json=self.jsonScenarioFile))
# Prepare this test run:
self.prepare()
self.results.append(Test2D_FB_1_results())
self.results[i].gvHistogram = numpy.zeros(self.gvMax, dtype=numpy.uint32)
self.results[i].SNRmax = snrMax
self.results[i].nominalMean = self.imax # after ff-correction
self.results[i].nominalFWHM = nominalFWHM
self.results[i].nominalSigma = nominalSigma
else:
if len(self.subtests) == 0:
raise Exception("Please provide keywords that identify which metadata file belongs to which subtest. Test {testname} accepts two keywords: 'SNR100' and 'SNR250'.".format(testname=self.testName))
else:
raise Exception("Number of provided image metadata files exceeds number of test runs ({expected}).".format(expected=len(self.subtests)))
def run(self, image):
if self.currentRun < self.nExpectedRuns:
self.prepareRun(self.currentRun)
i = self.currentRun
subtestName = self.subtests[i]
# Create Grey Value Histogram:
for x in range(image.getWidth()):
print("\rCalculating grey value histogram... {:.1f}%".format(100*x/image.getWidth()), end="")
for y in range(image.getHeight()):
gv = int(image.getPixel(x, y))
if gv >= 0 and gv < self.gvMax:
self.results[i].gvHistogram[gv] += 1
print("\rCalculating grey value histogram... 100% ")
self.results[i].mean = image.mean()
self.results[i].stdDev = image.stdDev()
self.results[i].snr = self.results[i].mean / self.results[i].stdDev
self.results[i].fwhm = 2.0*math.sqrt(2.0*math.log(2.0)) * self.results[i].stdDev
self.results[i].gvHistNormalized = self.results[i].gvHistogram / numpy.sum(self.results[i].gvHistogram)
log("Writing evaluation results...")
# Write evaluation text files:
result = self.results[i]
summary = "# Evaluation of test {name}, {subname}:\n".format(name=self.name, subname=subtestName)
summary += "#\n"
summary += "# Signal-to-noise ratio (SNR)\n"
summary += "# -----------------------------------------\n"
summary += "# Overall projection SNR: {snr:.3f}\n".format(snr=result.snr)
summary += "# Nominal SNR: {nominalSNR:.3f}\n".format(nominalSNR=result.SNRmax)
summary += "# Relative SNR deviation: {reldev:.5f}\n".format(reldev=(result.snr-result.SNRmax)/result.SNRmax)
summary += "#\n"
summary += "# Properties of grey value distribution\n"
summary += "# -----------------------------------------\n"
summary += "# Mean grey value: {meanGV:.3f}\n".format(meanGV=result.mean)
summary += "# Nominal mean grey value: {nominalMeanGV:.3f}\n".format(nominalMeanGV=result.nominalMean)
summary += "# Relative mean deviation: {reldev:.5f}\n".format(reldev=(result.mean-result.nominalMean)/self.imax)
summary += "#\n"
summary += "# Measured RMSD: {stdDev:.3f}\n".format(stdDev=result.stdDev)
summary += "# Nominal RMSD: {nominalStdDev:.3f}\n".format(nominalStdDev=result.nominalSigma)
summary += "# Relative RMSD deviation: {reldev:.5f}\n".format(reldev=(result.stdDev-result.nominalSigma)/result.nominalSigma)
summary += "#\n"
summary += "# Measured noise FWHM: {fwhm:.3f}\n".format(fwhm=result.fwhm)
summary += "# Nominal FWHM: {nominalFWHM:.3f}\n".format(nominalFWHM=result.nominalFWHM)
summary += "# Relative FWHM deviation: {reldev:.5f}\n".format(reldev=(result.fwhm-result.nominalFWHM)/result.nominalFWHM)
resultFileName = "{dir}/{name}_{subname}_summary.txt".format(dir=self.resultFileDirectory, name=self.name, subname=subtestName)
with open(resultFileName, 'w') as resultFile:
resultFile.write(summary)
resultFile.close()
histogram = "# Grey value histogram\n"
histogram += "# -----------------------------------------\n"
histogram += "# GV\tcounts\tprobability density\n"
for i in range(len(result.gvHistogram)):
histogram += "{GV}\t{counts}\t{probDensity}\n".format(GV=i, counts=result.gvHistogram[i], probDensity=result.gvHistNormalized[i])
histogramFileName = "{dir}/{name}_{subname}_histogram.txt".format(dir=self.resultFileDirectory, name=self.name, subname=subtestName)
with open(histogramFileName, 'w') as histogramFile:
histogramFile.write(histogram)
histogramFile.close()
# Make graphs:
try:
log("Plotting evaluation results...")
import matplotlib
import matplotlib.pyplot
#from matplotlib.ticker import (MultipleLocator, FormatStrFormatter, AutoMinorLocator)
# Display only an interval of +-4sigma
maxNominalSigma = result.nominalSigma
xStart = self.imax - 4*maxNominalSigma
xStop = self.imax + 4*maxNominalSigma
matplotlib.use("agg")
fig, ax = matplotlib.pyplot.subplots(nrows=1, ncols=1, figsize=(9, 7))
ax.plot(self.xValues, result.gvHistNormalized, 'o', markersize=1.0, label="Measured (Subtest {subtest})".format(subtest=subtestName), color='#1f77b4')
ax.set_xlabel("Grey Value")
ax.set_ylabel("Probability Density")
ax.set_title("Test 2D-FB-1, {subtest}: Grey Value Distribution".format(subtest=subtestName))
#ax.xaxis.set_major_locator(MultipleLocator(100))
#ax.xaxis.set_major_formatter(FormatStrFormatter('%d'))
#ax.xaxis.set_minor_locator(MultipleLocator(50))
ax.set_xlim(xStart, xStop)
ax.grid(visible=True, which='major', axis='both', color='#d9d9d9', linestyle='dashed')
ax.grid(visible=True, which='minor', axis='both', color='#e7e7e7', linestyle='dotted')
ax.legend()
fig.tight_layout(pad=5.0)
plotFilename = "{dir}/{name}_{subname}_histogram.png".format(dir=self.resultFileDirectory, name=self.name, subname=subtestName)
matplotlib.pyplot.savefig(plotFilename)
fig.clf()
matplotlib.pyplot.close('all')
except Exception as e:
log(f"Warning: Error plotting results for test {self.name}, {subtestName} using matplotlib: {e}")
log("Evaluation data for test {name}, {subtest} written to {dir}.".format(name=self.name, subtest=subtestName, dir=self.resultFileDirectory))
self.currentRun += 1
# Return image to the pipeline
return image
else:
raise Exception("Number of provided image metadata files exceeds expected number of test runs ({expected}).".format(expected=self.nExpectedRuns))
def followUp(self):
pass
Classes
class Test2D_FB_1 (resultFileDirectory='.', name=None, rawOutput=False)
-
CTSimU test 2D-FB-1: Noise.
Expand source code
class Test2D_FB_1(generalTest): """ CTSimU test 2D-FB-1: Noise. """ def __init__(self, resultFileDirectory=".", name=None, rawOutput=False): generalTest.__init__( self, testName="2D-FB-1", name=name, nExpectedRuns=2, resultFileDirectory=resultFileDirectory, rawOutput=rawOutput) self.imax = None # maximum (noise-free) grey value in free beam, as required by the scenario self.gvMax = 70000 # maximum grey value for the histograms self.xValues = None # x grey values for the histograms # Results for each sub test: self.results = [] def prepare(self): """ Preparations before the test will be run with the images from the pipeline. """ if not isinstance(self.pipe, Pipeline): self.prepared = False raise Exception("Step must be part of a processing pipeline before it can prepare. Current pipeline: {}".format(self.pipe)) if not self.prepared: self.results = [] self.xValues = numpy.linspace(0, self.gvMax, self.gvMax, endpoint=False) if self.jsonScenarioFile is not None: self.prepared = True else: raise Exception("Test {name}: Please provide a JSON scenario description.".format(name=self.name)) self.prepared = True def prepareRun(self, i): if i < len(self.subtests): if self.subtests[i] == "SNR100": self.jsonScenarioFile = "2D-FB-1_Detektor1_SNR100_2021-05-25v06r00dp.json" elif self.subtests[i] == "SNR250": self.jsonScenarioFile = "2D-FB-1_Detektor2_SNR250_2021-05-25v06r00dp.json" else: raise Exception("{key} is not a valid subtest identifier for test scenario {test}".format(key=self.subtests[i], test=self.testName)) if self.jsonScenarioFile is not None: scenario = Scenario(json_dict=json_from_pkg(pkg_scenario(self.jsonScenarioFile))) self.imax = scenario.detector.gray_value.imax.get() snrMax = scenario.detector.noise.snr_at_imax.get() if self.imax is None: raise Exception("Test {name}: Cannot find 'imax' value in JSON scenario description: {json}".format(name=self.name, json=self.jsonScenarioFile)) if snrMax is None: raise Exception("Test {name}: Cannot find 'snr_at_imax' value in JSON scenario description: {json}".format(name=self.name, json=self.jsonScenarioFile)) nominalSigma = self.imax / snrMax nominalFWHM = 2.0*math.sqrt(2.0*math.log(2.0))*self.imax / snrMax else: raise Exception("Test {name}: Cannot open JSON scenario description: {json}".format(name=self.name, json=self.jsonScenarioFile)) # Prepare this test run: self.prepare() self.results.append(Test2D_FB_1_results()) self.results[i].gvHistogram = numpy.zeros(self.gvMax, dtype=numpy.uint32) self.results[i].SNRmax = snrMax self.results[i].nominalMean = self.imax # after ff-correction self.results[i].nominalFWHM = nominalFWHM self.results[i].nominalSigma = nominalSigma else: if len(self.subtests) == 0: raise Exception("Please provide keywords that identify which metadata file belongs to which subtest. Test {testname} accepts two keywords: 'SNR100' and 'SNR250'.".format(testname=self.testName)) else: raise Exception("Number of provided image metadata files exceeds number of test runs ({expected}).".format(expected=len(self.subtests))) def run(self, image): if self.currentRun < self.nExpectedRuns: self.prepareRun(self.currentRun) i = self.currentRun subtestName = self.subtests[i] # Create Grey Value Histogram: for x in range(image.getWidth()): print("\rCalculating grey value histogram... {:.1f}%".format(100*x/image.getWidth()), end="") for y in range(image.getHeight()): gv = int(image.getPixel(x, y)) if gv >= 0 and gv < self.gvMax: self.results[i].gvHistogram[gv] += 1 print("\rCalculating grey value histogram... 100% ") self.results[i].mean = image.mean() self.results[i].stdDev = image.stdDev() self.results[i].snr = self.results[i].mean / self.results[i].stdDev self.results[i].fwhm = 2.0*math.sqrt(2.0*math.log(2.0)) * self.results[i].stdDev self.results[i].gvHistNormalized = self.results[i].gvHistogram / numpy.sum(self.results[i].gvHistogram) log("Writing evaluation results...") # Write evaluation text files: result = self.results[i] summary = "# Evaluation of test {name}, {subname}:\n".format(name=self.name, subname=subtestName) summary += "#\n" summary += "# Signal-to-noise ratio (SNR)\n" summary += "# -----------------------------------------\n" summary += "# Overall projection SNR: {snr:.3f}\n".format(snr=result.snr) summary += "# Nominal SNR: {nominalSNR:.3f}\n".format(nominalSNR=result.SNRmax) summary += "# Relative SNR deviation: {reldev:.5f}\n".format(reldev=(result.snr-result.SNRmax)/result.SNRmax) summary += "#\n" summary += "# Properties of grey value distribution\n" summary += "# -----------------------------------------\n" summary += "# Mean grey value: {meanGV:.3f}\n".format(meanGV=result.mean) summary += "# Nominal mean grey value: {nominalMeanGV:.3f}\n".format(nominalMeanGV=result.nominalMean) summary += "# Relative mean deviation: {reldev:.5f}\n".format(reldev=(result.mean-result.nominalMean)/self.imax) summary += "#\n" summary += "# Measured RMSD: {stdDev:.3f}\n".format(stdDev=result.stdDev) summary += "# Nominal RMSD: {nominalStdDev:.3f}\n".format(nominalStdDev=result.nominalSigma) summary += "# Relative RMSD deviation: {reldev:.5f}\n".format(reldev=(result.stdDev-result.nominalSigma)/result.nominalSigma) summary += "#\n" summary += "# Measured noise FWHM: {fwhm:.3f}\n".format(fwhm=result.fwhm) summary += "# Nominal FWHM: {nominalFWHM:.3f}\n".format(nominalFWHM=result.nominalFWHM) summary += "# Relative FWHM deviation: {reldev:.5f}\n".format(reldev=(result.fwhm-result.nominalFWHM)/result.nominalFWHM) resultFileName = "{dir}/{name}_{subname}_summary.txt".format(dir=self.resultFileDirectory, name=self.name, subname=subtestName) with open(resultFileName, 'w') as resultFile: resultFile.write(summary) resultFile.close() histogram = "# Grey value histogram\n" histogram += "# -----------------------------------------\n" histogram += "# GV\tcounts\tprobability density\n" for i in range(len(result.gvHistogram)): histogram += "{GV}\t{counts}\t{probDensity}\n".format(GV=i, counts=result.gvHistogram[i], probDensity=result.gvHistNormalized[i]) histogramFileName = "{dir}/{name}_{subname}_histogram.txt".format(dir=self.resultFileDirectory, name=self.name, subname=subtestName) with open(histogramFileName, 'w') as histogramFile: histogramFile.write(histogram) histogramFile.close() # Make graphs: try: log("Plotting evaluation results...") import matplotlib import matplotlib.pyplot #from matplotlib.ticker import (MultipleLocator, FormatStrFormatter, AutoMinorLocator) # Display only an interval of +-4sigma maxNominalSigma = result.nominalSigma xStart = self.imax - 4*maxNominalSigma xStop = self.imax + 4*maxNominalSigma matplotlib.use("agg") fig, ax = matplotlib.pyplot.subplots(nrows=1, ncols=1, figsize=(9, 7)) ax.plot(self.xValues, result.gvHistNormalized, 'o', markersize=1.0, label="Measured (Subtest {subtest})".format(subtest=subtestName), color='#1f77b4') ax.set_xlabel("Grey Value") ax.set_ylabel("Probability Density") ax.set_title("Test 2D-FB-1, {subtest}: Grey Value Distribution".format(subtest=subtestName)) #ax.xaxis.set_major_locator(MultipleLocator(100)) #ax.xaxis.set_major_formatter(FormatStrFormatter('%d')) #ax.xaxis.set_minor_locator(MultipleLocator(50)) ax.set_xlim(xStart, xStop) ax.grid(visible=True, which='major', axis='both', color='#d9d9d9', linestyle='dashed') ax.grid(visible=True, which='minor', axis='both', color='#e7e7e7', linestyle='dotted') ax.legend() fig.tight_layout(pad=5.0) plotFilename = "{dir}/{name}_{subname}_histogram.png".format(dir=self.resultFileDirectory, name=self.name, subname=subtestName) matplotlib.pyplot.savefig(plotFilename) fig.clf() matplotlib.pyplot.close('all') except Exception as e: log(f"Warning: Error plotting results for test {self.name}, {subtestName} using matplotlib: {e}") log("Evaluation data for test {name}, {subtest} written to {dir}.".format(name=self.name, subtest=subtestName, dir=self.resultFileDirectory)) self.currentRun += 1 # Return image to the pipeline return image else: raise Exception("Number of provided image metadata files exceeds expected number of test runs ({expected}).".format(expected=self.nExpectedRuns)) def followUp(self): pass
Ancestors
Methods
def prepare(self)
-
Preparations before the test will be run with the images from the pipeline.
Expand source code
def prepare(self): """ Preparations before the test will be run with the images from the pipeline. """ if not isinstance(self.pipe, Pipeline): self.prepared = False raise Exception("Step must be part of a processing pipeline before it can prepare. Current pipeline: {}".format(self.pipe)) if not self.prepared: self.results = [] self.xValues = numpy.linspace(0, self.gvMax, self.gvMax, endpoint=False) if self.jsonScenarioFile is not None: self.prepared = True else: raise Exception("Test {name}: Please provide a JSON scenario description.".format(name=self.name)) self.prepared = True
def prepareRun(self, i)
-
Expand source code
def prepareRun(self, i): if i < len(self.subtests): if self.subtests[i] == "SNR100": self.jsonScenarioFile = "2D-FB-1_Detektor1_SNR100_2021-05-25v06r00dp.json" elif self.subtests[i] == "SNR250": self.jsonScenarioFile = "2D-FB-1_Detektor2_SNR250_2021-05-25v06r00dp.json" else: raise Exception("{key} is not a valid subtest identifier for test scenario {test}".format(key=self.subtests[i], test=self.testName)) if self.jsonScenarioFile is not None: scenario = Scenario(json_dict=json_from_pkg(pkg_scenario(self.jsonScenarioFile))) self.imax = scenario.detector.gray_value.imax.get() snrMax = scenario.detector.noise.snr_at_imax.get() if self.imax is None: raise Exception("Test {name}: Cannot find 'imax' value in JSON scenario description: {json}".format(name=self.name, json=self.jsonScenarioFile)) if snrMax is None: raise Exception("Test {name}: Cannot find 'snr_at_imax' value in JSON scenario description: {json}".format(name=self.name, json=self.jsonScenarioFile)) nominalSigma = self.imax / snrMax nominalFWHM = 2.0*math.sqrt(2.0*math.log(2.0))*self.imax / snrMax else: raise Exception("Test {name}: Cannot open JSON scenario description: {json}".format(name=self.name, json=self.jsonScenarioFile)) # Prepare this test run: self.prepare() self.results.append(Test2D_FB_1_results()) self.results[i].gvHistogram = numpy.zeros(self.gvMax, dtype=numpy.uint32) self.results[i].SNRmax = snrMax self.results[i].nominalMean = self.imax # after ff-correction self.results[i].nominalFWHM = nominalFWHM self.results[i].nominalSigma = nominalSigma else: if len(self.subtests) == 0: raise Exception("Please provide keywords that identify which metadata file belongs to which subtest. Test {testname} accepts two keywords: 'SNR100' and 'SNR250'.".format(testname=self.testName)) else: raise Exception("Number of provided image metadata files exceeds number of test runs ({expected}).".format(expected=len(self.subtests)))
Inherited members
class Test2D_FB_1_results
-
Results for one sub test of the noise scenario.
Expand source code
class Test2D_FB_1_results: """ Results for one sub test of the noise scenario. """ def __init__(self): self.mean = 0 self.stdDev = 0 self.snr = 0 self.fwhm = 0 # Nominal values from JSON file: self.SNRmax = None self.nominalMean = None self.nominalFWHM = None self.nominalSigma = None # Grey Value Histogram: self.gvHistogram = None # stores the GV histogram self.gvHistNormalized = None # area-normalized GV histogram