imageProjection

  • 总体叙述
    • 类成员变量
    • 构造
    • imu/odometryHandler
  • cloudHandler 点云回调
    • cachePointCloud
    • deskewInfo
    • projectPointCloud
      • deskewPoint
    • cloudExtraction

总体叙述

主要功能为:

  1. 利用当前激光帧起止时刻间的imu数据计算旋转增量,IMU里程计数据(来自ImuPreintegration)计算平移增量,
  2. 对该帧激光每一时刻的激光点进行运动畸变校正(利用相对于激光帧起始时刻的位姿增量,
  3. 变换当前激光点到起始时刻激光点的坐标系下,实现校正);
  4. 最后用IMU数据的姿态角(RPY,roll、pitch、yaw)、IMU里程计数据的的位姿,对当前帧激光位姿进行粗略初始化。
struct VelodynePointXYZIRT
{PCL_ADD_POINT4D   // 位置PCL_ADD_INTENSITY;  //激光点云强度,也可存点的索引uint16_t ring;  //扫描线float time;     //时间戳,记录相对于当前帧第一个激光点的时差,第一个点time=0EIGEN_MAKE_ALIGNED_OPERATOR_NEW
} EIGEN_ALIGN16;

类成员变量

  • 互斥锁:std::mutex imuLock、odoLock
  • 订阅原始点云
    subLaserCloud、pubLaserCloud(未用)
  • 发布当前激光帧运动畸变校正后的点云
    pubExtractedCloud、pubLaserCloudInfo
  • imu数据队列(原始数据,转lidar系下)
    subImu、imuQueue
  • 激光点云数据队列
    cloudQueue,currentCloudMsg(PointCloud2)
  • 当前激光帧起止时刻间对应的imu数据,计算相对于起始时刻的旋转增量,以及时时间戳;
    double *imuTime,*imuRotX,*imuRotY,*imuRotZ

    • 目的:用于插值计算当前激光帧起止时间范围内,每一时刻的旋
  • 当前帧起始时间+当前帧起始对应imu位姿变化 imuPointerCur
  • 当前帧原始激光点云 + 当期帧运动畸变校正之后的激光点云
  • 当前激光帧起止时刻对应imu里程计位姿变换,该变换对应的平移增量;
    用于插值计算当前激光帧起止时间范围内,每一时刻的位置
    odomIncreX odomIncreY odomIncreZ
  • 当前帧激光点云运动畸变校正之后的数据,包括点云数据、初始位姿、姿态角等
    lio_sam::cloud_info cloudInfo

构造

// 订阅原始imu数据
subImu        = nh.subscribe<sensor_msgs::Imu>(imuTopic, 2000, &ImageProjection::imuHandler, this, ros::TransportHints().tcpNoDelay());
// 订阅imu里程计,由imuPreintegration积分计算得到的每时刻imu位姿
subOdom       = nh.subscribe<nav_msgs::Odometry>(odomTopic+"_incremental", 2000, &ImageProjection::odometryHandler, this, ros::TransportHints().tcpNoDelay());
// 订阅原始lidar数据
subLaserCloud = nh.subscribe<sensor_msgs::PointCloud2>(pointCloudTopic, 5, &ImageProjection::cloudHandler, this, ros::TransportHints().tcpNoDelay());
// 发布当前激光帧运动畸变校正后的点云,有效点
pubExtractedCloud = nh.advertise<sensor_msgs::PointCloud2> ("lio_sam/deskew/cloud_deskewed", 1);
// 发布当前激光帧运动畸变校正后的点云信息
pubLaserCloudInfo = nh.advertise<lio_sam::cloud_info> ("lio_sam/deskew/cloud_info", 1);allocateMemory();  // 初始化
resetParameters();  // 重置参数pcl::console::setVerbosityLevel(pcl::console::L_ERROR);

imu/odometryHandler

imu回调

  1. 将imu原始测量数据转换到lidar系,加速度,角速度,PRY
  2. 将数据放入队列

odom回调

  • 直接放入odom_deque

cloudHandler 点云回调

