SLAM算法与工程实践——SLAM基本库的安装与使用(6):g2o优化库(4)构建g2o的边

SLAM算法与工程实践系列文章

下面是SLAM算法与工程实践系列文章的总链接,本人发表这个系列的文章链接均收录于此

SLAM算法与工程实践系列文章链接


下面是专栏地址:

SLAM算法与工程实践系列专栏


文章目录

  • SLAM算法与工程实践系列文章
    • SLAM算法与工程实践系列文章链接
    • SLAM算法与工程实践系列专栏
  • 前言
  • SLAM算法与工程实践——SLAM基本库的安装与使用(6):g2o优化库(4)
    • 初步认识图的边
    • 如何自定义边
    • 如何向图中添加边
      • 添加一元边
      • 添加二元边
    • 补充


前言

这个系列的文章是分享SLAM相关技术算法的学习和工程实践


SLAM算法与工程实践——SLAM基本库的安装与使用(6):g2o优化库(4)

初步认识图的边

3种类型——BaseUnaryEdge、BaseBinaryEdge和BaseMultiEdge,它们分别表示一元边、二元边和多元边。

在这里插入图片描述

通常是二元边为主

比如我们用边表示三维点投影到图像平面上的重投影误差,就可以设置如下输入参数。

BaseBinaryEdge<2,Vector2D,VertexSBAPointXYZ,VertexSE3Expmap>

BaseBinaryEdge类型的边是一个二元边。

第1个参数“2”是说测量值是二维的,测量值就是图像的二维像素坐标,对应测量值的类型是Vector2D,边连接的两个顶点分别是三维点 VertexSBAPointXYZ 和李群位姿 VertexSE3Expmap

常用的函数,成员变量

// 读/写函数,一般情况下不需要进行读/写操作,仅声明一下就可以
virtual bool read(std::istream& is);
virtual bool write(std::ostream& os) const;

// 使用当前顶点的值计算的测量值与真实的测量值之间的误差
virtual void computeError ();

//误差对优化变量的偏导数,也就是我们说的 Jacobian
virtual void linearizeoplus ();


// 几个重要的成员变量和函数
_measurement		// 存储观测值
_error					// 存储计算的误差
_vertices[]			// 存储顶点信息


setVertex(int,vertex)		// 定义顶点及其编号
setId(int)		// 定义边的编号
setMeasurement(type)		// 定义观测值
setInformation()		// 定义信息矩阵

如何自定义边

g2o中边的模板

//g2o中边的定义格式
class myEdge:public g2o:BaseBinaryEdge<errorDim,errorType,Vertex1Type,Vertex2Type>
{
	public:
		EIGEN_MAKE_ALIGNED_OPERATOR_NEW
		myEdge(){}
		// 读/写函数
		virtual bool read(istream& in){}
		virtual bool write(ostream& out) const {}

  	//误差=测量值-估计值
		virtual void computeError() override
		{
			_error = _measurement - /*估计值*/;
		}

  	//增量计算函数:误差对优化变量的偏导数
		virtual void linearizeOplus() override
		{
			_jacobianoplusxi(pos,pos)=something;
			_jocobianOplusxj(pos,pos)=something;
		}
}

曲线拟合中一元边的简单例子

//曲线拟合中一元边的简单例子
class CurveFittingEdge:public g2o::BaseUnaryEdge<1,double,CurveFittingVertex>
{
	public:
		EIGEN_MAKE_ALIGNED_OPERATOR_NEW
    CurveFittingEdge(double x) : BaseUnaryEdge (), _x(x){}
		//计算曲线模型误差
		void computeError()
    {
        const CurveFittingVertex* v = static_cast<const CurveFittingVertex*>(vertices[0]);
				const Eigen::Vector3d abc = v->estimate();
				//曲线模型为a*×^2+b*x+c
				//误差=测量值-估计值
				_error(0,0) = _measurement - std:exp(abc(0,0)*_x*_x + abc(1,0)*x + 
abc(2,0));
			}
			//读/写函数
			virtual bool read(istream& in){}
			virtual bool write (ostream& out) const {}
	public:
		double _x;
};  

稍微复杂的例子,涉及3D-2D点的PP问题,也就是最小化重投影误差问题

