diff --git a/README.md b/README.md index eb5a407..eda94db 100644 --- a/README.md +++ b/README.md @@ -2111,16 +2111,30 @@ include_directories(${OpenCV_INCLUDE_DIRS}) * * 【实验目标】 * 1. 将前面学到的所有技术串联成一个完整流程 - * 2. 理解图像处理中"预处理 -> 提取 -> 后处理"的标准套路 + * 2. 理解图像处理中"预处理 → 提取 → 后处理"的标准套路 * 3. 为 RoboMaster 灯条检测打下完整基础 * + * 【生活化比喻】 + * 一条完整的图像处理流水线就像工厂的"生产流水线": + * - 原材料进厂(读取图像) + * - 清洗处理(模糊去噪、颜色转换) + * - 核心加工(颜色提取 inRange) + * - 精修打磨(形态学操作去掉毛刺) + * - 质检包装(边缘检测找轮廓、画框标记) + * - 成品出厂(保存结果图片) + * + * 每一步都依赖前一步的输出,整个流程像一个多米诺骨牌, + * 任何一个环节出问题,最终结果都会受影响。 + * * 【RoboMaster 灯条检测的标准流水线】 * 1. 读取摄像头采集的图像 - * 2. BGR -> HSV 颜色空间转换 + * 2. BGR → HSV 颜色空间转换 * 3. inRange 提取目标颜色(红/蓝) * 4. 形态学操作(开运算去噪、闭运算填充) * 5. 轮廓检测与筛选(按面积、长宽比、角度过滤) * 6. 画出灯条的位置框 + * + * 本实验完整演示整个流程! */ #include @@ -2135,25 +2149,10 @@ int main() // ========================================== std::cout << "[阶段 1/6] 生成模拟 RoboMaster 场景的测试图片..." << std::endl; + // 600×450 画布,深灰色背景(模拟比赛场地的昏暗环境) cv::Mat scene(450, 600, CV_8UC3, cv::Scalar(40, 40, 40)); - // 画两个"红色灯条"(竖直细长矩形) - 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; @@ -2163,29 +2162,61 @@ int main() 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 + // 阶段 2:颜色空间转换 BGR → HSV // ========================================== - std::cout << "[阶段 2/6] BGR -> HSV 颜色空间转换..." << std::endl; + 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; + 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); @@ -2195,60 +2226,84 @@ int main() 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 提取原图中的红色区域 + // 步骤 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 上 + // 步骤 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 + boundingRect 画框 + // 步骤 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(); - int valid_count = 0; + // 创建结果图:在原图上画检测框 + 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]); - // 筛选条件:面积 > 100 且 高度 > 宽度 x 2(竖直细长 = 灯条特征) + // 筛选条件:灯条应该是细长竖直的矩形 + // - 面积不能太小(过滤残留的噪点) double area = bbox.width * bbox.height; - if (area < 100) continue; - if (bbox.height < bbox.width * 2) continue; + if (area < 100) continue; // 面积小于 100 像素,忽略 - cv::rectangle(result, bbox, cv::Scalar(0, 255, 0), 3); + // - 高度应该大于宽度(竖直灯条的特征) + 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, @@ -2256,21 +2311,24 @@ int main() } cv::imwrite("06c_result.jpg", result); - std::cout << " -> 已保存 06c_result.jpg(最终结果:绿灯条框 = 检测到的灯条)" << std::endl; + 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 << "流水线回顾(你完整的加工步骤):" << std::endl; std::cout << " 1. 读取 / 生成图像" << std::endl; - std::cout << " 2. BGR -> HSV 颜色空间转换" << 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; } diff --git a/STUDENT_GUIDE.md b/STUDENT_GUIDE.md index 998e51c..b50fb8e 100644 --- a/STUDENT_GUIDE.md +++ b/STUDENT_GUIDE.md @@ -2338,16 +2338,30 @@ include_directories(${OpenCV_INCLUDE_DIRS}) * * 【实验目标】 * 1. 将前面学到的所有技术串联成一个完整流程 - * 2. 理解图像处理中"预处理 -> 提取 -> 后处理"的标准套路 + * 2. 理解图像处理中"预处理 → 提取 → 后处理"的标准套路 * 3. 为 RoboMaster 灯条检测打下完整基础 * + * 【生活化比喻】 + * 一条完整的图像处理流水线就像工厂的"生产流水线": + * - 原材料进厂(读取图像) + * - 清洗处理(模糊去噪、颜色转换) + * - 核心加工(颜色提取 inRange) + * - 精修打磨(形态学操作去掉毛刺) + * - 质检包装(边缘检测找轮廓、画框标记) + * - 成品出厂(保存结果图片) + * + * 每一步都依赖前一步的输出,整个流程像一个多米诺骨牌, + * 任何一个环节出问题,最终结果都会受影响。 + * * 【RoboMaster 灯条检测的标准流水线】 * 1. 读取摄像头采集的图像 - * 2. BGR -> HSV 颜色空间转换 + * 2. BGR → HSV 颜色空间转换 * 3. inRange 提取目标颜色(红/蓝) * 4. 形态学操作(开运算去噪、闭运算填充) * 5. 轮廓检测与筛选(按面积、长宽比、角度过滤) * 6. 画出灯条的位置框 + * + * 本实验完整演示整个流程! */ #include @@ -2362,25 +2376,10 @@ int main() // ========================================== std::cout << "[阶段 1/6] 生成模拟 RoboMaster 场景的测试图片..." << std::endl; + // 600×450 画布,深灰色背景(模拟比赛场地的昏暗环境) cv::Mat scene(450, 600, CV_8UC3, cv::Scalar(40, 40, 40)); - // 画两个"红色灯条"(竖直细长矩形) - 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; @@ -2390,29 +2389,61 @@ int main() 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 + // 阶段 2:颜色空间转换 BGR → HSV // ========================================== - std::cout << "[阶段 2/6] BGR -> HSV 颜色空间转换..." << std::endl; + 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; + 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); @@ -2422,60 +2453,84 @@ int main() 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 提取原图中的红色区域 + // 步骤 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 上 + // 步骤 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 + boundingRect 画框 + // 步骤 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(); - int valid_count = 0; + // 创建结果图:在原图上画检测框 + 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]); - // 筛选条件:面积 > 100 且 高度 > 宽度 x 2(竖直细长 = 灯条特征) + // 筛选条件:灯条应该是细长竖直的矩形 + // - 面积不能太小(过滤残留的噪点) double area = bbox.width * bbox.height; - if (area < 100) continue; - if (bbox.height < bbox.width * 2) continue; + if (area < 100) continue; // 面积小于 100 像素,忽略 - cv::rectangle(result, bbox, cv::Scalar(0, 255, 0), 3); + // - 高度应该大于宽度(竖直灯条的特征) + 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, @@ -2483,21 +2538,24 @@ int main() } cv::imwrite("06c_result.jpg", result); - std::cout << " -> 已保存 06c_result.jpg(最终结果:绿灯条框 = 检测到的灯条)" << std::endl; + 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 << "流水线回顾(你完整的加工步骤):" << std::endl; std::cout << " 1. 读取 / 生成图像" << std::endl; - std::cout << " 2. BGR -> HSV 颜色空间转换" << 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; } diff --git a/exp8_pipeline/main.cpp b/exp8_pipeline/main.cpp index 648c344..504ca40 100644 --- a/exp8_pipeline/main.cpp +++ b/exp8_pipeline/main.cpp @@ -47,18 +47,15 @@ int main() // 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); // 稍暗一点的红色 + // --- 先画背景噪点(必须在灯条之前!否则噪点会覆盖在灯条上)--- + 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); // 蓝色干扰圆 @@ -66,16 +63,20 @@ int main() 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(rand() % 128 + 60); - cv::circle(scene, cv::Point(x, y), 1, - cv::Scalar(noise_color, noise_color, noise_color), -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; @@ -177,6 +178,8 @@ int main() // 创建结果图:在原图上画检测框 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++) { diff --git a/images/exp1_forest.jpg b/images/exp1_forest.jpg new file mode 100644 index 0000000..d712187 Binary files /dev/null and b/images/exp1_forest.jpg differ diff --git a/images/exp1_sample.jpg b/images/exp1_sample.jpg deleted file mode 100644 index a89b84a..0000000 Binary files a/images/exp1_sample.jpg and /dev/null differ diff --git a/images/exp2_colorful.jpg b/images/exp2_colorful.jpg deleted file mode 100644 index 67d60f0..0000000 Binary files a/images/exp2_colorful.jpg and /dev/null differ diff --git a/images/exp2_macro.jpg b/images/exp2_macro.jpg new file mode 100644 index 0000000..877f007 Binary files /dev/null and b/images/exp2_macro.jpg differ diff --git a/images/exp3_texture.jpg b/images/exp3_texture.jpg index 2f98d83..9341b7b 100644 Binary files a/images/exp3_texture.jpg and b/images/exp3_texture.jpg differ diff --git a/images/exp4_building.jpg b/images/exp4_building.jpg new file mode 100644 index 0000000..a3bb931 Binary files /dev/null and b/images/exp4_building.jpg differ diff --git a/images/exp4_edges.jpg b/images/exp4_edges.jpg deleted file mode 100644 index 6056492..0000000 Binary files a/images/exp4_edges.jpg and /dev/null differ diff --git a/images/exp5_brightness.jpg b/images/exp5_brightness.jpg index 7528a7d..95d1eef 100644 Binary files a/images/exp5_brightness.jpg and b/images/exp5_brightness.jpg differ diff --git a/images/exp6_shapes.jpg b/images/exp6_shapes.jpg deleted file mode 100644 index 34cba88..0000000 Binary files a/images/exp6_shapes.jpg and /dev/null differ diff --git a/images/exp6_typo.jpg b/images/exp6_typo.jpg new file mode 100644 index 0000000..698f068 Binary files /dev/null and b/images/exp6_typo.jpg differ diff --git a/images/exp7_red.jpg b/images/exp7_red_apple.jpg similarity index 100% rename from images/exp7_red.jpg rename to images/exp7_red_apple.jpg diff --git a/images/exp8_complex.jpg b/images/exp8_complex.jpg deleted file mode 100644 index 2e10e6f..0000000 Binary files a/images/exp8_complex.jpg and /dev/null differ diff --git a/images/exp8_scene.jpg b/images/exp8_scene.jpg new file mode 100644 index 0000000..a5bdcf1 Binary files /dev/null and b/images/exp8_scene.jpg differ