订阅原始lidar数据

 * 1、添加一帧激光点云到队列,取出最早一帧作为当前帧,计算起止时间戳,检查数据有效性* 2、当前帧起止时刻对应的imu数据、imu里程计数据处理*   imu数据:*   1) 遍历当前激光帧起止时刻之间的imu数据,初始时刻对应imu的姿态角RPY设为当前帧的初始姿态角*   2) 用角速度、时间积分,计算每一时刻相对于初始时刻的旋转量,初始时刻旋转设为0*   imu里程计数据:*   1) 遍历当前激光帧起止时刻之间的imu里程计数据,初始时刻对应imu里程计设为当前帧的初始位姿*   2) 用起始、终止时刻对应imu里程计,计算相对位姿变换,保存平移增量* 3、当前帧激光点云运动畸变校正*   1) 检查激光点距离、扫描线是否合规*   2) 激光运动畸变校正,保存激光点* 4、提取有效激光点,存extractedCloud* 5、发布当前帧校正后点云,有效点* 6、重置参数,接收每帧lidar数据都要重置这些参数
void cloudHandler(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg)
{// 添加一帧激光点云到队列,取出最早一帧作为当前帧,计算起止时间戳,检查数据有效性if (!cachePointCloud(laserCloudMsg))return;// 当前帧起止时刻对应的imu数据、imu里程计数据处理if (!deskewInfo())return;// 当前帧激光点云运动畸变校正projectPointCloud();// 提取有效激光点,存extractedCloudcloudExtraction();// 发布当前帧校正后点云,有效点publishClouds();// 重置参数,接收每帧lidar数据都要重置这些参数resetParameters();
}

cachePointCloud

添加一帧激光点云到队列,取出最早一帧作为当前帧,计算起止时间戳,检查数据有效性

  • 缓存三帧,计算第一帧,检测数据的有效性+检测有time+ring通道
// 添加一帧激光点云到队列,取出最早一帧作为当前帧,计算起止时间戳,检查数据有效性
bool cachePointCloud(const sensor_msgs::PointCloud2ConstPtr& laserCloudMsg)
{// cache point cloud cloudQueue.push_back(*laserCloudMsg);// 延迟了两帧??if (cloudQueue.size() <= 2)return false;// convert cloud // 取出激光点云队列中最早的一帧currentCloudMsg = std::move(cloudQueue.front());cloudQueue.pop_front();if (sensor == SensorType::VELODYNE){// 转换成pcl点云格式pcl::moveFromROSMsg(currentCloudMsg, *laserCloudIn);}else if (sensor == SensorType::OUSTER){// Convert to Velodyne format // 转换成Velodyne格式pcl::moveFromROSMsg(currentCloudMsg, *tmpOusterCloudIn);laserCloudIn->points.resize(tmpOusterCloudIn->size());laserCloudIn->is_dense = tmpOusterCloudIn->is_dense;for (size_t i = 0; i < tmpOusterCloudIn->size(); i++){auto &src = tmpOusterCloudIn->points[i];auto &dst = laserCloudIn->points[i];dst.x = src.x;dst.y = src.y;dst.z = src.z;dst.intensity = src.intensity;dst.ring = src.ring;// 主要是时间戳dst.time = src.t * 1e-9f;}}else{ROS_ERROR_STREAM("Unknown sensor type: " << int(sensor));ros::shutdown();}// get timestampcloudHeader = currentCloudMsg.header;timeScanCur = cloudHeader.stamp.toSec();// 当前帧结束时刻,注:点云中激光点的time记录相对于当前帧第一个激光点的时差,第一个点time=0timeScanEnd = timeScanCur + laserCloudIn->points.back().time;// check dense flag // 存在无效点,Nan或者Infif (laserCloudIn->is_dense == false){ROS_ERROR("Point cloud is not in dense format, please remove NaN points first!");ros::shutdown();}// check ring channel // 检查是否存在ring通道,注意static只检查一次static int ringFlag = 0;if (ringFlag == 0){ringFlag = -1;for (int i = 0; i < (int)currentCloudMsg.fields.size(); ++i){if (currentCloudMsg.fields[i].name == "ring"){ringFlag = 1;break;}}if (ringFlag == -1){ROS_ERROR("Point cloud ring channel not available, please configure your point cloud data!");ros::shutdown();}}// check point time  // 检查是否存在time通道if (deskewFlag == 0){deskewFlag = -1;for (auto &field : currentCloudMsg.fields){if (field.name == "time" || field.name == "t"){deskewFlag = 1;break;}}if (deskewFlag == -1)ROS_WARN("Point cloud timestamp not available, deskew function disabled, system will drift significantly!");}return true;
}

