(原)人体姿态识别alphapose

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

(原)⼈体姿态识别alphapose
转载请注明出处:
论⽂
RMPE: Regional Multi-Person Pose Estimation
官⽅代码:
官⽅pytorch代码:
1. 简介
该论⽂指出,定位和识别中不可避免的会出现错误,这些错误会引起单⼈姿态估计(single-person pose estimator,SPPE)的错误,特别是完全依赖⼈体检测的姿态估计算法。

因⽽该论⽂提出了区域姿态估计(Regional Multi-Person Pose Estimation,RMPE)框架。

主要包括symmetric spatial transformer network (SSTN)、Parametric Pose Non-Maximum-Suppression (NMS), 和Pose-Guided Proposals Generator (PGPG)。

并且使⽤symmetric spatial transformer network (SSTN)、deep proposals generator (DPG) 、parametric pose nonmaximum suppression (p-NMS) 三个技术来解决野外场景下多⼈姿态估计问题。

2. 之前算法的问题
2.1检测框定位错误
如下图所⽰。

红框为真实框,黄框为检测到的框(IoU>0.5)。

由于定位错误,黄框得到的热图⽆法检测到关节点
解决⽅法:增⼤训练时的框(框增⼤0.2-0.3倍)
2.2 检测框冗余
如下图所⽰。

同⼀个⼈可能检测到多个框。

解决⽅法:使⽤p-NMS来解决⼈体检测框不准确时的姿态估计问题。

3. ⽹络结构
3.1 总体结构
总体⽹络结构如下图:
Symmetric STN=STN+SPPE+SDTN
STN:空间变换⽹络,对于不准确的输⼊,得到准确的⼈的框。

输⼊候选区域,⽤于获取⾼质量的候选区域。

SPPE:得到估计的姿态。

SDTN:空间逆变换⽹络,将估计的姿态映射回原始的图像坐标。

Pose-NMS:消除额外的估计到的姿态
Parallel SPPE:训练阶段作为额外的正则项,避免陷⼊局部最优,并进⼀步提升SSTN的效果。

包含相同的STN及SPPE(所有参数均被冻结),⽆SDTN。

测试阶段⽆此模块。

PGPG(Pose-guided Proposals Generator):通过PGPG⽹络得到训练图像,⽤来训练SSTN+SPPE模块。

3.2 SSTN
SSTN如下图所⽰。

不准确的输⼊(下图左侧input)经过STN+SPPE+SDTN,先姿态估计,把估计结果映射到原图,以此来调整原本的框,使框变的精准。

其中中间⿊⾊虚线的框认为是准确的输⼊(即中⼼化的输⼊,将姿态对齐到图像中⼼)。

3.3 STN和SDTN
STN为2D的仿射变换,定义如下:
SDTN定义如下:
其中为变换后坐标,为变换前坐标。

{{\theta }_{1}},{{\theta }_{2}},{{\theta }_{3}},{{\gamma }_{1}},{{\gamma }_{2}},{{\gamma }_{3}}为变换参数关系如下:
(使⽤SDTN进⾏反向传播的公式请见论⽂)
3.4 Parallel SPPE(PSPPE)
PSPPE模块和原始的SPPE共享相同的STN参数,但是⽆SDTN模块。

此分⽀的⼈体姿态已经中⼼化,和中⼼化后的真知标签直接⽐较。

训练阶段,PSPPE所有层的参数均被冻结,⽬的是反传中⼼化的姿态误差到STN模块。

因⽽若STN得到的姿态未中⼼化,会产⽣较⼤的误差,使得STN集中于正确的区域。

可以讲PSPPE作为训练阶段额外的正则项。

3.5 P-NMS
定义:令第i个姿态由m个关节点组成,定义为\left\{ \left\langle k_{i}^{1},c_{i}^{1} \right\rangle ,\cdots ,\left\langle k_{i}^{m},c_{i}^{m} \right\rangle \right\},其中k为location,c为socre。

消除过程:score最⾼的姿态作为基准,重复消除接近基准姿态的姿态,直到剩下单⼀的姿态。

