From 4d0de7c804914a55977635ec6bc46beb0cf7808a Mon Sep 17 00:00:00 2001 From: Théo de la Hogue Date: Wed, 17 Apr 2024 13:32:51 +0200 Subject: Renaming ArUcoMarkers into ArUcoMarker --- .../aruco_detector_configuration.md | 6 +- .../optic_parameters_calibration.md | 18 +- .../advanced_topics/scripting.md | 12 +- .../aruco_marker_pipeline/aoi_3d_frame.md | 20 +- .../aruco_marker_pipeline/aoi_3d_projection.md | 24 +- .../aruco_markers_description.md | 10 +- .../configuration_and_execution.md | 22 +- .../aruco_marker_pipeline/introduction.md | 10 +- .../aruco_marker_pipeline/pose_estimation.md | 18 +- docs/user_guide/utils/ready-made_scripts.md | 2 +- src/argaze.test/ArUcoMarker/ArUcoBoard.py | 50 +++ src/argaze.test/ArUcoMarker/ArUcoCamera.py | 80 ++++ src/argaze.test/ArUcoMarker/ArUcoDetector.py | 149 +++++++ src/argaze.test/ArUcoMarker/ArUcoMarker.py | 40 ++ .../ArUcoMarker/ArUcoMarkerDictionary.py | 50 +++ .../ArUcoMarker/ArUcoOpticCalibrator.py | 61 +++ src/argaze.test/ArUcoMarker/ArUcoScene.py | 227 ++++++++++ src/argaze.test/ArUcoMarker/__init__.py | 0 src/argaze.test/ArUcoMarker/utils/aoi_3d.obj | 7 + .../ArUcoMarker/utils/aruco_camera.json | 98 +++++ src/argaze.test/ArUcoMarker/utils/detector.json | 42 ++ .../ArUcoMarker/utils/detector_parameters.json | 5 + .../ArUcoMarker/utils/full_hd_board.png | Bin 0 -> 18475 bytes .../ArUcoMarker/utils/full_hd_marker.png | Bin 0 -> 116210 bytes .../ArUcoMarker/utils/optic_parameters.json | 31 ++ src/argaze.test/ArUcoMarker/utils/scene.json | 20 + src/argaze.test/ArUcoMarker/utils/scene.obj | 22 + src/argaze.test/ArUcoMarkers/ArUcoBoard.py | 50 --- src/argaze.test/ArUcoMarkers/ArUcoCamera.py | 80 ---- src/argaze.test/ArUcoMarkers/ArUcoDetector.py | 149 ------- src/argaze.test/ArUcoMarkers/ArUcoMarker.py | 40 -- .../ArUcoMarkers/ArUcoMarkersDictionary.py | 50 --- .../ArUcoMarkers/ArUcoOpticCalibrator.py | 61 --- src/argaze.test/ArUcoMarkers/ArUcoScene.py | 227 ---------- src/argaze.test/ArUcoMarkers/__init__.py | 0 src/argaze.test/ArUcoMarkers/utils/aoi_3d.obj | 7 - .../ArUcoMarkers/utils/aruco_camera.json | 98 ----- src/argaze.test/ArUcoMarkers/utils/detector.json | 42 -- .../ArUcoMarkers/utils/detector_parameters.json | 5 - .../ArUcoMarkers/utils/full_hd_board.png | Bin 18475 -> 0 bytes .../ArUcoMarkers/utils/full_hd_marker.png | Bin 116210 -> 0 bytes .../ArUcoMarkers/utils/optic_parameters.json | 31 -- src/argaze.test/ArUcoMarkers/utils/scene.json | 20 - src/argaze.test/ArUcoMarkers/utils/scene.obj | 22 - src/argaze/ArUcoMarker/ArUcoBoard.py | 82 ++++ src/argaze/ArUcoMarker/ArUcoCamera.py | 238 +++++++++++ src/argaze/ArUcoMarker/ArUcoDetector.py | 364 ++++++++++++++++ src/argaze/ArUcoMarker/ArUcoMarker.py | 106 +++++ src/argaze/ArUcoMarker/ArUcoMarkerDictionary.py | 161 +++++++ src/argaze/ArUcoMarker/ArUcoMarkerGroup.py | 476 +++++++++++++++++++++ src/argaze/ArUcoMarker/ArUcoOpticCalibrator.py | 162 +++++++ src/argaze/ArUcoMarker/ArUcoScene.py | 126 ++++++ src/argaze/ArUcoMarker/__init__.py | 6 + .../utils/A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf | Bin 0 -> 97542 bytes .../utils/A4_DICT_APRILTAG_16h5_5cm_0-7.pdf | Bin 0 -> 37670 bytes src/argaze/ArUcoMarker/utils/__init__.py | 5 + src/argaze/ArUcoMarkers/ArUcoBoard.py | 82 ---- src/argaze/ArUcoMarkers/ArUcoCamera.py | 238 ----------- src/argaze/ArUcoMarkers/ArUcoDetector.py | 364 ---------------- src/argaze/ArUcoMarkers/ArUcoMarker.py | 106 ----- src/argaze/ArUcoMarkers/ArUcoMarkersDictionary.py | 161 ------- src/argaze/ArUcoMarkers/ArUcoMarkersGroup.py | 476 --------------------- src/argaze/ArUcoMarkers/ArUcoOpticCalibrator.py | 162 ------- src/argaze/ArUcoMarkers/ArUcoScene.py | 126 ------ src/argaze/ArUcoMarkers/__init__.py | 6 - .../utils/A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf | Bin 97542 -> 0 bytes .../utils/A4_DICT_APRILTAG_16h5_5cm_0-7.pdf | Bin 37670 -> 0 bytes src/argaze/ArUcoMarkers/utils/__init__.py | 5 - src/argaze/__init__.py | 2 +- src/argaze/utils/aruco_markers_group_export.py | 234 ---------- src/argaze/utils/demo/aruco_markers_pipeline.json | 2 +- 71 files changed, 2681 insertions(+), 2915 deletions(-) create mode 100644 src/argaze.test/ArUcoMarker/ArUcoBoard.py create mode 100644 src/argaze.test/ArUcoMarker/ArUcoCamera.py create mode 100644 src/argaze.test/ArUcoMarker/ArUcoDetector.py create mode 100644 src/argaze.test/ArUcoMarker/ArUcoMarker.py create mode 100644 src/argaze.test/ArUcoMarker/ArUcoMarkerDictionary.py create mode 100644 src/argaze.test/ArUcoMarker/ArUcoOpticCalibrator.py create mode 100644 src/argaze.test/ArUcoMarker/ArUcoScene.py create mode 100644 src/argaze.test/ArUcoMarker/__init__.py create mode 100644 src/argaze.test/ArUcoMarker/utils/aoi_3d.obj create mode 100644 src/argaze.test/ArUcoMarker/utils/aruco_camera.json create mode 100644 src/argaze.test/ArUcoMarker/utils/detector.json create mode 100644 src/argaze.test/ArUcoMarker/utils/detector_parameters.json create mode 100644 src/argaze.test/ArUcoMarker/utils/full_hd_board.png create mode 100644 src/argaze.test/ArUcoMarker/utils/full_hd_marker.png create mode 100644 src/argaze.test/ArUcoMarker/utils/optic_parameters.json create mode 100644 src/argaze.test/ArUcoMarker/utils/scene.json create mode 100644 src/argaze.test/ArUcoMarker/utils/scene.obj delete mode 100644 src/argaze.test/ArUcoMarkers/ArUcoBoard.py delete mode 100644 src/argaze.test/ArUcoMarkers/ArUcoCamera.py delete mode 100644 src/argaze.test/ArUcoMarkers/ArUcoDetector.py delete mode 100644 src/argaze.test/ArUcoMarkers/ArUcoMarker.py delete mode 100644 src/argaze.test/ArUcoMarkers/ArUcoMarkersDictionary.py delete mode 100644 src/argaze.test/ArUcoMarkers/ArUcoOpticCalibrator.py delete mode 100644 src/argaze.test/ArUcoMarkers/ArUcoScene.py delete mode 100644 src/argaze.test/ArUcoMarkers/__init__.py delete mode 100644 src/argaze.test/ArUcoMarkers/utils/aoi_3d.obj delete mode 100644 src/argaze.test/ArUcoMarkers/utils/aruco_camera.json delete mode 100644 src/argaze.test/ArUcoMarkers/utils/detector.json delete mode 100644 src/argaze.test/ArUcoMarkers/utils/detector_parameters.json delete mode 100644 src/argaze.test/ArUcoMarkers/utils/full_hd_board.png delete mode 100644 src/argaze.test/ArUcoMarkers/utils/full_hd_marker.png delete mode 100644 src/argaze.test/ArUcoMarkers/utils/optic_parameters.json delete mode 100644 src/argaze.test/ArUcoMarkers/utils/scene.json delete mode 100644 src/argaze.test/ArUcoMarkers/utils/scene.obj create mode 100644 src/argaze/ArUcoMarker/ArUcoBoard.py create mode 100644 src/argaze/ArUcoMarker/ArUcoCamera.py create mode 100644 src/argaze/ArUcoMarker/ArUcoDetector.py create mode 100644 src/argaze/ArUcoMarker/ArUcoMarker.py create mode 100644 src/argaze/ArUcoMarker/ArUcoMarkerDictionary.py create mode 100644 src/argaze/ArUcoMarker/ArUcoMarkerGroup.py create mode 100644 src/argaze/ArUcoMarker/ArUcoOpticCalibrator.py create mode 100644 src/argaze/ArUcoMarker/ArUcoScene.py create mode 100644 src/argaze/ArUcoMarker/__init__.py create mode 100644 src/argaze/ArUcoMarker/utils/A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf create mode 100644 src/argaze/ArUcoMarker/utils/A4_DICT_APRILTAG_16h5_5cm_0-7.pdf create mode 100644 src/argaze/ArUcoMarker/utils/__init__.py delete mode 100644 src/argaze/ArUcoMarkers/ArUcoBoard.py delete mode 100644 src/argaze/ArUcoMarkers/ArUcoCamera.py delete mode 100644 src/argaze/ArUcoMarkers/ArUcoDetector.py delete mode 100644 src/argaze/ArUcoMarkers/ArUcoMarker.py delete mode 100644 src/argaze/ArUcoMarkers/ArUcoMarkersDictionary.py delete mode 100644 src/argaze/ArUcoMarkers/ArUcoMarkersGroup.py delete mode 100644 src/argaze/ArUcoMarkers/ArUcoOpticCalibrator.py delete mode 100644 src/argaze/ArUcoMarkers/ArUcoScene.py delete mode 100644 src/argaze/ArUcoMarkers/__init__.py delete mode 100644 src/argaze/ArUcoMarkers/utils/A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf delete mode 100644 src/argaze/ArUcoMarkers/utils/A4_DICT_APRILTAG_16h5_5cm_0-7.pdf delete mode 100644 src/argaze/ArUcoMarkers/utils/__init__.py delete mode 100644 src/argaze/utils/aruco_markers_group_export.py diff --git a/docs/user_guide/aruco_marker_pipeline/advanced_topics/aruco_detector_configuration.md b/docs/user_guide/aruco_marker_pipeline/advanced_topics/aruco_detector_configuration.md index 7d666ba..53c137a 100644 --- a/docs/user_guide/aruco_marker_pipeline/advanced_topics/aruco_detector_configuration.md +++ b/docs/user_guide/aruco_marker_pipeline/advanced_topics/aruco_detector_configuration.md @@ -5,13 +5,13 @@ As explain in [OpenCV ArUco documentation](https://docs.opencv.org/4.x/d1/dcd/st ## Load ArUcoDetector parameters -[ArUcoCamera.detector.parameters](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoDetector.Parameters) can be loaded thanks to a dedicated JSON entry. +[ArUcoCamera.detector.parameters](../../../argaze.md/#argaze.ArUcoMarker.ArUcoDetector.Parameters) can be loaded thanks to a dedicated JSON entry. -Here is an extract from the JSON [ArUcoCamera](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) configuration file with ArUco detector parameters: +Here is an extract from the JSON [ArUcoCamera](../../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) configuration file with ArUco detector parameters: ```json { - "argaze.ArUcoMarkers.ArUcoCamera.ArUcoCamera": { + "argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera": { "name": "My FullHD camera", "size": [1920, 1080], "aruco_detector": { diff --git a/docs/user_guide/aruco_marker_pipeline/advanced_topics/optic_parameters_calibration.md b/docs/user_guide/aruco_marker_pipeline/advanced_topics/optic_parameters_calibration.md index 54d0c94..7bbfc63 100644 --- a/docs/user_guide/aruco_marker_pipeline/advanced_topics/optic_parameters_calibration.md +++ b/docs/user_guide/aruco_marker_pipeline/advanced_topics/optic_parameters_calibration.md @@ -7,13 +7,13 @@ A camera device have to be calibrated to compensate its optical distorsion. ## Print calibration board -The first step to calibrate a camera is to create an [ArUcoBoard](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoBoard) like in the code below: +The first step to calibrate a camera is to create an [ArUcoBoard](../../../argaze.md/#argaze.ArUcoMarker.ArUcoBoard) like in the code below: ```python -from argaze.ArUcoMarkers import ArUcoMarkersDictionary, ArUcoBoard +from argaze.ArUcoMarker import ArUcoMarkerDictionary, ArUcoBoard # Create ArUco dictionary -aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary('DICT_APRILTAG_16h5') +aruco_dictionary = ArUcoMarkerDictionary.ArUcoMarkerDictionary('DICT_APRILTAG_16h5') # Create an ArUco board of 7 columns and 5 rows with 5 cm squares with 3cm ArUco markers inside aruco_board = ArUcoBoard.ArUcoBoard(7, 5, 5, 3, aruco_dictionary) @@ -23,13 +23,13 @@ aruco_board.save('./calibration_board.png', 300) ``` !!! note - There is **A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf** file located in *./src/argaze/ArUcoMarkers/utils/* folder ready to be printed on A3 paper sheet. + There is **A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf** file located in *./src/argaze/ArUcoMarker/utils/* folder ready to be printed on A3 paper sheet. Let's print the calibration board before to go further. ## Capture board pictures -Then, the calibration process needs to make many different captures of an [ArUcoBoard](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoBoard) through the camera and then, pass them to an [ArUcoDetector](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoDetector.ArUcoDetector) instance to detect board corners and store them as calibration data into an [ArUcoOpticCalibrator](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoOpticCalibrator) for final calibration process. +Then, the calibration process needs to make many different captures of an [ArUcoBoard](../../../argaze.md/#argaze.ArUcoMarker.ArUcoBoard) through the camera and then, pass them to an [ArUcoDetector](../../../argaze.md/#argaze.ArUcoMarker.ArUcoDetector.ArUcoDetector) instance to detect board corners and store them as calibration data into an [ArUcoOpticCalibrator](../../../argaze.md/#argaze.ArUcoMarker.ArUcoOpticCalibrator) for final calibration process. ![Calibration step](../../../img/optic_calibration_step.png) @@ -42,10 +42,10 @@ The sample of code below illustrates how to: * finally, save optic parameters into a JSON file. ```python -from argaze.ArUcoMarkers import ArUcoMarkersDictionary, ArUcoOpticCalibrator, ArUcoBoard, ArUcoDetector +from argaze.ArUcoMarker import ArUcoMarkerDictionary, ArUcoOpticCalibrator, ArUcoBoard, ArUcoDetector # Create ArUco dictionary -aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary('DICT_APRILTAG_16h5') +aruco_dictionary = ArUcoMarkerDictionary.ArUcoMarkerDictionary('DICT_APRILTAG_16h5') # Create ArUco optic calibrator aruco_optic_calibrator = ArUcoOpticCalibrator.ArUcoOpticCalibrator() @@ -134,9 +134,9 @@ Below, an optic_parameters JSON file example: ## Load and display optic parameters -[ArUcoCamera.detector.optic_parameters](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoOpticCalibrator.OpticParameters) can be enabled thanks to a dedicated JSON entry. +[ArUcoCamera.detector.optic_parameters](../../../argaze.md/#argaze.ArUcoMarker.ArUcoOpticCalibrator.OpticParameters) can be enabled thanks to a dedicated JSON entry. -Here is an extract from the JSON [ArUcoCamera](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) configuration file where optic parameters are loaded and displayed: +Here is an extract from the JSON [ArUcoCamera](../../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) configuration file where optic parameters are loaded and displayed: ```json { diff --git a/docs/user_guide/aruco_marker_pipeline/advanced_topics/scripting.md b/docs/user_guide/aruco_marker_pipeline/advanced_topics/scripting.md index c9a06a6..4d5d44c 100644 --- a/docs/user_guide/aruco_marker_pipeline/advanced_topics/scripting.md +++ b/docs/user_guide/aruco_marker_pipeline/advanced_topics/scripting.md @@ -6,11 +6,11 @@ This could be particularly useful for realtime AR interaction applications. ## Load ArUcoCamera configuration from dictionary -An [ArUcoCamera](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) configuration can be loaded from a Python dictionary. +An [ArUcoCamera](../../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) configuration can be loaded from a Python dictionary. ```python from argaze import DataFeatures -from argaze.ArUcoMarkers import ArUcoCamera +from argaze.ArUcoMarker import ArUcoCamera # Set working directory to enable relative file path loading DataFeatures.set_working_directory('path/to/folder') @@ -59,9 +59,9 @@ with ArUcoCamera.ArUcoCamera(**configuration) as aruco_camera: ## Access to ArUcoCamera and ArScenes attributes -Then, once the configuration is loaded, it is possible to access to its attributes: [read ArUcoCamera code reference](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) to get a complete list of what is available. +Then, once the configuration is loaded, it is possible to access to its attributes: [read ArUcoCamera code reference](../../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) to get a complete list of what is available. -Thus, the [ArUcoCamera.scenes](../../../argaze.md/#argaze.ArFeatures.ArCamera) attribute allows to access each loaded aruco scene and so, access to their attributes: [read ArUcoScene code reference](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) to get a complete list of what is available. +Thus, the [ArUcoCamera.scenes](../../../argaze.md/#argaze.ArFeatures.ArCamera) attribute allows to access each loaded aruco scene and so, access to their attributes: [read ArUcoScene code reference](../../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) to get a complete list of what is available. ```python from argaze import ArFeatures @@ -101,7 +101,7 @@ Let's understand the meaning of each returned data. ### *aruco_camera.aruco_detector.detected_markers()* -A dictionary containing all detected markers provided by [ArUcoDetector](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoDetector) class. +A dictionary containing all detected markers provided by [ArUcoDetector](../../../argaze.md/#argaze.ArUcoMarker.ArUcoDetector) class. ## Setup ArUcoCamera image parameters @@ -133,4 +133,4 @@ aruco_camera_image = aruco_camera.image(**image_parameters) ``` !!! note - [ArUcoCamera](../../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) inherits from [ArFrame](../../../argaze.md/#argaze.ArFeatures.ArFrame) and so, benefits from all image parameters described in [gaze analysis pipeline visualization section](../../gaze_analysis_pipeline/visualization.md). \ No newline at end of file + [ArUcoCamera](../../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) inherits from [ArFrame](../../../argaze.md/#argaze.ArFeatures.ArFrame) and so, benefits from all image parameters described in [gaze analysis pipeline visualization section](../../gaze_analysis_pipeline/visualization.md). \ No newline at end of file diff --git a/docs/user_guide/aruco_marker_pipeline/aoi_3d_frame.md b/docs/user_guide/aruco_marker_pipeline/aoi_3d_frame.md index c4514f5..e1614d3 100644 --- a/docs/user_guide/aruco_marker_pipeline/aoi_3d_frame.md +++ b/docs/user_guide/aruco_marker_pipeline/aoi_3d_frame.md @@ -9,11 +9,11 @@ When an 3D AOI of the scene contains others coplanar 3D AOI, like a screen with The [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame) class defines a rectangular area where timestamped gaze positions are projected in and inside which they need to be analyzed. -Here is the previous extract where "Left_Screen" and "Right_Screen" AOI are defined as a frame into [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) configuration: +Here is the previous extract where "Left_Screen" and "Right_Screen" AOI are defined as a frame into [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) configuration: ```json { - "argaze.ArUcoMarkers.ArUcoCamera.ArUcoCamera": { + "argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera": { "name": "My FullHD camera", "size": [1920, 1080], ... @@ -78,7 +78,7 @@ Now, let's understand the meaning of each JSON entry. ### *frames* -An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) instance can contains multiples [ArFrames](../../argaze.md/#argaze.ArFeatures.ArFrame) stored by name. +An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) instance can contains multiples [ArFrames](../../argaze.md/#argaze.ArFeatures.ArFrame) stored by name. ### Left_Screen & Right_Screen @@ -86,21 +86,21 @@ The names of 3D AOI **and** their related [ArFrames](../../argaze.md/#argaze.ArF !!! warning "AOI / Frame names policy" - An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) layer 3D AOI is defined as an [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) frame, **provided they have the same name**. + An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) layer 3D AOI is defined as an [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) frame, **provided they have the same name**. !!! warning "Layer name policy" - An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) frame layer is projected into [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) layer, **provided they have the same name**. + An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) frame layer is projected into [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) layer, **provided they have the same name**. !!! note - [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) frame layers are projected into their dedicated [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) layers when the JSON configuration file is loaded. + [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) frame layers are projected into their dedicated [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) layers when the JSON configuration file is loaded. ## Pipeline execution ### Map ArUcoCamera image into ArUcoScenes frames -After camera image is passed to [ArUcoCamera.watch](../../argaze.md/#argaze.ArFeatures.ArCamera.watch) method, it is possible to apply a perpective transformation in order to project watched image into each [ArUcoScenes](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) [frames background](../../argaze.md/#argaze.ArFeatures.ArFrame) image. +After camera image is passed to [ArUcoCamera.watch](../../argaze.md/#argaze.ArFeatures.ArCamera.watch) method, it is possible to apply a perpective transformation in order to project watched image into each [ArUcoScenes](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) [frames background](../../argaze.md/#argaze.ArFeatures.ArFrame) image. ```python # Assuming that Full HD (1920x1080) timestamped images are available @@ -115,15 +115,15 @@ After camera image is passed to [ArUcoCamera.watch](../../argaze.md/#argaze.ArFe ### Analyse timestamped gaze positions into ArUcoScenes frames -[ArUcoScenes](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) frames benefits from all the services described in [gaze analysis pipeline section](../gaze_analysis_pipeline/introduction.md). +[ArUcoScenes](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) frames benefits from all the services described in [gaze analysis pipeline section](../gaze_analysis_pipeline/introduction.md). !!! note - Timestamped [GazePositions](../../argaze.md/#argaze.GazeFeatures.GazePosition) passed to [ArUcoCamera.look](../../argaze.md/#argaze.ArFeatures.ArFrame.look) method are projected into [ArUcoScenes](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) frames if applicable. + Timestamped [GazePositions](../../argaze.md/#argaze.GazeFeatures.GazePosition) passed to [ArUcoCamera.look](../../argaze.md/#argaze.ArFeatures.ArFrame.look) method are projected into [ArUcoScenes](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) frames if applicable. ### Display each ArUcoScenes frames -All [ArUcoScenes](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) frames image can be displayed as any [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame). +All [ArUcoScenes](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) frames image can be displayed as any [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame). ```python ... diff --git a/docs/user_guide/aruco_marker_pipeline/aoi_3d_projection.md b/docs/user_guide/aruco_marker_pipeline/aoi_3d_projection.md index 306e26a..e0f7f4c 100644 --- a/docs/user_guide/aruco_marker_pipeline/aoi_3d_projection.md +++ b/docs/user_guide/aruco_marker_pipeline/aoi_3d_projection.md @@ -1,7 +1,7 @@ Project 3D AOI into camera frame ================================ -Once [ArUcoScene pose is estimated](pose_estimation.md) and [3D AOI are described](aoi_3d_description.md), AOI can be projected into [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) frame. +Once [ArUcoScene pose is estimated](pose_estimation.md) and [3D AOI are described](aoi_3d_description.md), AOI can be projected into [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) frame. ![3D AOI projection](../../img/aruco_camera_aoi_projection.png) @@ -9,11 +9,11 @@ Once [ArUcoScene pose is estimated](pose_estimation.md) and [3D AOI are describe The [ArLayer](../../argaze.md/#argaze.ArFeatures.ArLayer) class allows to load 3D AOI description. -Here is the previous extract where one layer is added to [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) configuration: +Here is the previous extract where one layer is added to [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) configuration: ```json { - "argaze.ArUcoMarkers.ArUcoCamera.ArUcoCamera": { + "argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera": { "name": "My FullHD camera", "size": [1920, 1080], ... @@ -43,7 +43,7 @@ Now, let's understand the meaning of each JSON entry. ### *layers* -An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) instance can contains multiples [ArLayers](../../argaze.md/#argaze.ArFeatures.ArLayer) stored by name. +An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) instance can contains multiples [ArLayers](../../argaze.md/#argaze.ArFeatures.ArLayer) stored by name. ### MyLayer @@ -55,11 +55,11 @@ The set of 3D AOI into the layer as defined at [3D AOI description chapter](aoi_ ## Add ArLayer to ArUcoCamera to project 3D AOI into -Here is the previous extract where one layer is added to [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) configuration and displayed: +Here is the previous extract where one layer is added to [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) configuration and displayed: ```json { - "argaze.ArUcoMarkers.ArUcoCamera.ArUcoCamera": { + "argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera": { "name": "My FullHD camera", "size": [1920, 1080], ... @@ -110,23 +110,23 @@ The name of an [ArLayer](../../argaze.md/#argaze.ArFeatures.ArLayer). Basically !!! warning "Layer name policy" - An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) layer is projected into an [ ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) layer, **provided they have the same name**. + An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) layer is projected into an [ ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) layer, **provided they have the same name**. !!! note - [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) layers are projected into their dedicated [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) layers when calling the [ArUcoCamera.watch](../../argaze.md/#argaze.ArFeatures.ArCamera.watch) method. + [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) layers are projected into their dedicated [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) layers when calling the [ArUcoCamera.watch](../../argaze.md/#argaze.ArFeatures.ArCamera.watch) method. ## Add AOI analysis features to ArUcoCamera layer When a scene layer is projected into a camera layer, it means that the 3D scene's AOI are transformed into 2D camera's AOI. -Therefore, it means that [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) benefits from all the services described in [AOI analysis pipeline section](../gaze_analysis_pipeline/aoi_analysis.md). +Therefore, it means that [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) benefits from all the services described in [AOI analysis pipeline section](../gaze_analysis_pipeline/aoi_analysis.md). -Here is the previous extract where AOI matcher, AOI scan path and AOI scan path analyzers are added to the [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) layer: +Here is the previous extract where AOI matcher, AOI scan path and AOI scan path analyzers are added to the [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) layer: ```json { - "argaze.ArUcoMarkers.ArUcoCamera.ArUcoCamera": { + "argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera": { "name": "My FullHD camera", "size": [1920, 1080], ... @@ -171,4 +171,4 @@ Here is the previous extract where AOI matcher, AOI scan path and AOI scan path !!! warning - Adding scan path and scan path analyzers to an [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) layer doesn't make sense as the space viewed thru camera frame doesn't necessary reflect the space the gaze is covering. + Adding scan path and scan path analyzers to an [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) layer doesn't make sense as the space viewed thru camera frame doesn't necessary reflect the space the gaze is covering. diff --git a/docs/user_guide/aruco_marker_pipeline/aruco_markers_description.md b/docs/user_guide/aruco_marker_pipeline/aruco_markers_description.md index 66a0581..6da600c 100644 --- a/docs/user_guide/aruco_marker_pipeline/aruco_markers_description.md +++ b/docs/user_guide/aruco_marker_pipeline/aruco_markers_description.md @@ -17,13 +17,13 @@ Many ArUco dictionaries exist with properties concerning the format, the number Here is the documention [about ArUco markers dictionaries](https://docs.opencv.org/3.4/d9/d6a/group__aruco.html#gac84398a9ed9dd01306592dd616c2c975). -The creation of [ArUcoMarkers](../../argaze.md/#argaze.ArUcoMarkers.ArUcoMarker) pictures from a dictionary is illustrated in the code below: +The creation of [ArUcoMarker](../../argaze.md/#argaze.ArUcoMarker.ArUcoMarker) pictures from a dictionary is illustrated in the code below: ```python -from argaze.ArUcoMarkers import ArUcoMarkersDictionary +from argaze.ArUcoMarker import ArUcoMarkerDictionary # Create a dictionary of specific April tags -aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary('DICT_APRILTAG_16h5') +aruco_dictionary = ArUcoMarkerDictionary.ArUcoMarkerDictionary('DICT_APRILTAG_16h5') # Export marker n°5 as 3.5 cm picture with 300 dpi resolution aruco_dictionary.create_marker(5, 3.5).save('./markers/', 300) @@ -33,7 +33,7 @@ aruco_dictionary.save('./markers/', 3.5, 300) ``` !!! note - There is **A4_DICT_APRILTAG_16h5_5cm_0-7.pdf** file located in *./src/argaze/ArUcoMarkers/utils/* folder ready to be printed on A4 paper sheet. + There is **A4_DICT_APRILTAG_16h5_5cm_0-7.pdf** file located in *./src/argaze/ArUcoMarker/utils/* folder ready to be printed on A4 paper sheet. Let's print some of them before to go further. @@ -42,7 +42,7 @@ Let's print some of them before to go further. ## Describe ArUco markers place -Once [ArUcoMarkers](../../argaze.md/#argaze.ArUcoMarkers.ArUcoMarker) pictures are placed into a scene it is possible to describe their 3D places into a file. +Once [ArUcoMarker](../../argaze.md/#argaze.ArUcoMarker.ArUcoMarker) pictures are placed into a scene it is possible to describe their 3D places into a file. ![ArUco markers description](../../img/aruco_markers_description.png) diff --git a/docs/user_guide/aruco_marker_pipeline/configuration_and_execution.md b/docs/user_guide/aruco_marker_pipeline/configuration_and_execution.md index 84877ca..f2bddf8 100644 --- a/docs/user_guide/aruco_marker_pipeline/configuration_and_execution.md +++ b/docs/user_guide/aruco_marker_pipeline/configuration_and_execution.md @@ -1,21 +1,21 @@ Load and execute pipeline ========================= -Once [ArUco markers are placed into a scene](aruco_markers_description.md), they can be detected thanks to [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) class. +Once [ArUco markers are placed into a scene](aruco_markers_description.md), they can be detected thanks to [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) class. -As [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) inherits from [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame), the [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) class also benefits from all the services described in [gaze analysis pipeline section](../gaze_analysis_pipeline/introduction.md). +As [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) inherits from [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame), the [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) class also benefits from all the services described in [gaze analysis pipeline section](../gaze_analysis_pipeline/introduction.md). ![ArUco camera frame](../../img/aruco_camera_frame.png) ## Load JSON configuration file -An [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) pipeline can be loaded from a JSON configuration file thanks to [argaze.load](../../argaze.md/#argaze.load) package method. +An [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) pipeline can be loaded from a JSON configuration file thanks to [argaze.load](../../argaze.md/#argaze.load) package method. -Here is a simple JSON [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) configuration file example: +Here is a simple JSON [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) configuration file example: ```json { - "argaze.ArUcoMarkers.ArUcoCamera.ArUcoCamera": { + "argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera": { "name": "My FullHD camera", "size": [1920, 1080], "aruco_detector": { @@ -66,25 +66,25 @@ with argaze.load('./configuration.json') as aruco_camera: Now, let's understand the meaning of each JSON entry. -### argaze.ArUcoMarkers.ArUcoCamera.ArUcoCamera +### argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera The loaded object class name. ### *name - inherited from [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame)* -The name of the [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) frame. Basically useful for visualization purpose. +The name of the [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) frame. Basically useful for visualization purpose. ### *size - inherited from [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame)* -The size of the [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) frame in pixels. Be aware that gaze positions have to be in the same range of value to be projected in. +The size of the [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) frame in pixels. Be aware that gaze positions have to be in the same range of value to be projected in. ### *aruco_detector* -The first [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) pipeline step is to detect ArUco markers inside input image. +The first [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) pipeline step is to detect ArUco markers inside input image. ![ArUco markers detection](../../img/aruco_camera_markers_detection.png) -The [ArUcoDetector](../../argaze.md/#argaze.ArUcoMarkers.ArUcoDetector) is in charge to detect all markers from a specific dictionary. +The [ArUcoDetector](../../argaze.md/#argaze.ArUcoMarker.ArUcoDetector) is in charge to detect all markers from a specific dictionary. !!! warning "Mandatory" JSON *aruco_detector* entry is mandatory. @@ -129,7 +129,7 @@ Pass each camera image to [ArUcoCamera.watch](../../argaze.md/#argaze.ArFeatures ### Analyse timestamped gaze positions into camera frame -As mentioned above, [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) inherits from [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame) and so, benefits from all the services described in [gaze analysis pipeline section](../gaze_analysis_pipeline/introduction.md). +As mentioned above, [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) inherits from [ArFrame](../../argaze.md/#argaze.ArFeatures.ArFrame) and so, benefits from all the services described in [gaze analysis pipeline section](../gaze_analysis_pipeline/introduction.md). Particularly, timestamped gaze positions can be passed one by one to [ArUcoCamera.look](../../argaze.md/#argaze.ArFeatures.ArFrame.look) method to execute the whole pipeline dedicated to gaze analysis. diff --git a/docs/user_guide/aruco_marker_pipeline/introduction.md b/docs/user_guide/aruco_marker_pipeline/introduction.md index 7e662f7..273de16 100644 --- a/docs/user_guide/aruco_marker_pipeline/introduction.md +++ b/docs/user_guide/aruco_marker_pipeline/introduction.md @@ -3,13 +3,13 @@ Overview This section explains how to build augmented reality pipelines based on [ArUco Markers technology](https://www.sciencedirect.com/science/article/abs/pii/S0031320314000235) for various use cases. -The OpenCV library provides a module to detect fiducial markers into a picture and estimate their poses. +The OpenCV library provides a module to detect fiducial markers in a picture and estimate their poses. ![OpenCV ArUco markers](../../img/opencv_aruco.png) -The ArGaze [ArUcoMarkers submodule](../../argaze.md/#argaze.ArUcoMarkers) eases markers creation, markers detection and 3D scene pose estimation through a set of high level classes. +The ArGaze [ArUcoMarker submodule](../../argaze.md/#argaze.ArUcoMarker) eases markers creation, markers detection, and 3D scene pose estimation through a set of high-level classes. -First, let's look at the schema below: it gives an overview of the main notions involved in the following chapters. +First, let's look at the schema below. It gives an overview of the main notions involved in the following chapters. ![ArUco marker pipeline](../../img/aruco_marker_pipeline.png) @@ -18,7 +18,7 @@ To build your own ArUco marker pipeline, you need to know: * [How to setup ArUco markers into a scene](aruco_markers_description.md), * [How to load and execute ArUco marker pipeline](configuration_and_execution.md), * [How to estimate scene pose](pose_estimation.md), -* [How to describe scene's AOI](aoi_3d_description.md), +* [How to describe a scene's AOI](aoi_3d_description.md), * [How to project 3D AOI into camera frame](aoi_3d_projection.md), * [How to define a 3D AOI as a frame](aoi_3d_frame.md). @@ -26,4 +26,4 @@ More advanced features are also explained like: * [How to script ArUco marker pipeline](advanced_topics/scripting.md), * [How to calibrate optic parameters](advanced_topics/optic_parameters_calibration.md), -* [How to improve ArUco markers detection](advanced_topics/aruco_detector_configuration.md). +* [How to improve ArUco marker detection](advanced_topics/aruco_detector_configuration.md). diff --git a/docs/user_guide/aruco_marker_pipeline/pose_estimation.md b/docs/user_guide/aruco_marker_pipeline/pose_estimation.md index affa232..5ebe783 100644 --- a/docs/user_guide/aruco_marker_pipeline/pose_estimation.md +++ b/docs/user_guide/aruco_marker_pipeline/pose_estimation.md @@ -1,19 +1,19 @@ Estimate scene pose =================== -Once [ArUco markers are placed into a scene](aruco_markers_description.md) and [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) is [configured](configuration_and_execution.md), scene pose can be estimated. +Once [ArUco markers are placed into a scene](aruco_markers_description.md) and [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) is [configured](configuration_and_execution.md), scene pose can be estimated. ![Scene pose estimation](../../img/aruco_camera_pose_estimation.png) ## Add ArUcoScene to ArUcoCamera JSON configuration file -An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) class defines a space with [ArUco markers inside](aruco_markers_description.md) helping to estimate scene pose when they are watched by [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera). +An [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) class defines a space with [ArUco markers inside](aruco_markers_description.md) helping to estimate scene pose when they are watched by [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera). -Here is an extract from the JSON [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) configuration file with a sample where one scene is added and displayed: +Here is an extract from the JSON [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) configuration file with a sample where one scene is added and displayed: ```json { - "argaze.ArUcoMarkers.ArUcoCamera.ArUcoCamera": { + "argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera": { "name": "My FullHD camera", "size": [1920, 1080], ... @@ -67,20 +67,20 @@ Now, let's understand the meaning of each JSON entry. ### *scenes* -An [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) instance can contains multiples [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) stored by name. +An [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) instance can contains multiples [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) stored by name. ### MyScene -The name of an [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene). Basically useful for visualization purpose. +The name of an [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene). Basically useful for visualization purpose. ### *aruco_markers_group* -The 3D places of ArUco markers into the scene as defined at [ArUco markers description chapter](aruco_markers_description.md). Thanks to this description, it is possible to estimate the pose of [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) in [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarkers.ArUcoCamera) frame. +The 3D places of ArUco markers into the scene as defined at [ArUco markers description chapter](aruco_markers_description.md). Thanks to this description, it is possible to estimate the pose of [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) in [ArUcoCamera](../../argaze.md/#argaze.ArUcoMarker.ArUcoCamera) frame. !!! note - [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) pose estimation is done when calling the [ArUcoCamera.watch](../../argaze.md/#argaze.ArFeatures.ArCamera.watch) method. + [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) pose estimation is done when calling the [ArUcoCamera.watch](../../argaze.md/#argaze.ArFeatures.ArCamera.watch) method. ### *draw_scenes* -The drawing parameters of each loaded [ArUcoScene](../../argaze.md/#argaze.ArUcoMarkers.ArUcoScene) in [ArUcoCamera.image](../../argaze.md/#argaze.ArFeatures.ArFrame.image). +The drawing parameters of each loaded [ArUcoScene](../../argaze.md/#argaze.ArUcoMarker.ArUcoScene) in [ArUcoCamera.image](../../argaze.md/#argaze.ArFeatures.ArFrame.image). diff --git a/docs/user_guide/utils/ready-made_scripts.md b/docs/user_guide/utils/ready-made_scripts.md index a7b3057..3640784 100644 --- a/docs/user_guide/utils/ready-made_scripts.md +++ b/docs/user_guide/utils/ready-made_scripts.md @@ -51,5 +51,5 @@ echo "context.resume()" > /tmp/argaze Detect DICTIONARY and SIZE ArUco markers inside a MOVIE frame then, export detected ArUco markers group as .obj file into an OUTPUT folder. ```shell -python ./src/argaze/utils/aruco_markers_group_export.py MOVIE DICTIONARY SIZE -o OUTPUT +python ./src/argaze/utils/aruco_marker_group_export.py MOVIE DICTIONARY SIZE -o OUTPUT ``` \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/ArUcoBoard.py b/src/argaze.test/ArUcoMarker/ArUcoBoard.py new file mode 100644 index 0000000..b20be13 --- /dev/null +++ b/src/argaze.test/ArUcoMarker/ArUcoBoard.py @@ -0,0 +1,50 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import unittest +import os + +from argaze.ArUcoMarker import ArUcoBoard, ArUcoMarkerDictionary + +import numpy + +class TestArUcoBoardClass(unittest.TestCase): + """Test ArUcoBoard class.""" + + def test_new(self): + """Test ArUcoBoard creation using a dictionary instance.""" + + columns = 4 + rows = 3 + square_size = 2 + marker_size = 1 + + # Check ArUco board creation + aruco_dictionary = ArUcoMarkerDictionary.ArUcoMarkerDictionary('DICT_APRILTAG_16h5') + aruco_board = ArUcoBoard.ArUcoBoard(columns, rows, square_size, marker_size, aruco_dictionary) + + # Check ArUco board dictionary name + self.assertEqual(aruco_board.dictionary.name, 'DICT_APRILTAG_16h5') + self.assertIsNone(numpy.testing.assert_array_equal(aruco_board.identifiers, [i for i in range(int((columns*rows)/2))])) + self.assertIsNone(numpy.testing.assert_array_equal(aruco_board.size, [columns, rows])) + self.assertEqual(aruco_board.markers_number, int((columns*rows)/2)) + self.assertEqual(aruco_board.corners_number, (columns-1)*(rows-1)) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/ArUcoCamera.py b/src/argaze.test/ArUcoMarker/ArUcoCamera.py new file mode 100644 index 0000000..eb930ab --- /dev/null +++ b/src/argaze.test/ArUcoMarker/ArUcoCamera.py @@ -0,0 +1,80 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import unittest +import os + +import argaze + +import numpy + +class TestArUcoCameraClass(unittest.TestCase): + """Test ArUcoCamera class.""" + + def test_from_json(self): + """Test ArUcoCamera creation from json file.""" + + # Edit test aruco camera file path + current_directory = os.path.dirname(os.path.abspath(__file__)) + json_filepath = os.path.join(current_directory, 'utils/aruco_camera.json') + + # Load test aruco camera + with argaze.load(json_filepath) as aruco_camera: + + # Check aruco camera meta data + self.assertEqual(aruco_camera.name, "TestArUcoCamera") + + # Check ArUco detector + self.assertEqual(aruco_camera.aruco_detector.dictionary.name, "DICT_ARUCO_ORIGINAL") + self.assertEqual(aruco_camera.aruco_detector.parameters.cornerRefinementMethod, 3) + self.assertEqual(aruco_camera.aruco_detector.parameters.aprilTagQuadSigma, 2) + self.assertEqual(aruco_camera.aruco_detector.parameters.aprilTagDeglitch, 1) + + # Check ArUco detector optic parameters + self.assertEqual(aruco_camera.aruco_detector.optic_parameters.rms, 1.0) + self.assertIsNone(numpy.testing.assert_array_equal(aruco_camera.aruco_detector.optic_parameters.dimensions, [1920, 1080])) + self.assertIsNone(numpy.testing.assert_array_equal(aruco_camera.aruco_detector.optic_parameters.K, [[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]])) + self.assertIsNone(numpy.testing.assert_array_equal(aruco_camera.aruco_detector.optic_parameters.D, [-1.0, -0.5, 0.0, 0.5, 1.0])) + + # Check camera scenes + self.assertEqual(len(aruco_camera.scenes), 2) + self.assertIsNone(numpy.testing.assert_array_equal(list(aruco_camera.scenes.keys()), ["TestSceneA", "TestSceneB"])) + + # Load test scene + ar_scene = aruco_camera.scenes["TestSceneA"] + + # Check Aruco scene + self.assertEqual(len(ar_scene.aruco_markers_group.places), 2) + self.assertIsNone(numpy.testing.assert_allclose(ar_scene.aruco_markers_group.places[0].corners[0], [-0.5, 1.5, 0.], rtol=0, atol=1e-3)) + self.assertEqual(ar_scene.aruco_markers_group.places[0].marker.identifier, 0) + + self.assertIsNone(numpy.testing.assert_allclose(ar_scene.aruco_markers_group.places[1].corners[0], [0., 2.5, -1.5], rtol=0, atol=1e-3)) + self.assertEqual(ar_scene.aruco_markers_group.places[1].marker.identifier, 1) + + # Check layers and AOI scene + self.assertEqual(len(ar_scene.layers.items()), 1) + self.assertEqual(len(ar_scene.layers["Main"].aoi_scene), 1) + self.assertEqual(ar_scene.layers["Main"].aoi_scene['Test'].points_number, 4) + + # Check ArScene + self.assertEqual(ar_scene.angle_tolerance, 1.0) + self.assertEqual(ar_scene.distance_tolerance, 2.0) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/ArUcoDetector.py b/src/argaze.test/ArUcoMarker/ArUcoDetector.py new file mode 100644 index 0000000..40b7d00 --- /dev/null +++ b/src/argaze.test/ArUcoMarker/ArUcoDetector.py @@ -0,0 +1,149 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import unittest +import os +import math + +from argaze.ArUcoMarker import ArUcoMarkerDictionary, ArUcoOpticCalibrator, ArUcoDetector, ArUcoBoard + +import cv2 as cv +import numpy + +class TestDetectorParametersClass(unittest.TestCase): + """Test DetectorParameters class.""" + + def test_from_json(self): + """Test DetectorParameters creation from json file.""" + + # Edit file path + current_directory = os.path.dirname(os.path.abspath(__file__)) + json_filepath = os.path.join(current_directory, 'utils/detector_parameters.json') + + # Load file + detector_parameters = ArUcoDetector.DetectorParameters.from_json(json_filepath) + + # Check data + self.assertEqual(detector_parameters.cornerRefinementMethod, 3) + self.assertEqual(detector_parameters.aprilTagQuadSigma, 2) + self.assertEqual(detector_parameters.aprilTagDeglitch, 1) + + # Check bad data access fails + with self.assertRaises(AttributeError): + + detector_parameters.unknown_data = 1 + +class TestArUcoDetectorClass(unittest.TestCase): + """Test ArUcoDetector class.""" + + def test_new(self): + """Test ArUcoDetector creation.""" + + aruco_detector = ArUcoDetector.ArUcoDetector(marker_size=3) + + # Check ArUcoDetector creation + self.assertEqual(aruco_detector.dictionary.name, 'DICT_ARUCO_ORIGINAL') + self.assertEqual(aruco_detector.marker_size, 3) + self.assertIsNone(numpy.testing.assert_array_equal(aruco_detector.optic_parameters.dimensions, [0, 0])) + self.assertEqual(aruco_detector.detected_markers_number(), 0) + self.assertEqual(aruco_detector.detected_markers(), {}) + + aruco_dictionary = ArUcoMarkerDictionary.ArUcoMarkerDictionary('DICT_APRILTAG_16h5') + aruco_detector = ArUcoDetector.ArUcoDetector(aruco_dictionary, 5.2) + + # Check ArUcoDetector creation + self.assertEqual(aruco_detector.dictionary.name, 'DICT_APRILTAG_16h5') + self.assertEqual(aruco_detector.marker_size, 5.2) + self.assertIsNone(numpy.testing.assert_array_equal(aruco_detector.optic_parameters.dimensions, [0, 0])) + self.assertEqual(aruco_detector.detected_markers_number(), 0) + self.assertEqual(aruco_detector.detected_markers(), {}) + + def test_from_json(self): + """Test ArUcoDetector creation.""" + + # Edit file path + current_directory = os.path.dirname(os.path.abspath(__file__)) + json_filepath = os.path.join(current_directory, 'utils/detector.json') + + # Load file + aruco_detector = ArUcoDetector.ArUcoDetector.from_json(json_filepath) + + # Check ArUcoDetector creation + self.assertEqual(aruco_detector.dictionary.name, 'DICT_ARUCO_ORIGINAL') + self.assertEqual(aruco_detector.marker_size, 3) + self.assertIsNone(numpy.testing.assert_array_equal(aruco_detector.optic_parameters.dimensions, [1920, 1080])) + self.assertEqual(aruco_detector.parameters.cornerRefinementMethod, 3) + self.assertEqual(aruco_detector.parameters.aprilTagQuadSigma, 2) + self.assertEqual(aruco_detector.parameters.aprilTagDeglitch, 1) + + def test_detect(self): + """Test detect method.""" + + aruco_detector = ArUcoDetector.ArUcoDetector(marker_size=3) + + # Load picture Full HD to test ArUcoMarker detection + current_directory = os.path.dirname(os.path.abspath(__file__)) + image = cv.imread(os.path.join(current_directory, 'utils/full_hd_marker.png')) + + # Check ArUcoMarker detection + aruco_detector.detect_markers(image) + + self.assertEqual(aruco_detector.detected_markers_number(), 1) + + self.assertEqual(aruco_detector.detected_markers()[0].dictionary, aruco_detector.dictionary) + self.assertEqual(aruco_detector.detected_markers()[0].identifier, 0) + self.assertEqual(aruco_detector.detected_markers()[0].size, 3) + + # Check corner positions with -/+ 10 pixels precision + self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].corners[0][0].astype(int), numpy.array([3823, 2073]), decimal=-1)) + self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].corners[0][1].astype(int), numpy.array([4177, 2073]), decimal=-1)) + self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].corners[0][2].astype(int), numpy.array([4177, 2427]), decimal=-1)) + self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].corners[0][3].astype(int), numpy.array([3823, 2427]), decimal=-1)) + + # Check marker pose estimation + aruco_detector.estimate_markers_pose([0]) + + # Check marker translation with -/+ 0.1 cm precision and rotation with -/+ 0.001 radian precision + self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].translation, numpy.array([33.87, 19.05, 0.]), decimal=1)) + self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].rotation, numpy.array([[1., 0., 0.], [0., -1., 0.], [0., 0., -1.]]), decimal=3)) + + # Check detect metrics + detect_count, markers_count = aruco_detector.detection_metrics + self.assertEqual(detect_count, 1) + self.assertEqual(markers_count[0], 1) + + def test_detect_board(self): + """Test detect board method.""" + + aruco_board = ArUcoBoard.ArUcoBoard(7, 5, 5, 3) + aruco_detector = ArUcoDetector.ArUcoDetector(marker_size=3) + + # Load picture Full HD to test ArUcoMarker board detection + current_directory = os.path.dirname(os.path.abspath(__file__)) + image = cv.imread(os.path.join(current_directory, 'utils/full_hd_board.png')) + + # Check ArUcoMarker board detection + aruco_detector.detect_board(image, aruco_board, aruco_board.markers_number) + + self.assertEqual(aruco_detector.board_corners_number(), aruco_board.corners_number) + self.assertEqual(len(aruco_detector.board_corners()), 24) + self.assertEqual(len(aruco_detector.board_corners_identifier()), 24) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/ArUcoMarker.py b/src/argaze.test/ArUcoMarker/ArUcoMarker.py new file mode 100644 index 0000000..6518c8c --- /dev/null +++ b/src/argaze.test/ArUcoMarker/ArUcoMarker.py @@ -0,0 +1,40 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import unittest + +from argaze.ArUcoMarker import ArUcoMarkerDictionary, ArUcoMarker + +class TestArUcoMarkerClass(unittest.TestCase): + """Test ArUcoMarker class.""" + + def test_new(self): + """Test ArUcoMarker creation.""" + + # Check DICT_ARUCO_ORIGINAL ArUcoMarker creation + aruco_dictionary = ArUcoMarkerDictionary.ArUcoMarkerDictionary('DICT_ARUCO_ORIGINAL') + + aruco_marker = aruco_dictionary.create_marker(0, 3) + + self.assertEqual(aruco_marker.dictionary, aruco_dictionary) + self.assertEqual(aruco_marker.identifier, 0) + self.assertEqual(aruco_marker.size, 3) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/ArUcoMarkerDictionary.py b/src/argaze.test/ArUcoMarker/ArUcoMarkerDictionary.py new file mode 100644 index 0000000..2f80a67 --- /dev/null +++ b/src/argaze.test/ArUcoMarker/ArUcoMarkerDictionary.py @@ -0,0 +1,50 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import unittest + +from argaze.ArUcoMarker import ArUcoMarkerDictionary + +class TestArUcoMarkerDictionaryClass(unittest.TestCase): + """Test ArUcoMarkerDictionary class.""" + + def test_new(self): + """Test ArUcoMarkerDictionary creation.""" + + # Check that ArUcoMarkerDictionary creation fails with bad name + with self.assertRaises(NameError): + + aruco_dictionary = ArUcoMarkerDictionary.ArUcoMarkerDictionary('BAD_DICT_NAME') + + # Check default ArUcoMarkerDictionary creation + aruco_dictionary = ArUcoMarkerDictionary.ArUcoMarkerDictionary() + + self.assertEqual(aruco_dictionary.name, 'DICT_ARUCO_ORIGINAL') + self.assertEqual(aruco_dictionary.format, '5X5') + self.assertEqual(aruco_dictionary.number, 1024) + + # Check DICT_APRILTAG_16h5 ArUcoMarkerDictionary creation + aruco_dictionary = ArUcoMarkerDictionary.ArUcoMarkerDictionary('DICT_APRILTAG_16h5') + + self.assertEqual(aruco_dictionary.name, 'DICT_APRILTAG_16h5') + self.assertEqual(aruco_dictionary.format, '4X4') + self.assertEqual(aruco_dictionary.number, 30) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/ArUcoOpticCalibrator.py b/src/argaze.test/ArUcoMarker/ArUcoOpticCalibrator.py new file mode 100644 index 0000000..d019a5d --- /dev/null +++ b/src/argaze.test/ArUcoMarker/ArUcoOpticCalibrator.py @@ -0,0 +1,61 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import unittest +import os + +from argaze.ArUcoMarker import ArUcoOpticCalibrator + +import numpy + +class TestOpticParametersClass(unittest.TestCase): + """Test OpticParameters class.""" + + def test_new(self): + """Test OpticParameters creation.""" + + # Check defaut optic parameters creation + optic_parameters = ArUcoOpticCalibrator.OpticParameters() + + # Check ArUco optic parameters + self.assertEqual(optic_parameters.rms, 0.0) + + #self.assertEqual(type(optic_parameters.K), numpy.array) + + self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.dimensions, [0, 0])) + self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.K, ArUcoOpticCalibrator.K0)) + self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.D, ArUcoOpticCalibrator.D0)) + + def test_from_json(self): + + # Edit optic parameters file path + current_directory = os.path.dirname(os.path.abspath(__file__)) + json_filepath = os.path.join(current_directory, 'utils/optic_parameters.json') + + # Load optic parameters + optic_parameters = ArUcoOpticCalibrator.OpticParameters.from_json(json_filepath) + + # Check ArUco camera + self.assertEqual(optic_parameters.rms, 1.0) + self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.dimensions, [1920, 1080])) + self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.K, [[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]])) + self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.D, [-1.0, -0.5, 0.0, 0.5, 1.0])) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/ArUcoScene.py b/src/argaze.test/ArUcoMarker/ArUcoScene.py new file mode 100644 index 0000000..67cb668 --- /dev/null +++ b/src/argaze.test/ArUcoMarker/ArUcoScene.py @@ -0,0 +1,227 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import unittest +import os +import math + +from argaze.ArUcoMarker import ArUcoMarkerGroup, ArUcoMarker + +import cv2 as cv +import numpy + +class TestArUcoMarkerGroupClass(unittest.TestCase): + + def new_from_obj(self): + + # Edit file path + current_directory = os.path.dirname(os.path.abspath(__file__)) + obj_filepath = os.path.join(current_directory, 'utils/scene.obj') + + # Load file + self.aruco_markers_group = ArUcoMarkerGroup.ArUcoMarkerGroup.from_obj(obj_filepath) + + def new_from_json(self): + + # Edit file path + current_directory = os.path.dirname(os.path.abspath(__file__)) + json_filepath = os.path.join(current_directory, 'utils/scene.json') + + # Load file + self.aruco_markers_group = ArUcoMarkerGroup.ArUcoMarkerGroup.from_json(json_filepath) + + def setup_markers(self): + + # Prepare detected markers + self.detected_markers = { + 0: ArUcoMarker.ArUcoMarker('DICT_ARUCO_ORIGINAL', 0, 1.), + 1: ArUcoMarker.ArUcoMarker('DICT_ARUCO_ORIGINAL', 1, 1.), + 2: ArUcoMarker.ArUcoMarker('DICT_ARUCO_ORIGINAL', 2, 1.), + 3: ArUcoMarker.ArUcoMarker('DICT_ARUCO_ORIGINAL', 3, 1.) + } + + # Prepare scene markers and remaining markers + self.scene_markers, self.remaining_markers = self.aruco_markers_group.filter_markers(self.detected_markers()) + + def test_new_from_obj(self): + """Test ArUcoMarkerGroup creation.""" + + self.new_from_obj() + self.setup_markers() + + # Check ArUcoMarkerGroup creation + self.assertEqual(len(self.aruco_markers_group.places), 3) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.identifiers, [0, 1, 2])) + self.assertEqual(self.aruco_markers_group.marker_size, 1.) + + self.assertEqual(self.aruco_markers_group.places[0].marker.identifier, 0) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[0].translation, [0., 0., 0.])) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[0].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) + + self.assertEqual(self.aruco_markers_group.places[1].marker.identifier, 1) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[1].translation, [10., 10., 0.])) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[1].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) + + self.assertEqual(self.aruco_markers_group.places[2].marker.identifier, 2) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[2].translation, [0., 10., 0.])) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[2].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) + + def test_new_from_json(self): + """Test ArUcoMarkerGroup creation.""" + + self.new_from_json() + self.setup_markers() + + # Check ArUcoMarkerGroup creation + self.assertEqual(len(self.aruco_markers_group.places), 3) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.identifiers, [0, 1, 2])) + self.assertEqual(self.aruco_markers_group.marker_size, 1.) + + self.assertEqual(self.aruco_markers_group.places[0].marker.identifier, 0) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[0].translation, [0., 0., 0.])) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[0].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) + + self.assertEqual(self.aruco_markers_group.places[1].marker.identifier, 1) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[1].translation, [10., 10., 0.])) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[1].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) + + self.assertEqual(self.aruco_markers_group.places[2].marker.identifier, 2) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[2].translation, [0., 10., 0.])) + self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[2].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) + + def test_filter_markers(self): + """Test ArUcoMarkerGroup markers filtering.""" + + self.new_from_obj() + self.setup_markers() + + # Check scene markers and remaining markers + self.assertEqual(len(self.scene_markers), 3) + self.assertEqual(len(self.remaining_markers), 1) + + self.assertIsNone(numpy.testing.assert_array_equal(list(self.scene_markers.keys()), self.aruco_markers_group.identifiers)) + self.assertIsNone(numpy.testing.assert_array_equal(list(self.remaining_markers.keys()), [3])) + + def test_check_markers_consistency(self): + """Test ArUcoMarkerGroup markers consistency checking.""" + + self.new_from_obj() + self.setup_markers() + + # Edit consistent marker poses + self.scene_markers[0].translation = numpy.array([1., 1., 5.]) + self.scene_markers[0].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) + + self.scene_markers[1].translation = numpy.array([11., 11., 5.]) + self.scene_markers[1].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) + + self.scene_markers[2].translation = numpy.array([1., 11., 5.]) + self.scene_markers[2].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) + + # Check consistency + consistent_markers, unconsistent_markers, unconsistencies = self.aruco_markers_group.check_markers_consistency(self.scene_markers, 1, 1) + + # Check consistent markers, unconsistent markers and unconsistencies + self.assertEqual(len(consistent_markers), 3) + self.assertEqual(len(unconsistent_markers), 0) + self.assertEqual(len(unconsistencies['rotation']), 0) + self.assertEqual(len(unconsistencies['translation']), 0) + + self.assertIsNone(numpy.testing.assert_array_equal(list(consistent_markers.keys()), self.aruco_markers_group.identifiers)) + + # Edit unconsistent marker poses + self.scene_markers[2].translation = numpy.array([5., 15., 5.]) + + # Check consistency + consistent_markers, unconsistent_markers, unconsistencies = self.aruco_markers_group.check_markers_consistency(self.scene_markers, 1, 1) + + # Check consistent markers, unconsistent markers and unconsistencies + self.assertEqual(len(consistent_markers), 2) + self.assertEqual(len(unconsistent_markers), 1) + self.assertEqual(len(unconsistencies['rotation']), 0) + self.assertEqual(len(unconsistencies['translation']), 2) + + self.assertIsNone(numpy.testing.assert_array_equal(list(unconsistent_markers.keys()), [2])) + self.assertIsNone(numpy.testing.assert_array_equal(list(unconsistencies['translation'].keys()), ['0/2', '1/2'])) + self.assertIsNone(numpy.testing.assert_array_equal(list(unconsistencies['translation']['0/2'].keys()), ['current', 'expected'])) + self.assertIsNone(numpy.testing.assert_array_equal(list(unconsistencies['translation']['1/2'].keys()), ['current', 'expected'])) + + def test_estimate_pose_from_single_marker(self): + """Test ArUcoMarkerGroup pose estimation from single marker.""" + + self.new_from_obj() + self.setup_markers() + + # Edit marke pose + self.scene_markers[0].translation = numpy.array([1., 1., 5.]) + self.scene_markers[0].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) + + # Estimate pose + tvec, rmat = self.aruco_markers_group.estimate_pose_from_single_marker(self.scene_markers[0]) + + self.assertIsNone(numpy.testing.assert_array_equal(tvec, [1., 1., 5.])) + self.assertIsNone(numpy.testing.assert_array_equal(rmat, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) + + def test_estimate_pose_from_markers(self): + """Test ArUcoMarkerGroup pose estimation from markers.""" + + self.new_from_obj() + self.setup_markers() + + # Edit markers pose + self.scene_markers[0].translation = numpy.array([1., 1., 5.]) + self.scene_markers[0].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) + + self.scene_markers[1].translation = numpy.array([11., 11., 5.]) + self.scene_markers[1].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) + + self.scene_markers[2].translation = numpy.array([1., 11., 5.]) + self.scene_markers[2].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) + + # Estimate pose + tvec, rmat = self.aruco_markers_group.estimate_pose_from_markers(self.scene_markers) + + self.assertIsNone(numpy.testing.assert_array_equal(tvec, [1., 1., 5.])) + self.assertIsNone(numpy.testing.assert_array_equal(rmat, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) + + @unittest.skip("ArUcoMarkerGroup estimate_pose_from_axis_markers method is broken.") + def test_estimate_pose_from_axis_markers(self): + """Test ArUcoMarkerGroup pose estimation from axis markers.""" + + self.new_from_obj() + self.setup_markers() + + # Edit markers pose + self.scene_markers[0].translation = numpy.array([1., 1., 5.]) + self.scene_markers[0].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) + + self.scene_markers[1].translation = numpy.array([11., 11., 5.]) + self.scene_markers[1].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) + + self.scene_markers[2].translation = numpy.array([1., 11., 5.]) + self.scene_markers[2].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) + + # Estimate pose + tvec, rmat = self.aruco_markers_group.estimate_pose_from_axis_markers(self.scene_markers[2], self.scene_markers[1], self.scene_markers[0]) + + self.assertIsNone(numpy.testing.assert_array_equal(tvec, [1., 1., 5.])) + self.assertIsNone(numpy.testing.assert_array_equal(rmat, [[1., 0., 0.], [0., -1., 0.], [0., 0., -1.]])) + +if __name__ == '__main__': + + unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/__init__.py b/src/argaze.test/ArUcoMarker/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/argaze.test/ArUcoMarker/utils/aoi_3d.obj b/src/argaze.test/ArUcoMarker/utils/aoi_3d.obj new file mode 100644 index 0000000..92e85bd --- /dev/null +++ b/src/argaze.test/ArUcoMarker/utils/aoi_3d.obj @@ -0,0 +1,7 @@ +o Test +v 0.000000 0.000000 0.000000 +v 25.000000 0.000000 0.000000 +v 0.000000 14.960000 0.000000 +v 25.000000 14.960000 0.000000 +s off +f 1 2 4 3 diff --git a/src/argaze.test/ArUcoMarker/utils/aruco_camera.json b/src/argaze.test/ArUcoMarker/utils/aruco_camera.json new file mode 100644 index 0000000..980dc9f --- /dev/null +++ b/src/argaze.test/ArUcoMarker/utils/aruco_camera.json @@ -0,0 +1,98 @@ +{ + "name": "TestArUcoCamera", + "size": [1920, 1080], + "aruco_detector": { + "dictionary": { + "name": "DICT_ARUCO_ORIGINAL" + }, + "optic_parameters": { + "rms": 1.0, + "dimensions": [ + 1920, + 1080 + ], + "K": [ + [ + 1.0, + 0.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.0, + 0.0, + 1.0 + ] + ], + "D": [ + -1.0, + -0.5, + 0.0, + 0.5, + 1.0 + ] + }, + "parameters": { + "cornerRefinementMethod": 3, + "aprilTagQuadSigma": 2, + "aprilTagDeglitch": 1 + } + }, + "scenes": { + "TestSceneA" : { + "aruco_markers_group": { + "dictionary": "DICT_ARUCO_ORIGINAL", + "places": { + "0": { + "translation": [1, 0, 0], + "rotation": [0, 0, 0], + "size": 3.0 + }, + "1": { + "translation": [0, 1, 0], + "rotation": [0, 90, 0], + "size": 3.0 + } + } + }, + "layers": { + "Main" : { + "aoi_scene": "aoi_3d.obj" + } + }, + "angle_tolerance": 1.0, + "distance_tolerance": 2.0 + }, + "TestSceneB" : { + "aruco_markers_group": { + "dictionary": "DICT_ARUCO_ORIGINAL", + "places": { + "0": { + "translation": [1, 0, 0], + "rotation": [0, 0, 0], + "size": 3.0 + }, + "1": { + "translation": [0, 1, 0], + "rotation": [0, 90, 0], + "size": 3.0 + } + } + }, + "layers": { + "Main" : { + "aoi_scene": "aoi_3d.obj" + } + }, + "angle_tolerance": 1.0, + "distance_tolerance": 2.0 + } + }, + "layers": { + "Main": {} + } +} \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/utils/detector.json b/src/argaze.test/ArUcoMarker/utils/detector.json new file mode 100644 index 0000000..8aada6d --- /dev/null +++ b/src/argaze.test/ArUcoMarker/utils/detector.json @@ -0,0 +1,42 @@ +{ + "dictionary": { + "name": "DICT_ARUCO_ORIGINAL" + }, + "marker_size": 3.0, + "optic_parameters": { + "rms": 1.0, + "dimensions": [ + 1920, + 1080 + ], + "K": [ + [ + 1.0, + 0.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.0, + 0.0, + 1.0 + ] + ], + "D": [ + -1.0, + -0.5, + 0.0, + 0.5, + 1.0 + ] + }, + "parameters": { + "cornerRefinementMethod": 3, + "aprilTagQuadSigma": 2, + "aprilTagDeglitch": 1 + } +} \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/utils/detector_parameters.json b/src/argaze.test/ArUcoMarker/utils/detector_parameters.json new file mode 100644 index 0000000..d26a3fa --- /dev/null +++ b/src/argaze.test/ArUcoMarker/utils/detector_parameters.json @@ -0,0 +1,5 @@ +{ + "cornerRefinementMethod": 3, + "aprilTagQuadSigma": 2, + "aprilTagDeglitch": 1 +} \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/utils/full_hd_board.png b/src/argaze.test/ArUcoMarker/utils/full_hd_board.png new file mode 100644 index 0000000..d30b300 Binary files /dev/null and b/src/argaze.test/ArUcoMarker/utils/full_hd_board.png differ diff --git a/src/argaze.test/ArUcoMarker/utils/full_hd_marker.png b/src/argaze.test/ArUcoMarker/utils/full_hd_marker.png new file mode 100644 index 0000000..42146fe Binary files /dev/null and b/src/argaze.test/ArUcoMarker/utils/full_hd_marker.png differ diff --git a/src/argaze.test/ArUcoMarker/utils/optic_parameters.json b/src/argaze.test/ArUcoMarker/utils/optic_parameters.json new file mode 100644 index 0000000..988731c --- /dev/null +++ b/src/argaze.test/ArUcoMarker/utils/optic_parameters.json @@ -0,0 +1,31 @@ +{ + "rms": 1.0, + "dimensions": [ + 1920, + 1080 + ], + "K": [ + [ + 1.0, + 0.0, + 1.0 + ], + [ + 0.0, + 1.0, + 1.0 + ], + [ + 0.0, + 0.0, + 1.0 + ] + ], + "D": [ + -1.0, + -0.5, + 0.0, + 0.5, + 1.0 + ] +} \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarker/utils/scene.json b/src/argaze.test/ArUcoMarker/utils/scene.json new file mode 100644 index 0000000..bdd9ae8 --- /dev/null +++ b/src/argaze.test/ArUcoMarker/utils/scene.json @@ -0,0 +1,20 @@ +{ + "dictionary": { + "name": "DICT_ARUCO_ORIGINAL" + }, + "marker_size": 1, + "places": { + "0": { + "translation": [0, 0, 0], + "rotation": [0, 0, 0] + }, + "1": { + "translation": [10, 10, 0], + "rotation": [0, 0, 0] + }, + "2": { + "translation": [0, 10, 0], + "rotation": [0, 0, 0] + } + } +} diff --git a/src/argaze.test/ArUcoMarker/utils/scene.obj b/src/argaze.test/ArUcoMarker/utils/scene.obj new file mode 100644 index 0000000..1eb9f81 --- /dev/null +++ b/src/argaze.test/ArUcoMarker/utils/scene.obj @@ -0,0 +1,22 @@ +# .OBJ file for ArUcoMarkerGroup unitary test +o DICT_ARUCO_ORIGINAL#0_Marker +v -0.500000 -0.500000 0.000000 +v 0.500000 -0.500000 0.000000 +v -0.500000 0.500000 0.000000 +v 0.500000 0.500000 0.000000 +vn 0.0000 0.0000 1.0000 +f 1//1 2//1 4//1 3//1 +o DICT_ARUCO_ORIGINAL#1_Marker +v 9.500000 9.500000 0.000000 +v 10.500000 9.500000 0.000000 +v 9.500000 10.500000 0.000000 +v 10.500000 10.500000 0.000000 +vn 0.0000 0.0000 1.0000 +f 5//2 6//2 8//2 7//2 +o DICT_ARUCO_ORIGINAL#2_Marker +v -0.500000 9.500000 0.000000 +v 0.500000 9.500000 0.000000 +v -0.500000 10.500000 0.000000 +v 0.500000 10.500000 0.000000 +vn 0.0000 0.0000 1.0000 +f 9//3 10//3 12//3 11//3 diff --git a/src/argaze.test/ArUcoMarkers/ArUcoBoard.py b/src/argaze.test/ArUcoMarkers/ArUcoBoard.py deleted file mode 100644 index 0bfa568..0000000 --- a/src/argaze.test/ArUcoMarkers/ArUcoBoard.py +++ /dev/null @@ -1,50 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import unittest -import os - -from argaze.ArUcoMarkers import ArUcoBoard, ArUcoMarkersDictionary - -import numpy - -class TestArUcoBoardClass(unittest.TestCase): - """Test ArUcoBoard class.""" - - def test_new(self): - """Test ArUcoBoard creation using a dictionary instance.""" - - columns = 4 - rows = 3 - square_size = 2 - marker_size = 1 - - # Check ArUco board creation - aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary('DICT_APRILTAG_16h5') - aruco_board = ArUcoBoard.ArUcoBoard(columns, rows, square_size, marker_size, aruco_dictionary) - - # Check ArUco board dictionary name - self.assertEqual(aruco_board.dictionary.name, 'DICT_APRILTAG_16h5') - self.assertIsNone(numpy.testing.assert_array_equal(aruco_board.identifiers, [i for i in range(int((columns*rows)/2))])) - self.assertIsNone(numpy.testing.assert_array_equal(aruco_board.size, [columns, rows])) - self.assertEqual(aruco_board.markers_number, int((columns*rows)/2)) - self.assertEqual(aruco_board.corners_number, (columns-1)*(rows-1)) - -if __name__ == '__main__': - - unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/ArUcoCamera.py b/src/argaze.test/ArUcoMarkers/ArUcoCamera.py deleted file mode 100644 index eb930ab..0000000 --- a/src/argaze.test/ArUcoMarkers/ArUcoCamera.py +++ /dev/null @@ -1,80 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import unittest -import os - -import argaze - -import numpy - -class TestArUcoCameraClass(unittest.TestCase): - """Test ArUcoCamera class.""" - - def test_from_json(self): - """Test ArUcoCamera creation from json file.""" - - # Edit test aruco camera file path - current_directory = os.path.dirname(os.path.abspath(__file__)) - json_filepath = os.path.join(current_directory, 'utils/aruco_camera.json') - - # Load test aruco camera - with argaze.load(json_filepath) as aruco_camera: - - # Check aruco camera meta data - self.assertEqual(aruco_camera.name, "TestArUcoCamera") - - # Check ArUco detector - self.assertEqual(aruco_camera.aruco_detector.dictionary.name, "DICT_ARUCO_ORIGINAL") - self.assertEqual(aruco_camera.aruco_detector.parameters.cornerRefinementMethod, 3) - self.assertEqual(aruco_camera.aruco_detector.parameters.aprilTagQuadSigma, 2) - self.assertEqual(aruco_camera.aruco_detector.parameters.aprilTagDeglitch, 1) - - # Check ArUco detector optic parameters - self.assertEqual(aruco_camera.aruco_detector.optic_parameters.rms, 1.0) - self.assertIsNone(numpy.testing.assert_array_equal(aruco_camera.aruco_detector.optic_parameters.dimensions, [1920, 1080])) - self.assertIsNone(numpy.testing.assert_array_equal(aruco_camera.aruco_detector.optic_parameters.K, [[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]])) - self.assertIsNone(numpy.testing.assert_array_equal(aruco_camera.aruco_detector.optic_parameters.D, [-1.0, -0.5, 0.0, 0.5, 1.0])) - - # Check camera scenes - self.assertEqual(len(aruco_camera.scenes), 2) - self.assertIsNone(numpy.testing.assert_array_equal(list(aruco_camera.scenes.keys()), ["TestSceneA", "TestSceneB"])) - - # Load test scene - ar_scene = aruco_camera.scenes["TestSceneA"] - - # Check Aruco scene - self.assertEqual(len(ar_scene.aruco_markers_group.places), 2) - self.assertIsNone(numpy.testing.assert_allclose(ar_scene.aruco_markers_group.places[0].corners[0], [-0.5, 1.5, 0.], rtol=0, atol=1e-3)) - self.assertEqual(ar_scene.aruco_markers_group.places[0].marker.identifier, 0) - - self.assertIsNone(numpy.testing.assert_allclose(ar_scene.aruco_markers_group.places[1].corners[0], [0., 2.5, -1.5], rtol=0, atol=1e-3)) - self.assertEqual(ar_scene.aruco_markers_group.places[1].marker.identifier, 1) - - # Check layers and AOI scene - self.assertEqual(len(ar_scene.layers.items()), 1) - self.assertEqual(len(ar_scene.layers["Main"].aoi_scene), 1) - self.assertEqual(ar_scene.layers["Main"].aoi_scene['Test'].points_number, 4) - - # Check ArScene - self.assertEqual(ar_scene.angle_tolerance, 1.0) - self.assertEqual(ar_scene.distance_tolerance, 2.0) - -if __name__ == '__main__': - - unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/ArUcoDetector.py b/src/argaze.test/ArUcoMarkers/ArUcoDetector.py deleted file mode 100644 index 62e8a09..0000000 --- a/src/argaze.test/ArUcoMarkers/ArUcoDetector.py +++ /dev/null @@ -1,149 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import unittest -import os -import math - -from argaze.ArUcoMarkers import ArUcoMarkersDictionary, ArUcoOpticCalibrator, ArUcoDetector, ArUcoBoard - -import cv2 as cv -import numpy - -class TestDetectorParametersClass(unittest.TestCase): - """Test DetectorParameters class.""" - - def test_from_json(self): - """Test DetectorParameters creation from json file.""" - - # Edit file path - current_directory = os.path.dirname(os.path.abspath(__file__)) - json_filepath = os.path.join(current_directory, 'utils/detector_parameters.json') - - # Load file - detector_parameters = ArUcoDetector.DetectorParameters.from_json(json_filepath) - - # Check data - self.assertEqual(detector_parameters.cornerRefinementMethod, 3) - self.assertEqual(detector_parameters.aprilTagQuadSigma, 2) - self.assertEqual(detector_parameters.aprilTagDeglitch, 1) - - # Check bad data access fails - with self.assertRaises(AttributeError): - - detector_parameters.unknown_data = 1 - -class TestArUcoDetectorClass(unittest.TestCase): - """Test ArUcoDetector class.""" - - def test_new(self): - """Test ArUcoDetector creation.""" - - aruco_detector = ArUcoDetector.ArUcoDetector(marker_size=3) - - # Check ArUcoDetector creation - self.assertEqual(aruco_detector.dictionary.name, 'DICT_ARUCO_ORIGINAL') - self.assertEqual(aruco_detector.marker_size, 3) - self.assertIsNone(numpy.testing.assert_array_equal(aruco_detector.optic_parameters.dimensions, [0, 0])) - self.assertEqual(aruco_detector.detected_markers_number(), 0) - self.assertEqual(aruco_detector.detected_markers(), {}) - - aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary('DICT_APRILTAG_16h5') - aruco_detector = ArUcoDetector.ArUcoDetector(aruco_dictionary, 5.2) - - # Check ArUcoDetector creation - self.assertEqual(aruco_detector.dictionary.name, 'DICT_APRILTAG_16h5') - self.assertEqual(aruco_detector.marker_size, 5.2) - self.assertIsNone(numpy.testing.assert_array_equal(aruco_detector.optic_parameters.dimensions, [0, 0])) - self.assertEqual(aruco_detector.detected_markers_number(), 0) - self.assertEqual(aruco_detector.detected_markers(), {}) - - def test_from_json(self): - """Test ArUcoDetector creation.""" - - # Edit file path - current_directory = os.path.dirname(os.path.abspath(__file__)) - json_filepath = os.path.join(current_directory, 'utils/detector.json') - - # Load file - aruco_detector = ArUcoDetector.ArUcoDetector.from_json(json_filepath) - - # Check ArUcoDetector creation - self.assertEqual(aruco_detector.dictionary.name, 'DICT_ARUCO_ORIGINAL') - self.assertEqual(aruco_detector.marker_size, 3) - self.assertIsNone(numpy.testing.assert_array_equal(aruco_detector.optic_parameters.dimensions, [1920, 1080])) - self.assertEqual(aruco_detector.parameters.cornerRefinementMethod, 3) - self.assertEqual(aruco_detector.parameters.aprilTagQuadSigma, 2) - self.assertEqual(aruco_detector.parameters.aprilTagDeglitch, 1) - - def test_detect(self): - """Test detect method.""" - - aruco_detector = ArUcoDetector.ArUcoDetector(marker_size=3) - - # Load picture Full HD to test ArUcoMarker detection - current_directory = os.path.dirname(os.path.abspath(__file__)) - image = cv.imread(os.path.join(current_directory, 'utils/full_hd_marker.png')) - - # Check ArUcoMarker detection - aruco_detector.detect_markers(image) - - self.assertEqual(aruco_detector.detected_markers_number(), 1) - - self.assertEqual(aruco_detector.detected_markers()[0].dictionary, aruco_detector.dictionary) - self.assertEqual(aruco_detector.detected_markers()[0].identifier, 0) - self.assertEqual(aruco_detector.detected_markers()[0].size, 3) - - # Check corner positions with -/+ 10 pixels precision - self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].corners[0][0].astype(int), numpy.array([3823, 2073]), decimal=-1)) - self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].corners[0][1].astype(int), numpy.array([4177, 2073]), decimal=-1)) - self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].corners[0][2].astype(int), numpy.array([4177, 2427]), decimal=-1)) - self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].corners[0][3].astype(int), numpy.array([3823, 2427]), decimal=-1)) - - # Check marker pose estimation - aruco_detector.estimate_markers_pose([0]) - - # Check marker translation with -/+ 0.1 cm precision and rotation with -/+ 0.001 radian precision - self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].translation, numpy.array([33.87, 19.05, 0.]), decimal=1)) - self.assertIsNone(numpy.testing.assert_almost_equal(aruco_detector.detected_markers()[0].rotation, numpy.array([[1., 0., 0.], [0., -1., 0.], [0., 0., -1.]]), decimal=3)) - - # Check detect metrics - detect_count, markers_count = aruco_detector.detection_metrics - self.assertEqual(detect_count, 1) - self.assertEqual(markers_count[0], 1) - - def test_detect_board(self): - """Test detect board method.""" - - aruco_board = ArUcoBoard.ArUcoBoard(7, 5, 5, 3) - aruco_detector = ArUcoDetector.ArUcoDetector(marker_size=3) - - # Load picture Full HD to test ArUcoMarker board detection - current_directory = os.path.dirname(os.path.abspath(__file__)) - image = cv.imread(os.path.join(current_directory, 'utils/full_hd_board.png')) - - # Check ArUcoMarker board detection - aruco_detector.detect_board(image, aruco_board, aruco_board.markers_number) - - self.assertEqual(aruco_detector.board_corners_number(), aruco_board.corners_number) - self.assertEqual(len(aruco_detector.board_corners()), 24) - self.assertEqual(len(aruco_detector.board_corners_identifier()), 24) - -if __name__ == '__main__': - - unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/ArUcoMarker.py b/src/argaze.test/ArUcoMarkers/ArUcoMarker.py deleted file mode 100644 index de88623..0000000 --- a/src/argaze.test/ArUcoMarkers/ArUcoMarker.py +++ /dev/null @@ -1,40 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import unittest - -from argaze.ArUcoMarkers import ArUcoMarkersDictionary, ArUcoMarker - -class TestArUcoMarkerClass(unittest.TestCase): - """Test ArUcoMarker class.""" - - def test_new(self): - """Test ArUcoMarker creation.""" - - # Check DICT_ARUCO_ORIGINAL ArUcoMarker creation - aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary('DICT_ARUCO_ORIGINAL') - - aruco_marker = aruco_dictionary.create_marker(0, 3) - - self.assertEqual(aruco_marker.dictionary, aruco_dictionary) - self.assertEqual(aruco_marker.identifier, 0) - self.assertEqual(aruco_marker.size, 3) - -if __name__ == '__main__': - - unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/ArUcoMarkersDictionary.py b/src/argaze.test/ArUcoMarkers/ArUcoMarkersDictionary.py deleted file mode 100644 index 7a5e9e8..0000000 --- a/src/argaze.test/ArUcoMarkers/ArUcoMarkersDictionary.py +++ /dev/null @@ -1,50 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import unittest - -from argaze.ArUcoMarkers import ArUcoMarkersDictionary - -class TestArUcoMarkersDictionaryClass(unittest.TestCase): - """Test ArUcoMarkersDictionary class.""" - - def test_new(self): - """Test ArUcoMarkersDictionary creation.""" - - # Check that ArUcoMarkersDictionary creation fails with bad name - with self.assertRaises(NameError): - - aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary('BAD_DICT_NAME') - - # Check default ArUcoMarkersDictionary creation - aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary() - - self.assertEqual(aruco_dictionary.name, 'DICT_ARUCO_ORIGINAL') - self.assertEqual(aruco_dictionary.format, '5X5') - self.assertEqual(aruco_dictionary.number, 1024) - - # Check DICT_APRILTAG_16h5 ArUcoMarkersDictionary creation - aruco_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary('DICT_APRILTAG_16h5') - - self.assertEqual(aruco_dictionary.name, 'DICT_APRILTAG_16h5') - self.assertEqual(aruco_dictionary.format, '4X4') - self.assertEqual(aruco_dictionary.number, 30) - -if __name__ == '__main__': - - unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/ArUcoOpticCalibrator.py b/src/argaze.test/ArUcoMarkers/ArUcoOpticCalibrator.py deleted file mode 100644 index 79d2ead..0000000 --- a/src/argaze.test/ArUcoMarkers/ArUcoOpticCalibrator.py +++ /dev/null @@ -1,61 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import unittest -import os - -from argaze.ArUcoMarkers import ArUcoOpticCalibrator - -import numpy - -class TestOpticParametersClass(unittest.TestCase): - """Test OpticParameters class.""" - - def test_new(self): - """Test OpticParameters creation.""" - - # Check defaut optic parameters creation - optic_parameters = ArUcoOpticCalibrator.OpticParameters() - - # Check ArUco optic parameters - self.assertEqual(optic_parameters.rms, 0.0) - - #self.assertEqual(type(optic_parameters.K), numpy.array) - - self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.dimensions, [0, 0])) - self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.K, ArUcoOpticCalibrator.K0)) - self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.D, ArUcoOpticCalibrator.D0)) - - def test_from_json(self): - - # Edit optic parameters file path - current_directory = os.path.dirname(os.path.abspath(__file__)) - json_filepath = os.path.join(current_directory, 'utils/optic_parameters.json') - - # Load optic parameters - optic_parameters = ArUcoOpticCalibrator.OpticParameters.from_json(json_filepath) - - # Check ArUco camera - self.assertEqual(optic_parameters.rms, 1.0) - self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.dimensions, [1920, 1080])) - self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.K, [[1.0, 0.0, 1.0], [0.0, 1.0, 1.0], [0.0, 0.0, 1.0]])) - self.assertIsNone(numpy.testing.assert_array_equal(optic_parameters.D, [-1.0, -0.5, 0.0, 0.5, 1.0])) - -if __name__ == '__main__': - - unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/ArUcoScene.py b/src/argaze.test/ArUcoMarkers/ArUcoScene.py deleted file mode 100644 index f29b1d3..0000000 --- a/src/argaze.test/ArUcoMarkers/ArUcoScene.py +++ /dev/null @@ -1,227 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import unittest -import os -import math - -from argaze.ArUcoMarkers import ArUcoMarkersGroup, ArUcoMarker - -import cv2 as cv -import numpy - -class TestArUcoMarkersGroupClass(unittest.TestCase): - - def new_from_obj(self): - - # Edit file path - current_directory = os.path.dirname(os.path.abspath(__file__)) - obj_filepath = os.path.join(current_directory, 'utils/scene.obj') - - # Load file - self.aruco_markers_group = ArUcoMarkersGroup.ArUcoMarkersGroup.from_obj(obj_filepath) - - def new_from_json(self): - - # Edit file path - current_directory = os.path.dirname(os.path.abspath(__file__)) - json_filepath = os.path.join(current_directory, 'utils/scene.json') - - # Load file - self.aruco_markers_group = ArUcoMarkersGroup.ArUcoMarkersGroup.from_json(json_filepath) - - def setup_markers(self): - - # Prepare detected markers - self.detected_markers = { - 0: ArUcoMarker.ArUcoMarker('DICT_ARUCO_ORIGINAL', 0, 1.), - 1: ArUcoMarker.ArUcoMarker('DICT_ARUCO_ORIGINAL', 1, 1.), - 2: ArUcoMarker.ArUcoMarker('DICT_ARUCO_ORIGINAL', 2, 1.), - 3: ArUcoMarker.ArUcoMarker('DICT_ARUCO_ORIGINAL', 3, 1.) - } - - # Prepare scene markers and remaining markers - self.scene_markers, self.remaining_markers = self.aruco_markers_group.filter_markers(self.detected_markers()) - - def test_new_from_obj(self): - """Test ArUcoMarkersGroup creation.""" - - self.new_from_obj() - self.setup_markers() - - # Check ArUcoMarkersGroup creation - self.assertEqual(len(self.aruco_markers_group.places), 3) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.identifiers, [0, 1, 2])) - self.assertEqual(self.aruco_markers_group.marker_size, 1.) - - self.assertEqual(self.aruco_markers_group.places[0].marker.identifier, 0) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[0].translation, [0., 0., 0.])) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[0].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) - - self.assertEqual(self.aruco_markers_group.places[1].marker.identifier, 1) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[1].translation, [10., 10., 0.])) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[1].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) - - self.assertEqual(self.aruco_markers_group.places[2].marker.identifier, 2) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[2].translation, [0., 10., 0.])) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[2].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) - - def test_new_from_json(self): - """Test ArUcoMarkersGroup creation.""" - - self.new_from_json() - self.setup_markers() - - # Check ArUcoMarkersGroup creation - self.assertEqual(len(self.aruco_markers_group.places), 3) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.identifiers, [0, 1, 2])) - self.assertEqual(self.aruco_markers_group.marker_size, 1.) - - self.assertEqual(self.aruco_markers_group.places[0].marker.identifier, 0) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[0].translation, [0., 0., 0.])) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[0].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) - - self.assertEqual(self.aruco_markers_group.places[1].marker.identifier, 1) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[1].translation, [10., 10., 0.])) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[1].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) - - self.assertEqual(self.aruco_markers_group.places[2].marker.identifier, 2) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[2].translation, [0., 10., 0.])) - self.assertIsNone(numpy.testing.assert_array_equal(self.aruco_markers_group.places[2].rotation, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) - - def test_filter_markers(self): - """Test ArUcoMarkersGroup markers filtering.""" - - self.new_from_obj() - self.setup_markers() - - # Check scene markers and remaining markers - self.assertEqual(len(self.scene_markers), 3) - self.assertEqual(len(self.remaining_markers), 1) - - self.assertIsNone(numpy.testing.assert_array_equal(list(self.scene_markers.keys()), self.aruco_markers_group.identifiers)) - self.assertIsNone(numpy.testing.assert_array_equal(list(self.remaining_markers.keys()), [3])) - - def test_check_markers_consistency(self): - """Test ArUcoMarkersGroup markers consistency checking.""" - - self.new_from_obj() - self.setup_markers() - - # Edit consistent marker poses - self.scene_markers[0].translation = numpy.array([1., 1., 5.]) - self.scene_markers[0].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) - - self.scene_markers[1].translation = numpy.array([11., 11., 5.]) - self.scene_markers[1].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) - - self.scene_markers[2].translation = numpy.array([1., 11., 5.]) - self.scene_markers[2].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) - - # Check consistency - consistent_markers, unconsistent_markers, unconsistencies = self.aruco_markers_group.check_markers_consistency(self.scene_markers, 1, 1) - - # Check consistent markers, unconsistent markers and unconsistencies - self.assertEqual(len(consistent_markers), 3) - self.assertEqual(len(unconsistent_markers), 0) - self.assertEqual(len(unconsistencies['rotation']), 0) - self.assertEqual(len(unconsistencies['translation']), 0) - - self.assertIsNone(numpy.testing.assert_array_equal(list(consistent_markers.keys()), self.aruco_markers_group.identifiers)) - - # Edit unconsistent marker poses - self.scene_markers[2].translation = numpy.array([5., 15., 5.]) - - # Check consistency - consistent_markers, unconsistent_markers, unconsistencies = self.aruco_markers_group.check_markers_consistency(self.scene_markers, 1, 1) - - # Check consistent markers, unconsistent markers and unconsistencies - self.assertEqual(len(consistent_markers), 2) - self.assertEqual(len(unconsistent_markers), 1) - self.assertEqual(len(unconsistencies['rotation']), 0) - self.assertEqual(len(unconsistencies['translation']), 2) - - self.assertIsNone(numpy.testing.assert_array_equal(list(unconsistent_markers.keys()), [2])) - self.assertIsNone(numpy.testing.assert_array_equal(list(unconsistencies['translation'].keys()), ['0/2', '1/2'])) - self.assertIsNone(numpy.testing.assert_array_equal(list(unconsistencies['translation']['0/2'].keys()), ['current', 'expected'])) - self.assertIsNone(numpy.testing.assert_array_equal(list(unconsistencies['translation']['1/2'].keys()), ['current', 'expected'])) - - def test_estimate_pose_from_single_marker(self): - """Test ArUcoMarkersGroup pose estimation from single marker.""" - - self.new_from_obj() - self.setup_markers() - - # Edit marke pose - self.scene_markers[0].translation = numpy.array([1., 1., 5.]) - self.scene_markers[0].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) - - # Estimate pose - tvec, rmat = self.aruco_markers_group.estimate_pose_from_single_marker(self.scene_markers[0]) - - self.assertIsNone(numpy.testing.assert_array_equal(tvec, [1., 1., 5.])) - self.assertIsNone(numpy.testing.assert_array_equal(rmat, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) - - def test_estimate_pose_from_markers(self): - """Test ArUcoMarkersGroup pose estimation from markers.""" - - self.new_from_obj() - self.setup_markers() - - # Edit markers pose - self.scene_markers[0].translation = numpy.array([1., 1., 5.]) - self.scene_markers[0].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) - - self.scene_markers[1].translation = numpy.array([11., 11., 5.]) - self.scene_markers[1].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) - - self.scene_markers[2].translation = numpy.array([1., 11., 5.]) - self.scene_markers[2].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) - - # Estimate pose - tvec, rmat = self.aruco_markers_group.estimate_pose_from_markers(self.scene_markers) - - self.assertIsNone(numpy.testing.assert_array_equal(tvec, [1., 1., 5.])) - self.assertIsNone(numpy.testing.assert_array_equal(rmat, [[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]])) - - @unittest.skip("ArUcoMarkersGroup estimate_pose_from_axis_markers method is broken.") - def test_estimate_pose_from_axis_markers(self): - """Test ArUcoMarkersGroup pose estimation from axis markers.""" - - self.new_from_obj() - self.setup_markers() - - # Edit markers pose - self.scene_markers[0].translation = numpy.array([1., 1., 5.]) - self.scene_markers[0].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) - - self.scene_markers[1].translation = numpy.array([11., 11., 5.]) - self.scene_markers[1].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) - - self.scene_markers[2].translation = numpy.array([1., 11., 5.]) - self.scene_markers[2].rotation = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) - - # Estimate pose - tvec, rmat = self.aruco_markers_group.estimate_pose_from_axis_markers(self.scene_markers[2], self.scene_markers[1], self.scene_markers[0]) - - self.assertIsNone(numpy.testing.assert_array_equal(tvec, [1., 1., 5.])) - self.assertIsNone(numpy.testing.assert_array_equal(rmat, [[1., 0., 0.], [0., -1., 0.], [0., 0., -1.]])) - -if __name__ == '__main__': - - unittest.main() \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/__init__.py b/src/argaze.test/ArUcoMarkers/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/argaze.test/ArUcoMarkers/utils/aoi_3d.obj b/src/argaze.test/ArUcoMarkers/utils/aoi_3d.obj deleted file mode 100644 index 92e85bd..0000000 --- a/src/argaze.test/ArUcoMarkers/utils/aoi_3d.obj +++ /dev/null @@ -1,7 +0,0 @@ -o Test -v 0.000000 0.000000 0.000000 -v 25.000000 0.000000 0.000000 -v 0.000000 14.960000 0.000000 -v 25.000000 14.960000 0.000000 -s off -f 1 2 4 3 diff --git a/src/argaze.test/ArUcoMarkers/utils/aruco_camera.json b/src/argaze.test/ArUcoMarkers/utils/aruco_camera.json deleted file mode 100644 index 980dc9f..0000000 --- a/src/argaze.test/ArUcoMarkers/utils/aruco_camera.json +++ /dev/null @@ -1,98 +0,0 @@ -{ - "name": "TestArUcoCamera", - "size": [1920, 1080], - "aruco_detector": { - "dictionary": { - "name": "DICT_ARUCO_ORIGINAL" - }, - "optic_parameters": { - "rms": 1.0, - "dimensions": [ - 1920, - 1080 - ], - "K": [ - [ - 1.0, - 0.0, - 1.0 - ], - [ - 0.0, - 1.0, - 1.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], - "D": [ - -1.0, - -0.5, - 0.0, - 0.5, - 1.0 - ] - }, - "parameters": { - "cornerRefinementMethod": 3, - "aprilTagQuadSigma": 2, - "aprilTagDeglitch": 1 - } - }, - "scenes": { - "TestSceneA" : { - "aruco_markers_group": { - "dictionary": "DICT_ARUCO_ORIGINAL", - "places": { - "0": { - "translation": [1, 0, 0], - "rotation": [0, 0, 0], - "size": 3.0 - }, - "1": { - "translation": [0, 1, 0], - "rotation": [0, 90, 0], - "size": 3.0 - } - } - }, - "layers": { - "Main" : { - "aoi_scene": "aoi_3d.obj" - } - }, - "angle_tolerance": 1.0, - "distance_tolerance": 2.0 - }, - "TestSceneB" : { - "aruco_markers_group": { - "dictionary": "DICT_ARUCO_ORIGINAL", - "places": { - "0": { - "translation": [1, 0, 0], - "rotation": [0, 0, 0], - "size": 3.0 - }, - "1": { - "translation": [0, 1, 0], - "rotation": [0, 90, 0], - "size": 3.0 - } - } - }, - "layers": { - "Main" : { - "aoi_scene": "aoi_3d.obj" - } - }, - "angle_tolerance": 1.0, - "distance_tolerance": 2.0 - } - }, - "layers": { - "Main": {} - } -} \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/utils/detector.json b/src/argaze.test/ArUcoMarkers/utils/detector.json deleted file mode 100644 index 8aada6d..0000000 --- a/src/argaze.test/ArUcoMarkers/utils/detector.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "dictionary": { - "name": "DICT_ARUCO_ORIGINAL" - }, - "marker_size": 3.0, - "optic_parameters": { - "rms": 1.0, - "dimensions": [ - 1920, - 1080 - ], - "K": [ - [ - 1.0, - 0.0, - 1.0 - ], - [ - 0.0, - 1.0, - 1.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], - "D": [ - -1.0, - -0.5, - 0.0, - 0.5, - 1.0 - ] - }, - "parameters": { - "cornerRefinementMethod": 3, - "aprilTagQuadSigma": 2, - "aprilTagDeglitch": 1 - } -} \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/utils/detector_parameters.json b/src/argaze.test/ArUcoMarkers/utils/detector_parameters.json deleted file mode 100644 index d26a3fa..0000000 --- a/src/argaze.test/ArUcoMarkers/utils/detector_parameters.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "cornerRefinementMethod": 3, - "aprilTagQuadSigma": 2, - "aprilTagDeglitch": 1 -} \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/utils/full_hd_board.png b/src/argaze.test/ArUcoMarkers/utils/full_hd_board.png deleted file mode 100644 index d30b300..0000000 Binary files a/src/argaze.test/ArUcoMarkers/utils/full_hd_board.png and /dev/null differ diff --git a/src/argaze.test/ArUcoMarkers/utils/full_hd_marker.png b/src/argaze.test/ArUcoMarkers/utils/full_hd_marker.png deleted file mode 100644 index 42146fe..0000000 Binary files a/src/argaze.test/ArUcoMarkers/utils/full_hd_marker.png and /dev/null differ diff --git a/src/argaze.test/ArUcoMarkers/utils/optic_parameters.json b/src/argaze.test/ArUcoMarkers/utils/optic_parameters.json deleted file mode 100644 index 988731c..0000000 --- a/src/argaze.test/ArUcoMarkers/utils/optic_parameters.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "rms": 1.0, - "dimensions": [ - 1920, - 1080 - ], - "K": [ - [ - 1.0, - 0.0, - 1.0 - ], - [ - 0.0, - 1.0, - 1.0 - ], - [ - 0.0, - 0.0, - 1.0 - ] - ], - "D": [ - -1.0, - -0.5, - 0.0, - 0.5, - 1.0 - ] -} \ No newline at end of file diff --git a/src/argaze.test/ArUcoMarkers/utils/scene.json b/src/argaze.test/ArUcoMarkers/utils/scene.json deleted file mode 100644 index bdd9ae8..0000000 --- a/src/argaze.test/ArUcoMarkers/utils/scene.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "dictionary": { - "name": "DICT_ARUCO_ORIGINAL" - }, - "marker_size": 1, - "places": { - "0": { - "translation": [0, 0, 0], - "rotation": [0, 0, 0] - }, - "1": { - "translation": [10, 10, 0], - "rotation": [0, 0, 0] - }, - "2": { - "translation": [0, 10, 0], - "rotation": [0, 0, 0] - } - } -} diff --git a/src/argaze.test/ArUcoMarkers/utils/scene.obj b/src/argaze.test/ArUcoMarkers/utils/scene.obj deleted file mode 100644 index c233da2..0000000 --- a/src/argaze.test/ArUcoMarkers/utils/scene.obj +++ /dev/null @@ -1,22 +0,0 @@ -# .OBJ file for ArUcoMarkersGroup unitary test -o DICT_ARUCO_ORIGINAL#0_Marker -v -0.500000 -0.500000 0.000000 -v 0.500000 -0.500000 0.000000 -v -0.500000 0.500000 0.000000 -v 0.500000 0.500000 0.000000 -vn 0.0000 0.0000 1.0000 -f 1//1 2//1 4//1 3//1 -o DICT_ARUCO_ORIGINAL#1_Marker -v 9.500000 9.500000 0.000000 -v 10.500000 9.500000 0.000000 -v 9.500000 10.500000 0.000000 -v 10.500000 10.500000 0.000000 -vn 0.0000 0.0000 1.0000 -f 5//2 6//2 8//2 7//2 -o DICT_ARUCO_ORIGINAL#2_Marker -v -0.500000 9.500000 0.000000 -v 0.500000 9.500000 0.000000 -v -0.500000 10.500000 0.000000 -v 0.500000 10.500000 0.000000 -vn 0.0000 0.0000 1.0000 -f 9//3 10//3 12//3 11//3 diff --git a/src/argaze/ArUcoMarker/ArUcoBoard.py b/src/argaze/ArUcoMarker/ArUcoBoard.py new file mode 100644 index 0000000..ce8097f --- /dev/null +++ b/src/argaze/ArUcoMarker/ArUcoBoard.py @@ -0,0 +1,82 @@ +"""Calibration chess board with ArUco markers inside. + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +from dataclasses import dataclass, field +from typing import Sequence + +import cv2 as cv +import cv2.aruco as aruco + +from argaze.ArUcoMarker import ArUcoMarkerDictionary + + +@dataclass +class ArUcoBoard(): + """ """ + + columns: int = field(default=0) + """Number of columns.""" + + rows: int = field(default=0) + """Number of rows.""" + + square_size: float = field(default=0.) + """Size of board square in centimeter.""" + + marker_size: float = field(default=0.) + """Size of ArUco markers inside board squares in centimeter.""" + + dictionary: ArUcoMarkerDictionary.ArUcoMarkerDictionary = field(default_factory=ArUcoMarkerDictionary.ArUcoMarkerDictionary) + """ArUco markers dictionary.""" + + def __post_init__(self): + + # Create board model + self.model = aruco.CharucoBoard((self.columns, self.rows), self.square_size/100., self.marker_size/100., self.dictionary.markers) + + @property + def identifiers(self) -> Sequence[int]: + """Get board markers identifiers.""" + + return self.model.getIds() + + @property + def size(self) -> Sequence[int]: + """Get numbers of columns and rows.""" + + return self.model.getChessboardSize() + + @property + def markers_number(self) -> int: + """Get number of markers.""" + + return len(self.model.getIds()) + + @property + def corners_number(self) -> int: + """Get number of corners.""" + + return (self.model.getChessboardSize()[0] - 1 ) * (self.model.getChessboardSize()[1] - 1) + + def save(self, filepath: str, dpi: int): + """Save calibration board picture at a given resolution.""" + + dimension = [round(d * self.model.getSquareLength() * 100 * dpi / 2.54) for d in self.model.getChessboardSize()] # 1 cm = 2.54 inches + + cv.imwrite(filepath, self.model.generateImage(dimension)) + diff --git a/src/argaze/ArUcoMarker/ArUcoCamera.py b/src/argaze/ArUcoMarker/ArUcoCamera.py new file mode 100644 index 0000000..2402df0 --- /dev/null +++ b/src/argaze/ArUcoMarker/ArUcoCamera.py @@ -0,0 +1,238 @@ +"""ArCamera based of ArUco markers technology. + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import logging + +import cv2 +import numpy + +from argaze import ArFeatures, DataFeatures +from argaze.ArUcoMarker import ArUcoDetector, ArUcoOpticCalibrator, ArUcoScene +from argaze.AreaOfInterest import AOI2DScene + +# Define default ArUcoCamera image_parameters values +DEFAULT_ARUCOCAMERA_IMAGE_PARAMETERS = { + "draw_detected_markers": { + "color": (0, 255, 0), + "draw_axes": { + "thickness": 3 + } + } +} + + +class ArUcoCamera(ArFeatures.ArCamera): + """ + Define an ArCamera based on ArUco marker detection. + """ + + @DataFeatures.PipelineStepInit + def __init__(self, **kwargs): + """Initialize ArUcoCamera""" + + # Init ArCamera class + super().__init__() + + # Init private attribute + self.__aruco_detector = None + self.__sides_mask = 0 + + # Init protected attributes + self._image_parameters = {**ArFeatures.DEFAULT_ARFRAME_IMAGE_PARAMETERS, **DEFAULT_ARUCOCAMERA_IMAGE_PARAMETERS} + + @property + def aruco_detector(self) -> ArUcoDetector.ArUcoDetector: + """ArUco marker detector.""" + return self.__aruco_detector + + @aruco_detector.setter + @DataFeatures.PipelineStepAttributeSetter + def aruco_detector(self, aruco_detector: ArUcoDetector.ArUcoDetector): + + self.__aruco_detector = aruco_detector + + # 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 DataFeatures.PipelineStepLoadingFaile( + '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])) + + # Edit parent + if self.__aruco_detector is not None: + self.__aruco_detector.parent = self + + @property + def sides_mask(self) -> int: + """Size of mask (pixel) to hide video left and right sides.""" + return self.__sides_mask + + @sides_mask.setter + def sides_mask(self, size: int): + + self.__sides_mask = size + + @ArFeatures.ArCamera.scenes.setter + @DataFeatures.PipelineStepAttributeSetter + def scenes(self, scenes: dict): + + self._scenes = {} + + for scene_name, scene_data in scenes.items(): + self._scenes[scene_name] = ArUcoScene.ArUcoScene(name=scene_name, **scene_data) + + # Edit parent + for name, scene in self._scenes.items(): + scene.parent = self + + # Update expected and excluded aoi + self._update_expected_and_excluded_aoi() + + @DataFeatures.PipelineStepMethod + def watch(self, image: DataFeatures.TimestampedImage): + """Detect environment aruco markers from image and project scenes into camera frame.""" + + logging.debug('ArUcoCamera.watch') + + # Use camera frame locker feature + with self._lock: + + # Draw black rectangles to mask sides + if self.__sides_mask > 0: + logging.debug('\t> drawing sides mask (%i px)', self.__sides_mask) + + height, width, _ = image.shape + + cv2.rectangle(image, (0, 0), (self.__sides_mask, height), (0, 0, 0), -1) + cv2.rectangle(image, (width - self.__sides_mask, 0), (width, height), (0, 0, 0), -1) + + # Fill camera frame background with timestamped image + self.background = image + + # Read projection from the cache if required + if not self._read_projection_cache(image.timestamp): + + # Detect aruco markers + logging.debug('\t> detect markers') + + self.__aruco_detector.detect_markers(image) + + # Clear former layers projection into camera frame + self._clear_projection() + + # 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 + ''' + + # Estimate scene pose from detected scene markers + logging.debug('\t> estimate %s scene pose', scene_name) + + try: + + tvec, rmat, _ = scene.estimate_pose(self.__aruco_detector.detected_markers(), timestamp=image.timestamp) + + # Project scene into camera frame according estimated pose + for layer_name, layer_projection in scene.project(tvec, rmat, self.visual_hfov, self.visual_vfov, timestamp=image.timestamp): + + logging.debug('\t> project %s scene %s layer', scene_name, layer_name) + + try: + + # Update camera layer aoi + self.layers[layer_name].aoi_scene |= layer_projection + + # Timestamp camera layer + self.layers[layer_name].timestamp = image.timestamp + + except KeyError: + + pass + + # Write projection into the cache if required + self._write_projection_cache(image.timestamp) + + except DataFeatures.TimestampedException as e: + + # Write exception into the cache if required + self._write_projection_cache(image.timestamp, e) + + # Raise exception + raise e + + @DataFeatures.PipelineStepImage + 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 visualization. + + 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 + """ + + logging.debug('ArUcoCamera.image %s', self.name) + + # Get camera frame image + # Note: don't lock/unlock camera frame here as super().image manage it. + image = super().image(**kwargs) + + # Use frame locker feature + with self._lock: + + # Draw optic parameters grid if required + if draw_optic_parameters_grid is not None: + logging.debug('\t> drawing optic parameters') + + 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(): + logging.debug('\t> drawing %s scene', scene_name) + + self.scenes[scene_name].draw(image, **draw_scenes_parameters) + + # Draw detected markers if required + if draw_detected_markers is not None: + logging.debug('\t> drawing detected markers') + + self.__aruco_detector.draw_detected_markers(image, draw_detected_markers) + + return image diff --git a/src/argaze/ArUcoMarker/ArUcoDetector.py b/src/argaze/ArUcoMarker/ArUcoDetector.py new file mode 100644 index 0000000..daa0f9d --- /dev/null +++ b/src/argaze/ArUcoMarker/ArUcoDetector.py @@ -0,0 +1,364 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import json +from collections import Counter +from typing import Self + +import cv2 as cv +import numpy +from cv2 import aruco + +from argaze import DataFeatures +from argaze.ArUcoMarker import ArUcoMarkerDictionary, ArUcoMarker, ArUcoOpticCalibrator + + +class DetectorParameters(): + """Wrapper class around ArUco marker detector parameters. + + !!! note + More details on [opencv page](https://docs.opencv.org/4.x/d1/dcd/structcv_1_1aruco_1_1DetectorParameters.html) + """ + + __parameters = aruco.DetectorParameters() + __parameters_names = [ + 'adaptiveThreshConstant', + 'adaptiveThreshWinSizeMax', + 'adaptiveThreshWinSizeMin', + 'adaptiveThreshWinSizeStep', + 'aprilTagCriticalRad', + 'aprilTagDeglitch', + 'aprilTagMaxLineFitMse', + 'aprilTagMaxNmaxima', + 'aprilTagMinClusterPixels', + 'aprilTagMinWhiteBlackDiff', + 'aprilTagQuadDecimate', + 'aprilTagQuadSigma', + 'cornerRefinementMaxIterations', + 'cornerRefinementMethod', + 'cornerRefinementMinAccuracy', + 'cornerRefinementWinSize', + 'markerBorderBits', + 'minMarkerPerimeterRate', + 'maxMarkerPerimeterRate', + 'minMarkerDistanceRate', + 'detectInvertedMarker', + 'errorCorrectionRate', + 'maxErroneousBitsInBorderRate', + 'minCornerDistanceRate', + 'minDistanceToBorder', + 'minOtsuStdDev', + 'perspectiveRemoveIgnoredMarginPerCell', + 'perspectiveRemovePixelPerCell', + 'polygonalApproxAccuracyRate', + 'useAruco3Detection' + ] + + def __init__(self, **kwargs): + + for parameter, value in kwargs.items(): + setattr(self.__parameters, parameter, value) + + self.__dict__.update(kwargs) + + def __setattr__(self, parameter, value): + + setattr(self.__parameters, parameter, value) + + def __getattr__(self, parameter): + + return getattr(self.__parameters, parameter) + + @classmethod + def from_json(cls, json_filepath) -> Self: + """Load detector parameters from .json file.""" + + with open(json_filepath) as configuration_file: + return DetectorParameters(**json.load(configuration_file)) + + def __str__(self) -> str: + """Detector parameters string representation.""" + + return f'{self}' + + def __format__(self, spec: str) -> str: + """Formated detector parameters string representation. + + Parameters: + spec: 'modified' to get only modified parameters. + """ + + output = '' + + for parameter in self.__parameters_names: + + if parameter in self.__dict__.keys(): + + output += f'\t*{parameter}: {getattr(self.__parameters, parameter)}\n' + + elif spec == "": + + output += f'\t{parameter}: {getattr(self.__parameters, parameter)}\n' + + return output + + @property + def internal(self): + return self.__parameters + + +class ArUcoDetector(DataFeatures.PipelineStepObject): + """OpenCV ArUco library wrapper.""" + + # noinspection PyMissingConstructor + @DataFeatures.PipelineStepInit + def __init__(self, **kwargs): + """Initialize ArUcoDetector.""" + + # Init private attributes + self.__dictionary = None + self.__optic_parameters = None + self.__parameters = None + + # Init detected markers data + self.__detected_markers = {} + + # Init detected board data + self.__board = None + self.__board_corners_number = 0 + self.__board_corners = [] + self.__board_corners_ids = [] + + @property + def dictionary(self) -> ArUcoMarkerDictionary.ArUcoMarkerDictionary: + """ArUco markers dictionary to detect.""" + return self.__dictionary + + @dictionary.setter + @DataFeatures.PipelineStepAttributeSetter + def dictionary(self, dictionary: ArUcoMarkerDictionary.ArUcoMarkerDictionary): + + self.__dictionary = dictionary + + @property + def optic_parameters(self) -> ArUcoOpticCalibrator.OpticParameters: + """Optic parameters to use for ArUco detection into image.""" + return self.__optic_parameters + + @optic_parameters.setter + @DataFeatures.PipelineStepAttributeSetter + def optic_parameters(self, optic_parameters: ArUcoOpticCalibrator.OpticParameters): + + self.__optic_parameters = optic_parameters + + @property + def parameters(self) -> DetectorParameters: + """ArUco detector parameters.""" + return self.__parameters + + @parameters.setter + @DataFeatures.PipelineStepAttributeSetter + def parameters(self, parameters: DetectorParameters): + + self.__parameters = parameters + + @DataFeatures.PipelineStepMethod + def detect_markers(self, image: numpy.array): + """Detect all ArUco markers into an image. + + !!! danger "DON'T MIRROR IMAGE" + It makes the markers detection to fail. + + !!! danger "DON'T UNDISTORTED IMAGE" + Camera intrinsic parameters and distortion coefficients are used later during pose estimation. + """ + + # Reset detected markers data + self.__detected_markers, detected_markers_corners, detected_markers_ids = {}, [], [] + + # Detect markers into gray picture + detected_markers_corners, detected_markers_ids, _ = aruco.detectMarkers(cv.cvtColor(image, cv.COLOR_BGR2GRAY), + self.__dictionary.markers, + parameters=self.__parameters.internal) + + # Is there detected markers ? + if len(detected_markers_corners) > 0: + + # Transform markers ids array into list + detected_markers_ids = detected_markers_ids.T[0] + + for i, marker_id in enumerate(detected_markers_ids): + marker = ArUcoMarker.ArUcoMarker(self.__dictionary, marker_id) + marker.corners = detected_markers_corners[i][0] + + # No pose estimation: call estimate_markers_pose to get one + marker.translation = numpy.empty([0]) + marker.rotation = numpy.empty([0]) + marker.points = numpy.empty([0]) + + self.__detected_markers[marker_id] = marker + + def estimate_markers_pose(self, size: float, ids: list = []): + """Estimate pose detected markers pose considering a marker size. + + Parameters: + size: size of markers in centimeters. + ids: markers id list to select detected markers. + """ + + # Is there detected markers ? + if len(self.__detected_markers) > 0: + + # Select all markers by default + if len(ids) == 0: + ids = self.__detected_markers.keys() + + # Prepare data for aruco.estimatePoseSingleMarkers function + selected_markers_corners = tuple() + selected_markers_ids = [] + + for marker_id, marker in self.__detected_markers.items(): + + if marker_id in ids: + selected_markers_corners += (marker.corners,) + selected_markers_ids.append(marker_id) + + # Estimate pose of selected markers + if len(selected_markers_corners) > 0: + + markers_rvecs, markers_tvecs, markers_points = aruco.estimatePoseSingleMarkers(selected_markers_corners, + size, numpy.array( + self.__optic_parameters.K), numpy.array(self.__optic_parameters.D)) + + for i, marker_id in enumerate(selected_markers_ids): + marker = self.__detected_markers[marker_id] + + marker.translation = markers_tvecs[i][0] + marker.rotation, _ = cv.Rodrigues(markers_rvecs[i][0]) + marker.size = size + marker.points = markers_points.reshape(4, 3).dot(marker.rotation) - marker.translation + + def detected_markers(self) -> dict[int, ArUcoMarker.ArUcoMarker]: + """Access to detected markers' dictionary.""" + + return self.__detected_markers + + def detected_markers_number(self) -> int: + """Return detected markers number.""" + + return len(list(self.__detected_markers.keys())) + + def draw_detected_markers(self, image: numpy.array, draw_marker: dict = None): + """Draw detected markers. + + Parameters: + image: image where to draw + draw_marker: ArucoMarker.draw parameters (if None, no marker drawn) + """ + + if draw_marker is not None: + + for marker_id, marker in self.__detected_markers.items(): + marker.draw(image, self.__optic_parameters.K, self.__optic_parameters.D, **draw_marker) + + def detect_board(self, image: numpy.array, board, expected_markers_number): + """Detect ArUco markers board in image setting up the number of detected markers needed to agree detection. + + !!! danger "DON'T MIRROR IMAGE" + It makes the markers detection to fail. + """ + + # detect markers from gray picture + gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY) + detected_markers_corners, detected_markers_ids, _ = aruco.detectMarkers(gray, self.__dictionary.markers, + parameters=self.__parameters.internal) + + # if all board markers are detected + if len(detected_markers_corners) == expected_markers_number: + + self.__board = board + self.__board_corners_number, self.__board_corners, self.__board_corners_ids = aruco.interpolateCornersCharuco( + detected_markers_corners, detected_markers_ids, gray, self.__board.model) + + else: + + self.__board = None + self.__board_corners_number = 0 + self.__board_corners = [] + self.__board_corners_ids = [] + + def draw_board(self, image: numpy.array): + """Draw detected board corners in image.""" + + if self.__board is not None: + cv.drawChessboardCorners(image, ((self.__board.size[0] - 1), (self.__board.size[1] - 1)), + self.__board_corners, True) + + def board_corners_number(self) -> int: + """Get detected board corners number.""" + + return self.__board_corners_number + + def board_corners_identifier(self) -> list[int]: + """Get detected board corners identifier.""" + + return self.__board_corners_ids + + def board_corners(self) -> list: + """Get detected board corners.""" + + return self.__board_corners + + +class Observer(): + """Define ArUcoDetector observer to count how many times detection succeeded and how many times markers are detected.""" + + def __init__(self): + """Initialize marker detection metrics.""" + + self.__try_count = 0 + self.__success_count = 0 + self.__detected_ids = [] + + @property + def metrics(self) -> tuple[int, int, dict]: + """Get marker detection metrics. + + Returns: + number of detect function call + dict with number of detection for each marker identifier + """ + + return self.__try_count, self.__success_count, Counter(self.__detected_ids) + + def reset(self): + """Reset marker detection metrics.""" + + self.__try_count = 0 + self.__success_count = 0 + self.__detected_ids = [] + + def on_detect_markers(self, timestamp, aruco_detector, exception): + """Update ArUco markers detection metrics.""" + + self.__try_count += 1 + detected_markers_list = list(aruco_detector.detected_markers().keys()) + + if len(detected_markers_list): + self.__success_count += 1 + self.__detected_ids.extend(detected_markers_list) diff --git a/src/argaze/ArUcoMarker/ArUcoMarker.py b/src/argaze/ArUcoMarker/ArUcoMarker.py new file mode 100644 index 0000000..fdc8071 --- /dev/null +++ b/src/argaze/ArUcoMarker/ArUcoMarker.py @@ -0,0 +1,106 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +from dataclasses import dataclass, field +import math + +from argaze.ArUcoMarker import ArUcoMarkerDictionary + +import numpy +import cv2 +import cv2.aruco as aruco + +@dataclass +class ArUcoMarker(): + """Define ArUco marker class.""" + + dictionary: ArUcoMarkerDictionary.ArUcoMarkerDictionary + """Dictionary to which it belongs.""" + + identifier: int + """Index into dictionary""" + + size: float = field(default=math.nan) + """Size of marker in centimeters.""" + + corners: numpy.array = field(init=False, repr=False) + """Estimated 2D corners position in camera image referential.""" + + translation: numpy.array = field(init=False, repr=False) + """Estimated 3D center position in camera world referential.""" + + rotation: numpy.array = field(init=False, repr=False) + """Estimated 3D marker rotation in camera world referential.""" + + points: numpy.array = field(init=False, repr=False) + """Estimated 3D corners positions in camera world referential.""" + + @property + def center(self) -> numpy.array: + """Get 2D center position in camera image referential.""" + + return self.corners[0].mean(axis=0) + + def image(self, dpi) -> numpy.array: + """Create marker matrix image at a given resolution. + + !!! warning + Marker size have to be setup before. + """ + + assert(not math.isnan(self.size)) + + dimension = round(self.size * dpi / 2.54) # 1 cm = 2.54 inches + matrix = numpy.zeros((dimension, dimension, 1), dtype="uint8") + + aruco.generateImageMarker(self.dictionary.markers, self.identifier, dimension, matrix, 1) + + return numpy.repeat(matrix, 3).reshape(dimension, dimension, 3) + + def draw(self, image: numpy.array, K: numpy.array, D: numpy.array, color: tuple = None, draw_axes: dict = None): + """Draw marker in image. + + Parameters: + image: image where to + K: + D: + color: marker color (if None, no marker drawn) + draw_axes: enable marker axes drawing + + !!! warning + draw_axes needs marker size and pose estimation. + """ + + # Draw marker if required + if color is not None: + + aruco.drawDetectedMarkers(image, [numpy.array([list(self.corners)])], numpy.array([self.identifier]), color) + + # Draw marker axes if pose has been estimated, marker have a size and if required + if self.translation.size == 3 and self.rotation.size == 9 and not math.isnan(self.size) and draw_axes is not None: + + cv2.drawFrameAxes(image, numpy.array(K), numpy.array(D), self.rotation, self.translation, self.size, **draw_axes) + + def save(self, destination_folder, dpi): + """Save marker image as .png file into a destination folder.""" + + filename = f'{self.dictionary.name}_{self.dictionary.format}_{self.identifier}.png' + filepath = f'{destination_folder}/{filename}' + + cv2.imwrite(filepath, self.image(dpi)) + diff --git a/src/argaze/ArUcoMarker/ArUcoMarkerDictionary.py b/src/argaze/ArUcoMarker/ArUcoMarkerDictionary.py new file mode 100644 index 0000000..ed423f1 --- /dev/null +++ b/src/argaze/ArUcoMarker/ArUcoMarkerDictionary.py @@ -0,0 +1,161 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import cv2.aruco as aruco + +all_aruco_markers_dictionaries = { + 'DICT_4X4_50': aruco.DICT_4X4_50, + 'DICT_4X4_100': aruco.DICT_4X4_100, + 'DICT_4X4_250': aruco.DICT_4X4_250, + 'DICT_4X4_1000': aruco.DICT_4X4_1000, + 'DICT_5X5_50': aruco.DICT_5X5_50, + 'DICT_5X5_100': aruco.DICT_5X5_100, + 'DICT_5X5_250': aruco.DICT_5X5_250, + 'DICT_5X5_1000': aruco.DICT_5X5_1000, + 'DICT_6X6_50': aruco.DICT_6X6_50, + 'DICT_6X6_100': aruco.DICT_6X6_100, + 'DICT_6X6_250': aruco.DICT_6X6_250, + 'DICT_6X6_1000': aruco.DICT_6X6_1000, + 'DICT_7X7_50': aruco.DICT_7X7_50, + 'DICT_7X7_100': aruco.DICT_7X7_100, + 'DICT_7X7_250': aruco.DICT_7X7_250, + 'DICT_7X7_1000': aruco.DICT_7X7_1000, + 'DICT_ARUCO_ORIGINAL': aruco.DICT_ARUCO_ORIGINAL, + 'DICT_APRILTAG_16h5': aruco.DICT_APRILTAG_16h5, + 'DICT_APRILTAG_25h9': aruco.DICT_APRILTAG_25h9, + 'DICT_APRILTAG_36h10': aruco.DICT_APRILTAG_36h10, + 'DICT_APRILTAG_36h11': aruco.DICT_APRILTAG_36h11 +} +"""Dictionary to list all built-in ArUco markers dictionaries from OpenCV ArUco package.""" + +class ArUcoMarkerDictionary(): + """Handle an ArUco markers dictionary.""" + + def __init__(self, name: str = 'DICT_ARUCO_ORIGINAL'): + + self.__name = name + + if all_aruco_markers_dictionaries.get(self.__name, None) is None: + raise NameError(f'Bad ArUco markers dictionary name: {self.__name}') + + @property + def name(self): + """Dictionary name""" + + return self.__name + + def __str__(self) -> str: + """String display""" + + output = f'{self.name}\n' + return output + + @property + def markers(self) -> aruco.Dictionary: + """Get all markers from dictionary.""" + + return aruco.getPredefinedDictionary(all_aruco_markers_dictionaries[self.name]) + + @property + def format(self) -> str: + """Get markers format.""" + + dict_name_split = self.name.split('_') + dict_type = dict_name_split[1] + + # DICT_ARUCO_ORIGINAL case + if dict_type == 'ARUCO': + return '5X5' + + # DICT_APRILTAG case + elif dict_type == 'APRILTAG': + + april_tag_format = dict_name_split[2] + + if april_tag_format == '16h5': + return '4X4' + + elif april_tag_format == '25h9': + return '5X5' + + elif april_tag_format == '36h10': + return '6X6' + + elif april_tag_format == '36h11': + return '6X6' + + # other cases + else: + return dict_type + + @property + def number(self) -> int: + """Get number of markers inside dictionary.""" + + dict_name_split = self.name.split('_') + dict_type = dict_name_split[1] + + # DICT_ARUCO_ORIGINAL case + if dict_type == 'ARUCO': + return 1024 + + # DICT_APRILTAG case + elif dict_type == 'APRILTAG': + + april_tag_format = dict_name_split[2] + + if april_tag_format == '16h5': + + return 30 + + elif april_tag_format == '25h9': + + return 30 + + elif april_tag_format == '36h10': + + return 2320 + + elif april_tag_format == '36h11': + + return 587 + + # other cases + else: + + return int(dict_name_split[2]) + + def create_marker(self, i, size): + """Create a marker.""" + + if i >= 0 and i < self.number: + + from argaze.ArUcoMarker import ArUcoMarker + + return ArUcoMarker.ArUcoMarker(self, i, size) + + else: + + raise ValueError(f'Bad index: {i}') + + def save(self, destination_folder, size, dpi): + """Save all markers dictionary into separated .png files.""" + + for i in range(self.number): + + self.create_marker(i, size).save(destination_folder, dpi) diff --git a/src/argaze/ArUcoMarker/ArUcoMarkerGroup.py b/src/argaze/ArUcoMarker/ArUcoMarkerGroup.py new file mode 100644 index 0000000..b013829 --- /dev/null +++ b/src/argaze/ArUcoMarker/ArUcoMarkerGroup.py @@ -0,0 +1,476 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import math +import re +from dataclasses import dataclass +from typing import Self + +import cv2 +import numpy + +from argaze import DataFeatures +from argaze.ArUcoMarker import ArUcoMarkerDictionary, ArUcoMarker + +T0 = numpy.array([0., 0., 0.]) +"""Define no translation vector.""" + +R0 = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) +"""Define no rotation matrix.""" + + +def make_rotation_matrix(x, y, z): + # Create rotation matrix around x-axis + c = numpy.cos(numpy.deg2rad(x)) + s = numpy.sin(numpy.deg2rad(x)) + rx = numpy.array([[1, 0, 0], [0, c, -s], [0, s, c]]) + + # Create rotation matrix around y-axis + c = numpy.cos(numpy.deg2rad(y)) + s = numpy.sin(numpy.deg2rad(y)) + ry = numpy.array([[c, 0, s], [0, 1, 0], [-s, 0, c]]) + + # Create rotation matrix around z axis + c = numpy.cos(numpy.deg2rad(z)) + s = numpy.sin(numpy.deg2rad(z)) + rz = numpy.array([[c, -s, 0], [s, c, 0], [0, 0, 1]]) + + # Return intrinsic rotation matrix + return rx.dot(ry.dot(rz)) + + +def is_rotation_matrix(mat): + rt = numpy.transpose(mat) + should_be_identity = numpy.dot(rt, mat) + i = numpy.identity(3, dtype=mat.dtype) + n = numpy.linalg.norm(i - should_be_identity) + + return n < 1e-3 + + +@dataclass(frozen=True) +class Place: + """Define a place as list of corners position and a marker. + + Parameters: + corners: 3D corners position in group referential. + marker: ArUco marker linked to the place. + """ + + corners: numpy.array + marker: ArUcoMarker.ArUcoMarker + + +class ArUcoMarkerGroup(DataFeatures.PipelineStepObject): + """ + Handle group of ArUco markers as one unique spatial entity and estimate its pose. + """ + + # noinspection PyMissingConstructor + @DataFeatures.PipelineStepInit + def __init__(self, **kwargs): + """Initialize ArUcoMarkerGroup""" + + # Init private attributes + self.marker_size = None + self.__dictionary = None + self.__places = {} + self.__translation = numpy.zeros(3) + self.__rotation = numpy.zeros(3) + + @property + def dictionary(self) -> ArUcoMarkerDictionary.ArUcoMarkerDictionary: + """Expected dictionary of all markers in the group.""" + return self.__dictionary + + @dictionary.setter + def dictionary(self, dictionary: ArUcoMarkerDictionary.ArUcoMarkerDictionary): + + self.__dictionary = dictionary + + @property + def places(self) -> dict: + """Expected markers place.""" + return self.__places + + @places.setter + def places(self, places: dict): + + # Normalize places data + new_places = {} + + for identifier, data in places.items(): + + # Convert string identifier to int value + if type(identifier) is str: + + identifier = int(identifier) + + # Get translation vector + tvec = numpy.array(data.pop('translation')).astype(numpy.float32) + + # Check rotation value shape + rvalue = numpy.array(data.pop('rotation')).astype(numpy.float32) + + # Rotation matrix + if rvalue.shape == (3, 3): + + rmat = rvalue + + # Rotation vector (expected in degree) + elif rvalue.shape == (3,): + + rmat = make_rotation_matrix(rvalue[0], rvalue[1], rvalue[2]).astype(numpy.float32) + + else: + + raise ValueError(f'Bad rotation value: {rvalue}') + + assert (is_rotation_matrix(rmat)) + + # Get marker size + size = float(numpy.array(data.pop('size')).astype(numpy.float32)) + + new_marker = ArUcoMarker.ArUcoMarker(self.__dictionary, identifier, size) + + # Build marker corners thanks to translation vector and rotation matrix + place_corners = numpy.array([[-size / 2, size / 2, 0], [size / 2, size / 2, 0], [size / 2, -size / 2, 0], [-size / 2, -size / 2, 0]]) + place_corners = place_corners.dot(rmat) + tvec + + new_places[identifier] = Place(place_corners, new_marker) + + # else places are configured using detected markers estimated points + elif isinstance(data, ArUcoMarker.ArUcoMarker): + + new_places[identifier] = Place(data.points, data) + + # else places are already at expected format + elif (type(identifier) is int) and isinstance(data, Place): + + new_places[identifier] = data + + self.__places = new_places + + @property + def identifiers(self) -> list: + """List place marker identifiers belonging to the group.""" + return list(self.__places.keys()) + + @property + def translation(self) -> numpy.array: + """Get ArUco marker group translation vector.""" + return self.__translation + + @translation.setter + def translation(self, tvec): + """Set ArUco marker group translation vector.""" + self.__translation = tvec + + @property + def rotation(self) -> numpy.array: + """Get ArUco marker group rotation matrix.""" + return self.__translation + + @rotation.setter + def rotation(self, rmat): + """Set ArUco marker group rotation matrix.""" + self.__rotation = rmat + + def as_dict(self) -> dict: + """Export ArUco marker group properties as dictionary.""" + + return { + **DataFeatures.PipelineStepObject.as_dict(self), + "dictionary": self.__dictionary, + "places": self.__places + } + + @classmethod + def from_obj(cls, obj_filepath: str) -> Self: + """Load ArUco markers group from .obj file. + + !!! note + Expected object (o) name format: #_Marker + + !!! note + All markers have to belong to the same dictionary. + + """ + + new_dictionary = None + new_places = {} + + # Regex rules for .obj file parsing + obj_rx_dict = { + 'object': re.compile(r'o (.*)#([0-9]+)_(.*)\n'), + 'vertices': re.compile(r'v ([+-]?[0-9]*[.]?[0-9]+) ([+-]?[0-9]*[.]?[0-9]+) ([+-]?[0-9]*[.]?[0-9]+)\n'), + 'face': re.compile(r'f ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)\n'), + 'comment': re.compile(r'#(.*)\n') + # keep comment regex after object regex because the # is used in object string too + } + + # Regex .obj line parser + def __parse_obj_line(ln): + + for k, rx in obj_rx_dict.items(): + m = rx.search(ln) + if m: + return k, m + + # If there are no matches + return None, None + + # Start parsing + try: + + identifier = None + vertices = [] + faces = {} + + # Open the file and read through it line by line + with open(obj_filepath, 'r') as file: + + line = file.readline() + + while line: + + # At each line check for a match with a regex + key, match = __parse_obj_line(line) + + # Extract comment + if key == 'comment': + pass + + # Extract marker dictionary and identifier + elif key == 'object': + + dictionary = str(match.group(1)) + identifier = int(match.group(2)) + + # Init new group dictionary with first dictionary name + if new_dictionary is None: + + new_dictionary = ArUcoMarkerDictionary.ArUcoMarkerDictionary(dictionary) + + # Check all others marker dictionary are equal to new group dictionary + elif dictionary != new_dictionary.name: + + raise NameError(f'Marker {identifier} dictionary is not {new_dictionary.name}') + + # Fill vertices array + elif key == 'vertices': + + vertices.append(tuple([float(match.group(1)), float(match.group(2)), float(match.group(3))])) + + # Extract vertices ids + elif key == 'face': + + faces[identifier] = [int(match.group(1)), int(match.group(2)), int(match.group(3)), int(match.group(4))] + + # Go to next line + line = file.readline() + + file.close() + + # Retrieve marker vertices thanks to face vertices ids + for identifier, face in faces.items(): + + # Gather place corners in clockwise order + cw_corners = numpy.array([vertices[i - 1] for i in reversed(face)]) + + # Edit place axis from corners positions + place_x_axis = cw_corners[2] - cw_corners[3] + place_x_axis_norm = numpy.linalg.norm(place_x_axis) + + place_y_axis = cw_corners[0] - cw_corners[3] + place_y_axis_norm = numpy.linalg.norm(place_y_axis) + + # Check axis size: they should be almost equal + if math.isclose(place_x_axis_norm, place_y_axis_norm, rel_tol=1e-3): + + new_marker_size = place_x_axis_norm + + else: + + raise ValueError(f'{new_dictionary}#{identifier}_Marker is not a square.') + + # Create a new place related to a new marker + new_marker = ArUcoMarker.ArUcoMarker(new_dictionary, identifier, new_marker_size) + new_places[identifier] = Place(cw_corners, new_marker) + + except IOError: + raise IOError(f'File not found: {obj_filepath}') + + # Instantiate ArUco markers group + data = { + 'dictionary': new_dictionary, + 'places': new_places + } + + return ArUcoMarkerGroup(**data) + + def filter_markers(self, detected_markers: dict) -> tuple[dict, dict]: + """Sort markers belonging to the group from given detected markers dict (cf ArUcoDetector.detect_markers()). + + Returns: + dict of markers belonging to this group + dict of remaining markers not belonging to this group + """ + + group_markers = {} + remaining_markers = {} + + for (marker_id, marker) in detected_markers.items(): + + if marker_id in self.__places.keys(): + + group_markers[marker_id] = marker + + else: + + remaining_markers[marker_id] = marker + + return group_markers, remaining_markers + + def estimate_pose_from_markers_corners(self, markers: dict, k: numpy.array, d: numpy.array) -> tuple[ + bool, numpy.array, numpy.array]: + """Estimate pose from markers corners and places corners. + + Parameters: + markers: detected markers to use for pose estimation. + k: intrinsic camera parameters + d: camera distortion matrix + + Returns: + success: True if the pose estimation succeeded + tvec: scene translation vector + rvec: scene rotation vector + """ + + markers_corners_2d = [] + places_corners_3d = [] + + for identifier, marker in markers.items(): + + try: + + place = self.__places[identifier] + + for marker_corner in marker.corners: + markers_corners_2d.append(list(marker_corner)) + + for place_corner in place.corners: + places_corners_3d.append(list(place_corner)) + + except KeyError: + + raise ValueError(f'Marker {marker.identifier} doesn\'t belong to the group.') + + # SolvPnP using cv2.SOLVEPNP_SQPNP flag + # TODO: it works also with cv2.SOLVEPNP_EPNP flag so we need to test which is the faster. + # About SolvPnP flags: https://docs.opencv.org/4.x/d5/d1f/calib3d_solvePnP.html + success, rvec, tvec = cv2.solvePnP(numpy.array(places_corners_3d), numpy.array(markers_corners_2d), numpy.array(k), numpy.array(d), flags=cv2.SOLVEPNP_SQPNP) + + # Refine pose estimation using Gauss-Newton optimisation + if success: + rvec, tvec = cv2.solvePnPRefineVVS(numpy.array(places_corners_3d), numpy.array(markers_corners_2d), numpy.array(k), numpy.array(d), rvec, tvec) + + self.__translation = tvec.T + self.__rotation = rvec.T + + return success, self.__translation, self.__rotation + + def draw_axes(self, image: numpy.array, k: numpy.array, d: numpy.array, thickness: int = 0, length: float = 0): + """Draw group axes.""" + + try: + axis_points = numpy.float32([[length, 0, 0], [0, length, 0], [0, 0, length], [0, 0, 0]]).reshape(-1, 3) + axis_points, _ = cv2.projectPoints(axis_points, self.__rotation, self.__translation, numpy.array(k), numpy.array(d)) + axis_points = axis_points.astype(int) + + cv2.line(image, tuple(axis_points[3].ravel()), tuple(axis_points[0].ravel()), (0, 0, 255), thickness) # X (red) + cv2.line(image, tuple(axis_points[3].ravel()), tuple(axis_points[1].ravel()), (0, 255, 0), thickness) # Y (green) + cv2.line(image, tuple(axis_points[3].ravel()), tuple(axis_points[2].ravel()), (255, 0, 0), thickness) # Z (blue) + + # Ignore errors due to out of field axis: their coordinate are larger than int32 limitations. + except cv2.error: + pass + + def draw_places(self, image: numpy.array, k: numpy.array, d: numpy.array, color: tuple = None, border_size: int = 0): + """Draw group places.""" + + for identifier, place in self.__places.items(): + + try: + + place_points, _ = cv2.projectPoints(place.corners, self.__rotation, self.__translation, numpy.array(k), numpy.array(d)) + place_points = place_points.astype(int) + + cv2.line(image, tuple(place_points[0].ravel()), tuple(place_points[1].ravel()), color, border_size) + cv2.line(image, tuple(place_points[1].ravel()), tuple(place_points[2].ravel()), color, border_size) + cv2.line(image, tuple(place_points[2].ravel()), tuple(place_points[3].ravel()), color, border_size) + cv2.line(image, tuple(place_points[3].ravel()), tuple(place_points[0].ravel()), color, border_size) + + # Ignore errors due to out of field places: their coordinate are larger than int32 limitations. + except cv2.error: + pass + + def draw(self, image: numpy.array, k: numpy.array, d: numpy.array, draw_axes: dict = None, draw_places: dict = None): + """Draw group axes and places. + + Parameters: + image: where to draw. + k: intrinsic camera parameters + d: camera distortion matrix + draw_axes: draw_axes parameters (if None, no axes drawn) + draw_places: draw_places parameters (if None, no places drawn) + """ + + # Draw axes if required + if draw_axes is not None: + self.draw_axes(image, k, d, **draw_axes) + + # Draw places if required + if draw_places is not None: + self.draw_places(image, k, d, **draw_places) + + def to_obj(self, obj_filepath): + """Save group to .obj file.""" + + with open(obj_filepath, 'w', encoding='utf-8') as file: + + file.write('# ArGaze OBJ File\n') + file.write('# https://achil.recherche.enac.fr/features/eye/argaze/\n') + + v_count = 0 + + for p, (identifier, place) in enumerate(self.__places.items()): + + file.write(f'o {self.__dictionary.name}#{identifier}_Marker\n') + + vertices = '' + + # Write vertices in reverse order + for v in [3, 2, 1, 0]: + file.write(f'v {" ".join(map(str, place.corners[v]))}\n') + v_count += 1 + + vertices += f' {v_count}' + + # file.write('s off\n') + file.write(f'f{vertices}\n') diff --git a/src/argaze/ArUcoMarker/ArUcoOpticCalibrator.py b/src/argaze/ArUcoMarker/ArUcoOpticCalibrator.py new file mode 100644 index 0000000..468e64a --- /dev/null +++ b/src/argaze/ArUcoMarker/ArUcoOpticCalibrator.py @@ -0,0 +1,162 @@ +""" + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +from dataclasses import dataclass, field + +from argaze import DataFeatures +from argaze.ArUcoMarker import ArUcoBoard + +import json +import numpy +import cv2 +import cv2.aruco as aruco + + +def K0(focal_length: tuple, width: int, height: int) -> numpy.array: + """Define default optic intrinsic parameters' matrix. + + Parameters: + focal_length: + width: in pixel. + height: in pixel. + """ + + return numpy.array([[focal_length[0], 0., width / 2], [0., focal_length[1], height / 2], [0., 0., 1.]]) + + +D0 = numpy.array([0.0, 0.0, 0.0, 0.0, 0.0]) +"""Define default optic distortion coefficients vector.""" + + +@dataclass +class OpticParameters(): + """Define optic parameters output by optic calibrator.""" + + rms: float = field(default=0) + """Root Mean Square error of calibration.""" + + dimensions: numpy.array = field(default_factory=lambda: numpy.array([0, 0])) + """Image dimensions in pixels from which the calibration have been done.""" + + K: numpy.array = field(default_factory=lambda: K0((0, 0), 0, 0)) + """Intrinsic parameters matrix (focal lengths and principal point).""" + + D: numpy.array = field(default_factory=lambda: D0) + """Distortion coefficients vector.""" + + @classmethod + def from_json(cls, json_filepath): + """Load optical parameters from .json file.""" + + with open(json_filepath) as calibration_file: + return OpticParameters(**json.load(calibration_file)) + + def to_json(self, json_filepath): + """Save optical parameters into .json file.""" + + with open(json_filepath, 'w', encoding='utf-8') as calibration_file: + json.dump(self, calibration_file, ensure_ascii=False, indent=4, cls=DataFeatures.JsonEncoder) + + def __str__(self) -> str: + """String display""" + + output = f'\trms: {self.rms}\n' + output += f'\tdimensions: {self.dimensions}\n' + output += f'\tK: {self.K}\n' + output += f'\tD: {self.D}\n' + + return output + + def draw(self, image: numpy.array, width: float = 0., height: float = 0., z: float = 0., point_size: int = 1, + point_color: tuple = (0, 0, 0)): + """Draw grid to display K and D""" + + if width * height > 0.: + + # Edit 3D grid + grid_3D = [] + for x in range(-int(width / 2), int(width / 2)): + for y in range(-int(height / 2), int(height / 2)): + grid_3D.append([x, y, z]) + + # Project 3d grid + grid_2D, _ = cv2.projectPoints(numpy.array(grid_3D).astype(float), numpy.array([0., 0., 0.]), + numpy.array([0., 0., 0.]), numpy.array(self.K), -numpy.array(self.D)) + + # Draw projection + for point in grid_2D: + + # Ignore point out field + try: + + cv2.circle(image, point.astype(int)[0], point_size, point_color, -1) + + except: + + pass + + +class ArUcoOpticCalibrator(): + """Handle optic calibration process.""" + + def __init__(self): + + # Calibration data + self.__corners_set_number = 0 + self.__corners_set = [] + self.__corners_set_ids = [] + + def calibrate(self, board: ArUcoBoard.ArUcoBoard, dimensions: list = None) -> OpticParameters: + """Retrieve K and D parameters from stored calibration data. + + Parameters: + board: [ArUcoBoard](argaze.md/#argaze.ArUcoMarker.ArUcoBoard.ArUcoBoard) instance + dimensions: camera image dimensions + + Returns: + Optic parameters + """ + + if dimensions is None: + dimensions = [0, 0] + + if self.__corners_set_number > 0: + rms, K, D, r, t = aruco.calibrateCameraCharuco(self.__corners_set, self.__corners_set_ids, board.model, + dimensions, None, None) + + return OpticParameters(rms, dimensions, K, D) + + def reset_calibration_data(self): + """Clear all calibration data.""" + + self.__corners_set_number = 0 + self.__corners_set = [] + self.__corners_set_ids = [] + + def store_calibration_data(self, corners, corners_identifiers): + """Store calibration data.""" + + self.__corners_set_number += 1 + self.__corners_set.append(corners) + self.__corners_set_ids.append(corners_identifiers) + + @property + def calibration_data_count(self) -> int: + """Get how much calibration data are stored.""" + + return self.__corners_set_number diff --git a/src/argaze/ArUcoMarker/ArUcoScene.py b/src/argaze/ArUcoMarker/ArUcoScene.py new file mode 100644 index 0000000..a4726fa --- /dev/null +++ b/src/argaze/ArUcoMarker/ArUcoScene.py @@ -0,0 +1,126 @@ +"""ArScene based of ArUco markers technology. + +This program is free software: you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free Software +Foundation, either version 3 of the License, or (at your option) any later +version. +This program is distributed in the hope that it will be useful, but WITHOUT +ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. +You should have received a copy of the GNU General Public License along with +this program. If not, see . +""" + +__author__ = "Théo de la Hogue" +__credits__ = [] +__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" +__license__ = "GPLv3" + +import numpy + +from argaze import ArFeatures, DataFeatures +from argaze.ArUcoMarker import ArUcoMarkerGroup + + +class ArUcoScene(ArFeatures.ArScene): + """ + Define an ArScene based on an ArUcoMarkerGroup description. + """ + + @DataFeatures.PipelineStepInit + def __init__(self, **kwargs): + """Initialize ArUcoScene""" + + # Init ArScene classes + super().__init__() + + # Init private attribute + self.__aruco_markers_group = None + self.__required_markers_number = 2 + + @property + def aruco_markers_group(self) -> ArUcoMarkerGroup.ArUcoMarkerGroup: + """ArUco markers 3D scene description used to estimate scene pose from detected markers: see [estimate_pose][argaze.ArFeatures.ArScene.estimate_pose] function below.""" + return self.__aruco_markers_group + + @aruco_markers_group.setter + @DataFeatures.PipelineStepAttributeSetter + def aruco_markers_group(self, aruco_markers_group: ArUcoMarkerGroup.ArUcoMarkerGroup): + + self.__aruco_markers_group = aruco_markers_group + + # Edit parent + if self.__aruco_markers_group is not None: + + self.__aruco_markers_group.parent = self + + @property + def required_markers_number(self) -> int: + """Numbers of markers that have to be detected to allow pose estimation (default: 2).""" + + return self.__required_markers_number + + @required_markers_number.setter + def required_markers_number(self, n: int): + + # Constrain number to 1 at least + if n < 1: + + self.__required_markers_number = 1 + + else: + + self.__required_markers_number = n + + @DataFeatures.PipelineStepMethod + def estimate_pose(self, detected_markers: dict) -> tuple[numpy.array, numpy.array, dict]: + """Estimate scene pose from detected ArUco markers. + + Parameters: + detected_markers: dictionary with all detected markers + + Returns: + scene translation vector + scene rotation matrix + dict of markers used to estimate the pose + """ + + # Pose estimation fails when no marker is detected + if len(detected_markers) == 0: + + raise ArFeatures.PoseEstimationFailed('No marker detected') + + scene_markers, _ = self.__aruco_markers_group.filter_markers(detected_markers) + + # Pose estimation fails when no marker belongs to the scene + if len(scene_markers) == 0: + + raise ArFeatures.PoseEstimationFailed('No marker belongs to the scene') + + # Pose estimation fails when not enough marker belongs to the scene + if len(scene_markers) < self.required_markers_number: + + raise ArFeatures.PoseEstimationFailed(f'Not enough marker belongs to the scene') + + # Estimate pose from markers corners + success, tvec, rmat = self.__aruco_markers_group.estimate_pose_from_markers_corners(scene_markers, self.parent.aruco_detector.optic_parameters.K, self.parent.aruco_detector.optic_parameters.D) + + if not success: + + raise ArFeatures.PoseEstimationFailed('Can\'t estimate pose from markers corners positions') + + return tvec, rmat, scene_markers + + def draw(self, image: numpy.array, draw_aruco_markers_group: dict = None): + """ + Draw scene into image. + + Parameters: + image: where to draw + draw_aruco_markers_group: ArUcoMarkerGroup.draw parameters (if None, no group drawn) + """ + + # Draw group if required + if draw_aruco_markers_group is not None: + + self.__aruco_markers_group.draw(image, self.parent.aruco_detector.optic_parameters.K, self.parent.aruco_detector.optic_parameters.D, **draw_aruco_markers_group) diff --git a/src/argaze/ArUcoMarker/__init__.py b/src/argaze/ArUcoMarker/__init__.py new file mode 100644 index 0000000..f297c0d --- /dev/null +++ b/src/argaze/ArUcoMarker/__init__.py @@ -0,0 +1,6 @@ +""" +Handle [OpenCV ArUco markers](https://docs.opencv.org/4.x/d5/dae/tutorial_aruco_detection.html): generate and detect +markers, calibrate camera, describe scene, ... +""" +__all__ = ['ArUcoMarkerDictionary', 'ArUcoMarker', 'ArUcoBoard', 'ArUcoOpticCalibrator', 'ArUcoDetector', + 'ArUcoMarkerGroup', 'ArUcoCamera', 'ArUcoScene', 'utils'] diff --git a/src/argaze/ArUcoMarker/utils/A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf b/src/argaze/ArUcoMarker/utils/A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf new file mode 100644 index 0000000..2adcee1 Binary files /dev/null and b/src/argaze/ArUcoMarker/utils/A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf differ diff --git a/src/argaze/ArUcoMarker/utils/A4_DICT_APRILTAG_16h5_5cm_0-7.pdf b/src/argaze/ArUcoMarker/utils/A4_DICT_APRILTAG_16h5_5cm_0-7.pdf new file mode 100644 index 0000000..fcf850d Binary files /dev/null and b/src/argaze/ArUcoMarker/utils/A4_DICT_APRILTAG_16h5_5cm_0-7.pdf differ diff --git a/src/argaze/ArUcoMarker/utils/__init__.py b/src/argaze/ArUcoMarker/utils/__init__.py new file mode 100644 index 0000000..923f5ec --- /dev/null +++ b/src/argaze/ArUcoMarker/utils/__init__.py @@ -0,0 +1,5 @@ +""" +Print **A3_DICT_ARUCO_ORIGINAL_3cm_35cmx25cm.pdf** onto A3 paper sheet to get board at expected dimensions. + +Print **A4_DICT_ARUCO_ORIGINAL_3cm_0-9.pdf** onto A4 paper sheet to get markers at expected dimensions. +""" \ No newline at end of file diff --git a/src/argaze/ArUcoMarkers/ArUcoBoard.py b/src/argaze/ArUcoMarkers/ArUcoBoard.py deleted file mode 100644 index be475d5..0000000 --- a/src/argaze/ArUcoMarkers/ArUcoBoard.py +++ /dev/null @@ -1,82 +0,0 @@ -"""Calibration chess board with ArUco markers inside. - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -from dataclasses import dataclass, field -from typing import Sequence - -import cv2 as cv -import cv2.aruco as aruco - -from argaze.ArUcoMarkers import ArUcoMarkersDictionary - - -@dataclass -class ArUcoBoard(): - """ """ - - columns: int = field(default=0) - """Number of columns.""" - - rows: int = field(default=0) - """Number of rows.""" - - square_size: float = field(default=0.) - """Size of board square in centimeter.""" - - marker_size: float = field(default=0.) - """Size of ArUco markers inside board squares in centimeter.""" - - dictionary: ArUcoMarkersDictionary.ArUcoMarkersDictionary = field(default_factory=ArUcoMarkersDictionary.ArUcoMarkersDictionary) - """ArUco markers dictionary.""" - - def __post_init__(self): - - # Create board model - self.model = aruco.CharucoBoard((self.columns, self.rows), self.square_size/100., self.marker_size/100., self.dictionary.markers) - - @property - def identifiers(self) -> Sequence[int]: - """Get board markers identifiers.""" - - return self.model.getIds() - - @property - def size(self) -> Sequence[int]: - """Get numbers of columns and rows.""" - - return self.model.getChessboardSize() - - @property - def markers_number(self) -> int: - """Get number of markers.""" - - return len(self.model.getIds()) - - @property - def corners_number(self) -> int: - """Get number of corners.""" - - return (self.model.getChessboardSize()[0] - 1 ) * (self.model.getChessboardSize()[1] - 1) - - def save(self, filepath: str, dpi: int): - """Save calibration board picture at a given resolution.""" - - dimension = [round(d * self.model.getSquareLength() * 100 * dpi / 2.54) for d in self.model.getChessboardSize()] # 1 cm = 2.54 inches - - cv.imwrite(filepath, self.model.generateImage(dimension)) - diff --git a/src/argaze/ArUcoMarkers/ArUcoCamera.py b/src/argaze/ArUcoMarkers/ArUcoCamera.py deleted file mode 100644 index 5b535b5..0000000 --- a/src/argaze/ArUcoMarkers/ArUcoCamera.py +++ /dev/null @@ -1,238 +0,0 @@ -"""ArCamera based of ArUco markers technology. - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import logging - -import cv2 -import numpy - -from argaze import ArFeatures, DataFeatures -from argaze.ArUcoMarkers import ArUcoDetector, ArUcoOpticCalibrator, ArUcoScene -from argaze.AreaOfInterest import AOI2DScene - -# Define default ArUcoCamera image_parameters values -DEFAULT_ARUCOCAMERA_IMAGE_PARAMETERS = { - "draw_detected_markers": { - "color": (0, 255, 0), - "draw_axes": { - "thickness": 3 - } - } -} - - -class ArUcoCamera(ArFeatures.ArCamera): - """ - Define an ArCamera based on ArUco marker detection. - """ - - @DataFeatures.PipelineStepInit - def __init__(self, **kwargs): - """Initialize ArUcoCamera""" - - # Init ArCamera class - super().__init__() - - # Init private attribute - self.__aruco_detector = None - self.__sides_mask = 0 - - # Init protected attributes - self._image_parameters = {**ArFeatures.DEFAULT_ARFRAME_IMAGE_PARAMETERS, **DEFAULT_ARUCOCAMERA_IMAGE_PARAMETERS} - - @property - def aruco_detector(self) -> ArUcoDetector.ArUcoDetector: - """ArUco marker detector.""" - return self.__aruco_detector - - @aruco_detector.setter - @DataFeatures.PipelineStepAttributeSetter - def aruco_detector(self, aruco_detector: ArUcoDetector.ArUcoDetector): - - self.__aruco_detector = aruco_detector - - # 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 DataFeatures.PipelineStepLoadingFaile( - '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])) - - # Edit parent - if self.__aruco_detector is not None: - self.__aruco_detector.parent = self - - @property - def sides_mask(self) -> int: - """Size of mask (pixel) to hide video left and right sides.""" - return self.__sides_mask - - @sides_mask.setter - def sides_mask(self, size: int): - - self.__sides_mask = size - - @ArFeatures.ArCamera.scenes.setter - @DataFeatures.PipelineStepAttributeSetter - def scenes(self, scenes: dict): - - self._scenes = {} - - for scene_name, scene_data in scenes.items(): - self._scenes[scene_name] = ArUcoScene.ArUcoScene(name=scene_name, **scene_data) - - # Edit parent - for name, scene in self._scenes.items(): - scene.parent = self - - # Update expected and excluded aoi - self._update_expected_and_excluded_aoi() - - @DataFeatures.PipelineStepMethod - def watch(self, image: DataFeatures.TimestampedImage): - """Detect environment aruco markers from image and project scenes into camera frame.""" - - logging.debug('ArUcoCamera.watch') - - # Use camera frame locker feature - with self._lock: - - # Draw black rectangles to mask sides - if self.__sides_mask > 0: - logging.debug('\t> drawing sides mask (%i px)', self.__sides_mask) - - height, width, _ = image.shape - - cv2.rectangle(image, (0, 0), (self.__sides_mask, height), (0, 0, 0), -1) - cv2.rectangle(image, (width - self.__sides_mask, 0), (width, height), (0, 0, 0), -1) - - # Fill camera frame background with timestamped image - self.background = image - - # Read projection from the cache if required - if not self._read_projection_cache(image.timestamp): - - # Detect aruco markers - logging.debug('\t> detect markers') - - self.__aruco_detector.detect_markers(image) - - # Clear former layers projection into camera frame - self._clear_projection() - - # 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 - ''' - - # Estimate scene pose from detected scene markers - logging.debug('\t> estimate %s scene pose', scene_name) - - try: - - tvec, rmat, _ = scene.estimate_pose(self.__aruco_detector.detected_markers(), timestamp=image.timestamp) - - # Project scene into camera frame according estimated pose - for layer_name, layer_projection in scene.project(tvec, rmat, self.visual_hfov, self.visual_vfov, timestamp=image.timestamp): - - logging.debug('\t> project %s scene %s layer', scene_name, layer_name) - - try: - - # Update camera layer aoi - self.layers[layer_name].aoi_scene |= layer_projection - - # Timestamp camera layer - self.layers[layer_name].timestamp = image.timestamp - - except KeyError: - - pass - - # Write projection into the cache if required - self._write_projection_cache(image.timestamp) - - except DataFeatures.TimestampedException as e: - - # Write exception into the cache if required - self._write_projection_cache(image.timestamp, e) - - # Raise exception - raise e - - @DataFeatures.PipelineStepImage - 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 visualization. - - 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 - """ - - logging.debug('ArUcoCamera.image %s', self.name) - - # Get camera frame image - # Note: don't lock/unlock camera frame here as super().image manage it. - image = super().image(**kwargs) - - # Use frame locker feature - with self._lock: - - # Draw optic parameters grid if required - if draw_optic_parameters_grid is not None: - logging.debug('\t> drawing optic parameters') - - 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(): - logging.debug('\t> drawing %s scene', scene_name) - - self.scenes[scene_name].draw(image, **draw_scenes_parameters) - - # Draw detected markers if required - if draw_detected_markers is not None: - logging.debug('\t> drawing detected markers') - - self.__aruco_detector.draw_detected_markers(image, draw_detected_markers) - - return image diff --git a/src/argaze/ArUcoMarkers/ArUcoDetector.py b/src/argaze/ArUcoMarkers/ArUcoDetector.py deleted file mode 100644 index f675c8f..0000000 --- a/src/argaze/ArUcoMarkers/ArUcoDetector.py +++ /dev/null @@ -1,364 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import json -from collections import Counter -from typing import Self - -import cv2 as cv -import numpy -from cv2 import aruco - -from argaze import DataFeatures -from argaze.ArUcoMarkers import ArUcoMarkersDictionary, ArUcoMarker, ArUcoOpticCalibrator - - -class DetectorParameters(): - """Wrapper class around ArUco marker detector parameters. - - !!! note - More details on [opencv page](https://docs.opencv.org/4.x/d1/dcd/structcv_1_1aruco_1_1DetectorParameters.html) - """ - - __parameters = aruco.DetectorParameters() - __parameters_names = [ - 'adaptiveThreshConstant', - 'adaptiveThreshWinSizeMax', - 'adaptiveThreshWinSizeMin', - 'adaptiveThreshWinSizeStep', - 'aprilTagCriticalRad', - 'aprilTagDeglitch', - 'aprilTagMaxLineFitMse', - 'aprilTagMaxNmaxima', - 'aprilTagMinClusterPixels', - 'aprilTagMinWhiteBlackDiff', - 'aprilTagQuadDecimate', - 'aprilTagQuadSigma', - 'cornerRefinementMaxIterations', - 'cornerRefinementMethod', - 'cornerRefinementMinAccuracy', - 'cornerRefinementWinSize', - 'markerBorderBits', - 'minMarkerPerimeterRate', - 'maxMarkerPerimeterRate', - 'minMarkerDistanceRate', - 'detectInvertedMarker', - 'errorCorrectionRate', - 'maxErroneousBitsInBorderRate', - 'minCornerDistanceRate', - 'minDistanceToBorder', - 'minOtsuStdDev', - 'perspectiveRemoveIgnoredMarginPerCell', - 'perspectiveRemovePixelPerCell', - 'polygonalApproxAccuracyRate', - 'useAruco3Detection' - ] - - def __init__(self, **kwargs): - - for parameter, value in kwargs.items(): - setattr(self.__parameters, parameter, value) - - self.__dict__.update(kwargs) - - def __setattr__(self, parameter, value): - - setattr(self.__parameters, parameter, value) - - def __getattr__(self, parameter): - - return getattr(self.__parameters, parameter) - - @classmethod - def from_json(cls, json_filepath) -> Self: - """Load detector parameters from .json file.""" - - with open(json_filepath) as configuration_file: - return DetectorParameters(**json.load(configuration_file)) - - def __str__(self) -> str: - """Detector parameters string representation.""" - - return f'{self}' - - def __format__(self, spec: str) -> str: - """Formated detector parameters string representation. - - Parameters: - spec: 'modified' to get only modified parameters. - """ - - output = '' - - for parameter in self.__parameters_names: - - if parameter in self.__dict__.keys(): - - output += f'\t*{parameter}: {getattr(self.__parameters, parameter)}\n' - - elif spec == "": - - output += f'\t{parameter}: {getattr(self.__parameters, parameter)}\n' - - return output - - @property - def internal(self): - return self.__parameters - - -class ArUcoDetector(DataFeatures.PipelineStepObject): - """OpenCV ArUco library wrapper.""" - - # noinspection PyMissingConstructor - @DataFeatures.PipelineStepInit - def __init__(self, **kwargs): - """Initialize ArUcoDetector.""" - - # Init private attributes - self.__dictionary = None - self.__optic_parameters = None - self.__parameters = None - - # Init detected markers data - self.__detected_markers = {} - - # Init detected board data - self.__board = None - self.__board_corners_number = 0 - self.__board_corners = [] - self.__board_corners_ids = [] - - @property - def dictionary(self) -> ArUcoMarkersDictionary.ArUcoMarkersDictionary: - """ArUco markers dictionary to detect.""" - return self.__dictionary - - @dictionary.setter - @DataFeatures.PipelineStepAttributeSetter - def dictionary(self, dictionary: ArUcoMarkersDictionary.ArUcoMarkersDictionary): - - self.__dictionary = dictionary - - @property - def optic_parameters(self) -> ArUcoOpticCalibrator.OpticParameters: - """Optic parameters to use for ArUco detection into image.""" - return self.__optic_parameters - - @optic_parameters.setter - @DataFeatures.PipelineStepAttributeSetter - def optic_parameters(self, optic_parameters: ArUcoOpticCalibrator.OpticParameters): - - self.__optic_parameters = optic_parameters - - @property - def parameters(self) -> DetectorParameters: - """ArUco detector parameters.""" - return self.__parameters - - @parameters.setter - @DataFeatures.PipelineStepAttributeSetter - def parameters(self, parameters: DetectorParameters): - - self.__parameters = parameters - - @DataFeatures.PipelineStepMethod - def detect_markers(self, image: numpy.array): - """Detect all ArUco markers into an image. - - !!! danger "DON'T MIRROR IMAGE" - It makes the markers detection to fail. - - !!! danger "DON'T UNDISTORTED IMAGE" - Camera intrinsic parameters and distortion coefficients are used later during pose estimation. - """ - - # Reset detected markers data - self.__detected_markers, detected_markers_corners, detected_markers_ids = {}, [], [] - - # Detect markers into gray picture - detected_markers_corners, detected_markers_ids, _ = aruco.detectMarkers(cv.cvtColor(image, cv.COLOR_BGR2GRAY), - self.__dictionary.markers, - parameters=self.__parameters.internal) - - # Is there detected markers ? - if len(detected_markers_corners) > 0: - - # Transform markers ids array into list - detected_markers_ids = detected_markers_ids.T[0] - - for i, marker_id in enumerate(detected_markers_ids): - marker = ArUcoMarker.ArUcoMarker(self.__dictionary, marker_id) - marker.corners = detected_markers_corners[i][0] - - # No pose estimation: call estimate_markers_pose to get one - marker.translation = numpy.empty([0]) - marker.rotation = numpy.empty([0]) - marker.points = numpy.empty([0]) - - self.__detected_markers[marker_id] = marker - - def estimate_markers_pose(self, size: float, ids: list = []): - """Estimate pose detected markers pose considering a marker size. - - Parameters: - size: size of markers in centimeters. - ids: markers id list to select detected markers. - """ - - # Is there detected markers ? - if len(self.__detected_markers) > 0: - - # Select all markers by default - if len(ids) == 0: - ids = self.__detected_markers.keys() - - # Prepare data for aruco.estimatePoseSingleMarkers function - selected_markers_corners = tuple() - selected_markers_ids = [] - - for marker_id, marker in self.__detected_markers.items(): - - if marker_id in ids: - selected_markers_corners += (marker.corners,) - selected_markers_ids.append(marker_id) - - # Estimate pose of selected markers - if len(selected_markers_corners) > 0: - - markers_rvecs, markers_tvecs, markers_points = aruco.estimatePoseSingleMarkers(selected_markers_corners, - size, numpy.array( - self.__optic_parameters.K), numpy.array(self.__optic_parameters.D)) - - for i, marker_id in enumerate(selected_markers_ids): - marker = self.__detected_markers[marker_id] - - marker.translation = markers_tvecs[i][0] - marker.rotation, _ = cv.Rodrigues(markers_rvecs[i][0]) - marker.size = size - marker.points = markers_points.reshape(4, 3).dot(marker.rotation) - marker.translation - - def detected_markers(self) -> dict[int, ArUcoMarker.ArUcoMarker]: - """Access to detected markers' dictionary.""" - - return self.__detected_markers - - def detected_markers_number(self) -> int: - """Return detected markers number.""" - - return len(list(self.__detected_markers.keys())) - - def draw_detected_markers(self, image: numpy.array, draw_marker: dict = None): - """Draw detected markers. - - Parameters: - image: image where to draw - draw_marker: ArucoMarker.draw parameters (if None, no marker drawn) - """ - - if draw_marker is not None: - - for marker_id, marker in self.__detected_markers.items(): - marker.draw(image, self.__optic_parameters.K, self.__optic_parameters.D, **draw_marker) - - def detect_board(self, image: numpy.array, board, expected_markers_number): - """Detect ArUco markers board in image setting up the number of detected markers needed to agree detection. - - !!! danger "DON'T MIRROR IMAGE" - It makes the markers detection to fail. - """ - - # detect markers from gray picture - gray = cv.cvtColor(image, cv.COLOR_BGR2GRAY) - detected_markers_corners, detected_markers_ids, _ = aruco.detectMarkers(gray, self.__dictionary.markers, - parameters=self.__parameters.internal) - - # if all board markers are detected - if len(detected_markers_corners) == expected_markers_number: - - self.__board = board - self.__board_corners_number, self.__board_corners, self.__board_corners_ids = aruco.interpolateCornersCharuco( - detected_markers_corners, detected_markers_ids, gray, self.__board.model) - - else: - - self.__board = None - self.__board_corners_number = 0 - self.__board_corners = [] - self.__board_corners_ids = [] - - def draw_board(self, image: numpy.array): - """Draw detected board corners in image.""" - - if self.__board is not None: - cv.drawChessboardCorners(image, ((self.__board.size[0] - 1), (self.__board.size[1] - 1)), - self.__board_corners, True) - - def board_corners_number(self) -> int: - """Get detected board corners number.""" - - return self.__board_corners_number - - def board_corners_identifier(self) -> list[int]: - """Get detected board corners identifier.""" - - return self.__board_corners_ids - - def board_corners(self) -> list: - """Get detected board corners.""" - - return self.__board_corners - - -class Observer(): - """Define ArUcoDetector observer to count how many times detection succeeded and how many times markers are detected.""" - - def __init__(self): - """Initialize marker detection metrics.""" - - self.__try_count = 0 - self.__success_count = 0 - self.__detected_ids = [] - - @property - def metrics(self) -> tuple[int, int, dict]: - """Get marker detection metrics. - - Returns: - number of detect function call - dict with number of detection for each marker identifier - """ - - return self.__try_count, self.__success_count, Counter(self.__detected_ids) - - def reset(self): - """Reset marker detection metrics.""" - - self.__try_count = 0 - self.__success_count = 0 - self.__detected_ids = [] - - def on_detect_markers(self, timestamp, aruco_detector, exception): - """Update ArUco markers detection metrics.""" - - self.__try_count += 1 - detected_markers_list = list(aruco_detector.detected_markers().keys()) - - if len(detected_markers_list): - self.__success_count += 1 - self.__detected_ids.extend(detected_markers_list) diff --git a/src/argaze/ArUcoMarkers/ArUcoMarker.py b/src/argaze/ArUcoMarkers/ArUcoMarker.py deleted file mode 100644 index cf573dc..0000000 --- a/src/argaze/ArUcoMarkers/ArUcoMarker.py +++ /dev/null @@ -1,106 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -from dataclasses import dataclass, field -import math - -from argaze.ArUcoMarkers import ArUcoMarkersDictionary - -import numpy -import cv2 -import cv2.aruco as aruco - -@dataclass -class ArUcoMarker(): - """Define ArUco marker class.""" - - dictionary: ArUcoMarkersDictionary.ArUcoMarkersDictionary - """Dictionary to which it belongs.""" - - identifier: int - """Index into dictionary""" - - size: float = field(default=math.nan) - """Size of marker in centimeters.""" - - corners: numpy.array = field(init=False, repr=False) - """Estimated 2D corners position in camera image referential.""" - - translation: numpy.array = field(init=False, repr=False) - """Estimated 3D center position in camera world referential.""" - - rotation: numpy.array = field(init=False, repr=False) - """Estimated 3D marker rotation in camera world referential.""" - - points: numpy.array = field(init=False, repr=False) - """Estimated 3D corners positions in camera world referential.""" - - @property - def center(self) -> numpy.array: - """Get 2D center position in camera image referential.""" - - return self.corners[0].mean(axis=0) - - def image(self, dpi) -> numpy.array: - """Create marker matrix image at a given resolution. - - !!! warning - Marker size have to be setup before. - """ - - assert(not math.isnan(self.size)) - - dimension = round(self.size * dpi / 2.54) # 1 cm = 2.54 inches - matrix = numpy.zeros((dimension, dimension, 1), dtype="uint8") - - aruco.generateImageMarker(self.dictionary.markers, self.identifier, dimension, matrix, 1) - - return numpy.repeat(matrix, 3).reshape(dimension, dimension, 3) - - def draw(self, image: numpy.array, K: numpy.array, D: numpy.array, color: tuple = None, draw_axes: dict = None): - """Draw marker in image. - - Parameters: - image: image where to - K: - D: - color: marker color (if None, no marker drawn) - draw_axes: enable marker axes drawing - - !!! warning - draw_axes needs marker size and pose estimation. - """ - - # Draw marker if required - if color is not None: - - aruco.drawDetectedMarkers(image, [numpy.array([list(self.corners)])], numpy.array([self.identifier]), color) - - # Draw marker axes if pose has been estimated, marker have a size and if required - if self.translation.size == 3 and self.rotation.size == 9 and not math.isnan(self.size) and draw_axes is not None: - - cv2.drawFrameAxes(image, numpy.array(K), numpy.array(D), self.rotation, self.translation, self.size, **draw_axes) - - def save(self, destination_folder, dpi): - """Save marker image as .png file into a destination folder.""" - - filename = f'{self.dictionary.name}_{self.dictionary.format}_{self.identifier}.png' - filepath = f'{destination_folder}/{filename}' - - cv2.imwrite(filepath, self.image(dpi)) - diff --git a/src/argaze/ArUcoMarkers/ArUcoMarkersDictionary.py b/src/argaze/ArUcoMarkers/ArUcoMarkersDictionary.py deleted file mode 100644 index 613a3c5..0000000 --- a/src/argaze/ArUcoMarkers/ArUcoMarkersDictionary.py +++ /dev/null @@ -1,161 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import cv2.aruco as aruco - -all_aruco_markers_dictionaries = { - 'DICT_4X4_50': aruco.DICT_4X4_50, - 'DICT_4X4_100': aruco.DICT_4X4_100, - 'DICT_4X4_250': aruco.DICT_4X4_250, - 'DICT_4X4_1000': aruco.DICT_4X4_1000, - 'DICT_5X5_50': aruco.DICT_5X5_50, - 'DICT_5X5_100': aruco.DICT_5X5_100, - 'DICT_5X5_250': aruco.DICT_5X5_250, - 'DICT_5X5_1000': aruco.DICT_5X5_1000, - 'DICT_6X6_50': aruco.DICT_6X6_50, - 'DICT_6X6_100': aruco.DICT_6X6_100, - 'DICT_6X6_250': aruco.DICT_6X6_250, - 'DICT_6X6_1000': aruco.DICT_6X6_1000, - 'DICT_7X7_50': aruco.DICT_7X7_50, - 'DICT_7X7_100': aruco.DICT_7X7_100, - 'DICT_7X7_250': aruco.DICT_7X7_250, - 'DICT_7X7_1000': aruco.DICT_7X7_1000, - 'DICT_ARUCO_ORIGINAL': aruco.DICT_ARUCO_ORIGINAL, - 'DICT_APRILTAG_16h5': aruco.DICT_APRILTAG_16h5, - 'DICT_APRILTAG_25h9': aruco.DICT_APRILTAG_25h9, - 'DICT_APRILTAG_36h10': aruco.DICT_APRILTAG_36h10, - 'DICT_APRILTAG_36h11': aruco.DICT_APRILTAG_36h11 -} -"""Dictionary to list all built-in ArUco markers dictionaries from OpenCV ArUco package.""" - -class ArUcoMarkersDictionary(): - """Handle an ArUco markers dictionary.""" - - def __init__(self, name: str = 'DICT_ARUCO_ORIGINAL'): - - self.__name = name - - if all_aruco_markers_dictionaries.get(self.__name, None) is None: - raise NameError(f'Bad ArUco markers dictionary name: {self.__name}') - - @property - def name(self): - """Dictionary name""" - - return self.__name - - def __str__(self) -> str: - """String display""" - - output = f'{self.name}\n' - return output - - @property - def markers(self) -> aruco.Dictionary: - """Get all markers from dictionary.""" - - return aruco.getPredefinedDictionary(all_aruco_markers_dictionaries[self.name]) - - @property - def format(self) -> str: - """Get markers format.""" - - dict_name_split = self.name.split('_') - dict_type = dict_name_split[1] - - # DICT_ARUCO_ORIGINAL case - if dict_type == 'ARUCO': - return '5X5' - - # DICT_APRILTAG case - elif dict_type == 'APRILTAG': - - april_tag_format = dict_name_split[2] - - if april_tag_format == '16h5': - return '4X4' - - elif april_tag_format == '25h9': - return '5X5' - - elif april_tag_format == '36h10': - return '6X6' - - elif april_tag_format == '36h11': - return '6X6' - - # other cases - else: - return dict_type - - @property - def number(self) -> int: - """Get number of markers inside dictionary.""" - - dict_name_split = self.name.split('_') - dict_type = dict_name_split[1] - - # DICT_ARUCO_ORIGINAL case - if dict_type == 'ARUCO': - return 1024 - - # DICT_APRILTAG case - elif dict_type == 'APRILTAG': - - april_tag_format = dict_name_split[2] - - if april_tag_format == '16h5': - - return 30 - - elif april_tag_format == '25h9': - - return 30 - - elif april_tag_format == '36h10': - - return 2320 - - elif april_tag_format == '36h11': - - return 587 - - # other cases - else: - - return int(dict_name_split[2]) - - def create_marker(self, i, size): - """Create a marker.""" - - if i >= 0 and i < self.number: - - from argaze.ArUcoMarkers import ArUcoMarker - - return ArUcoMarker.ArUcoMarker(self, i, size) - - else: - - raise ValueError(f'Bad index: {i}') - - def save(self, destination_folder, size, dpi): - """Save all markers dictionary into separated .png files.""" - - for i in range(self.number): - - self.create_marker(i, size).save(destination_folder, dpi) diff --git a/src/argaze/ArUcoMarkers/ArUcoMarkersGroup.py b/src/argaze/ArUcoMarkers/ArUcoMarkersGroup.py deleted file mode 100644 index fd33664..0000000 --- a/src/argaze/ArUcoMarkers/ArUcoMarkersGroup.py +++ /dev/null @@ -1,476 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import math -import re -from dataclasses import dataclass -from typing import Self - -import cv2 -import numpy - -from argaze import DataFeatures -from argaze.ArUcoMarkers import ArUcoMarkersDictionary, ArUcoMarker - -T0 = numpy.array([0., 0., 0.]) -"""Define no translation vector.""" - -R0 = numpy.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 1.]]) -"""Define no rotation matrix.""" - - -def make_rotation_matrix(x, y, z): - # Create rotation matrix around x-axis - c = numpy.cos(numpy.deg2rad(x)) - s = numpy.sin(numpy.deg2rad(x)) - rx = numpy.array([[1, 0, 0], [0, c, -s], [0, s, c]]) - - # Create rotation matrix around y-axis - c = numpy.cos(numpy.deg2rad(y)) - s = numpy.sin(numpy.deg2rad(y)) - ry = numpy.array([[c, 0, s], [0, 1, 0], [-s, 0, c]]) - - # Create rotation matrix around z axis - c = numpy.cos(numpy.deg2rad(z)) - s = numpy.sin(numpy.deg2rad(z)) - rz = numpy.array([[c, -s, 0], [s, c, 0], [0, 0, 1]]) - - # Return intrinsic rotation matrix - return rx.dot(ry.dot(rz)) - - -def is_rotation_matrix(mat): - rt = numpy.transpose(mat) - should_be_identity = numpy.dot(rt, mat) - i = numpy.identity(3, dtype=mat.dtype) - n = numpy.linalg.norm(i - should_be_identity) - - return n < 1e-3 - - -@dataclass(frozen=True) -class Place: - """Define a place as list of corners position and a marker. - - Parameters: - corners: 3D corners position in group referential. - marker: ArUco marker linked to the place. - """ - - corners: numpy.array - marker: ArUcoMarker.ArUcoMarker - - -class ArUcoMarkersGroup(DataFeatures.PipelineStepObject): - """ - Handle group of ArUco markers as one unique spatial entity and estimate its pose. - """ - - # noinspection PyMissingConstructor - @DataFeatures.PipelineStepInit - def __init__(self, **kwargs): - """Initialize ArUcoMarkersGroup""" - - # Init private attributes - self.marker_size = None - self.__dictionary = None - self.__places = {} - self.__translation = numpy.zeros(3) - self.__rotation = numpy.zeros(3) - - @property - def dictionary(self) -> ArUcoMarkersDictionary.ArUcoMarkersDictionary: - """Expected dictionary of all markers in the group.""" - return self.__dictionary - - @dictionary.setter - def dictionary(self, dictionary: ArUcoMarkersDictionary.ArUcoMarkersDictionary): - - self.__dictionary = dictionary - - @property - def places(self) -> dict: - """Expected markers place.""" - return self.__places - - @places.setter - def places(self, places: dict): - - # Normalize places data - new_places = {} - - for identifier, data in places.items(): - - # Convert string identifier to int value - if type(identifier) is str: - - identifier = int(identifier) - - # Get translation vector - tvec = numpy.array(data.pop('translation')).astype(numpy.float32) - - # Check rotation value shape - rvalue = numpy.array(data.pop('rotation')).astype(numpy.float32) - - # Rotation matrix - if rvalue.shape == (3, 3): - - rmat = rvalue - - # Rotation vector (expected in degree) - elif rvalue.shape == (3,): - - rmat = make_rotation_matrix(rvalue[0], rvalue[1], rvalue[2]).astype(numpy.float32) - - else: - - raise ValueError(f'Bad rotation value: {rvalue}') - - assert (is_rotation_matrix(rmat)) - - # Get marker size - size = float(numpy.array(data.pop('size')).astype(numpy.float32)) - - new_marker = ArUcoMarker.ArUcoMarker(self.__dictionary, identifier, size) - - # Build marker corners thanks to translation vector and rotation matrix - place_corners = numpy.array([[-size / 2, size / 2, 0], [size / 2, size / 2, 0], [size / 2, -size / 2, 0], [-size / 2, -size / 2, 0]]) - place_corners = place_corners.dot(rmat) + tvec - - new_places[identifier] = Place(place_corners, new_marker) - - # else places are configured using detected markers estimated points - elif isinstance(data, ArUcoMarker.ArUcoMarker): - - new_places[identifier] = Place(data.points, data) - - # else places are already at expected format - elif (type(identifier) is int) and isinstance(data, Place): - - new_places[identifier] = data - - self.__places = new_places - - @property - def identifiers(self) -> list: - """List place marker identifiers belonging to the group.""" - return list(self.__places.keys()) - - @property - def translation(self) -> numpy.array: - """Get ArUco marker group translation vector.""" - return self.__translation - - @translation.setter - def translation(self, tvec): - """Set ArUco marker group translation vector.""" - self.__translation = tvec - - @property - def rotation(self) -> numpy.array: - """Get ArUco marker group rotation matrix.""" - return self.__translation - - @rotation.setter - def rotation(self, rmat): - """Set ArUco marker group rotation matrix.""" - self.__rotation = rmat - - def as_dict(self) -> dict: - """Export ArUco marker group properties as dictionary.""" - - return { - **DataFeatures.PipelineStepObject.as_dict(self), - "dictionary": self.__dictionary, - "places": self.__places - } - - @classmethod - def from_obj(cls, obj_filepath: str) -> Self: - """Load ArUco markers group from .obj file. - - !!! note - Expected object (o) name format: #_Marker - - !!! note - All markers have to belong to the same dictionary. - - """ - - new_dictionary = None - new_places = {} - - # Regex rules for .obj file parsing - obj_rx_dict = { - 'object': re.compile(r'o (.*)#([0-9]+)_(.*)\n'), - 'vertices': re.compile(r'v ([+-]?[0-9]*[.]?[0-9]+) ([+-]?[0-9]*[.]?[0-9]+) ([+-]?[0-9]*[.]?[0-9]+)\n'), - 'face': re.compile(r'f ([0-9]+) ([0-9]+) ([0-9]+) ([0-9]+)\n'), - 'comment': re.compile(r'#(.*)\n') - # keep comment regex after object regex because the # is used in object string too - } - - # Regex .obj line parser - def __parse_obj_line(ln): - - for k, rx in obj_rx_dict.items(): - m = rx.search(ln) - if m: - return k, m - - # If there are no matches - return None, None - - # Start parsing - try: - - identifier = None - vertices = [] - faces = {} - - # Open the file and read through it line by line - with open(obj_filepath, 'r') as file: - - line = file.readline() - - while line: - - # At each line check for a match with a regex - key, match = __parse_obj_line(line) - - # Extract comment - if key == 'comment': - pass - - # Extract marker dictionary and identifier - elif key == 'object': - - dictionary = str(match.group(1)) - identifier = int(match.group(2)) - - # Init new group dictionary with first dictionary name - if new_dictionary is None: - - new_dictionary = ArUcoMarkersDictionary.ArUcoMarkersDictionary(dictionary) - - # Check all others marker dictionary are equal to new group dictionary - elif dictionary != new_dictionary.name: - - raise NameError(f'Marker {identifier} dictionary is not {new_dictionary.name}') - - # Fill vertices array - elif key == 'vertices': - - vertices.append(tuple([float(match.group(1)), float(match.group(2)), float(match.group(3))])) - - # Extract vertices ids - elif key == 'face': - - faces[identifier] = [int(match.group(1)), int(match.group(2)), int(match.group(3)), int(match.group(4))] - - # Go to next line - line = file.readline() - - file.close() - - # Retrieve marker vertices thanks to face vertices ids - for identifier, face in faces.items(): - - # Gather place corners in clockwise order - cw_corners = numpy.array([vertices[i - 1] for i in reversed(face)]) - - # Edit place axis from corners positions - place_x_axis = cw_corners[2] - cw_corners[3] - place_x_axis_norm = numpy.linalg.norm(place_x_axis) - - place_y_axis = cw_corners[0] - cw_corners[3] - place_y_axis_norm = numpy.linalg.norm(place_y_axis) - - # Check axis size: they should be almost equal - if math.isclose(place_x_axis_norm, place_y_axis_norm, rel_tol=1e-3): - - new_marker_size = place_x_axis_norm - - else: - - raise ValueError(f'{new_dictionary}#{identifier}_Marker is not a square.') - - # Create a new place related to a new marker - new_marker = ArUcoMarker.ArUcoMarker(new_dictionary, identifier, new_marker_size) - new_places[identifier] = Place(cw_corners, new_marker) - - except IOError: - raise IOError(f'File not found: {obj_filepath}') - - # Instantiate ArUco markers group - data = { - 'dictionary': new_dictionary, - 'places': new_places - } - - return ArUcoMarkersGroup(**data) - - def filter_markers(self, detected_markers: dict) -> tuple[dict, dict]: - """Sort markers belonging to the group from given detected markers dict (cf ArUcoDetector.detect_markers()). - - Returns: - dict of markers belonging to this group - dict of remaining markers not belonging to this group - """ - - group_markers = {} - remaining_markers = {} - - for (marker_id, marker) in detected_markers.items(): - - if marker_id in self.__places.keys(): - - group_markers[marker_id] = marker - - else: - - remaining_markers[marker_id] = marker - - return group_markers, remaining_markers - - def estimate_pose_from_markers_corners(self, markers: dict, k: numpy.array, d: numpy.array) -> tuple[ - bool, numpy.array, numpy.array]: - """Estimate pose from markers corners and places corners. - - Parameters: - markers: detected markers to use for pose estimation. - k: intrinsic camera parameters - d: camera distortion matrix - - Returns: - success: True if the pose estimation succeeded - tvec: scene translation vector - rvec: scene rotation vector - """ - - markers_corners_2d = [] - places_corners_3d = [] - - for identifier, marker in markers.items(): - - try: - - place = self.__places[identifier] - - for marker_corner in marker.corners: - markers_corners_2d.append(list(marker_corner)) - - for place_corner in place.corners: - places_corners_3d.append(list(place_corner)) - - except KeyError: - - raise ValueError(f'Marker {marker.identifier} doesn\'t belong to the group.') - - # SolvPnP using cv2.SOLVEPNP_SQPNP flag - # TODO: it works also with cv2.SOLVEPNP_EPNP flag so we need to test which is the faster. - # About SolvPnP flags: https://docs.opencv.org/4.x/d5/d1f/calib3d_solvePnP.html - success, rvec, tvec = cv2.solvePnP(numpy.array(places_corners_3d), numpy.array(markers_corners_2d), numpy.array(k), numpy.array(d), flags=cv2.SOLVEPNP_SQPNP) - - # Refine pose estimation using Gauss-Newton optimisation - if success: - rvec, tvec = cv2.solvePnPRefineVVS(numpy.array(places_corners_3d), numpy.array(markers_corners_2d), numpy.array(k), numpy.array(d), rvec, tvec) - - self.__translation = tvec.T - self.__rotation = rvec.T - - return success, self.__translation, self.__rotation - - def draw_axes(self, image: numpy.array, k: numpy.array, d: numpy.array, thickness: int = 0, length: float = 0): - """Draw group axes.""" - - try: - axis_points = numpy.float32([[length, 0, 0], [0, length, 0], [0, 0, length], [0, 0, 0]]).reshape(-1, 3) - axis_points, _ = cv2.projectPoints(axis_points, self.__rotation, self.__translation, numpy.array(k), numpy.array(d)) - axis_points = axis_points.astype(int) - - cv2.line(image, tuple(axis_points[3].ravel()), tuple(axis_points[0].ravel()), (0, 0, 255), thickness) # X (red) - cv2.line(image, tuple(axis_points[3].ravel()), tuple(axis_points[1].ravel()), (0, 255, 0), thickness) # Y (green) - cv2.line(image, tuple(axis_points[3].ravel()), tuple(axis_points[2].ravel()), (255, 0, 0), thickness) # Z (blue) - - # Ignore errors due to out of field axis: their coordinate are larger than int32 limitations. - except cv2.error: - pass - - def draw_places(self, image: numpy.array, k: numpy.array, d: numpy.array, color: tuple = None, border_size: int = 0): - """Draw group places.""" - - for identifier, place in self.__places.items(): - - try: - - place_points, _ = cv2.projectPoints(place.corners, self.__rotation, self.__translation, numpy.array(k), numpy.array(d)) - place_points = place_points.astype(int) - - cv2.line(image, tuple(place_points[0].ravel()), tuple(place_points[1].ravel()), color, border_size) - cv2.line(image, tuple(place_points[1].ravel()), tuple(place_points[2].ravel()), color, border_size) - cv2.line(image, tuple(place_points[2].ravel()), tuple(place_points[3].ravel()), color, border_size) - cv2.line(image, tuple(place_points[3].ravel()), tuple(place_points[0].ravel()), color, border_size) - - # Ignore errors due to out of field places: their coordinate are larger than int32 limitations. - except cv2.error: - pass - - def draw(self, image: numpy.array, k: numpy.array, d: numpy.array, draw_axes: dict = None, draw_places: dict = None): - """Draw group axes and places. - - Parameters: - image: where to draw. - k: intrinsic camera parameters - d: camera distortion matrix - draw_axes: draw_axes parameters (if None, no axes drawn) - draw_places: draw_places parameters (if None, no places drawn) - """ - - # Draw axes if required - if draw_axes is not None: - self.draw_axes(image, k, d, **draw_axes) - - # Draw places if required - if draw_places is not None: - self.draw_places(image, k, d, **draw_places) - - def to_obj(self, obj_filepath): - """Save group to .obj file.""" - - with open(obj_filepath, 'w', encoding='utf-8') as file: - - file.write('# ArGaze OBJ File\n') - file.write('# https://achil.recherche.enac.fr/features/eye/argaze/\n') - - v_count = 0 - - for p, (identifier, place) in enumerate(self.__places.items()): - - file.write(f'o {self.__dictionary.name}#{identifier}_Marker\n') - - vertices = '' - - # Write vertices in reverse order - for v in [3, 2, 1, 0]: - file.write(f'v {" ".join(map(str, place.corners[v]))}\n') - v_count += 1 - - vertices += f' {v_count}' - - # file.write('s off\n') - file.write(f'f{vertices}\n') diff --git a/src/argaze/ArUcoMarkers/ArUcoOpticCalibrator.py b/src/argaze/ArUcoMarkers/ArUcoOpticCalibrator.py deleted file mode 100644 index 7d4b271..0000000 --- a/src/argaze/ArUcoMarkers/ArUcoOpticCalibrator.py +++ /dev/null @@ -1,162 +0,0 @@ -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -from dataclasses import dataclass, field - -from argaze import DataFeatures -from argaze.ArUcoMarkers import ArUcoBoard - -import json -import numpy -import cv2 -import cv2.aruco as aruco - - -def K0(focal_length: tuple, width: int, height: int) -> numpy.array: - """Define default optic intrinsic parameters' matrix. - - Parameters: - focal_length: - width: in pixel. - height: in pixel. - """ - - return numpy.array([[focal_length[0], 0., width / 2], [0., focal_length[1], height / 2], [0., 0., 1.]]) - - -D0 = numpy.array([0.0, 0.0, 0.0, 0.0, 0.0]) -"""Define default optic distortion coefficients vector.""" - - -@dataclass -class OpticParameters(): - """Define optic parameters output by optic calibrator.""" - - rms: float = field(default=0) - """Root Mean Square error of calibration.""" - - dimensions: numpy.array = field(default_factory=lambda: numpy.array([0, 0])) - """Image dimensions in pixels from which the calibration have been done.""" - - K: numpy.array = field(default_factory=lambda: K0((0, 0), 0, 0)) - """Intrinsic parameters matrix (focal lengths and principal point).""" - - D: numpy.array = field(default_factory=lambda: D0) - """Distortion coefficients vector.""" - - @classmethod - def from_json(cls, json_filepath): - """Load optical parameters from .json file.""" - - with open(json_filepath) as calibration_file: - return OpticParameters(**json.load(calibration_file)) - - def to_json(self, json_filepath): - """Save optical parameters into .json file.""" - - with open(json_filepath, 'w', encoding='utf-8') as calibration_file: - json.dump(self, calibration_file, ensure_ascii=False, indent=4, cls=DataFeatures.JsonEncoder) - - def __str__(self) -> str: - """String display""" - - output = f'\trms: {self.rms}\n' - output += f'\tdimensions: {self.dimensions}\n' - output += f'\tK: {self.K}\n' - output += f'\tD: {self.D}\n' - - return output - - def draw(self, image: numpy.array, width: float = 0., height: float = 0., z: float = 0., point_size: int = 1, - point_color: tuple = (0, 0, 0)): - """Draw grid to display K and D""" - - if width * height > 0.: - - # Edit 3D grid - grid_3D = [] - for x in range(-int(width / 2), int(width / 2)): - for y in range(-int(height / 2), int(height / 2)): - grid_3D.append([x, y, z]) - - # Project 3d grid - grid_2D, _ = cv2.projectPoints(numpy.array(grid_3D).astype(float), numpy.array([0., 0., 0.]), - numpy.array([0., 0., 0.]), numpy.array(self.K), -numpy.array(self.D)) - - # Draw projection - for point in grid_2D: - - # Ignore point out field - try: - - cv2.circle(image, point.astype(int)[0], point_size, point_color, -1) - - except: - - pass - - -class ArUcoOpticCalibrator(): - """Handle optic calibration process.""" - - def __init__(self): - - # Calibration data - self.__corners_set_number = 0 - self.__corners_set = [] - self.__corners_set_ids = [] - - def calibrate(self, board: ArUcoBoard.ArUcoBoard, dimensions: list = None) -> OpticParameters: - """Retrieve K and D parameters from stored calibration data. - - Parameters: - board: [ArUcoBoard](argaze.md/#argaze.ArUcoMarkers.ArUcoBoard.ArUcoBoard) instance - dimensions: camera image dimensions - - Returns: - Optic parameters - """ - - if dimensions is None: - dimensions = [0, 0] - - if self.__corners_set_number > 0: - rms, K, D, r, t = aruco.calibrateCameraCharuco(self.__corners_set, self.__corners_set_ids, board.model, - dimensions, None, None) - - return OpticParameters(rms, dimensions, K, D) - - def reset_calibration_data(self): - """Clear all calibration data.""" - - self.__corners_set_number = 0 - self.__corners_set = [] - self.__corners_set_ids = [] - - def store_calibration_data(self, corners, corners_identifiers): - """Store calibration data.""" - - self.__corners_set_number += 1 - self.__corners_set.append(corners) - self.__corners_set_ids.append(corners_identifiers) - - @property - def calibration_data_count(self) -> int: - """Get how much calibration data are stored.""" - - return self.__corners_set_number diff --git a/src/argaze/ArUcoMarkers/ArUcoScene.py b/src/argaze/ArUcoMarkers/ArUcoScene.py deleted file mode 100644 index 0edb253..0000000 --- a/src/argaze/ArUcoMarkers/ArUcoScene.py +++ /dev/null @@ -1,126 +0,0 @@ -"""ArScene based of ArUco markers technology. - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import numpy - -from argaze import ArFeatures, DataFeatures -from argaze.ArUcoMarkers import ArUcoMarkersGroup - - -class ArUcoScene(ArFeatures.ArScene): - """ - Define an ArScene based on an ArUcoMarkersGroup description. - """ - - @DataFeatures.PipelineStepInit - def __init__(self, **kwargs): - """Initialize ArUcoScene""" - - # Init ArScene classes - super().__init__() - - # Init private attribute - self.__aruco_markers_group = None - self.__required_markers_number = 2 - - @property - def aruco_markers_group(self) -> ArUcoMarkersGroup.ArUcoMarkersGroup: - """ArUco markers 3D scene description used to estimate scene pose from detected markers: see [estimate_pose][argaze.ArFeatures.ArScene.estimate_pose] function below.""" - return self.__aruco_markers_group - - @aruco_markers_group.setter - @DataFeatures.PipelineStepAttributeSetter - def aruco_markers_group(self, aruco_markers_group: ArUcoMarkersGroup.ArUcoMarkersGroup): - - self.__aruco_markers_group = aruco_markers_group - - # Edit parent - if self.__aruco_markers_group is not None: - - self.__aruco_markers_group.parent = self - - @property - def required_markers_number(self) -> int: - """Numbers of markers that have to be detected to allow pose estimation (default: 2).""" - - return self.__required_markers_number - - @required_markers_number.setter - def required_markers_number(self, n: int): - - # Constrain number to 1 at least - if n < 1: - - self.__required_markers_number = 1 - - else: - - self.__required_markers_number = n - - @DataFeatures.PipelineStepMethod - def estimate_pose(self, detected_markers: dict) -> tuple[numpy.array, numpy.array, dict]: - """Estimate scene pose from detected ArUco markers. - - Parameters: - detected_markers: dictionary with all detected markers - - Returns: - scene translation vector - scene rotation matrix - dict of markers used to estimate the pose - """ - - # Pose estimation fails when no marker is detected - if len(detected_markers) == 0: - - raise ArFeatures.PoseEstimationFailed('No marker detected') - - scene_markers, _ = self.__aruco_markers_group.filter_markers(detected_markers) - - # Pose estimation fails when no marker belongs to the scene - if len(scene_markers) == 0: - - raise ArFeatures.PoseEstimationFailed('No marker belongs to the scene') - - # Pose estimation fails when not enough marker belongs to the scene - if len(scene_markers) < self.required_markers_number: - - raise ArFeatures.PoseEstimationFailed(f'Not enough marker belongs to the scene') - - # Estimate pose from markers corners - success, tvec, rmat = self.__aruco_markers_group.estimate_pose_from_markers_corners(scene_markers, self.parent.aruco_detector.optic_parameters.K, self.parent.aruco_detector.optic_parameters.D) - - if not success: - - raise ArFeatures.PoseEstimationFailed('Can\'t estimate pose from markers corners positions') - - return tvec, rmat, scene_markers - - def draw(self, image: numpy.array, draw_aruco_markers_group: dict = None): - """ - Draw scene into image. - - Parameters: - image: where to draw - draw_aruco_markers_group: ArUcoMarkersGroup.draw parameters (if None, no group drawn) - """ - - # Draw group if required - if draw_aruco_markers_group is not None: - - self.__aruco_markers_group.draw(image, self.parent.aruco_detector.optic_parameters.K, self.parent.aruco_detector.optic_parameters.D, **draw_aruco_markers_group) diff --git a/src/argaze/ArUcoMarkers/__init__.py b/src/argaze/ArUcoMarkers/__init__.py deleted file mode 100644 index b7b0bf8..0000000 --- a/src/argaze/ArUcoMarkers/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -Handle [OpenCV ArUco markers](https://docs.opencv.org/4.x/d5/dae/tutorial_aruco_detection.html): generate and detect -markers, calibrate camera, describe scene, ... -""" -__all__ = ['ArUcoMarkersDictionary', 'ArUcoMarker', 'ArUcoBoard', 'ArUcoOpticCalibrator', 'ArUcoDetector', - 'ArUcoMarkersGroup', 'ArUcoCamera', 'ArUcoScene', 'utils'] diff --git a/src/argaze/ArUcoMarkers/utils/A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf b/src/argaze/ArUcoMarkers/utils/A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf deleted file mode 100644 index 2adcee1..0000000 Binary files a/src/argaze/ArUcoMarkers/utils/A3_DICT_APRILTAG_16h5_3cm_35cmx25cm.pdf and /dev/null differ diff --git a/src/argaze/ArUcoMarkers/utils/A4_DICT_APRILTAG_16h5_5cm_0-7.pdf b/src/argaze/ArUcoMarkers/utils/A4_DICT_APRILTAG_16h5_5cm_0-7.pdf deleted file mode 100644 index fcf850d..0000000 Binary files a/src/argaze/ArUcoMarkers/utils/A4_DICT_APRILTAG_16h5_5cm_0-7.pdf and /dev/null differ diff --git a/src/argaze/ArUcoMarkers/utils/__init__.py b/src/argaze/ArUcoMarkers/utils/__init__.py deleted file mode 100644 index 923f5ec..0000000 --- a/src/argaze/ArUcoMarkers/utils/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -Print **A3_DICT_ARUCO_ORIGINAL_3cm_35cmx25cm.pdf** onto A3 paper sheet to get board at expected dimensions. - -Print **A4_DICT_ARUCO_ORIGINAL_3cm_0-9.pdf** onto A4 paper sheet to get markers at expected dimensions. -""" \ No newline at end of file diff --git a/src/argaze/__init__.py b/src/argaze/__init__.py index 2e004f1..a07fa93 100644 --- a/src/argaze/__init__.py +++ b/src/argaze/__init__.py @@ -1,7 +1,7 @@ """ ArGaze is divided in submodules dedicated to various specifics features. """ -__all__ = ['ArUcoMarkers', 'AreaOfInterest', 'ArFeatures', 'GazeFeatures', 'GazeAnalysis', 'PupilFeatures', 'PupilAnalysis', 'DataFeatures', 'utils'] +__all__ = ['ArUcoMarker', 'AreaOfInterest', 'ArFeatures', 'GazeFeatures', 'GazeAnalysis', 'PupilFeatures', 'PupilAnalysis', 'DataFeatures', 'utils'] def load(filepath: str) -> any: """ diff --git a/src/argaze/utils/aruco_markers_group_export.py b/src/argaze/utils/aruco_markers_group_export.py deleted file mode 100644 index 569ba6b..0000000 --- a/src/argaze/utils/aruco_markers_group_export.py +++ /dev/null @@ -1,234 +0,0 @@ -#!/usr/bin/env python - -""" - -This program is free software: you can redistribute it and/or modify it under -the terms of the GNU General Public License as published by the Free Software -Foundation, either version 3 of the License, or (at your option) any later -version. -This program is distributed in the hope that it will be useful, but WITHOUT -ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. -You should have received a copy of the GNU General Public License along with -this program. If not, see . -""" - -__author__ = "Théo de la Hogue" -__credits__ = [] -__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)" -__license__ = "GPLv3" - -import argparse -import contextlib - -import cv2 - -from argaze import DataFeatures -from argaze.ArUcoMarkers import ArUcoDetector, ArUcoOpticCalibrator, ArUcoMarkersGroup -from argaze.utils import UtilsFeatures - - -def main(): - """ - Detect DICTIONARY and SIZE ArUco markers inside a MOVIE frame then, export detected ArUco markers group as .obj file into an OUTPUT folder. - """ - - # Manage arguments - parser = argparse.ArgumentParser(description=main.__doc__.split('-')[0]) - parser.add_argument('movie', metavar='MOVIE', type=str, default=None, help='movie path') - parser.add_argument('dictionary', metavar='DICTIONARY', type=str, default=None, - help='expected ArUco markers dictionary') - parser.add_argument('size', metavar='SIZE', type=float, default=None, help='expected ArUco markers size (in cm)') - - parser.add_argument('-p', '--parameters', metavar='PARAMETERS', type=str, default=None, - help='ArUco detector parameters file') - parser.add_argument('-op', '--optic_parameters', metavar='OPTIC_PARAMETERS', type=str, default=None, - help='ArUco detector optic parameters file') - - parser.add_argument('-s', '--start', metavar='START', type=float, default=0., help='start time in second') - parser.add_argument('-o', '--output', metavar='OUTPUT', type=str, default='.', help='export folder path') - parser.add_argument('-v', '--verbose', action='store_true', default=False, - help='enable verbose mode to print information in console') - - args = parser.parse_args() - - # Load movie - video_capture = cv2.VideoCapture(args.movie) - - video_fps = video_capture.get(cv2.CAP_PROP_FPS) - image_width = int(video_capture.get(cv2.CAP_PROP_FRAME_WIDTH)) - image_height = int(video_capture.get(cv2.CAP_PROP_FRAME_HEIGHT)) - - # Edit ArUco detector configuration - configuration = { - "dictionary": args.dictionary - } - - if args.parameters: - configuration["parameters"] = args.parameters - - if args.optic_parameters: - configuration["optic_parameters"] = args.optic_parameters - - # Load ArUco detector configuration - aruco_detector = DataFeatures.from_dict(ArUcoDetector.ArUcoDetector, configuration) - - if args.verbose: - print(aruco_detector) - - # Create empty ArUco scene - aruco_markers_group = None - - # Edit draw parameters - draw_parameters = { - "color": [255, 255, 255], - "draw_axes": { - "thickness": 4 - } - } - - # Create a window - cv2.namedWindow("Export detected ArUco markers", cv2.WINDOW_AUTOSIZE) - - # Init image selection - current_image_index = -1 - _, current_image = video_capture.read() - next_image_index = int(args.start * video_fps) - refresh = False - - # Waiting for 'ctrl+C' interruption - with contextlib.suppress(KeyboardInterrupt): - - while True: - - # Select a new image and detect markers once - if next_image_index != current_image_index or refresh: - - video_capture.set(cv2.CAP_PROP_POS_FRAMES, next_image_index) - - success, video_image = video_capture.read() - - video_height, video_width, _ = video_image.shape - - # Create default optic parameters adapted to frame size - if aruco_detector.optic_parameters is None: - # Note: The choice of 1000 for default focal length should be discussed... - aruco_detector.optic_parameters = ArUcoOpticCalibrator.OpticParameters(rms=-1, dimensions=( - video_width, video_height), K=ArUcoOpticCalibrator.K0(focal_length=(1000., 1000.), - width=video_width, height=video_height)) - - if success: - - # Refresh once - refresh = False - - current_image_index = video_capture.get(cv2.CAP_PROP_POS_FRAMES) - 1 - current_image_time = video_capture.get(cv2.CAP_PROP_POS_MSEC) - - try: - - # Detect and project AR features - aruco_detector.detect_markers(video_image) - - # Estimate all detected markers pose - aruco_detector.estimate_markers_pose(args.size) - - # Build aruco scene from detected markers - aruco_markers_group = ArUcoMarkersGroup.ArUcoMarkersGroup(aruco_detector.dictionary, - aruco_detector.detected_markers()) - - # Detection succeeded - exception = None - - # Write errors - except Exception as e: - - aruco_markers_group = None - - exception = e - - # Draw detected markers - aruco_detector.draw_detected_markers(video_image, draw_parameters) - - # Write detected markers - cv2.putText(video_image, f'Detecting markers {list(aruco_detector.detected_markers().keys())}', - (20, video_height - 40), cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv2.LINE_AA) - - # Write timing - cv2.putText(video_image, f'Frame at {int(current_image_time)}ms', (20, 40), - cv2.FONT_HERSHEY_SIMPLEX, 1, (255, 255, 255), 1, cv2.LINE_AA) - - # Write exception - if exception is not None: - cv2.putText(video_image, f'error: {exception}', (20, 80), cv2.FONT_HERSHEY_SIMPLEX, 1, - (0, 255, 255), 1, cv2.LINE_AA) - - # Write documentation - cv2.putText(video_image, f'<- previous image', (video_width - 500, video_height - 160), - cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA) - cv2.putText(video_image, f'-> next image', (video_width - 500, video_height - 120), - cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA) - cv2.putText(video_image, f'r: reload config', (video_width - 500, video_height - 80), - cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA) - cv2.putText(video_image, f'Ctrl+s: export ArUco markers', (video_width - 500, video_height - 40), - cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 255), 1, cv2.LINE_AA) - - # Copy image - current_image = video_image.copy() - - # Keep last image - else: - - video_image = current_image.copy() - - key_pressed = cv2.waitKey(10) - - #if key_pressed != -1: - # print(key_pressed) - - # Select previous image with left arrow - if key_pressed == 2: - next_image_index -= 1 - - # Select next image with right arrow - if key_pressed == 3: - next_image_index += 1 - - # Clip image index - if next_image_index < 0: - next_image_index = 0 - - # r: reload configuration - if key_pressed == 114: - aruco_detector = DataFeatures.from_dict(ArUcoDetector.ArUcoDetector, configuration) - refresh = True - print('Configuration reloaded') - - # Save selected marker edition using 'Ctrl + s' - if key_pressed == 19: - - if aruco_markers_group: - - aruco_markers_group.to_obj(f'{args.output}/{int(current_image_time)}-aruco_markers_group.obj') - print(f'ArUco markers saved into {args.output}') - - else: - - print(f'No ArUco markers to export') - - # Close window using 'Esc' key - if key_pressed == 27: - break - - # Display video - cv2.imshow(aruco_detector.name, video_image) - - # Close movie capture - video_capture.release() - - # Stop image display - cv2.destroyAllWindows() - - -if __name__ == '__main__': - main() diff --git a/src/argaze/utils/demo/aruco_markers_pipeline.json b/src/argaze/utils/demo/aruco_markers_pipeline.json index b64dde3..48071ab 100644 --- a/src/argaze/utils/demo/aruco_markers_pipeline.json +++ b/src/argaze/utils/demo/aruco_markers_pipeline.json @@ -1,5 +1,5 @@ { - "argaze.ArUcoMarkers.ArUcoCamera.ArUcoCamera": { + "argaze.ArUcoMarker.ArUcoCamera.ArUcoCamera": { "name": "Head-mounted camera", "size": [1920, 1080], "aruco_detector": { -- cgit v1.1