deskewInfo

处理当前激光帧起止时刻对应的IMU数据、IMU里程计数据

  • 要求imu数据包含激光数据,否则不往下处理了

    if (imuQueue.empty() || imuQueue.front().header.stamp.toSec() > timeScanCur || imuQueue.back().header.stamp.toSec() < timeScanEnd)
    {ROS_DEBUG("Waiting for IMU data ...");return false;
    }
    

2、 当前帧对应imu数据处理

  • 遍历当前激光帧起止时刻之间的imu数据,初始时刻对应imu的姿态角RPY设为当前帧的初始姿态角
  • 用角速度、时间积分,计算每一时刻相对于初始时刻的旋转量,初始时刻旋转设为0
  • 注:imu数据都已经转换到lidar系下了
    void imuDeskewInfo()
    {cloudInfo.imuAvailable = false;// 从imu队列中删除当前激光帧0.01s前面时刻的imu数据while (!imuQueue.empty()){if (imuQueue.front().header.stamp.toSec() < timeScanCur - 0.01)imuQueue.pop_front();elsebreak;}if (imuQueue.empty())return;imuPointerCur = 0;// 遍历当前激光帧起止时刻(前后扩展0.01s)之间的imu数据for (int i = 0; i < (int)imuQueue.size(); ++i){sensor_msgs::Imu thisImuMsg = imuQueue[i];double currentImuTime = thisImuMsg.header.stamp.toSec();// get roll, pitch, and yaw estimation for this scan// 提取imu姿态角RPY,作为当前lidar帧初始姿态角if (currentImuTime <= timeScanCur)imuRPY2rosRPY(&thisImuMsg, &cloudInfo.imuRollInit, &cloudInfo.imuPitchInit, &cloudInfo.imuYawInit);// 超过当前激光帧结束时刻0.01s,结束if (currentImuTime > timeScanEnd + 0.01)break;// 第一帧imu旋转角初始化if (imuPointerCur == 0){imuRotX[0] = 0;imuRotY[0] = 0;imuRotZ[0] = 0;imuTime[0] = currentImuTime;++imuPointerCur;continue;}// get angular velocity// 提取imu角速度double angular_x, angular_y, angular_z;imuAngular2rosAngular(&thisImuMsg, &angular_x, &angular_y, &angular_z);// integrate rotation// imu帧间时差double timeDiff = currentImuTime - imuTime[imuPointerCur-1];// 当前时刻旋转角 = 前一时刻旋转角 + 角速度 * 时差imuRotX[imuPointerCur] = imuRotX[imuPointerCur-1] + angular_x * timeDiff;imuRotY[imuPointerCur] = imuRotY[imuPointerCur-1] + angular_y * timeDiff;imuRotZ[imuPointerCur] = imuRotZ[imuPointerCur-1] + angular_z * timeDiff;imuTime[imuPointerCur] = currentImuTime;++imuPointerCur;}--imuPointerCur;// 没有合规的imu数据if (imuPointerCur <= 0)return;cloudInfo.imuAvailable = true;
    }
    

