4D RADAR-TESLA汽车-nVidia-摄像头技术分析

4D RADAR-TESLA汽车-nVidia-摄像头技术分析
参考文献链接
https://mp.weixin.qq.com/s/iTvLUOI3h77xd8-KLbdllg
4D 成像毫米波雷达
 

 

 目前4D毫米波雷达首先要实现对传统毫米波雷达的替代。

4D成像毫米波雷达最近又热了起来。
今年CES上,4D成像毫米波雷达声势夺人,一众芯片企业诸如恩智浦、TI、Mobileye都陆续推出或更新了自己的成像雷达方案,毫米波雷达系统厂商Arbe、Zadar Labs、Smartmicro等也都带来了各自的成像雷达产品。
其中最受到业内人士关注的,莫过于Mobileye 首席执行官Amnon Shashua 在 CES 演讲中对4D成像毫米波雷达的强调,“(到2025年)除了正面,我们只想要毫米波雷达,不想要激光雷达。”
Yole Développement 的Imaging 首席分析师 Pierre Cambou 表示,该演讲引发了人们的猜测,即Mobileye 现在不认为激光雷达“比雷达更重要”。
VSI Labs 的合伙人兼咨询服务总监Danny Kim 更是在 CES 后发布的一份报告中写道:“与过去的 CES 活动不同,感觉激光雷达公司并没有为行业带来那么多突破性的发明” 。“另一方面,4D毫米波雷达越来越受欢迎”,并称4D毫米波雷达正成为汽车传感器中的“新星”。
事实上,在此前华为的入局下,4D成像毫米波雷达早就接受过市场的一波热议,且也远非新技术,但这次它为什么又在CES上被重点关注了?其中有什么因素发生了变化?4D成像毫米波雷达是否真的可以和激光雷达PK?抑或它只是一种过渡技术方案?
成像雷达抢风头
Yole将Mobileye的演讲总结为四点,其中两点就是:
  • 4D成像毫米波雷达是消费级自动驾驶车辆的一个重要推动因素;
  • 激光雷达不再是关键。
Mobileye瞄准了三个细分市场:辅助驾驶市场、带有地理围栏的L4 Robotaxi市场以及消费级L4 Robotaxi市场。
Shashua认为,Mobileye的感知方案要想在2025年达到消费级自动驾驶车辆的水平,一要想如何能够显著降低成本,二则是如何将ODD(运行设计域)扩大到L5的水平。
因此在Mobileye的计划中,除了开发可以单独在相机上行驶的消费级自动驾驶车辆方案,到2025年他们可能还将推出可单独在雷达/激光雷达上行驶的消费级自动驾驶车辆方案,后者搭载雷达-LiDAR子系统,届时车辆仅需一个前向激光雷达和360°全包覆车身的毫米波雷达即可。
 

 

 “除了正面,我们只想要毫米波雷达,不想要激光雷达。”Shashua表示。

问题在于,虽然毫米波雷达和激光雷达的成本不是一个数量级,但以目前毫米波雷达的分辨率水平,普通毫米波雷达在拥挤的交通中,并无法分辨彼此非常接近的行人和车辆,因此也就不能作为单独的子系统使用。
于是Mobileye将视线转向了4D成像毫米波雷达。
从性能效果来说,4D成像毫米波雷达算是3D毫米波雷达的升级版,另一方面,从成本上看,4D成像毫米波雷达的成本也仅为激光雷达的10%-20%。
相比于传统的 3D 毫米波雷达,车载 4D毫米波雷达在工作时,除了能够解算出目标的距离、速度、水平角信息,还能解算出目标的俯仰角信息,进而可以提供汽车周围的环境信息,能够避免窨井盖、路肩、减速带所产生的虚警现象。
除此之外,得益于能够提供目标的高度信息,捕捉到汽车周围目标的空间坐标和速度信息,4D 毫米波雷达还能够提供更加真实的路径规划、可通行空间检测功能。
有业内人士对新智驾介绍说,传统毫米波雷达也有点云但是数量少,且没有俯仰信息,4D毫米波雷达增加了俯仰信息和更多的点云数据,“点云一多就可以勾勒出物体轮廓,便是成像”。
不过4D毫米波雷达是否需要成像,要基于具体场景需求的策略制定。
据上述业内人士介绍,目前L3以上的场景会对成像雷达有更大的需求,从技术上来讲,4D成像毫米波雷达是必然趋势,有能力做4D毫米波雷达的厂商基本都在做4D成像毫米波雷达。
“4D毫米波雷达是未来的发展方向,而成像的重点是点云数据足够多,在车上是否要用4D毫米波雷达做成像输出,要看主机厂对传感融合、算力等因素的通盘综合考虑。”
新智驾曾在《刚刚,又一款智能汽车面世!主打卖点竟是「4D 毫米波雷达」》一文中,仔细介绍过4D毫米波雷达的工作原理。
角分辨率作为雷达的指向精度,其数值高低与波长与孔径大小有关,即波长越长,角分辨率越低,孔径越大,分辨率越高。
车载毫米波雷达系统厂商楚航科技创始人兼CEO楚詠焱对新智驾介绍,孔径大小是提升雷达角分辨率的关键,而天线的数量、天线间的排布间隔又会影响到孔径大小。
过去几十年来,车载毫米波雷达界常常是通过增加天线数量的方式来提高角分辨率。
目前车载 4D 毫米波雷达常用的工作机制,则是连续波雷达中的调频连续波雷达(FMCW),它能够以更低功耗、更大带宽的方式,向外连续地发射电磁波,从而实现测量目标的距离和速度信息。
而根据输入输出天线阵列数目的不同,FMCW 雷达可以分为单输入多输出(SIMO)雷达和多输入多输出(MIMO)雷达。
对车载毫米波雷达系统而言,SIMO 雷达早已在 3D 毫米波雷达中广泛应用,而 MIMO 雷达概念则是在 2003 年由 Bliss 和 Forsythe 首次提出,其是车载 4D 毫米波雷达发展的关键技术理论之一。
与传统方式不同,为了解决传统毫米波雷达角分辨率低、点云密度低的问题,当下出现了四种4D毫米波雷达解决方案:
  • 一是基于传统CMOS雷达芯片,强调“软件定义的雷达”,主要厂家有傲酷、Mobileye等;
  • 二则是将多发多收天线集成在一颗芯片,直接提供成像雷达芯片,比如Arbe、Vayyar等;
  • 最传统的,则是将标准雷达芯片进行多芯片级联,以增加天线数量,比如大陆、博世、ZF等一众公司;
  • 四则是通过超材料研发新型雷达架构,代表厂家有Metawave等。
