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 ADDED
@@ -0,0 +1,10 @@
1
+ import os
2
+ import tarfile
3
+ from . import __version__
4
+
5
+ def version():
6
+ return __version__.__version__
7
+
8
+ def archive(indir, output):
9
+ with tarfile.open(output, 'w:gz') as tar:
10
+ tar.add(indir, os.path.basename(indir))
@@ -0,0 +1,6 @@
1
+ __title__ = 'scanbuddy'
2
+ __description__ = 'Scan Buddy'
3
+ __url__ = 'https://github.com/harvard-nrg/scanbuddy'
4
+ __version__ = '0.1.5'
5
+ __author__ = 'Neuroinformatics Research Group'
6
+ __author_email__ = 'info@neuroinfo.org'
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
+
@@ -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
+