#!/usr/bin/env python """ArCamera based of ArUco markers technology.""" __author__ = "Théo de la Hogue" __credits__ = [] __copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" __license__ = "BSD" from typing import TypeVar, Tuple from dataclasses import dataclass, field import json import os import time from argaze import ArFeatures, DataStructures from argaze.ArUcoMarkers import ArUcoMarkersDictionary, ArUcoDetector, ArUcoOpticCalibrator, ArUcoScene from argaze.AreaOfInterest import AOI2DScene import cv2 import numpy ArUcoCameraType = TypeVar('ArUcoCamera', bound="ArUcoCamera") # Type definition for type annotation convenience # Define default ArUcoCamera image_paremeters values DEFAULT_ARUCOCAMERA_IMAGE_PARAMETERS = { "draw_detected_markers": { "color": (0, 255, 0), "draw_axes": { "thickness": 3 } } } @dataclass class ArUcoCamera(ArFeatures.ArCamera): """ Define an ArCamera based on ArUco marker detection. Parameters: aruco_detector: ArUco marker detector """ aruco_detector: ArUcoDetector.ArUcoDetector = field(default_factory=ArUcoDetector.ArUcoDetector) def __post_init__(self): super().__post_init__() # Check optic parameters if self.aruco_detector.optic_parameters is not None: # Optic parameters dimensions should be equal to camera frame size if self.aruco_detector.optic_parameters.dimensions != self.size: raise ArFeatures.LoadingFailed('ArUcoCamera: aruco_detector.optic_parameters.dimensions have to be equal to size.') # No optic parameters loaded else: # Create default optic parameters adapted to frame size # Note: The choice of 1000 for default focal length should be discussed... self.aruco_detector.optic_parameters = ArUcoOpticCalibrator.OpticParameters(rms=-1, dimensions=self.size, K=ArUcoOpticCalibrator.K0(focal_length=(1000., 1000.), width=self.size[0], height=self.size[1])) def __str__(self) -> str: """ Returns: String representation """ output = super().__str__() output += f'ArUcoDetector:\n{self.aruco_detector}\n' return output @classmethod def from_dict(self, aruco_camera_data: dict, working_directory: str = None) -> ArUcoCameraType: """ Load ArUcoCamera from dictionary. Parameters: aruco_camera_data: dictionary working_directory: folder path where to load files when a dictionary value is a relative filepath. """ # Load ArUco detector new_aruco_detector = ArUcoDetector.ArUcoDetector.from_dict(aruco_camera_data.pop('aruco_detector'), working_directory) # Load ArUcoScenes new_scenes = {} try: for aruco_scene_name, aruco_scene_data in aruco_camera_data.pop('scenes').items(): # Append name aruco_scene_data['name'] = aruco_scene_name # Create new aruco scene new_aruco_scene = ArUcoScene.ArUcoScene.from_dict(aruco_scene_data, working_directory) # Append new scene new_scenes[aruco_scene_name] = new_aruco_scene except KeyError: pass # Set image_parameters to default if there is not if 'image_parameters' not in aruco_camera_data.keys(): aruco_camera_data['image_parameters'] = {**ArFeatures.DEFAULT_ARFRAME_IMAGE_PARAMETERS, **DEFAULT_ARUCOCAMERA_IMAGE_PARAMETERS} # Set draw_layers to default if there is not if 'draw_layers' not in aruco_camera_data['image_parameters'].keys(): aruco_camera_data['image_parameters']['draw_layers'] = {} for layer_name, layer_data in aruco_camera_data['layers'].items(): aruco_camera_data['image_parameters']['draw_layers'][layer_name] = ArFeatures.DEFAULT_ARLAYER_DRAW_PARAMETERS # Get values of temporary ar frame created from aruco_camera_data temp_ar_frame_values = DataStructures.as_dict(ArFeatures.ArFrame.from_dict(aruco_camera_data, working_directory)) # Create new aruco camera using temporary ar frame values return ArUcoCamera(aruco_detector=new_aruco_detector, scenes=new_scenes, **temp_ar_frame_values) @classmethod def from_json(self, json_filepath: str) -> ArUcoCameraType: """ Load ArUcoCamera from .json file. Parameters: json_filepath: path to json file """ with open(json_filepath) as configuration_file: aruco_camera_data = json.load(configuration_file) working_directory = os.path.dirname(json_filepath) return ArUcoCamera.from_dict(aruco_camera_data, working_directory) def watch(self, image: numpy.array) -> Tuple[float, float, dict]: """Detect environment aruco markers from image and project scenes into camera frame. Returns: detection time: aruco marker detection time in ms. projection time: scenes projection time in ms. exception: dictionary with exception raised per scene. """ # Detect aruco markers detection_time = self.aruco_detector.detect_markers(image) # Lock camera frame exploitation super().acquire() # Store projection execution start date projection_start = time.perf_counter() # Fill camera frame background with image self.background = image # Clear former layers projection into camera frame for layer_name, layer in self.layers.items(): layer.aoi_scene = AOI2DScene.AOI2DScene() # Store exceptions for each scene exceptions = {} # Project each aoi 3d scene into camera frame for scene_name, scene in self.scenes.items(): ''' TODO: Enable aruco_aoi processing if scene.aruco_aoi: try: # Build AOI scene directly from detected ArUco marker corners self.layers[??].aoi_2d_scene |= scene.build_aruco_aoi_scene(self.aruco_detector.detected_markers) except ArFeatures.PoseEstimationFailed: pass ''' try: # Estimate scene pose from detected scene markers tvec, rmat, _ = scene.estimate_pose(self.aruco_detector.detected_markers) # Project scene into camera frame according estimated pose for layer_name, layer_projection in scene.project(tvec, rmat, self.visual_hfov, self.visual_vfov): try: self.layers[layer_name].aoi_scene |= layer_projection except KeyError: pass # Store exceptions and continue except Exception as e: exceptions[scene_name] = e # Assess projection time in ms projection_time = (time.perf_counter() - projection_start) * 1e3 # Unlock camera frame exploitation super().release() # Return detection time, projection time and exceptions return detection_time, projection_time, exceptions def __image(self, draw_detected_markers: dict = None, draw_scenes: dict = None, draw_optic_parameters_grid: dict = None, **kwargs: dict) -> numpy.array: """Get frame image with ArUco detection visualisation. Parameters: draw_detected_markers: ArucoMarker.draw parameters (if None, no marker drawn) draw_scenes: ArUcoScene.draw parameters (if None, no scene drawn) draw_optic_parameters_grid: OpticParameter.draw parameters (if None, no grid drawn) kwargs: ArCamera.image parameters """ # Get camera frame image # Note: don't lock/unlock camera frame here as super().image manage it. image = super().image(**kwargs) # Draw optic parameters grid if required if draw_optic_parameters_grid is not None: self.aruco_detector.optic_parameters.draw(image, **draw_optic_parameters_grid) # Draw scenes if required if draw_scenes is not None: for scene_name, draw_scenes_parameters in draw_scenes.items(): self.scenes[scene_name].draw(image, **draw_scenes_parameters) # Draw detected markers if required if draw_detected_markers is not None: self.aruco_detector.draw_detected_markers(image, draw_detected_markers) return image def image(self, **kwargs: dict) -> numpy.array: """ Get frame image. Parameters: kwargs: ArUcoCamera.__image parameters """ # Use image_parameters attribute if no kwargs if kwargs: return self.__image(**kwargs) return self.__image(**self.image_parameters)