227 lines
10 KiB
C++
227 lines
10 KiB
C++
/*
|
||
* ============================================================
|
||
* 实验 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;
|
||
}
|