消除准则:消除标准⽤于重复消除剩余姿态,为:
f({{P}_{i}},{{P}_{j}}|\Lambda ,\eta )=\mathbf{1}(d({{P}_{i}},{{P}_{j}}|\Lambda ,\lambda )\le \eta )
其中,距离函数d(\centerdot )包括姿态距离和空间距离,若d(\centerdot )不⼤于\eta ,则上⾯f(\centerdot )的输出为1,表明由于{{P}_{i}}和基准姿态{{P}_{j}}过于相似,因
⽽{{P}_{i}}需要被消除。

其定义如下:
d({{P}_{i}},{{P}_{j}}|\Lambda )\text{=}{{K}_{Sim}}({{P}_{i}},{{P}_{j}}|{{\sigma }_{1}})+\lambda {{H}_{sim}}({{P}_{i}},{{P}_{j}}|{{\sigma }_{2}})
其中,\Lambda =\{{{\sigma }_{1}},{{\sigma }_{2}},\lambda \}。

姿态距离⽤于消除和其他姿态太近且太相似的姿态,假定{{P}_{i}}的bbox是{{B}_{i}},其定义为如下的soft matching公式(不同特征之间score的相似度):
其中B(k_{i}^{n})为中⼼在k_{i}^{n}的box,并且每个坐标B(k_{i}^{n})为原始坐标{{B}_{i}}的1/10。

如下图所⽰。

其中蓝框为关节点{{P}_{i}}的框,各⿊点为蓝框{{P}_{i}}各个关节点位置k_{i}^{n}(为了⽅便,只显⽰了4个),各红框为宽⾼为蓝框1/10的⼦框,其中⼼为相应的关节点k_{i}^{n},三⾓为姿态{{P}_{j}}在红框内的关节点k_{j}^{n},五星为姿态{{P}_{j}}在红框外关节点k_{j}^{n}。

进⾏消除时,对三⾓使⽤上式的if进⾏消除,因该点在⼦框内;对五星使⽤otherwise,因该点在⼦框外(左上⾓既有三⾓,⼜有五星。

实际上对于⼀个检测到的姿态{{P}_{j}},是不会出现这种情况的,因为⼀个姿态的某个特定关节点只有⼀个,不会出现三⾓和五星两个关节点。

此处只是显⽰使⽤)。

空间距离⽤于衡量不同特征之间空间距离的相似度,令k_{i}^{n}和k_{j}^{n}为不同特征中⼼,其定义如下:
{{H}_{sim}}({{P}_{i}},{{P}_{j}}|{{\sigma }_{2}})=\sum\limits_{n}{\exp [-\frac{{{(k_{i}^{n}-k_{j}^{n})}^{2}}}{{{\sigma }_{2}}}]}
\lambda 为平衡姿态距离和空间距离的权重。

\eta 为阈值。

上式共四个参数{{\sigma }_{1}},{{\sigma }_{2}},\lambda ,\eta ,论⽂中说交替固定2个,训练另外两个。

但是pytorch代码中全部固定了。

3.6 PGPG
步骤:
1 归⼀化姿态,使得所有躯⼲有归⼀化长度。

2 使⽤kmeans聚类对齐的姿态,并且聚类得到的中⼼形成atomic poses。

3 对有相同atomic poses的⼈,计算gt bbox和detected bbox的偏移。

4 偏移使⽤gt bbox进⾏归⼀化。

5 此时,偏移作为频率的分布,且固定数据为⾼斯混合分布。

对于不同的atomic poses,有不同的⾼斯混合分布的参数。

注:没看此部分对应的代码
4. 代码
4.1 前向推断
⽹络前向推断使⽤InferenNet_fast函数,其中输⼊图像x为通过yolo V3检测到的单张⼈体。

输出为热图。

out.narrow原因是,训练时使⽤了COCO和MPII,因⽽特征维数维33,前17层为COCO特征。

代码中只测试COCO上性能,因⽽只取前17层热图。