Shashua认为,软件定义的图像毫米波雷达,将会是提高毫米波雷达分辨率的关键。
所谓软件定义的雷达,即通过软件后处理,系统可对雷达信号的接受/发送和处理进行配置,从而大幅提高雷达的性能。
事实上早在两年前,Mobileye就已开始打造这款图像雷达,在2021年的CES上,Shashua也曾对其进行过介绍,只不过所花的时间远没有今年CES上的多。
成像雷达只是过渡方案吗?
而从Mobileye公布的数据看,其成像毫米波雷达的性能在诸多同类产品中,也的确可圈可点。
资料显示,Mobileye 的软件定义雷达将有超过 2000 个虚拟信道,信号发射器与接收器各 48 个,水平角分辨率达0.5°,垂直分辨率达2°,有效探测距离预计为150米。
“Mobileye推出成像雷达的意义与Arbe差不多,因为目前主流方式(包括博世、大陆、ZF的量产方案)都是传统级联方式,新型方案对传统方式是否兼具成本和性能优势有待观察。”但另一业内人士则对新智驾如此表示。
这里有必要先来了解一下组成车载毫米波雷达的核心器件。
车载毫米波雷达的核心器件主要有单片微波集成电路和雷达数字信号处理芯片等。
单片微波集成电路可以实现低噪声放大器、混频器、变频器、功率放大器等功能,主要玩家包括意法半导体、德州仪器、 恩智浦、加特兰等。
雷达数字信号处理芯片,则是用于对毫米波雷达的中频信号进行数字处理,分为通用数字处理芯片和雷达专用处理芯片,目前提供专用雷达处理器的芯片厂商主要有德州仪器、NXP、Infineon、加特兰等。
近年来,随着市场对车载毫米波雷达需求的增加,国内涌现了一批新兴毫米波雷达系统厂商,比如森斯泰克、华域、凌波微步、隼眼科技、楚航科技以及几何伙伴等。
而在2018年,德州仪器提出了4D成像毫米波雷达的概念,并一举推出了基于AWR2243 FMCW单芯片收发器的4片级联4D毫米波雷达全套设计方案。
这一“交钥匙”工程大大降低了企业开发成像毫米波雷达产品的门槛,部分毫米波雷达系统厂商也纷纷开始着手研发自己的 4D 毫米波雷达产品。
目前,已经推出4D毫米波雷达产品的厂商主要有大陆、傲酷雷达、Arbe以及 Smartmicro 等。
上述业内人士对新智驾表示,开发4D成像毫米波雷达已渐渐成为毫米波雷达行业巨头和初创公司的必然选择。
4D成像毫米波雷达之间的性能之争也就愈发激烈。
目前的车载4D成像毫米波雷达多在48个通道(6发8收),有的雷达供应商也有在向192个以上数量的通道迈进,比如森思泰克的4D成像毫米波雷达STA77-8、大陆集团的4D成像毫米波雷达ARS540、傲酷的Eagle等,华为的4D成像毫米波雷达则有288个通道(12发24收)。
不过也有与Mobileye成像雷达通道数量相当的产品,它是由以色列创企Arbe提供的4D成像毫米波雷达Phoenix,采用48发48收,虚拟通道也超过2000个,可提供1°水平×1.5°垂直角分辨率。
华为智能汽车解决方案BU Marketing与销售服务部总裁此前也曾透露过,“华为的4D成像毫米波雷达天线已经做到128发128收,非常先进,华为的毫米波雷达是在通信技术的基础上开发出来的”。
安霸半导体在去年收购了傲酷雷达,其中国区市场营销副总裁郄建军对新智驾表示,其4D成像毫米波雷达产品目前性能已与32线束激光雷达性能类似。
“我们接下来还会继续提升角分辨率,做到4芯片级联,类似128线束激光雷达分辨率的效果。”
郄建军介绍,目前4D成像毫米波雷达的成本和传统毫米波雷达的成本相近,远低于激光雷达的成本。
“激光雷达的成本要想从几千元降到几百元,至少需要5年,所以4D成像毫米波雷达,在某种程度上是可以取代低线束激光雷达的。”
另外,当4D毫米波雷达与多目摄像头相结合时,理论上也被认为可完全不需要激光雷达。
那当测距精度更高的激光雷达的成本下探至可被接受范围时,4D成像毫米波雷达会被淘汰吗?换言之,4D成像毫米波雷达会不会只是一种暂时的“过渡性方案”?
对此楚詠焱的看法是,4D成像毫米波雷达和激光雷达各有特点,可以发挥各自的长处,并不存在谁替代谁的关系。
“比如一些L3级功能,并不需要那么高线束的激光雷达,这时候只需要把毫米波雷达的分辨率稍微提高一点,成本也不会增加太多,有可能很多L3级功能就能落地了,这会是4D成像毫米波雷达的主要市场。”
郄建军则认为,由于普通的毫米波雷达检测静止物体效果不好、角分辨率也低,所以目前成像雷达更多还是在对传统的毫米波雷达进行替代。
谁才会是主传感器?
但目前并没有一款能够真正规模落地的4D成像毫米波雷达产品。
楚詠焱介绍,这其中面临着两个难题。
首先是车企对4D毫米波雷达的需求并不明确。
汽车零部件企业研发产品时大多是需求导向,但目前车企并不确定在L3级自动驾驶车辆中,到底是哪个功能,需要4D成像毫米波雷达,或者仅需要输出点云的4D毫米波雷达。
“另外至于诸如AVP、HWP(高速自动驾驶)、TJP(中低速自动驾驶)等自动驾驶功能,对4D成像毫米波雷达的分辨率要求到底有多高,是1°还是2°,目前都还没有一个准确的定义。”
还有就是在4D成像毫米波雷达方面,虽然它能输出更多点,但目前的毫米波系统厂商并没有想清楚,“输出这些点之后,我到底要干什么”。
原本毫米波雷达只输出带有距离、速度信息的目标,但当4D成像毫米波雷达还额外提供了具有方位角信息的点,企业究竟要利用这一特征达到何种感知目的,目前业内也并没有明确的方案。
第二大难点则是现下业内并没有专门针对4D成像毫米波雷达的测试设备,行业的生态链并不成熟。
厂商们只能利用传统毫米波雷达的测试设备,来验证其4D成像毫米波雷达产品的性能。
但问题是,诸如目标模拟器这样的传统测试设备,其分辨率并不高,无法验证4D成像毫米波雷达的分辨率是否达到了1°或者零点几度。
“我们只能靠上路,用真值系统,用激光雷达去做比对。但这样的话,如果产品的一些基础性能出了问题,比如天线设计,我们根本没有办法在实验室里检测出来,做不了前端测试,无法形成一个完整的研发闭环。”
但4D成像毫米波雷达趋势已不可逆转,比如北汽集团投资了Arbe,其副总经理陈江此前曾表示,北汽集团车型有望搭载Arbe量产后的4D成像毫米波雷达产品,比如上汽也早在去年就宣布已在其R汽车搭载了4D成像毫米波雷达,再比如Magna与Fisker将在2022年底推出的Fisker Ocean车型,也将搭载4D成像毫米波雷达等等。
郄建军也向新智驾表示,其4D毫米波雷达已经拿到了多家主机厂的项目定点。
“今年我们看到几家主机厂都准备上车4D成像毫米波雷达,这就跟几年前的激光雷达一样,一旦有领军企业大规模上4D成像毫米波雷达了,后边的企业就会迅速跟进。”郄建军表示。
至于4D成像毫米波雷达生态链的构建,包括楚航科技在内的诸多企业,都在一起去构建行业的标准,比如雷达的性能要求以及试验方法等等。
“大家都在往4D成像毫米波雷达这个方向看,虽然发展的路线有所不同,但我觉得1-2年内,行业会形成一个简单的标准,也会有落地的尝试。”
楚詠焱预测,4D成像毫米波雷达要想形成一个标准化、可规模量产的产品,估计还需要3-5年。
这或许也是为什么Mobileye在今年的CES上,花了更多的笔墨和时间去强调4D成像毫米波雷达的重要性。
回到前文Mobileye提出的,他们将在2025年推出可单独在雷达/激光雷达上行驶的AV方案,即届时AV车辆仅需一个前向激光雷达和360°全包覆车身的毫米波雷达即可。
这意味着,4D成像毫米波雷达将成为自动驾驶车辆的主传感器。
这种方案可行吗?
楚詠焱认为可行,但由于毫米波雷达和激光雷达对颜色以及二维物体的感知都比较弱,因此这一方案可行的前提,则是车辆能做到车路协同,以准确感知车身周围的环境。
不过摄像头、激光雷达抑或4D成像毫米波雷达,究竟谁能成为未来智能驾驶车辆的主传感器,或许还是要看具体场景,根据不同的功能和需求配置不同的方案。
楚詠焱表示,比如对于ADAS功能来说,主流的标准化4D毫米波雷达产品,3发4收就已足够,对于L3级的功能,12发16收的4D成像毫米波雷达产品则就能满足车辆的感知需求。
而当车辆需要提升到L4级或者L5级的水平,比如矿山、港口,这些场景对价格并不那么敏感,各家厂商就可以根据自己的需求,选择激光雷达或者4D成像毫米波雷达,做差异化布局。
比如长城汽车曾推出的一款无人物流小车,就没有使用激光雷达,而是搭载了5个(1个主雷达+4个角雷达)4D成像毫米波雷达。
不同传感器各有优劣,智能驾驶车辆的感知方案也远未固定。
对于4D成像毫米波雷达来说,要想长期生存,当下最重要的,或许还是要先解决是否能够规模上车的问题。
能走会动的Tesla Bot,DOJO超算明年量产、还有FSD新进展
几个小时前,特斯拉正式举办了 2022 AI Day,一场全球汽车、人工智能、信息科技行业翘首以待足足 13 个月的发布会。

 

 严格意义上 AI Day 不像是「发布会」,而是「交流会」——马斯克本人也在推特上说,「此活动旨在招聘 AI 和机器人工程师,因此技术含量很高」——换句话说,这是马斯克的高山流水,为特斯拉的锺子期而开。

