/* * ============================================================ * 实验 7:颜色提取 —— inRange * 知识点:cv::inRange、HSV 颜色空间、颜色阈值 * ============================================================ * * 【实验目标】 * 1. 掌握 cv::inRange 函数的用法——按颜色范围提取目标 * 2. 理解为什么要用 HSV 而不是 BGR 来做颜色提取 * 3. 掌握 RoboMaster 红色灯条的 HSV 范围设定 * * 【生活化比喻】 * inRange 就像是一个"颜色过滤器"(或者说"筛子"): * - 你有一堆混在一起的彩色糖果(红、绿、蓝、黄...) * - 你只想挑出红色的糖果 * - inRange 就是那个"只能通过红色"的筛子 * - 筛出来的就是红色糖果(二值 mask),其他颜色的都被丢掉了 * * 为什么要用 HSV? * - 在 BGR 空间:红色 = (0, 0, 255),但光线暗的时候可能是 (0, 0, 80) * 亮度一变化,三个值全变了!很难用一个范围同时框住亮红和暗红 * - 在 HSV 空间:红色 = H≈0(或 H≈180,因为色相是环状的) * H (色相) 不受亮度影响!亮红、暗红都是 H≈0 * S (饱和度) 保证颜色够"纯",不要灰扑扑的 * V (明度) 保证不是黑的或太暗 * - 这样我们可以分别设定三个分量的范围,灵活又鲁棒 * * 【RoboMaster 红色灯条】 * 在比赛场地中,机器人装甲板上有红色/蓝色的 LED 灯条 * 视觉组需要通过摄像头找到这些灯条的位置 * inRange 提取颜色 → 找到灯条的轮廓 → 计算位置 → 发送给云台瞄准 * 这是视觉组最基础也最重要的操作之一! * * 【函数详解】 * cv::inRange(src, lowerb, upperb, dst) * * src :输入图像(推荐用 HSV 格式) * lowerb :颜色下界(Scalar(H_low, S_low, V_low)) * 或灰度下界(对灰度图直接取 Scalar(low)) * upperb :颜色上界(Scalar(H_high, S_high, V_high)) * dst :输出二值 mask(满足条件 = 255 白色,不满足 = 0 黑色) * * 返回的 dst 是一个单通道二值图(CV_8UC1): * - 白色区域(255):像素值在 [lowerb, upperb] 区间内 * - 黑色区域(0) :像素值不在区间内 * * 【OpenCV HSV 取值范围(重要!)】 * H (Hue):0 ~ 180(不是 360!OpenCV 把 360° 映射到 0~180 了) * S (Sat):0 ~ 255 * V (Val):0 ~ 255 * * 红色在 H 轴上的位置(因为色相是一个环): * H = 0 附近:纯红色 * H = 10 附近:偏橙的红色 * H = 160~180:偏紫的红色(从紫色那一端绕过来的红色) * * 所以提取红色通常需要两个范围: * 范围 1:H ∈ [0, 10],S ∈ [100, 255],V ∈ [100, 255] * 范围 2:H ∈ [160, 180], S ∈ [100, 255], V ∈ [100, 255] * 然后把两个范围的 mask 相加(按位或)得到最终结果 */ #include #include int main() { // ========================================== // 第一部分:生成包含多种颜色的测试图片 // ========================================== std::cout << "[步骤 1] 正在生成彩色测试图片..." << std::endl; // 400×600 画布,白色背景 cv::Mat image(400, 600, CV_8UC3, cv::Scalar(255, 255, 255)); // 画 6 个彩色矩形,模拟不同颜色的物体 // 颜色选择:红色、蓝色(RoboMaster 的两种对手颜色)、绿色、黄色、青色、品红 cv::rectangle(image, cv::Point(20, 50), cv::Point(180, 130), cv::Scalar(0, 0, 255), -1); // 红色 (B=0, G=0, R=255) cv::rectangle(image, cv::Point(210, 50), cv::Point(370, 130), cv::Scalar(255, 0, 0), -1); // 蓝色 (B=255, G=0, R=0) cv::rectangle(image, cv::Point(400, 50), cv::Point(560, 130), cv::Scalar(0, 255, 0), -1); // 绿色 (B=0, G=255, R=0) cv::rectangle(image, cv::Point(20, 160), cv::Point(180, 240), cv::Scalar(0, 255, 255), -1); // 黄色 (B=0, G=255, R=255) cv::rectangle(image, cv::Point(210, 160), cv::Point(370, 240), cv::Scalar(255, 255, 0), -1); // 青色 (B=255, G=255, R=0) cv::rectangle(image, cv::Point(400, 160), cv::Point(560, 240), cv::Scalar(255, 0, 255), -1); // 品红 (B=255, G=0, R=255) // 再画几个不同深浅的红色(模拟实际场景中光照不均的情况) // 深红色 cv::rectangle(image, cv::Point(20, 260), cv::Point(100, 320), cv::Scalar(0, 0, 120), -1); // 中等红色 cv::rectangle(image, cv::Point(120, 260), cv::Point(200, 320), cv::Scalar(0, 0, 200), -1); // 亮红色 cv::rectangle(image, cv::Point(220, 260), cv::Point(300, 320), cv::Scalar(0, 50, 255), -1); // 稍带一点绿,模拟光照偏暖 // 偏紫红色 cv::rectangle(image, cv::Point(320, 260), cv::Point(400, 320), cv::Scalar(100, 0, 200), -1); // 写标签 cv::putText(image, "Red", cv::Point(80, 105), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 255, 255), 2); cv::putText(image, "Blue", cv::Point(270, 105), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 255, 255), 2); cv::putText(image, "Green", cv::Point(450, 105), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(255, 255, 255), 2); cv::putText(image, "Yellow", cv::Point(65, 215), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); cv::putText(image, "Cyan", cv::Point(270, 215), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); cv::putText(image, "Magenta", cv::Point(430, 215), cv::FONT_HERSHEY_SIMPLEX, 0.7, cv::Scalar(0, 0, 0), 2); cv::putText(image, "Dark Red / Mid Red / Bright / Purple Red", cv::Point(20, 355), cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(0, 0, 0), 1); cv::imwrite("original.jpg", image); std::cout << " -> 原始彩色图片已保存为 original.jpg" << std::endl; // ========================================== // 第二部分:转换为 HSV // ========================================== std::cout << "[步骤 2] 将 BGR 图片转换为 HSV..." << std::endl; cv::Mat hsv; cv::cvtColor(image, hsv, cv::COLOR_BGR2HSV); cv::imwrite("hsv.jpg", hsv); std::cout << " -> HSV 图片已保存为 hsv.jpg" << std::endl; // ========================================== // 第三部分:使用 inRange 提取红色 // ========================================== std::cout << "[步骤 3] 使用 inRange 提取红色区域..." << std::endl; // 如前所述,红色的 H 值在 OpenCV 中分为两段: // 第一段:H 在 0~10 之间(橙红色 ~ 正红色 ~ 偏红色) // 第二段:H 在 160~180 之间(偏紫色 ~ 绕回到红色) // 【重要】S 和 V 的阈值设定说明: // S(饱和度)不能设太低,否则灰白色区域也会被选进来 // 通常设 S_low = 100(也就是约 40% 饱和度以上算"有颜色") // V(明度)不能设太低,否则黑色/很暗的区域也会被选进来 // 通常设 V_low = 100(也就是亮度 > 100/255 算"不是黑的") // --- 提取红色范围 1:H ∈ [0, 10] --- cv::Scalar lower_red1(0, 100, 100); // H=0, S=100, V=100 cv::Scalar upper_red1(10, 255, 255); // H=10, S=255, V=255 cv::Mat mask_red1; cv::inRange(hsv, lower_red1, upper_red1, mask_red1); cv::imwrite("mask_red1.jpg", mask_red1); std::cout << " -> 红色范围 1 的 mask 已保存为 mask_red1.jpg" << std::endl; // --- 提取红色范围 2:H ∈ [160, 180] --- cv::Scalar lower_red2(160, 100, 100); // H=160, S=100, V=100 cv::Scalar upper_red2(180, 255, 255); // H=180, S=255, V=255 cv::Mat mask_red2; cv::inRange(hsv, lower_red2, upper_red2, mask_red2); cv::imwrite("mask_red2.jpg", mask_red2); std::cout << " -> 红色范围 2 的 mask 已保存为 mask_red2.jpg" << std::endl; // --- 合并两个红色范围的 mask --- // cv::bitwise_or:对两幅图像做"按位或"操作 // 效果:mask 中任一为白色(255)的像素,结果就是白色 // 相当于:"范围1的红色" 或者 "范围2的红色" 都算红色 cv::Mat mask_red_combined; cv::bitwise_or(mask_red1, mask_red2, mask_red_combined); cv::imwrite("mask_red_combined.jpg", mask_red_combined); std::cout << " -> 合并后的红色 mask 已保存为 mask_red_combined.jpg" << std::endl; // ========================================== // 第四部分:把 mask 应用到原图(可视化提取结果) // ========================================== std::cout << "[步骤 4] 用 mask 提取原图中的红色部分..." << std::endl; // cv::bitwise_and:对两幅图像做"按位与"操作 // 把 mask 当作"模板"盖在原图上: // mask 中白色(255)的地方 → 保留原图该像素 // mask 中黑色(0)的地方 → 变成黑色 cv::Mat red_extracted; cv::bitwise_and(image, image, red_extracted, mask_red_combined); cv::imwrite("red_extracted.jpg", red_extracted); std::cout << " -> 提取出的红色区域已保存为 red_extracted.jpg" << std::endl; std::cout << " (只有红色物体保留,其他区域全黑)" << std::endl; // ========================================== // 第五部分(附加):也提取蓝色(RoboMaster 中另一种常见目标) // ========================================== std::cout << "[步骤 5] 额外实验:提取蓝色区域..." << std::endl; // 蓝色在 HSV 中的 H 范围大约是 100~130 // 蓝色不像红色那样跨 0 度,所以只需要一个范围 cv::Scalar lower_blue(100, 100, 100); // H=100, S=100, V=100 cv::Scalar upper_blue(130, 255, 255); // H=130, S=255, V=255 cv::Mat mask_blue; cv::inRange(hsv, lower_blue, upper_blue, mask_blue); cv::imwrite("mask_blue.jpg", mask_blue); cv::Mat blue_extracted; cv::bitwise_and(image, image, blue_extracted, mask_blue); cv::imwrite("blue_extracted.jpg", blue_extracted); std::cout << " -> 蓝色 mask 已保存为 mask_blue.jpg" << std::endl; std::cout << " -> 提取出的蓝色区域已保存为 blue_extracted.jpg" << std::endl; // 总结 std::cout << std::endl; std::cout << "===== 实验 7 完成 =====" << std::endl; std::cout << "你学会了:" << std::endl; std::cout << " 1. inRange 按颜色范围提取目标区域" << std::endl; std::cout << " 2. 为什么要用 HSV:分离颜色和亮度" << std::endl; std::cout << " 3. 红色需要两个 H 范围(因为色相是环状)" << std::endl; std::cout << " 4. bitwise_or 合并 mask,bitwise_and 提取原图" << std::endl; std::cout << " 5. 这是 RoboMaster 视觉灯条检测的基础!" << std::endl; return 0; }