// g2o/types/sba/edge project xyz2uv.h,g2o/types/sba/edge project xyz2uv.cpp
// PnP问题中三维点投影到二维图像上二元边定义示例
class g2o_TYPES_SBA_API EdgeProjectXYZ2UV : public BaseBinaryEdge<2,Vector2,VertexPointXYZ,VertexSE3Expmap>
{
	public:
	EIGEN MAKE ALIGNED OPERATOR NEW;
EdgeprojectXYZ2UV();
	//读/写函数
	bool read(std:istream& is);
	bool write(std::ostream& os) const;
	//计算误差
	void computeError();
	//增量计算函数
	virtual void linearizeOplus();
	//相机参数
	CameraParameters* _cam;
};

void EdgeProjectXYZ2UV::computeError()
{
  //将顶点中李群相机位姿记为v1
	const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
	//将顶点中三维点记为v2
	const VertexPointXYZ* v2 = static_cast<const VertexPointXYZ*>(vertices[0]);
const CameraParameters*cam static cast<const CameraParameters*>
(parameter(0));
	//误差=测量值-估计值
  _error = measurement()- cam->cam_map(v1->estimate().map(v2->estimate()));
}


// 增量计算函数:误差对优化变量的偏导数
void EdgeprojectXYZ2UV::linearizeOplus()
{
    VertexSE3Expmap* vj = static_cast<VertexSE3Expmap*>(vertices[1]);
    SE3Quat T(vj->estimate());
    VertexPointXYZ* vi = static_cast<VertexPointXYZ*>(vertices[0]);
    Vector3 xyz = vi->estimate();
    Vector3 xyz_trans = T.map(xyz);
    
    number_t x = xyz_trans[0];
    number_t y = xyz_trans[1];
    number_t z = xyz_trans[2];
    number_t z_2 = z * z;

    const CameraParameters* cam = static_cast<const CameraParameters*>(parameter(0));
    //重投影误差关于三维点的雅可比矩阵
    Eigen::Matrix<number_t,2,3,Eigen::ColMajor> tmp;
    tmp(0,0) = cam->focal_length;
    tmp(0,1) = 0:
    tmp(0,2) = -x / z * cam->focal_length;
    
    tmp(1,0) = 0;
    tmp(1,1) = cam->focal_length;
    tmp(1,2) = -y / z * cam->focal_length;
    
    _jacobianOplusXi = -1. / z * tmp * T.rotation().toRotationMatrix();

    //重投影误差关于相机位姿的雅可比矩阵
    _jacobianOplusXj(0,0) = x * y / z_2 * cam->focal_length;
    _jacobianOplusXj(0,1) = -(1 + (x * x / z_2)) * cam->focal_length;
    _jacobianOplusXj(0,2) = y / z * cam->focal_length;
    _jacobianOplusXj(0,3) = -1. / z * cam->focal_length;
    _jacobianOplusXj(0,4) = 0;
    _jacobianOplusXj(0,5) = x / z_2 * cam->focal_length;
    _jacobianOplusXj(1,0) = (1 + y * y / z_2) * cam->focal_length;
    _jacobianOplusXj(1,1) = -x * y / z_2 * cam->focal_length;
    _jacobianOplusXj(1,2) = -x / z * cam->focal_length;
    _jacobianOplusXj(1,3) = 0;
    _jacobianOplusXj(1,4) = -1./ z * cam->focal_length;
    _jacobianOplusXj(1,5) = y /z_2 * cam->focal_length;
}

其中有一些比较难理解的地方,我们分别解释。

首先是误差的计算:

//误差=测量值-估计值
_error = measurement()- cam->cam_map(v1->estimate().map(v2->estimate()));

这里的本质是误差 = 测量值 - 估计值。下面梳理一下思路。

我们先来看 cam_map 函数,它的功能是把相机坐标系下的三维点(输入)用内参转换为图像坐标(输出),具体定义如下。

// g2o/types/sba/types_six_dof_expmap.cpp
// cam_map函数定义
Vector2 CameraParameters::cam_map(const Vector3 & trans_xyz) const{
	Vector2 proj = project2d(trans_xyz);
	Vector2 res;
	res[0] = proj[0]*focal_length + principle_point[0];
	res[1] = proj[1]*focal_length + principle_point[1];
	return res;
}

然后看 map 函数,它的功能是把世界坐标系下的三维点转换到相机坐标系下,定义如下

// g2o/types/sim3/sim3.h
// map函数定义
Vector3 map (const Vector3& xyz) const
{
  return s*(r*xyz)+t;
}

因此,下面的代码就是用 v1 估计的位姿把 v2 代表的三维点转换到相机坐标系下。