3、当前帧对应imu里程计处理

  • 遍历当前激光帧起止时刻之间的imu里程计数据,初始时刻对应imu里程计设为当前帧的初始位姿
  • 用起始、终止时刻对应imu里程计,计算相对位姿变换,保存平移增量
  • 注:imu数据都已经转换到lidar系下了
    void odomDeskewInfo()
    {cloudInfo.odomAvailable = false;// 从imu里程计队列中删除当前激光帧0.01s前面时刻的imu数据while (!odomQueue.empty()){if (odomQueue.front().header.stamp.toSec() < timeScanCur - 0.01)odomQueue.pop_front();elsebreak;}if (odomQueue.empty())return;// 要求必须有当前激光帧时刻之前的imu里程计数据if (odomQueue.front().header.stamp.toSec() > timeScanCur)return;// get start odometry at the beinning of the scan// 提取当前激光帧起始时刻的imu里程计nav_msgs::Odometry startOdomMsg;for (int i = 0; i < (int)odomQueue.size(); ++i){startOdomMsg = odomQueue[i];if (ROS_TIME(&startOdomMsg) < timeScanCur)continue;elsebreak;}// 提取imu里程计姿态角tf::Quaternion orientation;tf::quaternionMsgToTF(startOdomMsg.pose.pose.orientation, orientation);double roll, pitch, yaw;tf::Matrix3x3(orientation).getRPY(roll, pitch, yaw);// Initial guess used in mapOptimization// 用当前激光帧起始时刻的imu里程计,初始化lidar位姿,后面用于mapOptmizationcloudInfo.initialGuessX = startOdomMsg.pose.pose.position.x;cloudInfo.initialGuessY = startOdomMsg.pose.pose.position.y;cloudInfo.initialGuessZ = startOdomMsg.pose.pose.position.z;cloudInfo.initialGuessRoll  = roll;cloudInfo.initialGuessPitch = pitch;cloudInfo.initialGuessYaw   = yaw;cloudInfo.odomAvailable = true;// get end odometry at the end of the scanodomDeskewFlag = false;// 如果当前激光帧结束时刻之后没有imu里程计数据,返回if (odomQueue.back().header.stamp.toSec() < timeScanEnd)return;// 提取当前激光帧结束时刻的imu里程计nav_msgs::Odometry endOdomMsg;for (int i = 0; i < (int)odomQueue.size(); ++i){endOdomMsg = odomQueue[i];if (ROS_TIME(&endOdomMsg) < timeScanEnd)continue;elsebreak;}// 如果起止时刻对应imu里程计的方差不等,返回if (int(round(startOdomMsg.pose.covariance[0])) != int(round(endOdomMsg.pose.covariance[0])))return;Eigen::Affine3f transBegin = pcl::getTransformation(startOdomMsg.pose.pose.position.x, startOdomMsg.pose.pose.position.y, startOdomMsg.pose.pose.position.z, roll, pitch, yaw);tf::quaternionMsgToTF(endOdomMsg.pose.pose.orientation, orientation);tf::Matrix3x3(orientation).getRPY(roll, pitch, yaw);Eigen::Affine3f transEnd = pcl::getTransformation(endOdomMsg.pose.pose.position.x, endOdomMsg.pose.pose.position.y, endOdomMsg.pose.pose.position.z, roll, pitch, yaw);// 起止时刻imu里程计的相对变换Eigen::Affine3f transBt = transBegin.inverse() * transEnd;// 相对变换,提取增量平移、旋转(欧拉角)float rollIncre, pitchIncre, yawIncre;pcl::getTranslationAndEulerAngles(transBt, odomIncreX, odomIncreY, odomIncreZ, rollIncre, pitchIncre, yawIncre);odomDeskewFlag = true;
    }
    

projectPointCloud

  • 当前帧激光点云运动畸变校正
void projectPointCloud()
{int cloudSize = laserCloudIn->points.size();// range image projectionfor (int i = 0; i < cloudSize; ++i){PointType thisPoint;thisPoint.x = laserCloudIn->points[i].x;thisPoint.y = laserCloudIn->points[i].y;thisPoint.z = laserCloudIn->points[i].z;thisPoint.intensity = laserCloudIn->points[i].intensity;// 距离超出阈值时,舍弃float range = pointDistance(thisPoint);if (range < lidarMinRange || range > lidarMaxRange)continue;// 激光点`线数`不在范围内,舍弃int rowIdn = laserCloudIn->points[i].ring;if (rowIdn < 0 || rowIdn >= N_SCAN)continue;// 该点为降频点时,舍弃if (rowIdn % downsampleRate != 0)continue;// 计算水平角度,直接转为角度了 horizonAngle = 90 -anglefloat horizonAngle = atan2(thisPoint.x, thisPoint.y) * 180 / M_PI;static float ang_res_x = 360.0/float(Horizon_SCAN); // 角分辨率// 计算图像的列,[-180,180] int columnIdn = -round((horizonAngle-90.0)/ang_res_x) + Horizon_SCAN/2;if (columnIdn >= Horizon_SCAN)columnIdn -= Horizon_SCAN;// 列数不能超过范围,否则 舍弃if (columnIdn < 0 || columnIdn >= Horizon_SCAN)continue;if (rangeMat.at<float>(rowIdn, columnIdn) != FLT_MAX)continue;// 运动去畸变thisPoint = deskewPoint(&thisPoint, laserCloudIn->points[i].time);// rangeMat 存放的长度rangeMat.at<float>(rowIdn, columnIdn) = range;// fullCloud 存放去畸变后的点云int index = columnIdn + rowIdn * Horizon_SCAN;fullCloud->points[index] = thisPoint;}
}

