服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

3D点云初探:基于全卷积神经网络实现3D物体识别

日期: 来源:新机器视觉收集编辑:郑博培

点击下方卡片,关注“新机器视觉”公众号

重磅干货,第一时间送达

来源丨人工智能大讲堂

在当今的计算机视觉系统中,2D图像识别技术已经相对成熟,但3D物体识别依然是一个关键但未被充分利用的领域。本文将基于百度的飞浆围绕3D点云的物体识别展开叙述。

一、从2D图像识别到3D物体识别

二维图像是由一个个像素点组成的,这些像素点可以用数字来进行表示,例如MINIST数据集里的一张图像,可以用一个28×28的二维矩阵来表示:

将图像作为一个二维矩阵输入到神经网络中,可以得到一个输出结果,这个结果便是对该图像的识别结果:

对于一个3D点云物体来说,其每个点都包含了x、y和z轴的坐标信息,对应的坐标信息其实也可以表示成二维矩阵:

既然都是二维矩阵,那么3D物体的识别便可以借鉴2D图像识别的思想来实现。

二、ModelNet10:3D CAD数据集


该数据集包含浴缸、床、椅子、桌子等10类CAD家具模型。

1.存储格式

图像文件的后缀一般是.jpg或.png;对于三维物体文件来说,通常使用.off文件进行保存,off是object file format的缩写,即物体文件格式的简称。其文件格式如下:

OFF顶点数 面片数 边数# 以下是顶点坐标x y zx y z...# 以下是每个面的顶点的索引和颜色n个顶点 顶点1的索引 顶点2的索引 … 顶点n的索引 RGB颜色表示...

比如常见的立方体格式为:

OFF8 6 121.0  0.0 1.41420.0  1.0 1.4142-1.0  0.0 1.41420.0 -1.0 1.41421.0  0.0 0.00.0  1.0 0.0-1.0  0.0 0.00.0 -1.0 0.04  0 1 2 3  255 0 0 #red4  7 4 0 3  0 255 0 #green4  4 5 1 0  0 0 255 #blue4  5 6 2 1  0 255 0 4  3 2 6 7  0 0 2554  6 5 4 7  255 0 0


该文件的第一行8 6 12 代表有8个顶点,6个面和12条边;后面8行代表8个顶点的坐标;最后6行是6个面的上的顶点的索引和颜色。

2.读取方法

.off文件在Python中不需要额外解析,只需要正常打开逐行读取分析信息即可。

# 解压数据集!unzip -oq /home/aistudio/data/data108288/ModelNet10.zip

import numpy as np
filename="ModelNet10/chair/train/chair_0001.off"f = open(filename, 'r')f.readline() # 第一行为OFF,跳过
num_views, num_groups, num_edges = map(int, f.readline().split())print("该文件有{}个顶点,{}个面和{}条边".format(num_views, num_groups, num_edges))
view_data = [] # 保存所有顶点的坐标信息for view_id in range(num_views): view_data.append(list(map(float, f.readline().split())))views = np.array(view_data)print("所有顶点的坐标信息:\n{}".format(views))f.close()
该文件有2382个顶点,2234个面和0条边
所有顶点的坐标信息:
[[ -9.6995 0.06625 -1.575088]
[ -9.6965 -8.83705 -18.008024]
[ -9.6995 -9.05255 -17.887559]
...
[ 9.3055 6.80925 -12.881839]
[ 9.3055 6.80925 -12.881839]

[ -9.1335 6.80925 -12.881839]]


3.点云可视化

为了直观地检查3D点云物体,可以通过可视化的方法进行验证。

可视化工具

MeshLab:https://www.meshlab.net/#download

MeshLab是一个用于处理和编辑三维点云的开源工具,通过它可以很方便地对.off文件进行可视化。

plt可视化

为了更方便地可视化3D点云文件,可以使用plt进行可视化。

import numpy as npfrom mpl_toolkits import mplot3d%matplotlib inlineimport matplotlib.pyplot as pltimport numpy as np
# 单独保存每个点的三维坐标zdata = []xdata = []ydata = []for point in views: xdata.append(point[0]) ydata.append(point[1]) zdata.append(point[2])xdata = np.array(xdata)ydata = np.array(ydata)zdata = np.array(zdata)
# 3D点云可视化ax = plt.axes(projection='3d')ax.scatter3D(xdata, ydata, zdata, c='r')plt.show()



