Skip to content

Commit 85405da

Browse files
authored
Merge pull request #30 from AGH-CEAI/feature/add_spatial_detection
Add spatial detection
2 parents 811591b + f31bb70 commit 85405da

File tree

9 files changed

+213
-33
lines changed

9 files changed

+213
-33
lines changed

.pre-commit-config.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ repos:
1717
- id: end-of-file-fixer
1818
- id: fix-byte-order-marker
1919
- id: mixed-line-ending
20-
- id: pretty-format-json
2120
- id: trailing-whitespace
2221
- id: check-yaml
2322
exclude: joint_limits.yaml # uses custom macro for deg<->rad transformation
23+
- id: pretty-format-json
24+
args: ['--autofix', '--no-sort-keys', '--indent', '2']
2425

2526
- repo: https://github.com/codespell-project/codespell
2627
rev: v2.4.1

aegis_bringup/docs/launch_diagram.plantuml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ package aegis_control {
4040
class ur_drivers << (Y,#ffffc9) YAML >> {}
4141
}
4242
package cameras {
43-
class cameras << (Y,#ffffc9) YAML >> {}
43+
class depthai_cameras << (Y,#ffffc9) YAML >> {}
44+
class yolo << (J,#ffffc9) JSON >> {}
4445
}
4546
}
4647
}
@@ -116,4 +117,5 @@ hide << YAML >> members
116117
hide << urdf.xacro >> members
117118
hide << SRDF >> members
118119
hide << rviz >> members
120+
hide << JSON >> members
119121
@enduml

aegis_control/CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
* [PR-30](https://github.com/AGH-CEAI/aegis_ros/pull/30) - Implemented YOLOv5 model for spatial detection.
13+
* [PR-24](https://github.com/AGH-CEAI/aegis_ros/pull/24) - Implemented point cloud support.
1214
* [PR-21](https://github.com/AGH-CEAI/aegis_ros/pull/21) - Initial version of the DepthAI driver with support for the OAK-D Pro camera.
13-
* [PR-24](https://github.com/AGH-CEAI/aegis_ros/pull/24) - Added a point cloud node.
1415
* [PR-9](https://github.com/AGH-CEAI/aegis_ros/pull/9) - Initial version of the `aegis_control` package.
1516

1617
### Changed

aegis_control/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ aegis_control/
3030
| [start_drivers.launch.py](./launch/start_drivers.launch.py) | The main launch file to run the entire Aegis' `ros2_control` stack. |
3131
| [ur_driver.launch.py](./launch/ur_driver.launch.py) | Launches nodes from the [ur_robot_driver](https://github.com/UniversalRobots/Universal_Robots_ROS2_Driver) to control the UR5e robot. |
3232

33+
## Neural network
34+
For details on training and deploying YOLOv5 model, see the [tutorial](./docs/yolov5_tutorial.md).
35+
3336
## Development notes
3437

3538
- ROS 2 Humble ships with the older structure of the [Universal_Robots_ROS2_Driver](https://github.com/UniversalRobots/Universal_Robots_ROS2_Driver/tree/humble) Due to the complexity of our project (where we need to merge several other controllers into a single `control_manager` ode configuration), the `ur_driver.launch.py` file is based on version `2.5.1` of UR's `ur_robot_driver` [launch file](https://github.com/UniversalRobots/Universal_Robots_ROS2_Driver/blob/humble/ur_robot_driver/launch/ur_control.launch.py). The main difference is the removal of all unused parameters, which are set by default by the driver itself.

aegis_control/config/cameras/depthai_cameras.yaml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
i_mx_id: 184430108157970F00
66
i_nn_type: spatial
77
i_pipeline_type: RGBD
8-
rgb:
9-
i_enable_preview: true
10-
i_keep_preview_aspect_ratio: false
118
nn:
12-
i_enable_passthrough: true
139
i_disable_resize: true
10+
i_enable_passthrough: true
11+
rgb:
12+
i_enable_preview: true
13+
i_keep_preview_aspect_ratio: true
14+
i_preview_size: 416
1415
stereo:
1516
i_subpixel: true
1617
spatial_bb_node:
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"model": {
3+
"zoo": "path"
4+
},
5+
"nn_config": {
6+
"output_format": "detection",
7+
"NN_family": "YOLO",
8+
"input_size": "416x416",
9+
"NN_specific_metadata": {
10+
"classes": 4,
11+
"coordinates": 4,
12+
"anchors": [
13+
10,
14+
13,
15+
16,
16+
30,
17+
33,
18+
23,
19+
30,
20+
61,
21+
62,
22+
45,
23+
59,
24+
119,
25+
116,
26+
90,
27+
156,
28+
198,
29+
373,
30+
326
31+
],
32+
"anchor_masks": {
33+
"side52": [
34+
0,
35+
1,
36+
2
37+
],
38+
"side26": [
39+
3,
40+
4,
41+
5
42+
],
43+
"side13": [
44+
6,
45+
7,
46+
8
47+
]
48+
},
49+
"iou_threshold": 0.5,
50+
"confidence_threshold": 0.5
51+
}
52+
},
53+
"mappings": {
54+
"labels": [
55+
"tetragon",
56+
"hexagon",
57+
"octagon",
58+
"dodecagon"
59+
]
60+
}
61+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# YOLOv5 tutorial
2+
This guide explains how to set up, train, convert and deploy a YOLOv5 model for this project.
3+
4+
## Directory structure
5+
After training, your directory structure should be organized as follows:
6+
7+
```
8+
datasets
9+
└── polygons
10+
├── test
11+
│ ├── images
12+
│ └── labels
13+
├── train
14+
│ ├── images
15+
│ └── labels
16+
└── val
17+
├── images
18+
└── labels
19+
yolov5
20+
├── data
21+
│ └── polygons.yaml
22+
├── runs
23+
│ └── train
24+
│ └── exp
25+
│ └── weights
26+
│ └── best.pt
27+
└── train.py
28+
```
29+
30+
## Dataset
31+
Download the `polygons` dataset from [this repository](https://github.com/Patrycj2a/praca_inzynierska/tree/main/datasets/polygons/) and place it in the `datasets` folder.
32+
33+
## Training configuration file
34+
Create the `polygons.yaml` file inside the `yolov5/data` directory with the following content:
35+
36+
```yaml
37+
names:
38+
0: tetragon
39+
1: hexagon
40+
2: octagon
41+
3: dodecagon
42+
nc: 4
43+
path: ../datasets/polygons
44+
train: train/images
45+
val: val/images
46+
test: test/images
47+
```
48+
49+
## YOLOv5 setup
50+
Set up the YOLOv5 repository by running the following commands:
51+
52+
```bash
53+
git clone https://github.com/ultralytics/yolov5.git
54+
cd yolov5
55+
git pull
56+
pip install -U -r yolov5/requirements.txt
57+
```
58+
59+
## Model training
60+
Train the YOLOv5 model using the following command:
61+
62+
```bash
63+
python3 train.py --img 416 --batch 16 --epochs 1000 --data polygons.yaml --weights yolov5s.pt --cos-lr
64+
```
65+
66+
The trained model weights will be saved as `best.pt` in the `yolov5/runs/train/exp/weights` directory.
67+
68+
## Model conversion
69+
1. Go to [Luxonis Tools](https://tools.luxonis.com/).
70+
2. Set `Yolo Version` to `YOLOv5`.
71+
3. Click `File` and upload the obtained model weights (`best.pt`).
72+
4. Set `Input image shape` to `416`.
73+
5. In advanced options, set `Shaves` to `5`.
74+
6. Click `Submit` to start the conversion.
75+
7. Extract the downloaded ZIP file and locate the model named `best_openvino_2022.1_5shave.blob`.
76+
77+
## Model placement
78+
Move the `best_openvino_2022.1_5shave.blob` file to the `ceai_models` in your home directory and rename it to `yolo.blob`.

aegis_control/launch/depthai_cameras_driver.launch.py

Lines changed: 58 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,25 @@
1-
from launch import LaunchDescription
1+
import json
2+
import yaml
3+
import tempfile
4+
from pathlib import Path
5+
from launch import LaunchDescription, LaunchContext
26
from launch.actions import OpaqueFunction
37
from launch.conditions import UnlessCondition
4-
from launch.substitutions import LaunchConfiguration, PathJoinSubstitution
5-
from launch_ros.actions import Node, ComposableNodeContainer, LoadComposableNodes
8+
from launch.substitutions import LaunchConfiguration
9+
from launch_ros.actions import ComposableNodeContainer, LoadComposableNodes, Node
610
from launch_ros.descriptions import ComposableNode
7-
from launch_ros.substitutions import FindPackageShare
11+
from ament_index_python.packages import get_package_share_directory
812

913

1014
class DepthAIConfig:
1115
def __init__(self):
16+
self._modify_config()
17+
1218
self.name_pro_scene = LaunchConfiguration(
1319
"name_pro_scene", default="oak_d_pro_scene"
1420
)
15-
self.params_file = PathJoinSubstitution(
16-
[
17-
FindPackageShare("aegis_control"),
18-
"config",
19-
"cameras",
20-
"depthai_cameras.yaml",
21-
]
22-
)
23-
# TODO(issue#26) create proper mock for the luxonis cameras
24-
self.mock_hardware = LaunchConfiguration("mock_hardware", default="true")
21+
# TODO(issue#26) Introduce a mock for the DepthAI cameras
22+
self.mock_hardware = LaunchConfiguration("mock_hardware", default="false")
2523

2624
self.cam_model_pro_scene = LaunchConfiguration(
2725
"camera_model_pro_scene", default="OAK-D-S2"
@@ -49,25 +47,59 @@ def __init__(self):
4947
)
5048
self.cam_yaw_pro_scene = LaunchConfiguration("cam_yaw_pro_scene", default="0")
5149

50+
def _modify_config(self) -> None:
51+
# TODO(issue#31) Fix YOLO configuration not being applied correctly
52+
package_share_path = Path(get_package_share_directory("aegis_control"))
53+
model_path = Path.home() / "ceai_models" / "yolo.blob"
54+
yolo_src_cfg_path = package_share_path / "config" / "cameras" / "yolo.json"
55+
cam_src_params_path = (
56+
package_share_path / "config" / "cameras" / "depthai_cameras.yaml"
57+
)
58+
59+
self.yolo_cfg_path = Path(
60+
tempfile.NamedTemporaryFile(suffix=".json", delete=False).name
61+
)
62+
self.cam_params_path = Path(
63+
tempfile.NamedTemporaryFile(suffix=".yaml", delete=False).name
64+
)
65+
66+
with open(yolo_src_cfg_path, "r") as file:
67+
yolo_cfg = json.load(file)
68+
69+
yolo_cfg["model"]["model_name"] = str(model_path)
70+
71+
with open(self.yolo_cfg_path, "w") as file:
72+
json.dump(yolo_cfg, file, indent=2)
73+
74+
with open(cam_src_params_path, "r") as file:
75+
cam_params = yaml.safe_load(file)
76+
77+
cam_params["/oak_d_pro_scene"]["ros__parameters"]["nn"]["i_nn_config_path"] = (
78+
str(self.yolo_cfg_path)
79+
)
80+
81+
with open(self.cam_params_path, "w") as file:
82+
yaml.safe_dump(cam_params, file)
83+
5284

5385
def generate_launch_description() -> LaunchDescription:
5486
return LaunchDescription([OpaqueFunction(function=launch_setup)])
5587

5688

57-
def launch_setup(context) -> list[Node]:
89+
def launch_setup(context: LaunchContext) -> list[Node]:
5890
# TODO(issue#22): Setup global log level configuration
5991
log_level = "info"
6092
if context.environment.get("DEPTHAI_DEBUG") == "1":
6193
log_level = "debug"
6294

6395
cfg = DepthAIConfig()
64-
name_pro_scene_str = cfg.name_pro_scene.perform(context)
96+
name_pro_scene = cfg.name_pro_scene.perform(context)
6597

6698
# TODO(issue#23): Investigate the necessity of tf parameters
6799
tf_params_pro_scene = {
68100
"camera": {
69101
"i_publish_tf_from_calibration": False,
70-
"i_tf_tf_prefix": name_pro_scene_str,
102+
"i_tf_tf_prefix": name_pro_scene,
71103
"i_tf_camera_model": cfg.cam_model_pro_scene,
72104
"i_tf_parent_frame": cfg.parent_frame_pro_scene.perform(context),
73105
"i_tf_base_frame": cfg.base_frame_pro_scene.perform(context),
@@ -83,22 +115,22 @@ def launch_setup(context) -> list[Node]:
83115
return [
84116
create_camera_node(
85117
cfg.mock_hardware,
86-
name_pro_scene_str,
118+
name_pro_scene,
87119
tf_params_pro_scene,
88-
cfg.params_file,
120+
cfg.cam_params_path,
89121
log_level,
90122
),
91-
create_rectify_node(cfg.mock_hardware, name_pro_scene_str),
92-
create_spatial_bb_node(cfg.mock_hardware, name_pro_scene_str, cfg.params_file),
93-
create_point_cloud_node(cfg.mock_hardware, name_pro_scene_str),
123+
create_rectify_node(cfg.mock_hardware, name_pro_scene),
124+
create_spatial_bb_node(cfg.mock_hardware, name_pro_scene, cfg.cam_params_path),
125+
create_point_cloud_node(cfg.mock_hardware, name_pro_scene),
94126
]
95127

96128

97129
def create_camera_node(
98130
mock_hardware: LaunchConfiguration,
99131
name: str,
100132
tf_params: dict,
101-
params_file: LaunchConfiguration,
133+
cam_params_path: LaunchConfiguration,
102134
log_level: str,
103135
) -> LoadComposableNodes:
104136
return ComposableNodeContainer(
@@ -112,7 +144,7 @@ def create_camera_node(
112144
package="depthai_ros_driver",
113145
plugin="depthai_ros_driver::Camera",
114146
name=name,
115-
parameters=[params_file, tf_params],
147+
parameters=[cam_params_path, tf_params],
116148
)
117149
],
118150
arguments=["--ros-args", "--log-level", log_level],
@@ -150,7 +182,7 @@ def create_rectify_node(
150182
def create_spatial_bb_node(
151183
mock_hardware: LaunchConfiguration,
152184
name: str,
153-
params_file: LaunchConfiguration,
185+
cam_params_path: LaunchConfiguration,
154186
) -> LoadComposableNodes:
155187
return LoadComposableNodes(
156188
condition=UnlessCondition(mock_hardware),
@@ -167,7 +199,7 @@ def create_spatial_bb_node(
167199
("overlay", name + "/overlay"),
168200
("spatial_bb", name + "/spatial_bb"),
169201
],
170-
parameters=[params_file],
202+
parameters=[cam_params_path],
171203
),
172204
],
173205
)

aegis_control/launch/start_drivers.launch.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def generate_launch_description() -> LaunchDescription:
5454
]
5555
)
5656
),
57+
launch_arguments=launch_args.items(),
5758
)
5859

5960
return LaunchDescription(

0 commit comments

Comments
 (0)