图像处理(五)——连通域

连通区域(Connected Component)一般是指图像中具有相同像素值且位置相邻的前景像素点组成的图像区域(Region,Blob)。连通区域分析(Connected Component Analysis,Connected Component Labeling)是指将图像中的各个连通区域找出并标记。
连通区域分析是一种在CVPR和图像分析处理的众多应用领域中较为常用和基本的方法。例如:OCR识别中字符分割提取(车牌识别、文本识别、字幕识别等)、视觉跟踪中的运动前景目标分割与提取(行人入侵检测、遗留物体检测、基于视觉的车辆检测与跟踪等)、医学图像处理(感兴趣目标区域提取)、等等。也就是说,在需要将前景目标提取出来以便后续进行处理的应用场景中都能够用到连通区域分析方法,通常连通区域分析处理的对象是一张二值化后的图像。

而这次我要做的是实现图像的快速连通域算法,可以提取出图像中的连通域,并将不同连通域用不同颜色表示。

寻找图像中的连通域的算法有两个,一个是Two-Pass方法

Two-Pass算法的简单步骤:

(1)第一次扫描:
访问当前像素B(x,y),如果B(x,y) == 1:

a、如果B(x,y)的领域中像素值都为0,则赋予B(x,y)一个新的label:
label += 1, B(x,y) = label;

b、如果B(x,y)的领域中有像素值 > 1的像素Neighbors:
1)将Neighbors中的最小值赋予给B(x,y):
B(x,y) = min{Neighbors}

2)记录Neighbors中各个值(label)之间的相等关系,即这些值(label)同属同一个连通区域;

labelSet[i] = { label_m, …, label_n },labelSet[i]中的所有label都属于同一个连通区域

图示为:
在这里插入图片描述

// 1. 第一次遍历

	_lableImg.release();
	_binImg.convertTo(_lableImg, CV_32SC1);

	int label = 1;  // start by 2
	std::vector<int> labelSet;
	labelSet.push_back(0);   // background: 0
	labelSet.push_back(1);   // foreground: 1

	int rows = _binImg.rows - 1;
	int cols = _binImg.cols - 1;
	for (int i = 1; i < rows; i++)
	{
		int* data_preRow = _lableImg.ptr<int>(i - 1);
		int* data_curRow = _lableImg.ptr<int>(i);
		for (int j = 1; j < cols; j++)
		{
			if (data_curRow[j] == 1)
			{
				std::vector<int> neighborLabels;
				neighborLabels.reserve(2);
				int leftPixel = data_curRow[j - 1];
				int upPixel = data_preRow[j];
				if (leftPixel > 1)
				{
					neighborLabels.push_back(leftPixel);
				}
				if (upPixel > 1)
				{
					neighborLabels.push_back(upPixel);
				}

				if (neighborLabels.empty())
				{
					labelSet.push_back(++label);  // assign to a new label
					data_curRow[j] = label;
					labelSet[label] = label;
				}
				else
				{
					std::sort(neighborLabels.begin(), neighborLabels.end());
					int smallestLabel = neighborLabels[0];
					data_curRow[j] = smallestLabel;

					// save equivalence
					for (size_t k = 1; k < neighborLabels.size(); k++)
					{
						int tempLabel = neighborLabels[k];
						int& oldSmallestLabel = labelSet[tempLabel];
						if (oldSmallestLabel > smallestLabel)
						{
							labelSet[oldSmallestLabel] = smallestLabel;
							oldSmallestLabel = smallestLabel;
						}
						else if (oldSmallestLabel < smallestLabel)
						{
							labelSet[smallestLabel] = oldSmallestLabel;
						}
					}
				}
			}
		}
	}

(2)第二次扫描:
访问当前像素B(x,y),如果B(x,y) > 1:

a、找到与label = B(x,y)同属相等关系的一个最小label值,赋予给B(x,y);
完成扫描后,图像中具有相同label值的像素就组成了同一个连通区域。

图示为:
在这里插入图片描述

// 2. 第二遍扫描
	for (int i = 0; i < rows; i++)
	{
		int* data = _lableImg.ptr<int>(i);
		for (int j = 0; j < cols; j++)
		{
			int& pixelLabel = data[j];
			pixelLabel = labelSet[pixelLabel];
		}
	}
}

另一个方法就是Seed-Filling方法

种子填充法的连通区域分析方法:

(1)扫描图像,直到当前像素点B(x,y) == 1:

