1# -*- coding: utf-8 -*-
2# Copyright (c) 2018, imageio contributors
3# imageio is distributed under the terms of the (new) BSD License.
4#
5
6""" Read LFR files (Lytro Illum).
7
8Backend: internal
9
10Plugin to read Lytro Illum .lfr and .raw files as produced
11by the Lytro Illum light field camera. It is actually a collection
12of plugins, each supporting slightly different keyword arguments
13
14Parameters
15----------
16meta_only : bool
17 Whether to only read the metadata.
18include_thumbnail : bool
19 (only for lytro-lfr and lytro-lfp)
20 Whether to include an image thumbnail in the metadata.
21
22"""
23#
24#
25# This code is based on work by
26# David Uhlig and his lfr_reader
27# (https://www.iiit.kit.edu/uhlig.php)
28# Donald Dansereau and his Matlab LF Toolbox
29# (http://dgd.vision/Tools/LFToolbox/)
30# and Behnam Esfahbod and his Python LFP-Reader
31# (https://github.com/behnam/python-lfp-reader/)
32
33
34import os
35import json
36import struct
37import logging
38
39
40import numpy as np
41
42from ..core import Format
43from ..v2 import imread
44
45
46logger = logging.getLogger(__name__)
47
48
49# Sensor size of Lytro Illum resp. Lytro F01 light field camera sensor
50LYTRO_ILLUM_IMAGE_SIZE = (5368, 7728)
51LYTRO_F01_IMAGE_SIZE = (3280, 3280)
52
53# Parameter of lfr file format
54HEADER_LENGTH = 12
55SIZE_LENGTH = 4 # = 16 - header_length
56SHA1_LENGTH = 45 # = len("sha1-") + (160 / 4)
57PADDING_LENGTH = 35 # = (4*16) - header_length - size_length - sha1_length
58DATA_CHUNKS_ILLUM = 11
59DATA_CHUNKS_F01 = 3
60
61
62class LytroFormat(Format):
63 """Base class for Lytro format.
64 The subclasses LytroLfrFormat, LytroLfpFormat, LytroIllumRawFormat and
65 LytroF01RawFormat implement the Lytro-LFR, Lytro-LFP and Lytro-RAW format
66 for the Illum and original F01 camera respectively.
67 Writing is not supported.
68 """
69
70 # Only single images are supported.
71 _modes = "i"
72
73 def _can_write(self, request):
74 # Writing of Lytro files is not supported
75 return False
76
77 # -- writer
78
79 class Writer(Format.Writer):
80 def _open(self, flags=0):
81 self._fp = self.request.get_file()
82
83 def _close(self):
84 # Close the reader.
85 # Note that the request object will close self._fp
86 pass
87
88 def _append_data(self, im, meta):
89 # Process the given data and meta data.
90 raise RuntimeError("The lytro format cannot write image data.")
91
92 def _set_meta_data(self, meta):
93 # Process the given meta data (global for all images)
94 # It is not mandatory to support this.
95 raise RuntimeError("The lytro format cannot write meta data.")
96
97
98class LytroIllumRawFormat(LytroFormat):
99 """This is the Lytro Illum RAW format.
100 The raw format is a 10bit image format as used by the Lytro Illum
101 light field camera. The format will read the specified raw file and will
102 try to load a .txt or .json file with the associated meta data.
103 This format does not support writing.
104
105
106 Parameters for reading
107 ----------------------
108 meta_only : bool
109 Whether to only read the metadata.
110 """
111
112 def _can_read(self, request):
113 # Check if mode and extensions are supported by the format
114 if request.extension in (".raw",):
115 return True
116
117 @staticmethod
118 def rearrange_bits(array):
119 # Do bit rearrangement for the 10-bit lytro raw format
120 # Normalize output to 1.0 as float64
121 t0 = array[0::5]
122 t1 = array[1::5]
123 t2 = array[2::5]
124 t3 = array[3::5]
125 lsb = array[4::5]
126
127 t0 = np.left_shift(t0, 2) + np.bitwise_and(lsb, 3)
128 t1 = np.left_shift(t1, 2) + np.right_shift(np.bitwise_and(lsb, 12), 2)
129 t2 = np.left_shift(t2, 2) + np.right_shift(np.bitwise_and(lsb, 48), 4)
130 t3 = np.left_shift(t3, 2) + np.right_shift(np.bitwise_and(lsb, 192), 6)
131
132 image = np.zeros(LYTRO_ILLUM_IMAGE_SIZE, dtype=np.uint16)
133 image[:, 0::4] = t0.reshape(
134 (LYTRO_ILLUM_IMAGE_SIZE[0], LYTRO_ILLUM_IMAGE_SIZE[1] // 4)
135 )
136 image[:, 1::4] = t1.reshape(
137 (LYTRO_ILLUM_IMAGE_SIZE[0], LYTRO_ILLUM_IMAGE_SIZE[1] // 4)
138 )
139 image[:, 2::4] = t2.reshape(
140 (LYTRO_ILLUM_IMAGE_SIZE[0], LYTRO_ILLUM_IMAGE_SIZE[1] // 4)
141 )
142 image[:, 3::4] = t3.reshape(
143 (LYTRO_ILLUM_IMAGE_SIZE[0], LYTRO_ILLUM_IMAGE_SIZE[1] // 4)
144 )
145
146 # Normalize data to 1.0 as 64-bit float.
147 # Division is by 1023 as the Lytro Illum saves 10-bit raw data.
148 return np.divide(image, 1023.0).astype(np.float64)
149
150 # -- reader
151
152 class Reader(Format.Reader):
153 def _open(self, meta_only=False):
154 self._file = self.request.get_file()
155 self._data = None
156 self._meta_only = meta_only
157
158 def _close(self):
159 # Close the reader.
160 # Note that the request object will close self._file
161 del self._data
162
163 def _get_length(self):
164 # Return the number of images.
165 return 1
166
167 def _get_data(self, index):
168 # Return the data and meta data for the given index
169
170 if index not in [0, "None"]:
171 raise IndexError("Lytro file contains only one dataset")
172
173 if not self._meta_only:
174 # Read all bytes
175 if self._data is None:
176 self._data = self._file.read()
177
178 # Read bytes from string and convert to uint16
179 raw = np.frombuffer(self._data, dtype=np.uint8).astype(np.uint16)
180
181 # Rearrange bits
182 img = LytroIllumRawFormat.rearrange_bits(raw)
183
184 else:
185 # Return empty image
186 img = np.array([])
187
188 # Return image and meta data
189 return img, self._get_meta_data(index=0)
190
191 def _get_meta_data(self, index):
192 # Get the meta data for the given index. If index is None, it
193 # should return the global meta data.
194
195 if index not in [0, None]:
196 raise IndexError("Lytro meta data file contains only one dataset")
197
198 # Try to read meta data from meta data file corresponding
199 # to the raw data file, extension in [.txt, .TXT, .json, .JSON]
200 filename_base = os.path.splitext(self.request.get_local_filename())[0]
201
202 meta_data = None
203
204 for ext in [".txt", ".TXT", ".json", ".JSON"]:
205 if os.path.isfile(filename_base + ext):
206 meta_data = json.load(open(filename_base + ext))
207
208 if meta_data is not None:
209 return meta_data
210
211 else:
212 logger.warning("No metadata file found for provided raw file.")
213 return {}
214
215
216class LytroLfrFormat(LytroFormat):
217 """This is the Lytro Illum LFR format.
218 The lfr is a image and meta data container format as used by the
219 Lytro Illum light field camera.
220 The format will read the specified lfr file.
221 This format does not support writing.
222
223 Parameters for reading
224 ----------------------
225 meta_only : bool
226 Whether to only read the metadata.
227 include_thumbnail : bool
228 Whether to include an image thumbnail in the metadata.
229 """
230
231 def _can_read(self, request):
232 # Check if mode and extensions are supported by the format
233 if request.extension in (".lfr",):
234 return True
235
236 # -- reader
237
238 class Reader(Format.Reader):
239 def _open(self, meta_only=False, include_thumbnail=True):
240 self._file = self.request.get_file()
241 self._data = None
242 self._chunks = {}
243 self.metadata = {}
244 self._content = None
245 self._meta_only = meta_only
246 self._include_thumbnail = include_thumbnail
247
248 self._find_header()
249 self._find_chunks()
250 self._find_meta()
251
252 try:
253 # Get sha1 dict and check if it is in dictionary of data chunks
254 chunk_dict = self._content["frames"][0]["frame"]
255 if (
256 chunk_dict["metadataRef"] in self._chunks
257 and chunk_dict["imageRef"] in self._chunks
258 and chunk_dict["privateMetadataRef"] in self._chunks
259 ):
260 if not self._meta_only:
261 # Read raw image data byte buffer
262 data_pos, size = self._chunks[chunk_dict["imageRef"]]
263 self._file.seek(data_pos, 0)
264 self.raw_image_data = self._file.read(size)
265
266 # Read meta data
267 data_pos, size = self._chunks[chunk_dict["metadataRef"]]
268 self._file.seek(data_pos, 0)
269 metadata = self._file.read(size)
270 # Add metadata to meta data dict
271 self.metadata["metadata"] = json.loads(metadata.decode("ASCII"))
272
273 # Read private metadata
274 data_pos, size = self._chunks[chunk_dict["privateMetadataRef"]]
275 self._file.seek(data_pos, 0)
276 serial_numbers = self._file.read(size)
277 self.serial_numbers = json.loads(serial_numbers.decode("ASCII"))
278 # Add private metadata to meta data dict
279 self.metadata["privateMetadata"] = self.serial_numbers
280
281 # Read image preview thumbnail
282 if self._include_thumbnail:
283 chunk_dict = self._content["thumbnails"][0]
284 if chunk_dict["imageRef"] in self._chunks:
285 # Read thumbnail image from thumbnail chunk
286 data_pos, size = self._chunks[chunk_dict["imageRef"]]
287 self._file.seek(data_pos, 0)
288 # Read binary data, read image as jpeg
289 thumbnail_data = self._file.read(size)
290 thumbnail_img = imread(thumbnail_data, format="jpeg")
291
292 thumbnail_height = chunk_dict["height"]
293 thumbnail_width = chunk_dict["width"]
294
295 # Add thumbnail to metadata
296 self.metadata["thumbnail"] = {
297 "image": thumbnail_img,
298 "height": thumbnail_height,
299 "width": thumbnail_width,
300 }
301
302 except KeyError:
303 raise RuntimeError("The specified file is not a valid LFR file.")
304
305 def _close(self):
306 # Close the reader.
307 # Note that the request object will close self._file
308 del self._data
309
310 def _get_length(self):
311 # Return the number of images. Can be np.inf
312 return 1
313
314 def _find_header(self):
315 """
316 Checks if file has correct header and skip it.
317 """
318 file_header = b"\x89LFP\x0D\x0A\x1A\x0A\x00\x00\x00\x01"
319 # Read and check header of file
320 header = self._file.read(HEADER_LENGTH)
321 if header != file_header:
322 raise RuntimeError("The LFR file header is invalid.")
323
324 # Read first bytes to skip header
325 self._file.read(SIZE_LENGTH)
326
327 def _find_chunks(self):
328 """
329 Gets start position and size of data chunks in file.
330 """
331 chunk_header = b"\x89LFC\x0D\x0A\x1A\x0A\x00\x00\x00\x00"
332
333 for i in range(0, DATA_CHUNKS_ILLUM):
334 data_pos, size, sha1 = self._get_chunk(chunk_header)
335 self._chunks[sha1] = (data_pos, size)
336
337 def _find_meta(self):
338 """
339 Gets a data chunk that contains information over content
340 of other data chunks.
341 """
342 meta_header = b"\x89LFM\x0D\x0A\x1A\x0A\x00\x00\x00\x00"
343 data_pos, size, sha1 = self._get_chunk(meta_header)
344
345 # Get content
346 self._file.seek(data_pos, 0)
347 data = self._file.read(size)
348 self._content = json.loads(data.decode("ASCII"))
349
350 def _get_chunk(self, header):
351 """
352 Checks if chunk has correct header and skips it.
353 Finds start position and length of next chunk and reads
354 sha1-string that identifies the following data chunk.
355
356 Parameters
357 ----------
358 header : bytes
359 Byte string that identifies start of chunk.
360
361 Returns
362 -------
363 data_pos : int
364 Start position of data chunk in file.
365 size : int
366 Size of data chunk.
367 sha1 : str
368 Sha1 value of chunk.
369 """
370 # Read and check header of chunk
371 header_chunk = self._file.read(HEADER_LENGTH)
372 if header_chunk != header:
373 raise RuntimeError("The LFR chunk header is invalid.")
374
375 data_pos = None
376 sha1 = None
377
378 # Read size
379 size = struct.unpack(">i", self._file.read(SIZE_LENGTH))[0]
380 if size > 0:
381 # Read sha1
382 sha1 = str(self._file.read(SHA1_LENGTH).decode("ASCII"))
383 # Skip fixed null chars
384 self._file.read(PADDING_LENGTH)
385 # Find start of data and skip data
386 data_pos = self._file.tell()
387 self._file.seek(size, 1)
388 # Skip extra null chars
389 ch = self._file.read(1)
390 while ch == b"\0":
391 ch = self._file.read(1)
392 self._file.seek(-1, 1)
393
394 return data_pos, size, sha1
395
396 def _get_data(self, index):
397 # Return the data and meta data for the given index
398 if index not in [0, None]:
399 raise IndexError("Lytro lfr file contains only one dataset")
400
401 if not self._meta_only:
402 # Read bytes from string and convert to uint16
403 raw = np.frombuffer(self.raw_image_data, dtype=np.uint8).astype(
404 np.uint16
405 )
406 im = LytroIllumRawFormat.rearrange_bits(raw)
407 else:
408 im = np.array([])
409
410 # Return array and dummy meta data
411 return im, self.metadata
412
413 def _get_meta_data(self, index):
414 # Get the meta data for the given index. If index is None,
415 # it returns the global meta data.
416 if index not in [0, None]:
417 raise IndexError("Lytro meta data file contains only one dataset")
418
419 return self.metadata
420
421
422class LytroF01RawFormat(LytroFormat):
423 """This is the Lytro RAW format for the original F01 Lytro camera.
424 The raw format is a 12bit image format as used by the Lytro F01
425 light field camera. The format will read the specified raw file and will
426 try to load a .txt or .json file with the associated meta data.
427 This format does not support writing.
428
429
430 Parameters for reading
431 ----------------------
432 meta_only : bool
433 Whether to only read the metadata.
434
435 """
436
437 def _can_read(self, request):
438 # Check if mode and extensions are supported by the format
439 if request.extension in (".raw",):
440 return True
441
442 @staticmethod
443 def rearrange_bits(array):
444 # Do bit rearrangement for the 12-bit lytro raw format
445 # Normalize output to 1.0 as float64
446 t0 = array[0::3]
447 t1 = array[1::3]
448 t2 = array[2::3]
449
450 a0 = np.left_shift(t0, 4) + np.right_shift(np.bitwise_and(t1, 240), 4)
451 a1 = np.left_shift(np.bitwise_and(t1, 15), 8) + t2
452
453 image = np.zeros(LYTRO_F01_IMAGE_SIZE, dtype=np.uint16)
454 image[:, 0::2] = a0.reshape(
455 (LYTRO_F01_IMAGE_SIZE[0], LYTRO_F01_IMAGE_SIZE[1] // 2)
456 )
457 image[:, 1::2] = a1.reshape(
458 (LYTRO_F01_IMAGE_SIZE[0], LYTRO_F01_IMAGE_SIZE[1] // 2)
459 )
460
461 # Normalize data to 1.0 as 64-bit float.
462 # Division is by 4095 as the Lytro F01 saves 12-bit raw data.
463 return np.divide(image, 4095.0).astype(np.float64)
464
465 # -- reader
466
467 class Reader(Format.Reader):
468 def _open(self, meta_only=False):
469 self._file = self.request.get_file()
470 self._data = None
471 self._meta_only = meta_only
472
473 def _close(self):
474 # Close the reader.
475 # Note that the request object will close self._file
476 del self._data
477
478 def _get_length(self):
479 # Return the number of images.
480 return 1
481
482 def _get_data(self, index):
483 # Return the data and meta data for the given index
484
485 if index not in [0, "None"]:
486 raise IndexError("Lytro file contains only one dataset")
487
488 if not self._meta_only:
489 # Read all bytes
490 if self._data is None:
491 self._data = self._file.read()
492
493 # Read bytes from string and convert to uint16
494 raw = np.frombuffer(self._data, dtype=np.uint8).astype(np.uint16)
495
496 # Rearrange bits
497 img = LytroF01RawFormat.rearrange_bits(raw)
498
499 else:
500 img = np.array([])
501
502 # Return image and meta data
503 return img, self._get_meta_data(index=0)
504
505 def _get_meta_data(self, index):
506 # Get the meta data for the given index. If index is None, it
507 # should return the global meta data.
508
509 if index not in [0, None]:
510 raise IndexError("Lytro meta data file contains only one dataset")
511
512 # Try to read meta data from meta data file corresponding
513 # to the raw data file, extension in [.txt, .TXT, .json, .JSON]
514 filename_base = os.path.splitext(self.request.get_local_filename())[0]
515
516 meta_data = None
517
518 for ext in [".txt", ".TXT", ".json", ".JSON"]:
519 if os.path.isfile(filename_base + ext):
520 meta_data = json.load(open(filename_base + ext))
521
522 if meta_data is not None:
523 return meta_data
524
525 else:
526 logger.warning("No metadata file found for provided raw file.")
527 return {}
528
529
530class LytroLfpFormat(LytroFormat):
531 """This is the Lytro Illum LFP format.
532 The lfp is a image and meta data container format as used by the
533 Lytro F01 light field camera.
534 The format will read the specified lfp file.
535 This format does not support writing.
536
537 Parameters for reading
538 ----------------------
539 meta_only : bool
540 Whether to only read the metadata.
541 include_thumbnail : bool
542 Whether to include an image thumbnail in the metadata.
543 """
544
545 def _can_read(self, request):
546 # Check if mode and extensions are supported by the format
547 if request.extension in (".lfp",):
548 return True
549
550 # -- reader
551
552 class Reader(Format.Reader):
553 def _open(self, meta_only=False):
554 self._file = self.request.get_file()
555 self._data = None
556 self._chunks = {}
557 self.metadata = {}
558 self._content = None
559 self._meta_only = meta_only
560
561 self._find_header()
562 self._find_meta()
563 self._find_chunks()
564
565 try:
566 # Get sha1 dict and check if it is in dictionary of data chunks
567 chunk_dict = self._content["picture"]["frameArray"][0]["frame"]
568 if (
569 chunk_dict["metadataRef"] in self._chunks
570 and chunk_dict["imageRef"] in self._chunks
571 and chunk_dict["privateMetadataRef"] in self._chunks
572 ):
573 if not self._meta_only:
574 # Read raw image data byte buffer
575 data_pos, size = self._chunks[chunk_dict["imageRef"]]
576 self._file.seek(data_pos, 0)
577 self.raw_image_data = self._file.read(size)
578
579 # Read meta data
580 data_pos, size = self._chunks[chunk_dict["metadataRef"]]
581 self._file.seek(data_pos, 0)
582 metadata = self._file.read(size)
583 # Add metadata to meta data dict
584 self.metadata["metadata"] = json.loads(metadata.decode("ASCII"))
585
586 # Read private metadata
587 data_pos, size = self._chunks[chunk_dict["privateMetadataRef"]]
588 self._file.seek(data_pos, 0)
589 serial_numbers = self._file.read(size)
590 self.serial_numbers = json.loads(serial_numbers.decode("ASCII"))
591 # Add private metadata to meta data dict
592 self.metadata["privateMetadata"] = self.serial_numbers
593
594 except KeyError:
595 raise RuntimeError("The specified file is not a valid LFP file.")
596
597 def _close(self):
598 # Close the reader.
599 # Note that the request object will close self._file
600 del self._data
601
602 def _get_length(self):
603 # Return the number of images. Can be np.inf
604 return 1
605
606 def _find_header(self):
607 """
608 Checks if file has correct header and skip it.
609 """
610 file_header = b"\x89LFP\x0D\x0A\x1A\x0A\x00\x00\x00\x01"
611
612 # Read and check header of file
613 header = self._file.read(HEADER_LENGTH)
614 if header != file_header:
615 raise RuntimeError("The LFP file header is invalid.")
616
617 # Read first bytes to skip header
618 self._file.read(SIZE_LENGTH)
619
620 def _find_chunks(self):
621 """
622 Gets start position and size of data chunks in file.
623 """
624 chunk_header = b"\x89LFC\x0D\x0A\x1A\x0A\x00\x00\x00\x00"
625
626 for i in range(0, DATA_CHUNKS_F01):
627 data_pos, size, sha1 = self._get_chunk(chunk_header)
628 self._chunks[sha1] = (data_pos, size)
629
630 def _find_meta(self):
631 """
632 Gets a data chunk that contains information over content
633 of other data chunks.
634 """
635 meta_header = b"\x89LFM\x0D\x0A\x1A\x0A\x00\x00\x00\x00"
636
637 data_pos, size, sha1 = self._get_chunk(meta_header)
638
639 # Get content
640 self._file.seek(data_pos, 0)
641 data = self._file.read(size)
642 self._content = json.loads(data.decode("ASCII"))
643 data = self._file.read(5) # Skip 5
644
645 def _get_chunk(self, header):
646 """
647 Checks if chunk has correct header and skips it.
648 Finds start position and length of next chunk and reads
649 sha1-string that identifies the following data chunk.
650
651 Parameters
652 ----------
653 header : bytes
654 Byte string that identifies start of chunk.
655
656 Returns
657 -------
658 data_pos : int
659 Start position of data chunk in file.
660 size : int
661 Size of data chunk.
662 sha1 : str
663 Sha1 value of chunk.
664 """
665 # Read and check header of chunk
666 header_chunk = self._file.read(HEADER_LENGTH)
667 if header_chunk != header:
668 raise RuntimeError("The LFP chunk header is invalid.")
669
670 data_pos = None
671 sha1 = None
672
673 # Read size
674 size = struct.unpack(">i", self._file.read(SIZE_LENGTH))[0]
675 if size > 0:
676 # Read sha1
677 sha1 = str(self._file.read(SHA1_LENGTH).decode("ASCII"))
678 # Skip fixed null chars
679 self._file.read(PADDING_LENGTH)
680 # Find start of data and skip data
681 data_pos = self._file.tell()
682 self._file.seek(size, 1)
683 # Skip extra null chars
684 ch = self._file.read(1)
685 while ch == b"\0":
686 ch = self._file.read(1)
687 self._file.seek(-1, 1)
688
689 return data_pos, size, sha1
690
691 def _get_data(self, index):
692 # Return the data and meta data for the given index
693 if index not in [0, None]:
694 raise IndexError("Lytro lfp file contains only one dataset")
695
696 if not self._meta_only:
697 # Read bytes from string and convert to uint16
698 raw = np.frombuffer(self.raw_image_data, dtype=np.uint8).astype(
699 np.uint16
700 )
701 im = LytroF01RawFormat.rearrange_bits(raw)
702 else:
703 im = np.array([])
704
705 # Return array and dummy meta data
706 return im, self.metadata
707
708 def _get_meta_data(self, index):
709 # Get the meta data for the given index. If index is None,
710 # it returns the global meta data.
711 if index not in [0, None]:
712 raise IndexError("Lytro meta data file contains only one dataset")
713
714 return self.metadata