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

222 lines
11 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* ============================================================
* 实验 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;
}