4.数据集定义

可以使用飞桨提供的paddle.io.Dataset基类,来快速实现自定义数据集。

import os
trainList = open("trainList.txt", "w") # 训练集testList = open("testList.txt", "w") # 测试集for root, dirs, files in os.walk("ModelNet10"): for file in files: if file.endswith(".off"): if "train" in root: trainList.writelines(os.path.join(root, file) + "\n") elif "test" in root: testList.writelines(os.path.join(root, file) + "\n")
trainList.close()testList.close()

这里说明一下数据处理的思路。在做手写数字识别时,可以把二维矩阵每一行首尾相接,变成一维矩阵;同理,三维物体也可以把三个轴的坐标分别拿出来,变成一维矩阵。

import osimport numpy as npimport paddlefrom paddle.io import Dataset
category = { 'bathtub': 0, 'bed': 1, 'chair': 2, 'desk': 3, 'dresser': 4, 'monitor': 5, 'night_stand': 6, 'sofa': 7, 'table': 8, 'toilet': 9}
def ThreeDFolder(DataList): filenameList = open(DataList, "r") AllData = [] for filename in filenameList: f = open(filename.split('\n')[0], 'r') f.readline() # 第一行为OFF,跳过 num_views, num_groups, num_edges = map(int, f.readline().split()) view_data = [] # 保存所有顶点的坐标信息 for view_id in range(num_views): view_data.append(list(map(float, f.readline().split()))) # 单独保存每个点的三维坐标 zdata = [] xdata = [] ydata = [] for point in view_data: xdata.append(point[0]) ydata.append(point[1]) zdata.append(point[2]) xdata = np.array(xdata) ydata = np.array(ydata) zdata = np.array(zdata) data = np.array([xdata, ydata, zdata])
label = np.array(category[filename.split('/')[1]])
AllData.append([data, label]) f.close()
return AllData
class ModelNetDataset(Dataset): """ 步骤一:继承paddle.io.Dataset类 """ def __init__(self, mode="Train"): """ 步骤二:实现构造函数,定义数据读取方式,划分训练和测试数据集 """ super(ModelNetDataset, self).__init__() train_dir = '/home/aistudio/trainList.txt' test_dir = '/home/aistudio/testList.txt' self.mode = mode train_data_folder = ThreeDFolder(DataList = train_dir) test_data_folder = ThreeDFolder(DataList = test_dir) if self.mode == "Train": self.data = train_data_folder elif self.mode == "Test": self.data = test_data_folder
def __getitem__(self, index): """ 步骤三:实现__getitem__方法,定义指定index时如何获取数据,并返回单条数据(训练数据,对应的标签) """ data = np.array(self.data[index][0]).astype('float32') label = np.array([self.data[index][1]]).astype('int64') return data, label
def __len__(self): """ 步骤四:实现__len__方法,返回数据集总数目 """ return len(self.data)
train_dataset = ModelNetDataset(mode="Train")test_dataset = ModelNetDataset(mode="Test")


查看第一条数据,这是一个用3×N矩阵((3表示x、y和z轴;N表示N个坐标点))表示的3D物体:

print(train_dataset[0])


(array([[ 35.3296 ,  30.6052 ,  35.3296 , ...,   3.9075 ,   4.1437 ,
4.1437 ],
[-89.62712, -89.62712, -89.62712, ..., 93.29006, 93.61506,
93.61506],
[ 16.1253 , 14.3143 , 14.3143 , ..., 14.4374 , 18.3374 ,

14.4374 ]], dtype=float32), array([0]))


三、模型组网与训练

在模型组网时,整体思路如下所示:


1.全卷积神经网络

全卷积神经网络,字面意思,就是只有卷积层的网络,因为没有线性变换层,因此可以接受任意大小的输入。

3D点云文件中,不同物体,其坐标点的个数是不同的,因此全卷积神经网络在处理这类问题时有很大的优势。

