Source code for Structures.Image

from __future__ import annotations

import urllib.request
from pathlib import Path
from typing import Dict, Optional, Union

import cv2
import numpy as np

from toolbox.utils.utils import is_url


[docs]class Image: """Structure to store an image. Allow to load an image from a local file or an URL. Attributes: path (Union[str, Path]): Path or URL to an image. id (str): Id of a ngsi-ld image entity. Overloaded operators: - __str__ - __eq__ - __repr__ - __iter__ """ def __init__(self, path: Union[str, Path] = "", image: Optional[np.ndarray] = None, width: Optional[int] = None, height: Optional[int] = None, id: str = ""): """Create an Image object. Args: path (Union[str, Path], optional): Path or URL to an image. Defaults to "". image (Optional[np.ndarray], optional): A np.ndarray image. Defaults to None. width (Optional[int], optional): Width of the image. Automatically obtained from the image if supplied. Defaults to None. height (Optional[int], optional): Height of the image. Automatically obtained from the image if supplied. Defaults to None. id (str, optional): Id of an ngsi-ld image entity. Defaults to "". """ self.id = id if image is not None: self._height, self._width = image.shape[:2] else: self._width = width self._height = height self._image = image self.path = self._parse_path(path) @property def image(self) -> np.ndarray: """Return the image as a numpy array, load the image if it's necessary. """ if self._image is None: self._load_image() return self._image @property def height(self) -> int: """Return the height of the image, load the image if it's necessary. """ if self._height is None: self._load_image() return self._height @property def width(self) -> int: """Return the width of the image, load the image if it's necessary. """ if self._width is None: self._load_image() return self._width
[docs] def load_image(self): """Manually load the image into memory. """ if self._image is None: self._load_image()
[docs] def save_image(self, path: Optional[Union[str, Path]] = None): """Save the image to a file. Args: path (Optional[Union[str, Path]]): Optional output file path. If None, the ``self.path`` will be used """ path = self.path if path is None else path path = Path(path) try: cv2.imwrite(str(path), self.image) except Exception as e: raise OSError(f"Can not write image to {path}") from e
def _parse_path(self, path: Union[str, Path]) -> Union[str, Path]: """Return a Path object if the path is a local file or a string if the path is a URL. """ if isinstance(path, Path): return path if not is_url(path): return Path(path) return path def _load_image(self): """Load an image form disk or an URL. Raises: FileNotFoundError IsADirectoryError ValueError """ if isinstance(self.path, Path): self._load_path(self.path) else: self._load_url(self.path) def _load_path(self, path: Path) -> None: """Load the image from a local path. Args: path (Path): Path to an image. Returns: None Raises: FileNotFoundError IsADirectoryError ValueError """ if not path.exists(): raise FileNotFoundError(path) if path.is_dir(): raise IsADirectoryError(path) self._image = cv2.imread(str(path)) if self._image is None: raise ValueError(f"Error reading image from {path}") self._height, self._width = self._image.shape[:2] def _load_url(self, url: str) -> None: """Load the image from an URL. Args: url (str): URL of an image. Returns: None Raises: ValueError """ try: with urllib.request.urlopen(url, timeout=10) as req: arr = np.asarray(bytearray(req.read()), dtype=np.uint8) self._image = cv2.imdecode(arr, cv2.IMREAD_COLOR) if self._image is None: raise ValueError(f"Error reading image from {url}") self._height, self._width = self._image.shape[:2] except urllib.error.HTTPError as e: raise ValueError(f"Error reading image from {url}") from e
[docs] @staticmethod def from_url(url: str) -> Image: """Create an Image object from an URL. Args: url (str): URL to an image. Returns: Image. """ image = Image("") image.path = url image._load_url(url) return image
[docs] @staticmethod def from_path(path: Path) -> Image: """Create an Image object from a Path. Args: path (Path): Path to an image file. Returns: Image. """ image = Image("") image.path = Path(path) image._load_path(image.path) return image
def __str__(self): return f"Image: {self.path} ({self.width} X {self.height})" def __repr__(self) -> str: return f"{self.__class__.__name__}(path={self.path},"\ f"width={self.width},height={self.height},id=" \ f"'{self.id}'"
[docs] def serialize(self) -> Dict[str, int]: """Serialize to a basic Python datatype. Returns: Dict[str, int] """ return { "path": str(self.path), "width": self.width, "height": self.height, "id": self.id }
[docs] @staticmethod def deserialize(value: Dict[str, int]) -> Image: """Deserialize value. Args: value (Dict[str, int]) Returns: Image """ return Image( path=value["path"], width=value["width"], height=value["height"], id=value["id"] )
def __eq__(self, other: Image) -> bool: if not isinstance(other, Image): return False if self._image is not None and other._image is not None: if not np.array_equal(self._image, other._image): return False return self.serialize() == other.serialize() # Pydantic methods def __iter__(self): d = self.serialize() yield from d.items() @classmethod def __get_validators__(cls): yield cls.validate
[docs] @classmethod def validate(cls, v): if isinstance(v, Image): return v try: return Image.deserialize(v) except: raise TypeError(f"Error parsing {v} ({type(v)}) to {cls}")
@classmethod def __modify_schema__(cls, field_schema): field_schema.update(example=Image(width=1920, height=1080).serialize())