diff --git a/.gitignore b/.gitignore
index db60fde..4d1490a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
*~
*.pyc
\#*
+__pycache__/
diff --git a/detectron2_detector/README.md b/detectron2_detector/README.md
new file mode 100644
index 0000000..e1bfe4f
--- /dev/null
+++ b/detectron2_detector/README.md
@@ -0,0 +1,16 @@
+# detectron2_detector
+
+This package implements a 3D object detector with [Mask RCNN](https://arxiv.org/pdf/1703.06870.pdf) ([detectron2](https://github.com/facebookresearch/detectron2) implementation) and basic pointcloud processing.
+We assume colored and ordered pointcloud aligned with pixels is available. We use Mask RCNN to produce masks for objects, then use these masks to extract points belong to the objects and estimate position and size.
+
+### Parameters
+| parameters | Meaning | Default |
+| ---------------- | ------------- | ------- |
+| detectron_config_file | config file to load detectron model,
check out [model zoo](https://github.com/facebookresearch/detectron2/blob/master/MODEL_ZOO.md) | COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml |
+| detectron_score_thresh | min score to be considered as an object | 0.8 |
+| pointcloud2_topic | ros topic for pointcloud data | /camera/depth/points |
+| categories | list of interested [categories](https://github.com/amikelive/coco-labels/blob/master/coco-labels-2014_2017.txt) in COCO dataset, left empty `[]` to detect all | [0] (person) |
+| pc_downsample_factor | factor to downsample the aligned pointcloud | 16 |
+| min_mask | min number of pixels in a mask | 20 |
+| nms_filter | IoU threshold for non-max suppression | 0.3 |
+| outlier_filter | threshold to filter outlier points,
model as multivariate normal distribution | 0.5 |
diff --git a/detectron2_detector/config/detectron2.yaml b/detectron2_detector/config/detectron2.yaml
new file mode 100644
index 0000000..59c8efc
--- /dev/null
+++ b/detectron2_detector/config/detectron2.yaml
@@ -0,0 +1,13 @@
+detectron2_node:
+ ros__parameters:
+ # detectron parameters
+ detectron_config_file: "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"
+ detectron_score_thresh: 0.8
+ # processing parameters
+ pointcloud2_topic: "/camera/depth/points"
+ categories: [0] # please check out COCO dataset category_id list if you want to config this; if you want to track all, leave it empty
+ pc_downsample_factor: 16
+ min_mask: 20 # minimum mask to be considered as an obstacle candidate
+ nms_filter: 0.3 # 3D non-max suppression threshold, [0, 1]
+ outlier_thresh: 0.5
+
\ No newline at end of file
diff --git a/detectron2_detector/detectron2_detector/__init__.py b/detectron2_detector/detectron2_detector/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/detectron2_detector/detectron2_detector/detectron2_node.py b/detectron2_detector/detectron2_detector/detectron2_node.py
new file mode 100644
index 0000000..8b43804
--- /dev/null
+++ b/detectron2_detector/detectron2_detector/detectron2_node.py
@@ -0,0 +1,196 @@
+import detectron2
+from detectron2.utils.logger import setup_logger
+setup_logger()
+
+# import from common libraries
+import numpy as np
+from scipy.stats import multivariate_normal
+
+# import some common detectron2 utilities
+from detectron2 import model_zoo
+from detectron2.engine import DefaultPredictor
+from detectron2.config import get_cfg
+from detectron2.utils.visualizer import Visualizer
+from detectron2.data import MetadataCatalog, DatasetCatalog
+
+import rclpy
+from rclpy.node import Node
+
+from sensor_msgs.msg import Image, PointCloud2
+from nav2_dynamic_msgs.msg import Obstacle, ObstacleArray
+from geometry_msgs.msg import Pose, Point
+from detectron2_detector.utils import NMS_3D
+
+class Detectron2Detector(Node):
+ '''use Detectron2 to detect object masks from 2D image and estimate 3D position with Pointcloud2 data
+ '''
+ def __init__(self):
+ super().__init__('detectron_node')
+ self.declare_parameters(
+ namespace='',
+ parameters=[
+ ('detectron_config_file', "COCO-InstanceSegmentation/mask_rcnn_R_50_FPN_3x.yaml"),
+ ('detectron_score_thresh', 0.8),
+ ('pointcloud2_topic', "/camera/depth/points"),
+ ('pc_downsample_factor', 16),
+ ('min_mask', 20),
+ ('categories', [0]),
+ ('nms_filter', 0.3),
+ ('outlier_thresh', 0.5)
+ ])
+ self.pc_downsample_factor = int(self.get_parameter("pc_downsample_factor")._value)
+ self.min_mask = self.get_parameter("min_mask")._value
+ self.categories = self.get_parameter("categories")._value
+ self.nms_filter = self.get_parameter("nms_filter")._value
+ self.outlier_thresh = self.get_parameter("outlier_thresh")._value
+
+ # setup detectron model
+ self.cfg = get_cfg()
+ config_file = self.get_parameter("detectron_config_file")._value
+ self.cfg.merge_from_file(model_zoo.get_config_file(config_file))
+ self.cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = self.get_parameter("detectron_score_thresh")._value
+ self.cfg.MODEL.WEIGHTS = model_zoo.get_checkpoint_url(config_file)
+ self.predictor = DefaultPredictor(self.cfg)
+
+ # subscribe to sensor
+ self.subscription = self.create_subscription(
+ PointCloud2,
+ self.get_parameter("pointcloud2_topic")._value,
+ self.callback,
+ 1)
+
+ # setup publisher
+ self.detect_obj_pub = self.create_publisher(ObstacleArray, 'detection', 2)
+ self.detect_img_pub = self.create_publisher(Image, 'image', 2)
+
+ self.count = -1
+
+ def outlier_filter(self, x, y, z, idx):
+ '''simple outlier filter, assume Gaussian distribution and drop points with low probability (too far away from center)'''
+ mean = [np.mean(x), np.mean(y), np.mean(z)]
+ cov = np.diag([np.var(x), np.var(y), np.var(z)])
+ rv = multivariate_normal(mean, cov)
+ points = np.dstack((x, y, z))
+ p = rv.pdf(points)
+ return idx[p > self.outlier_thresh]
+
+ def callback(self, msg):
+ # check if there is subscirbers
+ if self.detect_obj_pub.get_subscription_count() == 0 and self.detect_img_pub.get_subscription_count() == 0:
+ return
+
+ # extract data from msg
+ height = msg.height
+ width = msg.width
+ points = np.array(msg.data, dtype = 'uint8')
+
+ # decode rgb image
+ rgb_offset = msg.fields[3].offset
+ point_step = msg.point_step
+ r = points[rgb_offset::point_step]
+ g = points[(rgb_offset+1)::point_step]
+ b = points[(rgb_offset+2)::point_step]
+ img = np.concatenate([r[:, None], g[:, None], b[:, None]], axis = -1)
+ self.img = img.reshape((height, width, 3))
+
+ # decode point cloud data
+ if msg.fields[0].datatype < 3:
+ byte = 1
+ elif msg.fields[0].datatype < 5:
+ byte = 2
+ elif msg.fields[0].datatype < 8:
+ byte = 4
+ else:
+ byte = 8
+ points = points.view(' 0:
+ obstacle_array.obstacles = detections
+ self.detect_obj_pub.publish(obstacle_array)
+
+ # visualize detection with detectron API
+ if self.detect_img_pub.get_subscription_count() > 0:
+ v = Visualizer(self.img[:, :, ::-1], MetadataCatalog.get(self.cfg.DATASETS.TRAIN[0]), scale=1)
+ out = v.draw_instance_predictions(outputs["instances"].to("cpu"))
+ out_img = out.get_image()[:, :, ::-1]
+ out_img_msg = Image()
+ out_img_msg.header = self.header
+ out_img_msg.height = out_img.shape[0]
+ out_img_msg.width = out_img.shape[1]
+ out_img_msg.encoding = 'rgb8'
+ out_img_msg.step = 3 * out_img.shape[1]
+ out_img_msg.data = out_img.flatten().tolist()
+ self.detect_img_pub.publish(out_img_msg)
+
+def main():
+ rclpy.init(args = None)
+ node = Detectron2Detector()
+ node.get_logger().info("start spining detectron_node...")
+
+ rclpy.spin(node)
+
+ rclpy.shutdown()
+
+if __name__ == '__main__':
+ main()
diff --git a/detectron2_detector/detectron2_detector/utils.py b/detectron2_detector/detectron2_detector/utils.py
new file mode 100644
index 0000000..e52a922
--- /dev/null
+++ b/detectron2_detector/detectron2_detector/utils.py
@@ -0,0 +1,39 @@
+import numpy as np
+from nav2_dynamic_msgs.msg import Obstacle
+from functools import reduce
+
+def IoU_3D(obstacle1, obstacle2):
+ '''computer IoU of 3D bounding box given 2 Obstacle msgs '''
+ p1 = [obstacle1.position.x, obstacle1.position.y, obstacle1.position.z]
+ s1 = [obstacle1.size.x, obstacle1.size.y, obstacle1.size.z]
+ p2 = [obstacle2.position.x, obstacle2.position.y, obstacle2.position.z]
+ s2 = [obstacle2.size.x, obstacle2.size.y, obstacle2.size.z]
+
+ max1 = [p1[i] + s1[i] / 2 for i in range(3)]
+ min1 = [p1[i] - s1[i] / 2 for i in range(3)]
+ max2 = [p2[i] + s2[i] / 2 for i in range(3)]
+ min2 = [p2[i] - s2[i] / 2 for i in range(3)]
+
+ overlap = [max(0, min(max1[i], max2[i]) - max(min1[i], min2[i])) for i in range(3)]
+ vol = lambda x,y:x*y
+ intersection = reduce(vol, overlap)
+ union = reduce(vol, s1) + reduce(vol, s2) - intersection
+ return intersection / union
+
+def NMS_3D(obstacles, threshold):
+ '''use non-max suppression to filter a list of obstacles '''
+ if len(obstacles) < 2:
+ return obstacles
+
+ obstacles.sort(reverse = True, key = lambda x: x.score)
+ bboxes = [obstacles[0]]
+ for i in range(1, len(obstacles)):
+ bbox = obstacles[i]
+ flag = True
+ for j in range(len(bboxes)):
+ if IoU_3D(bboxes[j], bbox) > threshold:
+ flag = False
+ break
+ if flag:
+ bboxes.append(bbox)
+ return bboxes
diff --git a/detectron2_detector/launch/detectron.launch.py b/detectron2_detector/launch/detectron.launch.py
new file mode 100644
index 0000000..faed0d0
--- /dev/null
+++ b/detectron2_detector/launch/detectron.launch.py
@@ -0,0 +1,21 @@
+import os
+import launch
+from ament_index_python.packages import get_package_share_directory
+from launch import LaunchDescription
+from launch_ros.actions import Node
+
+def generate_launch_description():
+ config = os.path.join(
+ get_package_share_directory('detectron2_detector'),
+ 'config',
+ 'detectron2.yaml'
+ )
+
+ detectron_node = Node(
+ package = 'detectron2_detector',
+ name = 'detectron2_node',
+ executable = 'detectron2_node',
+ parameters = [config]
+ )
+
+ return launch.LaunchDescription([detectron_node])
\ No newline at end of file
diff --git a/detectron2_detector/package.xml b/detectron2_detector/package.xml
new file mode 100644
index 0000000..0287a20
--- /dev/null
+++ b/detectron2_detector/package.xml
@@ -0,0 +1,28 @@
+
+
+
+ detectron2_detector
+ 0.0.1
+ This detector uses detectron2 to get object mask and then uses pointcloud2 data to estimate 3D position.
+ Steven Macenski
+ Shengjian Chen
+ Apache-2.0
+
+ ament_copyright
+ ament_flake8
+ ament_pep257
+ python3-pytest
+
+ rclpy
+ launch_ros
+ numpy
+ scipy
+ detectron2
+ sensor_msgs
+ nav2_dynamic_msgs
+ geometry_msgs
+
+
+ ament_python
+
+
diff --git a/detectron2_detector/resource/detectron2_detector b/detectron2_detector/resource/detectron2_detector
new file mode 100644
index 0000000..e69de29
diff --git a/detectron2_detector/setup.cfg b/detectron2_detector/setup.cfg
new file mode 100644
index 0000000..ae88a03
--- /dev/null
+++ b/detectron2_detector/setup.cfg
@@ -0,0 +1,4 @@
+[develop]
+script-dir=$base/lib/detectron2_detector
+[install]
+install-scripts=$base/lib/detectron2_detector
diff --git a/detectron2_detector/setup.py b/detectron2_detector/setup.py
new file mode 100644
index 0000000..4d2377f
--- /dev/null
+++ b/detectron2_detector/setup.py
@@ -0,0 +1,29 @@
+from setuptools import setup
+import os, glob
+
+package_name = 'detectron2_detector'
+
+setup(
+ name=package_name,
+ version='0.0.1',
+ packages=[package_name],
+ data_files=[
+ ('share/ament_index/resource_index/packages',
+ ['resource/' + package_name]),
+ ('share/' + package_name, ['package.xml']),
+ ('share/' + package_name + '/config', glob.glob('config/*.yaml')),
+ ('share/' + package_name + '/launch', glob.glob('launch/*.launch.py'))
+ ],
+ install_requires=['setuptools'],
+ zip_safe=True,
+ maintainer='Shengjian Chen, Steven Macenski',
+ maintainer_email='csj15thu@gmail.com, stevenmacenski@gmail.com',
+ description='This detector uses detectron2 to get object mask and then uses pointcloud2 data to estimate 3D position.',
+ license='Apache-2.0',
+ tests_require=['pytest'],
+ entry_points={
+ 'console_scripts': [
+ 'detectron2_node = detectron2_detector.detectron2_node:main'
+ ],
+ },
+)
diff --git a/detectron2_detector/test/test_copyright.py b/detectron2_detector/test/test_copyright.py
new file mode 100644
index 0000000..cc8ff03
--- /dev/null
+++ b/detectron2_detector/test/test_copyright.py
@@ -0,0 +1,23 @@
+# Copyright 2015 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.
+
+from ament_copyright.main import main
+import pytest
+
+
+@pytest.mark.copyright
+@pytest.mark.linter
+def test_copyright():
+ rc = main(argv=['.', 'test'])
+ assert rc == 0, 'Found errors'
diff --git a/detectron2_detector/test/test_flake8.py b/detectron2_detector/test/test_flake8.py
new file mode 100644
index 0000000..27ee107
--- /dev/null
+++ b/detectron2_detector/test/test_flake8.py
@@ -0,0 +1,25 @@
+# Copyright 2017 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.
+
+from ament_flake8.main import main_with_errors
+import pytest
+
+
+@pytest.mark.flake8
+@pytest.mark.linter
+def test_flake8():
+ rc, errors = main_with_errors(argv=[])
+ assert rc == 0, \
+ 'Found %d code style errors / warnings:\n' % len(errors) + \
+ '\n'.join(errors)
diff --git a/detectron2_detector/test/test_pep257.py b/detectron2_detector/test/test_pep257.py
new file mode 100644
index 0000000..b234a38
--- /dev/null
+++ b/detectron2_detector/test/test_pep257.py
@@ -0,0 +1,23 @@
+# Copyright 2015 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.
+
+from ament_pep257.main import main
+import pytest
+
+
+@pytest.mark.linter
+@pytest.mark.pep257
+def test_pep257():
+ rc = main(argv=['.', 'test'])
+ assert rc == 0, 'Found code style errors / warnings'