import paddleimport paddle.nn.functional as F
class Block1024(paddle.nn.Layer): def __init__(self): super(Block1024, self).__init__() self.pool = paddle.nn.AdaptiveAvgPool1D(output_size=1024)
def forward(self, inputs): x = F.sigmoid(inputs) x = self.pool(x)
return x
class Block512(paddle.nn.Layer): def __init__(self): super(Block512, self).__init__() self.pool = paddle.nn.AdaptiveAvgPool1D(output_size=512)
def forward(self, inputs): x = F.sigmoid(inputs) x = self.pool(x)
return x
class Block256(paddle.nn.Layer): def __init__(self): super(Block256, self).__init__() self.pool = paddle.nn.AdaptiveAvgPool1D(output_size=256)
def forward(self, inputs): x = F.sigmoid(inputs) x = self.pool(x)
return x
class Block128(paddle.nn.Layer): def __init__(self): super(Block128, self).__init__() self.pool = paddle.nn.AdaptiveMaxPool1D(output_size=10)
def forward(self, inputs): x = F.sigmoid(inputs) x = self.pool(x)
return x
class BlockOut(paddle.nn.Layer): def __init__(self): super(BlockOut, self).__init__() self.conv = paddle.nn.Conv1D(in_channels=10, out_channels=1, kernel_size=3) self.pool = paddle.nn.AdaptiveAvgPool1D(output_size=10)
def forward(self, inputs): x = self.conv(inputs) x = F.sigmoid(x) x = self.pool(x)
return x
# Layer类继承方式组网class ThreeDNet(paddle.nn.Layer): def __init__(self): super(ThreeDNet, self).__init__() self.Block1024 = Block1024() self.Block512 = Block512() self.Block256 = Block256() self.Block128 = Block128() self.conv11 = paddle.nn.Conv1D(in_channels=3, out_channels=33, kernel_size=13) self.conv7a = paddle.nn.Conv1D(in_channels=33, out_channels=99, kernel_size=11) self.conv7b = paddle.nn.Conv1D(in_channels=33, out_channels=99, kernel_size=11) self.conv5a = paddle.nn.Conv1D(in_channels=99, out_channels=297, kernel_size=7) self.conv5b = paddle.nn.Conv1D(in_channels=99, out_channels=297, kernel_size=7) self.conv5c = paddle.nn.Conv1D(in_channels=99, out_channels=297, kernel_size=7) self.conv5d = paddle.nn.Conv1D(in_channels=99, out_channels=297, kernel_size=7) self.conv5e = paddle.nn.Conv1D(in_channels=99, out_channels=297, kernel_size=7) self.conv3a = paddle.nn.Conv1D(in_channels=297, out_channels=10, kernel_size=5) self.conv3b = paddle.nn.Conv1D(in_channels=297, out_channels=10, kernel_size=5) self.conv3c = paddle.nn.Conv1D(in_channels=297, out_channels=10, kernel_size=5) self.conv3d = paddle.nn.Conv1D(in_channels=297, out_channels=10, kernel_size=5) self.conv3e = paddle.nn.Conv1D(in_channels=297, out_channels=10, kernel_size=5) self.conv3f = paddle.nn.Conv1D(in_channels=297, out_channels=10, kernel_size=5) self.conv3g = paddle.nn.Conv1D(in_channels=297, out_channels=10, kernel_size=5) self.conv3h = paddle.nn.Conv1D(in_channels=297, out_channels=10, kernel_size=5) self.BlockOut = BlockOut()

def forward(self, inputs): x11 = self.conv11(inputs) a = self.Block1024(x11) x7 = self.conv7a(x11) b1 = self.Block512(x7) b2 = self.Block512(self.conv7b(a)) xb = paddle.concat(x=[b1, b2], axis=-1)
x5 = self.conv5a(x7) xb5 = self.conv5b(xb) c1 = self.Block256(self.conv5c(b1)) c2 = self.Block256(self.conv5d(b2)) c3 = self.Block256(self.conv5e(xb)) c4 = self.Block256(x5) c5 = self.Block256(xb5) xc = paddle.concat(x=[c1, c2, c3, c4, c5], axis=-1)
x3 = self.conv3a(x5) xbc3 = self.conv3b(xb5) xc3 = self.conv3c(xc) d1 = self.Block128(self.conv3d(c1)) d2 = self.Block128(self.conv3e(c2)) d3 = self.Block128(self.conv3f(c3)) d4 = self.Block128(self.conv3g(xc)) d5 = self.Block128(self.conv3h(xb5)) d6 = self.Block128(x3) d7 = self.Block128(xbc3) d8 = self.Block128(xc3) xd = paddle.concat(x=[d1, d2, d3, d4, d5, d6, d7, d8], axis=-1)
output = self.BlockOut(xd) return output
threeDNet = ThreeDNet()


