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

222 lines
11 KiB
C++
Raw Normal View History

/*
* ============================================================
* 实验 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 <opencv2/opencv.hpp>
#include <iostream>
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;
}