AVM系统算法框架搭建的方法

电子说

1.3w人已加入

描述

AVM(Around View Monitor),中文:全景环视系统。AVM已经是一种较为成熟的技术,中高端车型均有部署,但详细讲述AVM系统算法的技术博文并不多。作者在工作中搭建了一套AVM算法框架,有一些效果还不错的demo。本文主要是想将AVM算法框架中每个算子讲述清楚,与大家共同进步。本博文的风格为理论与实践结合,含有部分代码,适合有一些计算机视觉基础的同学。

作者仿真效果 

01  AVM系统概述

AVM汽车环视影像系统如图所示,由安装在前保险杠、后备箱、后视镜上的四个外置鱼眼相机构成。该系统包含的算子按照先后顺序:去畸变、四路鱼眼相机联合标定、投影变换、鸟瞰图微调、拼接融合、3D模型纹理映射等。四路鱼眼捕捉到的图像信息通过上述算子,生成一个2D、3D的全景图。AVM算法又分为离线阶段和在线阶段两部分,在线阶段是对离线阶段的简化,更加适合于工程实现。下面我们来一一讲述。

02  离线阶段算法pipeline

先来粗略浏览下AVM算法Pipeline包含哪些算子:

2DAVM

代码

2D AVM Pipeline

3DAVM

代码

2.1 基于畸变表的鱼眼相机去畸变

2.1.1 鱼眼相机畸变模型

我们知道普通相机和广角相机的投影方式一般为透视投影,即通过三角形相似原理,将相机坐标系下三维世界中的物体投影到平面上,这是基于理想的透视投影模型(无畸变)。但实际情况是我们得到的最终图像与理想的透视投影模型有一些区别,即径向畸变(桶形、枕型)、切向畸变。因此相机标定中都会对畸变做矫正。

鱼眼相机的投影方式有很多种假设,例如等距投影、等立体角投影、正交投影、体视投影、线性投影。但是真实的鱼眼相机镜头并不完全遵循上述的这些模型假设。因此Kannala-Brandt提出了一种一般形式的估计,适用于不同类型的鱼眼相机:

代码

这个也是纳入opencv中的鱼眼相机畸变模型。对照下图:

代码为光线入射角代码为出射光线在相机归一化平面上或者在相机成像平面上与O之间的距离(在opencv中代码表示光线在相机归一化平面上的成像位置)。

代码

鱼眼相机模型

相机去畸变通常使用张正友老师的棋盘格标定方法,首先通过矩阵推导得到一个比较好的初始解,然后通过非线性优化得到最优解,包括相机的内参、外参、畸变系数,然后对鱼眼图像做去畸变处理。内参即:

代码

相机内参矩阵

然而,张正友标定法并不适用于我们的场景。

2.1.2 基于厂家畸变表的鱼眼图像去畸变

由于张正友老师的棋盘格标定法是在图像的全局进行拟合得到一个全局的最优解,因此需要保证多次拍摄到的标定板的棋盘格可以覆盖整个图像区域。

而我们假设的场景为要求汽车整车上流水线进行标定,即相机已经安装在车上。很显然,由于车身遮挡的原因,很难保证上述条件。另外,棋盘格标定法并不适用于批量生产。因此,我们选择了基于厂家提供的畸变表对鱼眼相机图像进行去畸变。相机厂家都有专业的光学工程师,大厂提供的畸变表通常情况下比较准确。当然,也有一些在畸变表的基础上进行优化的方法,例如[2]中采用最小重投影的方法计算出最优的相机主点位置,然后使用畸变表进行去畸变处理。在其他场景中,还有些同学先标定出相机的内参,然后将内参与畸变表联合使用。下面我们来讲述基于畸变表的去畸变方法:

代码

厂家提供的畸变表

上面的表格展示了相机畸变表的一部分,厂家给出了入射角代码从0°到90°的光线在焦距为f的相机真实成像平面上成像点距离成像平面中心的真实距离代码,单位为mm。如果想用opencv提供的API做去畸变处理,需要使用厂家提供的焦距代码,将代码换算到相机的归一化平面上去(即除以代码)。然后通过多项式拟合的方法,计算出代码代码代码代码代码这几个畸变参数,例如可以使用python的curve_fit库进行多项式拟合。调用Opencv API,m_distortion_coeffs即为多项式拟合的畸变参数。

 

