1
|
|
"""
|
2
|
|
Useful function for managing Input/Output
|
3
|
|
|
4
|
|
Copyright (C) 2017-2019 Jiri Borovec <jiri.borovec@fel.cvut.cz>
|
5
|
|
"""
|
6
|
|
|
7
|
30
|
import logging
|
8
|
30
|
import os
|
9
|
30
|
import warnings
|
10
|
30
|
from functools import wraps
|
11
|
|
|
12
|
30
|
import SimpleITK as sitk
|
13
|
30
|
import cv2 as cv
|
14
|
30
|
import nibabel
|
15
|
30
|
import numpy as np
|
16
|
30
|
import pandas as pd
|
17
|
30
|
import yaml
|
18
|
30
|
from PIL import Image
|
19
|
30
|
from skimage.color import rgb2gray, gray2rgb
|
20
|
|
|
21
|
|
#: landmarks coordinates, loading from CSV file
|
22
|
30
|
LANDMARK_COORDS = ['X', 'Y']
|
23
|
|
# PIL.Image.DecompressionBombError: could be decompression bomb DOS attack.
|
24
|
|
# SEE: https://gitlab.mister-muffin.de/josch/img2pdf/issues/42
|
25
|
30
|
Image.MAX_IMAGE_PIXELS = None
|
26
|
|
|
27
|
|
|
28
|
30
|
def create_folder(path_folder, ok_existing=True):
|
29
|
|
""" create a folder if it not exists
|
30
|
|
|
31
|
|
:param str path_folder: path to creating folder
|
32
|
|
:param bool ok_existing: suppress warning for missing
|
33
|
|
:return str|None: path to created folder
|
34
|
|
|
35
|
|
>>> p_dir = create_folder('./sample-folder', ok_existing=True)
|
36
|
|
>>> create_folder('./sample-folder', ok_existing=False)
|
37
|
|
False
|
38
|
|
>>> os.rmdir(p_dir)
|
39
|
|
"""
|
40
|
30
|
path_folder = os.path.abspath(path_folder)
|
41
|
30
|
if not os.path.isdir(path_folder):
|
42
|
30
|
try:
|
43
|
30
|
os.makedirs(path_folder, mode=0o775)
|
44
|
0
|
except Exception:
|
45
|
0
|
logging.exception(
|
46
|
|
'Something went wrong (probably parallel access), the status of "%s" is %s',
|
47
|
|
path_folder,
|
48
|
|
os.path.isdir(path_folder),
|
49
|
|
)
|
50
|
0
|
path_folder = None
|
51
|
30
|
elif not ok_existing:
|
52
|
30
|
logging.warning('Folder already exists: %s', path_folder)
|
53
|
30
|
path_folder = False
|
54
|
|
|
55
|
30
|
return path_folder
|
56
|
|
|
57
|
|
|
58
|
30
|
def load_landmarks(path_file):
|
59
|
|
""" load landmarks in csv and txt format
|
60
|
|
|
61
|
|
:param str path_file: path to the input file
|
62
|
|
:return ndarray: np.array<np_points, dim> of landmarks points
|
63
|
|
|
64
|
|
>>> points = np.array([[1, 2], [3, 4], [5, 6]])
|
65
|
|
>>> save_landmarks('./sample_landmarks.csv', points)
|
66
|
|
>>> pts1 = load_landmarks('./sample_landmarks.csv')
|
67
|
|
>>> pts2 = load_landmarks('./sample_landmarks.pts')
|
68
|
|
>>> np.array_equal(pts1, pts2)
|
69
|
|
True
|
70
|
|
>>> os.remove('./sample_landmarks.csv')
|
71
|
|
>>> os.remove('./sample_landmarks.pts')
|
72
|
|
|
73
|
|
>>> # Wrong loading
|
74
|
|
>>> load_landmarks('./sample_landmarks.file')
|
75
|
|
>>> open('./sample_landmarks.file', 'w').close()
|
76
|
|
>>> load_landmarks('./sample_landmarks.file')
|
77
|
|
>>> os.remove('./sample_landmarks.file')
|
78
|
|
"""
|
79
|
30
|
if not os.path.isfile(path_file):
|
80
|
30
|
logging.warning('missing landmarks "%s"', path_file)
|
81
|
30
|
return None
|
82
|
30
|
_, ext = os.path.splitext(path_file)
|
83
|
30
|
if ext == '.csv':
|
84
|
30
|
return load_landmarks_csv(path_file)
|
85
|
30
|
elif ext == '.pts':
|
86
|
30
|
return load_landmarks_pts(path_file)
|
87
|
|
else:
|
88
|
30
|
logging.error('not supported landmarks file: %s', os.path.basename(path_file))
|
89
|
|
|
90
|
|
|
91
|
30
|
def load_landmarks_pts(path_file):
|
92
|
|
""" load file with landmarks in txt format
|
93
|
|
|
94
|
|
:param str path_file: path to the input file
|
95
|
|
:return ndarray: np.array<np_points, dim> of landmarks points
|
96
|
|
|
97
|
|
>>> points = np.array([[1, 2], [3, 4], [5, 6]])
|
98
|
|
>>> p_lnds = save_landmarks_pts('./sample_landmarks.csv', points)
|
99
|
|
>>> p_lnds
|
100
|
|
'./sample_landmarks.pts'
|
101
|
|
>>> pts = load_landmarks_pts(p_lnds)
|
102
|
|
>>> pts # doctest: +NORMALIZE_WHITESPACE
|
103
|
|
array([[ 1., 2.],
|
104
|
|
[ 3., 4.],
|
105
|
|
[ 5., 6.]])
|
106
|
|
>>> os.remove('./sample_landmarks.pts')
|
107
|
|
|
108
|
|
>>> # Empty landmarks
|
109
|
|
>>> open('./sample_landmarks.pts', 'w').close()
|
110
|
|
>>> load_landmarks_pts('./sample_landmarks.pts').size
|
111
|
|
0
|
112
|
|
>>> os.remove('./sample_landmarks.pts')
|
113
|
|
"""
|
114
|
30
|
assert os.path.isfile(path_file), 'missing file "%s"' % path_file
|
115
|
30
|
with open(path_file, 'r') as fp:
|
116
|
30
|
data = fp.read()
|
117
|
30
|
lines = data.split('\n')
|
118
|
|
# lines = [re.sub("(\\r|)\\n$", '', line) for line in lines]
|
119
|
30
|
if len(lines) < 2:
|
120
|
30
|
logging.warning('invalid format: file has less then 2 lines, "%r"', lines)
|
121
|
30
|
return np.zeros((0, 2))
|
122
|
30
|
nb_points = int(lines[1])
|
123
|
30
|
points = [[float(n) for n in line.split()] for line in lines[2:] if line]
|
124
|
30
|
assert nb_points == len(points), 'number of declared (%i) and found (%i) ' \
|
125
|
|
'does not match' % (nb_points, len(points))
|
126
|
30
|
return np.array(points, dtype=np.float)
|
127
|
|
|
128
|
|
|
129
|
30
|
def load_landmarks_csv(path_file):
|
130
|
|
""" load file with landmarks in cdv format
|
131
|
|
|
132
|
|
:param str path_file: path to the input file
|
133
|
|
:return ndarray: np.array<np_points, dim> of landmarks points
|
134
|
|
|
135
|
|
>>> points = np.array([[1, 2], [3, 4], [5, 6]])
|
136
|
|
>>> p_lnds = save_landmarks_csv('./sample_landmarks', points)
|
137
|
|
>>> p_lnds
|
138
|
|
'./sample_landmarks.csv'
|
139
|
|
>>> pts = load_landmarks_csv(p_lnds)
|
140
|
|
>>> pts # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
|
141
|
|
array([[1, 2],
|
142
|
|
[3, 4],
|
143
|
|
[5, 6]]...)
|
144
|
|
>>> os.remove('./sample_landmarks.csv')
|
145
|
|
"""
|
146
|
30
|
assert os.path.isfile(path_file), 'missing file "%s"' % path_file
|
147
|
30
|
df = pd.read_csv(path_file, index_col=0)
|
148
|
30
|
points = df[LANDMARK_COORDS].values
|
149
|
30
|
return points
|
150
|
|
|
151
|
|
|
152
|
30
|
def save_landmarks(path_file, landmarks):
|
153
|
|
""" save landmarks into a specific file
|
154
|
|
|
155
|
|
both used formats csv/pts is using the same coordinate frame,
|
156
|
|
the origin (0, 0) is located in top left corner of the image
|
157
|
|
|
158
|
|
:param str path_file: path to the output file
|
159
|
|
:param landmarks: np.array<np_points, dim>
|
160
|
|
"""
|
161
|
30
|
assert os.path.isdir(os.path.dirname(path_file)), \
|
162
|
|
'missing folder "%s"' % os.path.dirname(path_file)
|
163
|
30
|
path_file, _ = os.path.splitext(path_file)
|
164
|
30
|
landmarks = landmarks.values if isinstance(landmarks, pd.DataFrame) else landmarks
|
165
|
30
|
save_landmarks_csv(path_file + '.csv', landmarks)
|
166
|
30
|
save_landmarks_pts(path_file + '.pts', landmarks)
|
167
|
|
|
168
|
|
|
169
|
30
|
def save_landmarks_pts(path_file, landmarks):
|
170
|
|
""" save landmarks into a txt file
|
171
|
|
|
172
|
|
we are using VTK pointdata legacy format, ITK compatible::
|
173
|
|
|
174
|
|
<index, point>
|
175
|
|
<number of points>
|
176
|
|
point1-x point1-y [point1-z]
|
177
|
|
point2-x point2-y [point2-z]
|
178
|
|
|
179
|
|
.. seealso:: https://simpleelastix.readthedocs.io/PointBasedRegistration.html
|
180
|
|
|
181
|
|
:param str path_file: path to the output file
|
182
|
|
:param landmarks: np.array<np_points, dim>
|
183
|
|
:return str: file path
|
184
|
|
"""
|
185
|
30
|
assert os.path.isdir(os.path.dirname(path_file)), \
|
186
|
|
'missing folder "%s"' % os.path.dirname(path_file)
|
187
|
30
|
path_file = os.path.splitext(path_file)[0] + '.pts'
|
188
|
30
|
lines = ['point', str(len(landmarks))]
|
189
|
30
|
lines += [' '.join(str(i) for i in point) for point in landmarks]
|
190
|
30
|
with open(path_file, 'w') as fp:
|
191
|
30
|
fp.write('\n'.join(lines))
|
192
|
30
|
return path_file
|
193
|
|
|
194
|
|
|
195
|
30
|
def save_landmarks_csv(path_file, landmarks):
|
196
|
|
""" save landmarks into a csv file
|
197
|
|
|
198
|
|
we are using simple format::
|
199
|
|
|
200
|
|
,X,Y
|
201
|
|
0,point1-x,point1-y
|
202
|
|
1,point2-x,point2-y
|
203
|
|
|
204
|
|
:param str path_file: path to the output file
|
205
|
|
:param landmarks: np.array<np_points, dim>
|
206
|
|
:return str: file path
|
207
|
|
"""
|
208
|
30
|
assert os.path.isdir(os.path.dirname(path_file)), \
|
209
|
|
'missing folder "%s"' % os.path.dirname(path_file)
|
210
|
30
|
path_file = os.path.splitext(path_file)[0] + '.csv'
|
211
|
30
|
df = pd.DataFrame(landmarks, columns=LANDMARK_COORDS)
|
212
|
30
|
df.index = np.arange(1, len(df) + 1)
|
213
|
30
|
df.to_csv(path_file)
|
214
|
30
|
return path_file
|
215
|
|
|
216
|
|
|
217
|
30
|
def update_path(a_path, pre_path=None, lim_depth=5, absolute=True):
|
218
|
|
""" bubble in the folder tree up until it found desired file
|
219
|
|
otherwise return original one
|
220
|
|
|
221
|
|
:param str a_path: original path
|
222
|
|
:param str|None base_path: special case when you want to add something before
|
223
|
|
:param int lim_depth: max depth of going up in the folder tree
|
224
|
|
:param bool absolute: format as absolute path
|
225
|
|
:return str: updated path if it exists otherwise the original one
|
226
|
|
|
227
|
|
>>> os.path.exists(update_path('./birl', absolute=False))
|
228
|
|
True
|
229
|
|
>>> os.path.exists(update_path('/', absolute=False))
|
230
|
|
True
|
231
|
|
>>> os.path.exists(update_path('~', absolute=False))
|
232
|
|
True
|
233
|
|
"""
|
234
|
30
|
path_ = str(a_path)
|
235
|
30
|
if path_.startswith('/'):
|
236
|
30
|
return path_
|
237
|
30
|
elif path_.startswith('~'):
|
238
|
30
|
path_ = os.path.expanduser(path_)
|
239
|
|
# special case when you want to add something before
|
240
|
30
|
elif pre_path:
|
241
|
30
|
path_ = os.path.join(pre_path, path_)
|
242
|
|
|
243
|
30
|
tmp_path = path_[2:] if path_.startswith('./') else path_
|
244
|
30
|
for _ in range(lim_depth):
|
245
|
30
|
if os.path.exists(tmp_path):
|
246
|
30
|
path_ = tmp_path
|
247
|
30
|
break
|
248
|
30
|
tmp_path = os.path.join('..', tmp_path)
|
249
|
|
|
250
|
30
|
if absolute:
|
251
|
30
|
path_ = os.path.abspath(path_)
|
252
|
30
|
return path_
|
253
|
|
|
254
|
|
|
255
|
30
|
def io_image_decorate(func):
|
256
|
|
""" costume decorator to suppers debug messages from the PIL function
|
257
|
|
to suppress PIl debug logging
|
258
|
|
- DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
|
259
|
|
|
260
|
|
:param func: decorated function
|
261
|
|
:return func: output of the decor. function
|
262
|
|
"""
|
263
|
|
|
264
|
30
|
@wraps(func)
|
265
|
6
|
def wrap(*args, **kwargs):
|
266
|
30
|
log_level = logging.getLogger().getEffectiveLevel()
|
267
|
30
|
logging.getLogger().setLevel(logging.INFO)
|
268
|
30
|
with warnings.catch_warnings():
|
269
|
30
|
warnings.simplefilter("ignore")
|
270
|
30
|
response = func(*args, **kwargs)
|
271
|
30
|
logging.getLogger().setLevel(log_level)
|
272
|
30
|
return response
|
273
|
|
|
274
|
30
|
return wrap
|
275
|
|
|
276
|
|
|
277
|
30
|
@io_image_decorate
|
278
|
30
|
def image_sizes(path_image, decimal=1):
|
279
|
|
""" get image size (without loading image raster)
|
280
|
|
|
281
|
|
:param str path_image: path to the image
|
282
|
|
:param int decimal: rounding digits
|
283
|
|
:return tuple(tuple(int,int),float): image size (height, width) and diagonal
|
284
|
|
|
285
|
|
>>> img = np.random.random((50, 75, 3))
|
286
|
|
>>> save_image('./test_image.jpg', img)
|
287
|
|
>>> image_sizes('./test_image.jpg', decimal=0)
|
288
|
|
((50, 75), 90.0)
|
289
|
|
>>> os.remove('./test_image.jpg')
|
290
|
|
"""
|
291
|
30
|
assert os.path.isfile(path_image), 'missing image: %s' % path_image
|
292
|
30
|
width, height = Image.open(path_image).size
|
293
|
30
|
img_diag = np.sqrt(np.sum(np.array([height, width])**2))
|
294
|
30
|
return (height, width), np.round(img_diag, decimal)
|
295
|
|
|
296
|
|
|
297
|
30
|
@io_image_decorate
|
298
|
30
|
def load_image(path_image, force_rgb=True):
|
299
|
|
""" load the image in value range (0, 1)
|
300
|
|
|
301
|
|
:param str path_image: path to the image
|
302
|
|
:param bool force_rgb: convert RGB image
|
303
|
|
:return ndarray: np.array<height, width, ch>
|
304
|
|
|
305
|
|
>>> img = np.random.random((50, 50))
|
306
|
|
>>> save_image('./test_image.jpg', img)
|
307
|
|
>>> img2 = load_image('./test_image.jpg')
|
308
|
|
>>> img2.max() <= 1.
|
309
|
|
True
|
310
|
|
>>> os.remove('./test_image.jpg')
|
311
|
|
"""
|
312
|
30
|
assert os.path.isfile(path_image), 'missing image "%s"' % path_image
|
313
|
30
|
image = np.array(Image.open(path_image))
|
314
|
30
|
while image.max() > 1.5:
|
315
|
30
|
image = image / 255.
|
316
|
30
|
if force_rgb and (image.ndim == 2 or image.shape[2] == 1):
|
317
|
30
|
image = image[:, :, 0] if image.ndim == 3 else image
|
318
|
30
|
image = gray2rgb(image)
|
319
|
30
|
return image.astype(np.float32)
|
320
|
|
|
321
|
|
|
322
|
30
|
def convert_ndarray2image(image):
|
323
|
|
""" convert ndarray to PIL image if it not already
|
324
|
|
|
325
|
|
:param ndarray image: input image
|
326
|
|
:return Image: output image
|
327
|
|
|
328
|
|
>>> img = np.random.random((50, 50, 3))
|
329
|
|
>>> image = convert_ndarray2image(img)
|
330
|
|
>>> isinstance(image, Image.Image)
|
331
|
|
True
|
332
|
|
"""
|
333
|
30
|
if isinstance(image, np.ndarray):
|
334
|
30
|
if np.max(image) <= 1.5:
|
335
|
30
|
image = image * 255
|
336
|
30
|
np.clip(image, a_min=0, a_max=255, out=image)
|
337
|
30
|
if image.ndim == 3 and image.shape[-1] < 3:
|
338
|
0
|
image = image[:, :, 0]
|
339
|
30
|
image = Image.fromarray(image.astype(np.uint8))
|
340
|
30
|
return image
|
341
|
|
|
342
|
|
|
343
|
30
|
@io_image_decorate
|
344
|
6
|
def save_image(path_image, image):
|
345
|
|
""" save the image into given path
|
346
|
|
|
347
|
|
:param str path_image: path to the image
|
348
|
|
:param image: np.array<height, width, ch>
|
349
|
|
|
350
|
|
>>> # Wrong path
|
351
|
|
>>> save_image('./missing-path/any-image.png', np.zeros((10, 20)))
|
352
|
|
False
|
353
|
|
"""
|
354
|
30
|
path_dir = os.path.dirname(path_image)
|
355
|
30
|
if not os.path.isdir(path_dir):
|
356
|
30
|
logging.error('upper folder does not exists: "%s"', path_dir)
|
357
|
30
|
return False
|
358
|
30
|
image = convert_ndarray2image(image)
|
359
|
30
|
image.save(path_image)
|
360
|
|
|
361
|
|
|
362
|
30
|
@io_image_decorate
|
363
|
30
|
def convert_image_to_nifti(path_image, path_out_dir=None):
|
364
|
|
""" converting normal image to Nifty Image
|
365
|
|
|
366
|
|
:param str path_image: input image
|
367
|
|
:param str path_out_dir: path to output folder
|
368
|
|
:return str: resulted image
|
369
|
|
|
370
|
|
>>> path_img = os.path.join(update_path('data-images'), 'images',
|
371
|
|
... 'artificial_moving-affine.jpg')
|
372
|
|
>>> path_img2 = convert_image_to_nifti(path_img, '.')
|
373
|
|
>>> path_img2 # doctest: +ELLIPSIS
|
374
|
|
'...artificial_moving-affine.nii'
|
375
|
|
>>> os.path.isfile(path_img2)
|
376
|
|
True
|
377
|
|
>>> path_img3 = convert_image_from_nifti(path_img2)
|
378
|
|
>>> os.path.isfile(path_img3)
|
379
|
|
True
|
380
|
|
>>> list(map(os.remove, [path_img2, path_img3])) # doctest: +ELLIPSIS
|
381
|
|
[...]
|
382
|
|
"""
|
383
|
30
|
path_image = update_path(path_image)
|
384
|
30
|
path_img_out = _gene_out_path(path_image, '.nii', path_out_dir)
|
385
|
30
|
logging.debug('Convert image to Nifti format "%s" -> "%s"', path_image, path_img_out)
|
386
|
|
|
387
|
|
# img = Image.open(path_file).convert('LA')
|
388
|
30
|
img = load_image(path_image)
|
389
|
30
|
nim = nibabel.Nifti1Pair(img, np.eye(4))
|
390
|
30
|
del img
|
391
|
30
|
nibabel.save(nim, path_img_out)
|
392
|
|
|
393
|
30
|
return path_img_out
|
394
|
|
|
395
|
|
|
396
|
30
|
@io_image_decorate
|
397
|
30
|
def convert_image_to_nifti_gray(path_image, path_out_dir=None):
|
398
|
|
""" converting normal image to Nifty Image
|
399
|
|
|
400
|
|
:param str path_image: input image
|
401
|
|
:param str path_out_dir: path to output folder
|
402
|
|
:return str: resulted image
|
403
|
|
|
404
|
|
>>> path_img = './sample-image.png'
|
405
|
|
>>> save_image(path_img, np.zeros((100, 200, 3)))
|
406
|
|
>>> path_img2 = convert_image_to_nifti_gray(path_img)
|
407
|
|
>>> os.path.isfile(path_img2)
|
408
|
|
True
|
409
|
|
>>> path_img3 = convert_image_from_nifti(path_img2, '.')
|
410
|
|
>>> os.path.isfile(path_img3)
|
411
|
|
True
|
412
|
|
>>> list(map(os.remove, [path_img, path_img2, path_img3])) # doctest: +ELLIPSIS
|
413
|
|
[...]
|
414
|
|
"""
|
415
|
30
|
path_image = update_path(path_image)
|
416
|
30
|
path_img_out = _gene_out_path(path_image, '.nii', path_out_dir)
|
417
|
30
|
logging.debug('Convert image to Nifti format "%s" -> "%s"', path_image, path_img_out)
|
418
|
|
|
419
|
|
# img = Image.open(path_file).convert('LA')
|
420
|
30
|
img = rgb2gray(load_image(path_image))
|
421
|
30
|
nim = nibabel.Nifti1Pair(np.swapaxes(img, 1, 0), np.eye(4))
|
422
|
30
|
del img
|
423
|
30
|
nibabel.save(nim, path_img_out)
|
424
|
|
|
425
|
30
|
return path_img_out
|
426
|
|
|
427
|
|
|
428
|
30
|
def _gene_out_path(path_file, file_ext, path_out_dir=None):
|
429
|
|
""" generate new path with the same file name just changed extension (and folder)
|
430
|
|
|
431
|
|
:param str path_file: path of source file (image)
|
432
|
|
:param str file_ext: file extension of desired file
|
433
|
|
:param str path_out_dir: destination folder, if NOne use the input dir
|
434
|
|
:return str: desired file
|
435
|
|
"""
|
436
|
30
|
if not path_out_dir:
|
437
|
30
|
path_out_dir = os.path.dirname(path_file)
|
438
|
30
|
img_name, _ = os.path.splitext(os.path.basename(path_file))
|
439
|
30
|
path_out = os.path.join(path_out_dir, img_name + file_ext)
|
440
|
30
|
return path_out
|
441
|
|
|
442
|
|
|
443
|
30
|
@io_image_decorate
|
444
|
30
|
def convert_image_from_nifti(path_image, path_out_dir=None):
|
445
|
|
""" converting Nifti to standard image
|
446
|
|
|
447
|
|
:param str path_image: path to input image
|
448
|
|
:param str path_out_dir: destination directory, if Nne use the same folder
|
449
|
|
:return str: path to new image
|
450
|
|
"""
|
451
|
30
|
path_image = update_path(path_image)
|
452
|
30
|
path_img_out = _gene_out_path(path_image, '.jpg', path_out_dir)
|
453
|
30
|
logging.debug('Convert Nifti to image format "%s" -> "%s"', path_image, path_img_out)
|
454
|
30
|
nim = nibabel.load(path_image)
|
455
|
|
|
456
|
30
|
if len(nim.get_data().shape) > 2: # colour
|
457
|
30
|
img = nim.get_data()
|
458
|
|
else: # gray
|
459
|
30
|
img = np.swapaxes(nim.get_data(), 1, 0)
|
460
|
|
|
461
|
30
|
if img.max() > 1.5:
|
462
|
0
|
img = img / 255.
|
463
|
|
|
464
|
30
|
save_image(path_img_out, img)
|
465
|
30
|
return path_img_out
|
466
|
|
|
467
|
|
|
468
|
30
|
def image_resize(img, scale=1., v_range=255, dtype=int):
|
469
|
|
""" rescale image with other optional formating
|
470
|
|
|
471
|
|
:param ndarray img: input image
|
472
|
|
:param float scale: the new image size is im_size * scale
|
473
|
|
:param int|float v_range: desired output image range 1. or 255
|
474
|
|
:param dtype: output image type
|
475
|
|
:return ndarray: image
|
476
|
|
|
477
|
|
>>> np.random.seed(0)
|
478
|
|
>>> img = image_resize(np.random.random((250, 300, 3)), scale=2, v_range=255)
|
479
|
|
>>> np.array(img.shape, dtype=int)
|
480
|
|
array([500, 600, 3])
|
481
|
|
>>> img.max()
|
482
|
|
255
|
483
|
|
"""
|
484
|
30
|
if scale is None or scale == 1:
|
485
|
0
|
return img
|
486
|
|
# scale the image accordingly
|
487
|
30
|
interp = cv.INTER_CUBIC if scale > 1 else cv.INTER_LINEAR
|
488
|
30
|
img = cv.resize(img, None, fx=scale, fy=scale, interpolation=interp)
|
489
|
|
|
490
|
30
|
v_range = 255 if v_range > 1.5 else 1.
|
491
|
|
# if resulting image is in range to 1 and desired is in 255
|
492
|
30
|
if np.max(img) < 1.5 < v_range:
|
493
|
30
|
np.multiply(img, 255, out=img)
|
494
|
30
|
np.round(img, out=img)
|
495
|
|
|
496
|
|
# convert image datatype
|
497
|
30
|
if dtype is not None:
|
498
|
30
|
img = img.astype(dtype)
|
499
|
|
|
500
|
|
# clip image values in certain range
|
501
|
30
|
np.clip(img, a_min=0, a_max=v_range, out=img)
|
502
|
30
|
return img
|
503
|
|
|
504
|
|
|
505
|
30
|
@io_image_decorate
|
506
|
30
|
def convert_image_from_mhd(path_image, path_out_dir=None, img_ext='.png', scaling=None):
|
507
|
|
""" convert standard image to MHD format
|
508
|
|
|
509
|
|
.. seealso:: https://www.programcreek.com/python/example/96382/SimpleITK.WriteImage
|
510
|
|
|
511
|
|
:param str path_image: path to the input image
|
512
|
|
:param str path_out_dir: path to output directory, if None use the input dir
|
513
|
|
:param str img_ext: image extension like PNG or JPEG
|
514
|
|
:param float|None scaling: image down-scaling,
|
515
|
|
resulting image will be larger by this factor
|
516
|
|
:return str: path to exported image
|
517
|
|
|
518
|
|
>>> path_img = os.path.join(update_path('data-images'), 'images',
|
519
|
|
... 'artificial_reference.jpg')
|
520
|
|
>>> path_img = convert_image_to_mhd(path_img, scaling=1.5)
|
521
|
|
>>> convert_image_from_mhd(path_img, scaling=1.5) # doctest: +ELLIPSIS
|
522
|
|
'...artificial_reference.png'
|
523
|
|
"""
|
524
|
30
|
path_image = update_path(path_image)
|
525
|
30
|
assert os.path.isfile(path_image), 'missing image: %s' % path_image
|
526
|
|
# Reads the image using SimpleITK
|
527
|
30
|
itk_image = sitk.ReadImage(path_image)
|
528
|
|
|
529
|
|
# Convert the image to a numpy array first and then shuffle the dimensions
|
530
|
|
# to get axis in the order z,y,x
|
531
|
30
|
img = sitk.GetArrayFromImage(itk_image)
|
532
|
|
# Scaling image if requested
|
533
|
30
|
img = image_resize(img, scaling, v_range=255)
|
534
|
|
|
535
|
|
# define output/destination path
|
536
|
30
|
path_image = _gene_out_path(path_image, img_ext, path_out_dir)
|
537
|
30
|
save_image(path_image, img)
|
538
|
30
|
return path_image
|
539
|
|
|
540
|
|
|
541
|
30
|
@io_image_decorate
|
542
|
30
|
def convert_image_to_mhd(path_image, path_out_dir=None, to_gray=True, overwrite=True, scaling=None):
|
543
|
|
""" converting standard image to MHD (Nifty format)
|
544
|
|
|
545
|
|
.. seealso:: https://stackoverflow.com/questions/37290631
|
546
|
|
|
547
|
|
:param str path_image: path to the input image
|
548
|
|
:param str path_out_dir: path to output directory, if None use the input dir
|
549
|
|
:param bool overwrite: allow overwrite existing image
|
550
|
|
:param float|None scaling: image up-scaling
|
551
|
|
resulting image will be smaller by this factor
|
552
|
|
:return str: path to exported image
|
553
|
|
|
554
|
|
>>> path_img = os.path.join(update_path('data-images'), 'images',
|
555
|
|
... 'artificial_moving-affine.jpg')
|
556
|
|
>>> convert_image_to_mhd(path_img, scaling=2) # doctest: +ELLIPSIS
|
557
|
|
'...artificial_moving-affine.mhd'
|
558
|
|
"""
|
559
|
30
|
path_image = update_path(path_image)
|
560
|
30
|
path_image_new = _gene_out_path(path_image, '.mhd', path_out_dir)
|
561
|
|
# in case the image exists and you are not allowed to overwrite it
|
562
|
30
|
if os.path.isfile(path_image_new) and not overwrite:
|
563
|
0
|
logging.debug('skip converting since the image exists and no-overwrite: %s', path_image_new)
|
564
|
0
|
return path_image_new
|
565
|
|
|
566
|
30
|
img = load_image(path_image)
|
567
|
|
# if required and RGB on input convert to gray-scale
|
568
|
30
|
if to_gray and img.ndim == 3 and img.shape[2] in (3, 4):
|
569
|
30
|
img = rgb2gray(img)
|
570
|
|
# Scaling image if requested
|
571
|
30
|
scaling = 1. / scaling
|
572
|
|
# the MHD usually require pixel value range (0, 255)
|
573
|
30
|
img = image_resize(img, scaling, v_range=255)
|
574
|
|
|
575
|
30
|
logging.debug('exporting image of size: %r', img.shape)
|
576
|
30
|
image = sitk.GetImageFromArray(img.astype(np.uint8), isVector=False)
|
577
|
|
|
578
|
|
# do not use text in MHD, othwerwise it crash DROP method
|
579
|
30
|
sitk.WriteImage(image, path_image_new, False)
|
580
|
30
|
return path_image_new
|
581
|
|
|
582
|
|
|
583
|
30
|
def load_config_args(path_config, comment='#'):
|
584
|
|
"""load config arguments from file with dropping comments
|
585
|
|
|
586
|
|
:param str path_config: configuration file
|
587
|
|
:param str comment: character defining comments
|
588
|
|
:return str: concat arguments
|
589
|
|
|
590
|
|
>>> p_conf = './sample-arg-config.txt'
|
591
|
|
>>> with open(p_conf, 'w') as fp:
|
592
|
|
... fp.writelines(os.linesep.join(['# comment', '', ' -a 1 ', ' --b c#d']))
|
593
|
|
>>> load_config_args(p_conf)
|
594
|
|
'-a 1 --b c'
|
595
|
|
>>> os.remove(p_conf)
|
596
|
|
"""
|
597
|
30
|
assert os.path.isfile(path_config), 'missing file: %s' % path_config
|
598
|
30
|
lines = []
|
599
|
30
|
with open(path_config, 'r') as fp:
|
600
|
30
|
for ln in fp.readlines():
|
601
|
|
# drop comments
|
602
|
30
|
if comment in ln:
|
603
|
30
|
ln = ln[:ln.index(comment)]
|
604
|
|
# remove spaces from beinning and end
|
605
|
30
|
ln = ln.strip()
|
606
|
|
# skip empty lines
|
607
|
30
|
if ln:
|
608
|
30
|
lines.append(ln)
|
609
|
30
|
config = ' '.join(lines)
|
610
|
30
|
return config
|
611
|
|
|
612
|
|
|
613
|
30
|
def load_config_yaml(path_config):
|
614
|
|
""" loading the
|
615
|
|
|
616
|
|
:param str path_config:
|
617
|
|
:return dict:
|
618
|
|
|
619
|
|
>>> p_conf = './testing-congif.yaml'
|
620
|
|
>>> save_config_yaml(p_conf, {'a': 2})
|
621
|
|
>>> load_config_yaml(p_conf)
|
622
|
|
{'a': 2}
|
623
|
|
>>> os.remove(p_conf)
|
624
|
|
"""
|
625
|
30
|
with open(path_config, 'r') as fp:
|
626
|
30
|
config = yaml.safe_load(fp)
|
627
|
30
|
return config
|
628
|
|
|
629
|
|
|
630
|
30
|
def save_config_yaml(path_config, config):
|
631
|
|
""" exporting configuration as YAML file
|
632
|
|
|
633
|
|
:param str path_config:
|
634
|
|
:param dict config:
|
635
|
|
"""
|
636
|
30
|
with open(path_config, 'w') as fp:
|
637
|
30
|
yaml.dump(config, fp, default_flow_style=False)
|