| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- """Read/Write images using OpenCV.
- Backend Library: `OpenCV <https://opencv.org/>`_
- This plugin wraps OpenCV (also known as ``cv2``), a popular image processing
- library. Currently, it exposes OpenCVs image reading capability (no video or GIF
- support yet); however, this may be added in future releases.
- Methods
- -------
- .. note::
- Check the respective function for a list of supported kwargs and their
- documentation.
- .. autosummary::
- :toctree:
- OpenCVPlugin.read
- OpenCVPlugin.iter
- OpenCVPlugin.write
- OpenCVPlugin.properties
- OpenCVPlugin.metadata
- Pixel Formats (Colorspaces)
- ---------------------------
- OpenCV is known to process images in BGR; however, most of the python ecosystem
- (in particular matplotlib and other pydata libraries) use the RGB. As such,
- images are converted to RGB, RGBA, or grayscale (where applicable) by default.
- """
- import warnings
- from pathlib import Path
- from typing import Any, Dict, List, Optional, Union
- import cv2
- import numpy as np
- from ..core import Request
- from ..core.request import URI_BYTES, InitializationError, IOMode
- from ..core.v3_plugin_api import ImageProperties, PluginV3
- from ..typing import ArrayLike
- class OpenCVPlugin(PluginV3):
- def __init__(self, request: Request) -> None:
- super().__init__(request)
- self.file_handle = request.get_local_filename()
- if request._uri_type is URI_BYTES:
- self.filename = "<bytes>"
- else:
- self.filename = request.raw_uri
- mode = request.mode.io_mode
- if mode == IOMode.read and not cv2.haveImageReader(self.file_handle):
- raise InitializationError(f"OpenCV can't read `{self.filename}`.")
- elif mode == IOMode.write and not cv2.haveImageWriter(self.file_handle):
- raise InitializationError(f"OpenCV can't write to `{self.filename}`.")
- def read(
- self,
- *,
- index: int = None,
- colorspace: Union[int, str] = None,
- flags: int = cv2.IMREAD_COLOR,
- ) -> np.ndarray:
- """Read an image from the ImageResource.
- Parameters
- ----------
- index : int, Ellipsis
- If int, read the index-th image from the ImageResource. If ``...``,
- read all images from the ImageResource and stack them along a new,
- prepended, batch dimension. If None (default), use ``index=0`` if
- the image contains exactly one image and ``index=...`` otherwise.
- colorspace : str, int
- The colorspace to convert into after loading and before returning
- the image. If None (default) keep grayscale images as is, convert
- images with an alpha channel to ``RGBA`` and all other images to
- ``RGB``. If int, interpret ``colorspace`` as one of OpenCVs
- `conversion flags
- <https://docs.opencv.org/4.x/d8/d01/group__imgproc__color__conversions.html>`_
- and use it for conversion. If str, convert the image into the given
- colorspace. Possible string values are: ``"RGB"``, ``"BGR"``,
- ``"RGBA"``, ``"BGRA"``, ``"GRAY"``, ``"HSV"``, or ``"LAB"``.
- flags : int
- The OpenCV flag(s) to pass to the reader. Refer to the `OpenCV docs
- <https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
- for details.
- Returns
- -------
- ndimage : np.ndarray
- The decoded image as a numpy array.
- """
- if index is None:
- n_images = cv2.imcount(self.file_handle, flags)
- index = 0 if n_images == 1 else ...
- if index is ...:
- retval, img = cv2.imreadmulti(self.file_handle, flags=flags)
- is_batch = True
- else:
- retval, img = cv2.imreadmulti(self.file_handle, index, 1, flags=flags)
- is_batch = False
- if retval is False:
- raise ValueError(f"Could not read index `{index}` from `{self.filename}`.")
- if img[0].ndim == 2:
- in_colorspace = "GRAY"
- out_colorspace = colorspace or "GRAY"
- elif img[0].shape[-1] == 4:
- in_colorspace = "BGRA"
- out_colorspace = colorspace or "RGBA"
- else:
- in_colorspace = "BGR"
- out_colorspace = colorspace or "RGB"
- if isinstance(colorspace, int):
- cvt_space = colorspace
- elif in_colorspace == out_colorspace.upper():
- cvt_space = None
- else:
- out_colorspace = out_colorspace.upper()
- cvt_space = getattr(cv2, f"COLOR_{in_colorspace}2{out_colorspace}")
- if cvt_space is not None:
- img = np.stack([cv2.cvtColor(x, cvt_space) for x in img])
- else:
- img = np.stack(img)
- return img if is_batch else img[0]
- def iter(
- self,
- colorspace: Union[int, str] = None,
- flags: int = cv2.IMREAD_COLOR,
- ) -> np.ndarray:
- """Yield images from the ImageResource.
- Parameters
- ----------
- colorspace : str, int
- The colorspace to convert into after loading and before returning
- the image. If None (default) keep grayscale images as is, convert
- images with an alpha channel to ``RGBA`` and all other images to
- ``RGB``. If int, interpret ``colorspace`` as one of OpenCVs
- `conversion flags
- <https://docs.opencv.org/4.x/d8/d01/group__imgproc__color__conversions.html>`_
- and use it for conversion. If str, convert the image into the given
- colorspace. Possible string values are: ``"RGB"``, ``"BGR"``,
- ``"RGBA"``, ``"BGRA"``, ``"GRAY"``, ``"HSV"``, or ``"LAB"``.
- flags : int
- The OpenCV flag(s) to pass to the reader. Refer to the `OpenCV docs
- <https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
- for details.
- Yields
- ------
- ndimage : np.ndarray
- The decoded image as a numpy array.
- """
- for idx in range(cv2.imcount(self.file_handle)):
- yield self.read(index=idx, flags=flags, colorspace=colorspace)
- def write(
- self,
- ndimage: Union[ArrayLike, List[ArrayLike]],
- is_batch: bool = False,
- params: List[int] = None,
- ) -> Optional[bytes]:
- """Save an ndimage in the ImageResource.
- Parameters
- ----------
- ndimage : ArrayLike, List[ArrayLike]
- The image data that will be written to the file. It is either a
- single image, a batch of images, or a list of images.
- is_batch : bool
- If True, the provided ndimage is a batch of images. If False (default), the
- provided ndimage is a single image. If the provided ndimage is a list of images,
- this parameter has no effect.
- params : List[int]
- A list of parameters that will be passed to OpenCVs imwrite or
- imwritemulti functions. Possible values are documented in the
- `OpenCV documentation
- <https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#gabbc7ef1aa2edfaa87772f1202d67e0ce>`_.
- Returns
- -------
- encoded_image : bytes, None
- If the ImageResource is ``"<bytes>"`` the call to write returns the
- encoded image as a bytes string. Otherwise it returns None.
- """
- if isinstance(ndimage, list):
- ndimage = np.stack(ndimage, axis=0)
- elif not is_batch:
- ndimage = ndimage[None, ...]
- if ndimage[0].ndim == 2:
- n_channels = 1
- else:
- n_channels = ndimage[0].shape[-1]
- if n_channels == 1:
- ndimage_cv2 = [x for x in ndimage]
- elif n_channels == 4:
- ndimage_cv2 = [cv2.cvtColor(x, cv2.COLOR_RGBA2BGRA) for x in ndimage]
- else:
- ndimage_cv2 = [cv2.cvtColor(x, cv2.COLOR_RGB2BGR) for x in ndimage]
- retval = cv2.imwritemulti(self.file_handle, ndimage_cv2, params)
- if retval is False:
- # not sure what scenario would trigger this, but
- # it can occur theoretically.
- raise IOError("OpenCV failed to write.") # pragma: no cover
- if self.request._uri_type == URI_BYTES:
- return Path(self.file_handle).read_bytes()
- def properties(
- self,
- index: int = None,
- colorspace: Union[int, str] = None,
- flags: int = cv2.IMREAD_COLOR,
- ) -> ImageProperties:
- """Standardized image metadata.
- Parameters
- ----------
- index : int, Ellipsis
- If int, get the properties of the index-th image in the
- ImageResource. If ``...``, get the properties of the image stack
- that contains all images. If None (default), use ``index=0`` if the
- image contains exactly one image and ``index=...`` otherwise.
- colorspace : str, int
- The colorspace to convert into after loading and before returning
- the image. If None (default) keep grayscale images as is, convert
- images with an alpha channel to ``RGBA`` and all other images to
- ``RGB``. If int, interpret ``colorspace`` as one of OpenCVs
- `conversion flags
- <https://docs.opencv.org/4.x/d8/d01/group__imgproc__color__conversions.html>`_
- and use it for conversion. If str, convert the image into the given
- colorspace. Possible string values are: ``"RGB"``, ``"BGR"``,
- ``"RGBA"``, ``"BGRA"``, ``"GRAY"``, ``"HSV"``, or ``"LAB"``.
- flags : int
- The OpenCV flag(s) to pass to the reader. Refer to the `OpenCV docs
- <https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56>`_
- for details.
- Returns
- -------
- props : ImageProperties
- A dataclass filled with standardized image metadata.
- Notes
- -----
- Reading properties with OpenCV involves decoding pixel data, because
- OpenCV doesn't provide a direct way to access metadata.
- """
- if index is None:
- n_images = cv2.imcount(self.file_handle, flags)
- is_batch = n_images > 1
- elif index is Ellipsis:
- n_images = cv2.imcount(self.file_handle, flags)
- is_batch = True
- else:
- is_batch = False
- # unfortunately, OpenCV doesn't allow reading shape without reading pixel data
- if is_batch:
- img = self.read(index=0, flags=flags, colorspace=colorspace)
- return ImageProperties(
- shape=(n_images, *img.shape),
- dtype=img.dtype,
- n_images=n_images,
- is_batch=True,
- )
- img = self.read(index=index, flags=flags, colorspace=colorspace)
- return ImageProperties(shape=img.shape, dtype=img.dtype, is_batch=False)
- def metadata(
- self, index: int = None, exclude_applied: bool = True
- ) -> Dict[str, Any]:
- """Format-specific metadata.
- .. warning::
- OpenCV does not support reading metadata. When called, this function
- will raise a ``NotImplementedError``.
- Parameters
- ----------
- index : int
- This parameter has no effect.
- exclude_applied : bool
- This parameter has no effect.
- """
- warnings.warn("OpenCV does not support reading metadata.", UserWarning)
- return dict()
|