fisheye::initUndistortRectifyMap(m_intrinsic_matrix, m_distortion_coeffs, R, NewCoeff, image_size*2, CV_32FC1, mapx, mapy);
cv::remap(disImg, undisImg, mapx, mapy, INTER_LINEAR);

 

通俗讲:鱼眼相机去畸变的过程实际上就是遍历我们想要的无畸变图上的坐标点,通过mapx,mapy两个查找表,找到该坐标点在畸变图上的像素位置。通常这个像素的位置为浮点型,需要做双线性插值。否则在纹理边缘上会有锯齿状的问题,这个结论是作者实现了opencv remap函数验证过的,有兴趣的同学可以自己实现一下mapping的过程(查找+插值)。来看图:

代码

鱼眼图 去畸变

右图为基于畸变表去畸变的结果,可以看出去畸变的效果大体上满足要求,例如柱子边、标定布边、车道线为直线。但是仍有部分区域的去畸变效果不好,直线不够直。这个问题会在鸟瞰图中看起来更加突出,也是导致覆盖区域拼接不齐的重要原因。原因可能有几种:(1)相机光轴与成像平面的交点(主点)与图像平面的中心不重合,即内参矩阵中的代码,代码。(2)厂家给的焦距代码不准(3)厂家给的畸变表有误差。

理论上讲,相机的标定是一个计算全局最优解的过程,可以理解为:我的内参可以不那么准,我拿到的畸变表也可以不那么准,但是只要我的优化目标重投影误差很小,或者畸变去的比较干净,那么这个全局最优解就是可以接受的。因此引用【2】中使用了最小化重投影误差的方法得到内参中,然后再使用畸变表;在有的场景中,还有些同学用棋盘格标定出相机的内参,然后配合畸变表进行使用。这些内容作者后面都会陆续做优化。

2.2 四路鱼眼联合标定

鱼眼相机联合标定的目的是要得到四个鱼眼相机之间的位姿关系,然后将拍摄到的图像搞到同一个坐标系下得到一幅全景环视图。

代码

相机联合标定示意图

如图所示,全景鸟瞰图的视野范围是人为给定的参数,可根据用户喜好进行调节。标定布上的棋盘格大小、黑格子尺寸、汽车与标定布之间的间距这些都是已知的先验信息。上述先验信息在现实世界中与在全景图上的尺度关系为1:1,即1个像素代表1cm(当然这个尺度也可以调节,你想让一个像素代表代码厘米也没问题)。这样做联合标定的意义在于:我们可以知道前、后、左、右四个鱼眼相机去畸变后图像中棋盘格上角点,与前、后、左、右四个鸟瞰图中棋盘格角点之间对应的坐标关系。这样我们就可以根据投影变换,将整张图像投影到对应的鸟瞰图上去。又由于在联合标定中,四个鸟瞰图是刚好拼接到一块的,因此利用上述方法将四张图全部投影到鸟瞰图上,在不考虑误差的理想情况下,应该是刚好拼接在一起的。以上,就是联合标定的思路。

2.3 投影变换

2.3.1 投影变换原理

投影变换的通俗理解就是:假设同一个相机分别在A、B两个不同位置,以不同的位姿拍摄同一个平面(重点是拍摄平面,例如桌面、墙面、地平面),生成了两张图象,这两张图象之间的关系就叫做投影变换。张正友老师的相机标定法使用的就是从标定板平面到图像平面之间的投影模型。

代码

投影变换模型

图中相机从两个不同的角度拍摄同一个代码平面,两个相机拍摄到的图像之间的投影变换矩阵代码(单应矩阵)为:

代码

其中代码为相机内参矩阵,代码为两个相机之间的外参。这个公式怎么推导的网上有很多,我们只需要知道,这个单应矩阵代码内部实际是包含了两个相机之间的位姿关系即可。这也就解释了:为什么有的AVM pipeline的方法是需要标定相机的外参,然后通过厂家提供的相机安装参数将四路鱼眼全部统一到车身坐标系下,而我们不需要这个过程,只需要用标定布来做联合标定。其实两种方法内部都是相通的,都绕不开计算相机外参这件事情。

2.3.2 投影变换生成鸟瞰图