v1->estimate().map(v2->estimate())

linearizeOplus() 重投影误差关于相机位姿的雅可比矩阵为
∂ e ∂ δ ξ = [ f x X Y Z 2 − f x − f x X 2 Z 2 f x Y Z − f x Z 0 f x X Z 2 f y + f y Y 2 Z 2 − f y X Y Z 2 − f y X Z 0 − f y Z f y Y Z 2 ] \frac{\partial\boldsymbol{e}}{\partial\delta\boldsymbol{\xi}}=\begin{bmatrix}\frac{f_xXY}{Z^2}&-f_x-\frac{f_xX^2}{Z^2}&\frac{f_xY}{Z}&-\frac{f_x}{Z}&0&\frac{f_xX}{Z^2}\\\\f_y+\frac{f_yY^2}{Z^2}&-\frac{f_yXY}{Z^2}&-\frac{f_yX}{Z}&0&-\frac{f_y}{Z}&\frac{f_yY}{Z^2}\end{bmatrix} δξe= Z2fxXYfy+Z2fyY2fxZ2fxX2Z2fyXYZfxYZfyXZfx00ZfyZ2fxXZ2fyY

重投影误差关于三维点的雅可比矩阵为
∂ e ∂ P = − [ f x Z 0 − f x X Z 2 0 f y Z − f y Y Z 2 ] R \frac{\partial\boldsymbol{e}}{\partial\boldsymbol{P}}=-\begin{bmatrix}\frac{f_x}Z&0&-\frac{f_xX}{Z^2}\\\\0&\frac{f_y}Z&-\frac{f_yY}{Z^2}\end{bmatrix}\boldsymbol{R} Pe= Zfx00ZfyZ2fxXZ2fyY R
上述矩阵与函数 EdgeProjectXYZ2UV::computeError()中的实现是一一匹配的。

如何向图中添加边

添加一元边

先来看一元边的添加方法,仍然以曲线拟合的例子来说明。

添加一元边示例:曲线拟合

// 添加一元边示例:曲线拟合
for int i=0;i<N;i++)
{
	CurveFittingEdge* edge = new CurveFittingEdge(x_data[i])
	edge->setId(i);	//设置边的ID
	edge->setVertex(0,v);	//设置连接的顶点v,其编号为0
	edge->setMeasurement(y_data[i]);	//设置观测的数值
	edge->setInformation(Eigen::Matrix<double,1,1>::Identity()*1/(w_sigma * w_sigma)); 		//信息矩阵
	optimizer.addEdge(edge);		//将边添加到优化器
}

setMeasurement 函数输入的观测值具体指什么?

对于这个曲线拟合的例子来说,观测值就是实际观测到的数据。对于视觉SLAM来说,观测值通常就是我们观测到的特征点坐标。

添加二元边

添加二元边示例:PnP投影

// 添加二元边示例:PnP投影
// 顶点包括地图点和位姿
index = 1;
// points_2d是由二维图像特征点组成的向量
for (const Point2f p:points_2d)
{
  g2o::EdgeProjectXYZ2UV* edge = new g2o::EdgeProjectXYZ2UV();		// 设置边的ID
	edge->setId(index);
	// 设置边连接的第1个顶点:三维地图点
	edge->setVertex(0,dynamic_cast<g2o::VertexSBAPointXYZ*>(optimizer.vertex(index)));
	// 设置边连接的第2个顶点:位姿
	edge->setVertex(1,pose);
	// 设置观测:图像上的二维特征点坐标
	edge->setMeasurement(Eigen::Vector2d (p.x,p.y));
	// 设置信息矩阵
  edge->setInformation(Eigen::Matrix2d::Identity());
	// 将边添加到优化器中
	optimizer.addEdge(edge);
	//添加边的ID
	index++;
}

这里的 setMeasurement 函数中的 p 来自由特征点组成的向量 points_2d,也就是特征点的图像坐标(x,y)

另外,setVertex 有两个,一个是 0 和 VertexSBAPointXYZ 类型的顶点,另一个是 1 和 pos。

这里的0和1是什么意思?能否互换呢?

这里的0和1分别指代顶点的ID,能不能互换可能需要查看顶点定义部分的代码。

setVertex在g2o中的定义。

// g2o/core/hyper_graph.h
// set the ith vertex on the hyper-edge to the pointer supplied
void setVertex(size_t i,Vertex*v)
{
  assert(i<vertices.size() && "index out of bounds");
	_vertices[i]=v;
}

