opencv.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. """Read/Write images using OpenCV.
  2. Backend Library: `OpenCV <https://opencv.org/>`_
  3. This plugin wraps OpenCV (also known as ``cv2``), a popular image processing
  4. library. Currently, it exposes OpenCVs image reading capability (no video or GIF
  5. support yet); however, this may be added in future releases.
  6. Methods
  7. -------
  8. .. note::
  9. Check the respective function for a list of supported kwargs and their
  10. documentation.
  11. .. autosummary::
  12. :toctree:
  13. OpenCVPlugin.read
  14. OpenCVPlugin.iter
  15. OpenCVPlugin.write
  16. OpenCVPlugin.properties
  17. OpenCVPlugin.metadata
  18. Pixel Formats (Colorspaces)
  19. ---------------------------
  20. OpenCV is known to process images in BGR; however, most of the python ecosystem
  21. (in particular matplotlib and other pydata libraries) use the RGB. As such,
  22. images are converted to RGB, RGBA, or grayscale (where applicable) by default.
  23. """
  24. import warnings
  25. from pathlib import Path
  26. from typing import Any, Dict, List, Optional, Union
  27. import cv2
  28. import numpy as np
  29. from ..core import Request
  30. from ..core.request import URI_BYTES, InitializationError, IOMode
  31. from ..core.v3_plugin_api import ImageProperties, PluginV3
  32. from ..typing import ArrayLike
  33. class OpenCVPlugin(PluginV3):
  34. def __init__(self, request: Request) -> None:
  35. super().__init__(request)
  36. self.file_handle = request.get_local_filename()
  37. if request._uri_type is URI_BYTES:
  38. self.filename = "<bytes>"
  39. else:
  40. self.filename = request.raw_uri
  41. mode = request.mode.io_mode
  42. if mode == IOMode.read and not cv2.haveImageReader(self.file_handle):
  43. raise InitializationError(f"OpenCV can't read `{self.filename}`.")
  44. elif mode == IOMode.write and not cv2.haveImageWriter(self.file_handle):
  45. raise InitializationError(f"OpenCV can't write to `{self.filename}`.")
  46. def read(
  47. self,
  48. *,
  49. index: int = None,
  50. colorspace: Union[int, str] = None,
  51. flags: int = cv2.IMREAD_COLOR,
  52. ) -> np.ndarray:
  53. """Read an image from the ImageResource.
  54. Parameters
  55. ----------
  56. index : int, Ellipsis
  57. If int, read the index-th image from the ImageResource. If ``...``,
  58. read all images from the ImageResource and stack them along a new,
  59. prepended, batch dimension. If None (default), use ``index=0`` if
  60. the image contains exactly one image and ``index=...`` otherwise.
  61. colorspace : str, int
  62. The colorspace to convert into after loading and before returning
  63. the image. If None (default) keep grayscale images as is, convert
  64. images with an alpha channel to ``RGBA`` and all other images to
  65. ``RGB``. If int, interpret ``colorspace`` as one of OpenCVs
  66. `conversion flags
  67. <https://docs.opencv.org/4.x/d8/d01/group__imgproc__color__conversions.html>`_
  68. and use it for conversion. If str, convert the image into the given
  69. colorspace. Possible string values are: ``"RGB"``, ``"BGR"``,
  70. ``"RGBA"``, ``"BGRA"``, ``"GRAY"``, ``"HSV"``, or ``"LAB"``.
  71. flags : int
  72. The OpenCV flag(s) to pass to the reader. Refer to the `OpenCV docs
  73. <https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
  74. for details.
  75. Returns
  76. -------
  77. ndimage : np.ndarray
  78. The decoded image as a numpy array.
  79. """
  80. if index is None:
  81. n_images = cv2.imcount(self.file_handle, flags)
  82. index = 0 if n_images == 1 else ...
  83. if index is ...:
  84. retval, img = cv2.imreadmulti(self.file_handle, flags=flags)
  85. is_batch = True
  86. else:
  87. retval, img = cv2.imreadmulti(self.file_handle, index, 1, flags=flags)
  88. is_batch = False
  89. if retval is False:
  90. raise ValueError(f"Could not read index `{index}` from `{self.filename}`.")
  91. if img[0].ndim == 2:
  92. in_colorspace = "GRAY"
  93. out_colorspace = colorspace or "GRAY"
  94. elif img[0].shape[-1] == 4:
  95. in_colorspace = "BGRA"
  96. out_colorspace = colorspace or "RGBA"
  97. else:
  98. in_colorspace = "BGR"
  99. out_colorspace = colorspace or "RGB"
  100. if isinstance(colorspace, int):
  101. cvt_space = colorspace
  102. elif in_colorspace == out_colorspace.upper():
  103. cvt_space = None
  104. else:
  105. out_colorspace = out_colorspace.upper()
  106. cvt_space = getattr(cv2, f"COLOR_{in_colorspace}2{out_colorspace}")
  107. if cvt_space is not None:
  108. img = np.stack([cv2.cvtColor(x, cvt_space) for x in img])
  109. else:
  110. img = np.stack(img)
  111. return img if is_batch else img[0]
  112. def iter(
  113. self,
  114. colorspace: Union[int, str] = None,
  115. flags: int = cv2.IMREAD_COLOR,
  116. ) -> np.ndarray:
  117. """Yield images from the ImageResource.
  118. Parameters
  119. ----------
  120. colorspace : str, int
  121. The colorspace to convert into after loading and before returning
  122. the image. If None (default) keep grayscale images as is, convert
  123. images with an alpha channel to ``RGBA`` and all other images to
  124. ``RGB``. If int, interpret ``colorspace`` as one of OpenCVs
  125. `conversion flags
  126. <https://docs.opencv.org/4.x/d8/d01/group__imgproc__color__conversions.html>`_
  127. and use it for conversion. If str, convert the image into the given
  128. colorspace. Possible string values are: ``"RGB"``, ``"BGR"``,
  129. ``"RGBA"``, ``"BGRA"``, ``"GRAY"``, ``"HSV"``, or ``"LAB"``.
  130. flags : int
  131. The OpenCV flag(s) to pass to the reader. Refer to the `OpenCV docs
  132. <https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
  133. for details.
  134. Yields
  135. ------
  136. ndimage : np.ndarray
  137. The decoded image as a numpy array.
  138. """
  139. for idx in range(cv2.imcount(self.file_handle)):
  140. yield self.read(index=idx, flags=flags, colorspace=colorspace)
  141. def write(
  142. self,
  143. ndimage: Union[ArrayLike, List[ArrayLike]],
  144. is_batch: bool = False,
  145. params: List[int] = None,
  146. ) -> Optional[bytes]:
  147. """Save an ndimage in the ImageResource.
  148. Parameters
  149. ----------
  150. ndimage : ArrayLike, List[ArrayLike]
  151. The image data that will be written to the file. It is either a
  152. single image, a batch of images, or a list of images.
  153. is_batch : bool
  154. If True, the provided ndimage is a batch of images. If False (default), the
  155. provided ndimage is a single image. If the provided ndimage is a list of images,
  156. this parameter has no effect.
  157. params : List[int]
  158. A list of parameters that will be passed to OpenCVs imwrite or
  159. imwritemulti functions. Possible values are documented in the
  160. `OpenCV documentation
  161. <https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#gabbc7ef1aa2edfaa87772f1202d67e0ce>`_.
  162. Returns
  163. -------
  164. encoded_image : bytes, None
  165. If the ImageResource is ``"<bytes>"`` the call to write returns the
  166. encoded image as a bytes string. Otherwise it returns None.
  167. """
  168. if isinstance(ndimage, list):
  169. ndimage = np.stack(ndimage, axis=0)
  170. elif not is_batch:
  171. ndimage = ndimage[None, ...]
  172. if ndimage[0].ndim == 2:
  173. n_channels = 1
  174. else:
  175. n_channels = ndimage[0].shape[-1]
  176. if n_channels == 1:
  177. ndimage_cv2 = [x for x in ndimage]
  178. elif n_channels == 4:
  179. ndimage_cv2 = [cv2.cvtColor(x, cv2.COLOR_RGBA2BGRA) for x in ndimage]
  180. else:
  181. ndimage_cv2 = [cv2.cvtColor(x, cv2.COLOR_RGB2BGR) for x in ndimage]
  182. retval = cv2.imwritemulti(self.file_handle, ndimage_cv2, params)
  183. if retval is False:
  184. # not sure what scenario would trigger this, but
  185. # it can occur theoretically.
  186. raise IOError("OpenCV failed to write.") # pragma: no cover
  187. if self.request._uri_type == URI_BYTES:
  188. return Path(self.file_handle).read_bytes()
  189. def properties(
  190. self,
  191. index: int = None,
  192. colorspace: Union[int, str] = None,
  193. flags: int = cv2.IMREAD_COLOR,
  194. ) -> ImageProperties:
  195. """Standardized image metadata.
  196. Parameters
  197. ----------
  198. index : int, Ellipsis
  199. If int, get the properties of the index-th image in the
  200. ImageResource. If ``...``, get the properties of the image stack
  201. that contains all images. If None (default), use ``index=0`` if the
  202. image contains exactly one image and ``index=...`` otherwise.
  203. colorspace : str, int
  204. The colorspace to convert into after loading and before returning
  205. the image. If None (default) keep grayscale images as is, convert
  206. images with an alpha channel to ``RGBA`` and all other images to
  207. ``RGB``. If int, interpret ``colorspace`` as one of OpenCVs
  208. `conversion flags
  209. <https://docs.opencv.org/4.x/d8/d01/group__imgproc__color__conversions.html>`_
  210. and use it for conversion. If str, convert the image into the given
  211. colorspace. Possible string values are: ``"RGB"``, ``"BGR"``,
  212. ``"RGBA"``, ``"BGRA"``, ``"GRAY"``, ``"HSV"``, or ``"LAB"``.
  213. flags : int
  214. The OpenCV flag(s) to pass to the reader. Refer to the `OpenCV docs
  215. <https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
  216. for details.
  217. Returns
  218. -------
  219. props : ImageProperties
  220. A dataclass filled with standardized image metadata.
  221. Notes
  222. -----
  223. Reading properties with OpenCV involves decoding pixel data, because
  224. OpenCV doesn't provide a direct way to access metadata.
  225. """
  226. if index is None:
  227. n_images = cv2.imcount(self.file_handle, flags)
  228. is_batch = n_images > 1
  229. elif index is Ellipsis:
  230. n_images = cv2.imcount(self.file_handle, flags)
  231. is_batch = True
  232. else:
  233. is_batch = False
  234. # unfortunately, OpenCV doesn't allow reading shape without reading pixel data
  235. if is_batch:
  236. img = self.read(index=0, flags=flags, colorspace=colorspace)
  237. return ImageProperties(
  238. shape=(n_images, *img.shape),
  239. dtype=img.dtype,
  240. n_images=n_images,
  241. is_batch=True,
  242. )
  243. img = self.read(index=index, flags=flags, colorspace=colorspace)
  244. return ImageProperties(shape=img.shape, dtype=img.dtype, is_batch=False)
  245. def metadata(
  246. self, index: int = None, exclude_applied: bool = True
  247. ) -> Dict[str, Any]:
  248. """Format-specific metadata.
  249. .. warning::
  250. OpenCV does not support reading metadata. When called, this function
  251. will raise a ``NotImplementedError``.
  252. Parameters
  253. ----------
  254. index : int
  255. This parameter has no effect.
  256. exclude_applied : bool
  257. This parameter has no effect.
  258. """
  259. warnings.warn("OpenCV does not support reading metadata.", UserWarning)
  260. return dict()