deskewPoint

  • input: 点云去畸变,当前点+距离起点的时间戳
  • output: 去畸变后的点云
  • function:
    • 利用当前帧起止时刻之间的imu数据计算旋转增量,imu里程计数据计算平移增量,
    • 进而将每一时刻激光点位置变换到第一个激光点坐标系下,进行运动补偿
  • Code:
    PointType deskewPoint(PointType *point, double relTime)
    {if (deskewFlag == -1 || cloudInfo.imuAvailable == false)return *point;// relTime是当前激光点相对于激光帧起始时刻的时间,pointTime则是当前激光点的时间戳double pointTime = timeScanCur + relTime;float rotXCur, rotYCur, rotZCur;// 在当前激光帧起止时间范围内,计算某一时刻的旋转(相对于起始时刻的旋转增量)// 由imu队列计算得到findRotation(pointTime, &rotXCur, &rotYCur, &rotZCur);float posXCur, posYCur, posZCur;// 在当前激光帧起止时间范围内,计算某一时刻的平移(相对于起始时刻的平移增量)// 由里程计队列得到findPosition(relTime, &posXCur, &posYCur, &posZCur);// 第一个点的位姿增量(0),求逆if (firstPointFlag == true){transStartInverse = (pcl::getTransformation(posXCur, posYCur, posZCur, rotXCur, rotYCur, rotZCur)).inverse();firstPointFlag = false;}// transform points to start // 当前时刻激光点与第一个激光点的位姿变换Eigen::Affine3f transFinal = pcl::getTransformation(posXCur, posYCur, posZCur, rotXCur, rotYCur, rotZCur);Eigen::Affine3f transBt = transStartInverse * transFinal;PointType newPoint; // 当前激光点在第一个激光点坐标系下的坐标newPoint.x = transBt(0,0) * point->x + transBt(0,1) * point->y + transBt(0,2) * point->z + transBt(0,3);newPoint.y = transBt(1,0) * point->x + transBt(1,1) * point->y + transBt(1,2) * point->z + transBt(1,3);newPoint.z = transBt(2,0) * point->x + transBt(2,1) * point->y + transBt(2,2) * point->z + transBt(2,3);newPoint.intensity = point->intensity;return newPoint;
    }
    

cloudExtraction

  • 提取有效激光点,把长度不符合去掉,且只保留有效点
  • 记录了每条线 起始和终止5个点的索引 cloudInfo.startRingIndex/endRingIndex
void cloudExtraction()
{// 有效激光点数量int count = 0;// extract segmented cloud for lidar odometryfor (int i = 0; i < N_SCAN; ++i){// 记录每根扫描线起始第5个激光点在一维数组中的索引cloudInfo.startRingIndex[i] = count - 1 + 5;for (int j = 0; j < Horizon_SCAN; ++j){// 有效激光点,if (rangeMat.at<float>(i,j) != FLT_MAX){// mark the points' column index for marking occlusion later// 记录激光点对应的Horizon_SCAN方向上的索引cloudInfo.pointColInd[count] = j;// save range info// 激光点距离cloudInfo.pointRange[count] = rangeMat.at<float>(i,j);// save extracted cloud // 加入有效激光点extractedCloud->push_back(fullCloud->points[j + i*Horizon_SCAN]);// size of extracted cloud++count;}}// 记录每根扫描线倒数第5个激光点在一维数组中的索引cloudInfo.endRingIndex[i] = count -1 - 5;}
}