生成鸟瞰图的过程可以理解为:将鱼眼相机拍摄到的图像,投影到某个在汽车上方平行地面拍摄的相机的平面上去。这个单应矩阵代码具体是多少,由去畸变图中检测到的棋盘格角点坐标和联合标定全景图中棋盘格角点坐标来决定。如图所示,以后置相机为例,联合标定已知图(2)中框出棋盘格的坐标,图(1)中的棋盘格坐标可通过opencv的函数进行检测,从而建立单应矩阵H的求解模型。

代码

(1)去畸变图中棋盘格位置 (2)联合标定全景图中棋盘格位置 (3)瞰图

2.3.3 一些经验之谈

a. 尽量选择更多的角点计算单应矩阵

单应矩阵的求解是一个拟合的过程,如果选用过少的点,容易陷入局部最优解。造成的结果是就是鸟瞰图上只有你选择的那些点可以正确的投影,其他像素的投影可能不正确。这一点有点类似于深度学习中训练样本太少,导致过拟合的问题。

代码

单应矩阵三种形式(1)(2)(3)

上面公式可以看出,一对匹配点可以提供两组方程,理论上4对匹配点就可以求解出单应矩阵。Opencv求解单应矩阵提供了两个函数,findHomography和getPerspectiveTransform。

getPerspectiveTransform的输入是4对点,对(2)中矩阵求逆。理想情况下这种方法是可行的,但由于存在噪声,我们在图像上检测到的角点的误差、标定布棋盘格的误差,这种方法极其不准确。

findHomography求单应矩阵的方法输入点对很多,解一个超定方程(3)。经过一顿推导,单应矩阵为(3)中矩阵的奇异值分解中最小奇异值对应的特征向量。这种方法用于做拟合的样本更多,最终的效果更好。而且Opencv还有很多优化算法,例如基于ransac思想的单应矩阵求解方法。当然,为了提高效果,可以对标定布进行DIY,某宝上很多这种DIY标定布,你想搞多少格子就搞多少。

如1.2所述,由于畸变去除的不彻底,导致有些直线仍然是弯曲的。这一现象在投影到鸟瞰图上之后尤为明显,通过大量的棋盘格点进行投影变换,可以从一定程度上强制矫正这个问题。至少可以让车身附近的全景图效果更佳,而我们的avm系统最在意的恰好就是车身周围这部分,距离车身远的部分也不会呈现出来。如图所示为某厂DIY的标定布示意图。

代码

DIY标定布

b. 尽量让棋盘格处于相机拍摄图像的中心

鱼眼相机在中心部分畸变小,边缘位置畸变大。去畸变的结果通常也是中间的效果好,边缘残留的畸变多。因此,为了使单应矩阵计算的更佳准确,我们要保证标定布摆放的时候棋盘格位于鱼眼相机中央。这也是为什么某宝上标定布使用的示意图通常是图(2)这种,而不是图(1)。很显然,图(2)中棋盘格位于左侧后视镜附近(左鱼眼相机就在左后视镜上),即相机图像的中间位置,而图1中棋盘格则在相机图像边缘上。

代码

左侧鱼眼相机鸟瞰图(1)(2)

2.4 拼接融合

经过3中的投影变换,我们得到4张包含重叠区域的鸟瞰图如图所示,需要将这些鸟瞰图进行拼接融合。

代码

鸟瞰图

以左、前鱼眼相机俯视图为例,观察下它们的重叠区域重叠区域:

代码

白色为重叠区域、AB为前鸟瞰图边界、CD为右鸟瞰图边界

通常的做法是分别以AB、CD为边界,计算白色区域像素点与AB、CD之间的距离,然后计算一个权重,距离CD越近的位置,前俯视图权重越大;距离AB越近的位置,左俯视图权重越大。但会出现边界效应如图所示:

代码

前俯视图权重图

其原因也很容易理解:如图所示,将AB、CD延长至O点,在EAOCE这个区域内,使用上述方法计算权重图才是一个完整连续的模型,如果在EABFDCE这个区域内计算权重图相当于把一个完整连续的域强行截断,计算得到的权重图必然是有截断痕迹的

代码

完整、连续模型示意图

因此需要使用某种策略,让我们在一个连续的作用域上计算权重,这里提供一个思路[3]:在EAFCE这个连续的作用域中计算权重,

 

