cam_lidar_calibration代码详解(一)采样优化部分

news2024/12/28 11:55:54

目录

一、launch启动程序

1.1 run_optimiser.launch标定优化程序

1.2 assess_results.launch重投影误差评估程序

二、主要代码

2.1 feature_extraction_node.cpp文件

2.2 feature_extractor.cpp文件

2.2.1 FeatureExtractor::callback_camerainfo函数

2.2.2 serviceCB函数

2.2.3 compare_voq函数

2.2.4 FeatureExtractor::optimise函数

2.2.5 FeatureExtractor::publishBoardPointCloud函数

2.2.6 boundsCB函数

2.2.7 FeatureExtractor::visualiseSamples函数

2.2.8 FeatureExtractor::passthrough函数

2.2.9 FeatureExtractor::chessboardProjection函数

2.2.10 FeatureExtractor::locateChessboard函数

2.2.11 FeatureExtractor::extractBoard函数

2.2.12 FeatureExtractor::findEdges函数

2.2.14 FeatureExtractor::distoffset_passthrough 函数

2.2.15 FeatureExtractor::extractRegionOfInterest函数

2.2.16 FeatureExtractor::find_octant函数

2.2.17 FeatureExtractor::getDateTime函数

2.3 point_xyzir.h文件

2.4 load_params.cpp文件

2.5 optimiser.h文件


使用cam_lidar_calibration标定速腾激光雷达和单目相机外参(可见一班的博客)实现了功能,但是对于它内部的机理不是很懂。于是对采样、优化部分的代码进行了仔细阅读,同时把自己的自己的一些理解和笔记分享在这里,供大家一起探讨、进步!


一、launch启动程序

1.1 run_optimiser.launch标定优化程序

主要内容:

  • 传入参数import_samples决定执行示例/自己的参数
  • 执行feature_extraction_node.cpp
  • num_lowestvoq
  • 定义distance_offset_mm = 0,用于修正lidar内参
<?xml version="1.0" encoding="utf-8"?>

<!-- roslaunch cam_lidar_calibration run_optimiser.launch import_samples:=false -->

<launch>
    <!-- 读取params里的参数 -->
	<rosparam file="$(find cam_lidar_calibration)/cfg/params.yaml" />
	
    <!-- 执行的是feature_extraction_node.cpp -->
  	<node pkg="cam_lidar_calibration" type="feature_extraction_node" name="feature_extraction" output="screen">
		<param name="num_lowestvoq" type="int" value="50" /> 
		<param name="import_samples" value="$(arg import_samples)"/>
		<param name="import_path" value="$(find cam_lidar_calibration)/data/vlp/poses.csv"/>

		<!-- If your lidar is not calibrated well interally, it may require a distance offset (millimetres) on each point -->
		<param name="distance_offset_mm" value="0" /> 
  	</node>

  	<!-- Only open rviz and rqt if not importing samples -->
    <!-- 运行示例时,输入参数为import_samples:=false,不执行以下内容,不打开rviz和rqt_reconfigure -->
  	<group unless="$(arg import_samples)">
		<node type="rviz" name="rviz" pkg="rviz" args="-d $(find cam_lidar_calibration)/rviz/cam_lidar_calibration.rviz" />
		<node type="rqt_reconfigure" name="rqt_reconfigure" pkg="rqt_reconfigure"/>
	</group>

</launch>

1.2 assess_results.launch重投影误差评估程序

既使用camera_info,又在yaml中配置,为什么?因为要二次检验相机参数!

可以切换弧度和角度的表示。

  • 执行visualise_results.py
  • trans_binwidth
  • rot_binwidth_deg
  • visualise_pose_num
<!-- degree: true to display plot in degrees, false for radians -->
<param name="degree" value="false"/>

二、主要代码

首先,根据标定优化程序展开学习,也就是开源程序的第一部分。

2.1 feature_extraction_node.cpp文件

主函数,包含

  • 定义类,feature_extractor特征提取,调用feature_extractor.cpp里函数的功能
  • 参数优化 SimpleActionServer<cam_lidar_calibration::RunOptimiseAction> optimise_action,通过actionlib库实现,ROS的一种通讯机制action通信机制

简介:Action通信简介及案例1:发送单导航目标点_action 通信

2.2 feature_extractor.cpp文件

一系列函数名和命名空间定义在头文件feature_extractor.h里,命名空间为cam_lidar_calibration,含为类FeatureExtractor,具体包括以下函数:

 

2.2.1 FeatureExtractor::callback_camerainfo函数

功能:将相机参数从“camera_info”话题里读进来,包括内参和畸变系数。

2.2.2 serviceCB函数

与通讯机制action有关的函数

传入:Optimise::Request类型的req和Optimise::Response类型的res。

2.2.3 compare_voq函数

这个很简单,比较标定质量指标voq大小的。使用的数据格式是SetAssess,自定义类型。

 

2.2.4 FeatureExtractor::optimise函数

这个是cam_lidar_calibration标定程序包的核心思路。这里暂时只进行思路梳理,后面再仔细研究一下。总的目标是根据提取的特征,计算VOQ指标,对标定位姿进行优化。

功能1:根据启动程序时import_samples的值读取或写入参数,进行优化求解。import_samples为true时从文件里读取,也就是调用作者数据集的时候。import_samples为false时,往文件里写入。读取流程:按行读取数据,装入point3D向量row,写入时与此相反:

  1. 把read_samples里的数据读入到line
  2. 用逗号将line分割为word
  3. 把word装入向量line_double,一个一个push_back到后面
  4. 把{line_double[0], line_double[1], line_double[2]}装入row

包括相机中心、法向量;雷达中心、法向量;宽度、高度、偏移距离等等,19个为一组,。

功能2:从N个数据中构建3个为一组(set)的求解对象,如果少于100个样本,我们可以得到所有的100C3,再多一些样本,NC3就会变得太大,所以为了速度我们只能随机抽样。

2.2.5 FeatureExtractor::publishBoardPointCloud函数

发布激光点云采样的点,就是标定板上的点。

2.2.6 boundsCB函数

传入:boundsConfig类型的引用变量config和一个uint32_t类型的变量level

返回:void

功能:由cam_lidar_calibration::boundsConfig支持,重新读取gui中滑动条运动对应的值

2.2.7 FeatureExtractor::visualiseSamples函数

功能:可视化函数,显示提取激光点云提取到的标定板,绿色方框表述位置(单位为m),蓝色箭头表示法向量,赋值marker后进行发布。

2.2.8 FeatureExtractor::passthrough函数

功能:根据串口滑动条配置的区间,进行直通滤波。输入input_pc,按照xzy顺序进行滤波,定义了中间点云x、z,输出output_pc,不定义中间点云或者少定义中间点云可以节省内存。

2.2.9 FeatureExtractor::chessboardProjection函数

