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