连续模型和权重图

2.5 基于光流的鸟瞰图微调

在整个AVM系统中,厂家提供的畸变表、焦距、相机主点位置,联合标定使用的标定布都会引入误差。这些误差会导致生成的鸟瞰图在重叠区域有一些偏移。第4小节中的拼接融合模块是为了让鸟瞰图在覆盖区域过渡平滑,尽量避免伪影现象。但是我们不能将这个压力全部施加给拼接融合模块。因此需要在拼接融合模块之前,对鸟瞰图进行微调,这个微调功能是供客户或者4S店人员进行手动调节的。

在调研中发现,现在很多部署在车上的AVM系统都包含有微调功能。但大部分都存在一个问题:只能保证一边是对齐的,另外一边拼不齐。例如,前面对齐后面对不齐。

作者使用了[4],将前、后微调光流图进行融合,得到一个平缓过渡的光流图,兼顾了前后两侧的微调。以左俯视图为例,算法流程如下:

固定住前、后两个鸟瞰图

手动微调左鸟瞰图,使左鸟瞰图与前鸟瞰图之间的重叠区域贴合。记录微调矩阵M1,并根据矩阵计算光流map1

手动微调左鸟瞰图,使左鸟瞰图与后鸟瞰图之间的重叠区域贴合。记录微调矩阵M2,并根据矩阵计算光流map2

根据像素距离计算map1与map2的权重图w,即距离前鸟瞰图越近,map1的权重越大,反之则越小

使用w对map1和map2进行加权融合

当微调矩阵M1和M2方向正好相反时,这个基于光流的思想可以很好地将两者融合,因为矩阵变换是比较“硬”的一种数学方法,而光流却像水一样的“软”。

 

左侧为普通的融合效果 右侧为基于光流思想微调后的效果

可以看出这个方法可以兼顾前、后的微调效果。可以理解为将拼接区域的误差均摊给中间的区域,而中间这部分区域不存在拼接融合的问题,而且我们在泊车的过程中更注重的是车与周围物体的相对位置而非周围物体的精确位置,因此实际看上去也没什么问题。

最终的2D AVM 效果展示:

2D AVM Demo

2.6 三维模型纹理映射

2D的AVM算法是基于投影变换,将鱼眼图像投影到鸟瞰图上。而单应变换有一个前提:就是平面假设。是把一个平面投影到另外一个平面。存在的问题是:图像中所有的三维物体,例如汽车、柱子、墙,全部被当成平面来处理。这些内容在鸟瞰图上会被拉的很长

安装在车身周围的鱼眼相机是单目相机,单目相机不能获取三维物体的深度。在图形学中有一种真实感增强的方法:制作一个三维模型,把二维的纹理贴图以某种方式映射到三维模型上,3D AVM正是使用了这个纹理映射技术,为驾驶员呈现出一个伪3D的效果。

2.6.1 AVM 3D模型构建

3D模型是由一个个小面片构成的,可以是三角面片、多边形面片等等。面片又是由多个顶点构成的,例如三角面片对应的就是3个顶点。

 

3dsMax三维模型

放大看可以看到,3D模型是由很多个小的多边形面片构成。3D模型的文件形式有很多种,但大体上都是包含:模型顶点、面片、纹理坐标、法向量这些三维信息。具体如何使用3dsMax来制作3D模型,就不叙述了,作者不是专业的美工,方法可能不太聪明,领会精即可。

AVM 3D模型是一个碗状的三维模型。模拟驾驶员视角,即汽车周围附近是路面,这部分直接映射到碗底平面上;而距离汽车较远的位置可能是楼房、树木、墙等三维物体,这部分内容将使用某种方式映射到三维点上。下面展示的就是我们的3d模型中的必要信息,包含顶点坐标、纹理坐标、法向量、三角面片索引。

 