功能:提取图像中的棋盘格、标定板。标定板有四个角点和一个中心点;棋盘格有好多个内角点,一般加工方式下有白边。并且将世界坐标系下的物理坐标值,投影到图像坐标系下。

输入:角点向量corners和图像cv_ptr

输出:旋转矩阵、平移向量、板子的三维点(物理坐标,mm)

auto FeatureExtractor::chessboardProjection(const std::vector<cv::Point2d>& corners,
                                                const cv_bridge::CvImagePtr& cv_ptr)
    {
        // Find the chessboard in 3D space - in it's own object frame (position is arbitrary, so we place it flat)  
        // 在3D空间中找到棋盘——在它自己的对象框架中(位置是任意的,所以我们把它放平)

        // Location of board frame origin from the bottom left inner corner of the chessboard
        //从棋盘左下角内角开始的棋盘框架原点位置
        // 这段代码是用于生成棋盘格的三维坐标点的。首先,通过输入参数i_params中的棋盘格大小和方格长度计算出棋盘格左下角的坐标。然后,通过两个for循环遍历整个棋盘格,计算每个方格的三维坐标点,并将其存储在corners_3d向量中。具体地,对于每个方格,先计算出其在棋盘格中的二维坐标,然后将其乘以方格长度并减去棋盘格左下角的坐标,即可得到该方格的三维坐标点。最终,corners_3d向量中存储的就是整个棋盘格的所有三维坐标点。
        cv::Point3d chessboard_bleft_corner((i_params.chessboard_pattern_size.width - 1) * i_params.square_length / 2,
                                      (i_params.chessboard_pattern_size.height - 1)*i_params.square_length/2, 0);

        std::vector<cv::Point3d> corners_3d;
        for (int y = 0; y < i_params.chessboard_pattern_size.height; y++)
        {
            for (int x = 0; x < i_params.chessboard_pattern_size.width; x++)
            {
                corners_3d.push_back(cv::Point3d(x, y, 0) * i_params.square_length - chessboard_bleft_corner);
            }
        }

        // chessboard corners, middle square corners, board corners and centre
        std::vector<cv::Point3d> board_corners_3d;

        // Board corner coordinates from the centre of the chessboard
        // 这段代码是用于计算棋盘角点在三维空间中的坐标。其中,board_corners_3d是一个存储棋盘角点坐标的向量。代码中使用了cv::Point3d来表示三维坐标点,其中x、y、z分别表示点在x、y、z轴上的坐标值。i_params是一个结构体,包含了棋盘的尺寸和平移误差等参数。代码中通过计算棋盘中心点到棋盘四个角点的距离,来确定棋盘角点在三维空间中的坐标。具体来说,代码中先计算出棋盘中心点到棋盘左上角点的距离,然后根据棋盘尺寸和平移误差计算出棋盘左上角点在三维空间中的坐标,接着按照同样的方式计算出棋盘的其他三个角点的坐标。最后将这些坐标点存储到board_corners_3d向量中。
        board_corners_3d.push_back(cv::Point3d((i_params.board_dimensions.width - i_params.cb_translation_error.x)/2.0,
                                                (i_params.board_dimensions.height - i_params.cb_translation_error.y)/2.0,0.0));

        board_corners_3d.push_back(cv::Point3d(-(i_params.board_dimensions.width + i_params.cb_translation_error.x)/2.0,
                                               (i_params.board_dimensions.height - i_params.cb_translation_error.y)/2.0,0.0));

        board_corners_3d.push_back(cv::Point3d(-(i_params.board_dimensions.width + i_params.cb_translation_error.x)/2.0,
                                               -(i_params.board_dimensions.height + i_params.cb_translation_error.y)/2.0,0.0));

        board_corners_3d.push_back(cv::Point3d((i_params.board_dimensions.width - i_params.cb_translation_error.x)/2.0,
                                               -(i_params.board_dimensions.height + i_params.cb_translation_error.y)/2.0,0.0));
        
        // Board centre coordinates from the centre of the chessboard (due to incorrect placement of chessboard on board)从棋盘中心开始的棋盘中心坐标(由于棋盘格在板子上的位置不正确)
        board_corners_3d.push_back(cv::Point3d(-i_params.cb_translation_error.x/2.0, -i_params.cb_translation_error.y/2.0, 0.0));

        std::vector<cv::Point2d> inner_cbcorner_pixels, board_image_pixels;
        cv::Mat rvec(3, 3, cv::DataType<double>::type);  // Initialization for pinhole and fisheye cameras
        cv::Mat tvec(3, 1, cv::DataType<double>::type);

        if (valid_camera_info) {
            if (i_params.fisheye_model)
            {
                // Undistort the image by applying the fisheye intrinsic parameters
                // the final input param is the camera matrix in the new or rectified coordinate frame.
                // We put this to be the same as i_params_.cameramat or else it will be set to empty matrix by default.
                std::vector<cv::Point2d> corners_undistorted;
                cv::fisheye::undistortPoints(corners, corners_undistorted, i_params.cameramat, i_params.distcoeff,
                                             i_params.cameramat);
                cv::solvePnP(corners_3d, corners_undistorted, i_params.cameramat, cv::noArray(), rvec, tvec);
                cv::fisheye::projectPoints(corners_3d, inner_cbcorner_pixels, rvec, tvec, i_params.cameramat, i_params.distcoeff);
                cv::fisheye::projectPoints(board_corners_3d, board_image_pixels, rvec, tvec, i_params.cameramat,
                                           i_params.distcoeff);
            } else {
                // Pinhole model
                // 使用了OpenCV库中的solvePnP函数和projectPoints函数。solvePnP函数用于解决相机的位姿问题,即将3D物体坐标系中的点映射到2D图像坐标系中。它需要输入3D物体坐标系中的点和对应的2D图像坐标系中的点,以及相机的内参矩阵和畸变系数,输出相机的旋转向量和平移向量。projectPoints函数则用于将3D物体坐标系中的点投影到2D图像坐标系中,需要输入3D物体坐标系中的点、相机的旋转向量和平移向量,以及相机的内参矩阵和畸变系数,输出对应的2D图像坐标系中的点。这段代码中,corners_3d和corners分别表示3D物体坐标系中的点和对应的2D图像坐标系中的点,i_params.cameramat和i_params.distcoeff分别表示相机的内参矩阵和畸变系数,rvec和tvec分别表示相机的旋转向量和平移向量,inner_cbcorner_pixels和board_image_pixels分别表示对应的2D图像坐标系中的点。
                cv::solvePnP(corners_3d, corners, i_params.cameramat, i_params.distcoeff, rvec, tvec);
                cv::projectPoints(corners_3d, rvec, tvec, i_params.cameramat, i_params.distcoeff, inner_cbcorner_pixels);
                cv::projectPoints(board_corners_3d, rvec, tvec, i_params.cameramat, i_params.distcoeff, board_image_pixels);
            }
        } else {
            ROS_FATAL("No msgs from /camera_info - check camera_info topic in cfg/params.yaml is correct and is being published");
        }
        //检测到的角点,画圆
        for (int i = 0; i < board_image_pixels.size(); i++){
            if (i == 0){
                cv::circle(cv_ptr->image, board_image_pixels[i], 4, CV_RGB(255, 0, 0), -1);//红色,右下角
            } else if (i == 1) {
                cv::circle(cv_ptr->image, board_image_pixels[i], 4, CV_RGB(0, 255, 0), -1);
            } else if (i == 2) {
                cv::circle(cv_ptr->image, board_image_pixels[i], 4, CV_RGB(0, 0, 255), -1);
            } else if (i == 3) {
                cv::circle(cv_ptr->image, board_image_pixels[i], 4, CV_RGB(255, 255, 0), -1);//黄色
            } else if (i == 4) {
                cv::circle(cv_ptr->image, board_image_pixels[i], 4, CV_RGB(0, 255, 255), -1);//青色,中心偏移点
            }
        }

        for (auto& point : inner_cbcorner_pixels)
        {
            cv::circle(cv_ptr->image, point, 3, CV_RGB(255, 0, 0), -1);
        }

        //这段代码计算了一个棋盘格的 对角线长度 与 像素 之间的比例。首先,它使用了sqrt函数和pow函数来计算内部棋盘格角点的x和y坐标之间的距离,然后将它们平方相加并开方,得到了像素对角线的长度。接着,它使用同样的方法计算了3D角点的x和y坐标之间的距离,得到了实际对角线的长度。最后,将实际对角线的长度除以像素对角线的长度,得到了每个像素代表的实际长度,即米/像素。
        double pixdiagonal = sqrt(pow(inner_cbcorner_pixels.front().x-inner_cbcorner_pixels.back().x,2)+(pow(inner_cbcorner_pixels.front().y-inner_cbcorner_pixels.back().y,2)));

        double len_diagonal = sqrt(pow(corners_3d.front().x-corners_3d.back().x,2)+(pow(corners_3d.front().y-corners_3d.back().y,2)));
        metreperpixel_cbdiag = len_diagonal /(1000*pixdiagonal);

        // Return all the necessary coefficients
        return std::make_tuple(rvec, tvec, board_corners_3d);
    }

