Matlab通过GUI实现点云的双边(Bilateral)滤波(附最简版)

本次我们分享关于Matlab实现点云的双边滤波。在三维点云数据处理中,噪声去除与特征保留往往是一对矛盾的需求 ------ 传统滤波算法(如高斯滤波)在平滑噪声的同时,容易模糊点云中的边缘、棱角等关键特征。而双边滤波(Bilateral Filtering) 作为一种非线性滤波技术,通过同时考虑空间域权重与灰度(或强度)域权重,能够在抑制噪声的同时有效保留点云的细节特征,成为点云预处理中的核心算法之一。Matlab 作为工程领域常用的数值计算与可视化平台,提供了便捷的点云处理工具箱(PointCloud Toolbox),支持双边滤波的快速实现与优化。

一、点云双边滤波的核心原理与优势

  1. 基本原理

点云双边滤波的本质是对每个目标点,根据其邻域内点的空间距离属性差异(如颜色、法向量、强度值等)计算加权平均,从而更新目标点的坐标。其核心公式可表示为:

\(P_{filtered} = \frac{1}{W} \sum_{q \in N(p)} \left[ G_s(\|p - q\|) \cdot G_r(\|I_p - I_q\|) \right] \cdot q\)

其中:

  • \(p\) 为目标点,\(q\) 为 \(p\) 邻域内的点,\(N(p)\) 表示 \(p\) 的邻域集合;
  • \(G_s\) 为空间域高斯函数,根据点之间的欧氏距离分配权重,距离越近权重越大;
  • \(G_r\) 为属性域高斯函数,根据点的属性差异分配权重,差异越小权重越大;
  • \(W\) 为归一化系数,确保滤波后点的坐标范围合理。
  1. 核心优势

与传统滤波算法相比,Matlab 点云双边滤波具有以下特点:

  • 特征保留能力强:通过属性域权重约束,避免边缘、棱角等特征被过度平滑;
  • 参数可控性高:可通过调整空间域标准差(控制邻域范围)、属性域标准差(控制属性差异敏感度),适配不同噪声强度的点云;
  • 兼容性广:支持 XYZ 坐标点云、带颜色(RGB)点云、带强度(Intensity)点云等多种类型数据,可与 Matlab 点云分割、配准等模块无缝衔接。

二、Matlab 点云双边滤波的主要流程

Matlab 点云双边滤波的实现依赖于pointCloud类与bilateralFilter函数(需安装 PointCloud Toolbox),完整流程可分为数据准备→参数设置→滤波执行→结果评估四步,具体操作如下:

  1. 数据准备:加载与预处理

首先需加载点云数据,并进行基础预处理(如去除无效点、下采样),减少后续计算量。Matlab 支持多种点云格式(如 PLY、PCD、XYZ),常用函数包括pcread(读取点云)、pcremoveinvalid(去除无效点)、pcdownsample(下采样)。

示例代码

cpp 复制代码
% 读取PLY格式点云(以桌面场景点云为例)

ptCloud = pcread('desk_scene.ply');

% 去除无效点(如NaN坐标点)

ptCloud = pcremoveinvalid(ptCloud);

% 下采样(体素下采样,体素大小0.01m,减少点云数量)

ptCloud = pcdownsample(ptCloud, 'gridAverage', 0.01);

% 可视化原始点云

figure; pcshow(ptCloud); title('原始点云'); xlabel('X'); ylabel('Y'); zlabel('Z');
  1. 参数设置:关键参数选择

BilateralFilter函数的核心参数需根据点云噪声强度与特征需求调整,主要包括:

  • sigmaS (空间域标准差):控制邻域搜索范围,单位与点云坐标一致(如米)。噪声越强,需适当增大sigmaS(如 0.02~0.05),但过大易导致特征模糊;
  • sigmaR (属性域标准差):控制属性差异敏感度,若点云无颜色 / 强度信息,可基于点云法向量(需提前计算)或坐标差异设置。例如,带颜色点云可设sigmaR=0.05(RGB 值归一化后),无属性点云可基于 XYZ 差异设sigmaR=0.01;
  • kernelSize (核大小):邻域搜索的核尺寸,通常设为2*ceil(3*sigmaS)+1(确保覆盖 99.7% 的高斯分布范围),默认自动计算;
  • metric (属性度量方式):指定属性域的计算方式,如'color'(基于 RGB)、'intensity'(基于强度)、'normal'(基于法向量),无属性时用'euclidean'(基于坐标)。

