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