2.2.10 FeatureExtractor::locateChessboard函数

功能:调用OpenCV库中的函数findChessboardCorners提取图像中棋盘格角点,再使用cornerSubPix寻找亚像素坐标。最后再利用FeatureExtractor::chessboardProjection函数对图像角点进行投影。得到棋盘格的法向量。函数返回旋转、平移后的角点,和棋盘格法向量。

std::tuple<std::vector<cv::Point3d>, cv::Mat>
FeatureExtractor::locateChessboard(const sensor_msgs::Image::ConstPtr& image)
    {
        // Convert to OpenCV image object
        cv_bridge::CvImagePtr cv_ptr;
        cv_ptr = cv_bridge::toCvCopy(image, sensor_msgs::image_encodings::BGR8);

        cv::Mat gray;
        cv::cvtColor(cv_ptr->image, gray, CV_BGR2GRAY); //CV_BGR2GRAY是一个预定义的常量,表示将BGR颜色空间转换为灰度颜色空间。
        std::vector<cv::Point2f> cornersf;
        std::vector<cv::Point2d> corners;
        // Find chessboard pattern in the image
        // gray是输入的灰度图像,i_params.chessboard_pattern_size是棋盘格的大小,cornersf是输出的角点坐标,cv::CALIB_CB_ADAPTIVE_THRESH和cv::CALIB_CB_NORMALIZE_IMAGE是用于调整阈值和归一化图像的标志位。最终返回一个布尔值,表示是否找到了棋盘格角点。
        bool pattern_found = findChessboardCorners(gray, i_params.chessboard_pattern_size, cornersf,
                                                   cv::CALIB_CB_ADAPTIVE_THRESH + cv::CALIB_CB_NORMALIZE_IMAGE);
        if (!pattern_found)
        {
            ROS_WARN("No chessboard found");
            std::vector<cv::Point3d> empty_corners;
            cv::Mat empty_normal;
            return std::make_tuple(empty_corners, empty_normal);
        }
        ROS_INFO("Chessboard found");
        // Find corner points with sub-pixel accuracy
        // This throws an exception if the corner points are doubles and not floats!?!
        // 使用OpenCV库中的cornerSubPix函数对灰度图像中的角点进行亚像素级别的精确化处理。其中,gray表示输入的灰度图像,cornersf表示输入的角点坐标,Size(11, 11)表示窗口大小,Size(-1, -1)表示不使用搜索窗口,TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1)表示停止条件,即最大迭代次数为30次或者精度达到0.1。
        cornerSubPix(gray, cornersf, Size(11, 11), Size(-1, -1), TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 30, 0.1));

        for (auto& corner : cornersf)
        {
            corners.push_back(cv::Point2d(corner));
        }

        auto [rvec, tvec, board_corners_3d] = chessboardProjection(corners, cv_ptr); //使用OpenCV库中的函数chessboardProjection来进行棋盘格的投影。输出参数包括rvec(旋转向量)、tvec(平移向量)和board_corners_3d(棋盘格的三维坐标)
        // 使用OpenCV库中的函数对旋转向量进行转换,将其转换为旋转矩阵。然后创建一个三维点,其中z轴的值为-1,表示该点在z轴上的方向为向下。接着,使用旋转矩阵将该点的方向进行变换,得到棋盘的法向量。
        cv::Mat rmat;
        cv::Rodrigues(rvec, rmat);
        cv::Mat z = cv::Mat(cv::Point3d(0., 0., -1.)); // TODO: why is this normal -1 in z? Surabhi's is just 1
        auto chessboard_normal = rmat * z;

        std::vector<cv::Point3d> corner_vectors;
        for (auto& corner : board_corners_3d)
        {   
            cv::Mat m(rmat * cv::Mat(corner).reshape(1) + tvec);
            corner_vectors.push_back(cv::Point3d(m));
        }

        // Publish the image with all the features marked in it
        ROS_INFO("Publishing chessboard image");
        image_publisher.publish(cv_ptr->toImageMsg());
        return std::make_tuple(corner_vectors, chessboard_normal);
    }

2.2.11 FeatureExtractor::extractBoard函数

