2703 字
14 分钟
RM装甲板识别 - 04 轮廓检测
2025-10-24
统计加载中...

本阶段核心API清单#

API作用
cv::findContours()查找轮廓
cv::contourArea()计算轮廓面积
cv::minAreaRect()最小旋转矩形
cv::RotatedRect旋转矩形类
cv::drawContours()绘制轮廓
cv::boundingRect()正矩形包围框
cv::fitEllipse()椭圆拟合

核心思路#

  • 经过形态学处理后,二值图已经很干净:

  • 现在要做的是:

  1. 找轮廓 - 把每个白色区域的边界找出来
  2. 拟合矩形 - 用旋转矩形描述每个轮廓
  3. 筛选灯条 - 根据长宽比、面积、角度等特征过滤

1. cv::findContours() - 查找轮廓#

参数详解#

mode - 轮廓检索模式#

mode说明用途
RETR_EXTERNAL只检测最外层轮廓RM推荐,忽略内部孔洞
RETR_LIST检测所有轮廓,不建立层级当需要所有轮廓时
RETR_TREE检测所有并建立完整层级树复杂嵌套结构
RETR_CCOMP两层层级(外轮廓和孔)较少使用

method - 轮廓近似方法#

method说明优缺点
CHAIN_APPROX_NONE保存所有轮廓点精确但占内存
CHAIN_APPROX_SIMPLE压缩轮廓,只保留关键点RM推荐,节省内存

基础用法#

#include <opencv2/opencv.hpp>
#include <vector>
int main() {
cv::Mat img = cv::imread("armor.jpg");
// 获取二值图(假设已实现)
cv::Mat binary = processImage(img);
// 查找轮廓
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binary, contours, cv::RETR_EXTERNAL,
cv::CHAIN_APPROX_SIMPLE);
std::cout << "找到 " << contours.size() << " 个轮廓" << std::endl;
// 遍历所有轮廓
for (size_t i = 0; i < contours.size(); i++) {
std::cout << "轮廓 " << i << " 有 " << contours[i].size()
<< " 个点" << std::endl;
}
return 0;
}

重要注意事项#

注意1: findContours会修改输入图像!#