1class InferenNet_fast(nn.Module):
2def__init__(self, kernel_size, dataset):
3 super(InferenNet_fast, self).__init__()
4
5 model = createModel().cuda()
6print('Loading pose model from {}'.format('./models/sppe/duc_se.pth'))
7 model.load_state_dict(torch.load('./models/sppe/duc_se.pth'))
8 model.eval()
9 self.pyranet = model # 图像得到33维热图
10 self.dataset = dataset
11
12def forward(self, x):
13 out = self.pyranet(x) # 得到b*33*h*w的矩阵
14# https:///MVIG-SJTU/AlphaPose/issues/187#issuecomment-441416429 指出,代码联合训练COCO和MPII,前17个为COCO,后16个为MPII,故此处取前17层
15 out = out.narrow(1, 0, 17) # data = tensor:narrow(dim, index, size)取出tensor中第dim维上索引从index开始到index+size-1的所有元素存放在data中
16
17return out # 图像得到33维热图,取出channel上0—16维特征
18
19
20def createModel():
21return FastPose()
22
23
24class FastPose(nn.Module):
25 DIM = 128
26
27def__init__(self):
28 super(FastPose, self).__init__()
29 self.preact = SEResnet('resnet101') # 101层SE_ResNet
30 self.suffle1 = nn.PixelShuffle(2) #将Input: (N, C∗upscale_factor * upscale_factor2, H, W)转换成输出Output: (N, C, H∗upscale_factor, W∗upscale_factor),此处upscale_factor=2
31 self.duc1 = DUC(512, 1024, upscale_factor=2) # conv+BN+ReLU+PixelShuffle, PixelShuffle将1024维降低到256维
32 self.duc2 = DUC(256, 512, upscale_factor=2) # conv+BN+ReLU+PixelShuffle, PixelShuffle将512维降低到128维
33 self.conv_out = nn.Conv2d(self.DIM, opt.nClasses, kernel_size=3, stride=1, padding=1) # 128维降低到33维
34
35def forward(self, x: Variable):
36 out = self.preact(x)
37 out = self.suffle1(out)
38 out = self.duc1(out)
39 out = self.duc2(out)
40
41 out = self.conv_out(out)
42return out
43
44
45class DUC(nn.Module):
46'''
47 INPUT: inplanes, planes, upscale_factor
48 OUTPUT: (planes // 4)* ht * wd
49'''
50def__init__(self, inplanes, planes, upscale_factor=2):
51 super(DUC, self).__init__()
52 self.conv = nn.Conv2d(inplanes, planes, kernel_size=3, padding=1, bias=False)
53 self.bn = nn.BatchNorm2d(planes)
54 self.relu = nn.ReLU()
55
56 self.pixel_shuffle = nn.PixelShuffle(upscale_factor) #将Input: (N, C∗upscale_factor * upscale_factor2, H, W)转换成输出Output: (N, C, H∗upscale_factor, W∗upscale_factor) 57
58def forward(self, x):
59 x = self.conv(x)
60 x = self.bn(x)
61 x = self.relu(x)
62 x = self.pixel_shuffle(x)
63return x
View Code
4.2 预测
预测代码如下:
1def getPrediction(hms, pt1, pt2, inpH, inpW, resH, resW): # 由于对⼈体检测后裁剪的图像进⾏预测,后6个参数为裁剪图像的相关信息
2'''Get keypoint location from heatmaps'''
3assert hms.dim() == 4, 'Score maps should be 4-dim'
4# 每个通道最⼤值作为关节点,因为是⾃顶向下,前提就是每张图只有⼀个⼈,因⽽每个通道只有⼀个关节点
5 maxval, idx = torch.max(hms.view(hms.size(0), hms.size(1), -1), 2) # hms.size(0)为batchsize,hms.size(1)为channels,热图中h*w变成⼀维后的最⼤值及索引
6
7 maxval = maxval.view(hms.size(0), hms.size(1), 1) # b*c*1的矩阵
8 idx = idx.view(hms.size(0), hms.size(1), 1) + 1 # b*c*1的矩阵,+1是⽤于防⽌计算xy坐标时错误
9
10 preds = idx.repeat(1, 1, 2).float() # b*c*2的矩阵,将第2维重复⼀遍
11
12 preds[:, :, 0] = (preds[:, :, 0] - 1) % hms.size(3) # 得到x坐标
13 preds[:, :, 1] = torch.floor((preds[:, :, 1] - 1) / hms.size(3)) # 得到y坐标
14
15 pred_mask = maxval.gt(0).repeat(1, 1, 2).float() # 最⼤值中⼤于0的第2维重复⼀遍
16 preds *= pred_mask # 去掉maxval⼩于0对应的坐标
17
18# Very simple post-processing step to improve performance at tight PCK thresholds
19for i in range(preds.size(0)): # 遍历batchsize中每个输⼊的预测
20for j in range(preds.size(1)): # 遍历每个channels
21 hm = hms[i][j] # 当前热图
22 pX, pY = int(round(float(preds[i][j][0]))), int(round(float(preds[i][j][1]))) # 当前坐标
23# 得到热图每个关节点的坐标后,进⼀步结合上下左右四个点,优化坐标(论⽂中没有提到)
24if 0 < pX < opt.outputResW - 1 and 0 < pY < opt.outputResH - 1: # 当前坐标在特征图内
25 diff = torch.Tensor((hm[pY][pX + 1] - hm[pY][pX - 1], hm[pY + 1][pX] - hm[pY - 1][pX])) # 当前热图点右侧减左侧值,当前点热图下边减上边值
26 preds[i][j] += diff.sign() * 0.25 # diff.sign()得到diff每个元素的正负;此处将preds进⾏偏移
27 preds += 0.2 # preds进⼀步偏移??
28
29 preds_tf = torch.zeros(preds.size())
30 preds_tf = transformBoxInvert_batch(preds, pt1, pt2, inpH, inpW, resH, resW) # 热图中关节点坐标映射回原始图像上的坐标
31
32return preds, preds_tf, maxval # 返回关节点在原始图像裁剪后图像上的坐标,在原始图像上的坐标,热图最⼤值
View Code
4.3 P-NMS
p _poseNMS.py配置参数如下(固定的参数,并未体现出通过训练得到):
1 delta1 = 1
2 mu = 1.7
3 delta2 = 2.65
4 gamma = 22.48
5 scoreThreds = 0.3
6 matchThreds = 5
7 areaThres = 0#40 * 40.5
8 alpha = 0.1
9
10 pose_nms如下:
11def pose_nms(bboxes, bbox_scores, pose_preds, pose_scores):
12'''
13 Parametric Pose NMS algorithm
14 bboxes: bbox locations list (n, 4)
15 bbox_scores: bbox scores list (n,) # 各个框为⼈的score
16 pose_preds: pose locations list (n, 17, 2) 各关节点的坐标
17 pose_scores: pose scores list (n, 17, 1) 各个关节点的score
18'''
19#global ori_pose_preds, ori_pose_scores, ref_dists
20
21 pose_scores[pose_scores == 0] = 1e-5
22 final_result = []
23
24 ori_bbox_scores = bbox_scores.clone() # 各个框为⼈的score,下⾯要删除,此处先备份
25 ori_pose_preds = pose_preds.clone() # 各关节点的坐标,下⾯要删除,此处先备份
26 ori_pose_scores = pose_scores.clone() # 各个关节点的score,下⾯要删除,此处先备份 [n, 17, 1]
27
28 xmax = bboxes[:, 2] # 检测到的⼈在原始图像上的坐标
29 xmin = bboxes[:, 0]
30 ymax = bboxes[:, 3]
31 ymin = bboxes[:, 1]
32
33 widths = xmax - xmin # 检测到的⼈的宽⾼
34 heights = ymax - ymin
35 ref_dists = alpha * np.maximum(widths, heights) # alpha=0.1,为论⽂中的1/10,此处为NMS中当前batch各个⼈⼦框的阈值[n,]
36
37 nsamples = bboxes.shape[0]
38 human_scores = pose_scores.mean(dim=1) # 当前batch各个⼈姿态的均值 [n, 1]
39 human_ids = np.arange(nsamples)
40 pick = [] # Do pPose-NMS
41 merge_ids = []
42while(human_scores.shape[0] != 0):
43 pick_id = torch.argmax(human_scores) # Pick the one with highest score 找出分值最⾼的姿态的索引
44 pick.append(human_ids[pick_id]) # 由于后⾯要delete array的部分值,因⽽此处保存索引
45# num_visPart = torch.sum(pose_scores[pick_id] > 0.2)
46
47 ref_dist = ref_dists[human_ids[pick_id]] # Get numbers of match keypoints by calling PCK_match 当前⼈NMS⼦框的阈值
48 simi = get_parametric_distance(pick_id, pose_preds, pose_scores, ref_dist) # 公式(10)的距离,[n],由于每次均会删除id,因⽽n递减
49 num_match_keypoints = PCK_match(pose_preds[pick_id], pose_preds, ref_dist) # 返回满⾜条件的点的数量,[n],由于每次均会删除id,因⽽n递减
50
51# Delete humans who have more than matchThreds keypoints overlap and high similarity # gamma = 22.48,matchThreds = 5,
52 delete_ids = torch.from_numpy(np.arange(human_scores.shape[0]))[(simi > gamma) | (num_match_keypoints >= matchThreds)] # 迭代删除的索引
53
54if delete_ids.shape[0] == 0:
55 delete_ids = pick_id
56#else:
57# delete_ids = torch.from_numpy(delete_ids)
58
59 merge_ids.append(human_ids[delete_ids]) # 每次筛选出来的⼈的索引,如果没有近距离的⼈,merge_ids==pick
60 pose_preds = np.delete(pose_preds, delete_ids, axis=0)
61 pose_scores = np.delete(pose_scores, delete_ids, axis=0)
62 human_ids = np.delete(human_ids, delete_ids)
63 human_scores = np.delete(human_scores, delete_ids, axis=0)
64 bbox_scores = np.delete(bbox_scores, delete_ids, axis=0)
65
66assert len(merge_ids) == len(pick)
67 preds_pick = ori_pose_preds[pick] # 根据pick重新映射后的不同⼈各关节点的坐标
68 scores_pick = ori_pose_scores[pick]
69 bbox_scores_pick = ori_bbox_scores[pick]
70#final_result = pool.map(filter_result, zip(scores_pick, merge_ids, preds_pick, pick, bbox_scores_pick))
71#final_result = [item for item in final_result if item is not None]
72
73for j in range(len(pick)): # ⼈的数量。