参数选择原则

  • 若点云噪声密集(如激光雷达扫描的远距离点云),增大sigmaS(如 0.04),同时减小sigmaR(如 0.005),平衡平滑与特征保留;
  • 若点云含精细特征(如机械零件的棱角),减小sigmaS(如 0.02),增大sigmaR(如 0.08),避免特征丢失。
  1. 滤波执行:调用函数实现

根据点云类型调用bilateralFilter函数,输出滤波后的点云对象。若点云无颜色 / 强度属性,需先计算法向量作为属性输入。

示例代码

cpp 复制代码
% 情况1:带颜色的点云(直接基于RGB属性滤波)

if isfield(ptCloud.Location, 'Color')

% 设置参数:空间域标准差0.03m,颜色域标准差0.05(RGB归一化后)

sigmaS = 0.03;

sigmaR = 0.05;

% 执行双边滤波

ptCloudFiltered = bilateralFilter(ptCloud, sigmaS, sigmaR, 'metric', 'color');

else

% 情况2:无属性点云(基于法向量滤波,需先计算法向量)

% 计算法向量(邻域大小5,用于属性度量)

ptCloud = pcnormals(ptCloud, 5);

% 设置参数:空间域标准差0.03m,法向量域标准差0.1

sigmaS = 0.03;

sigmaR = 0.1;

% 执行双边滤波

ptCloudFiltered = bilateralFilter(ptCloud, sigmaS, sigmaR, 'metric', 'normal');

end

% 可视化滤波后点云

figure; pcshow(ptCloudFiltered); title('双边滤波后点云'); xlabel('X'); ylabel('Y'); zlabel('Z');
  1. 结果评估:定量与定性分析

滤波效果需通过定性观察 (特征保留程度)与定量指标(噪声抑制效果)结合评估:

  • 定性评估:对比原始点云与滤波后点云的边缘、棱角细节(如机械零件的孔、凸起),观察是否存在过度平滑;
  • 定量评估 :计算点云的均方根误差(RMSE) (若有真值点云)、点云密度变化 (滤波后点数量应与原始接近,避免过度删减)、法向量一致性(特征区域法向量方向是否连续)。

定量评估示例代码

cpp 复制代码
% 计算原始点云与滤波后点云的坐标差异(假设无真值,用自身差异近似噪声)

diffXYZ = ptCloud.Location - ptCloudFiltered.Location;

rmse = sqrt(mean(diffXYZ(:).^2)); % 均方根误差

fprintf('滤波后点云RMSE:%.6f m\n', rmse);

% 计算点云密度变化

originalCount = ptCloud.Count;

filteredCount = ptCloudFiltered.Count;

densityRatio = filteredCount / originalCount;

fprintf('滤波后点数量保留比例:%.2f%%\n', densityRatio * 100);

三、Matlab 点云双边滤波的应用领域

由于其 "噪声抑制 + 特征保留" 的核心优势,Matlab 点云双边滤波广泛应用于工业检测、自动驾驶、文化遗产保护、医疗影像等领域,具体场景如下:

  1. 工业零件检测

在机械零件三维扫描(如激光扫描、结构光扫描)中,点云易因设备精度、环境振动引入噪声,导致后续尺寸测量(如孔径、壁厚)误差。通过 Matlab 双边滤波,可在去除噪声的同时保留零件的棱角、螺纹等关键特征,为后续的尺寸标注(pcfitcylinder/pcfitsphere)缺陷检测提供高精度点云数据。

应用案例:汽车发动机缸体的三维扫描点云处理,滤波后可准确拟合缸体孔的圆柱面,测量孔径公差是否符合标准。

  1. 自动驾驶环境感知

自动驾驶的激光雷达(LiDAR)点云常包含雨雪噪声、地面杂点,若直接用于障碍物检测(如行人、车辆),易导致误识别。Matlab 双边滤波可基于点云的强度值(LiDAR 反射强度)或法向量(地面与障碍物法向量差异大),过滤雨雪噪声与地面杂点,同时保留障碍物的轮廓特征,提升后续目标分割(pcsegplane)跟踪的准确性。

  1. 文化遗产数字化保护