//顶点坐标
v  166.2457 190.1529 575.8246
v  169.0261 192.6147 575.0482
v  163.5212 194.2559 576.8094
v  160.4214 177.1097 576.3941
v  160.5880 183.6252 577.0156
......
//纹理坐标
vt 0.227618 0.463987
vt 0.254011 0.468448
vt 0.251903 0.470549
vt 0.248436 0.466586
vt 0.267204 0.509296
......
//法向量信息
vn 0.3556 -0.4772 -0.8036
vn 0.3606 -0.4537 -0.8149
vn 0.3145 -0.3999 -0.8609
vn 0.3101 -0.3998 -0.8626
vn 0.3170 -0.3811 -0.8685
......
//三角面片信息
f 5825/5825/4368 5826/5826/4369 5827/5827/4370
f 5828/5828/4371 5829/5829/4372 5830/5830/4373
f 5831/5831/4374 5832/5832/4375 5833/5833/4376
f 5834/5834/4377 5835/5835/4378 5836/5836/4379
f 5837/5837/4380 5838/5838/4381 5839/5839/4382

 

2.6.2 三维模型纹理映射

这一小节讲述的是:

(1)纹理映射是从哪里映射到哪里

(2)采用哪种策略进行映射

我们的最终目标是:找到3d模型上每个顶点对应在鱼眼2d图像上的纹理坐标。

作者采用的是一种基于虚拟相机思想的3d纹理映射方法[5],如图所示:

代码

基于虚拟相机思想的3d纹理映射模型

假设2D AVM的全景鸟瞰图是由汽车正上方某个虚拟相机拍摄到的图,将其当作2D纹理,以透视投影的方法映射到3d模型上面。图中Lw-Rw为全景俯视图,虚拟相机与顶点A的直线在鸟瞰图上的交点为A',从而得到A顶点对应的2D纹理映射坐标A'。然后通过逆投影变换H_inverse、畸变mapx、mapy查找到3d模型的顶点A在鱼眼相机上的纹理坐标。遍历3d模型上的每一个点,即可得到三维模型与鱼眼相机纹理坐标之间的映射关系:

根据透视投影原理,计算顶点A对应的鸟瞰图纹理A'

使用矩阵变换和单应变换逆推A'在去畸变图上的坐标A1

通过去畸变的查找表map查找A1在鱼眼相机畸变图上的坐标

遍历上述过程,即可得到3D模型上所有顶点对应鱼眼图上的纹理坐标

具体流程如图所示:

代码

3d模型纹理映射流程

看下效果:这个模型的法向量是反的,所以渲染的结果光线有问题,非常的暗。不过可以看到伪3D的真实感增强效果,领会精神即可。

代码

右侧模型映射

2.6.3 三维融合

实际上作者使用的是前、后、左、右4个曲面模型,这4个曲面模型与4路鱼眼图像一一对应,这样做是为了增加OpenGl渲染的并行,避免在做拼接融合时用到if、else这些判断语句。上图中的3d模型就是左侧鱼眼相机对应的曲面模型。

我们先来回顾2D AVM的做法:生成鸟瞰图,然后做融合。我们当时生成的鸟瞰图大小为10801080,这个分辨率等同于现实世界中的1080cm1080cm,足以显示车身周围。但是,这个范围最多只能映射到3d碗模型的底部附近区域,如下图:忽略边上的锯齿,那是渲染时插值的bug,先不去管他。从这图可以看出:如果鸟瞰图选的很小,会导致只映射到碗的底部。

代码

小鸟瞰图映射到模型上

我们把鸟瞰图的尺寸放大,可以看到:

代码

大鸟瞰图映射到模型上

图1为左侧鱼眼相机去畸变后的图像,图2是由图1做投影变换得到的鸟瞰图,图3为映射到左侧模型后的结果

这里要注意:在算法实现上不可以像2D AVM那样,去真正地生成一个鸟瞰图。从鸟瞰图我们不难看出,在远离棋盘格的部分被严重拉长,而图1中接近消失点、消失线的那些像素在鸟瞰图上将会被拉到无穷远。可以这样理解,图一中的消失点、消失线表示:在当前的相机位姿,某一个平面上(例如地面)的点全部在这条消失线以下。而鸟瞰图相当于我们把相机平行于地面进行拍摄,那么地平面无穷远处(即1中的消失点、消失线)成像在鸟瞰图中必然会被拉长到无穷远处,就像图2一样。有兴趣的同学可以看看消失点的解释。

 

消失点

