Add 'darknet_ros2/' from commit 'be4fd04c0ea8fbed80b3549283701e16145422c6'
git-subtree-dir: darknet_ros2 git-subtree-mainline: f33742e6e8ae5b436aa1ce48d8c40cb5e5e5562e git-subtree-split: be4fd04c0ea8fbed80b3549283701e16145422c6
This commit is contained in:
commit
aa265edfb1
75
darknet_ros2/CMakeLists.txt
Normal file
75
darknet_ros2/CMakeLists.txt
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.10)
|
||||||
|
project(openrobotics_darknet_ros)
|
||||||
|
|
||||||
|
# Default to C++14
|
||||||
|
if(NOT CMAKE_CXX_STANDARD)
|
||||||
|
set(CMAKE_CXX_STANDARD 14)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(NOT WIN32)
|
||||||
|
add_compile_options(-Wall -Wextra -Wpedantic)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
find_package(ament_cmake REQUIRED)
|
||||||
|
find_package(cv_bridge REQUIRED)
|
||||||
|
find_package(darknet_vendor REQUIRED)
|
||||||
|
find_package(rclcpp REQUIRED)
|
||||||
|
find_package(rclcpp_components REQUIRED)
|
||||||
|
find_package(sensor_msgs REQUIRED)
|
||||||
|
find_package(vision_msgs REQUIRED)
|
||||||
|
|
||||||
|
add_library(openrobotics_darknet_ros SHARED
|
||||||
|
src/detector_network.cpp
|
||||||
|
src/parse.cpp)
|
||||||
|
target_include_directories(openrobotics_darknet_ros PUBLIC include)
|
||||||
|
target_compile_definitions(openrobotics_darknet_ros PRIVATE "DARKNET_ROS_BUILDING_DLL")
|
||||||
|
ament_target_dependencies(openrobotics_darknet_ros
|
||||||
|
cv_bridge
|
||||||
|
darknet_vendor
|
||||||
|
sensor_msgs
|
||||||
|
vision_msgs)
|
||||||
|
|
||||||
|
add_library(detector_node SHARED
|
||||||
|
src/detector_node.cpp)
|
||||||
|
target_compile_definitions(detector_node PRIVATE "DARKNET_ROS_NODE_BUILDING_DLL")
|
||||||
|
ament_target_dependencies(detector_node PUBLIC
|
||||||
|
"rclcpp"
|
||||||
|
"rclcpp_components")
|
||||||
|
target_link_libraries(detector_node PUBLIC openrobotics_darknet_ros)
|
||||||
|
rclcpp_components_register_nodes(detector_node "openrobotics::darknet_ros::DetectorNode")
|
||||||
|
|
||||||
|
add_executable(detector_node_main
|
||||||
|
src/detector_node_main.cpp)
|
||||||
|
target_link_libraries(detector_node_main detector_node)
|
||||||
|
set_target_properties(detector_node_main PROPERTIES OUTPUT_NAME "detector_node")
|
||||||
|
|
||||||
|
if(BUILD_TESTING)
|
||||||
|
find_package(ament_lint_auto REQUIRED)
|
||||||
|
ament_lint_auto_find_test_dependencies()
|
||||||
|
|
||||||
|
ament_add_gtest(test_parser test/test_parser.cpp
|
||||||
|
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/test/")
|
||||||
|
target_link_libraries(test_parser openrobotics_darknet_ros)
|
||||||
|
|
||||||
|
ament_add_gtest(test_detector_network test/test_detector_network.cpp)
|
||||||
|
target_link_libraries(test_detector_network openrobotics_darknet_ros)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
ament_export_libraries(openrobotics_darknet_ros)
|
||||||
|
|
||||||
|
install(TARGETS openrobotics_darknet_ros
|
||||||
|
ARCHIVE DESTINATION lib
|
||||||
|
LIBRARY DESTINATION lib
|
||||||
|
RUNTIME DESTINATION bin)
|
||||||
|
|
||||||
|
install(TARGETS detector_node
|
||||||
|
ARCHIVE DESTINATION lib
|
||||||
|
LIBRARY DESTINATION lib
|
||||||
|
RUNTIME DESTINATION bin)
|
||||||
|
|
||||||
|
install(TARGETS detector_node_main
|
||||||
|
DESTINATION lib/${PROJECT_NAME})
|
||||||
|
|
||||||
|
install(DIRECTORY include/ DESTINATION include)
|
||||||
|
|
||||||
|
ament_package()
|
||||||
18
darknet_ros2/CONTRIBUTING.md
Normal file
18
darknet_ros2/CONTRIBUTING.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
Any contribution that you make to this repository will
|
||||||
|
be under the Apache 2 License, as dictated by that
|
||||||
|
[license](http://www.apache.org/licenses/LICENSE-2.0.html):
|
||||||
|
|
||||||
|
~~~
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
~~~
|
||||||
|
|
||||||
|
Contributors must sign-off each commit by adding a `Signed-off-by: ...`
|
||||||
|
line to commit messages to certify that they have the right to submit
|
||||||
|
the code they are contributing to the project according to the
|
||||||
|
[Developer Certificate of Origin (DCO)](https://developercertificate.org/).
|
||||||
202
darknet_ros2/LICENSE
Normal file
202
darknet_ros2/LICENSE
Normal file
@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
58
darknet_ros2/README.md
Normal file
58
darknet_ros2/README.md
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
# Open Robotics Darknet ROS
|
||||||
|
|
||||||
|
This is a ROS 2 wrapper around [darknet](https://pjreddie.com/darknet), an open source neural network framework.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## DetectorNode
|
||||||
|
|
||||||
|
This node can run object detectors like [YOLOv3](https://pjreddie.com/darknet/yolo/) on images.
|
||||||
|
|
||||||
|
### Subscribers
|
||||||
|
|
||||||
|
* `~/images` (type `sensor_msgs/msg/Image`) - Input mages to feed to the detector
|
||||||
|
|
||||||
|
### Publishers
|
||||||
|
|
||||||
|
* `~/detections` (type `vision_msgs/msg/Detection2DArray`) - Objects detected in an image (if any)
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
* `network.config` - a path to a file describing a darknet detector network
|
||||||
|
* `network.weights` - a path to a file with weights for the given network
|
||||||
|
* `network.class_names` - a path to a file with names of classes the network can detect (1 per line)
|
||||||
|
* `detection.threshold` - Minimum probability of a detection to be published
|
||||||
|
* `detection.nms_threshold` - Non-maximal Suppression threshold - controls filtering of overlapping boxes
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
Download `YOLOv3-tiny`.
|
||||||
|
|
||||||
|
```
|
||||||
|
wget https://raw.githubusercontent.com/pjreddie/darknet/f86901f6177dfc6116360a13cc06ab680e0c86b0/cfg/yolov3-tiny.cfg
|
||||||
|
wget https://pjreddie.com/media/files/yolov3-tiny.weights
|
||||||
|
wget https://raw.githubusercontent.com/pjreddie/darknet/c6afc7ff1499fbbe64069e1843d7929bd7ae2eaa/data/coco.names
|
||||||
|
```
|
||||||
|
|
||||||
|
Save the following as `detector_node_params.yaml`
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
/**:
|
||||||
|
ros__parameters:
|
||||||
|
network:
|
||||||
|
config: "./yolov3-tiny.cfg"
|
||||||
|
weights: "./yolov3-tiny.weights"
|
||||||
|
class_names: "./coco.names"
|
||||||
|
detection:
|
||||||
|
threshold: 0.25
|
||||||
|
nms_threshold: 0.45
|
||||||
|
```
|
||||||
|
|
||||||
|
Then run the node.
|
||||||
|
|
||||||
|
```
|
||||||
|
ros2 run openrobotics_darknet_ros detector_node __params:=detector_node_params.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
The node is now running.
|
||||||
|
Publish images on `~/images` to get the node to detect objects.
|
||||||
BIN
darknet_ros2/doc/example_darknet_yolov3-tiny.png
Normal file
BIN
darknet_ros2/doc/example_darknet_yolov3-tiny.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 757 KiB |
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#ifndef OPENROBOTICS_DARKNET_ROS__DETECTOR_NETWORK_HPP_
|
||||||
|
#define OPENROBOTICS_DARKNET_ROS__DETECTOR_NETWORK_HPP_
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "openrobotics_darknet_ros/visibility.hpp"
|
||||||
|
#include "sensor_msgs/msg/image.hpp"
|
||||||
|
#include "vision_msgs/msg/detection2_d_array.hpp"
|
||||||
|
|
||||||
|
namespace openrobotics
|
||||||
|
{
|
||||||
|
namespace darknet_ros
|
||||||
|
{
|
||||||
|
// Forward declaration
|
||||||
|
class DetectorNetworkPrivate;
|
||||||
|
|
||||||
|
class DetectorNetwork
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/// \brief load a network from disk
|
||||||
|
/// \param[in] config_file Path to a file describing the network
|
||||||
|
/// \param[in] weights_file Path to a file containing the network's weights
|
||||||
|
/// \param[in] classes Ordered list of class names the network can predict
|
||||||
|
DARKNET_ROS_PUBLIC
|
||||||
|
DetectorNetwork(
|
||||||
|
const std::string & config_file,
|
||||||
|
const std::string & weights_file,
|
||||||
|
const std::vector<std::string> & classes);
|
||||||
|
|
||||||
|
DARKNET_ROS_PUBLIC
|
||||||
|
~DetectorNetwork();
|
||||||
|
|
||||||
|
/// \brief Detect objects in image
|
||||||
|
/// \param[in] image An image to analyze
|
||||||
|
/// \param[in] threshold How confident the network must be to detect something [0.0, 1.0]
|
||||||
|
/// \param[in] nms_threshold Non-Maximal Suppression threhsold [0.0, 1.0].
|
||||||
|
/// When the intersection over union (iou) of two bounding boxes is greater than nms_threshold,
|
||||||
|
/// the box with the lower objectness score is discarded.
|
||||||
|
/// \param[out] output_detections Things detected in the image (does not set source_img)
|
||||||
|
/// \return number of objects detected
|
||||||
|
DARKNET_ROS_PUBLIC
|
||||||
|
size_t
|
||||||
|
detect(
|
||||||
|
const sensor_msgs::msg::Image & image,
|
||||||
|
double threshold,
|
||||||
|
double nms_threshold,
|
||||||
|
vision_msgs::msg::Detection2DArray * output_detections);
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<DetectorNetworkPrivate> impl_;
|
||||||
|
};
|
||||||
|
} // namespace darknet_ros
|
||||||
|
} // namespace openrobotics
|
||||||
|
|
||||||
|
#endif // OPENROBOTICS_DARKNET_ROS__DETECTOR_NETWORK_HPP_
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#ifndef OPENROBOTICS_DARKNET_ROS__DETECTOR_NODE_HPP_
|
||||||
|
#define OPENROBOTICS_DARKNET_ROS__DETECTOR_NODE_HPP_
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "rclcpp/node.hpp"
|
||||||
|
#include "openrobotics_darknet_ros/visibility_node.hpp"
|
||||||
|
|
||||||
|
|
||||||
|
namespace openrobotics
|
||||||
|
{
|
||||||
|
namespace darknet_ros
|
||||||
|
{
|
||||||
|
// Forward declaration
|
||||||
|
class DetectorNodePrivate;
|
||||||
|
|
||||||
|
class DetectorNode : public rclcpp::Node
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/// \brief Create a node that uses ROS parameters to get the network
|
||||||
|
DARKNET_ROS_NODE_PUBLIC
|
||||||
|
explicit DetectorNode(rclcpp::NodeOptions options);
|
||||||
|
|
||||||
|
DARKNET_ROS_NODE_PUBLIC
|
||||||
|
virtual ~DetectorNode();
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unique_ptr<DetectorNodePrivate> impl_;
|
||||||
|
};
|
||||||
|
} // namespace darknet_ros
|
||||||
|
} // namespace openrobotics
|
||||||
|
|
||||||
|
#endif // OPENROBOTICS_DARKNET_ROS__DETECTOR_NODE_HPP_
|
||||||
36
darknet_ros2/include/openrobotics_darknet_ros/parse.hpp
Normal file
36
darknet_ros2/include/openrobotics_darknet_ros/parse.hpp
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#ifndef OPENROBOTICS_DARKNET_ROS__PARSE_HPP_
|
||||||
|
#define OPENROBOTICS_DARKNET_ROS__PARSE_HPP_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "openrobotics_darknet_ros/visibility.hpp"
|
||||||
|
|
||||||
|
namespace openrobotics
|
||||||
|
{
|
||||||
|
namespace darknet_ros
|
||||||
|
{
|
||||||
|
/// \brief Read file containing class names, one per line
|
||||||
|
/// \param[in] filename a path to a file containing classes a network can detect
|
||||||
|
/// \return a container with all of the class names detected
|
||||||
|
DARKNET_ROS_PUBLIC
|
||||||
|
std::vector<std::string>
|
||||||
|
parse_class_names(const std::string & filename);
|
||||||
|
} // namespace darknet_ros
|
||||||
|
} // namespace openrobotics
|
||||||
|
|
||||||
|
#endif // OPENROBOTICS_DARKNET_ROS__PARSE_HPP_
|
||||||
56
darknet_ros2/include/openrobotics_darknet_ros/visibility.hpp
Normal file
56
darknet_ros2/include/openrobotics_darknet_ros/visibility.hpp
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/* This header must be included by all rclcpp headers which declare symbols
|
||||||
|
* which are defined in the rclcpp library. When not building the rclcpp
|
||||||
|
* library, i.e. when using the headers in other package's code, the contents
|
||||||
|
* of this header change the visibility of certain symbols which the rclcpp
|
||||||
|
* library cannot have, but the consuming code must have inorder to link.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef OPENROBOTICS_DARKNET_ROS__VISIBILITY_HPP_
|
||||||
|
#define OPENROBOTICS_DARKNET_ROS__VISIBILITY_HPP_
|
||||||
|
|
||||||
|
// This logic was borrowed (then namespaced) from the examples on the gcc wiki:
|
||||||
|
// https://gcc.gnu.org/wiki/Visibility
|
||||||
|
|
||||||
|
#if defined _WIN32 || defined __CYGWIN__
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#define DARKNET_ROS_EXPORT __attribute__ ((dllexport))
|
||||||
|
#define DARKNET_ROS_IMPORT __attribute__ ((dllimport))
|
||||||
|
#else
|
||||||
|
#define DARKNET_ROS_EXPORT __declspec(dllexport)
|
||||||
|
#define DARKNET_ROS_IMPORT __declspec(dllimport)
|
||||||
|
#endif
|
||||||
|
#ifdef DARKNET_ROS_BUILDING_LIBRARY
|
||||||
|
#define DARKNET_ROS_PUBLIC DARKNET_ROS_EXPORT
|
||||||
|
#else
|
||||||
|
#define DARKNET_ROS_PUBLIC DARKNET_ROS_IMPORT
|
||||||
|
#endif
|
||||||
|
#define DARKNET_ROS_PUBLIC_TYPE DARKNET_ROS_PUBLIC
|
||||||
|
#define DARKNET_ROS_LOCAL
|
||||||
|
#else
|
||||||
|
#define DARKNET_ROS_EXPORT __attribute__ ((visibility("default")))
|
||||||
|
#define DARKNET_ROS_IMPORT
|
||||||
|
#if __GNUC__ >= 4
|
||||||
|
#define DARKNET_ROS_PUBLIC __attribute__ ((visibility("default")))
|
||||||
|
#define DARKNET_ROS_LOCAL __attribute__ ((visibility("hidden")))
|
||||||
|
#else
|
||||||
|
#define DARKNET_ROS_PUBLIC
|
||||||
|
#define DARKNET_ROS_LOCAL
|
||||||
|
#endif
|
||||||
|
#define DARKNET_ROS_PUBLIC_TYPE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // OPENROBOTICS_DARKNET_ROS__VISIBILITY_HPP_
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
/* This header must be included by all rclcpp headers which declare symbols
|
||||||
|
* which are defined in the rclcpp library. When not building the rclcpp
|
||||||
|
* library, i.e. when using the headers in other package's code, the contents
|
||||||
|
* of this header change the visibility of certain symbols which the rclcpp
|
||||||
|
* library cannot have, but the consuming code must have inorder to link.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef OPENROBOTICS_DARKNET_ROS__VISIBILITY_NODE_HPP_
|
||||||
|
#define OPENROBOTICS_DARKNET_ROS__VISIBILITY_NODE_HPP_
|
||||||
|
|
||||||
|
// This logic was borrowed (then namespaced) from the examples on the gcc wiki:
|
||||||
|
// https://gcc.gnu.org/wiki/Visibility
|
||||||
|
|
||||||
|
#if defined _WIN32 || defined __CYGWIN__
|
||||||
|
#ifdef __GNUC__
|
||||||
|
#define DARKNET_ROS_NODE_EXPORT __attribute__ ((dllexport))
|
||||||
|
#define DARKNET_ROS_NODE_IMPORT __attribute__ ((dllimport))
|
||||||
|
#else
|
||||||
|
#define DARKNET_ROS_NODE_EXPORT __declspec(dllexport)
|
||||||
|
#define DARKNET_ROS_NODE_IMPORT __declspec(dllimport)
|
||||||
|
#endif
|
||||||
|
#ifdef DARKNET_ROS_NODE_BUILDING_LIBRARY
|
||||||
|
#define DARKNET_ROS_NODE_PUBLIC DARKNET_ROS_NODE_EXPORT
|
||||||
|
#else
|
||||||
|
#define DARKNET_ROS_NODE_PUBLIC DARKNET_ROS_NODE_IMPORT
|
||||||
|
#endif
|
||||||
|
#define DARKNET_ROS_NODE_PUBLIC_TYPE DARKNET_ROS_NODE_PUBLIC
|
||||||
|
#define DARKNET_ROS_NODE_LOCAL
|
||||||
|
#else
|
||||||
|
#define DARKNET_ROS_NODE_EXPORT __attribute__ ((visibility("default")))
|
||||||
|
#define DARKNET_ROS_NODE_IMPORT
|
||||||
|
#if __GNUC__ >= 4
|
||||||
|
#define DARKNET_ROS_NODE_PUBLIC __attribute__ ((visibility("default")))
|
||||||
|
#define DARKNET_ROS_NODE_LOCAL __attribute__ ((visibility("hidden")))
|
||||||
|
#else
|
||||||
|
#define DARKNET_ROS_NODE_PUBLIC
|
||||||
|
#define DARKNET_ROS_NODE_LOCAL
|
||||||
|
#endif
|
||||||
|
#define DARKNET_ROS_NODE_PUBLIC_TYPE
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif // OPENROBOTICS_DARKNET_ROS__VISIBILITY_NODE_HPP_
|
||||||
29
darknet_ros2/package.xml
Normal file
29
darknet_ros2/package.xml
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<package format="2">
|
||||||
|
<name>openrobotics_darknet_ros</name>
|
||||||
|
<version>0.1.0</version>
|
||||||
|
<description>ROS wrapper around darknet, an open source neural network framework.</description>
|
||||||
|
|
||||||
|
<author email="sloretz@openrobotics.org">Shane Loretz</author>
|
||||||
|
<maintainer email="sloretz@openrobotics.org">Shane Loretz</maintainer>
|
||||||
|
|
||||||
|
<license>Apache License 2.0</license>
|
||||||
|
|
||||||
|
<buildtool_depend>ament_cmake</buildtool_depend>
|
||||||
|
|
||||||
|
<depend>cv_bridge</depend>
|
||||||
|
<depend>darknet_vendor</depend>
|
||||||
|
<depend>rclcpp</depend>
|
||||||
|
<depend>rclcpp_components</depend>
|
||||||
|
<depend>sensor_msgs</depend>
|
||||||
|
<depend>vision_msgs</depend>
|
||||||
|
|
||||||
|
<test_depend>ament_cmake_gtest</test_depend>
|
||||||
|
<test_depend>ament_lint_auto</test_depend>
|
||||||
|
<test_depend>ament_lint_common</test_depend>
|
||||||
|
|
||||||
|
<export>
|
||||||
|
<build_type>ament_cmake</build_type>
|
||||||
|
</export>
|
||||||
|
</package>
|
||||||
|
|
||||||
44
darknet_ros2/src/darknet_detections.hpp
Normal file
44
darknet_ros2/src/darknet_detections.hpp
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
//
|
||||||
|
#ifndef DARKNET_DETECTIONS_HPP_
|
||||||
|
#define DARKNET_DETECTIONS_HPP_
|
||||||
|
|
||||||
|
#include <darknet_vendor/darknet_vendor.h>
|
||||||
|
|
||||||
|
namespace openrobotics
|
||||||
|
{
|
||||||
|
namespace darknet_ros
|
||||||
|
{
|
||||||
|
/// \brief RAII wrapper around darknet type `detections`
|
||||||
|
class DarknetDetections
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/// \brief Steal ownership of detections
|
||||||
|
DarknetDetections(detection * darknet_detections, size_t num_detections)
|
||||||
|
: detections_(darknet_detections), num_detections_(num_detections)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
~DarknetDetections()
|
||||||
|
{
|
||||||
|
free_detections(detections_, num_detections_);
|
||||||
|
}
|
||||||
|
|
||||||
|
detection * detections_;
|
||||||
|
const size_t num_detections_ = 0;
|
||||||
|
};
|
||||||
|
} // namespace darknet_ros
|
||||||
|
} // namespace openrobotics
|
||||||
|
#endif // DARKNET_DETECTIONS_HPP_
|
||||||
71
darknet_ros2/src/darknet_image.hpp
Normal file
71
darknet_ros2/src/darknet_image.hpp
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#ifndef DARKNET_IMAGE_HPP_
|
||||||
|
#define DARKNET_IMAGE_HPP_
|
||||||
|
|
||||||
|
#include <darknet_vendor/darknet_vendor.h>
|
||||||
|
#include <cv_bridge/cv_bridge.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace openrobotics
|
||||||
|
{
|
||||||
|
namespace darknet_ros
|
||||||
|
{
|
||||||
|
/// \brief RAII wrapper around darknet type `image`
|
||||||
|
class DarknetImage
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/// \brief Steal ownership of image
|
||||||
|
explicit DarknetImage(image darknet_image)
|
||||||
|
: image_(darknet_image)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
explicit DarknetImage(const sensor_msgs::msg::Image & image_msg)
|
||||||
|
{
|
||||||
|
// Convert to open cv type with a known format
|
||||||
|
std::shared_ptr<void const> dummy_object;
|
||||||
|
cv_bridge::CvImageConstPtr opencv_image = cv_bridge::toCvShare(image_msg, dummy_object, "rgb8");
|
||||||
|
const cv::Mat & image_matrix = opencv_image->image;
|
||||||
|
|
||||||
|
// Make a darknet image with this data
|
||||||
|
const int width = image_matrix.cols;
|
||||||
|
const int height = image_matrix.rows;
|
||||||
|
const int channels = 3; // rgb
|
||||||
|
image_ = make_image(width, height, channels);
|
||||||
|
|
||||||
|
for (int channel = 0; channel < channels; ++channel) {
|
||||||
|
for (int row = 0; row < image_matrix.rows; ++row) {
|
||||||
|
for (int column = 0; column < image_matrix.cols; ++column) {
|
||||||
|
// Darknet stores each channel separately in R G B order
|
||||||
|
// Within a channel pixels are in row-major order
|
||||||
|
size_t darknet_idx = channel * height * width + row * width + column;
|
||||||
|
image_.data[darknet_idx] = image_matrix.ptr(row, column)[channel] / 255.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~DarknetImage()
|
||||||
|
{
|
||||||
|
free_image(image_);
|
||||||
|
}
|
||||||
|
|
||||||
|
image image_;
|
||||||
|
};
|
||||||
|
} // namespace darknet_ros
|
||||||
|
} // namespace openrobotics
|
||||||
|
#endif // DARKNET_IMAGE_HPP_
|
||||||
176
darknet_ros2/src/detector_network.cpp
Normal file
176
darknet_ros2/src/detector_network.cpp
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#include <darknet_vendor/darknet_vendor.h>
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <memory>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "darknet_detections.hpp"
|
||||||
|
#include "darknet_image.hpp"
|
||||||
|
#include "openrobotics_darknet_ros/detector_network.hpp"
|
||||||
|
|
||||||
|
namespace openrobotics
|
||||||
|
{
|
||||||
|
namespace darknet_ros
|
||||||
|
{
|
||||||
|
class DetectorNetworkPrivate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DetectorNetworkPrivate() {}
|
||||||
|
|
||||||
|
~DetectorNetworkPrivate()
|
||||||
|
{
|
||||||
|
if (network_) {
|
||||||
|
free_network(network_);
|
||||||
|
network_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// darknet network
|
||||||
|
network * network_ = nullptr;
|
||||||
|
// Classes the network can detect
|
||||||
|
std::vector<std::string> class_names_;
|
||||||
|
};
|
||||||
|
|
||||||
|
DetectorNetwork::DetectorNetwork(
|
||||||
|
const std::string & config_file,
|
||||||
|
const std::string & weights_file,
|
||||||
|
const std::vector<std::string> & classes)
|
||||||
|
: impl_(new DetectorNetworkPrivate())
|
||||||
|
{
|
||||||
|
if (!std::ifstream(config_file)) {
|
||||||
|
std::stringstream str;
|
||||||
|
str << "Could not open " << config_file;
|
||||||
|
throw std::invalid_argument(str.str());
|
||||||
|
} else if (!std::ifstream(weights_file)) {
|
||||||
|
std::stringstream str;
|
||||||
|
str << "Could not open " << weights_file;
|
||||||
|
throw std::invalid_argument(str.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_->class_names_ = classes;
|
||||||
|
|
||||||
|
// Make copies because of darknet's lack of const
|
||||||
|
std::unique_ptr<char> config_mutable(new char[config_file.size() + 1]);
|
||||||
|
std::unique_ptr<char> weights_mutable(new char[weights_file.size() + 1]);
|
||||||
|
snprintf(&*config_mutable, config_file.size() + 1, "%s", config_file.c_str());
|
||||||
|
snprintf(&*weights_mutable, weights_file.size() + 1, "%s", weights_file.c_str());
|
||||||
|
|
||||||
|
const int clear = 0;
|
||||||
|
impl_->network_ = load_network(&*config_mutable, &*weights_mutable, clear);
|
||||||
|
if (nullptr == impl_->network_) {
|
||||||
|
std::stringstream str;
|
||||||
|
str << "Failed to load network from " << config_file << " and " << weights_file;
|
||||||
|
throw std::invalid_argument(str.str());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(sloretz) what is this and why do examples set it?
|
||||||
|
const int batch = 1;
|
||||||
|
set_batch_network(impl_->network_, batch);
|
||||||
|
|
||||||
|
const int num_classes_int = impl_->network_->layers[impl_->network_->n - 1].classes;
|
||||||
|
if (num_classes_int <= 0) {
|
||||||
|
throw std::invalid_argument("Invalid network, it expects no classes");
|
||||||
|
}
|
||||||
|
size_t num_classes = static_cast<size_t>(num_classes_int);
|
||||||
|
if (num_classes != classes.size()) {
|
||||||
|
std::stringstream str;
|
||||||
|
str << "DetectorNetwork expects " << num_classes << " class names but got " << classes.size();
|
||||||
|
throw std::invalid_argument(str.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectorNetwork::~DetectorNetwork()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t
|
||||||
|
DetectorNetwork::detect(
|
||||||
|
const sensor_msgs::msg::Image & image_msg,
|
||||||
|
double threshold,
|
||||||
|
double nms_threshold,
|
||||||
|
vision_msgs::msg::Detection2DArray * output_detections)
|
||||||
|
{
|
||||||
|
DarknetImage orig_image(image_msg);
|
||||||
|
|
||||||
|
// resize image to network size, filling rest with gray
|
||||||
|
DarknetImage resized_image(
|
||||||
|
letterbox_image(orig_image.image_, impl_->network_->w, impl_->network_->h));
|
||||||
|
|
||||||
|
// Ask network to make predictions
|
||||||
|
network_predict(impl_->network_, resized_image.image_.data);
|
||||||
|
|
||||||
|
// Get predictions from network
|
||||||
|
int num_detections = 0;
|
||||||
|
// TODO(sloretz) what do hier, map, and relative do?
|
||||||
|
const float hier = 0;
|
||||||
|
int * map = nullptr;
|
||||||
|
const int relative = 0;
|
||||||
|
detection * darknet_detections = get_network_boxes(
|
||||||
|
impl_->network_, image_msg.width, image_msg.height, threshold,
|
||||||
|
hier, map, relative,
|
||||||
|
&num_detections);
|
||||||
|
|
||||||
|
if (num_detections <= 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
DarknetDetections raii_detections(darknet_detections, static_cast<size_t>(num_detections));
|
||||||
|
|
||||||
|
// Non-maximal suppression: filters overlapping bounding boxes
|
||||||
|
if (nms_threshold > 0.0f) {
|
||||||
|
const int num_classes = impl_->network_->layers[impl_->network_->n - 1].classes;
|
||||||
|
do_nms_sort(darknet_detections, num_detections, num_classes, nms_threshold);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate output message
|
||||||
|
output_detections->header = image_msg.header;
|
||||||
|
output_detections->detections.reserve(num_detections);
|
||||||
|
for (int i = 0; i < num_detections; ++i) {
|
||||||
|
auto & detection = darknet_detections[i];
|
||||||
|
output_detections->detections.emplace_back();
|
||||||
|
auto & detection_ros = output_detections->detections.back();
|
||||||
|
|
||||||
|
// Copy probabilities of each class
|
||||||
|
for (int cls = 0; cls < detection.classes; ++cls) {
|
||||||
|
if (detection.prob[cls] > 0.0f) {
|
||||||
|
detection_ros.results.emplace_back();
|
||||||
|
auto & hypothesis = detection_ros.results.back();
|
||||||
|
hypothesis.id = impl_->class_names_.at(cls);
|
||||||
|
hypothesis.score = detection.prob[cls];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (detection_ros.results.empty()) {
|
||||||
|
// nms suppressed this detection
|
||||||
|
output_detections->detections.pop_back();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy bounding box, darknet uses center of bounding box too
|
||||||
|
detection_ros.bbox.center.x = detection.bbox.x;
|
||||||
|
detection_ros.bbox.center.y = detection.bbox.y;
|
||||||
|
detection_ros.bbox.size_x = detection.bbox.w;
|
||||||
|
detection_ros.bbox.size_y = detection.bbox.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not using num_detections because nms may have suppressed some
|
||||||
|
return output_detections->detections.size();
|
||||||
|
}
|
||||||
|
} // namespace darknet_ros
|
||||||
|
} // namespace openrobotics
|
||||||
165
darknet_ros2/src/detector_node.cpp
Normal file
165
darknet_ros2/src/detector_node.cpp
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#include <darknet_vendor/darknet_vendor.h>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "openrobotics_darknet_ros/detector_node.hpp"
|
||||||
|
#include "openrobotics_darknet_ros/detector_network.hpp"
|
||||||
|
#include "openrobotics_darknet_ros/parse.hpp"
|
||||||
|
#include "rcl_interfaces/msg/parameter_descriptor.hpp"
|
||||||
|
#include "rclcpp/parameter_value.hpp"
|
||||||
|
|
||||||
|
namespace openrobotics
|
||||||
|
{
|
||||||
|
namespace darknet_ros
|
||||||
|
{
|
||||||
|
class DetectorNodePrivate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void on_image_rx(const sensor_msgs::msg::Image::ConstSharedPtr image_msg)
|
||||||
|
{
|
||||||
|
vision_msgs::msg::Detection2DArray::UniquePtr detections(
|
||||||
|
new vision_msgs::msg::Detection2DArray);
|
||||||
|
// std::cerr << "using threshold " << threshold_ << " nms " << nms_threshold_ << "\n";
|
||||||
|
if (network_->detect(*image_msg, threshold_, nms_threshold_, &*detections)) {
|
||||||
|
detections_pub_->publish(std::move(detections));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rcl_interfaces::msg::SetParametersResult
|
||||||
|
on_parameters_change(const std::vector<rclcpp::Parameter> & new_values)
|
||||||
|
{
|
||||||
|
rcl_interfaces::msg::SetParametersResult result;
|
||||||
|
result.successful = true;
|
||||||
|
double new_threshold = threshold_;
|
||||||
|
double new_nms_threshold = nms_threshold_;
|
||||||
|
|
||||||
|
for (const auto & parameter : new_values) {
|
||||||
|
if (threshold_desc_.name == parameter.get_name()) {
|
||||||
|
new_threshold = parameter.as_double();
|
||||||
|
// TODO(sloretz) range constraints in parameter description
|
||||||
|
if (new_threshold < 0.0 || new_threshold > 1.0) {
|
||||||
|
result.successful = false;
|
||||||
|
result.reason = "threshold out of range [0.0, 1.0]";
|
||||||
|
}
|
||||||
|
} else if (nms_threshold_desc_.name == parameter.get_name()) {
|
||||||
|
new_nms_threshold = parameter.as_double();
|
||||||
|
if (new_nms_threshold < 0.0 || new_nms_threshold > 1.0) {
|
||||||
|
result.successful = false;
|
||||||
|
result.reason = "nms_threshold out of range [0.0, 1.0]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.successful) {
|
||||||
|
threshold_ = new_threshold;
|
||||||
|
nms_threshold_ = new_nms_threshold;
|
||||||
|
// std::cerr << "New threshold " << threshold_ << " nms " << nms_threshold_ << "\n";
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<DetectorNetwork> network_;
|
||||||
|
rclcpp::Publisher<vision_msgs::msg::Detection2DArray>::SharedPtr detections_pub_;
|
||||||
|
rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr image_sub_;
|
||||||
|
|
||||||
|
double threshold_ = 0.25;
|
||||||
|
double nms_threshold_ = 0.45;
|
||||||
|
|
||||||
|
rcl_interfaces::msg::ParameterDescriptor threshold_desc_;
|
||||||
|
rcl_interfaces::msg::ParameterDescriptor nms_threshold_desc_;
|
||||||
|
};
|
||||||
|
|
||||||
|
DetectorNode::DetectorNode(rclcpp::NodeOptions options)
|
||||||
|
: rclcpp::Node("detector_node", options), impl_(new DetectorNodePrivate)
|
||||||
|
{
|
||||||
|
// Read-only input parameters: cfg, weights, classes
|
||||||
|
rcl_interfaces::msg::ParameterDescriptor network_cfg_desc;
|
||||||
|
network_cfg_desc.description = "Path to config file describing network";
|
||||||
|
network_cfg_desc.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING;
|
||||||
|
network_cfg_desc.read_only = true;
|
||||||
|
network_cfg_desc.name = "network.config";
|
||||||
|
const std::string network_config_path = declare_parameter(
|
||||||
|
network_cfg_desc.name,
|
||||||
|
rclcpp::ParameterValue(),
|
||||||
|
network_cfg_desc).get<std::string>();
|
||||||
|
|
||||||
|
rcl_interfaces::msg::ParameterDescriptor network_weights_desc;
|
||||||
|
network_weights_desc.description = "Path to file describing network weights";
|
||||||
|
network_weights_desc.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING;
|
||||||
|
network_weights_desc.read_only = true;
|
||||||
|
network_weights_desc.name = "network.weights";
|
||||||
|
const std::string network_weights_path = declare_parameter(
|
||||||
|
network_weights_desc.name,
|
||||||
|
rclcpp::ParameterValue(),
|
||||||
|
network_weights_desc).get<std::string>();
|
||||||
|
|
||||||
|
rcl_interfaces::msg::ParameterDescriptor network_class_names_desc;
|
||||||
|
network_class_names_desc.description = "Path to file with class names (one per line)";
|
||||||
|
network_class_names_desc.type = rcl_interfaces::msg::ParameterType::PARAMETER_STRING;
|
||||||
|
network_class_names_desc.read_only = true;
|
||||||
|
network_class_names_desc.name = "network.class_names";
|
||||||
|
const std::string network_class_names_path = declare_parameter(
|
||||||
|
network_class_names_desc.name,
|
||||||
|
rclcpp::ParameterValue(),
|
||||||
|
network_class_names_desc).get<std::string>();
|
||||||
|
|
||||||
|
impl_->threshold_desc_.description = "Minimum detection confidence [0.0, 1.0]";
|
||||||
|
impl_->threshold_desc_.type = rcl_interfaces::msg::ParameterType::PARAMETER_DOUBLE;
|
||||||
|
impl_->threshold_desc_.name = "detection.threshold";
|
||||||
|
impl_->threshold_ = declare_parameter(
|
||||||
|
impl_->threshold_desc_.name,
|
||||||
|
rclcpp::ParameterValue(impl_->threshold_),
|
||||||
|
impl_->threshold_desc_).get<double>();
|
||||||
|
|
||||||
|
impl_->nms_threshold_desc_.description =
|
||||||
|
"Non Maximal Suppression threshold for filtering overlapping boxes [0.0, 1.0]";
|
||||||
|
impl_->nms_threshold_desc_.type = rcl_interfaces::msg::ParameterType::PARAMETER_DOUBLE;
|
||||||
|
impl_->nms_threshold_desc_.name = "detection.nms_threshold";
|
||||||
|
impl_->nms_threshold_ = declare_parameter(
|
||||||
|
impl_->nms_threshold_desc_.name,
|
||||||
|
rclcpp::ParameterValue(impl_->nms_threshold_),
|
||||||
|
impl_->nms_threshold_desc_).get<double>();
|
||||||
|
|
||||||
|
set_on_parameters_set_callback(
|
||||||
|
std::bind(&DetectorNodePrivate::on_parameters_change, &*impl_, std::placeholders::_1));
|
||||||
|
|
||||||
|
// TODO(sloretz) raise if user tried to initialize node with undeclared parameters
|
||||||
|
|
||||||
|
std::vector<std::string> class_names = parse_class_names(network_class_names_path);
|
||||||
|
impl_->network_.reset(
|
||||||
|
new DetectorNetwork(network_config_path, network_weights_path, class_names));
|
||||||
|
|
||||||
|
// Ouput topic ~/detections [vision_msgs/msg/Detection2DArray]
|
||||||
|
impl_->detections_pub_ = this->create_publisher<vision_msgs::msg::Detection2DArray>(
|
||||||
|
"~/detections", 1);
|
||||||
|
|
||||||
|
// Input topic ~/images [sensor_msgs/msg/Image]
|
||||||
|
impl_->image_sub_ = this->create_subscription<sensor_msgs::msg::Image>(
|
||||||
|
"~/images", 12, std::bind(&DetectorNodePrivate::on_image_rx, &*impl_, std::placeholders::_1));
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectorNode::~DetectorNode()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
} // namespace darknet_ros
|
||||||
|
} // namespace openrobotics
|
||||||
|
|
||||||
|
#include "rclcpp_components/register_node_macro.hpp"
|
||||||
|
|
||||||
|
RCLCPP_COMPONENTS_REGISTER_NODE(openrobotics::darknet_ros::DetectorNode)
|
||||||
31
darknet_ros2/src/detector_node_main.cpp
Normal file
31
darknet_ros2/src/detector_node_main.cpp
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
#include "openrobotics_darknet_ros/detector_node.hpp"
|
||||||
|
#include "rclcpp/rclcpp.hpp"
|
||||||
|
|
||||||
|
int main(int argc, char ** argv)
|
||||||
|
{
|
||||||
|
rclcpp::init(argc, argv);
|
||||||
|
|
||||||
|
rclcpp::NodeOptions options;
|
||||||
|
auto detector_node = std::make_shared<openrobotics::darknet_ros::DetectorNode>(options);
|
||||||
|
|
||||||
|
rclcpp::spin(detector_node);
|
||||||
|
|
||||||
|
rclcpp::shutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
51
darknet_ros2/src/parse.cpp
Normal file
51
darknet_ros2/src/parse.cpp
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "openrobotics_darknet_ros/parse.hpp"
|
||||||
|
|
||||||
|
namespace openrobotics
|
||||||
|
{
|
||||||
|
namespace darknet_ros
|
||||||
|
{
|
||||||
|
std::vector<std::string>
|
||||||
|
parse_class_names(const std::string & filename)
|
||||||
|
{
|
||||||
|
std::ifstream fin(filename);
|
||||||
|
std::vector<std::string> class_names;
|
||||||
|
std::string line;
|
||||||
|
while (fin) {
|
||||||
|
std::getline(fin, line);
|
||||||
|
if (!fin) {
|
||||||
|
if (!fin.eof()) {
|
||||||
|
std::stringstream str;
|
||||||
|
str << "Failed to read [" << filename << "] line " << class_names.size();
|
||||||
|
throw std::runtime_error(str.str());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (line.empty()) {
|
||||||
|
// Ignore blank lines
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
class_names.emplace_back(line);
|
||||||
|
}
|
||||||
|
return class_names;
|
||||||
|
}
|
||||||
|
} // namespace darknet_ros
|
||||||
|
} // namespace openrobotics
|
||||||
0
darknet_ros2/test/data/0_class_names.txt
Normal file
0
darknet_ros2/test/data/0_class_names.txt
Normal file
1
darknet_ros2/test/data/1_class_name.txt
Normal file
1
darknet_ros2/test/data/1_class_name.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
tomato
|
||||||
10
darknet_ros2/test/data/3_class_names_with_whitespace.txt
Normal file
10
darknet_ros2/test/data/3_class_names_with_whitespace.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
foo
|
||||||
|
|
||||||
|
|
||||||
|
bar
|
||||||
|
|
||||||
|
|
||||||
|
baz
|
||||||
5
darknet_ros2/test/data/5_class_names.txt
Normal file
5
darknet_ros2/test/data/5_class_names.txt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
car
|
||||||
|
boat
|
||||||
|
bus
|
||||||
|
airplane
|
||||||
|
space ship
|
||||||
33
darknet_ros2/test/test_detector_network.cpp
Normal file
33
darknet_ros2/test/test_detector_network.cpp
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "openrobotics_darknet_ros/detector_network.hpp"
|
||||||
|
|
||||||
|
TEST(network, config_does_not_exist)
|
||||||
|
{
|
||||||
|
const std::string config = "does_not_exist.cfg";
|
||||||
|
const std::string weights = "does_not_exist.weights";
|
||||||
|
std::vector<std::string> classes{"foo", "bar"};
|
||||||
|
|
||||||
|
try {
|
||||||
|
openrobotics::darknet_ros::DetectorNetwork network(config, weights, classes);
|
||||||
|
ASSERT_TRUE(false);
|
||||||
|
} catch (const std::invalid_argument &) {
|
||||||
|
}
|
||||||
|
}
|
||||||
67
darknet_ros2/test/test_parser.cpp
Normal file
67
darknet_ros2/test/test_parser.cpp
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
// Copyright 2019 Open Source Robotics Foundation, Inc.
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "openrobotics_darknet_ros/parse.hpp"
|
||||||
|
|
||||||
|
using openrobotics::darknet_ros::parse_class_names;
|
||||||
|
|
||||||
|
TEST(parse, names_file_does_not_exist)
|
||||||
|
{
|
||||||
|
std::vector<std::string> class_names;
|
||||||
|
|
||||||
|
class_names = parse_class_names("openrobotics_darknet_ros_file_does_not_exist");
|
||||||
|
|
||||||
|
ASSERT_EQ(0u, class_names.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(parse, one_name)
|
||||||
|
{
|
||||||
|
std::vector<std::string> class_names;
|
||||||
|
|
||||||
|
class_names = parse_class_names("data/1_class_name.txt");
|
||||||
|
|
||||||
|
ASSERT_EQ(1u, class_names.size());
|
||||||
|
EXPECT_EQ("tomato", class_names[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(parse, 3_class_names_with_whitespace)
|
||||||
|
{
|
||||||
|
std::vector<std::string> class_names;
|
||||||
|
|
||||||
|
class_names = parse_class_names("data/3_class_names_with_whitespace.txt");
|
||||||
|
|
||||||
|
ASSERT_EQ(3u, class_names.size());
|
||||||
|
EXPECT_EQ("foo", class_names[0]);
|
||||||
|
EXPECT_EQ("bar", class_names[1]);
|
||||||
|
EXPECT_EQ("baz", class_names[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(parse, five_names)
|
||||||
|
{
|
||||||
|
std::vector<std::string> class_names;
|
||||||
|
|
||||||
|
class_names = parse_class_names("data/5_class_names.txt");
|
||||||
|
|
||||||
|
ASSERT_EQ(5u, class_names.size());
|
||||||
|
EXPECT_EQ("car", class_names[0]);
|
||||||
|
EXPECT_EQ("boat", class_names[1]);
|
||||||
|
EXPECT_EQ("bus", class_names[2]);
|
||||||
|
EXPECT_EQ("airplane", class_names[3]);
|
||||||
|
EXPECT_EQ("space ship", class_names[4]);
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user