a、将B(x,y)作为种子(像素位置),并赋予其一个label,然后将该种子相邻的所有前景像素都压入栈中;
在这里插入图片描述
b、弹出栈顶像素,赋予其相同的label,然后再将与该栈顶像素相邻的所有前景像素都压入栈中;
在这里插入图片描述

// 推及到四个邻居
					if (_lableImg.at<int>(curX, curY - 1) == 1)
					{// 左边的像素
						neighborPixels.push(std::pair<int, int>(curX, curY - 1));
					}
					if (_lableImg.at<int>(curX, curY + 1) == 1)
					{// 右边的像素
						neighborPixels.push(std::pair<int, int>(curX, curY + 1));
					}
					if (_lableImg.at<int>(curX - 1, curY) == 1)
					{// 上面的像素
						neighborPixels.push(std::pair<int, int>(curX - 1, curY));
					}
					if (_lableImg.at<int>(curX + 1, curY) == 1)
					{// 下面的像素
						neighborPixels.push(std::pair<int, int>(curX + 1, curY));
					}

c、重复b步骤,直到栈为空;
此时,便找到了图像B中的一个连通区域,该区域内的像素值被标记为label;
在这里插入图片描述
(2)重复第(1)步,直到扫描结束;
在这里插入图片描述
扫描结束后,就可以得到图像B中所有的连通区域;

而我选择的是种子填充方法。
对下面这张图片做处理在这里插入图片描述
得到结果为:
在这里插入图片描述

改变连通域颜色:

我用的方法是在刚开始的时候就随机设置三个RGB 值,然后为填充不同连通域(每个连通域的像素的RGB值都是随机的)。

cv::Scalar icvprGetRandomColor()
{
	uchar r = 255 * (rand() / (1.0 + RAND_MAX));
	uchar g = 255 * (rand() / (1.0 + RAND_MAX));
	uchar b = 255 * (rand() / (1.0 + RAND_MAX));
	return cv::Scalar(b, g, r);
}

代码自取

// CVE6.cpp: 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <iostream>
#include <string>
#include <list>
#include <vector>
#include <map>
#include <stack>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <stdio.h>
using namespace cv;
//Two Pass方法
void icvprCcaByTwoPass(const cv::Mat& _binImg, cv::Mat& _lableImg)
{
	/*
	 Two-Pass算法的简单步骤:
     (1)第一次扫描:
     访问当前像素B(x,y),如果B(x,y) == 1:
     a、如果B(x,y)的领域中像素值都为0,则赋予B(x,y)一个新的label:
     label += 1, B(x,y) = label;
     b、如果B(x,y)的领域中有像素值 > 1的像素Neighbors:
     1)将Neighbors中的最小值赋予给B(x,y):
     B(x,y) = min{Neighbors} 
     2)记录Neighbors中各个值(label)之间的相等关系,即这些值(label)同属同一个连通区域;
     labelSet[i] = { label_m, .., label_n },labelSet[i]中的所有label都属于同一个连通区域
	 (2)第二次扫描:
	 访问当前像素B(x,y),如果B(x,y) > 1:
	 a、找到与label = B(x,y)同属相等关系的一个最小label值,赋予给B(x,y);
	 完成扫描后,图像中具有相同label值的像素就组成了同一个连通区域。
	*/

	if (_binImg.empty() ||
		_binImg.type() != CV_8UC1)
	{
		return;
	}

	// 1. 第一次遍历

	_lableImg.release();
	_binImg.convertTo(_lableImg, CV_32SC1);

	int label = 1;  // start by 2
	std::vector<int> labelSet;
	labelSet.push_back(0);   // background: 0
	labelSet.push_back(1);   // foreground: 1

	int rows = _binImg.rows - 1;
	int cols = _binImg.cols - 1;
	for (int i = 1; i < rows; i++)
	{
		int* data_preRow = _lableImg.ptr<int>(i - 1);
		int* data_curRow = _lableImg.ptr<int>(i);
		for (int j = 1; j < cols; j++)
		{
			if (data_curRow[j] == 1)
			{
				std::vector<int> neighborLabels;
				neighborLabels.reserve(2);
				int leftPixel = data_curRow[j - 1];
				int upPixel = data_preRow[j];
				if (leftPixel > 1)
				{
					neighborLabels.push_back(leftPixel);
				}
				if (upPixel > 1)
				{
					neighborLabels.push_back(upPixel);
				}

				if (neighborLabels.empty())
				{
					labelSet.push_back(++label);  // assign to a new label
					data_curRow[j] = label;
					labelSet[label] = label;
				}
				else
				{
					std::sort(neighborLabels.begin(), neighborLabels.end());
					int smallestLabel = neighborLabels[0];
					data_curRow[j] = smallestLabel;

					// save equivalence
					for (size_t k = 1; k < neighborLabels.size(); k++)
					{
						int tempLabel = neighborLabels[k];
						int& oldSmallestLabel = labelSet[tempLabel];
						if (oldSmallestLabel > smallestLabel)
						{
							labelSet[oldSmallestLabel] = smallestLabel;
							oldSmallestLabel = smallestLabel;
						}
						else if (oldSmallestLabel < smallestLabel)
						{
							labelSet[smallestLabel] = oldSmallestLabel;
						}
					}
				}
			}
		}
	}

	// 更新标签
	// 用每个连通域中最小的标签来表示这个连通域
	for (size_t i = 2; i < labelSet.size(); i++)
	{
		int curLabel = labelSet[i];
		int preLabel = labelSet[curLabel];
		while (preLabel != curLabel)
		{
			curLabel = preLabel;
			preLabel = labelSet[preLabel];
		}
		labelSet[i] = curLabel;
	}

	// 2. 第二遍扫描
	for (int i = 0; i < rows; i++)
	{
		int* data = _lableImg.ptr<int>(i);
		for (int j = 0; j < cols; j++)
		{
			int& pixelLabel = data[j];
			pixelLabel = labelSet[pixelLabel];
		}
	}
}