_vertices[i] 中的 i 对应的就是这里的 0 和 1。代码中的类型 g2o::EdgeProjectXYZ2UV 的定义如下。

class g2o_TYPES_SBA_API EdgeProjectXYZ2UV
{
  // ......
	// 相机位姿v1
	const VertexSE3Expmap* v1 = static_cast<const VertexSE3Expmap*>(_vertices[1]);
	// 三维点v2
	const VertexSBAPointXYZ* v2 = static_cast<const VertexSBAPointXYZ*> (_vertices[0]);
	// ......
}

vertices[0] 对应的是 VertexSBAPointXYZ 类型的顶点,也就是三维点。

vertices[1] 对应的是 VertexSE3Expmap 类型的顶点,也就是位姿pose。

因此,前面1对应的应该是pos,0对应的应该是三维点。所以,这个ID绝对不能互换

补充

static_castdynamic_cast 是C++中两种不同类型的类型转换操作符,它们在类型转换时的用途和行为有着显著的差异。

  1. static_cast

    • 用途static_cast 主要用于进行基本数据类型之间的转换(如 int 转 float)、类层次结构中基类和派生类指针或引用之间的转换(向上转型),以及具有转换构造函数或类型转换运算符的类之间的转换。

    • 行为static_cast 在编译时执行所有检查。如果转换是不合法的,编译器将报错。然而,它不进行运行时类型检查。这意味着,当你将派生类指针或引用向下转型为基类指针或引用时,static_cast 不会检查转换的安全性。

    • 例子

      float f = 3.5;
      int i = static_cast<int>(f); // 将 float 转换为 int
      
  2. dynamic_cast

    • 用途dynamic_cast 主要用于类层次结构中,尤其是进行向下转型(从基类指针或引用转换为派生类指针或引用)时。它被用于那些需要在运行时检查对象类型的情况。

    • 行为dynamic_cast 进行运行时类型检查,确保安全地进行向下转型。如果转换不合法或不安全,dynamic_cast 会返回空指针(对于指针类型)或抛出异常(对于引用类型)。

    • 例子

      class Base { /* ... */ };
      class Derived : public Base { /* ... */ };
      Base* b = new Derived();
      Derived* d = dynamic_cast<Derived*>(b); // 运行时检查
      

总结

  • static_cast 更适合那些在编译时就能确定安全性的转换,如基本数据类型转换或向上转型。
  • dynamic_cast 主要用于需要运行时类型检查的情况,特别是在向下转型时。
  • 使用 dynamic_cast 需要额外的运行时开销,因为它涉及到类型的运行时检查。而 static_cast 不涉及运行时检查,因此性能更好,但可能牺牲了安全性。

关于指针方面的 static_castdynamic_cast 的具体差异,可以从以下几个方面进行详细说明:

  1. 向上转型(Upcasting)

    • static_cast:

      • 安全地将派生类的指针或引用转换为基类的指针或引用。

      • 这种转换是安全的,因为派生类对象总是包含基类部分。

      • 示例:

        class Base {};
        class Derived : public Base {};
        Derived *d = new Derived();
        Base *b = static_cast<Base*>(d); // 安全的向上转型
        
    • dynamic_cast:

      • 也可以用于向上转型,但这通常没有必要,因为编译器会隐式进行这种转换。

      • 示例:

        Derived *d = new Derived();
        Base *b = dynamic_cast<Base*>(d); // 向上转型,但通常不必要
        
  2. 向下转型(Downcasting)

    • static_cast:

      • 可以将基类的指针或引用转换为派生类的指针或引用。

      • 这种转换不安全,因为没有运行时检查来确保转换的有效性。

      • 如果使用不当,可能会导致未定义行为。

      • 示例:

        Base *b = new Derived();
        Derived *d = static_cast<Derived*>(b); // 不安全的向下转型
        
    • dynamic_cast:

      • 安全地进行向下转型。

      • 在运行时检查对象是否真的是指定的派生类类型。

      • 如果转换不合法,对于指针类型返回空指针,对于引用类型抛出异常。

      • 需要基类中至少有一个虚函数(通常是虚析构函数)。

      • 示例:

        Base *b = new Derived();
        Derived *d = dynamic_cast<Derived*>(b); // 安全的向下转型
        if (d) {
            // 转换成功
        } else {
            // 转换失败
        }
        
  3. 性能

    • static_cast:
      • 由于没有运行时类型检查,性能较好。
    • dynamic_cast:
      • 需要运行时类型信息(RTTI),因此相比 static_cast 有一定的性能开销。
  4. 适用场景

    • 使用 static_cast 当你确定转换是安全的,并且了解你正在做的事情。
    • 使用 dynamic_cast 当你需要在运行时检查类型安全性,特别是在你不确定对象是否为某个派生类类型的时候。

