feat: init OpenCV C++ training course with 8 experiments and detailed tutorial

This commit is contained in:
TuxMonkey
2026-07-27 14:23:06 +08:00
commit 5fdfd9791a
18 changed files with 3843 additions and 0 deletions

226
exp8_pipeline/main.cpp Normal file
View File

@@ -0,0 +1,226 @@
/*
* ============================================================
* 实验 8:综合流水线 (Pipeline)
* 知识点:串联前面 7 个实验的所有技术
* ============================================================
*
* 【实验目标】
* 1. 将前面学到的所有技术串联成一个完整流程
* 2. 理解图像处理中"预处理 → 提取 → 后处理"的标准套路
* 3. 为 RoboMaster 灯条检测打下完整基础
*
* 【生活化比喻】
* 一条完整的图像处理流水线就像工厂的"生产流水线":
* - 原材料进厂(读取图像)
* - 清洗处理(模糊去噪、颜色转换)
* - 核心加工(颜色提取 inRange)
* - 精修打磨(形态学操作去掉毛刺)
* - 质检包装(边缘检测找轮廓、画框标记)
* - 成品出厂(保存结果图片)
*
* 每一步都依赖前一步的输出,整个流程像一个多米诺骨牌,
* 任何一个环节出问题,最终结果都会受影响。
*
* 【RoboMaster 灯条检测的标准流水线】
* 1. 读取摄像头采集的图像
* 2. BGR → HSV 颜色空间转换
* 3. inRange 提取目标颜色(红/蓝)
* 4. 形态学操作(开运算去噪、闭运算填充)
* 5. 轮廓检测与筛选(按面积、长宽比、角度过滤)
* 6. 画出灯条的位置框
*
* 本实验完整演示整个流程!
*/
#include <opencv2/opencv.hpp>
#include <iostream>
int main()
{
std::cout << "===== 实验 8:综合流水线 开始 =====\n" << std::endl;
// ==========================================
// 阶段 1:生成模拟场景的测试图片
// ==========================================
std::cout << "[阶段 1/6] 生成模拟 RoboMaster 场景的测试图片..." << std::endl;
// 600×450 画布,深灰色背景(模拟比赛场地的昏暗环境)
cv::Mat scene(450, 600, CV_8UC3, cv::Scalar(40, 40, 40));
// 画两个"红色灯条"(模拟 RoboMaster 装甲板上的 LED)
// 灯条是竖直的细长矩形,这是我们检测的目标
// 左灯条
cv::rectangle(scene, cv::Point(200, 120), cv::Point(240, 280),
cv::Scalar(0, 0, 255), -1); // 纯红色竖直矩形
// 右灯条
cv::rectangle(scene, cv::Point(360, 120), cv::Point(400, 280),
cv::Scalar(0, 0, 255), -1); // 纯红色竖直矩形
// 画一个红色"装甲板"(连接两个灯条的中间横梁)
cv::rectangle(scene, cv::Point(200, 180), cv::Point(400, 220),
cv::Scalar(0, 0, 200), -1); // 稍暗一点的红色
// 画一些干扰物(场地上可能有其他颜色的物体)
cv::circle(scene, cv::Point(100, 100), 30, cv::Scalar(255, 0, 0), -1); // 蓝色干扰圆
cv::rectangle(scene, cv::Point(450, 300), cv::Point(550, 380),
cv::Scalar(0, 255, 0), -1); // 绿色干扰矩形
cv::circle(scene, cv::Point(80, 350), 40, cv::Scalar(0, 255, 255), -1); // 黄色干扰圆
// 撒一些随机噪点(模拟摄像头传感器噪点)
for (int i = 0; i < 200; i++)
{
int x = rand() % 600;
int y = rand() % 450;
// 随机颜色(模拟 CCD/CMOS 噪点)
uchar noise_color = static_cast<uchar>(rand() % 128 + 60);
cv::circle(scene, cv::Point(x, y), 1,
cv::Scalar(noise_color, noise_color, noise_color), -1);
}
cv::imwrite("01_original_scene.jpg", scene);
std::cout << " -> 已保存 01_original_scene.jpg(原始场景图)" << std::endl;
// ==========================================
// 阶段 2:颜色空间转换 BGR → HSV
// ==========================================
std::cout << "[阶段 2/6] BGR → HSV 颜色空间转换..." << std::endl;
cv::Mat hsv;
cv::cvtColor(scene, hsv, cv::COLOR_BGR2HSV);
cv::imwrite("02_hsv.jpg", hsv);
std::cout << " -> 已保存 02_hsv.jpg" << std::endl;
// ==========================================
// 阶段 3:高斯模糊(去噪预处理)
// ==========================================
std::cout << "[阶段 3/6] 高斯模糊(减少噪点对后续处理的影响)..." << std::endl;
cv::Mat hsv_blurred;
// 使用 5×5 的高斯核对 HSV 图像进行模糊
// 为什么在 inRange 之前模糊?
// - 噪点在 HSV 空间中也可能落在红色范围内,产生误检
// - 先模糊可以平滑掉这些随机噪点,减少误检
// 注意:核不要太大(5~7 比较合适),否则会模糊掉灯条的边缘
cv::GaussianBlur(hsv, hsv_blurred, cv::Size(5, 5), 0);
cv::imwrite("03_hsv_blurred.jpg", hsv_blurred);
std::cout << " -> 已保存 03_hsv_blurred.jpg(模糊后的 HSV 图)" << std::endl;
// ==========================================
// 阶段 4:颜色提取(inRange 提取红色)
// ==========================================
std::cout << "[阶段 4/6] inRange 提取红色区域..." << std::endl;
// 红色需要两个范围(如实验 7 所述)
cv::Scalar lower_red1(0, 100, 100);
cv::Scalar upper_red1(10, 255, 255);
cv::Scalar lower_red2(160, 100, 100);
cv::Scalar upper_red2(180, 255, 255);
cv::Mat mask1, mask2, red_mask;
cv::inRange(hsv_blurred, lower_red1, upper_red1, mask1);
cv::inRange(hsv_blurred, lower_red2, upper_red2, mask2);
cv::bitwise_or(mask1, mask2, red_mask);
cv::imwrite("04_red_mask_raw.jpg", red_mask);
std::cout << " -> 已保存 04_red_mask_raw.jpg(原始红色 mask)" << std::endl;
// ==========================================
// 阶段 5:形态学后处理(清理 mask)
// ==========================================
std::cout << "[阶段 5/6] 形态学操作清理 mask..." << std::endl;
// 创建结构元素(5×5 矩形核)
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
// 步骤 5a:开运算(先腐蚀再膨胀)—— 消除小白点噪声
// 那些随机噪点中落在红色范围内的,经过腐蚀会消失
cv::Mat mask_opened;
cv::erode(red_mask, mask_opened, kernel);
cv::dilate(mask_opened, mask_opened, kernel);
cv::imwrite("05a_mask_opened.jpg", mask_opened);
std::cout << " -> 已保存 05a_mask_opened.jpg(开运算:去除噪点)" << std::endl;
// 步骤 5b:闭运算(先膨胀再腐蚀)—— 填充灯条内部的小黑洞
cv::Mat mask_cleaned;
cv::dilate(mask_opened, mask_cleaned, kernel);
cv::erode(mask_cleaned, mask_cleaned, kernel);
cv::imwrite("05b_mask_cleaned.jpg", mask_cleaned);
std::cout << " -> 已保存 05b_mask_cleaned.jpg(闭运算:填充内部空洞)" << std::endl;
// ==========================================
// 阶段 6:在原始场景上展示检测结果
// ==========================================
std::cout << "[阶段 6/6] 在原图上标记检测结果..." << std::endl;
// 步骤 6a:用 mask 提取原图中的红色区域
cv::Mat red_only;
cv::bitwise_and(scene, scene, red_only, mask_cleaned);
cv::imwrite("06a_red_extracted.jpg", red_only);
std::cout << " -> 已保存 06a_red_extracted.jpg(仅保留红色区域)" << std::endl;
// 步骤 6b:用 Canny 边缘检测在 mask 上找灯条边缘
// 这样可以看到灯条的轮廓是否被正确提取
cv::Mat mask_edges;
cv::Canny(mask_cleaned, mask_edges, 50, 150);
cv::imwrite("06b_mask_edges.jpg", mask_edges);
std::cout << " -> 已保存 06b_mask_edges.jpg(mask 的边缘检测结果)" << std::endl;
// 步骤 6c:使用 findContours 找到灯条的轮廓并画框(进阶内容预览)
// 这是 RoboMaster 视觉的下一步:从 mask 中提取灯条的位置
// findContours: 在二值图中寻找所有白色区域的轮廓
std::vector<std::vector<cv::Point>> contours;
// RETR_EXTERNAL: 只检索最外层轮廓
// CHAIN_APPROX_SIMPLE: 压缩水平/垂直/对角线,只保留端点
cv::findContours(mask_cleaned, contours, cv::RETR_EXTERNAL,
cv::CHAIN_APPROX_SIMPLE);
// 创建结果图:在原图上画检测框
cv::Mat result = scene.clone(); // clone() 创建一个完全相同的副本
int valid_count = 0; // 统计有效灯条的个数
for (size_t i = 0; i < contours.size(); i++)
{
// 用 boundingRect 获取能包围轮廓的最小直立矩形
cv::Rect bbox = cv::boundingRect(contours[i]);
// 筛选条件:灯条应该是细长竖直的矩形
// - 面积不能太小(过滤残留的噪点)
double area = bbox.width * bbox.height;
if (area < 100) continue; // 面积小于 100 像素,忽略
// - 高度应该大于宽度(竖直灯条的特征)
if (bbox.height < bbox.width * 2) continue; // 不满足"细长"条件,忽略
// 通过筛选,画框标记
cv::rectangle(result, bbox, cv::Scalar(0, 255, 0), 3); // 绿色边框,线宽 3
valid_count++;
// 在框上方写标签
cv::putText(result, "Target " + std::to_string(valid_count),
cv::Point(bbox.x, bbox.y - 10),
cv::FONT_HERSHEY_SIMPLEX, 0.6,
cv::Scalar(0, 255, 0), 2);
}
cv::imwrite("06c_result.jpg", result);
std::cout << " -> 已保存 06c_result.jpg(最终结果:绿灯条框标记 = 检测到的灯条)" << std::endl;
std::cout << " -> 共检测到 " << valid_count << " 个有效灯条" << std::endl;
// ==========================================
// 总结
// ==========================================
std::cout << std::endl;
std::cout << "===== 实验 8 完成 =====" << std::endl;
std::cout << "流水线回顾(你完整的加工步骤):" << std::endl;
std::cout << " 1. 读取 / 生成图像" << std::endl;
std::cout << " 2. BGR → HSV 颜色空间转换" << std::endl;
std::cout << " 3. 高斯模糊(去噪)" << std::endl;
std::cout << " 4. inRange 提取目标颜色" << std::endl;
std::cout << " 5. 形态学操作(清理 mask)" << std::endl;
std::cout << " 6. 轮廓检测 + 画框(定位目标)" << std::endl;
std::cout << std::endl;
std::cout << "恭喜你!这已经是一个简化的 RoboMaster 灯条检测流程了!" << std::endl;
std::cout << "接下来的课程将学习:颜色空间调参、轮廓筛选优化、以及用工业相机实时处理" << std::endl;
return 0;
}