不过这并不妨碍我们以比较轻松的视角,记录下这场科技狂欢。因为特斯拉团队几乎 100% 实现了去年的承诺,在本届 AI Day 上带来了以下技术成果:
不再需要群演的真·Tesla Bot 机器人原型机;不再停留在 PPT 的 DOJO POD 人工智能超级计算机;FSD 技术新进展,等等。
当然,即使我们会尽力写得简单点,今天的文章依然会相对硬核。趁着国庆假期,建议大家可以慢慢看,下面马上开始。
一、Tesla Bot 原型机
Optimus 它来了!

 

 13 个月前还需要群演的 Tesla Bot,今天正式以原型机的形式出现——原型意思是它还没穿衣服(外壳)

 

 原型机的样子比 PPT 里面明显更粗放,线束、促动器等零件堆砌略显凌乱。但好消息是,Tesla Bot 原型机已经可以走路、打招呼,双手可以完整举过头顶。

 

 在特斯拉的演示视频里,Optimus 已经可以做一些简单的工作,比如搬运箱子、浇花等等。

 

 

 

 

 但更重要的可能是这个画面:Optimus 眼中的世界,通过纯视觉发现并分析周边的一切,然后识别出自己的任务对象。

 

 事实上 Optimus 不是不能装上外壳,但出于工程原因,带外壳版本截止到发布会当天还不能自如地走路(原因后面再解释),只能简单挥舞一下手臂。

 

 装上外壳之后我们发现,更接近量产版的 Optimus,变得更胖了——现在它重 73 公斤,比去年 PPT 版「增重」超过 20%,整个「人」圆了一大圈。

 

 更接近量产,也意味着 Optimus 更高阶的参数也可以公布了:100W 静坐功耗、500W 快步走功耗、超过 200 档的关节自由度,光手部自由度就有 27 档。

 

 另外,Optimus 的大脑由单块 FSD Chip 组成,意味着算力应该是 HW3.0 的一半(72TOPS);电池则是 52V 电压、2.3kWh 容量、内置电子电气元件的一体单元。

说完数字,是时候聊聊 Optimus 的研发逻辑了。
1. 汽车化
马斯克说过「当你能解决自动驾驶,你就能解决现实世界中的人工智能」。这句话点破了特斯拉研发 Optimus 的方法论:大量借鉴汽车研发经验。
比如借鉴汽车碰撞模拟软件,为 Optimus 编写「跌倒测试」软件。

 

 再比如利用汽车大规模零件的生产经验,为 Optimus 挑选尽可能保证成本+效率的原材料。「我们不会用碳纤维、钛合金这样的原材料。因为它们虽然很优秀,但像肩膀这样的易损部位,制造和维修成本都太贵了」。

 

 除此以外,制造 Optimus 的中心思想,也基本和智能汽车相当:减少线束长度、计算和电子控制单元中心化,等等。

2. 仿生学
既然是类人机器人 humanoid,设计自然要借鉴人类仿生学。

 

 特斯拉用了几个例子解释 Optimus 的仿生学,首先是膝关节。特斯拉表示 Optimus 的关节希望尽量复刻生物学上的「非线性」逻辑,也就是贴合膝关节直立到完全弯曲时的受力曲线。

 

 为此,Optimus 的膝关节使用了类似于平面四杆机构的设计,最终发力效果会更接近人类。

 

 紧接着,我们创造人类文明的双手,才是 Optimus 类人之路更大的 boss。

 

 Optimus 光手掌区域就用了 6 个促动器,具有 11 档的自由度。拥有自适应的抓握角度、20 磅(9 公斤)负荷、工具使用能力、小物件精准抓握能力等等。

 

 此外,Optimus 的手掌用的是「non-backdrivable」无法反向驱动的指尖促动器。学术界的看法是,这样的促动器可以提升在「开放环境」下的性能。

最后是让 Optimus 学着像人类一样走路——这里用到的仿生学设计叫做「运动重心控制」。

 

 为什么有外壳的  Optimus 还不会走?其中一个原因就是重量变了,运动重心控制算法需要重新调试。

 

 事实上,Optimus 不仅要做到会走路,还要做到别摔倒。所以它不仅需要控制走路的重心,还要稳住受到外力(比如推搡)时的随机动态重心。

 

 训练 FSD 用到的神经网络和在线仿真模拟,这次在 Optimus 身上大显身手。路径规划、视觉融合、视觉导航等等熟悉的名词都被「灌输」到 Optimus 脑子里。

 

 这样的努力下,Optimus 今年 4 月迈出了它的第一步;7 月份解锁了骨盆活动;8 月走路时可以摆手臂了——发布会前几周,实现了脚趾离地的类人行走动作。

 

 3. 「肌肉」