总之,选择这两者之间的适当转换取决于你的具体需求,以及你对类型安全和性能的考量。在实际编程中,正确使用类型转换对于保证程序的正确性和稳定性至关重要。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[原创][R语言]股票分析实战[4]:周级别涨幅趋势的相关性

[简介] 常用网名: 猪头三 出生日期: 1981.XX.XX QQ联系: 643439947 个人网站: 80x86汇编小站 https://www.x86asm.org 编程生涯: 2001年~至今[共22年] 职业生涯: 20年 开发语言: C/C、80x86ASM、PHP、Perl、Objective-C、Object Pascal、C#、Python 开发工具: Visual Studio、D…

Redis数据库入门学习(下载与安装、常用命令、在Java中操作Redis)

简介 下载与安装 数据类型 常用命令 1.字符串操作命令 2.哈希操作命令 3.列表操作命令 push是将元素总是插入到第一个 0表示第一个&#xff0c;1表示第二个。-1表示倒数第一个&#xff0c;-2表示倒数第二个。当前命令的意思是第一个到倒数第一个&#xff0c;即就是全部元素 rpo…

NUAA-云计算-考试

19级期末 问题 答案: md格式 自己想办法看 # 随堂测验#### 一、请简述GFS 的系统架构和特点。**1. 系统架构**- GFS将整个系统节点分为三类角色&#xff1a;- Client&#xff08;客户端&#xff09;&#xff1a;Client是GFS提供给应用程序的访问接口&#xff0c;以库文件的…

迪文屏开发保姆级教程5—表盘时钟和文本RTC显示

这篇文章要讲啥事呢&#xff1f; 本篇文章主要介绍了在DGBUS平台上使用表盘时钟和文本时钟RTC显示功能的方法。 文哥悄悄话&#xff1a; 官方开发指南PDF&#xff1a;&#xff08;不方便下载的私聊我发给你&#xff09; https://download.csdn.net/download/qq_21370051/8864…

【GitHub精选项目】抖音/ TikTok 视频下载:TikTokDownloader 操作指南

前言 本文为大家带来的是 JoeanAmier 开发的 TikTokDownloader 项目&#xff0c;这是一个高效的下载 抖音/ TikTok 视频的开源工具。特别适合用户们保存他们喜欢的视频或分享给其他人。 TikTokDownloader 是一个专门设计用于下载 TikTok 视频的工具&#xff0c;旨在为用户提供一…

OpenCV学习笔记 - 使用密集光流检测运动的简单方法

一、简述 使用光流进行运动检测的方法与帧间差分方法类似。主要区别在于第一步,我们将从光流而不是帧差分中获取初始运动信息(一些神经网络模型也是基于光流和原始图像进行运动识别训练的)。 该算法概述如下: 1、计算密集光流 2、获得运动掩模的阈值光流 3、在运动蒙版中查…

Adobe InDesign各版本安装指南

下载链接 https://pan.baidu.com/s/16uvK0ICpDVzacD5FEhUiyQ?pwd0531 ​ #2024版 1.鼠标右击【Indesign2024(64bit)】压缩包&#xff08;win11及以上系统需先点击“显示更多选项”&#xff09;【解压到 Indesign2024(64bit)】。 2.打开解压后的文件夹&#xff0c;鼠标右击…

Real-Time Volumetric Cloudscapes

实时体积云景 GPU Pro 7 实时体积云景的翻译 4.1 概览 游戏中的实时体积云通常为了提高渲染效率而降低质量。最成功的方法仅限于低空蓬松半透明的层状云。我们提出了一种体积解决方案&#xff0c;可以使用不断变化并且逼真的结果填充天空&#xff0c;来描绘高海拔卷云和所有…

OpenAI开发者大会简介

文章目录 GPT-4 Turbo 昨天晚上 OpenAI的首届开发者大会召开 Sam Altman也做了公开演讲&#xff0c;应该说 这是继今年春天发布GPT-4之后 OpenAI在AI行业又创造的一个不眠夜 过去一年 ChatGPT绝对是整个科技领域最热的词汇 OpenAI 也依靠ChatGPT取得了惊人的成绩 ChatG…

