跳转至

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 语法要点

// 基本函数定义
返回类型 函数名(参数列表) {
    // 函数体
    return 返回值;
}

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 代替 NULLNULL 实际上是整数 0,类型不安全,容易引发隐蔽的 bug:

// 错误写法
if (ptr == NULL) { ... }

// 正确写法
if (ptr == nullptr) { ... }

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++ 中,structclass 唯一的区别是默认访问权限:struct 默认 publicclass 默认 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_backerase,这会导致迭代器失效和未定义行为:

// 错误!遍历中删除元素
for (auto it = armors.begin(); it != armors.end(); ++it) {
    if (it->confidence < 0.5f) {
        armors.erase(it);  // 迭代器失效!
    }
}

// 正确:使用 erase-remove 惯用法
armors.erase(
    std::remove_if(armors.begin(), armors.end(),
        [](const Armor& a) { return a.confidence < 0.5f; }),
    armors.end()
);

7 头文件和源文件

7.1 概念

C++ 项目将代码分为两类文件:

  • 头文件.h / .hpp):声明接口(类定义、函数原型、常量)
  • 源文件.cpp):实现逻辑(函数体)

这种分离带来两个好处:

  1. 编译速度:修改 .cpp 只需重新编译该文件,不需要全部重来
  2. 接口清晰:看头文件就知道一个类/模块提供了什么功能

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

#include <vector>
#include <opencv2/opencv.hpp>

class ArmorDetector { ... };

#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));

什么时候该用模板?

  • 当你发现自己在为 intfloatdouble几乎相同的函数时,考虑模板
  • 不要过度使用——视觉组代码的首要目标是可读性,如果一个函数只用于一种类型,直接写具体类型更清晰
  • 模板的错误信息通常很长很难读,调试时要有心理准备

本章小结

知识点 核心要点 在自瞄中的典型应用
变量和数据类型 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