我们通过结缔组织包裹着的肌肉完成运动,机器人的「肌肉」则叫做促动器 actuator。

 

 如上图所示,橙色部分均为 Optimus 的促动器,这些促动器也都是特斯拉完全自研的。

 

 特斯拉为  Optimus 从力度大小的角度,设计了 6 种各自独特的促动器——这其实是很小的数字,业界平均是 20-30,甚至 50 种,目的是覆盖尽可能多的人类活动细节。

为什么特斯拉的促动器种类这么少?原因还是 FSD 体系。
特斯拉举了 28 种人类常见活动,比如抬举手臂、弯曲右膝等。通过分析这些活动反馈的云数据,找出各类运动的相对共同点,然后就可以尽量减少专门设计促动器的种类。

 

 虽然只是轻描淡写的一张 PPT,但我认为促动器从 50 种减少到 6 种,意义实际上远大于借鉴特斯拉电机经验的促动器本体——因为它代表着数据为王的新工业时代。

不过促动器种类大幅度减少,也意味着 Optimus 前期的实际效果可能会没有那么「类人」,当然还是得等最终交付了。
最后来说一个数字:2 万美元(约 14 万元)。
这笔钱买不到半台 Model 3,但却是马斯克口中 Optimus 的目标售价。「它会彻底改变人类社会的效率,就像无人交通可以彻底改变运输效率」
二、DOJO 的终极形态?
本来发布会的第二部分是 FSD,但那部分过于硬核,我决定先让大家看点激动人心的数字。
去年 DOJO 惊艳全世界,但遗憾的是有太多细节未公布。D1 芯片是怎么组成 EXA POD 超算系统的?理论性能爆炸,能代表实际应用吗?
这部分,特斯拉举了大量的数据,证明自己已经是计算领域的新巨头。

 

 首先是散热。

先别发问号,超算平台的散热,一直是衡量超算制造者系统工程能力的重要维度。比如谷歌、华为、英伟达在公布自家方案的时候,都会花大篇幅讲散热。
DOJO POD 的散热可以用两个词概括:高集成度、高自研率。

 

 特斯拉在 DOJO POD 上使用了全自研的 VRM(电压调节模组),单个 VRM 模组可以在不足 25 美分硬币面积的电路上,提供超过 1000A 的电流。

高集成度带来的问题,是热膨胀系数 CTE。DOJO 堪称极限的体积集成率和发热,意味着 CTE 稍微失控,都会对系统结构造成巨大破坏(也就是会撑爆)。

 

 为此,这套自研 VRM 在过去两年内迭代了 14 个版本,最终才完全符合特斯拉对 CTE 指标的要求。

目前 DOJO POD 已经进入负载测试阶段——单机柜 2.2MW 的负载,相当于 6 台 Model Y 双电机全力输出。

 

 解决了散热,才有资格说集成度。

一个 DOJO POD 机柜由两层计算托盘和存储系统组成。每一层托盘都有 6 个 D1 Tile 计算「瓦片」——两层 12 片 组成的一个机柜,就可以提供 108PFLOPS 算力的深度学习性能。

 

 对了,DOJO POD 的供电模组也是 52V 电压的,Optimus 母亲实锤了。

每层托盘都连接着超高速存储系统:640GB 运行内存可以提供超过 18TB 每秒的运算带宽,另外还有超过 1TB 每秒的网络交换。

 

 为了适配训练软件以及运营/维护,每个托盘还配备了专属的管理计算中心。

 

 最终,可以提供 1.1E 算力、13TB 运存、1.3TB 缓存的 EXA POD,将于 2023 年 Q1,正式量产——这也是今天发布会唯一一个有确定日期的特斯拉产品。

 

 意大利炮有了,能不能轰下县城?

 

 特斯拉表示,配合专属的编译器,DOJO 的训练延迟,最低可以做到同等规模 GPU 的 1/50

最终,特斯拉的目标是到 2023 年 Q1 量产时,DOJO 可以实现相比英伟达 A100,最高 4.4 倍的单芯片训练速度——甚至能耗和成本都更低。

 

 三、FSD 的新进化

文章来到这里,大家的手指应该已经划了很多次屏幕。这也说明,看到这里依然兴致勃勃的你,一定是特斯拉老粉——那就聊点更「无聊」、更硬核的吧。

 

 篇幅有限,本届 AI Day 关于 FSD 的进展,我们只聊三个点:Occupancy Network、Training Optimization、Lanes

1. Occupancy Network
先聊一个概念:矢量图。做设计的朋友一定很熟悉,这是一种精度(分辨率)可以做到无限,但占用存储空间很小的数字绘图。
Occupancy Network,就是将 3D 向量数据绘制成矢量图的、 2019 年开始兴起的一种三维重建表达方法。
有意思的是,特斯拉用了最 Occupancy Network 的方式,表达他们对 Occupancy Network 的应用:网格(方块)化的 3D 模拟。
其实 FSD 眼中的世界并不是这样 Minecraft 化的,但 Occupancy Network 的本质特征,就是用「决策边界」描绘「物体边缘」。

 

 尽管 Occupancy Network 效率很高,但实际训练规模依然足够可观。目前特斯拉公布的数据是超过 14.4 亿帧视频数据,需要超过 10 万个 GPU 训练小时,实际视频缓存超过 30PB——而且全程 90℃ 满负载。

 

 二、因此,Training Optimization 训练优化尤为重要。

去年 Andrej 公布了特斯拉的千人 in-house 标注团队,今年特斯拉的重点,则在于优化自动标注流程。

 

 大概总结一下就是,优化过后,训练时视频帧选取会更智能,同时大幅度减少选取的视频帧数量——可以提高 30% 的训练速度

 

 另外视频模型训练时 smol 异步库文件体积可以缩小 11%,所需的读取次数足足缩小到 1/4...最终这套优化流程让特斯拉的 Occupancy Network 训练效率提升了 2.3 倍。

3. 最后聊聊车道线 Lanes。
从 FSD Beta 10.12 开始,几乎每一版更新,车道线和无保护左转,都是更新日志的第一条。

 

 为了更准确高效应对车道线,特斯拉这次「编」了一套「属于车道的语言」。其中包括车道级别的地理几何学和拓扑几何学、车道导航、公交车道计算、多乘员车辆车道计算等等。

 

 最终这套「车道的语言」,可以在小于 10 毫秒的延迟内,思考超过 7500 万个可能影响车辆决策的因素——而且 FSD 硬件「学会」这套语言的代价(功耗),还不足 8W。

 

 四、四十年后,开始圆梦?