功能:

  1. 输入手动直通滤波后的点云,求出 z max,再减去对角线长度,得到 z min,再进行一次直通滤波,这个z min是粗略的,如果板子倾斜状态下,板子上的真实z min应该大一点。
  2. RANSAC拟合平面,迭代次数1000,阈值0.004m,得到平面表达式及法向量,对法向量反向、归一化
  3. 将板子上的点都投影到这个求得的平面表达式上,得到点云cloud_projected,并发布
    FeatureExtractor::extractBoard(const PointCloud::Ptr& cloud, OptimisationSample &sample)
    {
        PointCloud::Ptr cloud_filtered(new PointCloud);
        // Filter out the board point cloud
        // find the point with max height(z val) in cloud_passthrough
        pcl::PointXYZIR cloud_min, cloud_max;
        pcl::getMinMax3D(*cloud, cloud_min, cloud_max);
        double z_max = cloud_max.z;
        // subtract by approximate diagonal length (in metres)
        double diag = std::hypot(i_params.board_dimensions.height, i_params.board_dimensions.width) /
                      1000.0;  // board dimensions are in mm
        double z_min = z_max - diag;
        pcl::PassThrough<pcl::PointXYZIR> pass_z;
        pass_z.setFilterFieldName("z");
        pass_z.setFilterLimits(z_min, z_max);
        pass_z.setInputCloud(cloud);
        pass_z.filter(*cloud_filtered);  // board point cloud

        // Fit a plane through the board point cloud
        // Inliers give the indices of the points that are within the RANSAC threshold
        pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients());
        pcl::PointIndices::Ptr inliers(new pcl::PointIndices());
        pcl::SACSegmentation<pcl::PointXYZIR> seg;
        seg.setOptimizeCoefficients(true);
        seg.setModelType(pcl::SACMODEL_PLANE);
        seg.setMethodType(pcl::SAC_RANSAC);
        seg.setMaxIterations(1000);
        seg.setDistanceThreshold(0.004);
        pcl::ExtractIndices<pcl::PointXYZIR> extract;
        seg.setInputCloud(cloud_filtered);
        seg.segment(*inliers, *coefficients);

        // Check that segmentation succeeded
        PointCloud::Ptr cloud_projected(new PointCloud);
        if (coefficients->values.size() < 3)
        {
            ROS_WARN("Chessboard plane segmentation failed");
            cv::Point3d null_normal;
            return std::make_tuple(cloud_projected, null_normal);
        }

        // Plane normal vector magnitude
        cv::Point3d lidar_normal(coefficients->values[0], coefficients->values[1], coefficients->values[2]);
        lidar_normal /= -cv::norm(lidar_normal);  // Normalise and flip the direction  cv::norm函数用于计算向量的模长,然后将该向量除以其模长的相反数,即可实现归一化并翻转方向的操作。

        // Project the inliers on the fitted plane
        // When it freezes the chessboard after capture, what you see are the inlier points (filtered from the original)
        pcl::ProjectInliers<pcl::PointXYZIR> proj;
        proj.setModelType(pcl::SACMODEL_PLANE);
        proj.setInputCloud(cloud_filtered);
        proj.setModelCoefficients(coefficients);
        proj.filter(*cloud_projected);


        // Publish the projected inliers
        pc_samples_.push_back(cloud_projected);
        // publishBoardPointCloud();
        return std::make_tuple(cloud_projected, lidar_normal);
    }

2.2.12 FeatureExtractor::findEdges函数

功能:根据输入点云edge_pair_cloud,提取出两条边缘线。第一次执行:左上边缘的直线参数,左下边缘的直线参数 ;第二次执行:右上,右下 。

提取方法:RANSAC,阈值 0.02m

基本步骤:

    FeatureExtractor::findEdges(const PointCloud::Ptr& edge_pair_cloud)
    {
        pcl::ModelCoefficients full_coeff, half_coeff;
        pcl::PointIndices::Ptr full_inliers(new pcl::PointIndices), half_inliers(new pcl::PointIndices);
        PointCloud::Ptr half_cloud(new PointCloud);

        //拟合出第一条直线参数
        pcl::SACSegmentation<pcl::PointXYZIR> seg;
        seg.setModelType(pcl::SACMODEL_LINE);
        seg.setMethodType(pcl::SAC_RANSAC);
        seg.setDistanceThreshold(0.02);

        //分割出第一条直线外的点
        seg.setInputCloud(edge_pair_cloud);
        seg.segment(*full_inliers, full_coeff);  // Fitting line1 through all points

         // Failed RANSAC returns empty coeffs
        if (full_coeff.values.empty()) {
            return std::make_pair(full_coeff, full_coeff);
        }

        pcl::ExtractIndices<pcl::PointXYZIR> extract;
        extract.setInputCloud(edge_pair_cloud);
        extract.setIndices(full_inliers);
        extract.setNegative(true);      //第一条边界外的点云half_cloud
        extract.filter(*half_cloud);

        //拟合第二条直线参数
        seg.setInputCloud(half_cloud);
        seg.segment(*half_inliers, half_coeff);
        
         // Failed RANSAC returns empty coeffs        
        if (half_coeff.values.empty()) {
            return std::make_pair(half_coeff, half_coeff);
        }

        // Fitting line2 through outlier points
        // Determine which is above the other
        // 区分两条直线的上 下
        // 返回的一对里,大的再前,也就是上面的边缘线在前
        pcl::PointXYZIR full_min, full_max, half_min, half_max;
        pcl::getMinMax3D(*edge_pair_cloud, full_min, full_max);
        pcl::getMinMax3D(*half_cloud, half_min, half_max);
        if (full_max.z > half_max.z)
        {
            return std::make_pair(full_coeff, half_coeff);
        }
        else
        {
            return std::make_pair(half_coeff, full_coeff);
        }
    }

 

2.2.14 FeatureExtractor::distoffset_passthrough 函数

输入:点云input_pc

输出:点云output_pc

功能:补偿雷达内参,参数distance_offset由launch文件distance_offset_mm传入,默认为0,不执行补偿,该函数不产生效果。若虚补充lidar内参,执行流程如下:

  1. 笛卡尔坐标系转极坐标系,配合最后一个函数FeatureExtractor::find_octant区分所在象限
  2. 标记笛卡尔坐标系象限,通过 FeatureExtractor::find_octant实现
  3. 根据标记把水平夹角phi转换到[-90,90]内,以利用 atan 进行反变换到笛卡尔坐标系

 

2.2.15 FeatureExtractor::extractRegionOfInterest函数

传入:sensor_msgs::Image::ConstPtr& img和pcl::PointCloud<pcl::PointXYZIR>::ConstPtr& pc

返回:void

具体内容定义在feature_extractor.cpp中

功能:

  1. 确定lidar_ring数目,32
  2. 雷达内参修正,定义、处理、发布,FeatureExtractor::distoffset_passthrough
  3. 获取棋盘格信息,用FeatureExtractor::locateChessboard
  4. 赋值sample,由cam_lidar_calibration::OptimisationSample定义
  5. 找出每个ring对应的最大和最小点,FeatureExtractor::extractBoard实现
  6. 根据ring和每条ring的y坐标进行排序,雷达y轴指向侧边,x轴向前
  7. 通过最小点和最大值拟合直线,用FeatureExtractor::findEdges实现
  8. 计算标定板角度,构建向量(这里为什么用345?)、计算夹角 acos()+dot() 函数
  9. 找标定板四个角点,用PCL库中的函数lineWithLineIntersection实现,逆时针存储
  10. 还考虑到相机在lidar后面的情况,对lidar法向量反转方向
  11. 计算标定板检测边长、面积真值、检测面积、边长误差(论文中公式4)、距离
  12. 计算板子参数的误差be_dim_err
  13. 打印输出一次采样的信息
  14. 激光雷达板的误差超过真值的10%,则拒绝该采样。说明该次截取的点云检测效果不佳。终端报错"Plane fitting error, LiDAR board dimensions incorrect; discarding sample - try capturing again"

