Source code for arguslib.camera.direct_camera

from .undistorted_camera import UndistortedCamera
from .camera import Camera
import numpy as np
from typing_extensions import override
from ..misc.geo import haversine
from ..misc.plotting import plot_range_rings


[docs] class DirectCamera( Camera ): # FIXME: annotation (range rings) is broken for direct camera but not direct undistorted """A Camera subclass that renders annotations directly onto the image array. This class is optimized for performance, especially for creating videos. Instead of using Matplotlib for plotting, it overrides the `show` and `annotate_positions` methods to use OpenCV functions (`cv2.line`, `cv2.putText`) to draw directly on the image data. As a result, its `show` and `annotate_positions` methods do not accept or return Matplotlib `Axes` objects. The final image can be accessed via the `.image` property or `.to_image_array()` method. """
[docs] def get_data_time(self, *args, **kwargs): ret_val = super().get_data_time(*args, **kwargs) self.data_loader.image = self.data_loader.image[:, :, ::-1] if isinstance(ret_val, tuple): return self.data_loader.image, *ret_val[1:] else: return self.data_loader.image
[docs] @override def annotate_positions( self, positions, dt, ax, *args, plotting_method=None, max_range_km=90, **kwargs ): from matplotlib.colors import to_rgb import cv2 if ax is not None: raise ValueError("Direct instruments should have no axes!") # TODO: this should take dt into account, mostly because the calibration may change for the same camera at different times... lats = [p.lat for p in positions] lons = [p.lon for p in positions] dists = np.array( [ haversine(self.position.lon, self.position.lat, lon, lat) for lon, lat in zip(lons, lats) ] ) behind_camera = np.array([self.target_iead(p)[0] < 0 for p in positions]) if (dists[~np.isnan(dists)] > max_range_km).all(): return pl_track = np.array([self.target_pix(p) for p in positions]) color = (255, 0, 0) if (kw_col := kwargs.get("color")) is not None: color = tuple((np.array(to_rgb(kw_col)) * 255).astype(int).tolist()) elif (kw_col := kwargs.get("c")) is not None: color = tuple((np.array(to_rgb(kw_col)) * 255).astype(int).tolist()) linewidth = 3 if (kw_linewidth := kwargs.get("kw_linewidth")) is not None: linewidth *= kw_linewidth if (kw_linewidth := kwargs.get("linewidth")) is not None: linewidth *= kw_linewidth if plotting_method is None: for i in range(pl_track.shape[0] - 1): if behind_camera[i] or behind_camera[i + 1]: continue if np.isnan(pl_track[i : i + 2]).any(): continue xy0 = tuple(pl_track[i][0:2].astype(int).tolist()) xy1 = tuple(pl_track[i + 1][0:2].astype(int).tolist()) # print(xy0, xy1) cv2.line( self.data_loader.image, xy0, xy1, color, thickness=int(linewidth) ) else: raise ValueError("Online line plotting for DirectCameras") return ax
@override def _show( self, dt, ax=None, fail_if_no_data=True, imshow_kw={}, brightness_adjust=1.0, **kwargs, ): """Show the nearest possible timestamp. Limited by camera time resolution (5s)""" if ax is not None: raise ValueError("No axes for DirectCameras") if imshow_kw != {}: raise ValueError("No imshow for DirectCameras") defaults = {"theta_behaviour": "bearing", "lr_flip": True} if "theta_behaviour" in kwargs and "lr_flip" not in kwargs: # if theta_behaviour is set, assume lr_flip should be false, unless it's bearing if kwargs["theta_behaviour"] != "bearing": defaults["lr_flip"] = False if "lr_flip" in kwargs and "theta_behaviour" not in kwargs: # if lr_flip is set, but we are inferring theta_behaviour, assume it's bearing if flipped, ordinal aligned if not flipped if kwargs["lr_flip"]: defaults["theta_behaviour"] = "bearing" else: defaults["theta_behaviour"] = "unflipped_ordinal_aligned" kwargs = defaults | kwargs self.direct_lr_flip = kwargs.pop("lr_flip") self.direct_theta_behaviour = kwargs.pop("theta_behaviour") try: img, timestamp = self.get_data_time(dt, return_timestamp=True) # ax.get_figure().timestamp = timestamp except FileNotFoundError as e: if fail_if_no_data: raise e else: return ax # new_vmin = np.round(brightness_adjust * 255) self.data_loader.image = np.clip( np.uint16(img) * brightness_adjust, 0, 255 ).astype(np.uint8) # img[img <= new_vmin] = new_vmin # img = np.round(255 * (img.astype(float) - new_vmin) / (255 - new_vmin)).astype( # int # ) plot_range_rings(self, self, dt, ax=ax) # ax.imshow(img[:, :, ::-1], origin="upper", **imshow_kw) return None # No ax for image @property def image(self): from PIL import Image return Image.fromarray( self.to_image_array() ) # Watch out! this image looks like the mp4 files. vertical flip to get it as matplotlib might plot.
[docs] def to_image_array(self, time=True): import cv2 slicer = ( slice(None, None, -1), slice(None, None, None if self.direct_lr_flip else -1), slice(None, None, None), ) im = self.data_loader.image if time: cv2.putText( im, self.data_loader.current_image_time.isoformat(timespec="seconds"), (50, 2900), 1, 10, (255, 200, 200), 20, bottomLeftOrigin=True, ) return im[slicer[0], slicer[1], slicer[2]]
[docs] class DirectUndistortedCamera(UndistortedCamera, DirectCamera): pass