写到这里,我真的很头疼。
一方面是我们大部分人,都不是这届 AI Day 的对象——马斯克眼里只有招聘。另一方面,是现在一家汽车公司的发布会,对知识面要求实在太高了。
还是说回马斯克吧,40 年前的他,还是个每天会看 10 个小时科幻小说的小孩子,沉醉于《银河系漫游指南》、《基地》、《严厉的月亮》等等。
但正是这些科幻小说,培养了马斯克冰冷却又宏大的事业观。他会跟你说人类社会生产力的效率可以扩大到无限,他会跟你说人口是维系文明的最重要因素。
所以,当我们把 52 岁的马斯克和 12 岁的马斯克放在一起,你会发现他俩依然在本质上是同一个人。
也正因如此,你看到他如今几乎涉猎了科幻小说所有最热门题材的商业帝国,才会觉得「哦,那很正常」。
希望明年我们能看到更接近现实的马斯克童梦吧。
从NVIDIA自动驾驶芯片Thor,看大芯片的发展趋势
北京时间,9月21凌晨,NVIDIA GTC 2022秋季发布会上,CEO黄仁勋发布了其2024年将推出的自动驾驶芯片。因为其2000TFLOPS的性能过于强大,英伟达索性直接把它全新命名为Thor,代替了之前1000TOPS的Altan。
Thor的发布,代表着在汽车领域,已经由分布式的ECU、DCU转向了完全集中的功能融合型的单芯片。也预示着一个残酷的现实:“许多做DCU级别的ADAS芯片公司,产品还在设计,就已经落后”。
云和边缘计算的数据中心,以及自动驾驶等超级终端领域,都是典型的复杂计算场景,这类场景的计算平台都是典型的大算力芯片。
大芯片的发展趋势已经越来越明显的从GPU、DSA的分离趋势走向DPU、超级终端的再融合,未来会进一步融合成超异构计算宏系统芯片(Macro-SOC)。
1 NVIDIA自动驾驶芯片Thor
1.1 自动驾驶汽车芯片的发展趋势
 

 

 上图是BOSCH给出的汽车电气架构演进示意图。从模块级的ECU到集中相关功能的域控制器,再到完全集中的车载计算机。每个阶段还分了两个子阶段,例如完全集中的车载计算机还包括了本地计算和云端协同两种方式。

 

 

 上图是NVIDIA Altan的芯片架构示意图(Thor刚出来,没有找类似的图),从此图可以看出:Altan&Thor的设计思路是完全的“终局思维”,相比BOSCH给出的一步步的演进还要更近一层,跨越集中式的车载计算机和云端协同的车载计算机,直接到云端融合的车载计算机。云端融合的意思是服务可以动态的、自适应的运行在云或端,方便云端的资源动态调节。Altan&Thor采用的是跟云端完全一致的计算架构:Grace-next CPU、Ampere-next GPU以及Bluefield DPU,硬件上可以做到云端融合。

1.2 Intel Mobileye、高通和NVIDIA芯片算力比较
 

 

 我们可以看到,Mobileye计划2023年发布的用于L4/L5的最高算力的EyeQ Ultra芯片只有176 TOPS。

 

 

 从上图我们可以看到,高通计划的L4/L5自动驾驶芯片是700+TOPS,并且是通过两颗AP和两个专用加速器共四颗芯片组成。

 

 

 再对照NVIDIA Altan,之前计划的用于L4/L5自动驾驶芯片Altan是1000TOPS算力。

 

 

 NVIDIA的王炸!推翻了之前的Altan,直接给了一个全新的命名Thor(雷神索尔),其算力达到了惊人的2000TOPS。

 

 

 NVIDIA Thor发布之后,高通“快速”的发布了自己的4芯片2000TOPS算力的解决方案。

1.3 单芯片实现通常5颗以上芯片的多域计算
 

 

 NVIDIA Thor提供2000TFLOPS的算力(相比较Atlan提供的2000TOPS)。

 

 

 Thor SoC能够实现多域计算,它可以为自动驾驶和车载娱乐划分任务。通常,这些各种类型的功能由分布在车辆各处的数十个控制单元控制。制造商可以利用Thor实现所有功能的融合,来整合整个车辆,而不是依赖这些分布式的ECU/DCU。

 

 

 这种多计算域隔离使得并发的时间敏感的进程可以不间断地运行。通过虚拟化机制,在一台计算机上,可以同时运行Linux、QNX和Android等。

2 自动驾驶SOC和手机SOC的本质区别
这里我们给出一个概念:复杂计算。复杂计算指的是,在传统AP/OS系统之上,还需要支持虚拟化、服务化,实现单设备多系统共存和跨设备多系统协同。因此,如果把AP级别的系统看做一个系统的话,那么复杂计算是很多个系统组成的宏系统。
 

 

 手机、平板、个人电脑等传统AP上部署好操作系统之后,我们在上面运行各种应用软件。整个系统是一个整体,各个具体的进程/线程会存在性能干扰的问题。

但在支持完全硬件虚拟化(包括CPU、内存、I/O、各种加速器等的完全硬件虚拟化)的平台下,不仅仅是要把宏系统切分成多个独立的系统,并且各个系统之间是需要做到应用、数据、性能等方面的物理隔离。
自动驾驶汽车,通常需要支持五个主要的功能域,包括动力域、车身域、自动驾驶域、底盘域、信息娱乐域。因此,集中式的自动驾驶汽车超级终端芯片,必须要实现完全的硬件虚拟化,必须要支持各个功能域的完全隔离(相互不干扰)。
我们把这一类虚拟化和多系统的计算场景称为复杂计算,支持复杂计算的芯片才能算是“大”芯片。这类场景目前主要包括:云计算、超算、边缘计算、5G/6G核心网的数据中心,以及自动驾驶、元宇宙等场景的超级终端。
3 绝对的算力优势面前,定制ASIC/SOC没有意义
随着云计算的发展,随着云网边端不断的协同甚至融合,随着系统的规模越来越庞大,ASIC和传统基于ASIC的SOC的发展道路越来越走向了“死胡同”。越简单的系统,变化越少;越复杂的系统,变化越多。复杂宏系统,必然是快速迭代,并且各个不同的用户有非常多差异性的,传统ASIC的方式在复杂计算场景,必然遇到非常大的困境。
在自动驾驶领域,在不采用加速引擎的情况下,传统的SOC可以把AI算力做到10 TOPS左右;很多公司通过定制加速引擎的方式,快速的提升算力,可以把AI算力提升到100甚至200 TOPS。然而,传统SOC的实现方式有很多问题:
  • 自动驾驶的智能算法以及各类上层应用,一直在快速的演进升级中。定制ASIC的生命周期会很短,因为功能确定,车辆难以更新更先进的系统升级包,这样导致ASIC无法很好的支持车辆全生命周期的功能升级。
  • 整个行业在快速演进,如果未来发展到L4/L5阶段,目前的所有工作就都没有了意义:包括芯片架构、定制ASIC引擎,以及基于此的整个软件堆栈及框架等,都需要推倒重来。
越来越体会到,在大芯片上,做定制ASIC是噩梦;现实的情况,需要是某种程度上软硬件解耦之后的实现通用芯片。只有软硬件解耦之后:硬件人员才能放开手脚,拼命的堆算力;软件人员才能更加专心于自己的算法优化和业务创新,而不需要关心底层硬件细节。
在同样的资源代价下,通用芯片为了实现通用,在性能上存在一定程度的损失。因此,做通用大芯片,也需要创新:
  • 需要创新的架构,实现足够通用的同时,最极致的性能以及性能数量级的提升;
  • 需要实现架构的向前兼容,支持平台化和生态化设计;
  • 需要站在更宏观的视角,实现云网边端架构的统一,才能更好的构建云网边端融合和算力等资源的充分利用。