代码注释:

void FeatureExtractor::extractRegionOfInterest(const sensor_msgs::Image::ConstPtr& image,
                                                   const PointCloud::ConstPtr& pointcloud)
    {
        // Check if we have deduced the lidar ring count
        // load_params.h里定义的lidar_ring_count初始值为0
        if (i_params.lidar_ring_count == 0)
        {
            // pcl::getMinMax3D only works on x,y,z
            for (const auto& p : pointcloud->points)
            {
                if (p.ring + 1 > i_params.lidar_ring_count)
                {
                    i_params.lidar_ring_count = p.ring + 1;
                }
            } // 得到最大ring值
            lidar_frame_ = pointcloud->header.frame_id;  // frame_id赋值给lidar_frame_变量
        }

        // 定义、直通滤波、发布
        PointCloud::Ptr cloud_bounded(new PointCloud);
        distoffset_passthrough(pointcloud, cloud_bounded);

        // Publish the experimental region point cloud
        bounded_cloud_pub_.publish(cloud_bounded);

        if (flag == Optimise::Request::CAPTURE)  //load_params.h里定义的flag初始值为0
        {
            ROS_INFO("Processing sample");
            auto [corner_vectors, chessboard_normal] = locateChessboard(image);     //返回一个包含角点坐标的向量和一个表示棋盘平面法向量的向量,这两个向量被分别赋值给了corner_vectors和chessboard_normal。
            if (corner_vectors.size() == 0)
            {
                flag = Optimise::Request::READY;
                ROS_ERROR("Sample capture failed: can't detect chessboard in camera image");
                ROS_INFO("Ready to capture sample");
                return;
            }

            // 一系列赋值操作,定义在optimiser.h
            cam_lidar_calibration::OptimisationSample sample;
            num_samples++;
            sample.sample_num = num_samples;
            sample.camera_centre = corner_vectors[4];  // Centre of board
            corner_vectors.pop_back();
            sample.camera_corners = corner_vectors;
            sample.camera_normal = cv::Point3d(chessboard_normal);
            sample.pixeltometre = metreperpixel_cbdiag;

            // FIND THE MAX AND MIN POINTS IN EVERY RING CORRESPONDING TO THE BOARD
            // extractBoard定义在前面
            auto [cloud_projected, lidar_normal] = extractBoard(cloud_bounded, sample);
            if (cloud_projected->points.size() == 0)
            {
                return;
            }
            sample.lidar_normal = lidar_normal;

            // First: Sort out the points in the point cloud according to their ring numbers
            // 首先:根据点云的环数对点云中的点进行排序
            // 创建了一个vector容器ring_pointclouds,容器的大小为i_params.lidar_ring_count
            std::vector<PointCloud> ring_pointclouds(i_params.lidar_ring_count);

            for (const auto& point : cloud_projected->points)
            {
                ring_pointclouds[point.ring].push_back(point);
            }

            // Second: Arrange points in every ring in descending order of y coordinate
            // 第二:按y坐标降序排列每个环上的点
            // 遍历了一个名为ring_pointclouds的容器中的每一个元素,并将其赋值给了一个名为ring的引用
            // 排序的是y坐标
            for (auto& ring : ring_pointclouds)
            {
                std::sort(ring.begin(), ring.end(), [](pcl::PointXYZIR p1, pcl::PointXYZIR p2) { return p1.y > p2.y; });
            }

            // Third: Find minimum and maximum points in a ring 求环上的极小点和最大值
            // 如果ring不为空,则将ring中最后一个元素添加到min_points容器的末尾,将ring中第一个元素添加到max_points容器的末尾。这段代码的作用是将ring_pointclouds中每个元素的最后一个和第一个点分别添加到min_points和max_points容器中。
            PointCloud::Ptr max_points(new PointCloud);
            PointCloud::Ptr min_points(new PointCloud);
            for (const auto& ring : ring_pointclouds)
            {
                if (ring.size() == 0)
                {
                    continue;
                }
                min_points->push_back(ring[ring.size() - 1]);
                max_points->push_back(ring[0]);
            }

            // Fit lines through minimum and maximum points
            //调用了一个函数findEdges,返回了两个点的坐标top_left、bottom_left和top_right、bottom_right。然后判断这四个点的values是否为空,如果有任何一个为空,就会输出一个错误信息
            // 所以,应该修改坐标系!
            auto [top_left, bottom_left] = findEdges(max_points);
            auto [top_right, bottom_right] = findEdges(min_points);

            if (top_left.values.empty() | top_right.values.empty()
            | bottom_left.values.empty() | bottom_right.values.empty()) {
                ROS_ERROR("RANSAC unsuccessful, discarding sample - Need more lidar points on board");
                pc_samples_.pop_back();
                num_samples--;
                flag = Optimise::Request::READY;
                ROS_INFO("Ready for capture\n");
                return;
            }

            // Get angles of targetboard
            // 为什么是 345 ?  是到0点的向量
            cv::Mat top_left_vector = (cv::Mat_<double>(3,1) << top_left.values[3], top_left.values[4], top_left.values[5]);
            cv::Mat top_right_vector = (cv::Mat_<double>(3,1) << top_right.values[3], top_right.values[4], top_right.values[5]);
            cv::Mat bottom_left_vector = (cv::Mat_<double>(3,1) << bottom_left.values[3], bottom_left.values[4], bottom_left.values[5]);
            cv::Mat bottom_right_vector = (cv::Mat_<double>(3,1) << bottom_right.values[3], bottom_right.values[4], bottom_right.values[5]);
            
            double a0 = acos(top_left_vector.dot(top_right_vector))*180/M_PI;
            double a1 = acos(bottom_left_vector.dot(bottom_right_vector))*180/M_PI;
            double a2 = acos(top_left_vector.dot(bottom_left_vector))*180/M_PI;
            double a3 = acos(top_right_vector.dot(bottom_right_vector))*180/M_PI;
            sample.angles_0.push_back(a0);
            sample.angles_0.push_back(a1);
            sample.angles_1.push_back(a2);
            sample.angles_1.push_back(a3);

            // Find the corners
            // 3D Lines rarely intersect - lineWithLineIntersection has default threshold of 1e-4
            // PCL库中的函数lineWithLineIntersection来计算两条直线的交点,坐标放入c0~c4
            Eigen::Vector4f corner;
            pcl::lineWithLineIntersection(top_left, top_right, corner);
            cv::Point3d c0(corner[0], corner[1], corner[2]);
            pcl::lineWithLineIntersection(bottom_left, bottom_right, corner);
            cv::Point3d c1(corner[0], corner[1], corner[2]);
            pcl::lineWithLineIntersection(top_left, bottom_left, corner);
            cv::Point3d c2(corner[0], corner[1], corner[2]);
            pcl::lineWithLineIntersection(top_right, bottom_right, corner);
            cv::Point3d c3(corner[0], corner[1], corner[2]);

            // Add points in same order as the paper
            // Convert to mm
            sample.lidar_corners.push_back(c3 * 1000);
            sample.lidar_corners.push_back(c0 * 1000);
            sample.lidar_corners.push_back(c2 * 1000);
            sample.lidar_corners.push_back(c1 * 1000);

            for (const auto& p : sample.lidar_corners)
            {
                // Average the corners
                sample.lidar_centre.x += p.x / 4.0;
                sample.lidar_centre.y += p.y / 4.0;
                sample.lidar_centre.z += p.z / 4.0;
            }

            // Flip the lidar normal if it is in the wrong direction (mainly happens for rear facing cameras)
            // Sample属于OptimisationSample
            // 一个点到一个向量的距离,若大于圆的半径,将向量反向,为啥???看lidar_normal解决
            double top_down_radius = sqrt(pow(sample.lidar_centre.x,2)+pow(sample.lidar_centre.y,2));
            double vector_dist = sqrt(pow(sample.lidar_centre.x + sample.lidar_normal.x,2) +
            	pow(sample.lidar_centre.y + sample.lidar_normal.y,2));
            if (vector_dist > top_down_radius) {
            	sample.lidar_normal.x = -sample.lidar_normal.x;
            	sample.lidar_normal.y = -sample.lidar_normal.y;
            	sample.lidar_normal.z = -sample.lidar_normal.z;
            }

            // Get line lengths for comparison with real board dimensions
            std::vector <double> lengths;
            lengths.push_back(sqrt(pow(c0.x - c3.x, 2) + pow(c0.y - c3.y, 2) + pow(c0.z - c3.z, 2)) * 1000);
            lengths.push_back(sqrt(pow(c0.x - c2.x, 2) + pow(c0.y - c2.y, 2) + pow(c0.z - c2.z, 2)) * 1000);
            lengths.push_back(sqrt(pow(c1.x - c3.x, 2) + pow(c1.y - c3.y, 2) + pow(c1.z - c3.z, 2)) * 1000);
            lengths.push_back(sqrt(pow(c1.x - c2.x, 2) + pow(c1.y - c2.y, 2) + pow(c1.z - c2.z, 2)) * 1000);

            std::sort(lengths.begin(), lengths.end());      //默认为less<T>(),即升序排序,如果需要降序排序,可以使用greater<T>()
            double w0 = lengths[0];
            double w1 = lengths[1];
            double h0 = lengths[2];
            double h1 = lengths[3];
            sample.widths.push_back(w0);
            sample.widths.push_back(w1);
            sample.heights.push_back(h0);
            sample.heights.push_back(h1);

            // 面积真值
            double gt_area = (double)i_params.board_dimensions.width/1000*(double)i_params.board_dimensions.height/1000;
            // 检测面积 = 小*小/2 + 大*大/2
            double b_area = (w0/1000*h0/1000)/2 + (w1/1000*h1/1000)/2;

            // Board dimension errors
            double w0_diff = abs(w0 - i_params.board_dimensions.width);
            double w1_diff = abs(w1 - i_params.board_dimensions.width);
            double h0_diff = abs(h0 - i_params.board_dimensions.height);
            double h1_diff = abs(h1 - i_params.board_dimensions.height);
            double be_dim_err = w0_diff + w1_diff + h0_diff + h1_diff;

            double distance = sqrt(pow(sample.lidar_centre.x/1000-0, 2) + pow(sample.lidar_centre.y/1000-0, 2) + pow(sample.lidar_centre.z/1000-0, 2));
            sample.distance_from_origin = distance;

            printf("\n--- Sample %d ---\n", num_samples);
            printf("Measured board has: dimensions = %dx%d mm; area = %6.5f m^2\n", i_params.board_dimensions.width, i_params.board_dimensions.height, gt_area);    //%6.5f表示输出浮点数,保留小数点后5位,整个输出文本中占据6个字符的宽度
            printf("Distance = %5.2f m\n", sample.distance_from_origin);
            printf("Board angles     = %5.2f,%5.2f,%5.2f,%5.2f degrees\n",a0, a1, a2, a3);
            printf("Board area       = %7.5f m^2 (%+4.5f m^2)\n", b_area, b_area-gt_area);
            printf("Board avg height = %6.2fmm (%+4.2fmm)\n", (h0+h1)/2, (h0+h1)/2-i_params.board_dimensions.height);
            printf("Board avg width  = %6.2fmm (%+4.2fmm)\n", (w0+w1)/2, (w0+w1)/2-i_params.board_dimensions.width);
            printf("Board dim        = %6.2f,%6.2f,%6.2f,%6.2f mm\n", w0, h0, h1, w1);
            printf("Board dim error  = %7.2f\n\n", be_dim_err);

            // If the lidar board dim is more than 10% of the measured, then reject sample
            if (abs(w0-i_params.board_dimensions.width) > i_params.board_dimensions.width*0.1 |
                abs(w1 - i_params.board_dimensions.width) > i_params.board_dimensions.width * 0.1 |
                abs(h0 - i_params.board_dimensions.height) > i_params.board_dimensions.height * 0.1 |
                abs(h1 - i_params.board_dimensions.height) > i_params.board_dimensions.height * 0.1) {
                ROS_ERROR("Plane fitting error, LiDAR board dimensions incorrect; discarding sample - try capturing again");
                pc_samples_.pop_back();
                num_samples--;
                flag = Optimise::Request::READY;
                ROS_INFO("Ready for capture\n");
                return;
            }

            ROS_INFO("Found line coefficients and outlined chessboard");
            publishBoardPointCloud();

            // cv_bridge库中的toCvCopy函数将ROS图像消息image转换为BGR8格式的OpenCV图像
            cv_bridge::CvImagePtr cv_ptr;
            cv_ptr = cv_bridge::toCvCopy(image, sensor_msgs::image_encodings::BGR8);

            // Save image 
            if(boost::filesystem::create_directory(newdatafolder))
            {   
                boost::filesystem::create_directory(newdatafolder + "/images");
                boost::filesystem::create_directory(newdatafolder + "/pcd");
                ROS_INFO_STREAM("Save data folder created at " << newdatafolder);
            } 
            std::string img_filepath = newdatafolder + "/images/pose" + std::to_string(num_samples)  + ".png" ;              
            std::string target_pcd_filepath = newdatafolder + "/pcd/pose" + std::to_string(num_samples)  + "_target.pcd" ;              
            std::string full_pcd_filepath = newdatafolder + "/pcd/pose" + std::to_string(num_samples)  + "_full.pcd" ;              
            
            // 使用ROS中的ROS_ASSERT宏来确保cv::imwrite函数成功将cv_ptr指向的图像保存到img_filepath指定的文件中。如果保存失败,ROS_ASSERT宏将会抛出一个错误。
            ROS_ASSERT( cv::imwrite( img_filepath,  cv_ptr->image ) );   
            pcl::io::savePCDFileASCII (target_pcd_filepath, *cloud_bounded);
            pcl::io::savePCDFileASCII (full_pcd_filepath, *pointcloud);
            ROS_INFO_STREAM("Image and pcd file saved");    //ROS_INFO_STREAM是ROS中用于输出信息的一个函数,可以将信息输出到ROS的日志系统中,方便调试和记录程序运行状态。


            if (num_samples == 1){
                // Check if save_dir has camera_info topic saved
                std::string pkg_path = ros::package::getPath("cam_lidar_calibration");

                std::ofstream camera_info_file;
                std::string camera_info_path = pkg_path + "/cfg/camera_info.yaml";
                ROS_INFO_STREAM("Camera_info saved at: " << camera_info_path);
                camera_info_file.open(camera_info_path, std::ios_base::out | std::ios_base::trunc);     // 以写入模式打开(即std::ios_base::out),如果文件已经存在则清空文件内容(即std::ios_base::trunc)
                std::string dist_model = (i_params.fisheye_model) ? "fisheye": "non-fisheye";
                camera_info_file << "distortion_model: \"" << dist_model << "\"\n";
                camera_info_file << "width: " << i_params.image_size.first << "\n";
                camera_info_file << "height: " << i_params.image_size.second << "\n";
                camera_info_file << "D: [" << i_params.distcoeff.at<double>(0)
                                        << "," << i_params.distcoeff.at<double>(1)
                                        << "," << i_params.distcoeff.at<double>(2)
                                        << "," << i_params.distcoeff.at<double>(3) << "]\n";
                camera_info_file << "K: [" << i_params.cameramat.at<double>(0,0)
                                        << ",0.0"
                                        << "," << i_params.cameramat.at<double>(0,2)
                                        << ",0.0"
                                        << "," << i_params.cameramat.at<double>(1,1)
                                        << "," << i_params.cameramat.at<double>(1,2)
                                        << ",0.0,0.0" 
                                        << "," << i_params.cameramat.at<double>(2, 2) 
                                        << "]\n";
                camera_info_file.close();
            }

            // Push this sample to the optimiser
            optimiser_->samples.push_back(sample);
            flag = Optimise::Request::READY;  // Reset the capture flag
            ROS_INFO("Ready for capture\n");
        }  // if (flag == Optimise::Request::CAPTURE)
    }  // End of extractRegionOfInterest

