Files
vision-training-2026-opencvcpp/exp8_pipeline/main.cpp

227 lines
10 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* ============================================================
* 实验 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;
}