​【C语言】乘法表

题目要求&#xff1a; 实现一个函数&#xff0c;打印乘法口诀表&#xff0c;口诀表的行数和列数自己指定 如&#xff1a;输入9&#xff0c;输出9 * 9口诀表&#xff0c;输出12&#xff0c;输出12 * 12的乘法口诀表。 题目分析&#xff1a; 我们观察乘法口诀表可以发现&#x…

【Kubernetes】控制器Statefulset

Statefulset控制器 一、概念二、Statefulset资源清单文件编写技巧2.1、查看定义Statefulset资源需要的字段2.2、查看statefulset.spec字段如何定义2.3、查看statefulset的spec.template字段如何定义 三、Statefulset使用案例&#xff1a;部署web站点3.1、编写一个Statefulset资…

【ctf】whireshark流量分析之tcp_杂篇

目录 简介 常考 图片类 提取png.pcap&#xff08;常规&#xff09; 异常的流量分析&#xff08;*&#xff0c;特殊&#xff09; john-in-the-middle&#xff08;特殊&#xff09; ​编辑 zip类 1.pcap&#xff08;常规&#xff09; 方法1&#xff08;常规提取压缩包&…

绝地求生电脑版的最低配置要求?

绝地求生&#xff08;PlayerUnknowns Battlegrounds&#xff09;是一款非常热门的战术竞技游戏&#xff0c;它在全球范围内有着大量的玩家。为了让更多的玩家能够顺畅地体验这款游戏&#xff0c;下面将介绍绝地求生电脑版的最低配置要求。 CPU&#xff1a;Intel Core i5-4430或…

基于包围盒算法的三维点云数据压缩和曲面重建matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1 包围盒构建 4.2 点云压缩 4.3 曲面重建 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 ...........................................…

4.3 媒资管理模块 - Minio系统上传图片与视频

文章目录 一、上传图片1.1 需求分析1.2 数据模型1.2.1 media_files 媒资信息表 1.3 准备Minio环境1.3.1 桶环境1.3.2 连接Minio参数1.3.3 Minio配置类 1.4 接口定义1.4.1 上传图片接口请求参数1.4.2 上传图片接口返回值1.4.3 接口代码 1.5 MediaFilesMapper1.6 MediaFileServic…

技术阅读周刊第十一期

技术阅读周刊&#xff0c;每周更新。 历史更新 20231124&#xff1a;第七期20231201&#xff1a;第八期20231215&#xff1a;第十‍期 A Comprehensive guide to Spring Boot 3.2 with Java 21, Virtual Threads, Spring Security, PostgreSQL, Flyway, Caching, Micrometer, O…

第19章总结

一.Java绘图类 1.Graphics类 Graphics类是所有图形上下文的抽象基类&#xff0c;它允许应用程序在组件以及闭屏图像上进行绘制。Graphics类封装了Java支持的基本绘图操作所需的状态信息&#xff0c;主要包括颜色、字体、画笔、文本、图像等。 2.Graphics2D类 Graphics2…

MFC 工具栏

目录 工具栏概述 工具栏的使用 添加工具栏资源 创建&#xff0c;加载工具栏 设置工具栏停靠 工具栏概述 CToolBarCtrl-父类CWnd&#xff0c;封装了关于工具栏控件的各种操作。 CToolBar一父类CControlBar&#xff0c;封装了关于工具栏的操作&#xff0c;以及和框架窗口的…

【力扣周赛】第 373 场周赛(交换得到字典序最小的数组 ⭐分解质因子+前缀和+哈希表)

文章目录 竞赛链接Q1&#xff1a;2946. 循环移位后的矩阵相似检查竞赛时代码——模拟 2947. 统计美丽子字符串 I竞赛时代码——前缀和暴力枚举 Q3&#xff1a;2948. 交换得到字典序最小的数组竞赛时代码——排序后判断相似题目——1202. 交换字符串中的元素&#xff08;使用并查…

【C++练级之路】【Lv.5】动态内存管理(都2023年了,不会有人还不知道new吧?)

目录 一、C/C内存分布二、new和delete的使用方式2.1 C语言内存管理2.2 C内存管理2.2.1 new和delete操作内置类型2.2.2 new和delete操作自定义类型 三、new和delete的底层原理3.1 operator new与operator delete函数3.2 原理总结3.2.1 内置类型3.2.2 自定义类型 四、定位new表达…