feat: init OpenCV C++ training course with 8 experiments and detailed tutorial
This commit is contained in:
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
# 编译生成目录(每个实验的 build/)
|
||||
build/
|
||||
# CMake 生成文件
|
||||
CMakeFiles/
|
||||
CMakeCache.txt
|
||||
cmake_install.cmake
|
||||
Makefile
|
||||
# 编译产物
|
||||
*.o
|
||||
*.obj
|
||||
*.exe
|
||||
*.out
|
||||
# 测试输出图片(由程序运行时生成)
|
||||
*.jpg
|
||||
*.png
|
||||
*.jpeg
|
||||
*.bmp
|
||||
# IDE 文件
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
41
exp1_read_write/CMakeLists.txt
Normal file
41
exp1_read_write/CMakeLists.txt
Normal file
@@ -0,0 +1,41 @@
|
||||
# ============================================================
|
||||
# 实验 1:图像读取与保存 - CMake 构建配置文件
|
||||
# ============================================================
|
||||
|
||||
# cmake_minimum_required: 指定构建此项目所需的最低 CMake 版本
|
||||
# VERSION 3.10 是一个相对较老但足够稳定的版本(WSL Ubuntu 默认通常满足)
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
# project: 给你的项目起一个名字
|
||||
# 这个名字会出现在生成的解决方案中,也方便你辨认
|
||||
# 命名规则:exp + 实验编号 + 下划线 + 实验英文名
|
||||
project(exp1_read_write)
|
||||
|
||||
# set(CMAKE_CXX_STANDARD 11): 指定使用 C++11 标准
|
||||
# C++11 提供了 auto、范围 for 等现代特性,代码写起来更舒服
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
# CMAKE_CXX_STANDARD_REQUIRED: 设为 ON 表示"必须"使用 C++11
|
||||
# 如果编译器不支持 C++11,CMake 会直接报错,而不是偷偷降级
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# find_package: 寻找外部库,这里是寻找 OpenCV
|
||||
# REQUIRED 表示 OpenCV 是必须的,找不到就报错并停止配置
|
||||
# CMake 会自动查找 OpenCV 的头文件路径和库文件路径
|
||||
find_package(OpenCV REQUIRED)
|
||||
|
||||
# add_executable: 告诉 CMake 我们要生成一个可执行文件
|
||||
# 第一个参数 "exp1" 是可执行文件的名字(编译后运行 ./exp1)
|
||||
# 第二个参数 "main.cpp" 是参与编译的源文件(可以有多个)
|
||||
add_executable(exp1 main.cpp)
|
||||
|
||||
# target_link_libraries: 将 OpenCV 的库文件链接到我们的可执行文件上
|
||||
# ${OpenCV_LIBS} 是 find_package(OpenCV) 自动填充的变量
|
||||
# 它包含了 OpenCV 所有需要的 .so 库文件路径
|
||||
# 不写这一行的话,编译时会出现"未定义引用"的链接错误
|
||||
target_link_libraries(exp1 ${OpenCV_LIBS})
|
||||
|
||||
# include_directories: 将 OpenCV 的头文件目录添加到编译器的搜索路径中
|
||||
# ${OpenCV_INCLUDE_DIRS} 也是 find_package 自动填充的
|
||||
# 这样编译器才能找到 #include <opencv2/opencv.hpp> 这样的头文件
|
||||
include_directories(${OpenCV_INCLUDE_DIRS})
|
||||
162
exp1_read_write/main.cpp
Normal file
162
exp1_read_write/main.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* ============================================================
|
||||
* 实验 1:图像读取与保存
|
||||
* 知识点:cv::Mat、cv::imread、cv::imwrite
|
||||
* ============================================================
|
||||
*
|
||||
* 【实验目标】
|
||||
* 1. 理解 cv::Mat —— OpenCV 中存储图像的核心数据类型
|
||||
* 2. 学会用 cv::imread 从文件中读取图像
|
||||
* 3. 学会用 cv::imwrite 将图像保存到文件
|
||||
* 4. 养成检查图像是否读取成功的习惯
|
||||
*
|
||||
* 【生活化比喻】
|
||||
* 把 cv::Mat 想象成一张"棋盘格纸":
|
||||
* - 每个格子是一个"像素"(就是图像上的一个小点)
|
||||
* - 格子里填的数字代表颜色(比如 0=黑, 255=白)
|
||||
* - 整张纸就是一幅图像,OpenCV 把它存在计算机内存里
|
||||
*
|
||||
* cv::imread 就像是"用扫描仪把一张打印好的照片扫进电脑"
|
||||
* cv::imwrite 就像是"用打印机把电脑里的图打印出来"
|
||||
*/
|
||||
|
||||
// 包含 OpenCV 核心头文件
|
||||
// opencv.hpp 是一个"总头文件",包含了 OpenCV 所有常用模块
|
||||
// 这是最方便的写法,学习阶段推荐使用
|
||||
#include <opencv2/opencv.hpp>
|
||||
|
||||
// 包含标准输入输出流,用于打印信息到终端
|
||||
#include <iostream>
|
||||
|
||||
// main 函数 —— 所有 C++ 程序的入口点
|
||||
// argc 是命令行参数个数,argv 是参数内容数组
|
||||
// 在本实验中我们不使用命令行参数,所以参数名可以省略
|
||||
int main()
|
||||
{
|
||||
// ==========================================
|
||||
// 第一部分:生成一张测试图片
|
||||
// ==========================================
|
||||
// 为什么先生成一张图片而不是直接读取?
|
||||
// 因为我们要确保每个同学都能运行成功,不依赖任何外部图片文件
|
||||
// 等学会读取之后,你可以把自己的照片放到这个文件夹里来测试!
|
||||
|
||||
// cv::Mat(行数, 列数, 类型, 颜色值) —— 创建一个矩阵/图像
|
||||
// - 480:图像高度 480 像素(行数),也就是垂直方向有 480 个点
|
||||
// - 640:图像宽度 640 像素(列数),也就是水平方向有 640 个点
|
||||
// - CV_8UC3:每个像素用 8 位无符号整数 × 3 个通道表示
|
||||
// - 8U = 8-bit Unsigned(取值范围 0~255)
|
||||
// - C3 = 3 个通道(Channel),也就是 B(蓝)、G(绿)、R(红)
|
||||
// - 所以每个像素需要 3 个字节来存储颜色信息
|
||||
// - cv::Scalar(255, 255, 255):Scalar 是一个四元组 (B, G, R, Alpha)
|
||||
// - 这里 (255,255,255) 就是纯白色(B=255, G=255, R=255)
|
||||
// 注意 OpenCV 的颜色顺序是 BGR,不是我们常见的 RGB!
|
||||
// 这是历史原因造成的,记住就好:OpenCV 里永远是 BGR 顺序
|
||||
std::cout << "[步骤 1] 正在生成一张 640x480 的彩色测试图片..." << std::endl;
|
||||
cv::Mat canvas(480, 640, CV_8UC3, cv::Scalar(50, 50, 50)); // 深灰色背景
|
||||
|
||||
// 在画布上画一些彩色图形,让图片看起来有趣
|
||||
// cv::circle(图像, 圆心, 半径, 颜色, 线宽) —— 画圆
|
||||
// 圆心用 cv::Point(x, y) 表示,注意 x 是水平方向(列),y 是垂直方向(行)
|
||||
// 线宽 = -1 表示填充整个圆(实心圆)
|
||||
cv::circle(canvas, cv::Point(150, 120), 80, cv::Scalar(255, 0, 0), -1); // 蓝色实心圆(B=255)
|
||||
cv::circle(canvas, cv::Point(320, 120), 80, cv::Scalar(0, 255, 0), -1); // 绿色实心圆(G=255)
|
||||
cv::circle(canvas, cv::Point(490, 120), 80, cv::Scalar(0, 0, 255), -1); // 红色实心圆(R=255)
|
||||
|
||||
// cv::rectangle(图像, 左上角, 右下角, 颜色, 线宽) —— 画矩形
|
||||
// 线宽 = 3 表示空心矩形,边框宽度为 3 像素
|
||||
cv::rectangle(canvas, cv::Point(100, 280), cv::Point(540, 440),
|
||||
cv::Scalar(0, 255, 255), 3); // 黄色空心矩形(B=0, G=255, R=255)
|
||||
|
||||
// cv::putText(图像, 文字, 起始位置, 字体, 字号, 颜色, 线宽) —— 写文字
|
||||
cv::putText(canvas, "OpenCV Experiment 1", cv::Point(130, 350),
|
||||
cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(255, 255, 255), 2); // 白色文字
|
||||
|
||||
// 将生成的图片保存为文件
|
||||
// cv::imwrite(文件路径, 图像矩阵) —— 把内存中的图像写入磁盘
|
||||
// 返回值是 bool 类型:true 表示保存成功,false 表示失败
|
||||
// 文件格式由扩展名决定,这里 .jpg 表示用 JPEG 格式压缩保存
|
||||
bool write_ok = cv::imwrite("test_image.jpg", canvas);
|
||||
if (write_ok)
|
||||
{
|
||||
std::cout << " -> 测试图片已保存为 test_image.jpg" << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << " [错误] 保存测试图片失败!" << std::endl;
|
||||
return -1; // 返回 -1 表示程序异常退出
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 第二部分:读取图片
|
||||
// ==========================================
|
||||
std::cout << "[步骤 2] 正在读取 test_image.jpg..." << std::endl;
|
||||
|
||||
// cv::imread(文件路径, 读取模式) —— 从磁盘读取图像到内存
|
||||
// 返回值是一个 cv::Mat 对象,包含了图像的像素数据
|
||||
//
|
||||
// 读取模式(第二个参数):
|
||||
// cv::IMREAD_COLOR = 1 :始终读取为 3 通道彩色图(默认值)
|
||||
// cv::IMREAD_GRAYSCALE = 0 :始终读取为单通道灰度图
|
||||
// cv::IMREAD_UNCHANGED = -1 :保持原样(包括 Alpha 透明通道)
|
||||
//
|
||||
// 这里使用 cv::IMREAD_COLOR 确保读进来是彩色图像
|
||||
cv::Mat image = cv::imread("test_image.jpg", cv::IMREAD_COLOR);
|
||||
|
||||
// 【极其重要】检查图像是否读取成功!
|
||||
// image.empty() 返回 true 表示 Mat 对象中没有数据
|
||||
// 常见失败原因:
|
||||
// 1. 文件名写错了(大小写、拼写错误)
|
||||
// 2. 文件不在程序运行的当前目录下
|
||||
// 3. 文件损坏或格式不支持
|
||||
// 养成好习惯:每次 imread 之后都要检查 empty()!
|
||||
if (image.empty())
|
||||
{
|
||||
std::cerr << "[错误] 无法读取图像!请检查:" << std::endl;
|
||||
std::cerr << " 1. test_image.jpg 是否在当前目录下?" << std::endl;
|
||||
std::cerr << " 2. 文件名是否正确(注意大小写)?" << std::endl;
|
||||
return -1; // 返回 -1 表示程序因错误退出
|
||||
}
|
||||
|
||||
// 打印图像的基本信息
|
||||
// image.rows —— 图像高度(行数,垂直像素数)
|
||||
// image.cols —— 图像宽度(列数,水平像素数)
|
||||
// image.channels() —— 通道数(彩色=3,灰度=1)
|
||||
// image.type() —— 像素数据类型编号
|
||||
std::cout << " -> 读取成功!图像信息:" << std::endl;
|
||||
std::cout << " 宽度 (cols): " << image.cols << " 像素" << std::endl;
|
||||
std::cout << " 高度 (rows): " << image.rows << " 像素" << std::endl;
|
||||
std::cout << " 通道数: " << image.channels() << " (3=彩色, 1=灰度)" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第三部分:保存读到的图片(验证读取无误)
|
||||
// ==========================================
|
||||
std::cout << "[步骤 3] 正在保存为 output.jpg..." << std::endl;
|
||||
|
||||
// 把读到的图像原封不动地保存为新文件
|
||||
// 这样可以验证"读取 → 保存"整个流程是否正常
|
||||
bool save_ok = cv::imwrite("output.jpg", image);
|
||||
if (save_ok)
|
||||
{
|
||||
std::cout << " -> 图片已成功保存为 output.jpg" << std::endl;
|
||||
std::cout << " -> 请在 Windows 资源管理器中打开该文件查看结果!" << std::endl;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::cerr << " [错误] 保存图片失败!请检查磁盘空间和写入权限。" << std::endl;
|
||||
return -1;
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 总结
|
||||
// ==========================================
|
||||
std::cout << std::endl;
|
||||
std::cout << "===== 实验 1 完成 =====" << std::endl;
|
||||
std::cout << "你学会了:" << std::endl;
|
||||
std::cout << " 1. cv::Mat 是 OpenCV 存储图像的数据类型" << std::endl;
|
||||
std::cout << " 2. cv::imread() 从文件读取图像" << std::endl;
|
||||
std::cout << " 3. cv::imwrite() 将图像写入文件" << std::endl;
|
||||
std::cout << " 4. 读取后一定要检查 empty()" << std::endl;
|
||||
|
||||
// return 0 表示程序正常结束
|
||||
return 0;
|
||||
}
|
||||
36
exp2_color_space/CMakeLists.txt
Normal file
36
exp2_color_space/CMakeLists.txt
Normal file
@@ -0,0 +1,36 @@
|
||||
# ============================================================
|
||||
# 实验 2:颜色空间转换 - CMake 构建配置文件
|
||||
# ============================================================
|
||||
|
||||
# 指定 CMake 最低版本要求(3.10 是一个广泛兼容的版本)
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
# 项目名称:exp2_color_space
|
||||
# 这个名称用于标识整个 CMake 项目
|
||||
project(exp2_color_space)
|
||||
|
||||
# 设置 C++ 标准为 C++11
|
||||
# C++11 引入了许多现代语言特性,使代码更简洁、更安全
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
# 强制要求编译器必须支持 C++11,否则停止配置
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# 查找并加载 OpenCV 库
|
||||
# REQUIRED 参数:如果找不到 OpenCV,CMake 会立即报错退出
|
||||
# 这一步会设置 ${OpenCV_LIBS} 和 ${OpenCV_INCLUDE_DIRS} 等变量
|
||||
find_package(OpenCV REQUIRED)
|
||||
|
||||
# 定义可执行文件目标
|
||||
# 可执行文件名:exp2(运行命令为 ./exp2)
|
||||
# 源文件:main.cpp
|
||||
add_executable(exp2 main.cpp)
|
||||
|
||||
# 将 OpenCV 的动态库/静态库链接到 exp2 可执行文件
|
||||
# 没有这行代码,编译器会报告大量"未定义符号"错误
|
||||
# 因为 main.cpp 中调用的 OpenCV 函数实现都在这些库里
|
||||
target_link_libraries(exp2 ${OpenCV_LIBS})
|
||||
|
||||
# 将 OpenCV 的头文件目录添加到 include 路径
|
||||
# 使 #include <opencv2/opencv.hpp> 能被正确找到
|
||||
include_directories(${OpenCV_INCLUDE_DIRS})
|
||||
161
exp2_color_space/main.cpp
Normal file
161
exp2_color_space/main.cpp
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* ============================================================
|
||||
* 实验 2:颜色空间转换(BGR → Gray、HSV、Lab、YCrCb)
|
||||
* 知识点:cv::cvtColor
|
||||
* ============================================================
|
||||
*
|
||||
* 【实验目标】
|
||||
* 1. 理解"颜色空间"的概念 —— 同一张图片的不同"坐标系"描述
|
||||
* 2. 掌握 cv::cvtColor 函数的使用方法
|
||||
* 3. 了解 Gray、HSV、Lab、YCrCb 各自的特点和用途
|
||||
*
|
||||
* 【生活化比喻】
|
||||
* "颜色空间"就像是描述一个人位置的不同方式:
|
||||
* - BGR 像"经纬度坐标"(给出精确的三维数值)
|
||||
* - Gray 像"海拔高度"(一维数值,只有亮度)
|
||||
* - HSV 像"方向+距离+高度"(色相、饱和度、明度)
|
||||
* 色相(H) :是什么颜色(红/黄/绿/蓝...),像钟表上的角度
|
||||
* 饱和度(S):颜色有多"纯"(鲜艳还是发灰),像果汁的浓度
|
||||
* 明度(V) :颜色有多亮(亮还是暗),像灯光的亮度
|
||||
* - 不同场景用不同描述方式更方便!
|
||||
*
|
||||
* RoboMaster 任务中,HSV 是最常用的颜色空间
|
||||
* 因为它把"颜色"和"亮度"分开了,不受光照变化的影响
|
||||
*/
|
||||
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <iostream>
|
||||
#include <vector> // std::vector —— 动态数组,用于生成渐变色块
|
||||
|
||||
int main()
|
||||
{
|
||||
// ==========================================
|
||||
// 第一部分:生成一张色彩丰富的测试图片
|
||||
// ==========================================
|
||||
std::cout << "[步骤 1] 正在生成彩色测试图片..." << std::endl;
|
||||
|
||||
// 创建一个 300×600 的彩色画布,背景为黑色
|
||||
cv::Mat color_image(300, 600, CV_8UC3, cv::Scalar(0, 0, 0));
|
||||
|
||||
// 画出 6 个彩色矩形,代表不同颜色
|
||||
// 颜色顺序:红、绿、蓝、黄、青、品红
|
||||
// 每个矩形宽 100 像素,高 300 像素
|
||||
std::vector<cv::Scalar> colors = {
|
||||
cv::Scalar(0, 0, 255), // 红色 (B=0, G=0, R=255)
|
||||
cv::Scalar(0, 255, 0), // 绿色 (B=0, G=255, R=0)
|
||||
cv::Scalar(255, 0, 0), // 蓝色 (B=255, G=0, R=0)
|
||||
cv::Scalar(0, 255, 255), // 黄色 (B=0, G=255, R=255)
|
||||
cv::Scalar(255, 255, 0), // 青色 (B=255, G=255, R=0)
|
||||
cv::Scalar(255, 0, 255) // 品红色 (B=255, G=0, R=255)
|
||||
};
|
||||
|
||||
// 用循环依次画出每个色块
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
// 每个矩形宽 100 像素,所以左上角 x = i * 100
|
||||
int x_start = i * 100;
|
||||
cv::rectangle(color_image,
|
||||
cv::Point(x_start, 0), // 左上角
|
||||
cv::Point(x_start + 100, 300), // 右下角
|
||||
colors[i], // 填充颜色
|
||||
-1); // -1 = 填充
|
||||
}
|
||||
|
||||
// 在每个色块上写颜色名称
|
||||
std::vector<std::string> color_names = {"Red", "Green", "Blue", "Yellow", "Cyan", "Magenta"};
|
||||
for (int i = 0; i < 6; i++)
|
||||
{
|
||||
cv::putText(color_image, color_names[i],
|
||||
cv::Point(i * 100 + 10, 150),
|
||||
cv::FONT_HERSHEY_SIMPLEX, 0.6,
|
||||
cv::Scalar(255, 255, 255), // 白色文字
|
||||
2);
|
||||
}
|
||||
|
||||
// 保存原始彩色图
|
||||
cv::imwrite("original.jpg", color_image);
|
||||
std::cout << " -> 原始彩色图已保存为 original.jpg" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第二部分:执行各种颜色空间转换
|
||||
// ==========================================
|
||||
|
||||
// --- 转换 1:BGR → 灰度 (Gray) ---
|
||||
// 灰度图像只有一个通道,每个像素的取值范围是 0~255
|
||||
// 转换公式(心理学灰度):
|
||||
// Gray = 0.299 × R + 0.587 × G + 0.114 × B
|
||||
// 人眼对绿色最敏感(权重 0.587),对蓝色最不敏感(权重 0.114)
|
||||
std::cout << "[步骤 2] BGR -> Gray(灰度)..." << std::endl;
|
||||
cv::Mat gray_image;
|
||||
cv::cvtColor(color_image, gray_image, cv::COLOR_BGR2GRAY);
|
||||
cv::imwrite("gray.jpg", gray_image);
|
||||
std::cout << " -> 已保存为 gray.jpg(单通道灰度图)" << std::endl;
|
||||
|
||||
// --- 转换 2:BGR → HSV ---
|
||||
// HSV 三分量:
|
||||
// H (Hue,色相) :0~180(OpenCV 中缩放到此范围,实际是 0~360 度)
|
||||
// S (Saturation,饱和度):0~255(越大越鲜艳,0 就是灰色)
|
||||
// V (Value,明度) :0~255(越大越亮)
|
||||
std::cout << "[步骤 3] BGR -> HSV..." << std::endl;
|
||||
cv::Mat hsv_image;
|
||||
cv::cvtColor(color_image, hsv_image, cv::COLOR_BGR2HSV);
|
||||
cv::imwrite("hsv.jpg", hsv_image);
|
||||
std::cout << " -> 已保存为 hsv.jpg(注意:HSV 图片直接查看颜色会怪怪的,"
|
||||
<< "因为查看器把它当 BGR 解释了)" << std::endl;
|
||||
|
||||
// --- 转换 3:BGR → Lab ---
|
||||
// Lab 是设备无关的颜色空间,设计目标是接近人类视觉感知
|
||||
// L (Lightness,亮度) :0~100(实际 OpenCV 映射到 0~255)
|
||||
// a (绿-红轴) :负值偏绿,正值偏红
|
||||
// b (蓝-黄轴) :负值偏蓝,正值偏黄
|
||||
std::cout << "[步骤 4] BGR -> Lab..." << std::endl;
|
||||
cv::Mat lab_image;
|
||||
cv::cvtColor(color_image, lab_image, cv::COLOR_BGR2Lab);
|
||||
cv::imwrite("lab.jpg", lab_image);
|
||||
std::cout << " -> 已保存为 lab.jpg" << std::endl;
|
||||
|
||||
// --- 转换 4:BGR → YCrCb ---
|
||||
// YCrCb 常用于图像/视频压缩(如 JPEG、MPEG)
|
||||
// Y (亮度分量) :类似灰度
|
||||
// Cr (红色色度分量) :红色与亮度的差异
|
||||
// Cb (蓝色色度分量) :蓝色与亮度的差异
|
||||
// 人眼对亮度敏感,对色度不敏感,所以压缩时可以对 CbCr 做更多压缩
|
||||
std::cout << "[步骤 5] BGR -> YCrCb..." << std::endl;
|
||||
cv::Mat ycrcb_image;
|
||||
cv::cvtColor(color_image, ycrcb_image, cv::COLOR_BGR2YCrCb);
|
||||
cv::imwrite("ycrcb.jpg", ycrcb_image);
|
||||
std::cout << " -> 已保存为 ycrcb.jpg" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第三部分:演示 HSV 各通道的分离
|
||||
// ==========================================
|
||||
std::cout << "[步骤 6] 分离 HSV 各通道..." << std::endl;
|
||||
|
||||
// cv::split(输入图像, 输出通道数组) —— 将多通道图像拆分成多个单通道图像
|
||||
// 例如 3 通道的 HSV 图像可以拆成 H、S、V 三张独立的灰度图
|
||||
// std::vector<cv::Mat> 是一个能存放多个 cv::Mat 的动态数组
|
||||
std::vector<cv::Mat> hsv_channels;
|
||||
cv::split(hsv_image, hsv_channels);
|
||||
// 拆分后:
|
||||
// hsv_channels[0] = H 通道(色相)
|
||||
// hsv_channels[1] = S 通道(饱和度)
|
||||
// hsv_channels[2] = V 通道(明度)
|
||||
|
||||
// 分别保存三个通道
|
||||
cv::imwrite("hsv_channel_H.jpg", hsv_channels[0]); // 色相通道
|
||||
cv::imwrite("hsv_channel_S.jpg", hsv_channels[1]); // 饱和度通道
|
||||
cv::imwrite("hsv_channel_V.jpg", hsv_channels[2]); // 明度通道
|
||||
std::cout << " -> 已保存 hsv_channel_H.jpg / S.jpg / V.jpg" << std::endl;
|
||||
|
||||
// 总结
|
||||
std::cout << std::endl;
|
||||
std::cout << "===== 实验 2 完成 =====" << std::endl;
|
||||
std::cout << "你学会了:" << std::endl;
|
||||
std::cout << " 1. cv::cvtColor() 可以在不同颜色空间之间转换" << std::endl;
|
||||
std::cout << " 2. BGR → Gray:加权平均得到亮度" << std::endl;
|
||||
std::cout << " 3. BGR → HSV:分开颜色和亮度,对光照变化更鲁棒" << std::endl;
|
||||
std::cout << " 4. cv::split() 把多通道图像拆成单通道" << std::endl;
|
||||
std::cout << " 5. RoboMaster 中 HSV 是核心!后面实验 7 会用到" << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
28
exp3_gaussian_blur/CMakeLists.txt
Normal file
28
exp3_gaussian_blur/CMakeLists.txt
Normal file
@@ -0,0 +1,28 @@
|
||||
# ============================================================
|
||||
# 实验 3:高斯模糊 - CMake 构建配置文件
|
||||
# ============================================================
|
||||
|
||||
# 最低 CMake 版本要求
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
|
||||
# 项目标识名称
|
||||
project(exp3_gaussian_blur)
|
||||
|
||||
# 使用 C++11 标准进行编译
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
# 强制 C++11,不可降级
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# 自动搜索并配置 OpenCV 的头文件和库文件路径
|
||||
find_package(OpenCV REQUIRED)
|
||||
|
||||
# 生成名为 "exp3" 的可执行文件,编译源文件 main.cpp
|
||||
add_executable(exp3 main.cpp)
|
||||
|
||||
# 链接 OpenCV 库到可执行文件
|
||||
# ${OpenCV_LIBS} 变量由 find_package 自动设置,包含所有必需的 OpenCV 模块
|
||||
target_link_libraries(exp3 ${OpenCV_LIBS})
|
||||
|
||||
# 添加 OpenCV 头文件搜索路径
|
||||
include_directories(${OpenCV_INCLUDE_DIRS})
|
||||
139
exp3_gaussian_blur/main.cpp
Normal file
139
exp3_gaussian_blur/main.cpp
Normal file
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* ============================================================
|
||||
* 实验 3:高斯模糊 (Gaussian Blur)
|
||||
* 知识点:cv::GaussianBlur
|
||||
* ============================================================
|
||||
*
|
||||
* 【实验目标】
|
||||
* 1. 理解"模糊"本质上是什么(图像怎么变模糊的)
|
||||
* 2. 掌握 cv::GaussianBlur 函数的参数含义
|
||||
* 3. 观察不同卷积核大小对模糊效果的影响
|
||||
*
|
||||
* 【生活化比喻】
|
||||
* 高斯模糊就像透过一块"毛玻璃"或"近视眼镜"看东西:
|
||||
* - 原来的图像每个像素都很清晰(边界分明)
|
||||
* - 模糊后,每个像素和它周围的像素颜色混在一起了
|
||||
* - 核(kernel)越大,相当于毛玻璃越厚,混的范围越大,越模糊
|
||||
*
|
||||
* 为什么叫"高斯"?
|
||||
* - 高斯(Gauss)是一位伟大的数学家
|
||||
* - 他发现了一种"钟形曲线"(正态分布),用来描述自然界的很多现象
|
||||
* - 在图像上,就是用这个钟形曲线来决定"周围的像素各占多大权重"
|
||||
* - 距离中心越近的像素权重越大(理所当然嘛,最近的邻居影响最大)
|
||||
*
|
||||
* 在 RoboMaster 中为什么需要模糊?
|
||||
* - 消除图像噪声(摄像头传感器本身的噪点)
|
||||
* - 让后续的边缘检测、颜色提取更稳定
|
||||
* - 像是"先把细节捋平,再找关键特征"
|
||||
*
|
||||
* 【函数详解】
|
||||
* cv::GaussianBlur(src, dst, ksize, sigmaX, sigmaY, borderType)
|
||||
*
|
||||
* src :输入图像(可以是一张有明显边缘的图)
|
||||
* dst :输出图像(大小和类型与输入相同)
|
||||
* ksize :高斯核的大小,必须是奇数!(如 (3,3), (5,5), (15,15))
|
||||
* 为什么是奇数?因为要有一个"中心像素",核以它为中心对称展开
|
||||
* sigmaX :X 方向上的高斯标准差(控制左右方向的模糊程度)
|
||||
* 如果设为 0,OpenCV 会根据 ksize 自动帮你算一个合适的值
|
||||
* sigmaY :Y 方向上的高斯标准差(控制上下方向的模糊程度)
|
||||
* 如果设为 0,等于 sigmaX
|
||||
* borderType :边界处理方式(可选参数,默认即可)
|
||||
*/
|
||||
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <iostream>
|
||||
#include <vector>
|
||||
|
||||
int main()
|
||||
{
|
||||
std::cout << "[步骤 1] 正在生成测试图片(含清晰边缘和纹理)..." << std::endl;
|
||||
|
||||
// 创建一张 400×400 的白色画布
|
||||
cv::Mat original(400, 400, CV_8UC3, cv::Scalar(255, 255, 255));
|
||||
|
||||
// 画各种形状 —— 这些形状有清晰的边缘,模糊后效果明显
|
||||
// 蓝色实心圆
|
||||
cv::circle(original, cv::Point(100, 100), 60, cv::Scalar(255, 0, 0), -1);
|
||||
// 绿色矩形框
|
||||
cv::rectangle(original, cv::Point(200, 50), cv::Point(350, 150),
|
||||
cv::Scalar(0, 200, 0), 3);
|
||||
// 红色三角形(用 line 画三条线)
|
||||
cv::line(original, cv::Point(50, 320), cv::Point(200, 200),
|
||||
cv::Scalar(0, 0, 255), 5);
|
||||
cv::line(original, cv::Point(200, 200), cv::Point(350, 320),
|
||||
cv::Scalar(0, 0, 255), 5);
|
||||
cv::line(original, cv::Point(350, 320), cv::Point(50, 320),
|
||||
cv::Scalar(0, 0, 255), 5);
|
||||
|
||||
// 写文字(文字边缘也是测试模糊的好素材)
|
||||
cv::putText(original, "OpenCV", cv::Point(80, 370),
|
||||
cv::FONT_HERSHEY_SIMPLEX, 1.2, cv::Scalar(0, 0, 0), 3);
|
||||
|
||||
// 画一些散点(模拟噪声,模糊后会被消除)
|
||||
for (int i = 0; i < 300; i++)
|
||||
{
|
||||
int x = rand() % 400; // 随机 X 坐标(0~399)
|
||||
int y = rand() % 400; // 随机 Y 坐标(0~399)
|
||||
// 每个点画一个 2×2 的小方块(因为单个像素可能看不清)
|
||||
cv::rectangle(original, cv::Point(x, y), cv::Point(x + 2, y + 2),
|
||||
cv::Scalar(0, 0, 0), -1);
|
||||
}
|
||||
|
||||
cv::imwrite("original.jpg", original);
|
||||
std::cout << " -> 原始图片已保存为 original.jpg(注意看上面的散点噪声)" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 核心实验:对比不同模糊程度
|
||||
// ==========================================
|
||||
|
||||
// --- 模糊程度 1:轻微模糊(ksize = 3×3)---
|
||||
// 3×3 的核非常小,只混合当前像素和它最近的 8 个邻居
|
||||
// 效果:能消除最细小的噪点,但基本保留图像结构
|
||||
std::cout << "[步骤 2] 应用轻微高斯模糊 (ksize = 3x3)..." << std::endl;
|
||||
cv::Mat blur_light;
|
||||
cv::GaussianBlur(original, blur_light, cv::Size(3, 3), 0, 0);
|
||||
cv::imwrite("blur_3x3.jpg", blur_light);
|
||||
std::cout << " -> 已保存为 blur_3x3.jpg(轻微模糊,细节基本保留)" << std::endl;
|
||||
|
||||
// --- 模糊程度 2:中度模糊(ksize = 9×9)---
|
||||
// 9×9 的核覆盖了 81 个像素的范围
|
||||
// 效果:散点噪声明显消失,边缘变得柔和
|
||||
std::cout << "[步骤 3] 应用中度高模糊 (ksize = 9x9)..." << std::endl;
|
||||
cv::Mat blur_medium;
|
||||
cv::GaussianBlur(original, blur_medium, cv::Size(9, 9), 0, 0);
|
||||
cv::imwrite("blur_9x9.jpg", blur_medium);
|
||||
std::cout << " -> 已保存为 blur_9x9.jpg(中度模糊,噪声消失,边缘变软)" << std::endl;
|
||||
|
||||
// --- 模糊程度 3:重度模糊(ksize = 21×21)---
|
||||
// 21×21 的核非常大,覆盖了 441 个像素的范围
|
||||
// 效果:图像变得严重模糊,细节几乎全部丢失
|
||||
// 这在实际应用中通常不会用这么大的核,这里纯粹为了演示
|
||||
std::cout << "[步骤 4] 应用重度高斯模糊 (ksize = 21x21)..." << std::endl;
|
||||
cv::Mat blur_heavy;
|
||||
cv::GaussianBlur(original, blur_heavy, cv::Size(21, 21), 0, 0);
|
||||
cv::imwrite("blur_21x21.jpg", blur_heavy);
|
||||
std::cout << " -> 已保存为 blur_21x21.jpg(重度模糊,近乎一片朦胧)" << std::endl;
|
||||
|
||||
// --- 模糊程度 4:X/Y 方向不同模糊程度 ---
|
||||
// 前面三个例子 sigmaX 和 sigmaY 都设为 0(自动计算)
|
||||
// 这里我们手动指定不同方向的标准差:X 方向模糊大,Y 方向模糊小
|
||||
std::cout << "[步骤 5] 应用各向异性模糊 (sigmaX=15, sigmaY=3)..." << std::endl;
|
||||
cv::Mat blur_aniso;
|
||||
cv::GaussianBlur(original, blur_aniso, cv::Size(0, 0), 15, 3);
|
||||
// 注意:当 ksize 设为 (0,0) 时,OpenCV 会根据 sigma 自动计算合适的核大小
|
||||
// sigmaX=15(水平方向大模糊)比 sigmaY=3(垂直方向小模糊)大很多
|
||||
// 所以效果是:水平方向被"抹平"得很厉害,垂直方向相对清晰
|
||||
cv::imwrite("blur_anisotropic.jpg", blur_aniso);
|
||||
std::cout << " -> 已保存为 blur_anisotropic.jpg(水平方向很糊,垂直方向较清晰)" << std::endl;
|
||||
|
||||
// 总结
|
||||
std::cout << std::endl;
|
||||
std::cout << "===== 实验 3 完成 =====" << std::endl;
|
||||
std::cout << "你学会了:" << std::endl;
|
||||
std::cout << " 1. GaussianBlur 用高斯分布对像素进行加权平均" << std::endl;
|
||||
std::cout << " 2. ksize 越大,模糊范围越大(必须是奇数!)" << std::endl;
|
||||
std::cout << " 3. sigma 控制模糊的标准差(0 表示自动计算)" << std::endl;
|
||||
std::cout << " 4. 模糊是后续边缘检测、颜色提取的重要预处理步骤" << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
11
exp4_canny/CMakeLists.txt
Normal file
11
exp4_canny/CMakeLists.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
# ============================================================
|
||||
# 实验 4:Canny 边缘检测 - CMake 构建配置文件
|
||||
# ============================================================
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(exp4_canny)
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
find_package(OpenCV REQUIRED)
|
||||
add_executable(exp4 main.cpp)
|
||||
target_link_libraries(exp4 ${OpenCV_LIBS})
|
||||
include_directories(${OpenCV_INCLUDE_DIRS})
|
||||
145
exp4_canny/main.cpp
Normal file
145
exp4_canny/main.cpp
Normal file
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* ============================================================
|
||||
* 实验 4:Canny 边缘检测
|
||||
* 知识点:cv::Canny
|
||||
* ============================================================
|
||||
*
|
||||
* 【实验目标】
|
||||
* 1. 理解什么是"边缘"——图像中亮度急剧变化的地方
|
||||
* 2. 掌握 Canny 边缘检测算法的原理和参数
|
||||
* 3. 观察不同阈值对边缘检测结果的影响
|
||||
*
|
||||
* 【生活化比喻】
|
||||
* 边缘检测就像用铅笔"描边":
|
||||
* - 给你一张彩色照片,让你把照片中所有物体的轮廓描出来
|
||||
* - Canny 算法就是一个"自动描边机器人"
|
||||
* - threshold1 像是"最低标准":低于这个的统统不是边缘
|
||||
* - threshold2 像是"最高标准":高于这个的绝对是边缘
|
||||
* - 介于两者之间的,看它是否和已确认的边缘连着(连着的算,孤立的忽略)
|
||||
*
|
||||
* 这就是所谓的"迟滞阈值"(Hysteresis Threshold):
|
||||
* - 高于高阈值 → 肯定是边缘(强边缘)
|
||||
* - 低于低阈值 → 肯定不是边缘(直接丢弃)
|
||||
* - 介于中间 → 如果和强边缘相邻,就算是边缘;否则丢弃
|
||||
* - 这就像"朋友的朋友也是朋友"——只要你能通过边缘连到确认真边缘,你就是边缘
|
||||
*
|
||||
* 【Canny 算法的五个步骤(了解即可)】
|
||||
* 1. 高斯滤波:去除噪声(因为噪声会被误检测为边缘)
|
||||
* 2. 计算梯度:找到亮度变化最大的方向和大小
|
||||
* 3. 非极大值抑制:只保留梯度方向上最大的那个像素(细化边缘)
|
||||
* 4. 双阈值检测:用两个阈值筛选强弱边缘
|
||||
* 5. 边缘连接:通过滞后阈值连接断开的边缘
|
||||
*
|
||||
* 【函数详解】
|
||||
* cv::Canny(src, dst, threshold1, threshold2, apertureSize, L2gradient)
|
||||
*
|
||||
* src :输入图像(必须是 8 位单通道灰度图!)
|
||||
* dst :输出图像(二值图,边缘为白色 255,非边缘为黑色 0)
|
||||
* threshold1 :低阈值(用于弱边缘判断)
|
||||
* threshold2 :高阈值(用于强边缘判断)
|
||||
* 推荐比例:threshold2 ≈ 2× threshold1 或 3× threshold1
|
||||
* apertureSize :Sobel 算子的孔径大小(默认 3,一般不用改)
|
||||
* L2gradient :是否使用更精确的 L2 范数计算梯度(默认 false)
|
||||
*/
|
||||
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main()
|
||||
{
|
||||
// ==========================================
|
||||
// 第一部分:生成测试图片
|
||||
// ==========================================
|
||||
std::cout << "[步骤 1] 正在生成测试图片(包含各种边缘结构)..." << std::endl;
|
||||
|
||||
// 创建一个 400×500 的白色画布
|
||||
cv::Mat image(400, 500, CV_8UC3, cv::Scalar(255, 255, 255));
|
||||
|
||||
// 画各种几何图形 —— 这些都有清晰的边缘
|
||||
cv::circle(image, cv::Point(100, 100), 60, cv::Scalar(50, 50, 50), -1); // 深灰实心圆
|
||||
cv::rectangle(image, cv::Point(220, 40), cv::Point(420, 160),
|
||||
cv::Scalar(100, 100, 100), -1); // 灰实心矩形
|
||||
cv::rectangle(image, cv::Point(300, 60), cv::Point(360, 140),
|
||||
cv::Scalar(200, 200, 200), -1); // 浅灰小矩形(内嵌)
|
||||
|
||||
// 画一些粗细不等的线条
|
||||
cv::line(image, cv::Point(20, 200), cv::Point(480, 200),
|
||||
cv::Scalar(0, 0, 0), 2); // 细横线
|
||||
cv::line(image, cv::Point(20, 250), cv::Point(480, 250),
|
||||
cv::Scalar(0, 0, 0), 8); // 粗横线
|
||||
cv::line(image, cv::Point(250, 200), cv::Point(250, 380),
|
||||
cv::Scalar(0, 0, 0), 2); // 细竖线
|
||||
cv::line(image, cv::Point(100, 300), cv::Point(400, 380),
|
||||
cv::Scalar(50, 50, 50), 4); // 斜线
|
||||
|
||||
// 画一些文字
|
||||
cv::putText(image, "Canny", cv::Point(150, 370),
|
||||
cv::FONT_HERSHEY_SIMPLEX, 1.5, cv::Scalar(0, 0, 0), 3);
|
||||
|
||||
cv::imwrite("original.jpg", image);
|
||||
std::cout << " -> 原始图片已保存为 original.jpg" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第二部分:转为灰度图(Canny 输入必须是灰度图!)
|
||||
// ==========================================
|
||||
std::cout << "[步骤 2] 转换为灰度图..." << std::endl;
|
||||
cv::Mat gray;
|
||||
cv::cvtColor(image, gray, cv::COLOR_BGR2GRAY);
|
||||
cv::imwrite("gray.jpg", gray);
|
||||
std::cout << " -> 灰度图已保存为 gray.jpg" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第三部分:Canny 边缘检测 —— 不同阈值对比
|
||||
// ==========================================
|
||||
|
||||
// --- Canny 场景 1:低阈值 (50, 150) ---
|
||||
// threshold1=50 意味着:强度大于 50 的可能是弱边缘
|
||||
// threshold2=150 意味着:强度大于 150 的绝对是强边缘
|
||||
// 由于两个阈值都相对较低,会检测出很多细节(包括一些纹理和噪声)
|
||||
std::cout << "[步骤 3] Canny 边缘检测 (阈值 50 / 150)..." << std::endl;
|
||||
cv::Mat edges_low;
|
||||
cv::Canny(gray, edges_low, 50, 150);
|
||||
cv::imwrite("edges_low.jpg", edges_low);
|
||||
std::cout << " -> 已保存为 edges_low.jpg(低阈值,细节丰富)" << std::endl;
|
||||
|
||||
// --- Canny 场景 2:中等阈值 (100, 200) ---
|
||||
// 这是比较常用的阈值范围,能平衡"检测到足够边缘"和"抑制噪声"
|
||||
std::cout << "[步骤 4] Canny 边缘检测 (阈值 100 / 200)..." << std::endl;
|
||||
cv::Mat edges_mid;
|
||||
cv::Canny(gray, edges_mid, 100, 200);
|
||||
cv::imwrite("edges_mid.jpg", edges_mid);
|
||||
std::cout << " -> 已保存为 edges_mid.jpg(中等阈值,适中结果)" << std::endl;
|
||||
|
||||
// --- Canny 场景 3:高阈值 (200, 400) ---
|
||||
// 两个阈值都很高,只有灰度变化非常剧烈的地方才会被检测为边缘
|
||||
// 结果:只保留最强的边缘,细节大量丢失
|
||||
std::cout << "[步骤 5] Canny 边缘检测 (阈值 200 / 400)..." << std::endl;
|
||||
cv::Mat edges_high;
|
||||
cv::Canny(gray, edges_high, 200, 400);
|
||||
cv::imwrite("edges_high.jpg", edges_high);
|
||||
std::cout << " -> 已保存为 edges_high.jpg(高阈值,只保留强边缘)" << std::endl;
|
||||
|
||||
// --- Canny 场景 4:先模糊再做边缘检测 ---
|
||||
// 这是实际应用中最常见的做法:先高斯模糊去噪,再做 Canny
|
||||
// 对比 edges_low 的结果,你会发现先模糊后边缘更"干净"
|
||||
std::cout << "[步骤 6] 先高斯模糊,再 Canny 边缘检测..." << std::endl;
|
||||
cv::Mat blurred;
|
||||
cv::GaussianBlur(gray, blurred, cv::Size(5, 5), 0);
|
||||
cv::Mat edges_blurred;
|
||||
cv::Canny(blurred, edges_blurred, 50, 150);
|
||||
cv::imwrite("blurred.jpg", blurred);
|
||||
cv::imwrite("edges_after_blur.jpg", edges_blurred);
|
||||
std::cout << " -> 已保存为 blurred.jpg(高斯模糊后)" << std::endl;
|
||||
std::cout << " -> 已保存为 edges_after_blur.jpg(模糊后再做 Canny,边缘更干净)" << std::endl;
|
||||
|
||||
// 总结
|
||||
std::cout << std::endl;
|
||||
std::cout << "===== 实验 4 完成 =====" << std::endl;
|
||||
std::cout << "你学会了:" << std::endl;
|
||||
std::cout << " 1. Canny 是一种经典的边缘检测算法" << std::endl;
|
||||
std::cout << " 2. threshold1 和 threshold2 控制检测灵敏度" << std::endl;
|
||||
std::cout << " 3. 输入必须是灰度图(单通道!)" << std::endl;
|
||||
std::cout << " 4. 先模糊 + 再 Canny = 更干净的边缘" << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
9
exp5_threshold/CMakeLists.txt
Normal file
9
exp5_threshold/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
# 实验 5:图像阈值化 - CMake 构建配置
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(exp5_threshold)
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
find_package(OpenCV REQUIRED)
|
||||
add_executable(exp5 main.cpp)
|
||||
target_link_libraries(exp5 ${OpenCV_LIBS})
|
||||
include_directories(${OpenCV_INCLUDE_DIRS})
|
||||
151
exp5_threshold/main.cpp
Normal file
151
exp5_threshold/main.cpp
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* ============================================================
|
||||
* 实验 5:图像阈值化 (Thresholding)
|
||||
* 知识点:cv::threshold
|
||||
* ============================================================
|
||||
*
|
||||
* 【实验目标】
|
||||
* 1. 理解"阈值化"是什么——把灰度图变成黑白图
|
||||
* 2. 掌握 cv::threshold 函数的各种阈值类型
|
||||
* 3. 理解阈值化在图像分割中的基础地位
|
||||
*
|
||||
* 【生活化比喻】
|
||||
* 阈值化就像是"考试及格线":
|
||||
* - 考试成绩 0~100 分
|
||||
* - 学校规定 60 分及格:≥ 60 算"通过",< 60 算"不通过"
|
||||
* - 这里的"60"就是阈值 (threshold)
|
||||
*
|
||||
* 应用到图像上:
|
||||
* - 每个像素的灰度值 0~255(相当于 0~100 分)
|
||||
* - 设定阈值 128(相当于 60 分)
|
||||
* - 灰度 ≥ 128 的像素 → 变白色(255,相当于"通过")
|
||||
* - 灰度 < 128 的像素 → 变黑色(0,相当于"不通过")
|
||||
* - 整张图就只剩黑白两色了!
|
||||
*
|
||||
* 为什么需要这个操作?
|
||||
* - 把"感兴趣的物体"从"背景"中分离出来
|
||||
* - 为后续的形态学操作(实验 6)做准备
|
||||
* - 很多算法(如轮廓检测)需要二值图像作为输入
|
||||
*
|
||||
* 【函数详解】
|
||||
* cv::threshold(src, dst, thresh, maxval, type)
|
||||
*
|
||||
* src :输入图像(必须是单通道灰度图)
|
||||
* dst :输出图像(和输入同样大小和类型)
|
||||
* thresh :阈值(就是那个"及格线")
|
||||
* maxval :最大值(当像素满足条件时,赋予它的新值,通常是 255)
|
||||
* type :阈值类型(决定"怎么比较"和"比较后怎么赋值")
|
||||
*
|
||||
* OpenCV 支持的阈值类型:
|
||||
* cv::THRESH_BINARY :>阈值→maxval,否则→0 (标准二值化)
|
||||
* cv::THRESH_BINARY_INV :>阈值→0,否则→maxval (反二值化)
|
||||
* cv::THRESH_TRUNC :>阈值→阈值,否则→原值不变 (截断)
|
||||
* cv::THRESH_TOZERO :>阈值→原值不变,否则→0 (置零)
|
||||
* cv::THRESH_TOZERO_INV :>阈值→0,否则→原值不变 (反置零)
|
||||
*/
|
||||
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main()
|
||||
{
|
||||
// ==========================================
|
||||
// 第一部分:生成一张灰度渐变图
|
||||
// ==========================================
|
||||
std::cout << "[步骤 1] 正在生成灰度渐变测试图..." << std::endl;
|
||||
|
||||
// 创建一张 300(高)× 512(宽)的灰度图
|
||||
// CV_8UC1 表示:8位无符号整数、1通道(灰度)
|
||||
cv::Mat gradient(300, 512, CV_8UC1);
|
||||
|
||||
// 用手工方式生成从左到右的灰度渐变
|
||||
// 左边的像素灰度值为 0(全黑),右边为 255(全白),逐渐过渡
|
||||
for (int row = 0; row < gradient.rows; row++)
|
||||
{
|
||||
for (int col = 0; col < gradient.cols; col++)
|
||||
{
|
||||
// 灰度值 = 列索引 × 255 / 511(比例映射)
|
||||
// 这样 col=0 对应灰度 = 0,col=511 对应灰度 = 255
|
||||
// gradient.at<uchar>(row, col) —— 访问/修改单个像素值
|
||||
// .at<uchar> 中的 uchar = unsigned char = 8比特无符号整数
|
||||
// 因为 CV_8UC1 的每个像素就是 uchar 类型
|
||||
gradient.at<uchar>(row, col) = static_cast<uchar>(col * 255 / 511);
|
||||
}
|
||||
}
|
||||
|
||||
// 在渐变图上画几个标记,方便观察阈值效果
|
||||
// 画一条垂直线标记阈值 128 的位置
|
||||
cv::line(gradient, cv::Point(256, 0), cv::Point(256, 299),
|
||||
cv::Scalar(255), 1); // 在 x=256 处画白线(灰度 128 的位置)
|
||||
// 放一些文字
|
||||
cv::putText(gradient, "0", cv::Point(5, 150),
|
||||
cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(255));
|
||||
cv::putText(gradient, "128", cv::Point(240, 150),
|
||||
cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(255));
|
||||
cv::putText(gradient, "255", cv::Point(470, 150),
|
||||
cv::FONT_HERSHEY_SIMPLEX, 0.6, cv::Scalar(0));
|
||||
|
||||
cv::imwrite("gradient.jpg", gradient);
|
||||
std::cout << " -> 原始渐变图已保存为 gradient.jpg" << std::endl;
|
||||
std::cout << " (从左到右灰度从 0 渐变到 255)" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第二部分:演示各种阈值类型
|
||||
// ==========================================
|
||||
// 统一使用阈值 128(中间值),maxval = 255(纯白)
|
||||
|
||||
// --- 类型 1:THRESH_BINARY(标准二值化)---
|
||||
// 逻辑:如果 像素值 > 128,则赋值为 255;否则赋值为 0
|
||||
// 效果:左边一半(<128)全黑,右边一半(>128)全白
|
||||
std::cout << "[步骤 2] THRESH_BINARY(标准二值化)..." << std::endl;
|
||||
cv::Mat thresh_binary;
|
||||
cv::threshold(gradient, thresh_binary, 128, 255, cv::THRESH_BINARY);
|
||||
cv::imwrite("thresh_binary.jpg", thresh_binary);
|
||||
std::cout << " -> 已保存为 thresh_binary.jpg" << std::endl;
|
||||
|
||||
// --- 类型 2:THRESH_BINARY_INV(反二值化)---
|
||||
// 逻辑:如果 像素值 > 128,则赋值为 0;否则赋值为 255
|
||||
// 效果:和上面完全相反——左边白,右边黑
|
||||
std::cout << "[步骤 3] THRESH_BINARY_INV(反二值化)..." << std::endl;
|
||||
cv::Mat thresh_binary_inv;
|
||||
cv::threshold(gradient, thresh_binary_inv, 128, 255, cv::THRESH_BINARY_INV);
|
||||
cv::imwrite("thresh_binary_inv.jpg", thresh_binary_inv);
|
||||
std::cout << " -> 已保存为 thresh_binary_inv.jpg" << std::endl;
|
||||
|
||||
// --- 类型 3:THRESH_TRUNC(截断)---
|
||||
// 逻辑:如果 像素值 > 128,则赋值为 128(被截断!);否则保持原值不变
|
||||
// 效果:左边(<128)保持渐变,右边(>128)全部变成 128(一片灰)
|
||||
std::cout << "[步骤 4] THRESH_TRUNC(截断)..." << std::endl;
|
||||
cv::Mat thresh_trunc;
|
||||
cv::threshold(gradient, thresh_trunc, 128, 255, cv::THRESH_TRUNC);
|
||||
cv::imwrite("thresh_trunc.jpg", thresh_trunc);
|
||||
std::cout << " -> 已保存为 thresh_trunc.jpg" << std::endl;
|
||||
|
||||
// --- 类型 4:THRESH_TOZERO(置零)---
|
||||
// 逻辑:如果 像素值 > 128,则保持原值不变;否则赋值为 0(变黑)
|
||||
// 效果:左边(<128)全黑,右边(>128)保持渐变
|
||||
std::cout << "[步骤 5] THRESH_TOZERO(置零)..." << std::endl;
|
||||
cv::Mat thresh_tozero;
|
||||
cv::threshold(gradient, thresh_tozero, 128, 255, cv::THRESH_TOZERO);
|
||||
cv::imwrite("thresh_tozero.jpg", thresh_tozero);
|
||||
std::cout << " -> 已保存为 thresh_tozero.jpg" << std::endl;
|
||||
|
||||
// --- 类型 5:THRESH_TOZERO_INV(反置零)---
|
||||
// 逻辑:如果 像素值 > 128,则赋值为 0(变黑);否则保持原值不变
|
||||
// 效果:左边(<128)保持渐变,右边(>128)全黑
|
||||
std::cout << "[步骤 6] THRESH_TOZERO_INV(反置零)..." << std::endl;
|
||||
cv::Mat thresh_tozero_inv;
|
||||
cv::threshold(gradient, thresh_tozero_inv, 128, 255, cv::THRESH_TOZERO_INV);
|
||||
cv::imwrite("thresh_tozero_inv.jpg", thresh_tozero_inv);
|
||||
std::cout << " -> 已保存为 thresh_tozero_inv.jpg" << std::endl;
|
||||
|
||||
// 总结
|
||||
std::cout << std::endl;
|
||||
std::cout << "===== 实验 5 完成 =====" << std::endl;
|
||||
std::cout << "你学会了:" << std::endl;
|
||||
std::cout << " 1. 阈值化就是把灰度图按某个界限分成两部分" << std::endl;
|
||||
std::cout << " 2. 有 5 种阈值类型,最常用的是 THRESH_BINARY" << std::endl;
|
||||
std::cout << " 3. 阈值化是图像分割的基础操作" << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
9
exp6_morphology/CMakeLists.txt
Normal file
9
exp6_morphology/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
# 实验 6:形态学操作:腐蚀与膨胀 - CMake 构建配置
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(exp6_morphology)
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
find_package(OpenCV REQUIRED)
|
||||
add_executable(exp6 main.cpp)
|
||||
target_link_libraries(exp6 ${OpenCV_LIBS})
|
||||
include_directories(${OpenCV_INCLUDE_DIRS})
|
||||
182
exp6_morphology/main.cpp
Normal file
182
exp6_morphology/main.cpp
Normal file
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* ============================================================
|
||||
* 实验 6:形态学操作 —— 腐蚀 (Erode) 与膨胀 (Dilate)
|
||||
* 知识点:cv::erode、cv::dilate、cv::getStructuringElement
|
||||
* ============================================================
|
||||
*
|
||||
* 【实验目标】
|
||||
* 1. 理解"腐蚀"和"膨胀"对二值图像的作用
|
||||
* 2. 了解"开运算"和"闭运算"的组合效果
|
||||
* 3. 掌握结构元素 (kernel) 的概念
|
||||
*
|
||||
* 【生活化比喻】
|
||||
* 腐蚀 (Erosion):
|
||||
* 想象沙滩上写着一些字,涨潮时海水冲上来,把字的边缘"吃掉"一圈
|
||||
* → 白色区域被"侵蚀",变小了、断开了、甚至消失了
|
||||
* → 作用:消除小白点噪声、断开粘连的物体
|
||||
*
|
||||
* 膨胀 (Dilation):
|
||||
* 想象给一栋房子外围再砌一圈围墙,房子"胖了"
|
||||
* → 白色区域被"扩张",变大了、连在一起了
|
||||
* → 作用:填充小黑洞、连接断裂的物体
|
||||
*
|
||||
* 开运算 (Opening) = 先腐蚀,再膨胀:
|
||||
* 想象洗菜时,先让虫子被水"冲掉"(腐蚀),
|
||||
* 然后蔬菜可能会缩一点,再"泡水"让它恢复原大小(膨胀)
|
||||
* → 去掉小白点噪声,同时保持物体大小基本不变
|
||||
*
|
||||
* 闭运算 (Closing) = 先膨胀,再腐蚀:
|
||||
* 想象一件有破洞的衣服,先"拉伸"布料让洞缩小(膨胀),
|
||||
* 再"熨平"恢复形状(腐蚀)
|
||||
* → 填充物体内部的黑色小洞,同时保持物体大小基本不变
|
||||
*
|
||||
* 结构元素 (Structuring Element / Kernel):
|
||||
* 腐蚀和膨胀不是乱"吃"乱"胀"的,而是用一个模板来做的
|
||||
* 这个模板就是"结构元素",它决定了"腐蚀/膨胀的形状和范围"
|
||||
* - 矩形结构元素 (MORPH_RECT):像盖章一样方方正正
|
||||
* - 椭圆形结构元素 (MORPH_ELLIPSE):圆形范围
|
||||
* - 十字形结构元素 (MORPH_CROSS):只影响上下左右
|
||||
*
|
||||
* 【函数详解】
|
||||
* cv::erode(src, dst, kernel, anchor, iterations)
|
||||
* cv::dilate(src, dst, kernel, anchor, iterations)
|
||||
*
|
||||
* src :输入图像(通常是二值图)
|
||||
* dst :输出图像
|
||||
* kernel :结构元素,用 cv::getStructuringElement() 创建
|
||||
* anchor :锚点位置,默认是核的中心 (-1,-1)
|
||||
* iterations :操作重复次数(相当于执行几次腐蚀/膨胀)
|
||||
*
|
||||
* cv::getStructuringElement(shape, ksize)
|
||||
* shape :MORPH_RECT(矩形)、MORPH_ELLIPSE(椭圆)、MORPH_CROSS(十字)
|
||||
* ksize :核大小,如 cv::Size(5,5)
|
||||
*/
|
||||
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <iostream>
|
||||
|
||||
int main()
|
||||
{
|
||||
// ==========================================
|
||||
// 第一部分:生成一张带噪声的二值测试图
|
||||
// ==========================================
|
||||
std::cout << "[步骤 1] 正在生成二值测试图片..." << std::endl;
|
||||
|
||||
// 创建黑色背景
|
||||
cv::Mat binary(300, 500, CV_8UC1, cv::Scalar(0));
|
||||
|
||||
// 画一个大的白色矩形(作为"主体目标")
|
||||
cv::rectangle(binary, cv::Point(80, 60), cv::Point(420, 240),
|
||||
cv::Scalar(255), -1); // 白色填充矩形
|
||||
|
||||
// 在矩形内部打几个孔(黑色小圆,模拟"目标内部的空洞")
|
||||
cv::circle(binary, cv::Point(180, 150), 20, cv::Scalar(0), -1); // 内部孔洞 1
|
||||
cv::circle(binary, cv::Point(320, 150), 20, cv::Scalar(0), -1); // 内部孔洞 2
|
||||
|
||||
// 在背景中撒一些白色噪点(模拟"背景中的噪声")
|
||||
// 这些小白点不是我们想要的目标,它们应该被腐蚀"消除"
|
||||
for (int i = 0; i < 50; i++)
|
||||
{
|
||||
int x = rand() % 500;
|
||||
int y = rand() % 300;
|
||||
cv::circle(binary, cv::Point(x, y), 3, cv::Scalar(255), -1);
|
||||
}
|
||||
|
||||
cv::imwrite("binary.jpg", binary);
|
||||
std::cout << " -> 原始二值图已保存为 binary.jpg" << std::endl;
|
||||
std::cout << " (白色矩形为主体,内部有小黑洞,背景有白色噪点)" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第二部分:创建结构元素
|
||||
// ==========================================
|
||||
// 结构元素的大小会影响腐蚀/膨胀的效果范围
|
||||
// 核越大,每次操作影响的范围越大
|
||||
// 这里使用 5×5 的矩形核作为演示
|
||||
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(5, 5));
|
||||
// 你也试试换成椭圆形核:
|
||||
// cv::Mat kernel = cv::getStructuringElement(cv::MORPH_ELLIPSE, cv::Size(5,5));
|
||||
|
||||
// ==========================================
|
||||
// 第三部分:腐蚀操作
|
||||
// ==========================================
|
||||
std::cout << "[步骤 2] 执行腐蚀操作 (Erosion)..." << std::endl;
|
||||
|
||||
cv::Mat eroded;
|
||||
cv::erode(binary, eroded, kernel);
|
||||
// 效果预期:
|
||||
// - 白色矩形整体"瘦"了一圈(边缘被腐蚀)
|
||||
// - 背景上的白色小噪点可能被完全消除(因为它们太小了)
|
||||
// - 矩形内部的黑洞变大了(因为黑洞周围的白色被腐蚀了)
|
||||
cv::imwrite("eroded.jpg", eroded);
|
||||
std::cout << " -> 已保存为 eroded.jpg(白色区域变小,噪点可能消失)" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第四部分:膨胀操作
|
||||
// ==========================================
|
||||
std::cout << "[步骤 3] 执行膨胀操作 (Dilation)..." << std::endl;
|
||||
|
||||
cv::Mat dilated;
|
||||
cv::dilate(binary, dilated, kernel);
|
||||
// 效果预期:
|
||||
// - 白色矩形整体"胖"了一圈(边缘被扩张)
|
||||
// - 背景上的白色小噪点也变大了
|
||||
// - 矩形内部的黑洞变小了(因为黑洞外围的白色膨胀进来)
|
||||
cv::imwrite("dilated.jpg", dilated);
|
||||
std::cout << " -> 已保存为 dilated.jpg(白色区域变大,内部黑洞变小)" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第五部分:开运算(先腐蚀再膨胀)
|
||||
// ==========================================
|
||||
std::cout << "[步骤 4] 执行开运算 (Opening = 腐蚀 + 膨胀)..." << std::endl;
|
||||
|
||||
// 开运算没有单独的函数,需要手动组合两个步骤
|
||||
cv::Mat opened;
|
||||
cv::erode(binary, opened, kernel); // 第一步:腐蚀
|
||||
cv::dilate(opened, opened, kernel); // 第二步:在原结果上膨胀
|
||||
// 效果预期:
|
||||
// - 白色的噪点被腐蚀消灭了 → 膨胀也无法复原 → 噪点彻底消失
|
||||
// - 矩形主体的边缘先瘦再胖,大小基本恢复原样
|
||||
// - 但是内部的黑洞变大了(因为腐蚀扩大了黑洞)
|
||||
cv::imwrite("opened.jpg", opened);
|
||||
std::cout << " -> 已保存为 opened.jpg(去掉了噪音,但内部黑洞变大了)" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第六部分:闭运算(先膨胀再腐蚀)
|
||||
// ==========================================
|
||||
std::cout << "[步骤 5] 执行闭运算 (Closing = 膨胀 + 腐蚀)..." << std::endl;
|
||||
|
||||
cv::Mat closed;
|
||||
cv::dilate(binary, closed, kernel); // 第一步:膨胀
|
||||
cv::erode(closed, closed, kernel); // 第二步:在原结果上腐蚀
|
||||
// 效果预期:
|
||||
// - 矩形内部的黑洞被膨胀填上了 → 腐蚀后也不会再出现 → 黑洞消失!
|
||||
// - 背景上的白色噪点膨胀变大 → 无法被腐蚀消除 → 噪点反而更大了
|
||||
// - 矩形主体的边缘先胖再瘦,大小基本恢复原样
|
||||
cv::imwrite("closed.jpg", closed);
|
||||
std::cout << " -> 已保存为 closed.jpg(填充了内部黑洞,但背景噪点变大了)" << std::endl;
|
||||
|
||||
// ==========================================
|
||||
// 第七部分:多次形态学操作
|
||||
// ==========================================
|
||||
std::cout << "[步骤 6] 执行多次腐蚀 (iterations=3)..." << std::endl;
|
||||
|
||||
cv::Mat eroded_multi;
|
||||
// iterations=3 表示连续执行 3 次腐蚀
|
||||
// 等价于手动写三次 cv::erode(..., eroded_multi, kernel)
|
||||
// 每腐蚀一次,白色区域就缩小一圈
|
||||
cv::erode(binary, eroded_multi, kernel, cv::Point(-1, -1), 3);
|
||||
cv::imwrite("eroded_x3.jpg", eroded_multi);
|
||||
std::cout << " -> 已保存为 eroded_x3.jpg(腐蚀 3 次,白色区域大幅缩小)" << std::endl;
|
||||
|
||||
// 总结
|
||||
std::cout << std::endl;
|
||||
std::cout << "===== 实验 6 完成 =====" << std::endl;
|
||||
std::cout << "你学会了:" << std::endl;
|
||||
std::cout << " 1. 腐蚀:白色区域缩小,可以去噪点" << std::endl;
|
||||
std::cout << " 2. 膨胀:白色区域扩大,可以填黑洞" << std::endl;
|
||||
std::cout << " 3. 开运算:去噪点但大体保持目标大小" << std::endl;
|
||||
std::cout << " 4. 闭运算:填空洞但大体保持目标大小" << std::endl;
|
||||
std::cout << " 5. 结构元素决定了操作的形状和范围" << std::endl;
|
||||
|
||||
return 0;
|
||||
}
|
||||
9
exp7_inrange/CMakeLists.txt
Normal file
9
exp7_inrange/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
# 实验 7:颜色提取:inRange - CMake 构建配置
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(exp7_inrange)
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
find_package(OpenCV REQUIRED)
|
||||
add_executable(exp7 main.cpp)
|
||||
target_link_libraries(exp7 ${OpenCV_LIBS})
|
||||
include_directories(${OpenCV_INCLUDE_DIRS})
|
||||
221
exp7_inrange/main.cpp
Normal file
221
exp7_inrange/main.cpp
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* ============================================================
|
||||
* 实验 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;
|
||||
}
|
||||
9
exp8_pipeline/CMakeLists.txt
Normal file
9
exp8_pipeline/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
# 实验 8:综合流水线 - CMake 构建配置
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(exp8_pipeline)
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
find_package(OpenCV REQUIRED)
|
||||
add_executable(exp8 main.cpp)
|
||||
target_link_libraries(exp8 ${OpenCV_LIBS})
|
||||
include_directories(${OpenCV_INCLUDE_DIRS})
|
||||
226
exp8_pipeline/main.cpp
Normal file
226
exp8_pipeline/main.cpp
Normal file
@@ -0,0 +1,226 @@
|
||||
/*
|
||||
* ============================================================
|
||||
* 实验 8:综合流水线 (Pipeline)
|
||||
* 知识点:串联前面 7 个实验的所有技术
|
||||
* ============================================================
|
||||
*
|
||||
* 【实验目标】
|
||||
* 1. 将前面学到的所有技术串联成一个完整流程
|
||||
* 2. 理解图像处理中"预处理 → 提取 → 后处理"的标准套路
|
||||
* 3. 为 RoboMaster 灯条检测打下完整基础
|
||||
*
|
||||
* 【生活化比喻】
|
||||
* 一条完整的图像处理流水线就像工厂的"生产流水线":
|
||||
* - 原材料进厂(读取图像)
|
||||
* - 清洗处理(模糊去噪、颜色转换)
|
||||
* - 核心加工(颜色提取 inRange)
|
||||
* - 精修打磨(形态学操作去掉毛刺)
|
||||
* - 质检包装(边缘检测找轮廓、画框标记)
|
||||
* - 成品出厂(保存结果图片)
|
||||
*
|
||||
* 每一步都依赖前一步的输出,整个流程像一个多米诺骨牌,
|
||||
* 任何一个环节出问题,最终结果都会受影响。
|
||||
*
|
||||
* 【RoboMaster 灯条检测的标准流水线】
|
||||
* 1. 读取摄像头采集的图像
|
||||
* 2. BGR → HSV 颜色空间转换
|
||||
* 3. inRange 提取目标颜色(红/蓝)
|
||||
* 4. 形态学操作(开运算去噪、闭运算填充)
|
||||
* 5. 轮廓检测与筛选(按面积、长宽比、角度过滤)
|
||||
* 6. 画出灯条的位置框
|
||||
*
|
||||
* 本实验完整演示整个流程!
|
||||
*/
|
||||
|
||||
#include <opencv2/opencv.hpp>
|
||||
#include <iostream>
|
||||
|
||||
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));
|
||||
|
||||
// 画两个"红色灯条"(模拟 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); // 稍暗一点的红色
|
||||
|
||||
// 画一些干扰物(场地上可能有其他颜色的物体)
|
||||
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;
|
||||
int y = rand() % 450;
|
||||
// 随机颜色(模拟 CCD/CMOS 噪点)
|
||||
uchar noise_color = static_cast<uchar>(rand() % 128 + 60);
|
||||
cv::circle(scene, cv::Point(x, y), 1,
|
||||
cv::Scalar(noise_color, noise_color, noise_color), -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<std::vector<cv::Point>> contours;
|
||||
// RETR_EXTERNAL: 只检索最外层轮廓
|
||||
// CHAIN_APPROX_SIMPLE: 压缩水平/垂直/对角线,只保留端点
|
||||
cv::findContours(mask_cleaned, contours, cv::RETR_EXTERNAL,
|
||||
cv::CHAIN_APPROX_SIMPLE);
|
||||
|
||||
// 创建结果图:在原图上画检测框
|
||||
cv::Mat result = scene.clone(); // clone() 创建一个完全相同的副本
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user