scanbuddy 0.1.5__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scanbuddy/__init__.py +10 -0
- scanbuddy/__version__.py +6 -0
- scanbuddy/broker/__init__.py +0 -0
- scanbuddy/broker/redis.py +29 -0
- scanbuddy/common/__init__.py +12 -0
- scanbuddy/config/__init__.py +25 -0
- scanbuddy/proc/__init__.py +375 -0
- scanbuddy/proc/params.py +58 -0
- scanbuddy/proc/snr.py +87 -0
- scanbuddy/proc/volreg.py +171 -0
- scanbuddy/view/__init__.py +0 -0
- scanbuddy/view/dash.py +781 -0
- scanbuddy/watcher/__init__.py +0 -0
- scanbuddy/watcher/dicom.py +131 -0
- scanbuddy/watcher/directory.py +39 -0
- scanbuddy-0.1.5.data/scripts/simulator.py +50 -0
- scanbuddy-0.1.5.data/scripts/start.py +67 -0
- scanbuddy-0.1.5.dist-info/LICENSE +26 -0
- scanbuddy-0.1.5.dist-info/METADATA +24 -0
- scanbuddy-0.1.5.dist-info/RECORD +22 -0
- scanbuddy-0.1.5.dist-info/WHEEL +6 -0
- scanbuddy-0.1.5.dist-info/top_level.txt +1 -0
scanbuddy/__init__.py
ADDED
scanbuddy/__version__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import redis
|
|
3
|
+
import logging
|
|
4
|
+
import redis.exceptions
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
class MessageBroker:
|
|
9
|
+
def __init__(self, host='localhost', port=6379):
|
|
10
|
+
self._host = host
|
|
11
|
+
self._port = port
|
|
12
|
+
self._conn = None
|
|
13
|
+
self._uri = f'redis://{self._host}:{self._port}'
|
|
14
|
+
self.connect()
|
|
15
|
+
|
|
16
|
+
def connect(self):
|
|
17
|
+
self._conn = redis.Redis(
|
|
18
|
+
host=self._host,
|
|
19
|
+
port=self._port,
|
|
20
|
+
decode_responses=True
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def publish(self, topic, message):
|
|
24
|
+
try:
|
|
25
|
+
self._conn.set(topic, message)
|
|
26
|
+
logger.info('message published successfully')
|
|
27
|
+
except redis.exceptions.ConnectionError as e:
|
|
28
|
+
logger.error(f'unable to send message to {self._uri}, service unavailable')
|
|
29
|
+
pass
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import platform
|
|
3
|
+
from tabulate import tabulate
|
|
4
|
+
|
|
5
|
+
def print_platform_info():
|
|
6
|
+
table = [
|
|
7
|
+
['Platform', platform.platform()],
|
|
8
|
+
['Processor', platform.processor()],
|
|
9
|
+
['Python version', platform.python_version()],
|
|
10
|
+
['GIL enabled', sys._is_gil_enabled()]
|
|
11
|
+
]
|
|
12
|
+
print(tabulate(table, tablefmt='simple_grid'))
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
import logging
|
|
3
|
+
from jsonpath_ng import parse
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
class Config:
|
|
8
|
+
def __init__(self, file):
|
|
9
|
+
self._file = file
|
|
10
|
+
self.parse()
|
|
11
|
+
|
|
12
|
+
def parse(self):
|
|
13
|
+
with open(self._file) as fo:
|
|
14
|
+
self._config = yaml.safe_load(fo)
|
|
15
|
+
|
|
16
|
+
def find_one(self, expr, default=None):
|
|
17
|
+
jsonpath = parse(expr)
|
|
18
|
+
match = jsonpath.find(self._config)
|
|
19
|
+
if not match:
|
|
20
|
+
return default
|
|
21
|
+
return match.pop().value
|
|
22
|
+
|
|
23
|
+
class ConfigError(Exception):
|
|
24
|
+
pass
|
|
25
|
+
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import pdb
|
|
4
|
+
import time
|
|
5
|
+
import math
|
|
6
|
+
import json
|
|
7
|
+
import shutil
|
|
8
|
+
import logging
|
|
9
|
+
import datetime
|
|
10
|
+
import threading
|
|
11
|
+
import numpy as np
|
|
12
|
+
from pubsub import pub
|
|
13
|
+
from sortedcontainers import SortedDict
|
|
14
|
+
from scanbuddy.proc.snr import SNR
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
class Processor:
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.reset()
|
|
21
|
+
pub.subscribe(self.reset, 'reset')
|
|
22
|
+
pub.subscribe(self.listener, 'incoming')
|
|
23
|
+
self._fdata_array = np.array([])
|
|
24
|
+
|
|
25
|
+
def reset(self):
|
|
26
|
+
self._instances = SortedDict()
|
|
27
|
+
self._slice_means = SortedDict()
|
|
28
|
+
pub.sendMessage('plot_snr', snr_metric=str(0.0))
|
|
29
|
+
logger.debug('received message to reset')
|
|
30
|
+
|
|
31
|
+
def getsize(self, obj):
|
|
32
|
+
size_in_bytes = sys.getsizeof(obj)
|
|
33
|
+
return size_in_bytes
|
|
34
|
+
|
|
35
|
+
def get_size_slice_means(self):
|
|
36
|
+
total_size = 0
|
|
37
|
+
for key in self._slice_means:
|
|
38
|
+
slice_means = self._slice_means[key]['slice_means']
|
|
39
|
+
total_size += slice_means.nbytes
|
|
40
|
+
return total_size
|
|
41
|
+
|
|
42
|
+
def get_size_mask(self):
|
|
43
|
+
total_size = 0
|
|
44
|
+
for key in self._slice_means:
|
|
45
|
+
mask = self._slice_means[key]['mask']
|
|
46
|
+
if mask is not None:
|
|
47
|
+
mb = mask.nbytes / (1024**2)
|
|
48
|
+
shape = mask.shape
|
|
49
|
+
logger.info(f'mask for instance {key} is dtype={mask.dtype}, shape={shape}, size={mb} MB')
|
|
50
|
+
total_size += mask.nbytes
|
|
51
|
+
return total_size
|
|
52
|
+
|
|
53
|
+
def listener(self, ds, path):
|
|
54
|
+
key = int(ds.InstanceNumber)
|
|
55
|
+
self._instances[key] = {
|
|
56
|
+
'path': path,
|
|
57
|
+
'volreg': None,
|
|
58
|
+
'nii_path': None
|
|
59
|
+
}
|
|
60
|
+
self._slice_means[key] = {
|
|
61
|
+
'path': path,
|
|
62
|
+
'slice_means': None,
|
|
63
|
+
'mask_threshold': None,
|
|
64
|
+
'mask': None
|
|
65
|
+
}
|
|
66
|
+
logger.debug('current state of instances')
|
|
67
|
+
logger.debug(json.dumps(self._instances, default=list, indent=2))
|
|
68
|
+
|
|
69
|
+
tasks = self.check_volreg(key)
|
|
70
|
+
logger.debug('publishing message to volreg topic with the following tasks')
|
|
71
|
+
logger.debug(json.dumps(tasks, indent=2))
|
|
72
|
+
pub.sendMessage('volreg', tasks=tasks)
|
|
73
|
+
logger.debug(f'publishing message to params topic')
|
|
74
|
+
pub.sendMessage('params', ds=ds)
|
|
75
|
+
|
|
76
|
+
logger.debug(f'after volreg')
|
|
77
|
+
logger.debug(json.dumps(self._instances, indent=2))
|
|
78
|
+
project = ds.get('StudyDescription', '[STUDY]')
|
|
79
|
+
session = ds.get('PatientID', '[PATIENT]')
|
|
80
|
+
scandesc = ds.get('SeriesDescription', '[SERIES]')
|
|
81
|
+
scannum = ds.get('SeriesNumber', '[NUMBER]')
|
|
82
|
+
subtitle_string = f'{project} • {session} • {scandesc} • {scannum}'
|
|
83
|
+
pub.sendMessage('plot', instances=self._instances, subtitle_string=subtitle_string)
|
|
84
|
+
|
|
85
|
+
snr_tasks = self.check_snr(key)
|
|
86
|
+
#logger.info(f'snr task sorted dict: {snr_tasks}')
|
|
87
|
+
|
|
88
|
+
snr = SNR()
|
|
89
|
+
nii_path = self._instances[key]['nii_path']
|
|
90
|
+
snr.do(nii_path, snr_tasks)
|
|
91
|
+
'''
|
|
92
|
+
size_of_snr_tasks = self.getsize(snr_tasks) / (1024**3)
|
|
93
|
+
size_of_slice_means = self.get_size_slice_means() / (1024**3)
|
|
94
|
+
size_of_fdata_array = self._fdata_array.nbytes / (1024**3)
|
|
95
|
+
logger.info('==============================================')
|
|
96
|
+
logger.info(f' SIZE OF snr_tasks IS {size_of_snr_tasks} GB')
|
|
97
|
+
logger.info(f' SIZE OF self._slice_means is {size_of_slice_means} GB')
|
|
98
|
+
logger.info(f' SIZE OF self._fdata_array is {size_of_fdata_array} GB')
|
|
99
|
+
logger.info('==============================================')
|
|
100
|
+
'''
|
|
101
|
+
logger.debug('after snr calculation')
|
|
102
|
+
logger.debug(json.dumps(self._instances, indent=2))
|
|
103
|
+
|
|
104
|
+
if key < 5:
|
|
105
|
+
self._num_vols = ds[(0x0020, 0x0105)].value
|
|
106
|
+
self._mask_threshold, self._decrement = self.get_mask_threshold(ds)
|
|
107
|
+
x, y, self._z, _ = self._slice_means[key]['slice_means'].shape
|
|
108
|
+
self._fdata_array = np.zeros((x, y, self._z, self._num_vols), dtype=np.float64)
|
|
109
|
+
self._slice_intensity_means = np.zeros((self._z, self._num_vols), dtype=np.float64)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
logger.info(f'shape of zeros: {self._fdata_array.shape}')
|
|
114
|
+
logger.info(f'shape of first slice means: {self._slice_means[key]['slice_means'].shape}')
|
|
115
|
+
|
|
116
|
+
if key >= 5:
|
|
117
|
+
insert_position = key - 5
|
|
118
|
+
self._fdata_array[:, :, :, insert_position] = self._slice_means[key]['slice_means'].squeeze()
|
|
119
|
+
self._slice_means[key]['slice_means'] = np.array([])
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if key > 53 and (key % 4 == 0) and key < self._num_vols:
|
|
123
|
+
logger.info('launching calculate and publish snr thread')
|
|
124
|
+
|
|
125
|
+
snr_thread = threading.Thread(target=self.calculate_and_publish_snr, args=(key,))
|
|
126
|
+
snr_thread.start()
|
|
127
|
+
|
|
128
|
+
if key == self._num_vols:
|
|
129
|
+
time.sleep(2)
|
|
130
|
+
data_path = os.path.dirname(self._instances[key]['path'])
|
|
131
|
+
logger.info(f'removing dicom dir: {data_path}')
|
|
132
|
+
shutil.rmtree(data_path)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def calculate_and_publish_snr(self, key):
|
|
136
|
+
start = time.time()
|
|
137
|
+
snr_metric = round(self.calc_snr(key), 2)
|
|
138
|
+
elapsed = time.time() - start
|
|
139
|
+
logger.info(f'snr calculation took {elapsed} seconds')
|
|
140
|
+
logger.info(f'running snr metric: {snr_metric}')
|
|
141
|
+
if np.isnan(snr_metric):
|
|
142
|
+
logger.info(f'snr is a nan, decrementing mask threshold by {self._decrement}')
|
|
143
|
+
self._mask_threshold = self._mask_threshold - self._decrement
|
|
144
|
+
logger.info(f'new threshold: {self._mask_threshold}')
|
|
145
|
+
self._slice_intensity_means = np.zeros( (self._z, self._num_vols) )
|
|
146
|
+
else:
|
|
147
|
+
pub.sendMessage('plot_snr', snr_metric=snr_metric)
|
|
148
|
+
|
|
149
|
+
def check_volreg(self, key):
|
|
150
|
+
tasks = list()
|
|
151
|
+
current = self._instances[key]
|
|
152
|
+
|
|
153
|
+
i = self._instances.bisect_left(key)
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
left_index = max(0, i - 1)
|
|
157
|
+
left = self._instances.values()[left_index]
|
|
158
|
+
logger.debug(f'to the left of {current["path"]} is {left["path"]}')
|
|
159
|
+
tasks.append((current, left))
|
|
160
|
+
except IndexError:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
right_index = i + 1
|
|
165
|
+
right = self._instances.values()[right_index]
|
|
166
|
+
logger.debug(f'to the right of {current["path"]} is {right["path"]}')
|
|
167
|
+
tasks.append((right, current))
|
|
168
|
+
except IndexError:
|
|
169
|
+
pass
|
|
170
|
+
|
|
171
|
+
return tasks
|
|
172
|
+
|
|
173
|
+
def calc_snr(self, key):
|
|
174
|
+
slice_intensity_means, slice_voxel_counts, data = self.get_mean_slice_intensities(key)
|
|
175
|
+
'''
|
|
176
|
+
size_slice_int_means = self.getsize(slice_intensity_means) / (1024**3)
|
|
177
|
+
size_data = self.getsize(data) / (1024**2)
|
|
178
|
+
logger.info('==============================================')
|
|
179
|
+
logger.info(f' SIZE OF slice_intensity_means IS {size_slice_int_means} MB')
|
|
180
|
+
logger.info(f' SIZE OF data IS {size_data} MB')
|
|
181
|
+
logger.info('==============================================')
|
|
182
|
+
'''
|
|
183
|
+
|
|
184
|
+
non_zero_columns = ~np.all(slice_intensity_means == 0, axis=0)
|
|
185
|
+
|
|
186
|
+
slice_intensity_means_2 = slice_intensity_means[:, non_zero_columns]
|
|
187
|
+
|
|
188
|
+
slice_count = slice_intensity_means_2.shape[0]
|
|
189
|
+
volume_count = slice_intensity_means_2.shape[1]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
slice_weighted_mean_mean = 0
|
|
193
|
+
slice_weighted_stdev_mean = 0
|
|
194
|
+
slice_weighted_snr_mean = 0
|
|
195
|
+
slice_weighted_max_mean = 0
|
|
196
|
+
slice_weighted_min_mean = 0
|
|
197
|
+
outlier_count = 0
|
|
198
|
+
total_voxel_count = 0
|
|
199
|
+
|
|
200
|
+
for slice_idx in range(slice_count):
|
|
201
|
+
slice_data = slice_intensity_means_2[slice_idx]
|
|
202
|
+
slice_voxel_count = slice_voxel_counts[slice_idx]
|
|
203
|
+
slice_mean = slice_data.mean()
|
|
204
|
+
slice_stdev = slice_data.std(ddof=1)
|
|
205
|
+
slice_snr = slice_mean / slice_stdev
|
|
206
|
+
|
|
207
|
+
slice_weighted_mean_mean += (slice_mean * slice_voxel_count)
|
|
208
|
+
slice_weighted_stdev_mean += (slice_stdev * slice_voxel_count)
|
|
209
|
+
slice_weighted_snr_mean += (slice_snr * slice_voxel_count)
|
|
210
|
+
|
|
211
|
+
total_voxel_count += slice_voxel_count
|
|
212
|
+
|
|
213
|
+
logger.debug(f"Slice {slice_idx}: Mean={slice_mean}, StdDev={slice_stdev}, SNR={slice_snr}")
|
|
214
|
+
|
|
215
|
+
return slice_weighted_snr_mean / total_voxel_count
|
|
216
|
+
|
|
217
|
+
def get_mean_slice_intensities(self, key):
|
|
218
|
+
|
|
219
|
+
data = self.generate_mask(key)
|
|
220
|
+
|
|
221
|
+
mask = np.ma.getmask(data)
|
|
222
|
+
dim_x, dim_y, dim_z, _ = data.shape
|
|
223
|
+
|
|
224
|
+
dim_t = key - 4
|
|
225
|
+
|
|
226
|
+
slice_voxel_counts = np.zeros( (dim_z), dtype='uint32' )
|
|
227
|
+
slice_size = dim_x * dim_y
|
|
228
|
+
|
|
229
|
+
for slice_idx in range(dim_z):
|
|
230
|
+
slice_voxel_counts[slice_idx] = slice_size - mask[:,:,slice_idx,0].sum()
|
|
231
|
+
|
|
232
|
+
zero_columns = np.where(np.all(self._slice_intensity_means[:,:dim_t] == 0, axis=0))[0].tolist()
|
|
233
|
+
|
|
234
|
+
logger.info(f'volumes being calculated: {zero_columns}')
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
if len(zero_columns) > 20:
|
|
238
|
+
for volume_idx in range(dim_t):
|
|
239
|
+
for slice_idx in range(dim_z):
|
|
240
|
+
slice_data = data[:,:,slice_idx,volume_idx]
|
|
241
|
+
self._slice_intensity_means[slice_idx,volume_idx] = slice_data.mean()
|
|
242
|
+
|
|
243
|
+
else:
|
|
244
|
+
|
|
245
|
+
for volume_idx in zero_columns:
|
|
246
|
+
for slice_idx in range(dim_z):
|
|
247
|
+
slice_data = data[:,:,slice_idx,volume_idx]
|
|
248
|
+
slice_vol_mean = slice_data.mean()
|
|
249
|
+
self._slice_intensity_means[slice_idx,volume_idx] = slice_vol_mean
|
|
250
|
+
|
|
251
|
+
if key == self._num_vols:
|
|
252
|
+
start = time.time()
|
|
253
|
+
differing_slices = self.find_mask_differences(key)
|
|
254
|
+
logger.info(f'finding mask differences took {time.time() - start}')
|
|
255
|
+
logger.info(f'recalculating slice means at the following slices: {differing_slices}')
|
|
256
|
+
logger.info(f'total of {len(differing_slices)} new slices being computed')
|
|
257
|
+
for volume_idx in range(dim_t):
|
|
258
|
+
for slice_idx in differing_slices:
|
|
259
|
+
slice_data = data[:,:,slice_idx,volume_idx]
|
|
260
|
+
slice_vol_mean = slice_data.mean()
|
|
261
|
+
self._slice_intensity_means[slice_idx,volume_idx] = slice_vol_mean
|
|
262
|
+
|
|
263
|
+
elif key % 2 == 0:
|
|
264
|
+
#elif key % 6 == 0:
|
|
265
|
+
logger.info(f'inside the even calculation')
|
|
266
|
+
start = time.time()
|
|
267
|
+
differing_slices = self.find_mask_differences(key)
|
|
268
|
+
logger.info(f'finding mask differences took {time.time() - start}')
|
|
269
|
+
logger.info(f'recalculating slice means at the following slices: {differing_slices}')
|
|
270
|
+
logger.info(f'total of {len(differing_slices)} new slices being computed')
|
|
271
|
+
for volume_idx in range(0, dim_t, 8):
|
|
272
|
+
for slice_idx in differing_slices:
|
|
273
|
+
slice_data = data[:,:,slice_idx,volume_idx]
|
|
274
|
+
slice_vol_mean = slice_data.mean()
|
|
275
|
+
self._slice_intensity_means[slice_idx,volume_idx] = slice_vol_mean
|
|
276
|
+
|
|
277
|
+
else:
|
|
278
|
+
#elif key % 5 == 0:
|
|
279
|
+
logger.info(f'inside the odd calculation')
|
|
280
|
+
start = time.time()
|
|
281
|
+
differing_slices = self.find_mask_differences(key)
|
|
282
|
+
logger.info(f'finding mask differences took {time.time() - start}')
|
|
283
|
+
logger.info(f'recalculating slice means at the following slices: {differing_slices}')
|
|
284
|
+
logger.info(f'total of {len(differing_slices)} new slices being computed')
|
|
285
|
+
for volume_idx in range(5, dim_t, 8):
|
|
286
|
+
for slice_idx in differing_slices:
|
|
287
|
+
slice_data = data[:,:,slice_idx,volume_idx]
|
|
288
|
+
slice_vol_mean = slice_data.mean()
|
|
289
|
+
self._slice_intensity_means[slice_idx,volume_idx] = slice_vol_mean
|
|
290
|
+
|
|
291
|
+
return self._slice_intensity_means[:, :dim_t], slice_voxel_counts, data
|
|
292
|
+
|
|
293
|
+
def generate_mask(self, key):
|
|
294
|
+
|
|
295
|
+
mean_data = np.mean(self._fdata_array[...,:key-4], axis=3)
|
|
296
|
+
|
|
297
|
+
numpy_3d_mask = np.zeros(mean_data.shape, dtype=bool)
|
|
298
|
+
|
|
299
|
+
to_mask = (mean_data <= self._mask_threshold)
|
|
300
|
+
|
|
301
|
+
mask_lower_count = int(to_mask.sum())
|
|
302
|
+
|
|
303
|
+
numpy_3d_mask = numpy_3d_mask | to_mask
|
|
304
|
+
|
|
305
|
+
numpy_4d_mask = np.zeros(self._fdata_array[..., :key-4].shape, dtype=bool)
|
|
306
|
+
|
|
307
|
+
numpy_4d_mask[numpy_3d_mask] = True
|
|
308
|
+
|
|
309
|
+
masked_data = np.ma.masked_array(self._fdata_array[..., :key-4], mask=numpy_4d_mask)
|
|
310
|
+
|
|
311
|
+
mask = np.ma.getmask(masked_data)
|
|
312
|
+
|
|
313
|
+
self._slice_means[key]['mask'] = mask
|
|
314
|
+
'''
|
|
315
|
+
size_mask = self.get_size_mask() / (1024**2)
|
|
316
|
+
logger.info(f'===============================')
|
|
317
|
+
logger.info(f'SHAPE OF MASK IS {mask.shape}')
|
|
318
|
+
logger.info(f'SIZE OF MASK IS {size_mask} MB')
|
|
319
|
+
logger.info(f'===============================')
|
|
320
|
+
'''
|
|
321
|
+
|
|
322
|
+
return masked_data
|
|
323
|
+
|
|
324
|
+
def find_mask_differences(self, key):
|
|
325
|
+
num_old_vols = key - 8
|
|
326
|
+
last_50 = num_old_vols - 50
|
|
327
|
+
logger.info(f'looking for mask differences between {key} and {key - 4}')
|
|
328
|
+
prev_mask = self._slice_means[key - 4]['mask']
|
|
329
|
+
current_mask = self._slice_means[key]['mask']
|
|
330
|
+
differences = prev_mask[:,:,:,-50:] != current_mask[:,:,:,last_50:num_old_vols]
|
|
331
|
+
diff_indices = np.where(differences)
|
|
332
|
+
differing_slices = []
|
|
333
|
+
for index in zip(*diff_indices):
|
|
334
|
+
if int(index[2]) not in differing_slices:
|
|
335
|
+
differing_slices.append(int(index[2]))
|
|
336
|
+
logger.info(f'reclaim memory for instance {key - 4 } mask')
|
|
337
|
+
self._slice_means[key - 4]['mask'] = np.array([])
|
|
338
|
+
return differing_slices
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def get_mask_threshold(self, ds):
|
|
342
|
+
bits_stored = ds.get('BitsStored', None)
|
|
343
|
+
receive_coil = self.find_coil(ds)
|
|
344
|
+
|
|
345
|
+
if bits_stored == 12:
|
|
346
|
+
logger.debug(f'scan has "{bits_stored}" bits and receive coil "{receive_coil}", setting mask threshold to 150.0')
|
|
347
|
+
return 150.0, 10
|
|
348
|
+
if bits_stored == 16:
|
|
349
|
+
if receive_coil in ['Head_32']:
|
|
350
|
+
logger.debug(f'scan has "{bits_stored}" bits and receive coil "{receive_coil}", setting mask threshold to 1500.0')
|
|
351
|
+
return 1500.0, 100
|
|
352
|
+
if receive_coil in ['Head_64', 'HeadNeck_64']:
|
|
353
|
+
logger.debug(f'scan has "{bits_stored}" bits and receive coil "{receive_coil}", setting mask threshold to 3000.0')
|
|
354
|
+
return 3000.0, 300
|
|
355
|
+
raise MaskThresholdError(f'unexpected bits stored "{bits_stored}" + receive coil "{receive_coil}"')
|
|
356
|
+
|
|
357
|
+
def find_coil(self, ds):
|
|
358
|
+
seq = ds[(0x5200, 0x9229)][0]
|
|
359
|
+
seq = seq[(0x0018, 0x9042)][0]
|
|
360
|
+
return seq[(0x0018, 0x1250)].value
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def check_snr(self, key):
|
|
364
|
+
tasks = list()
|
|
365
|
+
|
|
366
|
+
current_idx = self._slice_means.bisect_left(key)
|
|
367
|
+
|
|
368
|
+
try:
|
|
369
|
+
value = self._slice_means.values()[current_idx]
|
|
370
|
+
tasks.append(value)
|
|
371
|
+
except IndexError:
|
|
372
|
+
pass
|
|
373
|
+
|
|
374
|
+
return tasks
|
|
375
|
+
|
scanbuddy/proc/params.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import logging
|
|
3
|
+
from pubsub import pub
|
|
4
|
+
|
|
5
|
+
logger = logging.getLogger(__name__)
|
|
6
|
+
|
|
7
|
+
class Params:
|
|
8
|
+
def __init__(self, config, broker=None):
|
|
9
|
+
self._config = config.find_one('$.params', dict())
|
|
10
|
+
self._broker = broker
|
|
11
|
+
self._checked = False
|
|
12
|
+
pub.subscribe(self.listener, 'params')
|
|
13
|
+
pub.subscribe(self.reset, 'reset')
|
|
14
|
+
|
|
15
|
+
def reset(self):
|
|
16
|
+
self._checked = False
|
|
17
|
+
|
|
18
|
+
def listener(self, ds):
|
|
19
|
+
if self._checked:
|
|
20
|
+
logger.info(f'already checked an instance from series {ds.SeriesNumber}')
|
|
21
|
+
return
|
|
22
|
+
for item in self._config:
|
|
23
|
+
args = self._config[item]
|
|
24
|
+
f = getattr(self, item)
|
|
25
|
+
f(ds, args)
|
|
26
|
+
|
|
27
|
+
def coil_elements(self, ds, args):
|
|
28
|
+
self._checked = True
|
|
29
|
+
patient_name = ds.get('PatientName', 'UNKNOWN PATIENT')
|
|
30
|
+
series_number = ds.get('SeriesNumber', 'UNKNOWN SERIES')
|
|
31
|
+
receive_coil = self.findcoil(ds)
|
|
32
|
+
coil_elements = self.findcoilelements(ds)
|
|
33
|
+
message = args['message'].format(
|
|
34
|
+
SESSION=patient_name,
|
|
35
|
+
SERIES=series_number,
|
|
36
|
+
RECEIVE_COIL=receive_coil,
|
|
37
|
+
COIL_ELEMENTS=coil_elements
|
|
38
|
+
)
|
|
39
|
+
for bad in args['bad']:
|
|
40
|
+
a = ( receive_coil, coil_elements )
|
|
41
|
+
b = ( bad['receive_coil'], bad['coil_elements'] )
|
|
42
|
+
logger.info(f'checking if {a} == {b}')
|
|
43
|
+
if a == b:
|
|
44
|
+
logger.warning(message)
|
|
45
|
+
logger.info(f'publishing message to message broker')
|
|
46
|
+
self._broker.publish('scanbuddy_messages', message)
|
|
47
|
+
break
|
|
48
|
+
|
|
49
|
+
def findcoil(self, ds):
|
|
50
|
+
seq = ds[(0x5200, 0x9229)][0]
|
|
51
|
+
seq = seq[(0x0018, 0x9042)][0]
|
|
52
|
+
return seq[(0x0018, 0x1250)].value
|
|
53
|
+
|
|
54
|
+
def findcoilelements(self, ds):
|
|
55
|
+
seq = ds[(0x5200, 0x9230)][0]
|
|
56
|
+
seq = seq[(0x0021, 0x11fe)][0]
|
|
57
|
+
return seq[(0x0021, 0x114f)].value
|
|
58
|
+
|
scanbuddy/proc/snr.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import pdb
|
|
4
|
+
import glob
|
|
5
|
+
import json
|
|
6
|
+
import time
|
|
7
|
+
import shutil
|
|
8
|
+
import random
|
|
9
|
+
import logging
|
|
10
|
+
import pydicom
|
|
11
|
+
import subprocess
|
|
12
|
+
import numpy as np
|
|
13
|
+
import nibabel as nib
|
|
14
|
+
from pubsub import pub
|
|
15
|
+
import collections as c
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
class SNR:
|
|
21
|
+
def __init__(self):
|
|
22
|
+
pub.subscribe(self.listener, 'snr')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def do(self, nii_path, tasks):
|
|
26
|
+
logger.info('received tasks for fdata extraction')
|
|
27
|
+
self.snr_tasks = tasks
|
|
28
|
+
self._nii_path = nii_path
|
|
29
|
+
|
|
30
|
+
self.run()
|
|
31
|
+
|
|
32
|
+
def listener(self, nii_path, tasks):
|
|
33
|
+
logger.info('received tasks for fdata extraction')
|
|
34
|
+
self.snr_tasks = tasks
|
|
35
|
+
self._nii_path = nii_path
|
|
36
|
+
|
|
37
|
+
self.run()
|
|
38
|
+
|
|
39
|
+
def run(self):
|
|
40
|
+
self.get_num_tasks()
|
|
41
|
+
|
|
42
|
+
start = time.time()
|
|
43
|
+
|
|
44
|
+
dcm = self.read_dicoms(self._num_tasks-1)
|
|
45
|
+
|
|
46
|
+
instance_num = int(dcm.InstanceNumber)
|
|
47
|
+
|
|
48
|
+
logger.info(f'extracting fdata for volume {instance_num}')
|
|
49
|
+
data_array = self.get_nii_array()
|
|
50
|
+
|
|
51
|
+
self.insert_snr(data_array, self.snr_tasks[0], None)
|
|
52
|
+
|
|
53
|
+
self.clean_dir(instance_num)
|
|
54
|
+
|
|
55
|
+
elapsed = time.time() - start
|
|
56
|
+
|
|
57
|
+
logger.info(f'extracting fdata from volume {instance_num} took {elapsed} seconds')
|
|
58
|
+
|
|
59
|
+
def get_nii_array(self):
|
|
60
|
+
return nib.load(self._nii_path).get_fdata()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def insert_snr(self, slice_means, task, mask):
|
|
64
|
+
logger.debug(f'state of tasks when inserting {self.snr_tasks}')
|
|
65
|
+
x, y, z = slice_means.shape
|
|
66
|
+
data_array_4d = slice_means.reshape(x, y, z, 1)
|
|
67
|
+
task['slice_means'] = data_array_4d
|
|
68
|
+
|
|
69
|
+
def read_dicoms(self, last_idx):
|
|
70
|
+
logger.debug(f'state of tasks when reading dicom: {self.snr_tasks}')
|
|
71
|
+
dcm1 = self.snr_tasks[0]['path']
|
|
72
|
+
|
|
73
|
+
ds1 = pydicom.dcmread(dcm1, force=True, stop_before_pixels=True)
|
|
74
|
+
|
|
75
|
+
return ds1
|
|
76
|
+
|
|
77
|
+
def clean_dir(self, instance_num):
|
|
78
|
+
if instance_num == 1:
|
|
79
|
+
return
|
|
80
|
+
for file in glob.glob(f'{os.path.dirname(self._nii_path)}/*.json'):
|
|
81
|
+
os.remove(file)
|
|
82
|
+
for file in glob.glob(f'{os.path.dirname(self._nii_path)}/*.nii'):
|
|
83
|
+
os.remove(file)
|
|
84
|
+
|
|
85
|
+
def get_num_tasks(self):
|
|
86
|
+
self._num_tasks = len(self.snr_tasks)
|
|
87
|
+
|