此处是当⼈体检测器检测的不好,同⼀个⼈检测到了2个以上的框,这些框⽐较接近的情况
74 ids = np.arange(17)
75 max_score = torch.max(scores_pick[j, ids, 0])
76
77if max_score < scoreThreds:
78continue
79
80 merge_id = merge_ids[j] # Merge poses
81# 返回冗余关节点位置和这些关节点对应的score。

⽆冗余姿态的情况下,merge_pose==preds_pick[j]==ori_pose_preds[merge_id],merge_score==ori_pose_scores[merge_id]
82 merge_pose, merge_score = p_merge_fast(preds_pick[j], ori_pose_preds[merge_id], ori_pose_scores[merge_id], ref_dists[pick[j]])
83
84 max_score = torch.max(merge_score[ids])
85if max_score < scoreThreds:
86continue
87
88 xmax = max(merge_pose[:, 0])
89 xmin = min(merge_pose[:, 0])
90 ymax = max(merge_pose[:, 1])
91 ymin = min(merge_pose[:, 1])
92
93if (1.5 ** 2 * (xmax - xmin) * (ymax - ymin) < areaThres):
94continue
95
96 final_result.append({
97'keypoints': merge_pose - 0.3,
98'kp_score': merge_score,
99'proposal_score': torch.mean(merge_score) + bbox_scores_pick[j] + 1.25 * max(merge_score)
100 })
101
102return final_result
103
104
105
106def PCK_match(pick_pred, all_preds, ref_dist):
107 dist = torch.sqrt(torch.sum(torch.pow(pick_pred[np.newaxis, :] - all_preds, 2), dim=2 )) # 当前点和其他所有点的距离 [n, 17]
108 ref_dist = min(ref_dist, 7)
109 num_match_keypoints = torch.sum(dist / ref_dist <= 1, dim=1) # 得到满⾜条件的点的数量 [n]
110return num_match_keypoints # 返回满⾜条件的点的数量
111
112
113
114def get_parametric_distance(i, all_preds, keypoint_scores, ref_dist):
115 pick_preds = all_preds[i] # 当前预测关节点的坐标
116 pred_scores = keypoint_scores[i] # 当前预测关节点的分值
117 dist = torch.sqrt(torch.sum(torch.pow(pick_preds[np.newaxis, :] - all_preds, 2), dim=2)) # 当前⼈关节点和所有⼈关节点的距离 [n, 17]
118 mask = (dist <= 1) # 当前⼈关节点和所有⼈关节点的mask,此处指如果两套关节点距离太⼩(因是⼆维矩阵,不会出现某⼈部分关节点mask=1),则mask=1,⼀般来说,只是本⼈关节点mask=1 [n, 17] 119
120 score_dists = torch.zeros(all_preds.shape[0], 17) # Define a keypoints distance
121 keypoint_scores.squeeze_()
122if keypoint_scores.dim() == 1:
123 keypoint_scores.unsqueeze_(0) # 增加维度
124if pred_scores.dim() == 1:
125 pred_scores.unsqueeze_(1) # 增加维度
126 pred_scores = pred_scores.repeat(1, all_preds.shape[0]).transpose(0, 1) # The predicted scores are repeated up to do broadcast。

