# coding: utf-8
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional, Tuple
import logging
import numpy as np
from .overlay_object import Overlay
[docs]
@dataclass
class Box(Overlay):
"""This class represents a box to be drawn on top of the images of a
:class:`~crappy.tool.camera_config.CameraConfig` window or
:class:`~crappy.blocks.camera_processes.Displayer` Process of a
:class:`~crappy.blocks.Camera` Block.
It is a child of :class:`~crappy.tool.camera_config.config_tools.Overlay`.
It can represent either the box drawn when selecting a region, or the
bounding box of a tracked area.
.. versionadded:: 2.0.0
"""
x_start: Optional[int] = None
x_end: Optional[int] = None
y_start: Optional[int] = None
y_end: Optional[int] = None
x_disp: Optional[float] = None
y_disp: Optional[float] = None
x_centroid: Optional[float] = None
y_centroid: Optional[float] = None
[docs]
def __post_init__(self) -> None:
"""Needed to have the :obj:`~logging.Logger` of the parent class properly
initialized."""
super().__init__()
def __str__(self) -> str:
"""The string representation of this class, only for debugging."""
return (f"Box with coordinates ({self.x_start}, {self.y_start}), "
f"({self.x_end}, {self.y_end})")
[docs]
def draw(self, img: np.ndarray) -> None:
"""Draws the Box on top of the given image, and returns the modified image.
The thickness of the drawn lines adapts to the size of the image, so that
the lines are always visible even when casting the image to a smaller
format.
"""
# First, checking if all points are defined
if self.no_points():
self.log(logging.DEBUG, f"Cannot draw {self}, not all points are "
f"defined !")
# Getting the thickness of the lines to draw
x_top, x_bottom, y_left, y_right = self.sorted()
max_fact = max(img.shape[0] // 480, img.shape[1] // 640, 1)
# Drawing the lines on top of the image
try:
for line in (line for i in range(max_fact + 1) for line in
((self.y_start + i, slice(x_top, x_bottom)),
(self.y_end - i, slice(x_top, x_bottom)),
(slice(y_left, y_right), x_top + i),
(slice(y_left, y_right), x_bottom - i))):
img[line] = 255 * int(np.mean(img[line]) < 128)
self.log(logging.DEBUG, f"Drew {self} on top of the image to display")
# If anything goes wrong, aborting
except (Exception,) as exc:
self._logger.exception("Encountered exception while drawing boxes, "
"ignoring", exc_info=exc)
def update(self, box: Box) -> None:
"""Changes the coordinates of the box to those of another box."""
self.log(logging.DEBUG, f"Updating {self} to {box}")
self.x_start = box.x_start
self.y_start = box.y_start
self.x_end = box.x_end
self.y_end = box.y_end
self.x_disp = box.x_disp
self.y_disp = box.y_disp
self.x_centroid = box.x_centroid
self.y_centroid = box.y_centroid
[docs]
def no_points(self) -> bool:
"""Returns whether all four sides of the box are defined or not."""
return any(point is None for point in (self.x_start, self.x_end,
self.y_start, self.y_end))
[docs]
def reset(self) -> None:
"""Resets the sides to :obj:`None`."""
self.log(logging.DEBUG, f"Resetting {self}")
self.x_start = None
self.x_end = None
self.y_start = None
self.y_end = None
self.x_centroid = None
self.y_centroid = None
[docs]
def sorted(self) -> Tuple[int, int, int, int]:
"""Returns the four coordinates but sorted in the order : min x, max x,
min y, max y."""
if self.no_points():
self.log(logging.WARNING, f"Trying to sort the Box, but some of its "
f"coordinates are undefined !")
raise ValueError("Cannot sort, some values are None !")
x_top = min(self.x_start, self.x_end)
x_bottom = max(self.x_start, self.x_end)
y_left = min(self.y_start, self.y_end)
y_right = max(self.y_start, self.y_end)
self.log(logging.DEBUG, f"Sorted {self}, returning ({x_top}, {x_bottom}, "
f"{y_left}, {y_right})")
return x_top, x_bottom, y_left, y_right