在文物(如石雕、青铜器)三维重建中,需同时保留文物的纹理细节(如刻痕、花纹)与整体形态。传统滤波易模糊纹理,而 Matlab 双边滤波可通过调整颜色域(纹理颜色差异)或法向量域(纹理凹凸差异)参数,在平滑扫描噪声的同时,完整保留文物的纹理特征,为后续的数字化存档虚拟修复提供高保真点云。

  1. 医疗影像三维重建

在医学 CT、MRI 的三维点云重建(如骨骼、器官)中,点云噪声会影响手术规划的精度。Matlab 双边滤波可基于医学影像的灰度值(CT 值)作为属性域权重,过滤重建过程中的伪影噪声,同时保留骨骼的关节面、器官的轮廓等关键结构,为手术导航假体设计提供精准的三维模型。

四、总结与注意事项

Matlab 点云双边滤波凭借其灵活的参数设置与强大的特征保留能力,成为点云预处理的关键工具。在实际应用中,需注意以下事项:

1. 参数调试:sigmaS与sigmaR需根据点云噪声强度迭代调试,建议先从较小sigmaS(如 0.02)与中等sigmaR(如 0.05)开始,逐步优化;

2. 计算效率:双边滤波为非线性操作,点云数量过大时(如百万级)会导致计算缓慢,建议先通过pcdownsample下采样(体素大小 0.01~0.05m),平衡效率与精度;

3. 属性选择:优先使用点云的固有属性(如颜色、强度)作为metric参数,若无属性再使用法向量,避免仅基于 XYZ 坐标导致特征丢失。

本次使用的数据是------------兔砸!!!

一、点云实现双边滤波的程序

1、最简版

cpp 复制代码
%% 0. 清空环境
clear; clc; close all;

%% 1. 读入点云
[file, path] = uigetfile({'*.ply;*.pcd;*.xyz','点云文件 (*.ply,*.pcd,*.xyz)'},...
                         '请选择点云');
if file==0; return; end
fname = fullfile(path,file);
ptCloud = pcread(fname);
N = ptCloud.Count;
fprintf('原始点云有 %d 个点\n',N);

%% 2. 计算平均密度(最近邻距离均值)
kdtree0 = createns(ptCloud.Location,'NSMethod','kdtree');
[~, nndist0] = knnsearch(kdtree0,ptCloud.Location,'K',2); % 第2列=最近邻距离
density = mean(nndist0(:,2));
fprintf('点云平均密度 = %.4f\n',density);

%% 3. 添加高斯噪声
sigmaNoise = density * 2;
noise = sigmaNoise * randn(N,3);
noisyCloud = pointCloud(ptCloud.Location + noise,...
                        'Color',ptCloud.Color,...
                        'Normal',ptCloud.Normal);
fprintf('添加噪声后 %d 个点\n',noisyCloud.Count);

%% 4. 双边滤波
K      = 30;          % 与原脚本一致
srMm   = 10;          % 与原脚本一致
filtCloud = pcdBilateralFilter(noisyCloud,K,srMm);
fprintf('双边滤波后 %d 个点\n',filtCloud.Count);

%% 5. 可视化
figure('Name','原始点云','NumberTitle','off'); pcshow(ptCloud); axis on; view(3);
figure('Name','噪声点云','NumberTitle','off'); pcshow(noisyCloud); axis on; view(3);
figure('Name','双边滤波','NumberTitle','off'); pcshow(filtCloud); axis on; view(3);