在绝对的算力优势面前,一切定制芯片方案都没有意义。
4 大芯片的发展趋势:从分离到融合
 

 

 计算机体系结构在从GPU和DSA的分离向融合转变:

  • 第一阶段,CPU单一通用计算平台;
  • 第二阶段,从合到分,CPU+GPU/DSA的异构计算平台;
  • 第三阶段,从分到合的起点,以DPU为中心的异构计算平台;
  • 第四阶段,从分到合,众多异构整合重构的、更高效的超异构融合计算平台。
 

 

 自动驾驶领域已经是Thor这样的功能融合的独立单芯片了,在边缘计算和云计算场景,独立单芯片还会远吗?

在边缘计算等轻量级场景,可以通过功能融合的独立单芯片覆盖;在云计算业务主机等重量级场景,可以通过Chiplet的方式实现功能融合的单芯片。
5 各领域大芯片发展趋势
开门见山,简而言之。大芯片的发展趋势就是:功能融合的、超异构计算架构的单芯片MSoC。
 

 

 上图为基于CPU+GPU的异构计算节点的天河1A超级计算机架构图。

 

 

 E级的天河三依然是异构计算架构。

 

 

 最新TOP500第一名的Frontier,也选择的是基于AMD处理器的异构计算架构(每个节点配备一个 AMD Milan “Trento” 7A53 Epyc CPU 和 四个AMD Instinct MI250X GPU,GPU核心总数达到了37,632)。

 

 

 日本的富岳超算所采用的ARM A64FX处理器,是在常规的ARMv8.2-A指令集的基础上扩展了512Bit的SIMD指令,也可以看做是某种形态上的异构计算。

总结一下,在超算领域,千万亿次、百亿亿次(E级)超算使得异构计算成为主流。下一代超算,是十万亿亿次(Z级),几乎所有的目光都投向了超异构计算。
自动驾驶领域,NVIDIA Drive Thor提供2000TOPS的算力(目前,主流自动驾驶芯片AI算力为100TOPS),Thor之所以能有如此高的算力,跟其内部GPU集成的Tensor Core有很大的关系。Thor是功能融合的单芯片,其架构由集成的CPU、GPU和DPU组成,可以看做是超异构SOC。
在云和边缘服务器侧,CPU、GPU和DSAs融合的趋势也越来越明显,预计未来3年左右,服务器端独立单MSoC芯片(或者说超异构计算芯片)会出现。
6 大芯片需要考虑计算资源的协同和融合
 

 

 大芯片,担负着宏观算力提升的“重任”。

如果计算资源是一个个孤岛,那就没有宏观算力的说法。宏观算力势必需要各个计算节点芯片的协同甚至融合。这就需要考虑计算的跨云网边端。
 

 

 异质的引擎架构越来越多,计算资源池化的难度也越来越高。在超异构计算时代,要想把异质的资源池化,计算需要做到:

  • 维度一:跨同类型处理器架构。如软件可以跨x86、ARM和RISC-v CPU运行。
  • 维度二:跨不同类型处理器架构。软件需要跨CPU、GPU、FPGA和DSA等处理器运行。
  • 维度三:跨不同的芯片平台。如软件可以在Intel、AMD和NVIDIA等不同公司的芯片上运行。
  • 维度四:跨云网边端不同的位置。计算可以根据各种因素的变化,自适应的运行在云网边端最合适的位置。
  • 维度五:跨不同的云网边服务供应商、不同的终端用户、不同的终端设备类型。
参考文献:
https://mp.weixin.qq.com/s/KKJ0hsxvOoIhBgMPMgcEgQ,英伟达发布最强汽车芯!算力2000TOPS,车内计算全包了,车东西
https://mp.weixin.qq.com/s/lA8h9jTtgsPIjYAX3p5cvg,英伟达「史诗级」自动驾驶芯片亮相!算力2000TOPS,兼容座舱娱乐功能,新智驾
OpenCV + Kotlin 实现 USB 摄像头(相机)实时画面、拍照

一. 业务背景

我们团队前段时间做了一款小型的智能硬件,它能够自动拍摄一些商品的图片,这些图片将会出现在电商 App 的详情页并进行展示。
基于以上的背景,我们需要一个业务后台用于发送相应的拍照指令,还需要开发一款软件(上位机)用于接收拍照指令和操作硬件设备。

二. 原先的实现方式以及痛点

早期为了快速实现功能,我们团队使用 JavaCV 调用 USB 摄像头(相机)进行实时画面的展示和拍照。这样的好处在于,能够快速实现产品经理提出的功能,并快速上线。当然,也会遇到一些问题。
我列举几个遇到的问题:
软件体积过大
 
编译速度慢
 
软件运行时占用大量的内存
 
对于获取的实时画面,不利于在软件侧(客户端侧)调用机器学习或者深度学习的库,因为整个软件采用 Java/Kotlin 编写的。
 

三. 使用 OpenCV 进行重构

基于上述的原因,我尝试用 OpenCV 替代 JavaCV 看看能否解决这些问题。
3.1JNI 调用的设计
由于我使用 OpenCV C++ 版本来进行开发,因此在开发之前需要先设计好应用层(我们的软件主要是采用 Java/Kotlin 编写的)如何跟 Native 层进行交互的一些的方法。比如:USB 摄像头(相机)的开启和关闭、拍照、相机相关参数的设置等等。
为此,设计了一个专门用于图像处理的类 WImagesProcess(W 是项目的代号),它包含了上述的方法。
object WImagesProcess {

    init {
        System.load("${FileUtil.loadPath}WImagesProcess.dll")
    }

    /**
     * 
算法的版本号
     */

    external fun getVersion():String

    /**
     * 
获取 OpenCV 对应相机的 index id
     * 
@param pidvid 相机的 pid、vid
     */

    external fun getCameraIndexIdFromPidVid(pidvid:String):Int

    /**
     * 
开启俯拍相机
     * 
@param index 相机的 index id
     * 
@param cameraParaMap 相机相关的参数
     * 
@param listener jni 层给 Java 层的回调
     */

    external fun startTopVideoCapture(index:Int, cameraParaMap:Map<String,String>, listener: VideoCaptureListener)

    /**
     * 
开启侧拍相机
     * 
@param index 相机的 index id
     * 
@param cameraParaMap 相机相关的参数
     * 
@param listener jni 层给 Java 层的回调
     */

    external fun startRightVideoCapture(index:Int, cameraParaMap:Map<String,String>, listener: VideoCaptureListener)

    /**
     * 
调用对应的相机拍摄照片,使用时需要将 IntArray 转换成 BufferedImage
     * 
@param cameraId  1:俯拍相机; 2:侧拍相机
     */

    external fun takePhoto(cameraId:Int): IntArray

    /**
     * 
设置相机的曝光
     * 
@param cameraId  1:俯拍相机; 2:侧拍相机
     */

    external fun exposure(cameraId: Int, value: Double):Double

    /**
     * 
设置相机的亮度
     * 
@param cameraId  1:俯拍相机; 2:侧拍相机
     */

    external fun brightness(cameraId: Int, value: Double):Double

    /**
     * 
设置相机的焦距
     * 
@param cameraId  1:俯拍相机; 2:侧拍相机
     */

    external fun focus(cameraId: Int, value: Double):Double

    /**
     * 
关闭相机,释放相机的资源
     * 
@param cameraId 1:俯拍相机; 2:侧拍相机
     */

