公众号|沐洒(ID:musama2018)
背景
上一篇文章《当前端遇到自动驾驶》有详细介绍过自动驾驶点云标注的背景,就不再赘述了,这里只稍微再补充一点领域知识:
常见的点云标注任务有:动态帧(连续帧),静态帧(叠帧),2D&3D融合 等等。
不同的任务场景有不同的技术难点,我们今天针对动&静态帧标注这两个场景下的点云渲染和加载性能优化来展开。
挑战
挑战1: 数据量大
每个PCD[1]的点云数量高达几十万量级,而3D框体调整需要实时渲染,全量扫描点云极度消耗机器资源,产生交互卡顿,如何优化?
挑战2: 文件体积大
每个PCD文件包含大量数据,ASCII编码模式下单文件大小高达20多MB,在静态帧标注场景,单帧能达到几百MB,用户光加载个文件都要等很久,如何优化?
静态帧:将N个PCD数据叠加在同一个场景(scene)下进行处理,主要用于标注一些建筑物和路标等静止物体。
方法论
在进入实际问题解决之前,我们先引入几个思想模型(方法论):
分治
分治[2],字面意思就是“分而治之”,就是把复杂问题拆分成多个简单问题来处理,最后再合并处理结果。
这个思想在计算机领域极其常见,本身就是计算机诞生的原理性支点。
编码
编解码在计算机领域也是极其常见的,在不同场景下切换不同的编解码方式,可以在易用和高效之间灵活切换。
流式
从微观视角看没有绝对的流式,一切都是离散的。但我们在顶层应用的开发时,从宏观视角看,就会出现流式现象,本质上就是粒度更小的批处理。
实战效果
先简单讲下基本思路,其实不管是解决什么问题,多大规模的问题,最本质的方法论都是普适的。比如解决规模问题,无非就是采用“分治”的策略嘛,分而治之,多大的问题都能被拆解为若干个相似的小问题,再逐一治理。
上面我们已经介绍了三种最常用的思想模型,接下来我们看看如何在遇到的两个挑战里进行实操。
挑战1
挑战1(数据量大)明显是个规模问题,既然是规模问题,就可以用分治思想解决。
我们先把整个点云所覆盖的XY平面,拆分为N个矩形单元,比如10 x 10一个单元,那如果整体覆盖面大小是1000 x 1000 的话,就会被分拆为10000个处理单元,每个单元都有自己的坐标边界(Xmin,Xmax,Ymin,Ymax),当我们绘制3D框体时候,可以先用哈希表找出该框体所占用的单元面积,做一个标记,等整体用户操作完成后,再统一渲染,并且只渲染被标记为“活动” 的处理单元即可。
原始点云
网格化点云
这样下来,其实每次渲染只需要重新更新10 x 10范围内的点云,这个数量级基本上可以从十万级降低到百级(取决于处理单元的大小),处理速度也会快很多。
目前这个方案的代码尚未开发完成,暂时不放出来了,感兴趣的朋友也可以自己试着实现一版。
挑战2
挑战2(文件体积大)其实就是解决资源文件过大问题,在前端这里早就司空见惯了,无非就是下面几种方式:
- 压缩文件 —— 对应编解码模型
- 拆分文件 —— 对应分治模型
- 流式加载 —— 对应流式模型
压缩
先来说压缩,PCD文件有很多种编码格式,其中ASCII格式比较直观,我们可以直接读懂文件,明文看到该文件点位的信息,方便我们及时纠错,但是缺点就是太大了。
ASCII编码的PCD文件
所以我们将生产环境用的PCD文件,统一重新进行了二进制编码,采用binary方式写文件,这就极大的缩小了文件体积 (压缩到原来的20%) 。
二进制编码的PCD文件
代码参考如下:
const transformToBinaryPcd = (points) => {
// XYZI模式,共4个参数,每个参数4个字节
const dataview = new DataView(new ArrayBuffer(16 * points.length));
points.forEach((point, rowIndex) => {
if (point.length) {
point.forEach((axis, axisIndex) => {
dataview.setFloat32(rowIndex * 16 + axisIndex * 4, Number(axis), true)
})
}
})
return dataview;
}
const run = (input, output) => new Promise((resolve) => {
const data = fs.readFileSync(input)
const ab = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
const result = parse(ab);
const finalPoints = trans(result.points)
const dataview = transformToBinaryPcd(finalPoints);
const wstream = fs.createWriteStream(output, 'binary');
wstream.write(getPCDHeaderString(finalPoints.length))
wstream.write(Buffer.from(dataview.buffer))
wstream.end(() => {
resolve(`${output} 写入成功!`);
});
})
拆分&流式
在静态帧标注场景,我们一开始采用离线堆叠的方式处理文件,处理好合并帧PCD之后,再整体加载,结果不言而喻,非常差的体验,一个叠20帧的PCD文件大小高达五六百MB,即便进行二进制压缩,也需要上百MB。
单帧PCD渲染图,约30万个点
叠20帧PCD渲染图,约600万个点
最终我们决定采取 分片流式加载 的方式,渐进式的加载PCD,并增量绘制到场景(scene)里,效果如下:
分片流式加载(模糊是因为我压缩了GIF)
优化完,体验就舒服多了,从图上可以明显看出来这种渐进式加载体验带来的丝滑效果很是令人满意。
全文完。 码字不易,如果你还想继续看我写的东西,就关注我吧(记得加星标🌟哦),顺便给个赞👍或点一下在看,你的支持是我继续写下去的动力。
公众号|沐洒(ID:musama2018)
参考资料
[1]pointclouds.org/documentati…: PCD
[2]zh.wikipedia.org/zh-cn/%E5%8…: 分治
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 1 天,点击查看活动详情