2.查看网络结构

使用高层API查看网络结构,由于是全卷积神经网络,因此神经网络可以适配3D点云中点的个数,即坐标点的个数可以是任意的:

paddle.summary(threeDNet, (1, 3, 1024)) # (1, 3, N)N表示坐标点的个数,可以是的任意值(因为第一层卷积核为7,因此要大于或等于7,但实际中应该不存在只有7个点的点云)


paddle.summary(threeDNet, (1, 3, 1024)) # (1, 3, N)N表示坐标点的个数,可以是的任意值(因为第一层卷积核为7,因此要大于或等于7,但实际中应该不存在只有7个点的点云)
-------------------------------------------------------------------------------
Layer (type) Input Shape Output Shape Param #
===============================================================================
Conv1D-1 [[1, 3, 1024]] [1, 33, 1012] 1,320
AdaptiveAvgPool1D-1 [[1, 33, 1012]] [1, 33, 1024] 0
Block1024-1 [[1, 33, 1012]] [1, 33, 1024] 0
Conv1D-2 [[1, 33, 1012]] [1, 99, 1002] 36,036
AdaptiveAvgPool1D-2 [[1, 99, 1014]] [1, 99, 512] 0
Block512-1 [[1, 99, 1014]] [1, 99, 512] 0
Conv1D-3 [[1, 33, 1024]] [1, 99, 1014] 36,036
Conv1D-4 [[1, 99, 1002]] [1, 297, 996] 206,118
Conv1D-5 [[1, 99, 1024]] [1, 297, 1018] 206,118
Conv1D-6 [[1, 99, 512]] [1, 297, 506] 206,118
AdaptiveAvgPool1D-3 [[1, 297, 1018]] [1, 297, 256] 0
Block256-1 [[1, 297, 1018]] [1, 297, 256] 0
Conv1D-7 [[1, 99, 512]] [1, 297, 506] 206,118
Conv1D-8 [[1, 99, 1024]] [1, 297, 1018] 206,118
Conv1D-9 [[1, 297, 996]] [1, 10, 992] 14,860
Conv1D-10 [[1, 297, 1018]] [1, 10, 1014] 14,860
Conv1D-11 [[1, 297, 1280]] [1, 10, 1276] 14,860
Conv1D-12 [[1, 297, 256]] [1, 10, 252] 14,860
AdaptiveMaxPool1D-1 [[1, 10, 1276]] [1, 10, 10] 0
Block128-1 [[1, 10, 1276]] [1, 10, 10] 0
Conv1D-13 [[1, 297, 256]] [1, 10, 252] 14,860
Conv1D-14 [[1, 297, 256]] [1, 10, 252] 14,860
Conv1D-15 [[1, 297, 1280]] [1, 10, 1276] 14,860
Conv1D-16 [[1, 297, 1018]] [1, 10, 1014] 14,860
Conv1D-17 [[1, 10, 80]] [1, 1, 78] 31
AdaptiveAvgPool1D-4 [[1, 1, 78]] [1, 1, 10] 0
BlockOut-1 [[1, 10, 80]] [1, 1, 10] 0
===============================================================================
Total params: 1,222,893
Trainable params: 1,222,893
Non-trainable params: 0
-------------------------------------------------------------------------------
Input size (MB): 0.01
Forward/backward pass size (MB): 13.88
Params size (MB): 4.66
Estimated Total Size (MB): 18.55
-------------------------------------------------------------------------------

{'total_params': 1222893, 'trainable_params': 1222893}

3.模型训练