    external fun closeVideoCapture(cameraId:Int)
}
其中,VideoCaptureListener 是监听 USB 摄像头(相机)行为的 Listener。
interface VideoCaptureListener {

    /**
     * Native 
层调用相机成功
     */

    fun onSuccess()

    /**
     * jni 
将 Native 层调用相机获取每一帧的 Mat 转换成 IntArray,回调给 Java 层
     * 
@param array 回调给 Java 层的 IntArray,Java 层可以将其转化成 BufferedImage
     */

    fun onRead(array: IntArray)

    /**
     * Native 
层调用相机失败
     */

    fun onFailed()
}
VideoCaptureListener#onRead() 方法是在摄像头(相机)打开后,会实时将每一帧的数据通过回调的形式返回给应用层。
23.2 JNI && Native 层的实现
定义一个 xxx_WImagesProcess.h,它与应用层的 WImagesProcess 类对应。
#include <jni.h>

#ifndef _Include_xxx_WImagesProcess
#define _Include_xxx_WImagesProcess
#ifdef __cplusplus
extern "C" {
#endif

JNIEXPORT jstring JNICALL Java_xxx_WImagesProcess_getVersion
(JNIEnv* env, jobject);

JNIEXPORT void JNICALL Java_xxx_WImagesProcess_startTopVideoCapture
(JNIEnv* env, jobject,int index,jobject cameraParaMap ,jobject listener);

JNIEXPORT void JNICALL Java_xxx_WImagesProcess_startRightVideoCapture
(JNIEnv* env, jobject, int index, jobject cameraParaMap, jobject listener);

JNIEXPORT jintArray JNICALL Java_xxx_WImagesProcess_takePhoto
(JNIEnv* env, jobject, int cameraId);

JNIEXPORT double JNICALL Java_xxx_WImagesProcess_exposure
(JNIEnv* env, jobject, int cameraId,double value);

JNIEXPORT double JNICALL Java_xxx_WImagesProcess_brightness
(JNIEnv* env, jobject, int cameraId, double value);

JNIEXPORT double JNICALL Java_xxx_WImagesProcess_focus
(JNIEnv* env, jobject, int cameraId, double value);

JNIEXPORT void JNICALL Java_xxx_WImagesProcess_closeVideoCapture
(JNIEnv* env, jobject, int cameraId);

JNIEXPORT int JNICALL Java_xxx_WImagesProcess_getCameraIndexIdFromPidVid
(JNIEnv* env, jobject, jstring pidvid);

#ifdef __cplusplus
}
#endif
#endif
#pragma once
xxx 代表的是 Java 项目中 WImagesProcess 类所在的 package 名称。毕竟是公司项目,我不便贴出完整的 package 名称。不熟悉这种写法的,可以参考 JNI 的规范。
接下来,需要定义一个 xxx_WImagesProcess.cpp 用于实现上述的方法。
3.2.1 USB 摄像头(相机)的开启
仅以 startTopVideoCapture() 为例,它的作用是开启智能硬件的俯拍相机,该硬件有 2 款相机介绍其中一种实现方式,另一种也很类似。
JNIEXPORT void JNICALL Java_xxx_WImagesProcess_startTopVideoCapture
(JNIEnv* env, jobject, int index, jobject cameraParaMap, jobject listener){
 jobject topListener = env-> NewLocalRef(listener);

 std::map<stringstring> mapOut;
 JavaHashMapToStlMap(env,cameraParaMap,mapOut);

 jclass listenerClass = env->GetObjectClass(topListener);
 jmethodID successId = env->GetMethodID(listenerClass, "onSuccess""()V");
 jmethodID readId = env->GetMethodID(listenerClass, "onRead""([I)V");
 jmethodID failedId = env->GetMethodID(listenerClass, "onFailed""()V");
 jobject listenerObject = env->NewLocalRef(listenerClass);


 try {
  topVideoCapture = wImageProcess.getVideoCapture(index, mapOut);
  env->CallVoidMethod(listenerObject, successId);

  jintArray jarray;
  topVideoCapture >> topFrame;
  int* data = new int[topFrame.total()];
  int size = topFrame.rows * topFrame.cols;
  jarray = env->NewIntArray(size);

  char r, g, b;

  while (topFlag) {
   topVideoCapture >> topFrame;

   for (int i = 0;i < topFrame.total();i++) {
    r = topFrame.data[3 * i + 2];
    g = topFrame.data[3 * i + 1];
    b = topFrame.data[3 * i + 0];
    data[i] = (((jint)r << 16) & 0x00FF0000) +
     (((jint)g << 8) & 0x0000FF00) + ((jint)b & 0x000000FF);
   }

   env->SetIntArrayRegion(jarray, 0, size, (jint*)data);
   env->CallVoidMethod(listenerObject, readId, jarray);
   waitKey(100);
  }
  topVideoCapture.release();
  env->ReleaseIntArrayElements(jarray, env->GetIntArrayElements(jarray, JNI_FALSE), 0);
  delete []data;
 }
 catch (...) {
  env->CallVoidMethod(listenerObject, failedId);
 }

 env->DeleteLocalRef(listenerObject);
 env->DeleteLocalRef(topListener);
}
这个方法用了很多 JNI 相关的内容,接下来会简单说明。
首先,JavaHashMapToStlMap() 方法用于将 Java 的 HashMap 转换成 C++ STL 的 Map。开启相机时,需要传递相机相关的参数。由于相机需要设置参数很多,因此在应用层使用 HashMap,传递到 JNI 层需要将他们进行转化成 C++ 能用的 Map。
void JavaHashMapToStlMap(JNIEnv* env, jobject hashMap, std::map<stringstring>& mapOut) {
 // Get the Map's entry Set.
 jclass mapClass = env->FindClass("java/util/Map");
 if (mapClass == NULL) {
  return;
 }
 jmethodID entrySet =
  env->GetMethodID(mapClass, "entrySet""()Ljava/util/Set;");
 if (entrySet == NULL) {
  return;
 }
 jobject set = env->CallObjectMethod(hashMap, entrySet);
 if (set == NULL) {
  return;
 }
 // Obtain an iterator over the Set
 jclass setClass = env->FindClass("java/util/Set");
 if (setClass == NULL) {
  return;
 }
 jmethodID iterator =
  env->GetMethodID(setClass, "iterator""()Ljava/util/Iterator;");
 if (iterator == NULL) {
  return;
 }
 jobject iter = env->CallObjectMethod(set, iterator);
 if (iter == NULL) {
  return;
 }
 // Get the Iterator method IDs
 jclass iteratorClass = env->FindClass("java/util/Iterator");
 if (iteratorClass == NULL) {
  return;
 }
 jmethodID hasNext = env->GetMethodID(iteratorClass, "hasNext""()Z");
 if (hasNext == NULL) {
  return;
 }
 jmethodID next =
  env->GetMethodID(iteratorClass, "next""()Ljava/lang/Object;");
 if (next == NULL) {
  return;
 }
 // Get the Entry class method IDs
 jclass entryClass = env->FindClass("java/util/Map$Entry");
 if (entryClass == NULL) {
  return;
 }
 jmethodID getKey =
  env->GetMethodID(entryClass, "getKey""()Ljava/lang/Object;");
 if (getKey == NULL) {
  return;
 }
 jmethodID getValue =
  env->GetMethodID(entryClass, "getValue""()Ljava/lang/Object;");
 if (getValue == NULL) {
  return;
 }
 // Iterate over the entry Set
 while (env->CallBooleanMethod(iter, hasNext)) {
  jobject entry = env->CallObjectMethod(iter, next);
  jstring key = (jstring)env->CallObjectMethod(entry, getKey);
  jstring value = (jstring)env->CallObjectMethod(entry, getValue);
  const char* keyStr = env->GetStringUTFChars(key, NULL);
  if (!keyStr) {
   return;
  }
  const char* valueStr = env->GetStringUTFChars(value, NULL);
  if (!valueStr) {
   env->ReleaseStringUTFChars(key, keyStr);
   return;
  }

  mapOut.insert(std::make_pair(string(keyStr), string(valueStr)));

  env->DeleteLocalRef(entry);
  env->ReleaseStringUTFChars(key, keyStr);
  env->DeleteLocalRef(key);
  env->ReleaseStringUTFChars(value, valueStr);
  env->DeleteLocalRef(value);
 }
}
接下来几行,表示将应用层传递的 VideoCaptureListener 在 JNI 层需要获取其类型。然后,查找 VideoCaptureListener 中的几个方法,便于后面调用。这样 JNI 层就可以跟应用层的 Java/Kotlin 进行交互了。
jclass listenerClass = env->GetObjectClass(topListener);
jmethodID successId = env->GetMethodID(listenerClass, "onSuccess""()V");
jmethodID readId = env->GetMethodID(listenerClass, "onRead""([I)V");
jmethodID failedId = env->GetMethodID(listenerClass, "onFailed""()V");
接下来,开始打开摄像头(相机),并回调给应用层,这样 VideoCaptureListener#onSuccess() 方法就能收到回调。
topVideoCapture = wImageProcess.getVideoCapture(index, mapOut);
env->CallVoidMethod(listenerObject, successId);
打开摄像头(相机)后,就可以实时把获取的每一帧返回给应用层。同样,VideoCaptureListener#onRead() 方法就能收到回调。
  while (topFlag) {
   topVideoCapture >> topFrame;

   for (int i = 0;i < topFrame.total();i++) {
    r = topFrame.data[3 * i + 2];
    g = topFrame.data[3 * i + 1];
    b = topFrame.data[3 * i + 0];
    data[i] = (((jint)r << 16) & 0x00FF0000) +
     (((jint)g << 8) & 0x0000FF00) + ((jint)b & 0x000000FF);
   }

   env->SetIntArrayRegion(jarray, 0, size, (jint*)data);
   env->CallVoidMethod(listenerObject, readId, jarray);
   waitKey(100);
  }
