OpenCV C++ 二值图像分析:从连通组件到轮廓匹配
二值图像分析是计算机视觉的重要基础,通过对二值化后的图像进行连通组件标记、轮廓提取与特征分析,可实现目标计数、形状识别、尺寸测量等核心任务。本章将系统讲解二值图像分析的关键技术,从基础算法到实战应用,构建完整的技术体系。
一、连通组件标记算法原理
连通组件标记(Connected Component Labeling)是二值图像分析的基础,它能识别图像中相互连通的像素区域并赋予唯一标识,实现目标分离。
1.1 连通性定义
在二值图像中,连通性描述像素间的连接关系:
- 4 连通:一个像素只与其上、下、左、右四个方向的像素连接
- 8 连通:一个像素与其上、下、左、右及四个对角线方向的像素连接
4连通邻居 8连通邻居N N NE E
W C E NW C SES W SW S
1.2 两遍扫描算法
两遍扫描算法是最常用的连通组件标记方法,通过两次图像扫描完成标记:
// 两遍扫描连通组件标记算法(4连通)
Mat twoPassLabeling(const Mat& binary) {CV_Assert(binary.type() == CV_8UC1);int rows = binary.rows;int cols = binary.cols;Mat labels = Mat::zeros(rows, cols, CV_32SC1); // 标记结果int currentLabel = 0;vector<int> parent; // 用于处理等价标签// 第一遍扫描for (int i = 0; i < rows; ++i) {for (int j = 0; j < cols; ++j) {if (binary.at<uchar>(i, j) == 255 && labels.at<int>(i, j) == 0) {currentLabel++;parent.push_back(currentLabel); // 初始父标签为自身// 检查左邻和上邻(4连通)vector<int> neighborLabels;// 左邻if (j > 0 && labels.at<int>(i, j-1) > 0) {neighborLabels.push_back(labels.at<int>(i, j-1));}// 上邻if (i > 0 && labels.at<int>(i-1, j) > 0) {neighborLabels.push_back(labels.at<int>(i-1, j));}if (neighborLabels.empty()) {// 无邻居,分配新标签labels.at<int>(i, j) = currentLabel;} else {// 有邻居,取最小标签int minLabel = *min_element(neighborLabels.begin(), neighborLabels.end());labels.at<int>(i, j) = minLabel;// 记录等价标签for (int label : neighborLabels) {// 路径压缩的并查集操作while (parent[label-1] != label) {parent[label-1] = parent[parent[label-1]-1];label = parent[label-1];}if (label != minLabel) {parent[label-1] = minLabel;}}}}}}// 第二遍扫描:处理等价标签for (int i = 0; i < rows; ++i) {for (int j = 0; j < cols; ++j) {if (labels.at<int>(i, j) > 0) {int label = labels.at<int>(i, j);// 找到根标签while (parent[label-1] != label) {label = parent[label-1];}labels.at<int>(i, j) = label;}}}return labels;
}
算法步骤解析:
- 第一遍扫描:逐像素遍历图像,对每个前景像素(255)检查其左邻和上邻
- 标签分配:根据邻居标签情况分配新标签或复用已有标签,记录等价标签对
- 等价处理:使用并查集(Union-Find)数据结构管理等价标签
- 第二遍扫描:将所有等价标签替换为其根标签,确保同一组件具有唯一标识
1.3 种子填充算法
种子填充算法通过从种子像素开始,递归或迭代地标记所有连通像素:
// 种子填充算法(8连通)
void floodFillLabeling(const Mat& binary, Mat& labels, int i, int j, int label) {int rows = binary.rows;int cols = binary.cols;// 边界检查if (i < 0 || i >= rows || j < 0 || j >= cols) return;// 若为前景且未标记,则标记并处理邻居if (binary.at<uchar>(i, j) == 255 && labels.at<int>(i, j) == 0) {labels.at<int>(i, j) = label;// 8连通邻居floodFillLabeling(binary, labels, i-1, j-1, label); // 左上floodFillLabeling(binary, labels, i-1, j, label); // 上floodFillLabeling(binary, labels, i-1, j+1, label); // 右上floodFillLabeling(binary, labels, i, j-1, label); // 左floodFillLabeling(binary, labels, i, j+1, label); // 右floodFillLabeling(binary, labels, i+1, j-1, label); // 左下floodFillLabeling(binary, labels, i+1, j, label); // 下floodFillLabeling(binary, labels, i+1, j+1, label); // 右下}
}// 种子填充算法包装函数
Mat floodFillLabeling(const Mat& binary) {CV_Assert(binary.type() == CV_8UC1);int rows = binary.rows;int cols = binary.cols;Mat labels = Mat::zeros(rows, cols, CV_32SC1);int currentLabel = 0;for (int i = 0; i < rows; ++i) {for (int j = 0; j < cols; ++j) {if (binary.at<uchar>(i, j) == 255 && labels.at<int>(i, j) == 0) {currentLabel++;floodFillLabeling(binary, labels, i, j, currentLabel);}}}return labels;
}
两种算法对比:
- 两遍扫描:内存效率高,适合处理大图像,但实现较复杂
- 种子填充:实现简单直观,但递归版本可能栈溢出,迭代版本需队列 / 栈存储像素
二、连通组件标记算法应用
连通组件标记为后续分析提供基础,可实现目标计数、筛选与特性分析。
2.1 组件计数与可视化
// 连通组件可视化(为每个组件分配不同颜色)
Mat visualizeComponents(const Mat& labels) {// 生成随机颜色表RNG rng(12345);int maxLabel = 0;minMaxLoc(labels, 0, &maxLabel);vector<Vec3b> colors(maxLabel + 1);colors[0] = Vec3b(0, 0, 0); // 背景为黑色for (int i = 1; i <= maxLabel; ++i) {colors[i] = Vec3b(rng.uniform(0, 256), rng.uniform(0, 256), rng.uniform(0, 256));}// 为每个标签分配颜色Mat result(labels.size(), CV_8UC3);for (int i = 0; i < labels.rows; ++i) {for (int j = 0; j < labels.cols; ++j) {int label = labels.at<int>(i, j);result.at<Vec3b>(i, j) = colors[label];}}return result;
}// 连通组件分析示例
int main() {Mat img = imread("objects.jpg", IMREAD_GRAYSCALE);if (img.empty()) {cout << "图像加载失败!" << endl;return -1;}// 二值化Mat binary;threshold(img, binary, 0, 255, THRESH_BINARY_INV | THRESH_OTSU);// 连通组件标记Mat labels = twoPassLabeling(binary); // 或使用floodFillLabeling(binary)// 计算组件数量int maxLabel;minMaxLoc(labels, 0, &maxLabel);cout << "检测到 " << maxLabel << " 个连通组件" << endl;// 可视化Mat visualization = visualizeComponents(labels);imshow("原图", i