前言
很多简要的例子都是cv::Mat image = cv::imread(imagePath);之后,直接使用image来处理图像。单一的场景体现不出冲突性,容易让人忽略image是一种公共/共享的资源变量。当存在修改image的场景时就需要注意上下文是否存在冲突了。以下用一个例子进行讲解。
示例功能
对一张图片描绘出20x20的网格(白色)、查找轮廓发现的全部轮廓(绿色),以及最小外接(物体)矩形(红色)。这是一个比较简易的调试物体轮廓的功能,以下是效果图

通用模块说明
(1) 对一张图片描绘出20x20的网格(白色)
要点:
-
计算网格步长:获取每个网格的宽和高
-
画网格:通过循环画行和列的直线cv::line(),组成网格
// 绘制网格函数并返回排除区域 - 添加绘制开关
void drawGrid(Mat& image, int gridSize = 36, bool drawGridLines = true) {
int rows = image.rows;
int cols = image.cols;// 计算网格步长 int gridWidth = cols / gridSize; int gridHeight = rows / gridSize; // 只在需要时绘制网格线 if (drawGridLines) { // 使用较粗的线条,以便在压缩后仍可见 int lineThickness = 3; // 绘制网格线 for (int i = 0; i <= gridSize; i++) { // 垂直线 int x = i * gridWidth; line(image, Point(x, 0), Point(x, rows), Scalar(255, 255, 255), lineThickness); // 水平线 int y = i * gridHeight; line(image, Point(0, y), Point(cols, y), Scalar(255, 255, 255), lineThickness); } }}
(2)查找轮廓发现的全部轮廓(绿色)
比较典型的流程:灰色通道->高斯模糊去噪->边缘检测->形态学操作->操作轮廓
void getallContours(const cv::Mat& inputImage,std::vector<std::vector<cv::Point>>& contours){
cv::Mat gray, blurred,edges;
cv::cvtColor(inputImage, gray, cv::COLOR_BGR2GRAY);
GaussianBlur(gray, blurred, Size(9, 9), 3.0);
Canny(blurred, edges, 50, 150);
// 增强形态学操作,更好地连接边缘
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(7, 7));
cv::morphologyEx(edges, edges, cv::MORPH_CLOSE, kernel);
cv::dilate(edges, edges, kernel); // 添加膨胀操作
// 查找轮廓
cv::findContours(edges, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
std::cout << "find " << contours.size() << " Contours" << std::endl;
}
(3)最小外接(物体)矩形(红色)
要点:找最大轮廓->获取最小外接矩形->获取矩形坐标->绘制直线,顶点
void makerectContours(const std::vector<std::vector<cv::Point>>& contours,cv::Mat& inputImage){
//3.找到面积最大的轮廓
auto maxContour = *std::max_element(contours.begin(), contours.end(),
[](const std::vector<cv::Point>& a, const std::vector<cv::Point>& b) {
return cv::contourArea(a) < cv::contourArea(b);
});
// 获取最小外接矩形
cv::RotatedRect rotatedRect = cv::minAreaRect(maxContour);
cv::Point2f vertices[4];
rotatedRect.points(vertices);
// 转换为整数点坐标
std::vector<cv::Point> rectPoints;
for (int i = 0; i < 4; i++) {
rectPoints.push_back(cv::Point(
static_cast<int>(vertices[i].x),
static_cast<int>(vertices[i].y)
));
}
if (rectPoints.empty()) return ;
// 用红色绘制矩形轮廓
for (int i = 0; i < 4; i++) {
std::cout << "top" << i+1 << ": (" << rectPoints[i].x << ", " << rectPoints[i].y << ")" << std::endl;
cv::line(inputImage, rectPoints[i], rectPoints[(i+1)%4], cv::Scalar(0, 0, 255), 3);//画矩形线
cv::circle(inputImage, rectPoints[i], 8, cv::Scalar(0, 255, 255), -1);//顶点画一个原点
}
// 计算并显示面积
double area = cv::contourArea(rectPoints);
std::cout << "area: " << area << std::endl;
}
测试模块说明
1、不使用clone()创建副本的样例
因为对图片描绘网格,是对图片资源变量的改变,3个模块中(1)画网格修改了图片资源变量,(2)和(3)只是坐标集合的处理,不涉及图片资源变量修改。所以,如果不使用clone()创建副本,就必须把模块(2)放在模块(1)之前,才能保证获取到的轮廓坐标集合是原始的,代码如下
void testContour1(cv::Mat& inputImage) {
//1. 查找所有轮廓并用绿色绘制
std::vector<std::vector<cv::Point>> contours;
getallContours(inputImage,contours);
//2.在原始图像上绘制gridsizeXgridsize网格并获取排除区域
int gridsize=20;
bool drawGridLines = true;// 控制是否绘制网格线
drawGrid(inputImage, gridsize, drawGridLines);
// 用绿色绘制所有轮廓
cv::drawContours(inputImage, contours, -1, cv::Scalar(0, 255, 0), 2);
makerectContours(contours,inputImage);
}
2、使用clone()创建副本的样例
先获取一个副本cv::Mat getImagecontours = inputImage.clone();如果先画图再操作轮廓,也不影响返回的轮廓集合,代码如下
void testContour2(cv::Mat& inputImage) {
cv::Mat getImagecontours = inputImage.clone();
//1.在原始图像上绘制gridsizeXgridsize网格并获取排除区域
int gridsize=20;
bool drawGridLines = true;// 控制是否绘制网格线
drawGrid(inputImage, gridsize, drawGridLines);
//2. 查找所有轮廓并用绿色绘制
std::vector<std::vector<cv::Point>> contours;
getallContours(getImagecontours,contours);
// 用绿色绘制所有轮廓
cv::drawContours(inputImage, contours, -1, cv::Scalar(0, 255, 0), 2);
makerectContours(contours,inputImage);
}
如果以上代码不创建副本,使用与画网格同一个的图片资源变量时,此时返回的轮廓就变化了,如下图红色边框占满了整张图片

完整代码
#include <opencv2/opencv.hpp>
#include <iostream>
#include <vector>
#include <string>
using namespace cv;
using namespace std;
// 显示函数 - 保持20%压缩比例
void showimg(const string& name, const Mat& curImage) {
Mat resizedImage;
double scale = 0.2; // 固定压缩到20%
resize(curImage, resizedImage, Size(), scale, scale);
imshow(name, resizedImage);
}
// 绘制网格函数并返回排除区域 - 添加绘制开关
void drawGrid(Mat& image, int gridSize = 36, bool drawGridLines = true) {
int rows = image.rows;
int cols = image.cols;
// 计算网格步长
int gridWidth = cols / gridSize;
int gridHeight = rows / gridSize;
// 只在需要时绘制网格线
if (drawGridLines) {
// 使用较粗的线条,以便在压缩后仍可见
int lineThickness = 3;
// 绘制网格线
for (int i = 0; i <= gridSize; i++) {
// 垂直线
int x = i * gridWidth;
line(image, Point(x, 0), Point(x, rows), Scalar(255, 255, 255), lineThickness);
// 水平线
int y = i * gridHeight;
line(image, Point(0, y), Point(cols, y), Scalar(255, 255, 255), lineThickness);
}
}
}
void getallContours(const cv::Mat& inputImage,std::vector<std::vector<cv::Point>>& contours){
cv::Mat gray, blurred,edges;
cv::cvtColor(inputImage, gray, cv::COLOR_BGR2GRAY);
GaussianBlur(gray, blurred, Size(9, 9), 3.0);
Canny(blurred, edges, 50, 150);
// 增强形态学操作,更好地连接边缘
cv::Mat kernel = cv::getStructuringElement(cv::MORPH_RECT, cv::Size(7, 7));
cv::morphologyEx(edges, edges, cv::MORPH_CLOSE, kernel);
cv::dilate(edges, edges, kernel); // 添加膨胀操作
// 查找轮廓
cv::findContours(edges, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE);
std::cout << "find " << contours.size() << " Contours" << std::endl;
}
void makerectContours(const std::vector<std::vector<cv::Point>>& contours,cv::Mat& inputImage){
//3.找到面积最大的轮廓
auto maxContour = *std::max_element(contours.begin(), contours.end(),
[](const std::vector<cv::Point>& a, const std::vector<cv::Point>& b) {
return cv::contourArea(a) < cv::contourArea(b);
});
// 获取最小外接矩形
cv::RotatedRect rotatedRect = cv::minAreaRect(maxContour);
cv::Point2f vertices[4];
rotatedRect.points(vertices);
// 转换为整数点坐标
std::vector<cv::Point> rectPoints;
for (int i = 0; i < 4; i++) {
rectPoints.push_back(cv::Point(
static_cast<int>(vertices[i].x),
static_cast<int>(vertices[i].y)
));
}
if (rectPoints.empty()) return ;
// 用红色绘制矩形轮廓
for (int i = 0; i < 4; i++) {
std::cout << "top" << i+1 << ": (" << rectPoints[i].x << ", " << rectPoints[i].y << ")" << std::endl;
cv::line(inputImage, rectPoints[i], rectPoints[(i+1)%4], cv::Scalar(0, 0, 255), 3);//画矩形线
cv::circle(inputImage, rectPoints[i], 8, cv::Scalar(0, 255, 255), -1);//顶点画一个原点
}
// 计算并显示面积
double area = cv::contourArea(rectPoints);
std::cout << "area: " << area << std::endl;
}
void testContour1(cv::Mat& inputImage) {
//1. 查找所有轮廓并用绿色绘制
std::vector<std::vector<cv::Point>> contours;
getallContours(inputImage,contours);
//2.在原始图像上绘制gridsizeXgridsize网格并获取排除区域
int gridsize=20;
bool drawGridLines = true;// 控制是否绘制网格线
drawGrid(inputImage, gridsize, drawGridLines);
// 用绿色绘制所有轮廓
cv::drawContours(inputImage, contours, -1, cv::Scalar(0, 255, 0), 2);
makerectContours(contours,inputImage);
}
void testContour2(cv::Mat& inputImage) {
cv::Mat getImagecontours = inputImage.clone();
//1.在原始图像上绘制gridsizeXgridsize网格并获取排除区域
int gridsize=20;
bool drawGridLines = true;// 控制是否绘制网格线
drawGrid(inputImage, gridsize, drawGridLines);
//2. 查找所有轮廓并用绿色绘制
std::vector<std::vector<cv::Point>> contours;
getallContours(inputImage,contours);
// 用绿色绘制所有轮廓
cv::drawContours(inputImage, contours, -1, cv::Scalar(0, 255, 0), 2);
makerectContours(contours,inputImage);
}
int main(int argc, char* argv[]) {
std::string imagePath = "test.png";
// 读取图片
cv::Mat image = cv::imread(imagePath);
if (image.empty()) {
std::cout << "no can open: " << imagePath << std::endl;
return -1;
}
std::cout << "size: " << image.cols << "x" << image.rows << std::endl;
// 创建副本用于显示
cv::Mat result = image.clone();
testContour2(result);
// 显示结果
showimg("result", result);
cv::waitKey(0);
return 0;
}
篇尾
同一个事件流程中遇到绘图需求场景时,需要关注是哪个步骤修改图片资源变量,通过.clone创建副本的代码健壮性会更好。另外多线程读取公共图片资源时,也应该使用.clone创建副本,避免影响其他线程读取不到原始图片。