LIO-SAM imageProjection相关推荐

  1. SLAM相关学习资料:综述/激光/视觉/数据集/常用库

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 作者丨菠萝包包包@知乎 来源丨https://zhuanlan.zhihu.com/p/4348743 ...

  2. 八种常用激光雷达和视觉SLAM算法的评估与比较

    文章:Evaluation and comparison of eight popular Lidar and Visual SLAM algorithms 作者:Bharath Garigipati ...

  3. 国内外LiDAR SLAM实验室总结

    知乎上有一个关于视觉的SLAM实验室以及20年前的视觉SLAM开源算法的汇总SLAM 领域国内外优秀实验室汇总,非常详细,但是偏视觉一点,本文主要关注LiDAR SLAM. 这篇文章一开始是非常简短的 ...

  4. 激光SLAM论文简单导读--LOAM、VLOAM、LeGO-LOAM、LIO-SAM、LVI-SAM、LIMO、LIC-FUSION、TVL-SLAM、R2LIVE、R3LIVE

    激光SLAM论文简单导读--LOAM.VLOAM.LeGO-LOAM.LIO-SAM.LVI-SAM.LIMO.LIC-FUSION.TVL-SLAM.R2LIVE.R3LIVE 时间线 开篇巨作LO ...

  5. iSCSI target介绍及LIO实操入门

    前文介绍了iSCSI的基本架构及启动器的基本操作,也就是在客户端的操作.今天我们介绍一下目标器的相关概念.开源实现和基本操作.Linux操作系统下面有很多目标器的开源实现,比如LIO.SCST和TGT ...

  6. lio linux工具,Linux中三种SCSI target的介绍之LIO

    1. 简介 Linux-IO Target在Linux内核中(linux 2.6.38后),用软件实现各种SCSI Target,其支持的SAN技术中所有流行的存储协议包括Fibre Channel( ...

  7. 2019.03.01 bzoj2555: SubString(sam+lct)

    传送门 题意简述: 要求在线支持两个操作 (1):在当前字符串的后面插入一个字符串 (2):询问字符串s在当前字符串中出现了几次?(作为连续子串) 思路: 考虑用lctlctlct来动态维护samsa ...

  8. 4566: [Haoi2016]找相同字符 SAM

    折腾了好久.不过收获还是很多的.第一次自己去画SAM所建出来fail树.深入体会了这棵树的神奇性质. 当然,我最终靠着自己A掉了.(这是我第一次推SAM的性质(以前都是抄别人的,感觉自己好可耻),不过 ...

  9. BZOJ1396:识别子串(SAM)

    Description Input 一行,一个由小写字母组成的字符串S,长度不超过10^5 Output L行,每行一个整数,第i行的数据表示关于S的第i个元素的最短识别子串有多长. Sample I ...

  10. 由 粗 到 精 学 习 LVI-SAM: imageProjection模块

    一.LVI-SAM的节点关系 运行LVI-SAM,并打开rqt_graph,查看节点关系如图所示: 激光雷达原始数据的topic为/points_raw,观察发现它仅被imageProjection节 ...

最新文章

  1. 正则表达式之IP地址检验
  2. 网站怎样留住浏览用户
  3. mime java_MIME - Wei_java - 博客园
  4. docker registry push 覆盖_原创 | 全网最实在的docker入门教程四
  5. c语言编写简单的成绩管理系统,用c语言编写学生成绩管理系统
  6. python支持向量机回归_支持向量机——核函数与支持向量回归(附Python代码)
  7. win10系统自带图标/壁纸位置
  8. 京东大数据技术白皮书
  9. 淘宝钻石(信用)的等级
  10. 大师之路解惑基础概念篇:Tomcat与servlet联系与区别
  11. laravel 框架使用hdjs 实现富文本编辑器功能
  12. 编译器和编辑器的区别
  13. 人工智能机器学习入门资料免费送
  14. win python虚拟环境安装
  15. 【开发随记】【提效】工作习惯那些事系列之二——TOP3
  16. 火了几年的大前端,现在怎么样了?
  17. 反射型XSS攻击原理
  18. 行业前沿 | 数字孪生技术发展研究
  19. 通过实战探究 GraalVM 静态编译
  20. python panda 库_python基础库-Pandas

热门文章

  1. Fiddler抓取HTTPs流量
  2. rk3588调试之imx415摄像头
  3. 技术人从职场中脱颖而出的成长秘诀
  4. 《520七夕情人节表白礼物》:虚幻浪漫的爱情故事——❤520表白星空漫漫3D相册❤(HTML+CSS+JavaScript)...
  5. 英特尔第十代处理器为什么不支持win7_10代cpu能不能装win7?10代cpu装win7全面分析(支持十代...
  6. JavaScript (一) js的介绍及基本语法变量
  7. 教你如何制作浪漫的3D相册表白网站 HTML+CSS+JavaScript
  8. 服务器怎么查服务端数据
  9. 区块链黑暗森林自救手册
  10. C++ 智能指针(二) std::unique_ptr