C++ 基础:面向 RoboMaster 视觉开发¶
本章目标
本章面向视觉组新成员,从零开始讲解 C++ 核心语法。所有示例均取自自瞄(auto-aim)视觉代码的实际场景,帮助你理解每个知识点在 RoboMaster 视觉开发中的用途。
前置要求:基本了解编程概念(变量、循环、条件判断),有任意语言的编程经验即可。
1 变量和数据类型¶
1.1 概念¶
C++ 是静态类型语言,每个变量在声明时就确定了类型,编译器会据此分配内存和检查合法性。RoboMaster 视觉代码中,我们最常用的类型有:
| 类型 | 大小 | 用途 |
|---|---|---|
int |
通常 4 字节 | 计数器、索引、帧号 |
float |
4 字节 | 图像坐标、置信度 |
double |
8 字节 | 高精度计算(角度、装甲板中心坐标) |
bool |
1 字节 | 状态标志 |
std::string |
可变 | 灯条文本、数字识别结果 |
auto |
推导 | 让编译器自动推导类型 |
const |
不限 | 声明不可修改的常量 |
1.2 基本语法¶
int frame_id = 0; // 整数:当前帧号
float confidence = 0.85f; // 浮点数:检测置信度
double xc = 0.0; // 双精度:装甲板中心 x 坐标
double yc = 0.0; // 双精度:装甲板中心 y 坐标
bool tracking = false; // 布尔值:是否正在跟踪目标
std::string number = "3"; // 字符串:装甲板上识别到的数字
// auto 让编译器自动推导类型
auto image = cv::imread("frame.jpg"); // 推导为 cv::Mat
// const 声明不可修改的常量
const double PI = 3.14159265358979;
const int MAX_DETECTORS = 4;
1.3 在自瞄代码中的应用¶
在自瞄系统中,我们经常需要保存装甲板的中心坐标用于 PnP 解算:
// 装甲板中心坐标通常用 double,因为 PnP 解算需要高精度
double xc = 0.0; // 装甲板中心 x(像素坐标)
double yc = 0.0; // 装甲板中心 y(像素坐标)
// 跟踪状态用 bool 标记
bool tracking = false;
// 每一帧更新帧号
int frame_id = 0;
frame_id++;
// 检测置信度阈值通常定义为 const
const double confidence_threshold = 0.7;
为什么装甲板坐标用 double 而不是 float?
PnP(Perspective-n-Point)解算涉及矩阵运算,浮点精度误差会累积。使用 double 可以减少数值误差,让最终的三维坐标更准确。对于不涉及复杂计算的场景(如图像像素坐标),用 float 就够了。
2 函数¶
2.1 概念¶
函数是代码复用的基本单位。在视觉代码中,每一个处理步骤(颜色分离、灯条检测、装甲板匹配、坐标解算)通常都会封装成一个函数。
C++ 的参数传递有三种方式,理解它们是读代码的前提:
| 传递方式 | 语法 | 是否拷贝 | 能否修改原值 |
|---|---|---|---|
| 值传递 | T param |
是(拷贝一份) | 不能(改的是副本) |
| 引用传递 | T& param |
否(直接操作原变量) | 能 |
| const 引用传递 | const T& param |
否(直接读原变量) | 不能(编译器报错) |
2.2 语法要点¶
2.3 三种参数传递方式¶
// ========== 1. 值传递:适合 int、bool 等小类型 ==========
void setFrameId(int id) {
id = id + 1; // 只改了副本,外部不受影响
}
// ========== 2. 引用传递:需要修改原变量时使用 ==========
// cv::Mat 是 OpenCV 图像矩阵,非常大(640x480x3 ≈ 900KB)
// 值传递会完整拷贝,非常浪费,所以用引用
void preprocessImage(cv::Mat& image) {
cv::cvtColor(image, image, cv::COLOR_BGR2GRAY); // 直接修改原图
cv::GaussianBlur(image, image, cv::Size(5, 5), 0);
}
// ========== 3. const 引用传递:只读,不拷贝,最常用 ==========
// 参数用 const& 修饰 = "我只看,不改"
std::vector<Armor> detectArmors(const cv::Mat& image) {
std::vector<Armor> results;
// ... 检测逻辑
return results;
}
2.4 在自瞄代码中的应用¶
PnP 解算是自瞄的核心函数之一——将二维图像坐标转换为三维空间坐标:
/**
* @brief 通过 PnP 解算将装甲板像素坐标投影为相机坐标系下的三维点
* @param rvec 旋转向量(3x1),描述装甲板相对于相机的姿态
* @param tvec 平移向量(3x1),描述装甲板相对于相机的位置
* @return 装甲板中心在图像平面上的投影点
*/
cv::Point2f projectArmor(const cv::Mat& rvec, const cv::Mat& tvec) {
// 定义装甲板在自身坐标系下的中心点(单位:米)
std::vector<cv::Point3f> object_points = {
cv::Point3f(0.0f, 0.0f, 0.0f) // 装甲板中心
};
std::vector<cv::Point2f> image_points;
cv::projectPoints(object_points, rvec, tvec, camera_matrix,
dist_coeffs, image_points);
return image_points[0];
}
函数参数怎么选?
- 参数很小(
int,double,bool)且不需要修改 → 值传递 - 参数很大(
cv::Mat,std::vector)且只读 →const& - 参数很大且需要原地修改 →
&(非 const 引用)
3 指针和引用¶
3.1 概念¶
指针(*)和引用(&)是 C++ 中访问变量的两种间接方式。初学者容易混淆,核心区别是:
| 特性 | 引用 (&) |
指针 (*) |
|---|---|---|
| 能否为空 | 不能,必须绑定 | 能,可以是 nullptr |
| 能否重新绑定 | 不能 | 能 |
| 语法 | 和普通变量一样 | 需要 * 解引用、-> 访问成员 |
| 常见用途 | 函数参数传递 | 可选对象、多态 |
3.2 引用的基本用法¶
int a = 10;
int& ref = a; // ref 是 a 的别名
ref = 20; // 此时 a 也变成了 20
// 在函数参数中,引用 = "传入原变量的别名"
void setImage(cv::Mat& image) {
image = cv::Mat::zeros(480, 640, CV_8UC3); // 修改原变量
}
cv::Mat img;
setImage(img); // img 现在变成了 480x640 的全零矩阵
3.3 指针的基本用法¶
int value = 42;
int* ptr = &value; // ptr 指向 value 的地址
*ptr = 100; // 通过指针修改 value,value 变为 100
// 指针可以为空
int* p = nullptr; // 不指向任何东西
if (p != nullptr) {
// 安全使用
}
// 指针访问结构体/类成员用 ->
Armor* armor_ptr = new Armor();
armor_ptr->number = "3"; // 等价于 (*armor_ptr).number = "3"
delete armor_ptr; // 手动释放内存
3.4 在自瞄代码中的应用¶
OpenCV 的 cv::Mat 传参时,几乎总是使用引用,因为拷贝一张图像代价很大:
// cv::Mat& image —— 直接操作原图,不拷贝
// 这是视觉代码中最常见的参数形式
void findLightBars(cv::Mat& binary_image,
std::vector<cv::RotatedRect>& light_bars) {
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binary_image, contours, cv::RETR_EXTERNAL,
cv::CHAIN_APPROX_SIMPLE);
for (const auto& contour : contours) {
if (contour.size() < 5) continue;
cv::RotatedRect rect = cv::minAreaRect(contour);
light_bars.push_back(rect);
}
}
// const cv::Mat& —— 只读,最常见的"输入图像"写法
double calculateBrightness(const cv::Mat& image,
const cv::RotatedRect& roi) {
// 只读取图像,不修改
cv::Mat mask = cv::Mat::zeros(image.size(), CV_8UC1);
cv::ellipse(mask, roi, cv::Scalar(255), -1);
return cv::mean(image, mask)[0];
}
nullptr 代替 NULL
在现代 C++(C++11 及以后)中,永远用 nullptr 代替 NULL。NULL 实际上是整数 0,类型不安全,容易引发隐蔽的 bug:
4 struct 和 enum¶
4.1 struct —— 数据打包¶
概念:struct(结构体)将多个相关变量打包成一个整体。在视觉代码中,每个检测到的装甲板有多个属性(位置、颜色、数字、类型),用 struct 组织最自然。
// 装甲板类型枚举(见下方 enum 讲解)
enum class ArmorType { SMALL, LARGE, INVALID };
// 装甲板结构体 —— 把一个装甲板的所有信息放在一起
struct Armor {
std::string number; // 识别到的数字("1"-"5", "base")
ArmorType type; // 装甲板类型
cv::Point2f center; // 装甲板中心像素坐标
float confidence; // 检测置信度
std::vector<cv::Point2f> vertices; // 四个顶点
// struct 也可以有成员函数
float area() const {
return cv::contourArea(vertices);
}
};
// 使用
Armor armor;
armor.number = "3";
armor.type = ArmorType::SMALL;
armor.center = cv::Point2f(320.0f, 240.0f);
armor.confidence = 0.92f;
4.2 enum class —— 类型安全的枚举¶
概念:enum class 定义一组命名常量。相比普通的 enum,它有类型检查,不会和整数混淆。
// enum class 是"强类型枚举",不会隐式转换为 int
enum class Color { RED, BLUE };
enum class ArmorType { SMALL, LARGE, INVALID };
// 使用时必须带上作用域
Color enemy_color = Color::RED;
// 普通 enum(不推荐)可以直接当 int 用,容易出错
// enum Color { RED, BLUE }; // RED = 0, BLUE = 1
// int c = RED; // 编译通过,但语义不清
4.3 在自瞄代码中的应用¶
// 完整的装甲板识别流程中 struct 和 enum 的配合使用
enum class Color { RED, BLUE };
enum class ArmorType { SMALL, LARGE, INVALID };
struct LightBar {
cv::RotatedRect rect; // 灯条的旋转矩形
Color color; // 灯条颜色
float length; // 灯条长度
};
struct Armor {
LightBar left_light; // 左灯条
LightBar right_light; // 右灯条
std::string number; // 数字分类结果
ArmorType type; // 装甲板类型
cv::Point2f center; // 中心坐标
float confidence; // 检测置信度
};
// 判断是否是有效装甲板
bool isValidArmor(const Armor& armor) {
if (armor.type == ArmorType::INVALID) return false;
if (armor.confidence < 0.7f) return false;
return true;
}
struct vs class
在 C++ 中,struct 和 class 唯一的区别是默认访问权限:struct 默认 public,class 默认 private。团队中通常约定:纯数据用 struct,有复杂逻辑用 class。
5 class¶
5.1 概念¶
class 是 C++ 面向对象编程的核心。当一个功能涉及到状态管理(如检测器需要维护参数、缓存上一帧结果)时,就应该用 class 封装。
核心要素:
- 构造函数:创建对象时自动调用,用于初始化
- 成员函数:定义对象的行为
- 访问控制:
public(外部可见)、private(仅内部使用)、protected(子类可见) - 初始化列表:构造函数的高效初始化方式
5.2 基本语法¶
class Detector {
public:
// 构造函数 —— 使用初始化列表
// 初始化列表比在函数体内赋值更高效(尤其是 const 成员和引用成员)
Detector(const Params& p)
: threshold_(p.threshold),
min_area_(p.min_area),
enemy_color_(p.enemy_color)
{
// 构造函数体(如果初始化列表已经处理完,这里可以为空)
}
// 公有成员函数 —— 外部可以直接调用
std::vector<Armor> detect(const cv::Mat& image) {
std::vector<LightBar> lights = findLightBars(image);
return matchArmors(lights);
}
// setter —— 运行时修改参数
void setEnemyColor(Color color) {
enemy_color_ = color;
}
private:
// 私有成员变量 —— 外部不能直接访问
// 命名约定:以下划线结尾
double threshold_;
double min_area_;
Color enemy_color_;
// 私有成员函数 —— 内部辅助函数
std::vector<LightBar> findLightBars(const cv::Mat& image) {
// ... 灯条检测逻辑
}
std::vector<Armor> matchArmors(const std::vector<LightBar>& lights) {
// ... 装甲板匹配逻辑
}
};
5.3 初始化列表详解¶
class Tracker {
public:
// 错误写法(效率低,且 const 成员必须用初始化列表)
// Tracker() {
// last_position_ = cv::Point3f(0, 0, 0); // 赋值,不是初始化
// max_distance_ = 0.5; // 对 const 成员编译报错
// }
// 正确写法 —— 初始化列表
Tracker()
: last_position_(cv::Point3f(0, 0, 0)),
max_distance_(0.5),
frame_count_(0)
{
}
private:
cv::Point3f last_position_;
const double max_distance_; // const 成员必须在初始化列表中初始化
int frame_count_;
};
5.4 在自瞄代码中的应用¶
一个典型的装甲板检测器类:
// 检测器参数结构体
struct Params {
double threshold = 120.0; // 二值化阈值
double min_light_area = 10.0; // 灯条最小面积
double max_light_ratio = 0.4; // 灯条宽高比上限
Color enemy_color = Color::RED;
};
class ArmorDetector {
public:
// 构造函数:接收参数
ArmorDetector(const Params& params)
: params_(params)
{
}
// 核心接口:输入图像,输出装甲板列表
std::vector<Armor> detect(const cv::Mat& bgr_image) {
// 1. 预处理:分离颜色通道,二值化
cv::Mat binary = preprocess(bgr_image);
// 2. 找灯条
std::vector<LightBar> lights = findLightBars(binary);
// 3. 匹配灯条对,形成装甲板
std::vector<Armor> armors = matchArmors(lights);
return armors;
}
// 动态更新敌人颜色
void setEnemyColor(Color color) {
params_.enemy_color = color;
}
private:
Params params_; // 保存参数
cv::Mat preprocess(const cv::Mat& image) {
std::vector<cv::Mat> channels;
cv::split(image, channels);
cv::Mat binary;
if (params_.enemy_color == Color::RED) {
// 红色通道减去蓝色通道
cv::subtract(channels[2], channels[0], binary);
} else {
// 蓝色通道减去红色通道
cv::subtract(channels[0], channels[2], binary);
}
cv::threshold(binary, binary, params_.threshold, 255,
cv::THRESH_BINARY);
return binary;
}
std::vector<LightBar> findLightBars(const cv::Mat& binary) {
// ... 灯条检测
}
std::vector<Armor> matchArmors(const std::vector<LightBar>& lights) {
// ... 装甲板匹配
}
};
使用方式:
Params params;
params.enemy_color = Color::RED;
params.threshold = 100.0;
ArmorDetector detector(params);
cv::Mat frame = cv::imread("test.jpg");
std::vector<Armor> results = detector.detect(frame);
for (const auto& armor : results) {
std::cout << "检测到装甲板: " << armor.number
<< " 置信度: " << armor.confidence << std::endl;
}
为什么用 private?
private 保护了内部实现。外部代码只需要知道 detect() 能返回装甲板列表,不需要知道内部用什么算法。这样修改内部实现时,不会影响使用检测器的其他代码。这就是封装的好处。
6 STL 容器¶
6.1 概念¶
STL(Standard Template Library,标准模板库)提供了常用的数据结构。视觉代码中用得最多的是:
| 容器 | 特点 | 典型用途 |
|---|---|---|
std::vector |
动态数组,随机访问快 | 存储灯条列表、装甲板列表 |
std::map |
有序键值对,按 key 排序 | 保存 ID 到历史位置的映射 |
std::pair |
两个值的组合 | 临时配对(如灯条对) |
6.2 vector —— 动态数组¶
// 声明一个存 Armor 的动态数组
std::vector<Armor> armors;
// 添加元素
armors.push_back(armor1);
armors.push_back(armor2);
// 访问元素
Armor first = armors[0]; // 下标访问(不检查越界)
Armor first_safe = armors.at(0); // 安全访问(越界会抛异常)
// 大小
int count = armors.size(); // 元素个数
bool empty = armors.empty(); // 是否为空
// 清空
armors.clear();
6.3 map —— 键值对映射¶
// 用 map 存储每个装甲板 ID 对应的历史位置
std::map<int, cv::Point3f> position_history;
// 插入/更新
position_history[1] = cv::Point3f(1.0, 2.0, 3.0);
position_history[2] = cv::Point3f(4.0, 5.0, 6.0);
// 查找
if (position_history.find(1) != position_history.end()) {
cv::Point3f last_pos = position_history[1];
}
// 遍历
for (const auto& [id, pos] : position_history) {
std::cout << "ID: " << id
<< " 位置: (" << pos.x << ", " << pos.y << ", " << pos.z
<< ")" << std::endl;
}
6.4 pair 和 range-based for¶
// pair —— 将两个值打包
std::pair<LightBar, LightBar> light_pair = {left_light, right_light};
LightBar left = light_pair.first;
LightBar right = light_pair.second;
// 或者用结构化绑定(C++17)
auto [left, right] = light_pair;
6.5 range-based for —— 遍历容器¶
std::vector<Armor> armors = detector.detect(image);
// 基本写法
for (const auto& armor : armors) {
std::cout << armor.number << std::endl;
}
// 为什么用 const auto& ?
// - const :遍历时不修改元素
// - auto :不用手写 Armor 类型,编译器自动推导
// - & :避免拷贝,尤其是 cv::Mat 这种大对象
// 需要修改元素时去掉 const
for (auto& armor : armors) {
armor.confidence *= 1.1f; // 调整置信度
}
6.6 在自瞄代码中的应用¶
// 一个完整的装甲板筛选流程
std::vector<Armor> detectArmors(const cv::Mat& image) {
std::vector<Armor> all_armors = detector.detect(image);
std::vector<Armor> valid_armors;
// 用 range-based for 遍历,筛选有效装甲板
for (const auto& armor : all_armors) {
if (armor.confidence > 0.7f && armor.type != ArmorType::INVALID) {
valid_armors.push_back(armor);
}
}
// 按距离排序,选最近的目标
std::sort(valid_armors.begin(), valid_armors.end(),
[](const Armor& a, const Armor& b) {
return a.center.x < b.center.x; // 简化的排序条件
});
return valid_armors;
}
// 用 map 追踪多个目标的历史轨迹
std::map<int, std::vector<cv::Point3f>> target_tracks;
void updateTrack(int target_id, const cv::Point3f& position) {
target_tracks[target_id].push_back(position);
// 只保留最近 30 帧的轨迹
auto& track = target_tracks[target_id];
if (track.size() > 30) {
track.erase(track.begin());
}
}
避免在循环中修改 vector 大小
不要在遍历 vector 的同时 push_back 或 erase,这会导致迭代器失效和未定义行为:
7 头文件和源文件¶
7.1 概念¶
C++ 项目将代码分为两类文件:
- 头文件(
.h/.hpp):声明接口(类定义、函数原型、常量) - 源文件(
.cpp):实现逻辑(函数体)
这种分离带来两个好处:
- 编译速度:修改
.cpp只需重新编译该文件,不需要全部重来 - 接口清晰:看头文件就知道一个类/模块提供了什么功能
7.2 include guard¶
头文件必须有 include guard(包含保护),防止同一个头文件被多次 #include 导致重复定义错误。
// armor_detector.hpp
#ifndef ARMOR_DETECTOR_HPP // 如果没有定义这个宏
#define ARMOR_DETECTOR_HPP // 定义它
#include <vector>
#include <string>
#include <opencv2/opencv.hpp>
enum class ArmorType { SMALL, LARGE, INVALID };
enum class Color { RED, BLUE };
struct Armor {
std::string number;
ArmorType type;
cv::Point2f center;
float confidence;
std::vector<cv::Point2f> vertices;
};
class ArmorDetector {
public:
ArmorDetector();
std::vector<Armor> detect(const cv::Mat& image);
private:
double threshold_;
};
#endif // ARMOR_DETECTOR_HPP // 结束保护
对应的源文件:
// armor_detector.cpp
#include "armor_detector.hpp" // 包含自己的头文件
// 构造函数实现
ArmorDetector::ArmorDetector()
: threshold_(120.0)
{
}
// detect 函数实现
std::vector<Armor> ArmorDetector::detect(const cv::Mat& image) {
std::vector<Armor> results;
// ... 实现细节
return results;
}
7.3 #include 规则¶
// 系统/第三方库用尖括号
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
// 自己的头文件用双引号
#include "armor_detector.hpp"
#include "pnp_solver.h"
// 头文件中尽量用前置声明代替 #include,减少编译依赖
// 例如在某个头文件中只需要知道 Armor 类存在,不需要完整定义:
class Armor; // 前置声明
class Tracker {
public:
void update(const Armor& armor);
};
7.4 典型项目结构¶
rm_vision/
├── CMakeLists.txt
├── include/
│ └── rm_vision/
│ ├── armor_detector.hpp # 装甲板检测器声明
│ ├── pnp_solver.hpp # PnP 解算器声明
│ ├── tracker.hpp # 目标跟踪器声明
│ └── common.hpp # 公共结构体(Armor, LightBar 等)
└── src/
├── armor_detector.cpp # 检测器实现
├── pnp_solver.cpp # 解算器实现
├── tracker.cpp # 跟踪器实现
└── main.cpp # 主程序入口
使用 #pragma once 的替代方案
现代编译器大多支持 #pragma once,功能和 include guard 相同,写法更简洁:
#pragma once 和 #ifndef guard 选哪种都行,团队统一用一种即可。
8 模板简介¶
8.1 概念¶
模板(template)是 C++ 的泛型编程机制——让函数或类可以处理任意类型,而不必为每种类型都写一遍。STL 中的 std::vector<T>、std::map<K, V> 都是模板。
8.2 函数模板¶
// 定义一个模板函数:计算两个值中较小的那个
template<typename T>
T clamp(T value, T min_val, T max_val) {
if (value < min_val) return min_val;
if (value > max_val) return max_val;
return value;
}
// 编译器会根据调用时的参数类型自动实例化
int a = clamp(5, 0, 10); // T = int
double b = clamp(3.14, 0.0, 1.0); // T = double
8.3 类模板¶
// 一个简单的环形缓冲区,用于存储最近 N 帧的数据
template<typename T>
class RingBuffer {
public:
explicit RingBuffer(size_t capacity)
: buffer_(capacity), capacity_(capacity), head_(0), size_(0)
{
}
void push(const T& item) {
buffer_[head_] = item;
head_ = (head_ + 1) % capacity_;
if (size_ < capacity_) size_++;
}
T latest() const {
size_t idx = (head_ + capacity_ - 1) % capacity_;
return buffer_[idx];
}
size_t size() const { return size_; }
private:
std::vector<T> buffer_;
size_t capacity_;
size_t head_;
size_t size_;
};
8.4 在自瞄代码中的应用¶
// 存储最近 30 帧的装甲板位置,用于滤波和预测
RingBuffer<cv::Point3f> position_buffer(30);
// 每一帧检测到目标后更新
void onArmorDetected(const Armor& armor, const cv::Point3f& position) {
position_buffer.push(position);
// 当积累了足够数据后,可以做简单的移动平均滤波
if (position_buffer.size() >= 5) {
// ... 滤波逻辑
}
}
// 也可以用模板函数统一处理不同类型的坐标
template<typename PointType>
float distance2D(const PointType& a, const PointType& b) {
float dx = a.x - b.x;
float dy = a.y - b.y;
return std::sqrt(dx * dx + dy * dy);
}
// 可以传入 cv::Point2f、cv::Point2d,甚至自定义的 Point 结构体
float dist = distance2D(armor.center, cv::Point2f(320, 240));
什么时候该用模板?
- 当你发现自己在为
int、float、double写几乎相同的函数时,考虑模板 - 但不要过度使用——视觉组代码的首要目标是可读性,如果一个函数只用于一种类型,直接写具体类型更清晰
- 模板的错误信息通常很长很难读,调试时要有心理准备
本章小结¶
| 知识点 | 核心要点 | 在自瞄中的典型应用 |
|---|---|---|
| 变量和数据类型 | double 高精度、bool 状态、const 常量 |
装甲板坐标、跟踪状态、阈值常量 |
| 函数 | const& 传大对象只读 |
detect(), projectArmor() |
| 指针和引用 | & 引用避免拷贝,nullptr 替代 NULL |
cv::Mat& image 图像传参 |
| struct / enum | 数据打包 + 类型安全枚举 | Armor, LightBar, Color |
| class | 封装 + 初始化列表 + 访问控制 | ArmorDetector, Tracker |
| STL 容器 | vector 动态数组、map 键值对、range-based for |
装甲板列表、目标轨迹 |
| 头文件/源文件 | 声明与实现分离、include guard | .hpp + .cpp 项目结构 |
| 模板 | 泛型函数/类,template<typename T> |
环形缓冲区、通用距离计算 |
下一步
掌握了本章内容后,你就能读懂视觉代码中的大部分 C++ 语法。接下来的章节将介绍 OpenCV 图像处理和 ROS2 通信框架,届时这些语法会反复出现。
练习¶
动手练习
建议在 VSCode 中创建一个 C++ 项目,逐个完成以下练习。
练习 1:定义 Armor 结构体¶
定义一个 Armor 结构体,包含以下成员:
- std::string number — 装甲板编号("1"~"5", "outpost", "base")
- std::string type — 类型("small" 或 "large")
- float confidence — 识别置信度(0~1)
- float x, y — 图像坐标
然后创建一个 Armor 对象,赋值并打印。
#include <iostream>
#include <string>
struct Armor {
std::string number;
std::string type;
float confidence;
float x, y;
};
int main() {
Armor a;
a.number = "3";
a.type = "small";
a.confidence = 0.92f;
a.x = 640.0f;
a.y = 360.0f;
std::cout << "装甲板 " << a.number
<< " (" << a.type << ")"
<< " 置信度: " << a.confidence
<< " 位置: (" << a.x << ", " << a.y << ")"
<< std::endl;
return 0;
}
预期输出: 装甲板 3 (small) 置信度: 0.92 位置: (640, 360)
练习 2:写一个 ArmorDetector 类¶
基于练习 1 的 Armor 结构体,写一个 ArmorDetector 类:
- 私有成员: double threshold_(二值化阈值)
- 构造函数: 接收阈值参数,使用初始化列表
- 公有方法: bool isValid(const Armor& a) — 当置信度 > threshold_ 时返回 true
class ArmorDetector {
public:
ArmorDetector(double threshold) : threshold_(threshold) {}
bool isValid(const Armor& a) const {
return a.confidence > threshold_;
}
private:
double threshold_;
};
测试: 创建 ArmorDetector detector(0.8),用不同置信度的 Armor 测试 isValid()。
练习 3:用 vector 存储和筛选装甲板¶
创建 std::vector<Armor>,放入 5 个装甲板,然后用 range-based for 循环筛选出置信度 > 0.8 的装甲板。
#include <vector>
std::vector<Armor> armors = {
{"1", "small", 0.95f, 100, 200},
{"2", "large", 0.60f, 300, 400},
{"3", "small", 0.88f, 500, 300},
{"outpost", "large", 0.45f, 700, 100},
{"5", "small", 0.91f, 200, 500}
};
ArmorDetector detector(0.8);
for (const auto& armor : armors) {
if (detector.isValid(armor)) {
std::cout << "有效目标: " << armor.number << std::endl;
}
}
预期输出: 有效目标: 1, 有效目标: 3, 有效目标: 5