ialdev-io 0.1.0__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.
iad/io/ciif.py ADDED
@@ -0,0 +1,852 @@
1
+ """ Operate with ciif and tif file formats."""
2
+
3
+ import math
4
+ import re
5
+ import warnings
6
+ from ast import literal_eval
7
+ from functools import reduce
8
+ from os import path, makedirs
9
+
10
+ import numpy as np
11
+ import skimage.io as io
12
+
13
+ from iad.core.short import unless
14
+ from iad.core.logs import logger
15
+ from iad.core.events import TimePoints
16
+
17
+ SCHEME_DEF_LOC = r'\\bkp01\tmp\Algo\test_point\test_points_scheme.xlsx'
18
+
19
+ _TM = TimePoints(True)
20
+ _log = logger('io')
21
+
22
+
23
+ def load_scheme(info_file=SCHEME_DEF_LOC, sheet_name=None) -> dict:
24
+ """ Load test points info excel file to the dictionary DB
25
+
26
+ :return dict: keys - test points file names (no extension)
27
+ values: dict with keys: ('signals', 'io', 'groups')
28
+ """
29
+ import pandas as pd
30
+
31
+ def parse_struct(structure):
32
+ struc = structure.strip().split('.')
33
+ comp = struc[0] if struc else None # dpe, ppr, …
34
+ block = struc[1] if struc and (len(struc) > 1) else None # agg, unite, opt
35
+ view = struc[2] if struc and (len(struc) > 2) and (struc[2] in ['left', 'right']) else None # # left|right
36
+ tp_name = '.'.join([w for w in struc if w not in [comp, block, view]])
37
+ return comp, block, view, tp_name
38
+
39
+ def parse_bits(str_format, total_length):
40
+ bits = [int(s) for s in re.split('[^\d]+', str_format.strip())]
41
+ bits_in_group = sum(bits)
42
+
43
+ unless(total_length % bits_in_group == 0,
44
+ '%d signals in ciif file %s can not be grouped by %d' % (total_length, i, bits_in_group))
45
+ bit_groups_num = total_length//bits_in_group
46
+ return bits * bit_groups_num
47
+
48
+ def index_groups(names, num, sep='_'):
49
+ """ Given names in a group generate for multiple groups """
50
+ if num == 1:
51
+ return names
52
+ return reduce(list.__add__, ([f + sep + str(gid) for f in names] for gid in range(num)))
53
+
54
+ _log.debug('reading from: %s' % path.abspath(info_file))
55
+ dfs = pd.read_excel(info_file, sheet_name=sheet_name, converters={'format': str})
56
+ columns = ['structure', 'comments', 'name', 'alias', 'signals', 'length', 'format'] # the columns we will need
57
+
58
+ df = pd.concat(dv[columns].dropna(axis='index', how='any', subset=['structure', 'name', 'signals', 'length', 'format'])
59
+ for dv in dfs.values()
60
+ if all(c in dv.columns for c in columns)) # type: pd.DataFrame
61
+
62
+ df.set_index('name', inplace=True)
63
+ # file_name: name
64
+ df.rename_axis('file_name', inplace=True)
65
+
66
+ df = df.reindex(columns=columns + ['groups', 'component', 'block', 'view', 'tp_name'])
67
+ df['groups'] = np.zeros((len(df), 1)) # number of groups may be useful
68
+
69
+ for i, r in df.iterrows():
70
+ try:
71
+ bits = parse_bits(r.format, int(r.length))
72
+
73
+ signals = re.split('[^\w]+', r.signals.strip())
74
+ sig_in_group = len(signals)
75
+ unless(len(bits) % sig_in_group == 0,
76
+ '%s signals in ciif file %s can not be grouped by %s' % (r.signals.strip(), i, r.format.strip()))
77
+ sig_groups_num = len(bits)//sig_in_group
78
+ unless(all(bits[k*sig_in_group:(k+1)*sig_in_group] == bits[0:sig_in_group] for k in range(sig_groups_num)),
79
+ 'format in ciif file %s is not a repeating pattern of size %d' % (i, sig_in_group))
80
+ signals = index_groups(signals, sig_groups_num)
81
+
82
+ comp, block, view, tp_name = parse_struct(r.structure) if r.structure else None, None, None, None
83
+ df.loc[i, ['signals', 'format', 'groups', 'component', 'block', 'view', 'tp_name']] = \
84
+ signals, bits, sig_groups_num, comp, block, view, tp_name
85
+ except Exception as e:
86
+ raise Exception('TP %s descr problem in file %s: %s' % (i, info_file, e))
87
+
88
+ return df.to_dict('index')
89
+
90
+
91
+ def load_signals(ciif_file, tp_scheme=SCHEME_DEF_LOC, out_df=False, show=False):
92
+ """Load content of ciif files to dictionary according to the tp scheme.
93
+ High level wrapper on load_ciif.
94
+
95
+ :param ciif_file: test points - the source of the signals
96
+ :param tp_scheme: as excel file or as dictionary
97
+ :param out_df: if True - return data as pandas DataFrame
98
+ :param show: show additional processing information
99
+ :return: dict of {field_name: image_ndarray} or additional DataFrame if requested
100
+ """
101
+ tps_db = load_scheme(info_file=tp_scheme) if isinstance(tp_scheme, str) else tp_scheme
102
+
103
+ m = re.match('(?P<name>\w+)_(0|1)_00.ciif', path.basename(ciif_file)).groupdict()
104
+
105
+ # name = '_'.join(file_name(ciif_file).split('_')[:-2])
106
+ return load_ciif(ciif_file, data_fields=tps_db[m['name']]['signals'], out='df' if out_df else dict,
107
+ show=(['summary'] if show else []))
108
+
109
+
110
+ def images_to_ciif(file_path: str, images,
111
+ bits_num=None,
112
+ start_bits=None,
113
+ stream_names=None,
114
+ lane_shape=None,
115
+ stream_sign=None,
116
+ bits_write=None,
117
+ new_hdr=True):
118
+ """Save array of same-sized images to ciif file io.
119
+
120
+ :Hdr io:
121
+
122
+ //Frame_width=(200, 0)
123
+ //Frame_height=(26, 0)
124
+ //Stream_name=(y10)
125
+ //Stream_bit_num=(10)
126
+ //Stream_sign=(unsigned)
127
+
128
+ :Example:
129
+
130
+ images_to_ciif('resulting.ciif', img) # simplest form - use first max_bit(img) bits
131
+ images_to_ciif('resulting.ciif', [img1, img2], [2, 13]) # usual form - 2 and 13 bits data
132
+ images_to_ciif('resulting.ciif', [img, img], [6, 8], [0, 6]) # extract fields from same img
133
+
134
+ :param file_path: Distention file path
135
+ :param images: iterable of images of numpy.ndarray type (or a single image)
136
+ :param bits_num: numbers of meaningful bits in the data elements (default: estimate from data)
137
+ :param start_bits: list of (0-based) starting bits used in each image data element (default = 0)
138
+ :param stream_names: names for the data elements (by default will be set: 'f0', 'f1' ...)
139
+ :param lane_shape: shape of lane (default: 0,0)
140
+ :param stream_sign: types of the data elements (by default will be set: 'unsigned')
141
+ :param bits_write: numbers of stream bits in output ciif (default: from bits_num)
142
+ :param new_hdr: flag for header io; True by default)
143
+ :param pix_format: used if new_hdr==False; USER_DEFINED by default)
144
+ """
145
+
146
+ def data_bits(d):
147
+ return np.ceil(np.log(1 + min(1, d.max() + 1)) / np.log(2))
148
+
149
+ # normalize the inputs io
150
+ if isinstance(images, np.ndarray):
151
+ images = [images]
152
+
153
+ if isinstance(stream_names, str):
154
+ stream_names = [stream_names]
155
+
156
+ if not len(images):
157
+ warnings.warn('No images provided - exiting!')
158
+ return
159
+
160
+ if start_bits is None:
161
+ start_bits = [0] * len(images)
162
+ if not type(start_bits) in (tuple, list):
163
+ start_bits = [start_bits] * len(images)
164
+ if isinstance(start_bits[0], str):
165
+ start_bits = [int(bits) for bits in start_bits]
166
+
167
+ if stream_names is None:
168
+ if isinstance(images[0], dict):
169
+ stream_names = images.keys()
170
+ else:
171
+ stream_names = ['f%d' % i for i in range(len(images))]
172
+
173
+ if bits_num is None:
174
+ bits_num = [data_bits(im) for im in images]
175
+ if not type(bits_num) in (tuple, list):
176
+ bits_num = [bits_num] * len(images)
177
+ if isinstance(bits_num[0], str):
178
+ bits_num = [int(bits) for bits in bits_num]
179
+
180
+ if bits_write is None:
181
+ bits_write = bits_num
182
+ if not type(bits_write) in (tuple, list):
183
+ bits_write = [bits_write] * len(images)
184
+ if isinstance(bits_write[0], str):
185
+ bits_write = [int(bits) for bits in bits_write]
186
+
187
+ if stream_sign is None:
188
+ stream_sign = ['unsigned' for im in images]
189
+ elif not type(stream_sign) in (tuple, list):
190
+ stream_sign = [stream_sign] * len(images)
191
+
192
+ if lane_shape is None:
193
+ lane_shape = [0, 0]
194
+
195
+ # validate inputs
196
+ unless(len(images) == len(start_bits) == len(bits_num) == len(stream_sign) == len(bits_write),
197
+ 'mismatched arrays of images and bits') # It shouldt work
198
+ for im, start, bits, bits_w, signed in zip(images, start_bits, bits_num, bits_write, stream_sign):
199
+ unless(im.dtype.kind in 'bBui', 'Only integer data types are supported')
200
+ if signed != 'unsigned': # Consider: add support
201
+ unless(bits <= 32, 'Negative data supported only up to 32 bits')
202
+ unless(start >= 0 and bits != 0 and bits_w != 0 and bits <= bits_w,
203
+ 'invalid start_bits, bits_num, bits_write: %s, %s, %s' % (start, bits, bits_w))
204
+
205
+ images = [im.astype('uint32') if sgn == 'signed' else im for sgn, im in zip(stream_sign, images)]
206
+
207
+ unless(all(len(im.shape) == 2 for im in images), 'images must be greyscaled')
208
+ height, width = next(iter(images)).shape
209
+ unless(all(im.shape == (height, width) for im in images), 'images must be of same size')
210
+
211
+ # define formatting
212
+ if new_hdr:
213
+ ciif_header = ("Frame_width=({width}, {lane_shape_w})\n"
214
+ "Frame_height=({height}, {lane_shape_h})\n"
215
+ "Stream_name=({stream_names})\n"
216
+ "Stream_bit_num=({bits_o})\n"
217
+ "Stream_sign=({signed})"
218
+ ).format(width=width, lane_shape_w=lane_shape[0],
219
+ height=height, lane_shape_h=lane_shape[1],
220
+ stream_names=', '.join([name for name in stream_names]),
221
+ bits_o=', '.join(['%u' % b_num for b_num in bits_write]),
222
+ signed=', '.join([sign for sign in stream_sign]))
223
+ else:
224
+ ciif_header = ("Frame_width={width}\n"
225
+ "Frame_height={height}\n"
226
+ "Number_disparities=1\n"
227
+ "Number_frames=1\n"
228
+ "Pixel_format=({bits_o})\n" # USER_SPECIFIC
229
+ "Line_direction=LeftToRight\n"
230
+ "Data_format=Hex"
231
+ ).format(width=width, height=height,
232
+ bits_o=', '.join(['%u' % b_num for b_num in bits_write]),)
233
+
234
+ # line_format = "( 0,%4u,%4u) " + "_".join("%%0%ux" % np.ceil(bits / 4) for bits in bits_num)
235
+ if len(bits_write) > 1:
236
+ line_format = "( 0,%4u,%4u) " + "_".join(
237
+ "%%0%lux" % np.ceil(abs(bits) / 4) for bits in bits_write) # abs(bits) for signed numbers support
238
+ else:
239
+ line_format = "( 0,%4u,%4u) " + "%%0%lux" % np.ceil(abs(bits_write[0]) / 4)
240
+
241
+ # prepare data for printing out
242
+ masked_images = (((im.flatten() >> start) & int(2 ** abs(bits) - 1)) for # abs(bits) for signed numbers support
243
+ im, start, bits in zip(images, start_bits, bits_num))
244
+
245
+ data = np.stack((*np.where(np.ones((height, width))), *masked_images))
246
+ data = data.astype('uint64') if np.sum(bits_write) > 32 else data.astype('uint32')
247
+ np.savetxt(file_path, data.T, fmt=line_format, header=ciif_header, comments='//')
248
+
249
+
250
+ def _ciif_hdr_info(ciif_path) -> dict:
251
+ """ Extract header info as dict; count number of header lines
252
+
253
+ :param ciif_path: file_path to ciif file
254
+ :return: hdr_attr_dict
255
+ """
256
+ import pandas as pd
257
+
258
+ def extract_stream_name(header_dict):
259
+ """ Extract stream names from from ciif hdr dict
260
+
261
+ :param header_dict: dict read from ciif hdr dict
262
+
263
+ :return: stream_names list
264
+ """
265
+
266
+ names_str = header_dict.get('Stream_name', None)
267
+ if names_str and isinstance(names_str, str): # new hdr io
268
+ names = re.split(',', names_str.strip('()'))
269
+ return [field.strip() for field in names]
270
+ return None
271
+
272
+ def extract_shape(header_dict):
273
+ """ Extract Frame_height and Frame_width from ciif hdr dict
274
+
275
+ :param header_dict: dict read from ciif hdr
276
+ :return: (Frame_height, Frame_width) values tuple
277
+ """
278
+
279
+ dim_fields = ['Frame_height', 'Frame_width']
280
+ the_shape = None
281
+ if set(dim_fields).issubset(header_dict):
282
+ h = literal_eval(header_dict['Frame_height'])
283
+ w = literal_eval(header_dict['Frame_width'])
284
+
285
+ if isinstance(h, tuple): # new hdr io
286
+ the_shape = (h[0], w[0])
287
+ else: # old hdr io
288
+ the_shape = (h, w)
289
+ return the_shape
290
+
291
+ def extract_stream_bit_num(header_dict):
292
+ """ Extract Stream_bit_num from ciif hdr dict
293
+
294
+ :param header_dict: dict read from ciif hdr
295
+ :return: Stream_bit_num values list
296
+ """
297
+ if 'Stream_bit_num' not in header_dict:
298
+ return None
299
+
300
+ str_bits = re.split(',', header_dict.get('Stream_bit_num').strip('()'))
301
+ return [int(str_bits_num) for str_bits_num in str_bits]
302
+
303
+ def extract_stream_sign(header_dict):
304
+ """ Extract Stream_sign from ciif hdr dict
305
+
306
+ :param header_dict: dict read from ciif hdr
307
+ :return: Stream_sign str list
308
+ """
309
+ if 'Stream_sign' not in header_dict:
310
+ return None
311
+ signed = re.split(',', header_dict.get('Stream_sign').strip('()'))
312
+ return [s.strip(' ') for s in signed]
313
+
314
+ def extract_dtypes(header_dict):
315
+ """ Extract Stream_sign from ciif hdr dict
316
+
317
+ :param header_dict: dict read from ciif hdr
318
+ :return: Stream_sign str list
319
+ """
320
+
321
+ def hdr_str_to_dtype(str_signed, str_bits_num):
322
+ num_of_bits = int(str_bits_num)
323
+ num_of_bits = 2 ** math.ceil(math.log2(num_of_bits))
324
+ num_of_bits = max(num_of_bits, 8)
325
+ if str_signed.strip() == 'unsigned':
326
+ signed_int = 'uint'
327
+ else:
328
+ signed_int = 'int'
329
+ return eval('np.{}{}'.format(signed_int, num_of_bits))
330
+
331
+ stream_signed = extract_stream_sign(header_dict)
332
+ bits_num = extract_stream_bit_num(header_dict)
333
+ dtypes = None
334
+ if stream_signed and bits_num:
335
+ dtypes = []
336
+ for sign, bits in zip(stream_signed, bits_num):
337
+ dtype = hdr_str_to_dtype(sign, bits)
338
+ dtypes.append(dtype)
339
+ return dtypes
340
+
341
+ try:
342
+ header = pd.read_table(ciif_path, sep='=|//', engine='python', nrows=50,
343
+ usecols=range(1, 3), names=['', ' ', 'val'], index_col=0).dropna()
344
+ except BaseException as e:
345
+ # print('missing header in "%s"' % ciif_path)
346
+ return {}
347
+ else:
348
+ hdr_dict = header['val'].to_dict()
349
+ shape_info = extract_shape(hdr_dict)
350
+
351
+ return {'Frame_height': shape_info[0], 'Frame_width': shape_info[1],
352
+ 'Stream_name': extract_stream_name(hdr_dict),
353
+ 'Stream_bit_num': extract_stream_bit_num(hdr_dict),
354
+ 'Stream_sign': extract_stream_sign(hdr_dict),
355
+ 'dtypes': extract_dtypes(hdr_dict),
356
+ 'hdr_rows': header.shape[0]}
357
+
358
+
359
+ def _ciif_data_info(ciif_path) -> dict:
360
+ """ Extract header info as dict; count number of header lines
361
+
362
+ :param ciif_path: file_path to ciif file
363
+ :return: attr_dict extracted from data
364
+ """
365
+
366
+ import pandas as pd
367
+
368
+ def default_field_names(df_readdata):
369
+ num_found_fields = df_readdata.shape[1] - len(('frame', 'row', 'col'))
370
+ return ['f%d' % i for i in range(num_found_fields)]
371
+
372
+ hdr_info = _ciif_hdr_info(ciif_path)
373
+ hdr_rows = hdr_info.get('hdr_rows', 0) if hdr_info else 0
374
+
375
+ df = pd.read_table(ciif_path, sep=r',\s*|\(\s*|\)\s*|_', engine='python', header=None,
376
+ skiprows=hdr_rows, usecols=range(1, 10000), index_col=False)
377
+ data_fields = default_field_names(df)
378
+
379
+ position_fields = ('frame', 'row', 'col')
380
+ df.columns = position_fields + tuple(data_fields)
381
+ data_shape = tuple(np.array(df[['row', 'col']].apply(np.max)) + 1)
382
+
383
+ return {'Stream_name': default_field_names(df),
384
+ 'Frame_height': data_shape[0],
385
+ 'Frame_width': data_shape[1],
386
+ 'hdr_rows': hdr_rows}
387
+
388
+
389
+ def extract_metadata(file_name) -> dict:
390
+ """ Extract CIIF file info from ciif_hdr or from ciif_data.
391
+
392
+ :param file_name: name of the ciif file.
393
+ :return: dict with ciif info.
394
+ """
395
+
396
+ all_keys = ['Frame_width', 'Frame_height', 'Stream_name', 'Stream_bit_num', 'Stream_sign', 'dtypes', 'hdr_rows']
397
+
398
+ res_dict = _ciif_hdr_info(file_name)
399
+ default_keys = [k for k in all_keys if res_dict.get(k, None) is None]
400
+
401
+ if len(default_keys) > 0:
402
+ res_dict.update({'default': True})
403
+ data_dict = _ciif_data_info(file_name)
404
+ for key in default_keys:
405
+ res_dict.update({key: data_dict.get(key, None)})
406
+ return res_dict
407
+
408
+
409
+ def load_ciif(file_name, data_fields=None, out=dict, partial=False, show=None):
410
+ """ Load CIIF file with multiple fields and extract them into dictionary of corresponding images
411
+
412
+ :param file_name: name of the ciif file
413
+ :param data_fields: list of names of the fields - defaults=['f0', 'f1', ...]
414
+ :param out: output io [dict] | tuple | list | df (DataFrame) | 'content';
415
+ IF out=’content’, ignore other flags and return instead of the data:
416
+ - if new header io: list of stream names
417
+ - if old header io: number of streams.
418
+ :param partial: if True, falls on not complete data
419
+ :param show: list of strings to control verbosity: ['summary', ]
420
+ :return: dict of {field_name: image_ndarray}
421
+ """
422
+
423
+ import pandas as pd
424
+
425
+ def device_data_fields(param_field_names, default_field_names):
426
+ """
427
+ Device data field names, using stream names read from new hdr, field names from call parameters, default field names.
428
+
429
+ :param param_field_names: field names from call parameter;
430
+ :param default_field_names:
431
+
432
+ :return: Data field names to be used in result df or dict.
433
+ """
434
+ field_names = param_field_names or []
435
+ if not all(re.fullmatch('\w+', field) for field in field_names):
436
+ raise ValueError('An invalid data field in: %s' % field_names)
437
+
438
+ if param_field_names and (len(param_field_names) != 0) and (len(default_field_names) != len(param_field_names)):
439
+ print('Found %d fields instead of %d. Using defaults.' % (len(default_field_names), len(param_field_names)))
440
+ return default_field_names
441
+
442
+ return field_names or default_field_names
443
+
444
+ if not out:
445
+ out = dict
446
+
447
+ _TM.point(file_name)
448
+ meta_dict = extract_metadata(file_name)
449
+
450
+ # -------------- Format output ---------------
451
+ field_names = meta_dict.get('Stream_name', None)
452
+ if out == 'content':
453
+ return len(field_names) if meta_dict.get('default', False) else field_names
454
+
455
+ if out in {pd.DataFrame, 'df'}:
456
+ out = pd.DataFrame
457
+ elif out not in {dict, tuple, list}:
458
+ raise ValueError('Unsupported output io: ', out)
459
+
460
+ # -------------- data_fields ---------------
461
+ if meta_dict.get('default', False):
462
+ data_fields = device_data_fields(param_field_names=data_fields, default_field_names=field_names)
463
+ else:
464
+ data_fields = field_names
465
+
466
+ # -------------- read data ---------------
467
+ df = pd.read_table(file_name, sep=r',\s*|\(\s*|\)\s*|_', engine='python', header=None,
468
+ skiprows=meta_dict.get('hdr_rows', 0), usecols=range(1, 10000), index_col=False)
469
+ position_fields = ('frame', 'row', 'col')
470
+ df.columns = position_fields + tuple(data_fields)
471
+
472
+ # -------------- data_shape: check consistency with header ---------------
473
+ data_shape = tuple(np.array(df[['row', 'col']].apply(np.max)) + 1)
474
+ hdr_shape = (meta_dict.get('Frame_height', None), meta_dict.get('Frame_width', None))
475
+ if not data_shape == hdr_shape:
476
+ msg = 'Mismatch ({})!=({}) in {}'.format(data_shape, hdr_shape, file_name)
477
+ if partial:
478
+ warnings.warn(msg)
479
+ else:
480
+ raise ValueError(msg)
481
+ # update actual number of rows:
482
+ data_shape = int(df.shape[0] / data_shape[1]), data_shape[1]
483
+ df = df[: (data_shape[0] * data_shape[1])]
484
+
485
+ # -------------- dtypes ---------------
486
+ dtypes = meta_dict.get('dtypes', [])
487
+ stream_signed = meta_dict.get('Stream_sign', None)
488
+ bits_num = meta_dict.get('Stream_bit_num', None)
489
+
490
+ def hdr_str_to_dtype(str_signed, num_of_bits):
491
+ num_of_bits = 2 ** math.ceil(math.log2(num_of_bits))
492
+ num_of_bits = max(num_of_bits, 8)
493
+ if str_signed.strip() == 'unsigned':
494
+ signed_int = 'uint'
495
+ else:
496
+ signed_int = 'int'
497
+ return eval('np.{}{}'.format(signed_int, num_of_bits))
498
+
499
+ if stream_signed and bits_num:
500
+ for sign, bits in zip(stream_signed, bits_num):
501
+ dtype = hdr_str_to_dtype(sign, bits)
502
+ dtypes.append(dtype)
503
+
504
+ # _TM.point('convert hex')
505
+ # convert data to numeric
506
+ def str_to_dtype(str):
507
+ num_of_bits = len(str) * 4
508
+ num_of_bits = 2 ** math.ceil(math.log2(num_of_bits))
509
+ num_of_bits = max(num_of_bits, 8)
510
+ return eval('np.uint{}'.format(num_of_bits))
511
+
512
+ for field in data_fields:
513
+ if df[field].dtype is np.dtype('int64'):
514
+ df.loc[:, field] = df[field].apply(str)
515
+
516
+ if meta_dict.get('default', False): # is_old_format
517
+ dtype = str_to_dtype(df[field].iloc[0])
518
+ dtypes.append(dtype)
519
+
520
+ df.loc[:, field] = df[field].apply(int, args=(16,))
521
+
522
+ # -------------- show summary ---------------
523
+ _TM.point(measure_from=file_name)
524
+ if show and 'summary' in show:
525
+ print(df[[*data_fields]].describe())
526
+
527
+ # -------------- out ---------------
528
+ # special request to return only data frame
529
+ if out in {'df', pd.DataFrame}:
530
+ return df[[*data_fields]]
531
+
532
+ images = {field: df[field].values.reshape(data_shape)[df.row, df.col].reshape(data_shape).astype(image_type)
533
+ for field, image_type in zip(data_fields, dtypes)}
534
+ return images if out is dict else out(images[field] for field in data_fields)
535
+
536
+
537
+ def save_tiffs(images, path_prefix, compress=0):
538
+ """ Save dictionary of images into multiple tiff files as type 'uint16'
539
+ using given path_prefix as prefix and keys as suffix
540
+
541
+ :param images: dictionary of 1:3 images, of view {field:image}, where image is ndarray
542
+ :param path_prefix: output_folder [and output files basename without suffix and extension]
543
+ :param compress: not used for now
544
+ :usage: process_ciif_to_tiffs(images,args.prefix)
545
+ """
546
+ with warnings.catch_warnings():
547
+ warnings.simplefilter("ignore")
548
+ for field in images:
549
+ io.imsave('%s_%s.tif' % (path_prefix, field), images[field].astype('uint16'),
550
+ compress=compress)
551
+
552
+
553
+ def parse_command_line():
554
+ import argparse
555
+
556
+ parser = argparse.ArgumentParser(prog='ciif')
557
+
558
+ x_group = parser.add_mutually_exclusive_group(required=True)
559
+ x_group.add_argument('-ciif', action='store_true',
560
+ help='Input CIIF mode. Mutually exclusive with --img')
561
+ x_group.add_argument('-img', '--image', action='store_true',
562
+ help='Input images mode - provide image files accordingly')
563
+
564
+ # parser = parser.add_argument_group('calling conventions')
565
+ parser.add_argument('input_files', nargs='+', type=str,
566
+ help='single *.ciif file in -ciif mode; '
567
+ '1+ *.ciif files in -ciif -c mode;'
568
+ '1+ image files in -img mode')
569
+ parser.add_argument('-o', '--output_folder', type=str, nargs='?', default=[],
570
+ help='Default: the same as input files folder')
571
+ parser.add_argument('-p', '--prefix', type=str,
572
+ help='Prefix for output file names. '
573
+ '-ciif mode: --signals are used as suffix.'
574
+ ' Default: use prefix extracted from the input name.'
575
+ '-ciif -c mode: full name of the output file.'
576
+ '-img mode: name of the output file.'
577
+ ' May contain folder name if -o not defined.'
578
+ ' default: _ joined names of the input files.')
579
+
580
+ # input param in -ciif -c mode: concat option
581
+ parser.add_argument('-c', '--concat', type=int, nargs='+',
582
+ help='(-ciif -c mode only) Number of streams to use from each input ciif file')
583
+ parser.add_argument('-w', '--width', type=int,
584
+ help='(-ciif -c mode only) Width of concatenated result stream')
585
+
586
+ # input params in -img mode and -ciif -c mode
587
+ parser.add_argument('-f', '--first_bit', type=int, nargs='+',
588
+ help='(-img mode; -ciif -c mode) Starting bit (zero based) for each input signal')
589
+ parser.add_argument('-b', '--bits_read', type=int, nargs='+',
590
+ help='(-img mode; -ciif -c mode) Number of bits to read from each tiff file')
591
+
592
+ # input params in -img mode
593
+ parser.add_argument('-g', '--grey', action='store_true',
594
+ help='(-img mode only) If set color inputs will be excepted and converted to grey')
595
+
596
+ # output params in -img mode
597
+ parser.add_argument('-ho', '--header_old', action='store_true',
598
+ help='(-img mode only) Ciif header format to be written: new header by default')
599
+ parser.add_argument('-bw', '--bits_write', type=int, nargs='+',
600
+ help='(-img mode only) Number of bits in each signal')
601
+ parser.add_argument('-ft', '--formats', type=str, nargs='+',
602
+ help='(-img mode only) Format "signed"/"unsigned" for each signal')
603
+
604
+ f_group = parser.add_mutually_exclusive_group()
605
+ f_group.add_argument('-s', '--signals', nargs='+',
606
+ help='(-img mode; -ciif -c mode) list names of all the signals in the ciif or in repeating group'
607
+ 'Default values: "s1", "s2", ...')
608
+ f_group.add_argument('-db', '--formats_db',
609
+ help='Excel file describing io of test points')
610
+
611
+ specific_verb_terms = ['time', 'summary', 'db']
612
+ general_verb_terms = ['0', 'none', 'all']
613
+ parser.add_argument('-v', '--verbose', type=str, nargs='+', default=[],
614
+ choices=specific_verb_terms + general_verb_terms,
615
+ help='Add information type to hear about')
616
+
617
+ class Args:
618
+ def __init__(self): # just to please PyCharm ;-)
619
+ self.ciif = None
620
+ self.image = None
621
+ self.input_files = None
622
+ self.input_folder = None
623
+ self.output_folder = None
624
+ self.prefix = None
625
+ self.ciif_file = None
626
+ self.grey = None
627
+ self.first_bit = None
628
+ self.bits_read = None
629
+ self.header_old = None
630
+ self.bits_write = None
631
+ self.formats_db = None
632
+ self.concat = None
633
+ self.width = None
634
+ self.signals = []
635
+ self.formats = []
636
+ self.verbose = []
637
+
638
+ def devise_out_folder(self):
639
+ if not self.output_folder:
640
+ input_folder = path.dirname(self.input_files[0])
641
+ if path.commonpath(self.input_files) != input_folder:
642
+ warnings.warn('Using first input file folder for output: %s' % input_folder)
643
+ self.output_folder = input_folder
644
+ # Prepare output dir
645
+ if not path.exists(self.output_folder):
646
+ makedirs(self.output_folder)
647
+ return self.output_folder
648
+
649
+ def devise_out_ciif_path(self):
650
+ if self.prefix: # output folder may be part of the prefix argument:
651
+ if path.split(self.prefix)[0]:
652
+ unless(not self.output_folder, 'output folder provided also with prefix')
653
+ self.output_folder = path.dirname(self.prefix)
654
+ self.ciif_file = path.basename(self.prefix)
655
+ else:
656
+ self.ciif_file = self.prefix
657
+ else: # if output ciif image name not provided combine it from inputs names
658
+ # eliminate common prefix from file names to leave only meaningful signal tag
659
+ file_names = [path.split(inp)[1] for inp in self.input_files] # throw paths away
660
+ if len(file_names) > 1:
661
+ prefix = path.commonprefix(file_names)
662
+ signals = [path.splitext(name[len(prefix):])[0] for name in file_names]
663
+ self.ciif_file = prefix + '_' + '_'.join(signals) + '.ciif'
664
+ else:
665
+ self.ciif_file = path.splitext(file_names[0])[0] + '.ciif'
666
+
667
+ self.devise_out_folder()
668
+ self.ciif_file = path.join(self.output_folder, self.ciif_file)
669
+
670
+ arguments = Args()
671
+ parser.parse_args(namespace=arguments)
672
+
673
+ if 'all' in arguments.verbose:
674
+ arguments.verbose += specific_verb_terms
675
+
676
+ return arguments
677
+
678
+
679
+ def _concat_im_fields(images, signals, first_bits, bits_reads):
680
+ """Concat fields of images
681
+
682
+ :param images: dict of {signal : im} pairs
683
+ :param signals: list of signals from dict.keys() to be used for result
684
+ :param first_bits: list of first_bits per signal from signals to be used for result
685
+ :param bits_reads list of bits_reads per signal from signals to be used for result
686
+ :return: uint64 3ith MSB from signals[0] and LSB from signals[-1]
687
+ """
688
+ if type(images) is dict:
689
+ init_base = [np.zeros_like(im) for _, im in images.items()][0]
690
+ elif type(images) is list:
691
+ init_base = np.zeros_like(images[0])
692
+ else:
693
+ init_base = np.zeros_like(images)
694
+
695
+ res = init_base.flatten().astype('uint64')
696
+ shift = 0
697
+ for signal, first_bit, bits in zip(signals, first_bits, bits_reads):
698
+ im = ((images[signal].flatten() >> first_bit) & int(2 ** abs(bits) - 1))
699
+ # old: signals_list[0] will be LSB in output: res += im << shift; shift += bits
700
+
701
+ # new: signals_list[0] will be MSB in output
702
+ shift += bits
703
+ res = (res << bits) + im
704
+ return res.reshape(init_base.shape)
705
+
706
+
707
+ def concat_ciifs_to_ciif(args):
708
+ """Treat ciif(s) -> ciif concat mode of the command line processing.
709
+
710
+ :param args: command line arguments
711
+ :return:
712
+ """
713
+ ciif_num = len(args.concat)
714
+ if len(args.input_files) != ciif_num:
715
+ warnings.warn('-c arg needs the same num of vals as input files num')
716
+ return
717
+
718
+ im_dicts_list = [load_ciif(f) for f in args.input_files]
719
+
720
+ start_pos = 0
721
+ res = []
722
+ bits_reads = []
723
+ for im_dict, f_num in zip(im_dicts_list, args.concat):
724
+ num = int(f_num)
725
+ res.append(_concat_im_fields(images=im_dict,
726
+ signals=args.signals[start_pos:start_pos + num],
727
+ first_bits=args.first_bit[start_pos:start_pos + num],
728
+ bits_reads=args.bits_read[start_pos:start_pos + num]))
729
+ read = sum(args.bits_read[start_pos:start_pos + num])
730
+ bits_reads.append(read)
731
+ start_pos += num
732
+
733
+ res = _concat_im_fields(images=res, signals=range(ciif_num),
734
+ first_bits=[0] * ciif_num, bits_reads=bits_reads)
735
+ images_to_ciif(args.prefix, res, bits_num=sum(bits_reads), bits_write=args.width, new_hdr=False)
736
+ pass
737
+
738
+
739
+ def process_ciif_to_tiff(args):
740
+ """Treat ciif -> tiff mode of the command line processing
741
+
742
+ :param args: command line arguments
743
+ :return:
744
+ """
745
+ unless(len(args.input_files) == 1, 'ciif source requires single input file name')
746
+ args.ciif_file = args.input_files[0]
747
+ args.devise_out_folder()
748
+
749
+ def file_name_part(filename):
750
+ return path.splitext(path.basename(filename))[0]
751
+
752
+ if not args.prefix: # extract prefix from the ciif file: /ddd/aa_bb_ok.ext -> aa
753
+ args.prefix = file_name_part(args.ciif_file).split('_')[0]
754
+ args.prefix = path.join(args.output_folder, args.prefix)
755
+
756
+ # _TM.point('load DB')
757
+ if args.formats_db: # if excel Test Points Data Base is provided get signals from there
758
+ tp_db = load_scheme(args.formats_db)
759
+ if 'db' in args.verbose:
760
+ print('Test Points formats found in the DB:')
761
+ [print('\t' + tp) for tp in tp_db]
762
+
763
+ try:
764
+ args.signals = tp_db[file_name_part(args.ciif_file)]['signals']
765
+ except KeyError:
766
+ warnings.warn('No io info from file "%s" - using defaults' % args.format[0])
767
+ else:
768
+ print('Successfully extracted Test Points data io.')
769
+
770
+ # _TM.point('load ciif complete', measure_from='load DB')
771
+ images = load_ciif(args.ciif_file, args.signals, show=args.verbose)
772
+
773
+ # _TM.point('save tiffs', measure_from='load ciif complete')
774
+ save_tiffs(images, args.prefix)
775
+ # _TM.point(measure_from='save tiffs')
776
+
777
+
778
+ def rgb2gray(img):
779
+ from skimage.color import rgb2gray
780
+ return (rgb2gray(img) * 0xFF).astype('uint8')
781
+
782
+
783
+ def process_tiff_to_ciif(args):
784
+ """Treat tiff -> ciif mode of the command line processing
785
+
786
+ :param args: command line arguments
787
+ :return:
788
+ """
789
+ args.devise_out_ciif_path()
790
+ images = [io.imread(file) for file in args.input_files]
791
+
792
+ if args.grey:
793
+ images = [im[:, :, 1] if im.ndim == 3 and im.shape[2] in [3, 4] else im for im in images]
794
+ # images = [rgb2gray(im) for im in images]
795
+
796
+ if not args.bits_write:
797
+ args.bits_write = args.bits_read
798
+
799
+ images_to_ciif(args.ciif_file,
800
+ images=images,
801
+ bits_num=args.bits_read,
802
+ start_bits=args.first_bit,
803
+ stream_names=args.signals,
804
+ new_hdr = (not args.header_old),
805
+ bits_write=args.bits_write)
806
+
807
+ # OLDER VERSION?
808
+ # def cif_to_tif(file):
809
+ # """
810
+ # Convert images in a single ciif file into multiple separate tif files
811
+ # named `{original}_{internal_item_name}.tif`
812
+ #
813
+ # :param file: The source
814
+ # :return: number of files created
815
+ # """
816
+ # import warnings
817
+ # from skimage.io import imsave
818
+ # mport ciff as cif
819
+ # ext = '.ciif'
820
+ #
821
+ # try:
822
+ # file = Path(file)
823
+ # if not file.is_file():
824
+ # raise FileExistsError(file)
825
+ # if not file.suffix.lower() is ext:
826
+ # raise TypeError(f'{file} not {ext} file')
827
+ #
828
+ # ciif_data = cif.load_ciif(file)
829
+ # for key, image in ciif_data.items():
830
+ # with warnings.catch_warnings():
831
+ # warnings.simplefilter('ignore')
832
+ # img_name = f'{file[:-len(ext)]}_{key}.tif'
833
+ # imsave(img_name, image)
834
+ # print(f'Created: {img_name}')
835
+ # except Exception as e:
836
+ # print('Failed to parse:', file)
837
+ # print(e)
838
+ # return 0
839
+ # return len(ciif_data)
840
+
841
+
842
+ if __name__ == '__main__':
843
+ cmd_args = parse_command_line()
844
+ _TM.enable = 'time' in cmd_args.verbose
845
+
846
+ if cmd_args.ciif:
847
+ if cmd_args.width:
848
+ concat_ciifs_to_ciif(cmd_args)
849
+ else:
850
+ process_ciif_to_tiff(cmd_args)
851
+ elif cmd_args.image:
852
+ process_tiff_to_ciif(cmd_args)