2.2.16 FeatureExtractor::find_octant函数

功能:将点具体划分到 8 个坐标系象限,并添加标签,在2.2.14函数中使用

输入:xyz坐标

输出:坐标系标签1~8

2.2.17 FeatureExtractor::getDateTime函数

功能:获取时间作为文件存储路径,在 FeatureExtractor::optimise里使用。

// Get current date/time, format is YYYY-MM-DD-HH:mm:ss
// 获取当前时间并将其格式化为ISO 8601标准的日期时间字符串,不包含时区信息。
    std::string FeatureExtractor::getDateTime()
    {
        auto time = std::time(nullptr);     //它返回当前的系统时间(以秒为单位),并将其转换为 std::time_t 类型的值
        std::stringstream ss;
        ss << std::put_time(std::localtime(&time), "%F_%T"); // ISO 8601 without timezone information.
        auto s = ss.str();      //将stringstream对象ss中存储的字符串转换为标准字符串(string类型)
        std::replace(s.begin(), s.end(), ':', '-');     //该函数属于 C++ STL 中的 algorithm 头文件中的函数,其功能是在指定范围内将指定值替换为另一个值。
        return s;
    }

2.3 point_xyzir.h文件

用到的点云类型,XYZIR,格式和速腾的一致,所以可以直接执行程序而不用转velodyne雷达。