# 用 DataLoader 实现数据加载train_loader = paddle.io.DataLoader(train_dataset, batch_size=1, shuffle=True)
# mnist=Mnist()threeDNet.train()
# 设置迭代次数epochs = 100
# 设置优化器lr = paddle.optimizer.lr.CosineAnnealingDecay(learning_rate=0.00025, T_max=int(train_dataset.__len__() * epochs / 128))optim = paddle.optimizer.Momentum(learning_rate=lr, parameters=threeDNet.parameters(), weight_decay=1e-5)# 设置损失函数loss_fn = paddle.nn.CrossEntropyLoss()
# 评估指标BestAcc = 0
for epoch in range(epochs): acc = 0 loss = 0 count = 0 for batch_id, data in enumerate(train_loader()): x_data = data[0] # 训练数据 y_data = data[1] # 训练数据标签 predicts = threeDNet(x_data) # 预测结果 # 计算损失 等价于 prepare 中loss的设置 loss += loss_fn(predicts, y_data)
# 计算准确率 等价于 prepare 中metrics的设置 acc += paddle.metric.accuracy(predicts, y_data) if count >= 128: # 反向传播 loss.backward() # 更新参数 optim.step() # 梯度清零 optim.clear_grad() count = 0 count += 1
loss.backward() optim.step() optim.clear_grad()
print("epoch: {}, batch_id: {}, loss is: {}, acc is: {}".format(epoch, batch_id+1, loss.numpy()/train_dataset.__len__(), acc.numpy()/train_dataset.__len__())) if acc.numpy()/train_dataset.__len__() > BestAcc: state_dict = threeDNet.state_dict() # save state_dict of threeDNet paddle.save(state_dict, "threeDNet/threeDNet.pdparams") BestAcc = acc.numpy()/train_dataset.__len__()


部分训练日志截取:

epoch: 0, batch_id: 3991, loss is: [2.2351303], acc is: [0.19418693]epoch: 1, batch_id: 3991, loss is: [2.1812398], acc is: [0.22275119]epoch: 2, batch_id: 3991, loss is: [2.1573942], acc is: [0.22625908]epoch: 3, batch_id: 3991, loss is: [2.138466], acc is: [0.24805813]epoch: 4, batch_id: 3991, loss is: [2.1174479], acc is: [0.2731145]epoch: 5, batch_id: 3991, loss is: [2.1015415], acc is: [0.29766977]epoch: 6, batch_id: 3991, loss is: [2.0876462], acc is: [0.331997]epoch: 7, batch_id: 3991, loss is: [2.0767505], acc is: [0.3755951]epoch: 8, batch_id: 3991, loss is: [2.0677485], acc is: [0.41618642]epoch: 9, batch_id: 3991, loss is: [2.0585632], acc is: [0.43172136]epoch: 10, batch_id: 3991, loss is: [2.0521076], acc is: [0.4550238]... ...epoch: 95, batch_id: 3991, loss is: [1.8270358], acc is: [0.72788775]epoch: 96, batch_id: 3991, loss is: [1.8227352], acc is: [0.72563267]epoch: 97, batch_id: 3991, loss is: [1.8221083], acc is: [0.73189676]epoch: 98, batch_id: 3991, loss is: [1.8253901], acc is: [0.71961915]epoch: 99, batch_id: 3991, loss is: [1.8208766], acc is: [0.72588325]


四、测试效果评估

# 加载模型权重threeDNet_PATH = "threeDNet/threeDNet.pdparams"threeDNet_state_dict = paddle.load(threeDNet_PATH)threeDNet.set_dict(threeDNet_state_dict)threeDNet.eval()
# 加载测试集test_loader = paddle.io.DataLoader(test_dataset, batch_size=1)


  1. 批量测试

loss_fn = paddle.nn.CrossEntropyLoss()loss = 0acc = 0count = 0for batch_id, data in enumerate(test_loader()):    if count > 500: # 只取前500条数据,数据过多会内存溢出        break
x_data = data[0] # 测试数据 y_data = data[1] # 测试数据标签 predicts = threeDNet(x_data)# 预测结果
# 计算损失与精度 loss += loss_fn(predicts, y_data) acc += paddle.metric.accuracy(predicts, y_data) count += 1
# 打印信息print("loss is: {}, acc is: {}".format(loss.numpy()/count, acc.numpy()/count))