[n, 1]
127
128# 由于broadcast,pred_scores!=keypoint_scores,但是pred_scores[mask] == keypoint_scores[mask]
129 score_dists[mask] = torch.tanh(pred_scores[mask] / delta1) * torch.tanh(keypoint_scores[mask] / delta1) # delta1 = 1,当前点和近距离点的score的相似度,公式(8)
130
131 point_dist = torch.exp((-1) * dist / delta2) # delta2 = 2.65,当前点和近距离点的距离的相似度,公式(9)
132 final_dist = torch.sum(score_dists, dim=1) + mu * torch.sum(point_dist, dim=1) # mu = 1.7,最终的距离 [n]
133
134return final_dist # 返回最终的距离
135
136
137# 如果⼈体检测器效果很好,⽆冗余检测,则此函数⽆效
138def p_merge_fast(ref_pose, cluster_preds, cluster_scores, ref_dist):
139'''
140 Score-weighted pose merging
141 INPUT:
142 ref_pose: reference pose -- [17, 2] ref_pose # 根据pick重新映射后的当前⼈各关节点的坐标
143 cluster_preds: redundant poses -- [n, 17, 2] cluster_preds # 筛选出来的当前⼈各关节点的坐标
144 cluster_scores: redundant poses score -- [n, 17, 1] cluster_scores # 筛选出来的当前⼈各个关节点的score
145 ref_dist: reference scale -- Constant ref_dist # 根据pick重新映射后当前⼈NMS⼦框的阈值
146 OUTPUT:
147 final_pose: merged pose -- [17, 2]
148 final_score: merged score -- [17]
149'''
150# ⽆冗余姿态的情况下,ref_pose==cluster_preds==final_pose,dist=[[0....0]] 17个
151 dist = torch.sqrt(torch.sum(torch.pow(ref_pose[np.newaxis, :] - cluster_preds, 2), dim=2))
152
153 kp_num = 17
154 ref_dist = min(ref_dist, 15)
155
156 mask = (dist <= ref_dist)
157 final_pose = torch.zeros(kp_num, 2)
158 final_score = torch.zeros(kp_num)
159
160if cluster_preds.dim() == 2:
161 cluster_preds.unsqueeze_(0) # [17,2] ==> [1, 17, 2]
162 cluster_scores.unsqueeze_(0) # [17,1] ==> [1, 17, 1]
163if mask.dim() == 1:
164 mask.unsqueeze_(0) # [1,17] ==> [1, 17] 不变
165
166# Weighted Merge
167 masked_scores = cluster_scores.mul(mask.float().unsqueeze(-1)) # [1, 17, 1] 冗余score乘以mask,并进⾏归⼀化
168 normed_scores = masked_scores / torch.sum(masked_scores, dim=0) # [1, 17, 1] 的全1矩阵
169
170# 冗余关节点位置乘归⼀化分数,得到冗余关节点位置。

⽆冗余姿态的情况下,⽆冗余姿态的情况下,ref_pose==cluster_preds==final_pose
171 final_pose = torch.mul(cluster_preds, normed_scores.repeat(1, 1, 2)).sum(dim=0)
172# 归⼀化之前的冗余关节点分数 final_score==cluster_scores==masked_scores
173 final_score = torch.mul(masked_scores, normed_scores).sum(dim=0)
174
175return final_pose, final_score # 返回冗余关节点位置和这些关节点对应的score
View Code
Processing math: 0%。

相关文档
最新文档