zoukankan      html  css  js  c++  java
  • (原)人体姿态识别alphapose

    转载请注明出处:

    https://www.cnblogs.com/darkknightzh/p/12150171.html

    论文

    RMPE: Regional Multi-Person Pose Estimation

    https://arxiv.org/abs/1612.00137

    官方代码:

    https://github.com/MVIG-SJTU/AlphaPose

    官方pytorch代码:

    https://github.com/MVIG-SJTU/AlphaPose/tree/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定义如下:

    其中为变换后坐标,为变换前坐标。${{ heta }_{1}}$,${{ heta }_{2}}$,${{ heta }_{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{ leftlangle k_{i}^{1},c_{i}^{1} ight angle ,cdots ,leftlangle k_{i}^{m},c_{i}^{m} ight angle  ight}$,其中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 ) ext{=}{{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}}$的框,红框为宽高为蓝框1/10的子框,黑点为蓝框中心,即关节点${{P}_{i}}$的中心${{k}_{i}}$,三角为在红框内关节点${{P}_{j}}$中心${{k}_{j}}$,五星为在红框外关节点${{P}_{j}}$中心${{k}_{j}}$。则进行消除时,对三角使用上式的if进行消除,因该点在子框内;对五星使用otherwise,因该点在子框外。

    空间距离用于衡量不同特征之间空间距离的相似度,令$k_{i}^{n}$和$k_{j}^{n}$为不同特征中心,其定义如下:

    ${{H}_{sim}}({{P}_{i}},{{P}_{j}}|{{sigma }_{2}})=sumlimits_{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层热图。

     1 class InferenNet_fast(nn.Module):
     2     def __init__(self, kernel_size, dataset):
     3         super(InferenNet_fast, self).__init__()
     4 
     5         model = createModel().cuda()
     6         print('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 
    12     def forward(self, x):
    13         out = self.pyranet(x)   # 得到b*33*h*w的矩阵
    14         # https://github.com/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 
    17         return out   # 图像得到33维热图,取出channel上0—16维特征
    18 
    19 
    20 def createModel():
    21     return FastPose()
    22 
    23 
    24 class FastPose(nn.Module):
    25     DIM = 128
    26 
    27     def __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 
    35     def 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)
    42         return out
    43 
    44 
    45 class DUC(nn.Module):
    46     '''
    47     INPUT: inplanes, planes, upscale_factor
    48     OUTPUT: (planes // 4)* ht * wd
    49     '''
    50     def __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 
    58     def forward(self, x):
    59         x = self.conv(x)
    60         x = self.bn(x)
    61         x = self.relu(x)
    62         x = self.pixel_shuffle(x)
    63         return x
    View Code

    4.2 预测

    预测代码如下:

     1 def getPrediction(hms, pt1, pt2, inpH, inpW, resH, resW):  # 由于对人体检测后裁剪的图像进行预测,后6个参数为裁剪图像的相关信息
     2     '''Get keypoint location from heatmaps'''
     3     assert 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
    19     for i in range(preds.size(0)):        # 遍历batchsize中每个输入的预测
    20         for 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             # 得到热图每个关节点的坐标后,进一步结合上下左右四个点,优化坐标(论文中没有提到)
    24             if 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 
    32     return 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如下:
     11 def 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 = []
     42     while(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 
     54         if 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 
     66     assert 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 
     73     for j in range(len(pick)):   # 人的数量。此处是当人体检测器检测的不好,同一个人检测到了2个以上的框,这些框比较接近的情况
     74         ids = np.arange(17)
     75         max_score = torch.max(scores_pick[j, ids, 0])
     76 
     77         if max_score < scoreThreds:
     78             continue
     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])
     85         if max_score < scoreThreds:
     86             continue
     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 
     93         if (1.5 ** 2 * (xmax - xmin) * (ymax - ymin) < areaThres):
     94             continue
     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 
    102     return final_result
    103 
    104 
    105 
    106 def 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]
    110     return num_match_keypoints    # 返回满足条件的点的数量
    111 
    112 
    113 
    114 def 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_()
    122     if keypoint_scores.dim() == 1:
    123         keypoint_scores.unsqueeze_(0)  # 增加维度
    124     if 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 
    134     return final_dist   # 返回最终的距离
    135 
    136 
    137 # 如果人体检测器效果很好,无冗余检测,则此函数无效
    138 def 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 
    160     if cluster_preds.dim() == 2:
    161         cluster_preds.unsqueeze_(0)    # [17,2] ==> [1, 17, 2]
    162         cluster_scores.unsqueeze_(0)   # [17,1] ==> [1, 17, 1]
    163     if 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 
    175     return final_pose, final_score   # 返回冗余关节点位置和这些关节点对应的score
    View Code
  • 相关阅读:
    页面打印
    scala
    IntelliJ Idea 常用快捷键列表
    flume
    spring事务管理方式,aop
    oldboy es和logstash
    elasticsearch视频34季
    elasticsearch视频
    python3
    git
  • 原文地址:https://www.cnblogs.com/darkknightzh/p/12150171.html
Copyright © 2011-2022 走看看