使用CImg实现canny边缘检测和跟踪

第一步,函数toGrayScale 将彩色图像转换为灰度图像,根据RGB值和公式计算出灰度值即可,比较简单。

CImg<uchar> canny::toGrayScale()
{
	grayscaled = CImg<uchar>(img.width(), img.height(), 1, 1);
	cimg_forXY(grayscaled, x, y) {
		int r = img(x, y, 0);
		int g = img(x, y, 1);
		int b = img(x, y, 2);

		double newValue = (r * 0.2126 + g * 0.7152 + b * 0.0722);
		for (int i = 0; i < 3; ++i)
			grayscaled(x, y) = newValue;
	}
	return grayscaled;
}

第二步,函数createFilter 创建一个高斯滤波器,目的是为了降噪。使用正态分布计算权重矩阵,然后正规化。计算公式
在这里插入图片描述

vector<vector<double>> canny::createFilter(int row, int column, double sigmaIn)
{
	vector<vector<double>> filter;

	for (int i = 0; i < row; i++)
	{
		vector<double> col;
		for (int j = 0; j < column; j++)
		{
			col.push_back(-1);
		}
		filter.push_back(col);
	}

	float coordSum = 0;
	float constant = 2.0 * sigmaIn * sigmaIn;

	// Sum is for normalization
	float sum = 0.0;

	for (int x = -row / 2; x <= row / 2; x++)
	{
		for (int y = -column / 2; y <= column / 2; y++)
		{
			coordSum = (x*x + y * y);
			filter[x + row / 2][y + column / 2] = (exp(-(coordSum) / constant)) / (M_PI * constant);
			sum += filter[x + row / 2][y + column / 2];
		}
	}

	// Normalize the Filter
	for (int i = 0; i < row; i++)
		for (int j = 0; j < column; j++)
			filter[i][j] /= sum;

	return filter;
}

第三步,函数 useFilter 使用第二步创建的滤波器,创建一个3*3的权重矩阵。
任何边缘检测算法都不可能在未经处理的原始数据上很好地处理,对原始数据与高斯平滑模板作卷积,得到的图像与原始图像相比有些轻微的模糊。这样,单独的一个像素噪声在经过高斯平滑的图像上变得几乎没有影响。

CImg<uchar> canny::useFilter(CImg<uchar> img_in, vector<vector<double>> filterIn)
{
	int size = (int)filterIn.size() / 2;
	CImg<uchar> filteredImg(img_in.width() - 2 * size, img_in.height() - 2 * size, 1, 1);
	for (int i = size; i < img_in.width() - size; i++)
	{
		for (int j = size; j < img_in.height() - size; j++)
		{
			double sum = 0;

			for (int x = 0; x < filterIn.size(); x++)
				for (int y = 0; y < filterIn.size(); y++)
				{
					sum += filterIn[x][y] * (double)(img_in(i + x - size, j + y - size));
				}

			filteredImg(i - size, j - size) = sum;
		}

	}
	return filteredImg;
}

第四步,函数 sobel 使用索伯算子处理图像,用来寻找图像中的亮度梯度。
计算公式:
在这里插入图片描述
分别对横向和纵向进行处理。图像的每一个像素的横向及纵向梯度近似值可用以下的公式
结合:在这里插入图片描述 ,梯度方向:在这里插入图片描述

这里注意因为CImg(x,y)是以左上角为原点,横向为x,纵向为y访问像素,而opencv的at(x,y)访问的是第x行第y列的像素,所以这里计算sumx和sumy的模板交换。
在这里插入图片描述
在后面的操作中,都要注意这个问题。

CImg<uchar> canny::sobel()
{
	//Sobel X Filter
	double x1[] = { 1.0, 0, -1.0 };
	double x2[] = { 2.0, 0, -2.0 };
	double x3[] = { 1.0, 0, -1.0 };

	vector<vector<double>> xFilter(3);
	xFilter[0].assign(x1, x1 + 3);
	xFilter[1].assign(x2, x2 + 3);
	xFilter[2].assign(x3, x3 + 3);

	//Sobel Y Filter
	double y1[] = { -1.0, -2.0, -1.0 };
	double y2[] = { 0, 0, 0 };
	double y3[] = { 1.0, 2.0, 1.0 };

	vector<vector<double>> yFilter(3);
	yFilter[0].assign(y1, y1 + 3);
	yFilter[1].assign(y2, y2 + 3);
	yFilter[2].assign(y3, y3 + 3);

	//Limit Size
	int size = (int)xFilter.size() / 2;

	CImg<uchar> filteredImg(gFiltered.width() - 2 * size, gFiltered.height() - 2 * size, 1, 1);

	angles = CImg<float>(gFiltered.width() - 2 * size, gFiltered.height() - 2 * size, 1, 1); //AngleMap

	for (int i = size; i < gFiltered.width() - size; i++)
	{
		for (int j = size; j < gFiltered.height() - size; j++)
		{
			double sumx = 0;
			double sumy = 0;

			for (int x = 0; x < xFilter.size(); x++)
				for (int y = 0; y < yFilter.size(); y++)
				{
					sumx += yFilter[x][y] * (double)(gFiltered(i + x - size, j + y - size)); //Sobel_X Filter Value
					sumy += xFilter[x][y] * (double)(gFiltered(i + x - size, j + y - size)); //Sobel_Y Filter Value
				}
			double sumxsq = sumx * sumx;
			double sumysq = sumy * sumy;

			double sq2 = sqrt(sumxsq + sumysq);

			if (sq2 > 255) //Unsigned Char Fix
				sq2 = 255;
			filteredImg(i - size, j - size) = sq2;

			if (sumx == 0) //Arctan Fix
				angles(i - size, j - size) = 90;
			else
				angles(i - size, j - size) = atan(sumy / sumx)*(180.0 / M_PI);
		}
	}
	
	return filteredImg;
}

