首页 > 编程语言 >3D点云-Pointnet++模型解读(附源码+论文)

3D点云-Pointnet++模型解读(附源码+论文)

时间:2024-12-04 18:32:39浏览次数:10  
标签:采样 ++ xyz Pointnet 源码 128 new self points

3D点云-Pointnet++模型

代码链接:pointnet2-pytorch-study(关键部分代码注释详细,参考Pointnet_Pointnet2_pytorch

论文链接:PointNet++: Deep Hierarchical Feature Learning on Point Sets in a Metric Space

官方链接:pointnet2(源码基于TensorFlow)

公开3D点云数据集:

modelnet40_normal_resampled(分类任务)

shapenetcore_partanno_segmentation_benchmark_v0_normal(部位分割任务)

Stanford3dDataset_v1.2_Aligned_Version(语义分割任务)

工具:CloudCopmare

给大家先看一下数据集,这是一个3D点云的飞机,应该能看出来吧。它是由很多个点组成的3维数据,与平时图像、视频这些2D数据不一样。这张图大家应该能感受到点云的无序性。点x1与点x2互换了位置,对数据没有任何影响,点的排列顺序不影响其几何结构。
在这里插入图片描述

我们再看一下部位分割任务的数据。应该能看出来是把枪吧,我给倒过来了,因为蓝色和背景重在一起看不清。不同的颜色代表不同的位置。

在这里插入图片描述

可以看一下数据的组成。由6个指标组成(x, y, z, Nx, Ny, Nz),分别表示三维坐标信息和三个法向量信息。

在这里插入图片描述

总体流程

我先简单的梳理一下流程,然后再根据代码详细讲解。

最前面是一个特征提取层,我们输入一个3D点云数据,采样一些中心点,可以理解为用这些中心点来代替原来的数据。以中心点画圈(当然这是立体的,球状的),获取圈内点数据,然后做特征提取,再画圈再提取,可以看到点的数量一层一层减少。

如果做分类任务就连几个全连接层就好了,最后输出k,k为分类数量。

如果做分割任务,需要对每一个点做分类,而我们特征提取最后一层里都没啥点了,所以需要进行上采样,将点的数量还原回原来的数量。在上采样过程中,我们可以看到上面那条虚线,这是对之前的数据做了一个拼接操作。最后也连个全连接层输出k个类别。这里的k个类别和分类不一样哦,比如分类任务只判别这是飞机不,而分割任务判别的是这是机翼不。
在这里插入图片描述
假设我们在原始点数据里找7个中心点,然后按半径画一个圈,获取这些圈里m个点的数据(获取的点数是固定的),可以将这些半径内的点数据理解为一个簇。

这里就会有一个问题,获取点的数量是固定,如果这个圈里没有这么多点呢?又或者这个圈里不止这么多点呢?点多的好解决,我们直接距离排序,取离中心点近的那些点就好了。那点不够的怎么办?我们可以将离中心点最近的那个点复制,复制到足够我们的需求。

为了让特征更丰富,同一个采样点通过不同的“视野”(半径大小)去采集周围的点,从而捕获不同尺度的几何特征。对一个采样点设定多个不同的半径,每个半径的邻域包含不同数量的点,形成不同尺度的局部区域。比如,半径[0.1, 0.2, 0.4] ,对应簇的样本个数[16, 32, 128]。最后,分别对每个尺度区域提取特征,然后把这些特征拼接在一起。这个思想就是下图的(a),MSG多尺度分组。下图的(b),MRG多分辨率分组,类似用高清和低清两个版本的照片分别描述局部特征,高清抓细节,低清看全局,最后合成。因为我没用这个方法所以不做多解释。
在这里插入图片描述

下图看一下这两种方法的对比。如果不做任何处理,可以看到当采样的点的数量减少时,数据的准确率跟瀑布一样往下掉,而MSG和MRG方法可以有效避免。

在这里插入图片描述

那这些中心点是怎么来的?

第一个中心点c1我们是随机生成的,然后计算每个点离这个中心点的距离,选择距离最远的那个作为下一个中心点c2。这时候再计算所有点离c1,c2的距离,取最小的作为距离值。比如有一个点与c1距离2,与c2距离3,那么它的距离值为2,取最小的那个值。然后找到最大距离值的那个点作为下一个中心点c3。这就是最远点采样

网络模型部分我放在代码里说。

代码

读取数据

def _get_item(self, index):
    if self.process_data:
        point_set, label = self.list_of_points[index], self.list_of_labels[index]
    else:
        fn = self.datapath[index]  # 存储路径
        cls = self.classes[self.datapath[index][0]]  # 类别
        label = np.array([cls]).astype(np.int32)  # 标签
        point_set = np.loadtxt(fn[1], delimiter=',').astype(np.float32)  # 坐标点 (x,y,z,Nx,Ny,Nz) 位置信息+法向量信息

        if self.uniform:
            point_set = farthest_point_sample(point_set, self.npoints)  # 最远点采样
        else:
            point_set = point_set[0:self.npoints, :]

    point_set[:, 0:3] = pc_normalize(point_set[:, 0:3])  # 对xyz标准化
    if not self.use_normals:
        point_set = point_set[:, 0:3]

    return point_set, label[0]

随机传入一个index,比如读取第10个飞机的数据,然后获取数据的路径,它的类别,类别对应的标签(飞机),获取它所有的坐标数据,然后取样固定数量的点,这些点来代替原来整个飞机的数据。

所有的数据构成的点数是不一样的,比如一架飞机有10000个点,一个椅子有5000个点,那这个输入不一样怎么办,我们会采样固定个值来代替全部的点。比如说我们这个飞机有10000个点,我们只提取1024个点,用这个1024个点来表示原来的飞机。

分类网络模型架构

我这里讲的是分类任务,在pointnet2_cls_msg.py中。

class get_model(nn.Module):
    def __init__(self, num_class, normal_channel=True):
        super(get_model, self).__init__()
        in_channel = 3 if normal_channel else 0
        self.normal_channel = normal_channel  # 中心采样点数量、中心采样点半径、半径内采集数量
        self.sa1 = PointNetSetAbstractionMsg(512, [0.1, 0.2, 0.4], [16, 32, 128], in_channel,
                                             [[32, 32, 64], [64, 64, 128], [64, 96, 128]])
        self.sa2 = PointNetSetAbstractionMsg(128, [0.2, 0.4, 0.8], [32, 64, 128], 320,
                                             [[64, 64, 128], [128, 128, 256], [128, 128, 256]])
        self.sa3 = PointNetSetAbstraction(None, None, None, 640 + 3, [256, 512, 1024], True)
        self.fc1 = nn.Linear(1024, 512)
        self.bn1 = nn.BatchNorm1d(512)
        self.drop1 = nn.Dropout(0.4)
        self.fc2 = nn.Linear(512, 256)
        self.bn2 = nn.BatchNorm1d(256)
        self.drop2 = nn.Dropout(0.5)
        self.fc3 = nn.Linear(256, num_class)

    def forward(self, xyz):
        B, _, _ = xyz.shape
        if self.normal_channel:
            norm = xyz[:, 3:, :]
            xyz = xyz[:, :3, :]
        else:
            norm = None
        l1_xyz, l1_points = self.sa1(xyz, norm)
        l2_xyz, l2_points = self.sa2(l1_xyz, l1_points)
        l3_xyz, l3_points = self.sa3(l2_xyz, l2_points)
        x = l3_points.view(B, 1024)
        x = self.drop1(F.relu(self.bn1(self.fc1(x))))
        x = self.drop2(F.relu(self.bn2(self.fc2(x))))
        x = self.fc3(x)
        x = F.log_softmax(x, -1)

        return x, l3_points

xyz放的就是三维位置信息,norm放的是三个法向量信息 。self.sa1self.sa2对应的都是PointNetSetAbstractionMsg(),这是一个最远点采样的函数,我下面详说。可以观察到,中心采样点不是一蹴而就的,而是先从1024个点里采样512个,然后再从512个点里采样128个,有一个循序渐进的过程。self.sa3就是通过卷积特征提取,最后通过max得到最终的特征,我下面会详说。最后经过几个全连接、BN、Relu和drop,都是简单的网络结构不多说,输出num_class(分类数量)个结果,做一个softmax得到每个结果的概率。

帮助理解,变量里有xyz的都代表包含位置信息,比如l1_xyz。变量里有point的都是对xyzNxNyNz数据进行升维后的结果,比如l1_points。为什么要对数据升维呢,你可以这样想。你最后要做分类任务,那么网络需要对你输入的数据进行判断属于哪个类别。如果你输入的数据只有6个,那有点难为网络了,信息量太少了,而你升维后(特征提取)数据变为1024个,那信息量充足了,网络就更容易判断了。

最远点采样

这里就是上面的self.sa1self.sa2对应的PointNetSetAbstractionMsg()

def forward(self, xyz, points):
    xyz = xyz.permute(0, 2, 1)
    if points is not None:
        points = points.permute(0, 2, 1)  # 第一次是三个法向量特征 第二次是额外提取的特征

    B, N, C = xyz.shape
    S = self.npoint  # 从 N 个点中暂时选 S 个点作为中心点
    new_xyz = index_points(xyz, farthest_point_sample(xyz, S))  # 通过最远点采样选择中心点
    new_points_list = []
    for i, radius in enumerate(self.radius_list):
        K = self.nsample_list[i]
        group_idx = query_ball_point(radius, K, xyz, new_xyz)  # 获得中心采样点半径内的点索引
        grouped_xyz = index_points(xyz, group_idx)  # 通过索引得到点的xyz
        grouped_xyz -= new_xyz.view(B, S, 1, C)  # 去mean 每个点减去中心点的值
        if points is not None:
            grouped_points = index_points(points, group_idx)
            grouped_points = torch.cat([grouped_points, grouped_xyz], dim=-1)  # 拼接位置特征和额外特征
        else:
            grouped_points = grouped_xyz

        grouped_points = grouped_points.permute(0, 3, 2, 1)  # [B, D, K, S]
        for j in range(len(self.conv_blocks[i])):
            conv = self.conv_blocks[i][j]
            bn = self.bn_blocks[i][j]
            grouped_points = F.relu(bn(conv(grouped_points)))
        new_points = torch.max(grouped_points, 2)[0]  # [B, D', S] 取最大的特征维度
        new_points_list.append(new_points)  # 最终获得每个中心采样点的特征

    new_xyz = new_xyz.permute(0, 2, 1)
    new_points_concat = torch.cat(new_points_list, dim=1)  # 将三种半径的数据拼接一块
    return new_xyz, new_points_concat

在第一次进入这个函数的时候,point传入的是norm,是三个法向量的数据。后面会将这三个法向量数据升维,映射更多的数据。

我们现在要从 N 个点中选 S 个点作为中心点。如果是第一步self.sa1,那就是从1024个点离采样512个点。我们来看看这个函数farthest_point_sample()

def farthest_point_sample(xyz, npoint):
    device = xyz.device
    B, N, C = xyz.shape
    centroids = torch.zeros(B, npoint, dtype=torch.long).to(device)
    distance = torch.ones(B, N).to(device) * 1e10
    farthest = torch.randint(0, N, (B,), dtype=torch.long).to(device)  # 第一个点是随机选择的
    batch_indices = torch.arange(B, dtype=torch.long).to(device)
    for i in range(npoint):
        centroids[:, i] = farthest  # 采样点索引
        centroid = xyz[batch_indices, farthest, :].view(B, 1, 3)  # 根据索引获得采样点xyz
        dist = torch.sum((xyz - centroid) ** 2, -1)  # 计算所有点到当前采样点的距离
        mask = dist < distance  # 比如一个点距离采样点1,采样点2,采样点3的距离分别是d1>d2>d3,那么选最小的那个值d3作为采样点距离
        distance[mask] = dist[mask]  # 小于了mask会变为True, 用最小值替换原来的值
        farthest = torch.max(distance, -1)[1]  # 选择最远的那个点作为采样点
    return centroids

centroids存放中心采样点的索引,distance存放每个点离每个中心采样点的最短距离。

我们的第一个中心采样点是随机选的,后面会计算每个点到中心采样点的距离不断更新distance。比如一开始只有一个中心采样点c1,有一个点与c1距离5,那么它在distance里的值就为5。后面又来了一个新的中心采样点c2,与c2距离3,那么它在distance里的值就要更新为3。后面又来了一个新的中心采样点c3,与c3距离4,那就不更新了。

通过不断更新distance,选择里面最大的,也就是距离最远的作为新的中心采样点,最后返回一个包含npoint个的中心采样点索引centroids

===================================================================================

回到PointNetSetAbstractionMsg()中,继续看。

farthest_point_sample()返回的中心采样点索引,我们通过index_points()来获得索引对应的点位置信息xyz。现在我们进入一个for循环,来遍历采样半径。之前有说过,我们需要画多个圈来获取更多的信息,提取更多的特征。

为了让问题具体化一点,我假设一下数值。query_ball_point(radius, K, xyz, new_xyz),我们的原始数据为xyz(1024个点),中心点数据为new_xyz(512个中心点)。现在我们要对512个中心点,以半径radius(0.1)为范围,搜刮K(16)个点,作为我们簇的数据信息。现在我们进query_ball_point()函数看看。

def query_ball_point(radius, nsample, xyz, new_xyz):
    device = xyz.device
    B, N, C = xyz.shape
    _, S, _ = new_xyz.shape
    group_idx = torch.arange(N, dtype=torch.long).to(device).view(1, 1, N).repeat([B, S, 1])
    sqrdists = square_distance(new_xyz, xyz)  # 计算每个点到中心采样点的距离
    group_idx[sqrdists > radius ** 2] = N  # 将距离大于半径的点设置为一个较大的特定值
    group_idx = group_idx.sort(dim=-1)[0][:, :, :nsample]
    group_first = group_idx[:, :, 0].view(B, S, 1).repeat([1, 1, nsample])  # 距离最近的那个点复制多份用来代替不足的样本
    mask = group_idx == N  # 将大于半径的点抹掉
    group_idx[mask] = group_first[mask]
    return group_idx

square_distance(new_xyz, xyz)就不看了,就是一个距离公式,计算每个点到中心点的距离。对于距离大于半径的数据mask掉,只看在半径内的数据。对距离排个序,只取前nsample个数据,而对于不足nsample个数据的情况,将距离最近的那个点复制多份用来代替不足的样本。最后会返回这些簇的信息(B,S,nsample),包含的也是点的索引。

===================================================================================

回到PointNetSetAbstractionMsg()中,继续看。

我们还是通过index_points()获得这些簇里点的xyz位置信息grouped_xyz。代码里grouped_xyz减去了new_xyz,相当于一个取均值的操作的,每个点以它们簇的中心点为中心。再通过index_points()获得这些簇里点的NxNyNz信息,将其与xyz信息拼接,对这6个特征信息做升维(特征提取)操作。

代码里可以看到,通过for循环做卷积卷积卷积。举个例子,一开始特征数量是6,做了三层卷积6->32->32->64。然后用max提取最大的特征,其实用maxpool一样,但是代码里用的max,我下面画了一张图帮大家理解。最后new_points_list会存放三种半径获得的特征数据,然后将这三种半径的特征数据拼接在一起,大功告成,返回中心点位置数据和升维后的数据。

在这里插入图片描述

预测结果输出模块

输出模块就很简单了,简单讲讲。先跳到self.sa3,也就是PointNetSetAbstraction()函数。

def forward(self, xyz, points):
    xyz = xyz.permute(0, 2, 1)
    if points is not None:
        points = points.permute(0, 2, 1)

    if self.group_all:
        new_xyz, new_points = sample_and_group_all(xyz, points)
    else:
        new_xyz, new_points = sample_and_group(self.npoint, self.radius, self.nsample, xyz, points)
    # new_xyz: sampled points position data, [B, npoint, C]
    # new_points: sampled points data, [B, npoint, nsample, C+D]
    new_points = new_points.permute(0, 3, 2, 1)  # [B, C+D, nsample,npoint]
    for i, conv in enumerate(self.mlp_convs):
        bn = self.mlp_bns[i]
        new_points = F.relu(bn(conv(new_points)))

    new_points = torch.max(new_points, 2)[0]
    new_xyz = new_xyz.permute(0, 2, 1)
    return new_xyz, new_points

xyz就是最后得到的128个中心点的位置信息,而points是升维后的信息。如果没有改参数的话,这里的points的shape为(B,128,640)sample_and_group_all()将128个数据当成1个组,返回的new_xyz是一个空的(B,1,3)的向量,new_points是points拼接了xyz信息的向量(B,1,128,643)。做一个permute(0, 3, 2, 1)是因为卷积时需要将特征信息放在第一维(B,643,128,1)

通过for循环做卷积卷积卷积,643->256->512->1024,最终将数据还原为1024。最后通过max提取最大的那个中心点特征返回。

最后的部分我在网络模型架构里讲过了,基本的分类网络结构我就不说了。

部位分割网络模型架构

我这里讲的是部位分割任务,在pointnet2_part_seg_msg.py中。

class get_model(nn.Module):
    def __init__(self, num_classes, normal_channel=False):
        super(get_model, self).__init__()
        if normal_channel:
            additional_channel = 3
        else:
            additional_channel = 0
        self.normal_channel = normal_channel
        self.sa1 = PointNetSetAbstractionMsg(512, [0.1, 0.2, 0.4], [32, 64, 128], 3 + additional_channel,
                                             [[32, 32, 64], [64, 64, 128], [64, 96, 128]])
        self.sa2 = PointNetSetAbstractionMsg(128, [0.4, 0.8], [64, 128], 128 + 128 + 64,
                                             [[128, 128, 256], [128, 196, 256]])
        self.sa3 = PointNetSetAbstraction(npoint=None, radius=None, nsample=None, in_channel=512 + 3,
                                          mlp=[256, 512, 1024], group_all=True)
        self.fp3 = PointNetFeaturePropagation(in_channel=1536, mlp=[256, 256])
        self.fp2 = PointNetFeaturePropagation(in_channel=576, mlp=[256, 128])
        self.fp1 = PointNetFeaturePropagation(in_channel=150 + additional_channel, mlp=[128, 128])
        self.conv1 = nn.Conv1d(128, 128, 1)
        self.bn1 = nn.BatchNorm1d(128)
        self.drop1 = nn.Dropout(0.5)
        self.conv2 = nn.Conv1d(128, num_classes, 1)

    def forward(self, xyz, cls_label):
        # Set Abstraction layers
        B, C, N = xyz.shape
        if self.normal_channel:
            l0_points = xyz
            l0_xyz = xyz[:, :3, :]
        else:
            l0_points = xyz
            l0_xyz = xyz
        l1_xyz, l1_points = self.sa1(l0_xyz, l0_points)
        l2_xyz, l2_points = self.sa2(l1_xyz, l1_points)
        l3_xyz, l3_points = self.sa3(l2_xyz, l2_points)
        # Feature Propagation layers
        l2_points = self.fp3(l2_xyz, l3_xyz, l2_points, l3_points)
        l1_points = self.fp2(l1_xyz, l2_xyz, l1_points, l2_points)
        cls_label_one_hot = cls_label.view(B, 16, 1).repeat(1, 1, N)
        l0_points = self.fp1(l0_xyz, l1_xyz, torch.cat([cls_label_one_hot, l0_xyz, l0_points], 1), l1_points)
        # FC layers
        feat = F.relu(self.bn1(self.conv1(l0_points)))
        x = self.drop1(feat)
        x = self.conv2(x)
        x = F.log_softmax(x, dim=1)
        x = x.permute(0, 2, 1)
        return x, l3_points

代码跟分类差不多的地方我就不说了,只讲不同的地方。

我们先简单的看一下流程。对了,分割任务里面,设置的开始点的数量是2048个,所以xyz开始的shape是(B,6,2048)。后面和分类一样,先中心采样512个,然后再中心采样128个,不一样的点也就半径和对应簇的样本个数不一样,别的没啥了。最后还原回1024个点,但是我们需要还原回的是2048个点,所以我们进行上采样。

看到self.fp3self.fp2self.fp1了没,这三个就是上采样网络PointNetFeaturePropagation(),我们可以看到它的输入是前面两层的的数据哎,因为我们上采样后还会对之前的数据做一个拼接操作,进去看看。

def forward(self, xyz1, xyz2, points1, points2):
    xyz1 = xyz1.permute(0, 2, 1)
    xyz2 = xyz2.permute(0, 2, 1)

    points2 = points2.permute(0, 2, 1)
    B, N, C = xyz1.shape
    _, S, _ = xyz2.shape

    if S == 1:
        interpolated_points = points2.repeat(1, N, 1)  # 把点复制N个 好跟前面的数据做拼接
    else:
        dists = square_distance(xyz1, xyz2)
        dists, idx = dists.sort(dim=-1)
        dists, idx = dists[:, :, :3], idx[:, :, :3]  # [B, N, 3]
        # xyz2的S个点拓展大小为xyz1的N个点 通过加权 即计算xyz2与xyz1每个点的距离 距离越远权重越小 越大权重越大 加权得到插值点
        dist_recip = 1.0 / (dists + 1e-8)
        norm = torch.sum(dist_recip, dim=2, keepdim=True)
        weight = dist_recip / norm
        interpolated_points = torch.sum(index_points(points2, idx) * weight.view(B, N, 3, 1), dim=2)

    if points1 is not None:
        points1 = points1.permute(0, 2, 1)
        new_points = torch.cat([points1, interpolated_points], dim=-1)  # 插值完后做拼接
    else:
        new_points = interpolated_points

    new_points = new_points.permute(0, 2, 1)
    for i, conv in enumerate(self.mlp_convs):
        bn = self.mlp_bns[i]
        new_points = F.relu(bn(conv(new_points)))
    return new_points

我们第一次上采样的时候,取的最后一层数据,那个数据因为做过了max,所以只有1个点的数据,这咋做上采样啊。因为我们上采样需要用插值,一个点咋插。所以当S为1的时候,直接把这个点复制128份,这样我们就有128份数据了。然后我们跟之前的层做个拼接,for循环做几个1d的卷积,返回上采样后的数据。

再做上采样的时候就不止1个点,比如我们现在是需要将128个点上采样到512个点,那怎么插值呢?我们计算这128个点与512个点的距离,越远的对我影响越小,权重越小,越近的对我影响越大,权重越大。然后加权求和得到我们插值后的数据。代码里也可以看到,square_distance()来求距离dists,然后1.0 / (dists + 1e-8)取倒数,距离越大值越小,dist_recip / norm标准化后得到权重,point(B,S,D)weight(B,N,3)加权求和得到最终的上采样数据(B,N,D)。后面拼接卷积内容一样不说了,最后还原回了2048个点。

===================================================================================

这章内容写了我一天,累死我了。

标签:采样,++,xyz,Pointnet,源码,128,new,self,points
From: https://blog.csdn.net/m0_46677695/article/details/144246479

相关文章

  • 深入解析Java线程源码:从基础到并发控制的全面指南(一)
    一:Java线程基础和源码解析packagejava.lang;importjava.lang.ref.Reference;importjava.lang.ref.ReferenceQueue;importjava.lang.ref.WeakReference;importjava.security.AccessController;importjava.security.AccessControlContext;importjava.security.Pr......
  • C++ 标准模板库(STL)——bitset的使用
    目录一、问题二、定义和初始化三、访问元素四、修改元素五、成员函数1、count()函数2、size()函数3、test()函数4、any()函数5、none()函数6、all()函数7、to_string()函数8、to_ulong()和to_ullong()六、运算符七、总结一、问题std::bitset是C++标准......
  • 蓝桥杯准备训练(lesson2 ,c++)
    3.1字符型char//character的缩写在键盘上可以敲出各种字符,如:a,q,@,#等,这些符号都被称为字符,字符是⽤单引号括起来的,如:‘a’,‘b’,‘@’。为了能说明这些字符,给他们抽象出⼀种类型,就是字符型,C语⾔中就是char。ASCII编码我们知道在计算机中所有的数据都......
  • 蓝桥杯准备训练(lesson1,c++方向)
    前言报名参加了蓝桥杯(c++)方向的宝子们,今天我将与大家一起努力参赛,后序会与大家分享我的学习情况,我将从最基础的内容开始学习,带大家打好基础,在每节课后都会有练习题,刚开始的练习题难度很低,但希望大家也简单的做一下,防止与课程脱节,最后希望大家都能取得好成绩。1、工具安装......
  • 【C++入门】【六】
    本节目标一、继承的概念及定义二、基类和派生类对象赋值转换三、继承中的作用域四、派生类的默认成员函数五、继承与友元六、继承与静态成员七、复杂的菱形继承及菱形虚拟继承八、继承的总结和反思九、笔试面试题一、继承的概念及定义1.继承的概念继承......
  • 打卡信奥刷题(360)用C++工具信奥P3353[普及组/提高] 在你窗外闪耀的星星
    在你窗外闪耀的星星题目背景飞逝的的时光不会模糊我对你的记忆。难以相信从我第一次见到你以来已经过去了3年。我仍然还生动地记得,3年前,在美丽的集美中学,从我看到你微笑着走出教室,你将头向后仰,柔和的晚霞照耀着你玫瑰色的脸颊。我明白,我已经沉醉于你了。之后,经过几个月......
  • spring 源码解析
    一。当启动tomcat服务器的过程中(接收请求前),当bean被注入到容器后会执行一系列的初始化过程。SpringMVC源码分析DispatcherServlet源码分析_51CTO博客_dispatcherServlet源码 二。HandlerMapping的主要作用是将客户端发送的HTTP请求映射到相应的处理器(Handler)。处理器可以是......
  • Python扩展C/C++ 实现原理分析
    Python扩展C/C++实现原理分析https://blog.csdn.net/HaoBBNuanMM/article/details/112243129?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522ab2ac79057d38453c0328d6726560514%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request......
  • 基于SpringBoot+Vue的宠物咖啡馆系统-无偿分享 (附源码+LW+调试)
    目录1.项目技术2.功能菜单3.部分功能截图4.研究背景5.研究目的6.可行性分析6.1技术可行性6.2经济可行性6.3操作可行性7.系统设计7.1概述7.2系统流程和逻辑7.3系统结构8.数据库设计8.1数据库ER图(1)宠物订单实体属性图(2)健康状况实体属性图(3)菜品......
  • uniapp精仿微信源码,基于SumerUI和Uniapp前端框架的一款仿微信APP应用,界面漂亮颜值高,视
    uniapp精仿微信源码,基于SumerUI和Uniapp前端框架的一款仿微信APP应用,界面漂亮颜值高,视频商城小工具等,朋友圈视频号即时聊天用于视频,商城,直播,聊天,等等场景,源码分享sumer-weixin介绍uniapp精仿微信,基于SumerUI3.0和Uniapp前端框架的一款仿微信APP应用,界面漂亮颜值高,视频......