-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathDECT_PhantomCalibration.py
154 lines (122 loc) · 6 KB
/
DECT_PhantomCalibration.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import SimpleITK as sitk
import sys
import os
import numpy as np
import pandas as pd
import argparse
from bonelab.util.echo_arguments import echo_arguments
def DECT_Calibration(filePath,lowenergy_filename,highenergy_filename,HA_mask_fnm):
#read in images:
mono_40_fnm = lowenergy_filename
mono_90_fnm = highenergy_filename
# mono_40 = sitk.ReadImage(filePath+'/'+mono_40_fnm+'.mha', sitk.sitkFloat32)
mono_40 = sitk.ReadImage(filePath+'/'+mono_40_fnm+'.mha', sitk.sitkFloat32)
mono_90 = sitk.ReadImage(filePath+'/'+mono_90_fnm+'.mha', sitk.sitkFloat32)
img_size = mono_40.GetSize()
img_spacing = mono_40.GetSpacing()
img_origin = mono_40.GetOrigin()
img_direction = mono_40.GetDirection()
#Calculate average intensity in phantom and find line of best fit:
HA800_label = 3
HA400_label = 2
HA100_label = 1
HAphantom_mask = sitk.ReadImage(filePath+'/'+HA_mask_fnm+'.mha')
HA800_mask = HAphantom_mask == HA800_label
HA400_mask = HAphantom_mask == HA400_label
HA100_mask = HAphantom_mask == HA100_label
HA800_phantom_40 = sitk.Mask(mono_40,HA800_mask)
HA400_phantom_40 = sitk.Mask(mono_40,HA400_mask)
HA100_phantom_40 = sitk.Mask(mono_40,HA100_mask)
HA800_phantom_90 = sitk.Mask(mono_90,HA800_mask)
HA400_phantom_90 = sitk.Mask(mono_90,HA400_mask)
HA100_phantom_90 = sitk.Mask(mono_90,HA100_mask)
HA800_array_40 = sitk.GetArrayFromImage(HA800_phantom_40)
HA800_array_90 = sitk.GetArrayFromImage(HA800_phantom_90)
HA800_mask_array = sitk.GetArrayFromImage(HA800_mask)
HA400_array_40 = sitk.GetArrayFromImage(HA400_phantom_40)
HA400_array_90 = sitk.GetArrayFromImage(HA400_phantom_90)
HA400_mask_array = sitk.GetArrayFromImage(HA400_mask)
HA100_array_40 = sitk.GetArrayFromImage(HA100_phantom_40)
HA100_array_90 = sitk.GetArrayFromImage(HA100_phantom_90)
HA100_mask_array = sitk.GetArrayFromImage(HA100_mask)
HU_800_40 = np.average(HA800_array_40, weights=HA800_mask_array)
print('HU_800: '+str(HU_800_40))
HU_400_40 = np.average(HA400_array_40, weights=HA400_mask_array)
print('HU_400: '+str(HU_400_40))
HU_100_40 = np.average(HA100_array_40, weights=HA100_mask_array)
print('HU_100: '+str(HU_100_40))
HU_800_90 = np.average(HA800_array_90, weights=HA800_mask_array)
print('HU_800: '+str(HU_800_90))
HU_400_90 = np.average(HA400_array_90, weights=HA400_mask_array)
print('HU_400: '+str(HU_400_90))
HU_100_90 = np.average(HA100_array_90, weights=HA100_mask_array)
print('HU_100: '+str(HU_100_90))
y_40 = [HU_100_40, HU_400_40, HU_800_40]
x = [100, 400, 800]
m_40,b_40 = np.polyfit(x, y_40, 1)
print('m_40: '+str(m_40))
print('b_40: '+str(b_40))
y_90 = [HU_100_90, HU_400_90, HU_800_90]
x = [100, 400, 800]
m_90,b_90 = np.polyfit(x, y_90, 1)
print('m_90: '+str(m_90))
print('b_90: '+str(b_90))
#save the slope and offset values to csv file
d = {'0 slope 40keV': [m_40], '1 offset 40keV': [b_40], '2 slope 90keV': [m_90], '3 offset 90keV': [b_90]}
df = pd.DataFrame(data=d)
df.to_csv(filePath+'/CalibrationSlope&Offset_DECT.csv',mode='a')
#Convert image from HU to mgHA using best fit line from phantom.
#Based on method from Sfeir et al., Bone 2018
mono40_array = sitk.GetArrayFromImage(mono_40)
mono90_array = sitk.GetArrayFromImage(mono_90)
mgHA_array = ((mono90_array - b_90) - (mono40_array - b_40))/(m_90 - m_40)
img_HA = sitk.GetImageFromArray(mgHA_array)
img_HA.SetSpacing(img_spacing)
img_HA.SetOrigin(img_origin)
img_HA.SetDirection(img_direction)
sitk.WriteImage(img_HA,filePath+'/'+'Calibrated_DECT.mha',True)
def main():
# Set up description
description='''Function to calibrate bone density images based on dual-energy CT input.
This program will read in two simulated monoenergetic images (generated by GE), as well as a mask image
indicating the location of calibration rods (mgHA). This script is meant to be used on images scanned with
QRM calibration phantom. Phantom mask image should have the following values:
800 mgHA rod label = 3
400 mgHA rod label = 2
100 mgHA rod label = 1
The output will be an image that's calibrated using a DECT calibration method, such that hounsfield units are converted to mgHA/ccm based on
correlation to the QRM phantom.
DECT calibration method based on pubications by Sfeir et al., Bone 2018, https://pubmed.ncbi.nlm.nih.gov/29704696/
and Gluer et al., J Computer Assisted Tomography 1988, https://pubmed.ncbi.nlm.nih.gov/3351039/
Care should be taken when selecting monoenergetic energy levels for input images. For trabecular compartment only,
40 and 90 keV energy levels work fine. For density analysis that includes cortical bone, 40keV image gets saturated
-- sould select higher energy levels (e.g., 90 and 140 keV) for any density analysis that includes cortex.
'''
# Set up argument parsing
parser = argparse.ArgumentParser(
formatter_class=argparse.RawTextHelpFormatter,
prog="DECT_PhantomCalibration",
description=description
)
parser.add_argument("filePath",
type=str,
help="The filepath")
parser.add_argument("--lowenergy_filename","-le",
default='40keV',
type=str,
help="Filename for the low energy simulated monochromatic image")
parser.add_argument("--highenergy_filename","-he",
default='90keV',
type=str,
help="Filename for the high energy simulated monochromatic image")
parser.add_argument("--HA_mask_fnm","-m",
default='HA_mask',
type=str,
help="Filename for the HA rod mask image")
# Parse and display
args = parser.parse_args()
print(echo_arguments('DECT_PhantomCalibration', vars(args)))
# Run program
DECT_Calibration(**vars(args))
if __name__ == '__main__':
main()