function out = pcdBilateralFilter(pc,K,srMm)
    K   = max(round(K),2);
    sr  = srMm/1000;
    xyz = pc.Location;
    n   = size(xyz,1);
    kd  = createns(xyz,'NSMethod','kdtree');

    % 自算法向量
    block  = 10000;
    normals= zeros(n,3,'like',xyz);
    h      = waitbar(0,'计算法向量...');
    for i = 1:block:n
        ir = min(i+block-1,n);
        [idxMat,~] = knnsearch(kd, xyz(i:ir,:), 'K', K);
        for k = i:ir
            idx = idxMat(k-i+1,:);
            P   = xyz(idx,:);
            [~,~,V] = svd(P - mean(P,1),0);
            nk  = V(:,3);
            if nk(3)>0, nk=-nk; end
            normals(k,:) = nk;
        end
        waitbar(ir/n,h);
    end
    close(h);

    % 双边滤波
    xyzNew = zeros(n,3,'like',xyz);
    h = waitbar(0,'双边滤波...');
    for i = 1:block:n
        ir = min(i+block-1,n);
        [idxMat,~] = knnsearch(kd, xyz(i:ir,:), 'K', K);
        for k = i:ir
            idx = idxMat(k-i+1,:);
            p0  = xyz(k,:);
            n0  = normals(k,:);
            Ws  = 0; Z = 0;
            for t = 2:numel(idx)
                pj  = xyz(idx(t),:);
                vec = pj - p0;
                dd  = norm(vec);
                dn  = dot(vec, n0);
                ws  = exp(-(dd^2)/(2*(0.05)^2));
                wr  = exp(-(dn^2)/(2*sr^2));
                w   = ws*wr;
                Ws  = Ws + w;
                Z   = Z  + w*dn;
            end
            xyzNew(k,:) = p0 + (Z/(Ws+eps))*n0;
        end
        waitbar(ir/n,h);
    end
    close(h);

    out = pointCloud(xyzNew,'Color',pc.Color,'Normal',normals);
end

2、GUI版本

cpp 复制代码
function bilateralFilterGUI
% 双边滤波 GUI(三栏:原始 | 加噪 | 滤波)------ 2020a 兼容
fig = figure('Name','双边滤波工具','NumberTitle','off',...
             'MenuBar','none','ToolBar','none','Position',[100 100 1280 720]);

%% ==================== 界面布局 ====================
imgWidth = 0.78;
panelW   = imgWidth/3 - 0.005;

pnlOrig = uipanel('Parent',fig,'Units','normalized',...
                  'FontSize',16,'Position',[0.02 0.02 panelW 0.96],'Title','原始点云');
pnlNois = uipanel('Parent',fig,'Units','normalized',...
                  'FontSize',16,'Position',[0.02+panelW+0.005 0.02 panelW 0.96],'Title','加噪声');
pnlFilt = uipanel('Parent',fig,'Units','normalized',...
                  'FontSize',16,'Position',[0.02+2*(panelW+0.005) 0.02 panelW 0.96],'Title','双边滤波');

axOrig = axes('Parent',pnlOrig,'Units','normalized','Position',[0.05 0.05 0.90 0.90]);
axNois = axes('Parent',pnlNois,'Units','normalized','Position',[0.05 0.05 0.90 0.90]);
axFilt = axes('Parent',pnlFilt,'Units','normalized','Position',[0.05 0.05 0.90 0.90]);

pnlCtrl = uipanel('Parent',fig,'Units','normalized',...
                  'FontSize',16,'Position',[0.78 0 0.22 1],'Title','控制');

%% ==================== 控件 ====================
txtH  = 0.04;  btnH = 0.06;  gap = 0.02;  yTop = 0.94;

uicontrol('Parent',pnlCtrl,'Style','pushbutton','String','浏览...',...
          'FontSize',16,'Units','normalized','Position',[0.05 yTop-btnH 0.90 btnH],...
          'Callback',@loadCloud);
yTop = yTop - btnH - gap;

lblInfo = uicontrol('Parent',pnlCtrl,'Style','text','String','未加载点云',...
                    'FontSize',10,'Units','normalized','Position',[0.05 yTop-txtH 0.90 txtH],...
                    'HorizontalAlignment','left');
yTop = yTop - txtH - gap;

% 噪声强度
uicontrol('Parent',pnlCtrl,'Style','text','String','噪声强度 / mm',...
          'FontSize',12,'FontWeight','bold','Units','normalized','Position',[0.05 yTop-txtH 0.90 txtH],...
          'HorizontalAlignment','left');
yTop = yTop - txtH - gap;

sliderSig = uicontrol('Parent',pnlCtrl,'Style','slider','Min',0,'Max',20,'Value',5,...
                      'FontSize',16,'Units','normalized','Position',[0.05 yTop-btnH 0.65 btnH],...
                      'Callback',@refreshNoise);
txtSig    = uicontrol('Parent',pnlCtrl,'Style','edit','String','5',...
                      'FontSize',16,'Units','normalized','Position',[0.75 yTop-btnH 0.20 btnH],...
                      'Callback',@editSigCB);
yTop = yTop - btnH - gap;

