이번 시간에는 ROS 2와 딥러닝 모델을 결합하여 Application을 구현해보는 예시를 준비했습니다. 사실 제가 딥러닝 분야에 지식은 매우 얕은지라 모델에 대한 설명은 불가합니다. PyTorch를 통해 구현된 오픈소스에 기반하여 동작하는 ROS 2 코드를 개발하는 맥락에 집중해주시면 감사하겠습니다!
이번 예시는 cuda, cudnn 등 GPU를 활용하는 부분이 많습니다. 사용하시는 노트북이 GPU 드라이버를 지원한다면 괜찮지만 그렇지 않은 경우 학습 시 매우 많은 시간이 요구됩니다.
reference : PointNet: Deep Learning on Point Sets for 3D Classification and Segmentation
참고로, Anaconda와 같이 파이썬 가상환경을 사용하고 있다면 ROS 2 python 시스템과 호환이 불가할 수 있습니다. 파이썬 가상환경에 ROS 2 관련 패키지를 설치하시거나, ROS 2 python 환경에 torch 등 필요 패키지를 설치하시기 바랍니다.
git clone https://github.com/fxia22/pointnet.pytorch
cd pointnet.pytorch
pip install -e .
cd scripts
# build C++ code for visualization
bash build.sh
# download dataset
bash download.sh
현재 데모에서 사용하는 dataset은 약 1.1GB로 매우 적은 종류의 물체를 담고 있습니다. 단지 빠른 데모를 위해 이렇게 적용하는 점에 유의하세요!
cd utils
python train_classification.py --dataset <dataset path> --nepoch=<number epochs> --dataset_type <modelnet40 | shapenet>
python train_segmentation.py --dataset <dataset path> --nepoch=<number epochs>
# example
python3 train_segmentation.py --dataset /home/kimsooyoung/Downloads/pointnet.pytorch/shapenetcore_partanno_segmentation_benchmark_v0 --nepoch=24
python3 train_classification.py --dataset /home/kimsooyoung/Downloads/pointnet.pytorch/shapenetcore_partanno_segmentation_benchmark_v0 --nepoch=24 --dataset_type shapenet
ROS 2 연동 전 학습된 pth 모델을 검증해봅시다. 별도의 clone은 필요 없으며 오픈 소스의 수정으로 제가 미리 준비한 코드를 사용하겠습니다.
해당 코드는 isl-org의 오픈소스에 기반하였습니다. ⇒ https://github.com/isl-org/Open3D-PointNet
pip install torchvision
pip install open3d
pip install --upgrade numpy
cd Open3D-PointNet
pip install -r requirements.txt
# 코드 중 아래 내용 수정
NUM_POINTS = 10000
MODEL_PATH = '/home/kimsooyoung/Downloads/pointnet.pytorch/utils/cls/cls_model_23.pth'
DATA_FOLDER = '/home/kimsooyoung/Downloads/pointnet.pytorch/shapenetcore_partanno_segmentation_benchmark_v0'
# 데모 실행
python3 pointnet_test.py
prediction 결과 vector와 pointcloud 데이터를 시각화해서 보여줍니다.
# Problem ontology
classes_dict = {'Airplane': 0, 'Bag': 1, 'Cap': 2, 'Car': 3, 'Chair': 4,
'Earphone': 5, 'Guitar': 6, 'Knife': 7, 'Lamp': 8, 'Laptop': 9,
'Motorbike': 10, 'Mug': 11, 'Pistol': 12, 'Rocket': 13,
'Skateboard': 14, 'Table': 15}
드디어 ROS 2와 PointNet을 연동시켜보려 합니다. 개발의 시작 전, 항상 어떠한 기능을 구현해야 하는지, 이미 개발 가능한 것을 무엇이고 새롭게 구현해야 하는 것은 무엇인지 습관적으로 구체화시키시길 바랍니다.
예시 실행 전, pointnet_torch/pointnet_node.py
코드 내 pth 파일 위치를 갱신해주세요!
class PointNetNode(Node):
def __init__(self):
super().__init__('pointnet_cls_node')
self.declare_parameter('model_path', '/home/kimsooyoung/Downloads/pointnet/pointnet.pytorch/utils/cls/cls_model_23.pth')
self.declare_parameter('num_points', 10000)
# Terminal 1 - gazebo launch
ros2 launch rgbd_world pointnet_world.launch.py
# Terminal 2 - object spawn
ros2 run py_service_tutorial pointnet_object_wo_gravity
# Terminal 3 - clustering
ros2 run py_pcl_tutorial pointnet_cluster_pub
# Terminal 4 - torch classification
ros2 run pointnet_torch pointnet_torch
마지막 터미널에서 검출된 물체와 정확도를 확인할 수 있습니다.
nvidia-smi -l 1
def send_spawn_req(self):
# model_name = "beer"
model_id = input("""Enter Model name Among Below List
1.chair_1 \t2.chair_2 \t3.chair_3
4.labtop_mac_1 \t5.labtop_mac_2 \t6.labtop_mac_3
7.cup_green \t8.cup_blue \t9.cup_yellow
10.cup_paper \t11.lamp_table_large \t12.lamp_table_small
13.side_table_1 \t14.side_table_3 \t15.side_table_set_1
16.side_table_set_2 \t17.table_dining \t18.cafe_table
[Type your choice]: """)
# RANSAC plane segmentation
# Create the segmentation object
seg = cloud_voxed.make_segmenter()
...
# Extract inliers
extracted_inliers = cloud_voxed.extract(inliers, negative=True)
# inliers into list for ROS 2 Conversion
color_cluster_point_list = []
# cloud size color generator
color = size_color_gen(extracted_inliers)
for i, indices in enumerate(extracted_inliers):
color_cluster_point_list.append([extracted_inliers[i][0],
extracted_inliers[i][1],
extracted_inliers[i][2],
rgb_to_float(color)])
카메라가 한쪽 방향만을 인지하여 아래와 같이 객체가 분리되는 상황이 발생하기 때문에 알고리즘을 간소화하였습니다.
마지막으로 PointCloud2 topic을 통해 Inference를 실행하는 코드입니다.
class PointNetNode(Node):
def __init__(self):
super().__init__('pointnet_cls_node')
self.declare_parameter('model_path', '/home/kimsooyoung/Downloads/pointnet/pointnet.pytorch/utils/cls/cls_model_23.pth')
self.declare_parameter('num_points', 10000)
MODEL_PATH = self.get_parameter('model_path').value
NUM_POINTS = self.get_parameter('num_points').value
# Create the classification network from pre-trained model
self.classifier = PointNetCls(k=len(classes_dict.items()), num_points=NUM_POINTS)
if torch.cuda.is_available():
self.classifier.cuda()
self.classifier.load_state_dict(torch.load(MODEL_PATH))
else:
self.classifier.load_state_dict(torch.load(MODEL_PATH, map_location='cpu'))
self.classifier.eval()
def obj_callback(self, msg):
# 1. Convert PointCloud2 message to numpy array
points_list = []
for data in pc2.read_points(msg, skip_nans=True):
points_list.append([data[0], data[1], data[2]])
points = np.asarray(points_list, dtype=np.float32)
# 2. numpy resample
m, n = points.shape
choice = np.random.choice(m, 10000, replace=True)
point_set = points[choice, :]
# 3. numpy to torch
point_set = torch.from_numpy(point_set)
# 4. perform inference in GPU
if self.cb_flag == True:
points = Variable(point_set.unsqueeze(0))
points = points.transpose(2, 1)
if torch.cuda.is_available():
points = points.cuda()
pred_logsoft, _ = self.classifier(points)
# move data back to cpu
pred_logsoft_cpu = pred_logsoft.data.cpu().numpy().squeeze()
pred_soft_cpu = np.exp(pred_logsoft_cpu)
pred_class = np.argmax(pred_soft_cpu)
self.get_logger().info(f'Your object is a [{list(classes_dict.keys())[pred_class]}]')
self.get_logger().info('Probability is a [{:0.3}]'.format(pred_soft_cpu[pred_class]))