如果想要将整个碗状模型填满纹理,需要生成一个特别大的鸟瞰图。即需要计算一个特别大的map,这个计算量是巨大的。因此在算法实现上,要选用遍历模型上的每个顶点,进行逆向纹理映射的方法计算纹理坐标(不再依赖于生成一个鸟瞰图),顶点之间的空缺将由渲染引擎通过插值的方法进行填充,这个是种成熟的技术。

讲了这么多终于说到三维的融合。2D的融合是对鸟瞰图的覆盖区域做形态学操作,得到下图,然后计算权重。而3D的算法强调的是离散点的思维,不会再生成一个超级大的鸟瞰图。换句话说,算法不会再计算一个像下图一样覆盖区域的图像。因此,要寻找其他的方式来解决3D融合的问题。

 

左上角重叠区域

如图所示[6]:

代码

右上方重叠区域示意图

大概思路就是计算3D顶点对应鸟瞰图的纹理坐标B。通过AB与m、l的夹角与θ0计算权重。

当然,重叠区域不可能这么理想,这个论文中的示意图l和m正好交于A点。实际情况是它上面那个图的样子。需要使用某种专门针对3D AVM融合的策略来实现之[7]。将3D模型顶点对应的权重图映射到二维的示意图:

代码

放大看

总体来讲,3D AVM算法就是先搞一个3维模型,然后通过纹理映射,将3维模型上的每一个顶点与二维的纹理图进行绑定。OpenGl利用上述数据进行渲染。最终的3D效果后续会发上来。

03  在线阶段工程实现pipeline

前面介绍的是离线阶段的算法流程,离线阶段只有在流水线上或者4s店才会用到,是一个初始化的过程。初始化的内容包括:畸变表、投影变换矩阵、纹理映射关系、拼接融合权重图等。最重要的是要将去畸变、投影变换、纹理映射这些过程写入一个查找表,存入内存,在线处理的时候直接调用即可。附上部分代码,对map做remap这块可能会稍微难理解一些。

 


//4个label是鸟瞰图在avm全景上的位置坐标
for (int i = label1; i < label2; i++)
{
    float *map2_x = map2_xR.ptr(i);
    float *map2_y = map2_yR.ptr(i); 
    for (int j = label3; j < label4; j++)
    {
        Mat vec = (Mat_(3, 1) << j, i, 1);//AVM全景图的grid网格坐标
        vec = matrix * (vec);//获取鸟瞰图坐标
        Mat coor = Homo_inverse * vec;//从鸟瞰图反向投影到去畸变图
        map2_x[j] = coor.at(0, 0);
        map2_y[j] = coor.at(1, 0);
    }
}
//map1(畸变) remap map2(投影+旋转)
remap(map1_y, my, map2_xR, map2_yR, INTER_LINEAR);
remap(map1_x, mx, map2_xR, map2_yR, INTER_LINEAR);


//畸变+投影+旋转+finetune
if (finetune)
{
    remap(mx, mx, m_finetune_l_blendX, m_finetune_l_blendY, INTER_LINEAR, BORDER_REPLICATE);
    remap(my, my, m_finetune_l_blendX, m_finetune_l_blendY, INTER_LINEAR, BORDER_REPLICATE);
}
9.1 2D AVM

 

代码

2D AVM Pipeline

9.2 3D AVM

代码

3D AVM Pipeline

04  其他

另外还有一种做法:通过厂家提供的相机安装参数计算鱼眼相机与同意坐标系(汽车中心)之间的外参,通过外参将3d模型上的顶点坐标转换到相机坐标系下,再通过相机内参转换到图像坐标系上。用此方法同样可以得到2d图像纹理与3d模型顶点之间的一一映射关系。这两种方法的基本思想其实是相通的,殊途同归。只不过这种方法相机安装参数会有一些误差,可能会导致最终的3d拼接效果不佳。

05  总结

AVM2D、3D全景环视是一个需要算法理论和实践强结合的自动驾驶系统,其中涉及到的领域为计算机视觉、图像增强、三维等。后续作者还会对标定、去畸变等算子进行优化,并加入自标定、车轮视角、广角、透明底盘等内容。

本文的每个章节都是首先讲述基础理论,再结合实验demo来进行呈现,尽量避免繁琐的公式推导,目的在于将AVM系统算法框架搭建的方法讲述清楚。如果有不清楚或者哪里说的不够严谨,欢迎大家一起交流进步。

  
      审核编辑:彭静
打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分