namespace pcl
{
struct PointXYZIR
{
  PCL_ADD_POINT4D;
  float intensity;
  uint16_t ring;
  EIGEN_MAKE_ALIGNED_OPERATOR_NEW
} EIGEN_ALIGN16;
}  // namespace pcl

POINT_CLOUD_REGISTER_POINT_STRUCT(PointXYZIR, (float, x, x)(float, y, y)(float, z, z)(float, intensity,
                                                                                      intensity)(uint16_t, ring, ring))

2.4 load_params.cpp文件

引用头文件load_params.h,里面定义了初始参数initial_parameters_t。通过loadParams从ROS参数服务器中读取参数,并将这些参数存储在initial_parameters_t类型的变量i_params中。其中,ros::NodeHandle类型的引用n用于指定参数服务器的命名空间。

功能:

  1. 定义变量名
  2. 从params.yaml文件中读取数据,并赋值给变量

2.5 optimiser.h文件

功能:定义FeatureExtractor::optimise优化函数用到的数据结构,SetAssess等。

暂时没有仔细分析这部分计算过程,后期补上!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/617576.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

QT快速操作Excel的实现介绍及操作类封装

QT中操作Excel还是比较简单的&#xff0c;Qt提供了QAxObject&#xff0c;包装COM组件的类&#xff0c;通过COM通过COM操作使用QAxObject类&#xff0c;使用此类&#xff0c;需要在pro文件中添加"QT   axcontainer "。 基本流程介绍 QAxObject&#xff1a; QAxObj…

SAP从入门到放弃系列之pMRP

最近学习pMRP&#xff0c;查了很多博客&#xff0c;机翻一篇内容非常详细的文章&#xff0c;感谢大佬&#xff1a; 原文地址&#xff1a; pMRP–Predictive Material and Resource Planning in SAP S/4HANA : Step by Step execution pMRP https://blogs.sap.com/2020/04/14/p…

机器学习【线性回归】

机器学习【线性回归】 回归预测的结果是离散型变量&#xff0c;身高和年龄 损失函数&#xff1a;SSE&#xff08;误差平方和&#xff09;&#xff0c;RSS&#xff08;残差平方和&#xff09;&#xff0c;误差越大越差 最小二乘法&#xff1a;通过最小化真实值和预测值之间的…

大数据:sparkSQL,历史,DataSet,DataFrame,sparkSession

大数据&#xff1a;sparkSQL 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;可能很多算法学生都得去找开发&#xff0c;测开 测开的话&#xff0c;你就得学数据库&#xff0c;sql&#xff0c;oracle&#xff0c;尤其sql要学…