// 正确:传入副本
cv::Mat binary_copy = binary.clone();
cv::findContours(binary_copy, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// 或者直接在临时对象上调用
cv::findContours(binary.clone(), contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);

注意2: 输入必须是二值图#

// 错误:彩色图不行
cv::Mat img = cv::imread("armor.jpg");
cv::findContours(img, contours, ...); // 结果不对
// 正确:必须是二值图(0或255)
cv::Mat binary;
cv::threshold(gray, binary, 128, 255, cv::THRESH_BINARY);
cv::findContours(binary, contours, ...);

2. 轮廓数据结构理解#

轮廓的本质#

// 轮廓 = 点的集合
std::vector<cv::Point> contour = contours[0];
// 每个点是(x, y)坐标
cv::Point p = contour[0];
std::cout << "第一个点: (" << p.x << ", " << p.y << ")" << std::endl;

轮廓操作示例#

std::vector<std::vector<cv::Point>> contours;
cv::findContours(binary, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
// 轮廓数量
int num = contours.size();
// 第i个轮廓的点数
int pointCount = contours[i].size();
// 访问第i个轮廓的第j个点
cv::Point p = contours[i][j];

3. cv::contourArea() - 计算轮廓面积#

函数原型#

double cv::contourArea(InputArray contour, bool oriented = false);

基础用法#

for (const auto& contour : contours) {
double area = cv::contourArea(contour);
std::cout << "轮廓面积: " << area << " 像素" << std::endl;
}

用于过滤噪点#

std::vector<std::vector<cv::Point>> validContours;
for (const auto& contour : contours) {
double area = cv::contourArea(contour);
// 过滤太小的轮廓(噪点)
if (area < 100) continue;
// 过滤太大的轮廓(可能是整个装甲板或背景)
if (area > 10000) continue;
// 保留合理大小的轮廓
validContours.push_back(contour);
}
std::cout << "过滤后剩余 " << validContours.size() << " 个轮廓" << std::endl;

面积阈值如何选择?#

推荐方法

// 根据图像大小自适应
int imageArea = binary.rows * binary.cols;
double minArea = imageArea * 0.0001; // 0.01%
double maxArea = imageArea * 0.05; // 5%
for (const auto& contour : contours) {
double area = cv::contourArea(contour);
if (area > minArea && area < maxArea) {
// 合理范围
}
}

4. cv::minAreaRect() - 最小旋转矩形#

基础用法#

for (const auto& contour : contours) {
// 拟合最小旋转矩形
cv::RotatedRect rect = cv::minAreaRect(contour);
// 获取矩形信息
cv::Point2f center = rect.center; // 中心点
cv::Size2f size = rect.size; // 尺寸(width, height)
float angle = rect.angle; // 旋转角度(-90到0)
std::cout << "中心: (" << center.x << ", " << center.y << ")" << std::endl;
std::cout << "尺寸: " << size.width << " × " << size.height << std::endl;
std::cout << "角度: " << angle << "°" << std::endl;
}

5. cv::RotatedRect 类详解#

核心属性#

cv::RotatedRect rect = cv::minAreaRect(contour);
// 中心点
cv::Point2f center = rect.center; // 矩形中心的(x,y)坐标
// 尺寸
cv::Size2f size = rect.size; // width × height
float width = size.width;
float height = size.height;
// 旋转角度
float angle = rect.angle; // 范围: -90° ~ 0°

angle的陷阱#

OpenCV的angle定义很反直觉!

// angle的含义:
// 从水平方向逆时针旋转到矩形长边的角度
// 范围:-90° 到 0°
// 例子:
angle = -90° → 矩形垂直(竖着)
angle = -45° → 矩形倾斜45度
angle = 0° → 矩形水平(横着)

确保height是长边#

cv::RotatedRect rect = cv::minAreaRect(contour);
float width = rect.size.width;
float height = rect.size.height;
// OpenCV不保证height > width!
// 需要手动调整
if (width > height) {
std::swap(width, height);
// 如果需要,也可以调整angle
}
// 现在 height 一定是长边
float aspectRatio = height / width; // 长宽比

获取四个顶点坐标#

cv::RotatedRect rect = cv::minAreaRect(contour);
// 获取四个顶点
cv::Point2f vertices[4];
rect.points(vertices);
// vertices[0], vertices[1], vertices[2], vertices[3]
// 按逆时针顺序排列
// 绘制旋转矩形
for (int i = 0; i < 4; i++) {
cv::line(img, vertices[i], vertices[(i+1)%4],
cv::Scalar(0, 255, 0), 2);
}

6. 灯条特征筛选(核心)#

灯条的典型特征#

特征合理范围说明
长宽比2.0 ~ 10.0灯条是细长的
面积100 ~ 5000像素根据分辨率调整
角度接近垂直通常在±30°内
填充度> 0.5轮廓面积/外接矩形面积

完整筛选代码#

#include <opencv2/opencv.hpp>
#include <vector>
struct LightBar {
cv::RotatedRect rect;
double area;
float aspectRatio;
float angle;
};
std::vector<LightBar> detectLightBars(const cv::Mat& binary) {
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binary.clone(), contours,
cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
std::vector<LightBar> lightBars;
for (const auto& contour : contours) {
// 1. 面积筛选
double area = cv::contourArea(contour);
if (area < 100 || area > 5000) continue;
// 2. 拟合旋转矩形
cv::RotatedRect rect = cv::minAreaRect(contour);
// 3. 确保height是长边
float width = rect.size.width;
float height = rect.size.height;
if (width > height) {
std::swap(width, height);
}
// 4. 长宽比筛选
float aspectRatio = height / width;
if (aspectRatio < 2.0 || aspectRatio > 10.0) continue;
// 5. 角度筛选(可选,根据实际情况)
// 灯条应该接近垂直
float angle = std::abs(rect.angle);
if (angle > 30 && angle < 60) continue; // 太斜的排除
// 6. 填充度筛选(可选)
double rectArea = width * height;
double fillRatio = area / rectArea;
if (fillRatio < 0.5) continue; // 太空的不是灯条
// 通过所有筛选,保存
LightBar bar;
bar.rect = rect;
bar.area = area;
bar.aspectRatio = aspectRatio;
bar.angle = angle;
lightBars.push_back(bar);
}
std::cout << "找到 " << lightBars.size() << " 个灯条" << std::endl;
return lightBars;
}

7. cv::drawContours() - 绘制轮廓(调试)#

基础用法#

cv::Mat display = img.clone();
// 绘制所有轮廓
cv::drawContours(display, contours, -1, cv::Scalar(0, 255, 0), 2);
// -1 表示绘制所有轮廓
// 绘制第i个轮廓
cv::drawContours(display, contours, i, cv::Scalar(255, 0, 0), 2);
// 填充轮廓
cv::drawContours(display, contours, i, cv::Scalar(0, 0, 255), -1);
// thickness=-1 表示填充

彩色调试可视化#

void visualizeContours(const cv::Mat& img,
const std::vector<std::vector<cv::Point>>& contours) {
cv::Mat display = img.clone();
// 为每个轮廓随机颜色
cv::RNG rng(12345);
for (size_t i = 0; i < contours.size(); i++) {
cv::Scalar color(rng.uniform(0, 256),
rng.uniform(0, 256),
rng.uniform(0, 256));
// 绘制轮廓
cv::drawContours(display, contours, i, color, 2);
// 标注序号
cv::Moments m = cv::moments(contours[i]);
cv::Point2f center(m.m10/m.m00, m.m01/m.m00);
cv::putText(display, std::to_string(i), center,
cv::FONT_HERSHEY_SIMPLEX, 0.5, color, 2);
}
cv::imshow("轮廓可视化", display);
}

8. 绘制旋转矩形#

标准方法#

void drawRotatedRect(cv::Mat& img, const cv::RotatedRect& rect,
const cv::Scalar& color, int thickness = 2) {
cv::Point2f vertices[4];
rect.points(vertices);
for (int i = 0; i < 4; i++) {
cv::line(img, vertices[i], vertices[(i+1)%4], color, thickness);
}
}
// 使用
for (const auto& bar : lightBars) {
drawRotatedRect(display, bar.rect, cv::Scalar(0, 255, 0), 2);
}

增强版(带中心点和文字)#

void drawLightBar(cv::Mat& img, const LightBar& bar, int id) {
// 绘制旋转矩形
cv::Point2f vertices[4];
bar.rect.points(vertices);
for (int i = 0; i < 4; i++) {
cv::line(img, vertices[i], vertices[(i+1)%4],
cv::Scalar(0, 255, 0), 2);
}
// 绘制中心点
cv::circle(img, bar.rect.center, 5, cv::Scalar(0, 0, 255), -1);
// 标注信息
std::string text = "ID:" + std::to_string(id) +
" R:" + std::to_string((int)bar.aspectRatio);
cv::putText(img, text,
cv::Point(bar.rect.center.x + 10, bar.rect.center.y),
cv::FONT_HERSHEY_SIMPLEX, 0.5,
cv::Scalar(255, 255, 255), 2);
}

10. cv::fitEllipse() - 椭圆拟合(能量机关用)#

函数原型#

RotatedRect cv::fitEllipse(InputArray points);

基础用法#

for (const auto& contour : contours) {
// 至少需要5个点
if (contour.size() < 5) continue;
// 拟合椭圆
cv::RotatedRect ellipse = cv::fitEllipse(contour);
// 绘制椭圆
cv::ellipse(img, ellipse, cv::Scalar(0, 255, 255), 2);
}

能量机关扇叶识别#

// 能量机关的R标是椭圆形
bool isEnergyRune(const std::vector<cv::Point>& contour) {
if (contour.size() < 5) return false;
cv::RotatedRect ellipse = cv::fitEllipse(contour);
// 椭圆的长短轴比应该接近1(近似圆形)
float ratio = ellipse.size.width / ellipse.size.height;
if (ratio < 0.8 || ratio > 1.2) return false;
// 面积合理
double area = CV_PI * ellipse.size.width/2 * ellipse.size.height/2;
if (area < 500 || area > 3000) return false;
return true;
}

完整实战代码#

完整的灯条检测系统#

#include <opencv2/opencv.hpp>
#include <vector>
#include <iostream>
// 灯条结构体
struct LightBar {
cv::RotatedRect rect;
double area;
float aspectRatio;
LightBar(const cv::RotatedRect& r, double a, float ar)
: rect(r), area(a), aspectRatio(ar) {}
};
// 灯条检测函数
std::vector<LightBar> detectLightBars(const cv::Mat& binary,
const cv::Mat& display) {
// 1. 查找轮廓
std::vector<std::vector<cv::Point>> contours;
cv::findContours(binary.clone(), contours,
cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
std::cout << "总轮廓数: " << contours.size() << std::endl;
std::vector<LightBar> lightBars;
// 2. 遍历筛选
for (size_t i = 0; i < contours.size(); i++) {
const auto& contour = contours[i];
// 面积筛选
double area = cv::contourArea(contour);
if (area < 100) {
std::cout << "轮廓" << i << ": 面积=" << area
<< " 太小" << std::endl;
continue;
}
if (area > 5000) {
std::cout << "轮廓" << i << ": 面积=" << area
<< " 太大" << std::endl;
continue;
}
// 拟合旋转矩形
cv::RotatedRect rect = cv::minAreaRect(contour);
// 确保height是长边
float width = rect.size.width;
float height = rect.size.height;
if (width > height) {
std::swap(width, height);
}
// 长宽比筛选
float aspectRatio = height / width;
if (aspectRatio < 2.0) {
std::cout << "轮廓" << i << ": 长宽比=" << aspectRatio
<< " 太矮" << std::endl;
continue;
}
if (aspectRatio > 10.0) {
std::cout << "轮廓" << i << ": 长宽比=" << aspectRatio
<< "太细" << std::endl;
continue;
}
// 填充度筛选
double rectArea = width * height;
double fillRatio = area / rectArea;
if (fillRatio < 0.5) {
std::cout << "轮廓" << i << ": 填充度=" << fillRatio
<< " 太空" << std::endl;
continue;
}
// 通过筛选!
std::cout << "轮廓" << i << ": 灯条候选" << std::endl;
std::cout << " 面积=" << area << ", 长宽比=" << aspectRatio
<< ", 填充度=" << fillRatio << std::endl;
lightBars.emplace_back(rect, area, aspectRatio);
// 绘制(调试用)
if (!display.empty()) {
cv::Point2f vertices[4];
rect.points(vertices);
for (int j = 0; j < 4; j++) {
cv::line(display, vertices[j], vertices[(j+1)%4],
cv::Scalar(0, 255, 0), 2);
}
cv::circle(display, rect.center, 5, cv::Scalar(0, 0, 255), -1);
}
}
std::cout << "\n最终找到 " << lightBars.size() << " 个灯条" << std::endl;
return lightBars;
}
int main() {
// 读取图像
cv::Mat img = cv::imread("armor.jpg");
if (img.empty()) {
std::cerr << "无法读取图像" << std::endl;
return -1;
}
// 颜色提取(假设已实现)
cv::Mat binary = extractRedColor(img);
// 形态学处理(假设已实现)
binary = morphologyProcess(binary);
// 灯条检测
cv::Mat display = img.clone();
std::vector<LightBar> lightBars = detectLightBars(binary, display);
// 显示结果
cv::imshow("原图", img);
cv::imshow("二值图", binary);
cv::imshow("检测结果", display);
cv::waitKey(0);
return 0;
}

常见问题排查#

问题1: 找不到轮廓#

// 检查清单:
// 1. 二值图是否正确?
cv::imshow("binary", binary); // 应该是黑白的
// 2. 前景是白色吗?
// findContours找的是白色区域!如果反了需要反转
cv::bitwise_not(binary, binary);
// 3. 图像是单通道吗?
std::cout << "通道数: " << binary.channels() << std::endl; // 应该是1

问题2: 找到太多轮廓(都是噪点)#

// 解决方案:
// 1. 加强形态学处理
cv::morphologyEx(binary, binary, cv::MORPH_OPEN, kernel);
// 2. 提高面积阈值
if (area < 200) continue; // 增大
// 3. 添加填充度筛选
double fillRatio = area / (width * height);
if (fillRatio < 0.6) continue;

问题3: 灯条被筛掉了#

// 调试步骤:
// 1. 打印每个轮廓的参数
for (size_t i = 0; i < contours.size(); i++) {
double area = cv::contourArea(contours[i]);
cv::RotatedRect rect = cv::minAreaRect(contours[i]);
float ratio = rect.size.height / rect.size.width;
std::cout << i << ": area=" << area << ", ratio=" << ratio << std::endl;
}
// 2. 放宽筛选条件
if (aspectRatio < 1.5 || aspectRatio > 15.0) continue; // 放宽范围
// 3. 可视化被筛掉的轮廓
cv::drawContours(rejected_img, contours, i, cv::Scalar(0,0,255), 2);

问题4: 旋转矩形角度不对#

// OpenCV的angle有时不符合预期
// 解决方案:自己计算角度
cv::Point2f vertices[4];
rect.points(vertices);
// 计算长边的角度
cv::Point2f edge = vertices[1] - vertices[0];
float angle = std::atan2(edge.y, edge.x) * 180 / CV_PI;

RM装甲板识别 - 04 轮廓检测
https://www.nanye404.top/posts/rm_opencv_04/
作者
南叶酱
发布于
2025-10-24
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
示例歌曲
示例艺术家
封面
示例歌曲
示例艺术家
0:00 / 0:00