第五步,函数 nonMaxSupp 执行非极大值抑制,在8邻域内寻找像素点局部最大值,将非极大值点所对应的灰度值置为0,这样可以剔除掉一大部分非边缘的点。
使用CImg实现canny边缘检测和跟踪_第1张图片

CImg<uchar> canny::nonMaxSupp()
{
	CImg<uchar> nonMaxSupped(sFiltered.width() - 2, sFiltered.height() - 2, 1, 1);
	for (int i = 1; i < sFiltered.width() - 1; i++) {
		for (int j = 1; j < sFiltered.height() - 1; j++) {
			float Tangent = angles(i, j);
			//cout << Tangent << " ";
			nonMaxSupped(i - 1, j - 1) = sFiltered(i, j);
			//Horizontal Edge
			if (((-22.5 < Tangent) && (Tangent <= 22.5)) || ((157.5 < Tangent) && (Tangent <= -157.5)))
			{
				if ((sFiltered(i, j) < sFiltered(i+1, j )) || (sFiltered(i, j) < sFiltered(i-1, j )))
					nonMaxSupped(i - 1, j - 1) = 0;
			}
			//Vertical Edge
			if (((-112.5 < Tangent) && (Tangent <= -67.5)) || ((67.5 < Tangent) && (Tangent <= 112.5)))
			{
				if ((sFiltered(i, j) < sFiltered(i , j+1)) || (sFiltered(i, j) < sFiltered(i , j-1)))
					nonMaxSupped(i - 1, j - 1) = 0;
			}

			//-45 Degree Edge
			if (((-67.5 < Tangent) && (Tangent <= -22.5)) || ((112.5 < Tangent) && (Tangent <= 157.5)))
			{
				//if ((sFiltered(i, j) < sFiltered(i - 1, j + 1)) || (sFiltered(i, j) < sFiltered(i + 1, j - 1)))
				if ((sFiltered(i, j) < sFiltered(i + 1, j + 1)) || (sFiltered(i, j) < sFiltered(i - 1, j - 1)))
					nonMaxSupped(i - 1, j - 1) = 0;
			}

			//45 Degree Edge
			if (((-157.5 < Tangent) && (Tangent <= -112.5)) || ((22.5 < Tangent) && (Tangent <= 67.5)))
			{
				//if ((sFiltered(i, j) < sFiltered(i + 1, j + 1)) || (sFiltered(i, j) < sFiltered(i - 1, j - 1)))
				if ((sFiltered(i, j) < sFiltered(i - 1, j + 1)) || (sFiltered(i, j) < sFiltered(i + 1, j - 1)))
					nonMaxSupped(i - 1, j - 1) = 0;
			}
		}
	}
	return nonMaxSupped;
}

第六步,函数 threshold 执行滞后阈值化,由于噪声的影响,经常会在本应该连续的边缘出现断裂的问题。滞后阈值化设定两个阈值:一个为高阈值,一个为低阈值。如果任何像素边缘算子的影响超过高阈值,将这些像素标记为边缘;响应超过低阈值(高低阈值之间)的像素,如果与已经标记为边缘的像素8-邻接,则将这些像素也标记为边缘。