后面的代码是关闭相机,释放资源。
3.2.2 打开相机,设置相机参数
在 3.2.1 中,有以下这样一段代码:
topVideoCapture = wImageProcess.getVideoCapture(index, mapOut);
它的用途是通过 index id 打开对应的相机,并设置相机需要的参数,最后返回 VideoCapture 对象。
VideoCapture WImageProcess::getVideoCapture(int index, std::map<stringstring> cameraParaMap) {
 VideoCapture capture(index);
 
 for (auto & t : cameraParaMap) {
  int key = stoi(t.first);
  double value = stod(t.second);
  capture.set(key, value);
 }

 return capture;
}
对于存在同时调用多个相机的情况,OpenCV 需要基于 index id 来获取对应的相机。那如何获取 index id 呢?以后有机会再写一篇文章吧。
WImagesProcess 类还额外提供了多个方法用于设置相机的曝光、亮度、焦距等。我们在启动相机的时候不是可以通过 HashMap 来传递相机需要的参数嘛,为何还提供这些方法呢?这样做的目的是因为针对不同商品拍照时,可能会调节相机相关的参数,因此 WImagesProcess 类提供了这些方法。
3.2.3 拍照
基于 cameraId 来找到对应的相机进行拍照,并将结果返回给应用层,唯一需要注意的是 C++ 得手动释放资源。
JNIEXPORT jintArray JNICALL Java_xxx_WImagesProcess_takePhoto
(JNIEnv* env, jobject, int cameraId) {

 Mat mat;
 if (cameraId == 1) {
  mat = topFrame;
 }
 else if (cameraId == 2) {
  mat = rightFrame;
 }

 int* data = new int[mat.total()];

 char r, g, b;

 for (int i = 0;i < mat.total();i++) {
  r = mat.data[3 * i + 2];
  g = mat.data[3 * i + 1];
     b = mat.data[3 * i + 0];
  data[i] = (((jint)r << 16) & 0x00FF0000) +
   (((jint)g << 8) & 0x0000FF00) + ((jint)b & 0x000000FF);
 }

 jint* _data = (jint*)data;

 int size = mat.rows * mat.cols;
 jintArray jarray = env->NewIntArray(size);
 env->SetIntArrayRegion(jarray, 0, size, _data);
 delete []data;
 return jarray;
}
最后,将 CV 程序和 JNI 相关的代码最终编译成一个 dll 文件,供软件(上位机)调用,实现最终的需求。
3.3 应用层的调用
上述代码写好后,摄像头(相机)在应用层的打开就非常简单了,大致的代码如下:
val map = HashMap<String,String>()
map[CAP_PROP_FRAME_WIDTH] = 4208.toString()
map[CAP_PROP_FRAME_HEIGHT] = 3120.toString()
map[CAP_PROP_AUTO_EXPOSURE] = 0.25.toString()
map[CAP_PROP_EXPOSURE] = getTopExposure()
map[CAP_PROP_GAIN] = getTopFocus()
map[CAP_PROP_BRIGHTNESS] = getTopBrightness()
WImagesProcess.startTopVideoCapture(index + CAP_DSHOW, map, object : VideoCaptureListener {
     override fun onSuccess() {
             ......
     }

      override fun onRead(array: IntArray) {
             ......
      }

      override fun onFailed() {
             ......
      }
})
应用层的拍照也很简单:
val bufferedImage = WImagesProcess.takePhoto(cameraId).toBufferedImage()
其中,toBufferedImage() 是 Kotlin 的扩展函数。因为 takePhoto() 方法返回 IntArray 对象。
fun IntArray.toBufferedImage():BufferedImage {
    val destImage = BufferedImage(FRAME_WIDTH,FRAME_HEIGHT, BufferedImage.TYPE_INT_RGB)
    destImage.setRGB(0,0,FRAME_WIDTH,FRAME_HEIGHT, this,0,FRAME_WIDTH)
    return destImage
}
这样,对于应用层的调用是非常简单的。

四. 总结

通过 OpenCV 替换 JavaCV 之后,软件遇到的痛点问题基本可以解决。例如软件体积明显变小了。

 

 另外,软件在运行时占用大量内存的情况也得到明显改善。如果需要在展示实时画面时,对图像做一些处理,也可以在 Native 层使用 OpenCV 来处理每一帧,然后将结果返回给应用层。

 
 
参考文献链接
https://mp.weixin.qq.com/s/iTvLUOI3h77xd8-KLbdllg
posted @ 2022-10-02 05:54  吴建明wujianming  阅读(472)  评论(0编辑  收藏  举报