第十章:创建和管理表

第十章&#xff1a;创建和管理表 10.1&#xff1a;基础知识 一条数据存储的过程 ​ 存储数据是处理数据的第一步。只有正确地把数据存储起来&#xff0c;我们才能进行有效的处理和分析。否则&#xff0c;只能是一团乱麻&#xff0c;无从下手。 ​ 在MySQL中&#xff0c;一个完…

使用模板方法模式封装协议消息

目录 整体框架 模板方法介绍 关于本案例设计 c impl惯用法 cimpl惯用法好处 此案例impl惯用法的设计 关于序列化和反序列化 序列化和反序列化 本项目使用介绍 谷歌测试 谷歌测试环境 谷歌测试用例 完整源码地址 概述 本文介绍了从 设计模式之模板方法模式协议消…

面对CPU狂飙时的5步解决方案

现在企业对后端开发的要求越来越高&#xff0c;不仅要求我们会写代码&#xff0c;还要我们能够进行部署和运维&#xff01; 项目上线并运行一段时间后&#xff0c;可能会发现部署所在的Linux服务器CPU占用过高&#xff0c;该如何排查解决&#xff1f; 本文用5步带你搞定线上CPU…

操作系统-进程和线程-同步、互斥、死锁

目录 一、同步互斥 二、互斥的实现方法 2.1软件实现 2.1.1单标志法 2.1.2双标志先检查 2.1.3双标志后检查 2.1.4Petersons算法 2.2硬件实现 2.2.1 TestAndSet指令 2.2.2 Swap指令 三、信号量机制 3.1整形变量 3.2 记录型变量 3.3用信号量实现进程互斥、同步、前驱关系…

Sui与F1甲骨文红牛车队达成合作

在近期达成的一项为期多年的合作协议中&#xff0c;甲骨文红牛车队将利用Sui网络开发&#xff0c;为粉丝带来全新的数字化体验。 甲骨文红牛车队的粉丝将很快在Sui网络上体验到他们最爱的一级方程式车队带来的激情。最近几个赛季一直统治着F1赛场的甲骨文红牛车队&#xff0c;与…

代码随想录算法训练营第五十三天 | 力扣 1143.最长公共子序列, 1035.不相交的线, 53. 最大子序和

1143.最长公共子序列 题目 1143. 最长公共子序列 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 &#xff0c;返回 0 。 一个字符串的 子序列 是指这样一个新的字符串&#xff1a;它是由原字符串在不改变字符…

2022年值得关注的7个主要SD-WAN趋势

随着SD-WAN技术在2022年继续发展成熟&#xff0c;该技术在集成远程访问、自动化和多云连接方面的支持有望得到更多的改进。 软件定义WAN仍然是增强用户体验(UX)&#xff0c;提高安全性&#xff0c;以及提供与基于云计算的应用程序的连接的一项关键技术。 随着SD-WAN的成熟&…

java设计模式之:原型模式

在我们的生活中&#xff0c;有很多例子&#xff0c;都是用到了原型模式&#xff0c;例如&#xff1a;我们去配钥匙的时候&#xff0c;肯定先要有一个原配钥匙才可以去配钥匙&#xff1b;《西游记》中孙悟空可以用猴毛根据自己的形象&#xff0c;复制&#xff08;又称“克隆”或…

时间序列预测 | Matlab基于北方苍鹰算法优化随机森林(NGO-RF)与随机森林(RF)的时间序列预测对比

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 时间序列预测 | Matlab基于北方苍鹰算法优化随机森林(NGO-RF)与随机森林(RF)的时间序列预测对比 评价指标包括:MAE、RMSE和R2等,代码质量极高,方便学习和替换数据。要求2018版本及以上。 部分源码 %-----------…

【Apache-Flink零基础入门】「入门到精通系列」手把手+零基础带你玩转大数据流式处理引擎Flink(基础概念解析)

手把手零基础带你玩转大数据流式处理引擎Flink 前言介绍Apache Flink 的定义、架构及原理Flink应用服务Streams有限数据流和无限数据流的区别 StateTimeAPI Flink架构体系 Flink操作处理Flink 的应用场景Flink 的应用场景&#xff1a;Data Pipeline实时数仓搜索引擎推荐 Flink …

华为诺亚 VanillaNet

文章标题&#xff1a;《VanillaNet: the Power of Minimalism in Deep Learning》 文章地址&#xff1a;https://arxiv.org/abs/2305.12972 github地址&#xff1a;https://github.com/huawei-noah/VanillaNet 华为诺亚方舟实验室和悉尼大学&#xff0c;2023年5月代码刚开源的…

基于HAL库的STM32的单定时器的多路输入捕获测量脉冲频率(外部时钟实现)

目录 写在前面 一般的做法&#xff08;定时器单通道输入捕获) 以外部时钟的方式(高低频都适用) 测试效果 写在前面 STM32的定时器本身有输入捕获的功能。可选择双端捕获&#xff0c;上升沿捕获或者是下降沿捕获。对应捕获频率来说,连续捕获上升沿或下降沿的时间间隔就是其脉…

手把手教你F103工程文件的创建并且通过protesu仿真验证创建工程文件的正确性(低成本)

目录 一、新建工程文件夹 二、新建一个工程框架 三、添加文件 四、仿真验证 五、仿真调试中遇到的问题并解决 一、新建工程文件夹 新建工程文件夹分为 2 个步骤&#xff1a;1&#xff0c;新建工程文件夹&#xff1b;2&#xff0c;拷贝工程相关文件。 1.新建工程文件 首先…

【04】STM32·HAL库开发-MDK5使用技巧 |文本美化 | 代码编辑技巧 | 查找与替换技巧 | 编译问题定位 | 窗口视图初始化

目录 1.文本美化&#xff08;熟悉&#xff09;1.1编辑器设置1.2字体和颜色设置1.3用户关键字设置1.4代码提示&语法检测1.5global.prop文件妙用 2.代码编辑技巧&#xff08;熟悉&#xff09;2.1Tab键的妙用2.2快速定位函数或变量被定义的地方2.3快速注释&快速取消注释 3…

python面向对象操作2(速通版)

目录 一、私有和公有属性的定义和使用 1.公有属性定义和使用 2.私有属性 二、继承 1.应用 2.子类不能用父类的私有方法 3.子类初始化父类 4.子类重写和调用父类方法 5.多层继承 6.多层继承-初始化过程 7.多继承基本格式 8.多层多继承时的初始化问题 9.多继承初始化…

云原生Docker Cgroups资源控制操作

资源控制 Docker 通过 Cgroup 来控制容器使用的资源配额&#xff0c;包括 CPU、内存、磁盘三大方面&#xff0c; 基本覆盖了常见的资源配额和使用量控制。 Cgroup 是 ControlGroups 的缩写&#xff0c;是 Linux 内核提供的一种可以限制、记录、隔离进程组所使用的物理资源(如…