void icvprCcaBySeedFill(const cv::Mat& _binImg, cv::Mat& _lableImg)
{
	/*
	种子填充法的连通区域分析方法:
(1)扫描图像,直到当前像素点B(x,y) == 1:
a、将B(x,y)作为种子(像素位置),并赋予其一个label,然后将该种子相邻的所有前景像素都压入栈中;
b、弹出栈顶像素,赋予其相同的label,然后再将与该栈顶像素相邻的所有前景像素都压入栈中;
c、重复b步骤,直到栈为空;
此时,便找到了图像B中的一个连通区域,该区域内的像素值被标记为label;
(2)重复第(1)步,直到扫描结束;
扫描结束后,就可以得到图像B中所有的连通区域;
	*/

	if (_binImg.empty() ||
		_binImg.type() != CV_8UC1)
	{
		return;
	}

	_lableImg.release();
	_binImg.convertTo(_lableImg, CV_32SC1);

	int label = 1;  // start by 2

	int rows = _binImg.rows - 1;
	int cols = _binImg.cols - 1;
	for (int i = 1; i < rows - 1; i++)
	{
		int* data = _lableImg.ptr<int>(i);
		for (int j = 1; j < cols - 1; j++)
		{
			if (data[j] == 1)
			{
				std::stack<std::pair<int, int>> neighborPixels;
				neighborPixels.push(std::pair<int, int>(i, j));     // 像素坐标: <i,j>
				++label;  //从一个新label开始
				while (!neighborPixels.empty())
				{
					// 栈中最上面的像素给予和与其连通的像素相同的label
					std::pair<int, int> curPixel = neighborPixels.top();
					int curX = curPixel.first;
					int curY = curPixel.second;
					_lableImg.at<int>(curX, curY) = label;

					// 弹出最上面的像素
					neighborPixels.pop();

					// 推及到四个邻居
					if (_lableImg.at<int>(curX, curY - 1) == 1)
					{// 左边的像素
						neighborPixels.push(std::pair<int, int>(curX, curY - 1));
					}
					if (_lableImg.at<int>(curX, curY + 1) == 1)
					{// 右边的像素
						neighborPixels.push(std::pair<int, int>(curX, curY + 1));
					}
					if (_lableImg.at<int>(curX - 1, curY) == 1)
					{// 上面的像素
						neighborPixels.push(std::pair<int, int>(curX - 1, curY));
					}
					if (_lableImg.at<int>(curX + 1, curY) == 1)
					{// 下面的像素
						neighborPixels.push(std::pair<int, int>(curX + 1, curY));
					}
				}
			}
		}
	}
}

//为连通域加上颜色
cv::Scalar icvprGetRandomColor()
{
	uchar r = 255 * (rand() / (1.0 + RAND_MAX));
	uchar g = 255 * (rand() / (1.0 + RAND_MAX));
	uchar b = 255 * (rand() / (1.0 + RAND_MAX));
	return cv::Scalar(b, g, r);
}