% 邻域点数 K
uicontrol('Parent',pnlCtrl,'Style','text','String','邻域点数 K',...
          'FontSize',12,'FontWeight','bold','Units','normalized','Position',[0.05 yTop-txtH 0.90 txtH],...
          'HorizontalAlignment','left');
yTop = yTop - txtH - gap;

sliderK = uicontrol('Parent',pnlCtrl,'Style','slider','Min',2,'Max',100,'Value',30,...
                    'FontSize',16,'Units','normalized','Position',[0.05 yTop-btnH 0.65 btnH],...
                    'Callback',@refreshFilter);
txtK    = uicontrol('Parent',pnlCtrl,'Style','edit','String','30',...
                    'FontSize',16,'Units','normalized','Position',[0.75 yTop-btnH 0.20 btnH],...
                    'Callback',@editKCB);
yTop = yTop - btnH - gap;

% 法向权重 σr
uicontrol('Parent',pnlCtrl,'Style','text','String','法向权重 σr',...
          'FontSize',12,'FontWeight','bold','Units','normalized','Position',[0.05 yTop-txtH 0.90 txtH],...
          'HorizontalAlignment','left');
yTop = yTop - txtH - gap;

sliderR = uicontrol('Parent',pnlCtrl,'Style','slider','Min',1,'Max',50,'Value',10,...
                    'FontSize',16,'Units','normalized','Position',[0.05 yTop-btnH 0.65 btnH],...
                    'Callback',@refreshFilter);
txtR    = uicontrol('Parent',pnlCtrl,'Style','edit','String','10',...
                    'FontSize',16,'Units','normalized','Position',[0.75 yTop-btnH 0.20 btnH],...
                    'Callback',@editRCB);
yTop = yTop - btnH - gap;

% 保存
uicontrol('Parent',pnlCtrl,'Style','pushbutton','String','保存滤波结果',...
          'FontSize',16,'Units','normalized','Position',[0.05 yTop-btnH 0.90 btnH],...
          'Callback',@(s,e)saveCloud(ptCloudFilt));

%% ==================== 数据 ====================
ptCloudOrig = pointCloud.empty;
ptCloudNois = pointCloud.empty;
ptCloudFilt = pointCloud.empty;

    %% ==================== 回调 ====================
    function loadCloud(~,~)
        [file,path] = uigetfile({'*.pcd;*.ply;*.xyz','点云文件'},'选择点云');
        if isequal(file,0), return; end
        try
            ptCloudOrig = pcread(fullfile(path,file));
        catch ME
            errordlg(ME.message,'读取失败'); return;
        end