loss is: [2.1944385], acc is: [0.1996008]

2.逐个测试

取某一条数据进行可视化,更直观地检测模型效果:

import numpy as npfrom mpl_toolkits import mplot3d%matplotlib inlineimport matplotlib.pyplot as pltimport numpy as np
index = 0for batch_id, data in enumerate(test_loader()): if batch_id != index: continue x_data = data[0] # 测试数据 y_data = data[1] # 测试数据标签 predicts = threeDNet(x_data)# 预测结果 # 可视化 xdata = np.array(data[0][0][0]) ydata = np.array(data[0][0][1]) zdata = np.array(data[0][0][2]) # 3D点云可视化 ax = plt.axes(projection='3d') ax.scatter3D(xdata, ydata, zdata, c='r') plt.show()
print("该3D点云物体的期望结果是{},实际结果是{}".format([k for k,v in category.items() if v == int(y_data)], [k for k,v in category.items() if v == np.argsort(predicts[0][0][-1][0])]))



<Figure size 432x288 with 1 Axes>
该3D点云物体的期望结果是['bathtub'],实际结果是['bathtub']


五、总结与升华

本项目是对3D点云物体识别的尝试,在数据处理和模型组网的时候,我花了较多的时间。

特别是模型组网,我设计了一个全卷积神经网络来识别3D点云物体,刚开始设计的网络比较小,识别的效果并不理想,后来,我把卷积核增大,并加深网络以及加上跳连,使模型有一个较好的拟合效果。


本文仅做学术分享,如有侵权,请联系删文。

—THE END—

相关阅读

  • 一图看完深度学习架构谱系

  • 点击图片,查看会议报名详情 完整图 记忆网络在记忆网络分支中,hunkim 标注了三篇重要论文:《记忆网络》、《端到端记忆网络》、《DMN:动态记忆网络》。神经编程是记忆网络的下
  • 轻量级模型设计与部署总结

  • 编辑 | 嵌入式视觉 点击下方卡片,关注“自动驾驶之心”公众号ADAS巨卷干货,即可获取点击进入→自动驾驶之心【模型部署】技术交流群后台回复【模型部署工程】获取基于TensorRT
  • 曲面组合:顶点编辑器2和SUbD插件年终大促

  • Thomthom 的参数化细分曲面插件 SUbD可以让我们方便地通过控制简单的四边面网格模型生成复杂的曲面物体。 而SketchUp本身对于点的选择和编辑是比较薄弱的,顶点编辑器插件的
  • 安德烈·塞迈雷迪的工作

  • “”W. T. Gowers/文 史永堂/译1. 引言安德烈•塞迈雷迪是数学中组合数学领域的杰出人物,特别是在极值组合论这一子领域做出了重要的贡献。稍后我将解释这些术语的意思,但是这
  • 客流恢复与消费恢复的国际经验

  • 随着疫情防控政策的优化,消费的恢复情况成为市场关注的重点。我们来简单看一下海外的经验。我们使用的客流量数据是谷歌发布的COVID-19 Mobility
    Trends,选取的是Retail and r
  • 突发最新“秀”!拜登下令、加拿大支持

  • 又来了!五角大楼宣称F-16战机在休伦湖上空击落一个空中物体据美国有线电视新闻网(CNN)消息,五角大楼当地时间12日宣称,一架F-16战机当天早些时候在位于美加边境的休伦湖上空击落

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 3D点云初探:基于全卷积神经网络实现3D物体识别

  • 点击下方卡片,关注“新机器视觉”公众号重磅干货,第一时间送达来源丨人工智能大讲堂在当今的计算机视觉系统中,2D图像识别技术已经相对成熟,但3D物体识别依然是一个关键但未被充
  • ​CBS多机器人路径规划

  • 点击下方卡片,关注“新机器视觉”公众号重磅干货,第一时间送达编辑丨古月居CBS多机器人路径规划单个机器人通过路径规划、运动控制,能够躲避环境中的障碍物,但会面临一个严峻的