void icvprLabelColor(const cv::Mat& _labelImg, cv::Mat& _colorLabelImg)
{
	if (_labelImg.empty() ||
		_labelImg.type() != CV_32SC1)
	{
		return;
	}

	std::map<int, cv::Scalar> colors;

	int rows = _labelImg.rows;
	int cols = _labelImg.cols;

	_colorLabelImg.release();
	_colorLabelImg.create(rows, cols, CV_8UC3);
	_colorLabelImg = cv::Scalar::all(0);

	for (int i = 0; i < rows; i++)
	{
		const int* data_src = (int*)_labelImg.ptr<int>(i);
		uchar* data_dst = _colorLabelImg.ptr<uchar>(i);
		for (int j = 0; j < cols; j++)
		{
			int pixelValue = data_src[j];
			if (pixelValue > 1)
			{
				if (colors.count(pixelValue) <= 0)
				{
					colors[pixelValue] = icvprGetRandomColor();
				}
				cv::Scalar color = colors[pixelValue];
				*data_dst++ = color[0];
				*data_dst++ = color[1];
				*data_dst++ = color[2];
			}
			else
			{
				data_dst++;
				data_dst++;
				data_dst++;
			}
		}
	}
}

int main(int argc, char** argv)
{
	cv::Mat binImage = cv::imread("E:/C++/CVE6/图片2.png", 0);
	cv::imshow("img", binImage);
	//cv::Mat binImage2;
	cv::threshold(binImage, binImage, 50, 1, CV_THRESH_BINARY_INV);
	
	cv::Mat labelImg;
	//icvprCcaByTwoPass(binImage, labelImg);
	icvprCcaBySeedFill(binImage, labelImg) ;

	// 展示结果
	cv::Mat grayImg;
	//结果*10,更突出
	labelImg *= 10;
	labelImg.convertTo(grayImg, CV_8UC1);
	cv::imshow("labelImg", grayImg);

	cv::Mat colorLabelImg;
	//更改连通域颜色
	icvprLabelColor(labelImg, colorLabelImg);
	cv::imshow("colorImg", colorLabelImg);
	cv::waitKey(0);

	return 0;
}