%         disp(isvalid(axOrig))   % 必须 =1
        % 显示原始点云
        showPointCloud(axOrig,ptCloudOrig);
        
        set(lblInfo,'String',sprintf('已加载:%s  (%d 点)',file,ptCloudOrig.Count));
        refreshNoise();             % 自动生成噪声并滤波
    end

    function refreshNoise(~,~)
        if isempty(ptCloudOrig), return; end
        sigma = get(sliderSig,'Value')/1000;        % mm → m
        xyz   = ptCloudOrig.Location;
        xyzNois = xyz + sigma*randn(size(xyz));
        ptCloudNois = pointCloud(xyzNois,'Color',ptCloudOrig.Color);
        showPointCloud(axNois,ptCloudNois);
        set(txtSig,'String',num2str(get(sliderSig,'Value')));
        refreshFilter();
    end

    function refreshFilter(~,~)
        if isempty(ptCloudNois), return; end
        K  = round(get(sliderK,'Value'));
        sr = get(sliderR,'Value');
        ptCloudFilt = pcdBilateralFilter(ptCloudNois,K,sr);
        showPointCloud(axFilt,ptCloudFilt);
        set(txtK,'String',num2str(K));
        set(txtR,'String',num2str(sr));
    end

    function editSigCB(src,~)
        v = str2double(get(src,'String'));
        if isnan(v), v = 5; end
        v = max(0,min(20,v));
        set(sliderSig,'Value',v);
        refreshNoise();
    end

    function editKCB(src,~)
        v = str2double(get(src,'String'));
        if isnan(v), v = 30; end
        v = max(2,min(100,round(v)));
        set(sliderK,'Value',v);
        refreshFilter();
    end

    function editRCB(src,~)
        v = str2double(get(src,'String'));
        if isnan(v), v = 10; end
        v = max(1,min(50,v));
        set(sliderR,'Value',v);
        refreshFilter();
    end

    function saveCloud(cloud)
        if isempty(cloud)
            errordlg('请先完成滤波','提示'); return;
        end
        [file,path] = uiputfile({'*.pcd','PCD';'*.ply','PLY';'*.xyz','XYZ'},'保存滤波点云');
        if isequal(file,0), return; end
        try
            pcwrite(cloud,fullfile(path,file),'Precision','double');
            msgbox('保存成功!','提示');
        catch ME
            errordlg(ME.message,'保存失败');
        end
    end

    function showPointCloud(ax,pc)
        %---- 2020a 暖启动 + 强制刷新 ----
        cla(ax); set(ax,'Color','w');
        pcshow(pointCloud(nan(0,3)),'Parent',ax,'MarkerSize',1); drawnow;
        pcshow(pc,'Parent',ax,'MarkerSize',35);
        axis(ax,'tight'); grid(ax,'on'); view(ax,3);
        drawnow;        % 确保刷新
    end

    %% ==================== 双边滤波核心(零 pcnormals) ====================
    function out = pcdBilateralFilter(pc,K,srMm)
    K   = max(round(K),2);
    sr  = srMm/1000;
    xyz = pc.Location;
    n   = size(xyz,1);
    kd  = createns(xyz,'NSMethod','kdtree');

    % 自算法向量
    block  = 10000;
    normals= zeros(n,3,'like',xyz);
    h      = waitbar(0,'计算法向量...');
    for i = 1:block:n
        ir = min(i+block-1,n);
        [idxMat,~] = knnsearch(kd, xyz(i:ir,:), 'K', K);
        for k = i:ir
            idx = idxMat(k-i+1,:);
            P   = xyz(idx,:);
            [~,~,V] = svd(P - mean(P,1),0);
            nk  = V(:,3);
            if nk(3)>0, nk=-nk; end
            normals(k,:) = nk;
        end
        waitbar(ir/n,h);
    end
    close(h);

    % 双边滤波
    xyzNew = zeros(n,3,'like',xyz);
    h = waitbar(0,'双边滤波...');
    for i = 1:block:n
        ir = min(i+block-1,n);
        [idxMat,~] = knnsearch(kd, xyz(i:ir,:), 'K', K);
        for k = i:ir
            idx = idxMat(k-i+1,:);
            p0  = xyz(k,:);
            n0  = normals(k,:);
            Ws  = 0; Z = 0;
            for t = 2:numel(idx)
                pj  = xyz(idx(t),:);
                vec = pj - p0;
                dd  = norm(vec);
                dn  = dot(vec, n0);
                ws  = exp(-(dd^2)/(2*(0.05)^2));
                wr  = exp(-(dn^2)/(2*sr^2));
                w   = ws*wr;
                Ws  = Ws + w;
                Z   = Z  + w*dn;
            end
            xyzNew(k,:) = p0 + (Z/(Ws+eps))*n0;
        end
        waitbar(ir/n,h);
    end
    close(h);

    out = pointCloud(xyzNew,'Color',pc.Color,'Normal',normals);
    end
end

二、点云双边滤波的结果

依旧采用GUI布局,添加了噪声设置功能,双边滤波设置了邻域个数控制和法向量权重变量。总体来说性能还比较平稳。小伙伴们快来试试吧!

就酱,下次见^-^

相关推荐
二十雨辰3 小时前
vite如何处理项目中的资源
开发语言·javascript
聆风吟º3 小时前
远程录制新体验:Bililive-go与cpolar的无缝协作
开发语言·后端·golang
白水先森4 小时前
C语言作用域与数组详解
java·数据结构·算法
想唱rap4 小时前
直接选择排序、堆排序、冒泡排序
c语言·数据结构·笔记·算法·新浪微博
豆浆whisky4 小时前
netpoll性能调优:Go网络编程的隐藏利器|Go语言进阶(8)
开发语言·网络·后端·golang·go
蓝天白云下遛狗4 小时前
go环境的安装
开发语言·后端·golang
CAir24 小时前
go协程的前世今生
开发语言·golang·协程
@大迁世界4 小时前
Go 会成为“老生态”的新引擎吗?
开发语言·后端·golang
Absinthe_苦艾酒4 小时前
golang基础语法(六)Map
开发语言·后端·golang