CImg<uchar> canny::threshold(CImg<uchar> imgin, int low, int high)
{
	if (low > 255)
		low = 255;
	if (high > 255)
		high = 255;

	CImg<uchar> EdgeMat(imgin.width(), imgin.height(), 1, 1);

	for (int i = 0; i < imgin.width(); i++)
	{
		for (int j = 0; j < imgin.height(); j++)
		{
			EdgeMat(i, j) = imgin(i, j);
			if (EdgeMat(i, j) > high)
				EdgeMat(i, j) = 255;
			else if (EdgeMat(i, j) < low)
				EdgeMat(i, j) = 0;
			else
			{
				bool anyHigh = false;
				bool anyBetween = false;
				for (int x = i - 1; x < i + 2; x++)
				{
					for (int y = j - 1; y < j + 2; y++)
					{
						if (x <= 0 || y <= 0 || x >= EdgeMat.width() || y >= EdgeMat.height()) //Out of bounds
							continue;
						else
						{
							if (EdgeMat(x, y) > high)
							{
								EdgeMat(i, j) = 255;
								anyHigh = true;
								break;
							}
							else if (EdgeMat(x, y) <= high && EdgeMat(x, y) >= low)
								anyBetween = true;
						}
					}
					if (anyHigh)
						break;
				}
				if (!anyHigh && anyBetween)
					for (int x = i - 2; x < i + 3; x++)
					{
						for (int y = j - 1; y < j + 3; y++)
						{
							if (x < 0 || y < 0 || x >= EdgeMat.width() || y >= EdgeMat.height()) //Out of bounds
								continue;
							else
							{
								if (EdgeMat(x, y) > high)
								{
									EdgeMat(i, j) = 255;
									anyHigh = true;
									break;
								}
							}
						}
						if (anyHigh)
							break;
					}
				if (!anyHigh)
					EdgeMat(i, j) = 0;
			}
		}
	}
	return EdgeMat;
}

第七步,也就是要求自己写的函数 edgeTrace ,使用8邻域边缘跟踪算法,对每个像素进行8邻域搜索,如果存在边缘,就将其归为一组,终止条件是8邻域都不存在边缘。

使用CImg实现canny边缘检测和跟踪_第2张图片

注意这里当寻找到7时,立即将当前节点替换为7,并从5开始继续搜索下去,即使不是7,也会直接替换,而不会管后面的节点。所以这一步的运行结果CImg和opencv会有些许不同,因为遍历的节点顺序不同。

这里参考博客 https://blog.csdn.net/sinat_31425585/article/details/78558849

CImg<uchar> canny::edgeTrack(CImg<uchar> Edge)
{
	// 8 neighbors
	const Point directions[8] = { { 1, 0 }, {1,1}, { 0, 1 }, { -1, 1 }, { -1, 0 },  { -1, -1 }, { 0, -1 },{ 1, -1 } };

	vector<Point> edge_t;
	vector<vector<Point>> edges;

	// 边缘跟踪
	int i, j, counts = 0, curr_d = 0;
	for (i = 1; i < Edge.width() - 1; i++) {
		for (j = 1; j < Edge.height() - 1; j++)
		{
			// 起始点及当前点
			Point b_pt = Point(i, j);
			Point c_pt = Point(i, j);

			// 如果当前点为前景点
			if (255 == Edge(c_pt.x, c_pt.y))
			{
				edge_t.clear();
				bool tra_flag = false;
				// 存入
				edge_t.push_back(c_pt);
				Edge(c_pt.x, c_pt.y) = 0;    // 用过的点直接给设置为0

				// 进行跟踪
				while (!tra_flag)
				{
					// 循环八次
					for (counts = 0; counts < 8; counts++)
					{
						// 防止索引出界
						if (curr_d >= 8)
						{
							curr_d -= 8;
						}
						if (curr_d < 0)
						{
							curr_d += 8;
						}

						// 当前点坐标
						// 跟踪的过程,应该是个连续的过程,需要不停的更新搜索的root点
						c_pt = Point(b_pt.x + directions[curr_d].x, b_pt.y + directions[curr_d].y);

						// 边界判断
						if ((c_pt.x > 0) && (c_pt.x < Edge.width() - 1) &&
							(c_pt.y > 0) && (c_pt.y < Edge.height() - 1))
						{
							// 如果存在边缘
							if (255 == Edge(c_pt.x, c_pt.y))
							{
								curr_d -= 2;   // 更新当前方向
								edge_t.push_back(c_pt);
								Edge(c_pt.x, c_pt.y) = 0;

								// 更新b_pt:跟踪的root点
								b_pt = c_pt;

								break;   // 跳出for循环
							}
						}
						curr_d++;
					}   // end for
					// 跟踪的终止条件:如果8邻域都不存在边缘
					if (8 == counts)
					{
						// 清零
						curr_d = 0;
						tra_flag = true;
						edges.push_back(edge_t);

						break;
					}

				}  // end if
			}  // end while

		}
	}

	CImg<uchar> trace_edge_color(Edge.width(), Edge.height(), 1, 1, 0);

	for (i = 0; i < edges.size(); i++)
	{
		// 过滤掉较小的边缘 
		if (edges[i].size() > 20)
		{
			for (j = 0; j < edges[i].size(); j++)
			{
				trace_edge_color(edges[i][j].x, edges[i][j].y) = 255;
			}
		}

	}

	return trace_edge_color;
}

github代码,包含cimg和opencv两个版本
https://github.com/CurryYuan/Computer-Vision/tree/master/canny edge detection

你可能感兴趣的:(计算机视觉)