bsdf.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. # -*- coding: utf-8 -*-
  2. # imageio is distributed under the terms of the (new) BSD License.
  3. """Read/Write BSDF files.
  4. Backend Library: internal
  5. The BSDF format enables reading and writing of image data in the
  6. BSDF serialization format. This format allows storage of images, volumes,
  7. and series thereof. Data can be of any numeric data type, and can
  8. optionally be compressed. Each image/volume can have associated
  9. meta data, which can consist of any data type supported by BSDF.
  10. By default, image data is lazily loaded; the actual image data is
  11. not read until it is requested. This allows storing multiple images
  12. in a single file and still have fast access to individual images.
  13. Alternatively, a series of images can be read in streaming mode, reading
  14. images as they are read (e.g. from http).
  15. BSDF is a simple generic binary format. It is easy to extend and there
  16. are standard extension definitions for 2D and 3D image data.
  17. Read more at http://bsdf.io.
  18. Parameters
  19. ----------
  20. random_access : bool
  21. Whether individual images in the file can be read in random order.
  22. Defaults to True for normal files, and to False when reading from HTTP.
  23. If False, the file is read in "streaming mode", allowing reading
  24. files as they are read, but without support for "rewinding".
  25. Note that setting this to True when reading from HTTP, the whole file
  26. is read upon opening it (since lazy loading is not possible over HTTP).
  27. compression : int
  28. Use ``0`` or "no" for no compression, ``1`` or "zlib" for Zlib
  29. compression (same as zip files and PNG), and ``2`` or "bz2" for Bz2
  30. compression (more compact but slower). Default 1 (zlib).
  31. Note that some BSDF implementations may not support compression
  32. (e.g. JavaScript).
  33. """
  34. import numpy as np
  35. from ..core import Format
  36. def get_bsdf_serializer(options):
  37. from . import _bsdf as bsdf
  38. class NDArrayExtension(bsdf.Extension):
  39. """Copy of BSDF's NDArrayExtension but deal with lazy blobs."""
  40. name = "ndarray"
  41. cls = np.ndarray
  42. def encode(self, s, v):
  43. return dict(shape=v.shape, dtype=str(v.dtype), data=v.tobytes())
  44. def decode(self, s, v):
  45. return v # return as dict, because of lazy blobs, decode in Image
  46. class ImageExtension(bsdf.Extension):
  47. """We implement two extensions that trigger on the Image classes."""
  48. def encode(self, s, v):
  49. return dict(array=v.array, meta=v.meta)
  50. def decode(self, s, v):
  51. return Image(v["array"], v["meta"])
  52. class Image2DExtension(ImageExtension):
  53. name = "image2d"
  54. cls = Image2D
  55. class Image3DExtension(ImageExtension):
  56. name = "image3d"
  57. cls = Image3D
  58. exts = [NDArrayExtension, Image2DExtension, Image3DExtension]
  59. serializer = bsdf.BsdfSerializer(exts, **options)
  60. return bsdf, serializer
  61. class Image:
  62. """Class in which we wrap the array and meta data. By using an extension
  63. we can make BSDF trigger on these classes and thus encode the images.
  64. as actual images.
  65. """
  66. def __init__(self, array, meta):
  67. self.array = array
  68. self.meta = meta
  69. def get_array(self):
  70. if not isinstance(self.array, np.ndarray):
  71. v = self.array
  72. blob = v["data"]
  73. if not isinstance(blob, bytes): # then it's a lazy bsdf.Blob
  74. blob = blob.get_bytes()
  75. self.array = np.frombuffer(blob, dtype=v["dtype"])
  76. self.array.shape = v["shape"]
  77. return self.array
  78. def get_meta(self):
  79. return self.meta
  80. class Image2D(Image):
  81. pass
  82. class Image3D(Image):
  83. pass
  84. class BsdfFormat(Format):
  85. """The BSDF format enables reading and writing of image data in the
  86. BSDF serialization format. This format allows storage of images, volumes,
  87. and series thereof. Data can be of any numeric data type, and can
  88. optionally be compressed. Each image/volume can have associated
  89. meta data, which can consist of any data type supported by BSDF.
  90. By default, image data is lazily loaded; the actual image data is
  91. not read until it is requested. This allows storing multiple images
  92. in a single file and still have fast access to individual images.
  93. Alternatively, a series of images can be read in streaming mode, reading
  94. images as they are read (e.g. from http).
  95. BSDF is a simple generic binary format. It is easy to extend and there
  96. are standard extension definitions for 2D and 3D image data.
  97. Read more at http://bsdf.io.
  98. Parameters for reading
  99. ----------------------
  100. random_access : bool
  101. Whether individual images in the file can be read in random order.
  102. Defaults to True for normal files, and to False when reading from HTTP.
  103. If False, the file is read in "streaming mode", allowing reading
  104. files as they are read, but without support for "rewinding".
  105. Note that setting this to True when reading from HTTP, the whole file
  106. is read upon opening it (since lazy loading is not possible over HTTP).
  107. Parameters for saving
  108. ---------------------
  109. compression : {0, 1, 2}
  110. Use ``0`` or "no" for no compression, ``1`` or "zlib" for Zlib
  111. compression (same as zip files and PNG), and ``2`` or "bz2" for Bz2
  112. compression (more compact but slower). Default 1 (zlib).
  113. Note that some BSDF implementations may not support compression
  114. (e.g. JavaScript).
  115. """
  116. def _can_read(self, request):
  117. if request.mode[1] in (self.modes + "?"):
  118. # if request.extension in self.extensions:
  119. # return True
  120. if request.firstbytes.startswith(b"BSDF"):
  121. return True
  122. def _can_write(self, request):
  123. if request.mode[1] in (self.modes + "?"):
  124. if request.extension in self.extensions:
  125. return True
  126. # -- reader
  127. class Reader(Format.Reader):
  128. def _open(self, random_access=None):
  129. # Validate - we need a BSDF file consisting of a list of images
  130. # The list is typically a stream, but does not have to be.
  131. assert self.request.firstbytes[:4] == b"BSDF", "Not a BSDF file"
  132. # self.request.firstbytes[5:6] == major and minor version
  133. if not (
  134. self.request.firstbytes[6:15] == b"M\x07image2D"
  135. or self.request.firstbytes[6:15] == b"M\x07image3D"
  136. or self.request.firstbytes[6:7] == b"l"
  137. ):
  138. pass # Actually, follow a more duck-type approach ...
  139. # raise RuntimeError('BSDF file does not look like an '
  140. # 'image container.')
  141. # Set options. If we think that seeking is allowed, we lazily load
  142. # blobs, and set streaming to False (i.e. the whole file is read,
  143. # but we skip over binary blobs), so that we subsequently allow
  144. # random access to the images.
  145. # If seeking is not allowed (e.g. with a http request), we cannot
  146. # lazily load blobs, but we can still load streaming from the web.
  147. options = {}
  148. if self.request.filename.startswith(("http://", "https://")):
  149. ra = False if random_access is None else bool(random_access)
  150. options["lazy_blob"] = False # Because we cannot seek now
  151. options["load_streaming"] = not ra # Load as a stream?
  152. else:
  153. ra = True if random_access is None else bool(random_access)
  154. options["lazy_blob"] = ra # Don't read data until needed
  155. options["load_streaming"] = not ra
  156. file = self.request.get_file()
  157. bsdf, self._serializer = get_bsdf_serializer(options)
  158. self._stream = self._serializer.load(file)
  159. # Another validation
  160. if (
  161. isinstance(self._stream, dict)
  162. and "meta" in self._stream
  163. and "array" in self._stream
  164. ):
  165. self._stream = Image(self._stream["array"], self._stream["meta"])
  166. if not isinstance(self._stream, (Image, list, bsdf.ListStream)):
  167. raise RuntimeError(
  168. "BSDF file does not look seem to have an " "image container."
  169. )
  170. def _close(self):
  171. pass
  172. def _get_length(self):
  173. if isinstance(self._stream, Image):
  174. return 1
  175. elif isinstance(self._stream, list):
  176. return len(self._stream)
  177. elif self._stream.count < 0:
  178. return np.inf
  179. return self._stream.count
  180. def _get_data(self, index):
  181. # Validate
  182. if index < 0 or index >= self.get_length():
  183. raise IndexError(
  184. "Image index %i not in [0 %i]." % (index, self.get_length())
  185. )
  186. # Get Image object
  187. if isinstance(self._stream, Image):
  188. image_ob = self._stream # singleton
  189. elif isinstance(self._stream, list):
  190. # Easy when we have random access
  191. image_ob = self._stream[index]
  192. else:
  193. # For streaming, we need to skip over frames
  194. if index < self._stream.index:
  195. raise IndexError(
  196. "BSDF file is being read in streaming "
  197. "mode, thus does not allow rewinding."
  198. )
  199. while index > self._stream.index:
  200. self._stream.next()
  201. image_ob = self._stream.next() # Can raise StopIteration
  202. # Is this an image?
  203. if (
  204. isinstance(image_ob, dict)
  205. and "meta" in image_ob
  206. and "array" in image_ob
  207. ):
  208. image_ob = Image(image_ob["array"], image_ob["meta"])
  209. if isinstance(image_ob, Image):
  210. # Return as array (if we have lazy blobs, they are read now)
  211. return image_ob.get_array(), image_ob.get_meta()
  212. else:
  213. r = repr(image_ob)
  214. r = r if len(r) < 200 else r[:197] + "..."
  215. raise RuntimeError("BSDF file contains non-image " + r)
  216. def _get_meta_data(self, index): # pragma: no cover
  217. return {} # This format does not support global meta data
  218. # -- writer
  219. class Writer(Format.Writer):
  220. def _open(self, compression=1):
  221. options = {"compression": compression}
  222. bsdf, self._serializer = get_bsdf_serializer(options)
  223. if self.request.mode[1] in "iv":
  224. self._stream = None # Singleton image
  225. self._written = False
  226. else:
  227. # Series (stream) of images
  228. file = self.request.get_file()
  229. self._stream = bsdf.ListStream()
  230. self._serializer.save(file, self._stream)
  231. def _close(self):
  232. # We close the stream here, which will mark the number of written
  233. # elements. If we would not close it, the file would be fine, it's
  234. # just that upon reading it would not be known how many items are
  235. # in there.
  236. if self._stream is not None:
  237. self._stream.close(False) # False says "keep this a stream"
  238. def _append_data(self, im, meta):
  239. # Determine dimension
  240. ndim = None
  241. if self.request.mode[1] in "iI":
  242. ndim = 2
  243. elif self.request.mode[1] in "vV":
  244. ndim = 3
  245. else:
  246. ndim = 3 # Make an educated guess
  247. if im.ndim == 2 or (im.ndim == 3 and im.shape[-1] <= 4):
  248. ndim = 2
  249. # Validate shape
  250. assert ndim in (2, 3)
  251. if ndim == 2:
  252. assert im.ndim == 2 or (im.ndim == 3 and im.shape[-1] <= 4)
  253. else:
  254. assert im.ndim == 3 or (im.ndim == 4 and im.shape[-1] <= 4)
  255. # Wrap data and meta data in our special class that will trigger
  256. # the BSDF image2D or image3D extension.
  257. if ndim == 2:
  258. ob = Image2D(im, meta)
  259. else:
  260. ob = Image3D(im, meta)
  261. # Write directly or to stream
  262. if self._stream is None:
  263. assert not self._written, "Cannot write singleton image twice"
  264. self._written = True
  265. file = self.request.get_file()
  266. self._serializer.save(file, ob)
  267. else:
  268. self._stream.append(ob)
  269. def set_meta_data(self, meta): # pragma: no cover
  270. raise RuntimeError("The BSDF format only supports " "per-image meta data.")