<think>首先,用户的问题是:“请问单次扫描连通域标记算法 图像处理”。这看起来是中文的,意思是“请问单次扫描连通域标记算法 图像处理”。所以,用户想知道关于图像处理中的单次扫描连通域标记算法。 从系统级指令中,我需要: - 所有行内数学表达式使用 $...$ 格式。 - 独立公式使用 $$...$$ 格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 在回答中引用的段落末尾自然地添加引用标识,例如 [^1]。 用户可见层要求:回答结构清晰,帮助用户逐步解决问题,保证回答真实可靠。 参考站内引用:我有几个引用,这些是关于连通域标记算法的。引用[1]到[^5]提供了信息,包括两次扫描算法、一次扫描算法、种子算法等。具体到用户的问题,是关于单次扫描连通域标记算法。 从引用中: - 引用[1]提到“连通域标记算法的种类(1)两次逐行逐列逐像素扫描算法。(2)一次扫描算法。(3)种子算法。(4)基于图像的其他表示算法。” - 引用[2]说“二值图像连通域标记快速算法实现”,并提到“标记处理利用中间数据缓存分为连续的两种类型”,这可能涉及一次扫描。 - 引用[3]详细描述了图像标记算法,包括等价表等。 - 引用[4]讨论改进方法,节省栈空间。 - 引用[5]描述八邻域标记算法。 用户的问题是“单次扫描连通域标记算法”,这应该对应于引用中的“一次扫描算法”。 所以,我需要基于这些引用构建一个关于单次扫描连通域标记算法的回答。 结构清晰的回答:我应该逐步解释算法。 - 先介绍连通域标记是什么。 - 然后解释单次扫描算法的概念。 - 详细步骤。 - 可能包括伪代码或例子。 - 最后生成相关问题。 使用中文回答。 数学表达式:如果有方程或变量,用LaTeX格式。例如,像素坐标等。 引用标识:在引用的段落末尾添加 [^1] 等。我需要参考给定的引用。 现在,梳理引用内容: - [^1]:提到一次扫描算法作为种类之一。 - [^2]:描述一个快速算法实现,涉及一次扫描和类型1、类型2处理。 - [^3]:详细步骤,包括等价表等。 - [^4]:改进方法,节省栈空间。 - [^5]:八邻域标记算法。 对于单次扫描算法,引用[2]似乎最相关:它说“标记处理利用中间数据缓存分为连续的两种类型”,其中类型1用于初始标记,类型2用于连通域检测和归并。这听起来像是一次扫描,但可能涉及多个阶段。 引用[1]明确列出“一次扫描算法”。 在回答中,我应该基于这些描述单次扫描算法。 关键点: - 单次扫描算法:只扫描图像一次,同时进行标记和等价关系处理。 - 通常使用等价表来记录连通区域。 - 目标是高效,减少扫描次数。 步骤: 1. 初始化:设置标记计数器,等价表。 2. 扫描图像:逐行逐列扫描每个像素。 3. 对于每个像素,检查其邻域像素(如上、左、左上等)。 4. 根据邻域像素的标记,决定当前像素的标记: - 如果邻域有标记,继承最小标记。 - 如果冲突,记录等价关系。 5. 更新等价表。 6. 扫描完成后,整理等价表,将等价标记统一。 7. 最后,代换标记。 在引用[3]中,详细描述了这些步骤。 对于单次扫描,算法可能优化为在扫描时处理等价关系,减少后续整理。 引用[2]提到“类型1采用逆时钟顺序连通域检测”,然后“类型2对经过类型1初始标记过的图像数据再进行水平方向的连通域检测和归并”,所以可能不是严格的单次扫描,而是分为两个类型,但硬件上可能视为一次传输。 用户指定“单次扫描”,所以我应该聚焦于真正的单次扫描算法。 在文献中,单次扫描连通域标记算法如“One-pass algorithm”,使用等价表。 例如,经典算法是使用并查集(union-find)数据结构。 在引用[3]中,它描述了等价表操作。 回答结构: 1. **介绍**:解释连通域标记和单次扫描算法的优势。 2. **算法原理**:描述核心思想。 3. **详细步骤**:逐步解释算法流程。 4. **例子**:可选,用简单图像演示。 5. **优缺点**:讨论算法的优点和局限性。 6. **生成相关问题**:在结尾。 使用中文。 数学表达式:例如,像素位置 $(i,j)$,标记值 $l$ 等。 引用:当引用信息时,添加标识。 例如,基于引用[3],等价表的操作。 现在,草拟回答。</think>### 单次扫描连通域标记算法 在图像处理中,连通域标记是识别和标记二值图像中连通区域的关键技术,常用于目标检测、图像分割和特征提取。单次扫描连通域标记算法(也称为一次扫描算法)通过只扫描图像一次来完成标记过程,显著提高了效率,尤其适合实时或硬件加速场景。该算法利用等价表(equivalence table)动态记录和解析连通区域的等价关系,确保在单次遍历中完成初步标记和等价处理。下面我将逐步解释该算法的原理、详细步骤和实现要点,确保内容清晰可靠。 #### 1. **算法原理** 单次扫描算法基于二值图像(通常为0表示背景,1表示前景),通过逐行逐列扫描每个像素,并检查其邻域(如8邻域或4邻域)来决定当前像素的标记。核心思想是: - 在扫描过程中,动态分配临时标记并记录等价关系(即不同标记属于同一连通域)。 - 使用等价表存储这些关系,并在扫描结束时统一整理标记。 - 算法优势在于高效性:只扫描图像一次,减少内存和计算开销,适合处理大图像或嵌入式系统[^2][^3]。 例如,对于一个像素位置 $(i,j)$,其邻域像素(如左上、上、右上、左)的标记会影响当前标记的分配。数学上,标记值 $l$ 初始为0(背景),前景像素则分配新标记或继承邻域的最小标记。 #### 2. **详细步骤** 以下是单次扫描连通域标记算法的逐步流程,基于等价表机制。算法假设使用8邻域(包括上、下、左、右、左上、右上、左下、右下),但4邻域类似。参考引用[^3],关键环节包括等价表初始化、扫描标记和等价关系处理。 **步骤1: 初始化** - 创建一个等价表(数组结构),索引对应临时标记值,内容存储最小等价标记。 - 设置全局标记计数器 $count = 1$(从1开始,0保留给背景)。 - 初始化图像标记矩阵,所有像素设为未标记(例如,0)。 **步骤2: 逐行扫描图像** - 从图像左上角 $(1,1)$ 开始,逐行逐列扫描每个像素。 - 对于每个前景像素(值非0): - **检查邻域像素**:查看当前像素的邻域(通常只检查已扫描的邻域,如左、左上、上、右上,以确保依赖关系)。 - 如果没有邻域是前景,分配新标记:$l_{current} = count$,然后 $count = count + 1$。 - 如果有邻域是前景,继承这些邻域标记中的最小值 $min\_label$,并设置 $l_{current} = min\_label$。 - 如果邻域标记有冲突(即多个不同标记),记录等价关系:在等价表中,将这些标记设为等价(例如,等价表索引为标记值,内容更新为最小标记)。 - 示例:假设当前像素 $(i,j)$,邻域像素标记为 $l_{left} = 2$ 和 $l_{up} = 3$,则设置 $l_{current} = 2$,并在等价表中记录等价关系:$equivalence\_table[3] = 2$(表示标记3等价于2)。 **步骤3: 等价表整理** - 扫描完成后,等价表可能包含冗余关系(如标记4等价于3,3等价于2,则4应等价于2)。 - 整理等价表:从最小索引开始,追踪每个标记的根标记(最小等价标记),并更新等价表内容。 - 例如,遍历等价表,对每个标记 $k$,递归找到其最小等价标记 $root_k$,并设置 $equivalence\_table[k] = root_k$。 - 此步骤确保所有等价标记统一为最小连续整数,便于后续处理[^3]。 **步骤4: 标记代换** - 再次快速遍历图像(此步骤通常不计入“扫描”,因为只涉及查表操作),用等价表中的最终标记替换每个像素的临时标记。 - 同时,可计算连通域属性(如面积、中心坐标):对每个最终标记,累加像素数。 - 输出标记图像:连通域被标记为连续整数(如1,2,3,...),背景为0。 #### 3. **算法示例** 考虑一个简单二值图像(3x3): ``` 0 1 1 1 1 0 0 1 0 ``` - **扫描过程**: - 扫描第一行:像素(1,2)无邻域前景,分配标记1;像素(1,3)左邻域标记1,继承标记1。 - 扫描第二行:像素(2,1)无邻域前景,分配标记2;像素(2,2)上邻域标记1和左邻域标记2,取最小值 $min=1$,设置标记1,并记录等价关系:$equivalence\_table[2] = 1$。 - 扫描第三行:像素(3,2)上邻域标记1,继承标记1。 - **等价表整理**:等价表显示标记2等价于1,因此统一所有标记为1。 - **输出**:所有前景像素标记为1,表示一个连通域。 #### 4. **优缺点分析** - **优点**: - 高效:只扫描图像一次,时间复杂度为 $O(n)$($n$ 为像素数),内存开销低(等价表大小与标记数相关)。 - 适合硬件实现:引用[^2]提到,该算法可通过缓存优化减少边界效应,用于ASIC设计。 - 实时性好:适用于视频处理或实时系统。 - **缺点**: - 等价表整理可能增加额外开销,尤其在复杂图像中(标记冲突多)。 - 对噪声敏感:如果图像有孔洞或不规则区域,可能导致过度标记,需后处理优化[^4][^5]。 #### 5. **实现提示** - **邻域检查**:使用8邻域时,算法更鲁棒,但可能增加计算量;4邻域更简单,但易漏检对角连通。 - **优化技巧**:引用[^4]建议,沿扫描线填充水平像素段,节省栈空间(避免存储所有邻域点)。例如,只压入水平段的起始位置。 - **代码示例**(伪代码): ```python def single_pass_labeling(image): labels = np.zeros_like(image) # 初始化标记矩阵 equivalence_table = {} # 等价表:字典或数组 next_label = 1 # 扫描图像 for i in range(image.shape[0]): for j in range(image.shape[1]): if image[i][j] == 1: # 前景像素 neighbors = get_neighbors(labels, i, j) # 获取已扫描邻域标记 if not neighbors: labels[i][j] = next_label equivalence_table[next_label] = next_label # 初始化等价表 next_label += 1 else: min_label = min(neighbors) labels[i][j] = min_label for label in neighbors: if label != min_label: equivalence_table[label] = min_label # 记录等价关系 # 整理等价表:找到每个标记的根标记 for label in equivalence_table: current = label while equivalence_table[current] != current: current = equivalence_table[current] equivalence_table[label] = current # 标记代换 for i in range(image.shape[0]): for j in range(image.shape[1]): if labels[i][j] > 0: labels[i][j] = equivalence_table[labels[i][j]] return labels ``` 单次扫描算法在图像处理中广泛应用,如工业检测中的缺陷识别或医学图像分割。如需进一步优化,可参考引用[^2][^3]中的硬件加速方法。
评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

南木Sir

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值