/* * ============================================================ * 实验 8:综合流水线 (Pipeline) * 知识点:串联前面 7 个实验的所有技术 * ============================================================ * * 【实验目标】 * 1. 将前面学到的所有技术串联成一个完整流程 * 2. 理解图像处理中"预处理 → 提取 → 后处理"的标准套路 * 3. 为 RoboMaster 灯条检测打下完整基础 * * 【生活化比喻】 * 一条完整的图像处理流水线就像工厂的"生产流水线": * - 原材料进厂(读取图像) * - 清洗处理(模糊去噪、颜色转换) * - 核心加工(颜色提取 inRange) * - 精修打磨(形态学操作去掉毛刺) * - 质检包装(边缘检测找轮廓、画框标记) * - 成品出厂(保存结果图片) * * 每一步都依赖前一步的输出,整个流程像一个多米诺骨牌, * 任何一个环节出问题,最终结果都会受影响。 * * 【RoboMaster 灯条检测的标准流水线】 * 1. 读取摄像头采集的图像 * 2. BGR → HSV 颜色空间转换 * 3. inRange 提取目标颜色(红/蓝) * 4. 形态学操作(开运算去噪、闭运算填充) * 5. 轮廓检测与筛选(按面积、长宽比、角度过滤) * 6. 画出灯条的位置框 * * 本实验完整演示整个流程! */ #include #include 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)); // --- 先画背景噪点(必须在灯条之前!否则噪点会覆盖在灯条上)--- for (int i = 0; i < 200; i++) { int x = rand() % 600; int y = rand() % 450; uchar noise_color = static_cast(rand() % 128 + 60); cv::circle(scene, cv::Point(x, y), 1, cv::Scalar(noise_color, noise_color, noise_color), -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); // 黄色干扰圆 // 画装甲板的中间横梁(深灰色,不会被 inRange 误检为红色) // 注意:横梁必须在灯条之前画!这样红色灯条覆盖在横梁上方, // 保证灯条从顶到底都是纯红色,不被横梁切断 cv::rectangle(scene, cv::Point(200, 180), cv::Point(400, 220), cv::Scalar(80, 80, 80), -1); // 深灰色横梁 // 在上面画红色灯条(最后画,确保不被其他元素覆盖) // 灯条是竖直的细长矩形,模拟 RoboMaster 装甲板侧面的 LED 灯条 // 左灯条 cv::rectangle(scene, cv::Point(200, 120), cv::Point(240, 280), cv::Scalar(0, 0, 255), -1); // 纯红色竖直矩形 (B=0,G=0,R=255) // 右灯条 cv::rectangle(scene, cv::Point(360, 120), cv::Point(400, 280), cv::Scalar(0, 0, 255), -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> contours; // RETR_EXTERNAL: 只检索最外层轮廓 // CHAIN_APPROX_SIMPLE: 压缩水平/垂直/对角线,只保留端点 cv::findContours(mask_cleaned, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); // 创建结果图:在原图上画检测框 cv::Mat result = scene.clone(); // clone() 创建一个完全相同的副本 std::cout << " -> 共找到 " << contours.size() << " 个轮廓" << std::endl; 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; }