opencv c++ canny 实现 以及与halcon canny的对比

发布时间:2026/7/31 2:28:18

opencv c++ canny 实现 以及与halcon canny的对比 Opencv和C实现canny边缘检测_opencv边缘增强-CSDN博客一、canny实现步骤1、图像必须是单通道的也就是说必须是灰度图像2、图像进行高斯滤波去掉噪点3、sobel 算子过程的实现计算x y方向 、梯度用不到但是可以看看xy 两个组合起来的结果以及梯度方向很重要4、局部非极大值抑制5、双阈值连接处理具体可以分为上面的5个步骤下面一起边看原理边实现。二、原理与实现1、图像灰度化如果是一张3通道的图像也就是我们常见的彩色图那么们就需要将其转换成一个灰度图其规则如下1.浮点算法Gray R*0.3 G*0.59 B*0.112.整数方法Gray (R*30G*59B*11)/1003.移位方法Gray (R*28G*151B*77 84.平均值法Gray (RGB)/35.仅取绿色Gray G但是通常我们自己实现一般都是拿第一种实现的。OpenCV转灰度图像特别简单只需调用函数 cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) 即可。codevoid ConvertRGB2GRAY(const Mat image, Mat imageGray) { if (!image.data || image.channels() ! 3) { return; } // 创建一个单通道的灰度图像 imageGray Mat::zeros(image.size(), CV_8UC1); // 取出存储图像的数组的指针 uchar* pointImage image.data; uchar* pointImageGray imageGray.data; int stepImage image.step; int stepImageGray imageGray.step; for (int i 0; i imageGray.rows; i) { for (int j 0; j imageGray.cols; j) { pointImageGray[i * stepImageGray j] 0.114 * pointImage[i * stepImage 3 * j] 0.587 * pointImage[i * stepImage 3 * j 1] 0.299 * pointImage[i * stepImage 3 * j 2]; } } }2、高斯滤波在高斯滤波的时候先要生成一个2元高斯核然后进行高斯滤波其作用是去掉噪点其图像变的平滑起来二元高斯函数作用去噪平滑图像避免误检边缘随着sigma的增大整个高斯函数的尖峰逐渐减小整体也变的更加平缓则对图像的平滑效果越来越明显。高斯核代码里面最后一定要归一化void CreateGaussianKernel(int kernel_size, int sigma, Mat kernel) { const double PI 3.1415926; int center kernel_size / 2; kernel Mat(kernel_size, kernel_size,CV_32FC1); float segma_pow 2 * sigma * sigma; float sum 0; // 二元高斯函数 for (size_t i 0; i kernel_size; i) { for (size_t j 0; j kernel_size; j) { float temp ((i - center) * (i - center) (j - center) * (j - center) )/ segma_pow; kernel.atfloat(i, j) 1 / (PI * segma_pow) * exp(-temp); sum kernel.atfloat(i, j); } } // 归一化 for (size_t i 0; i kernel_size; i) { for (size_t j 0; j kernel_size; j) { kernel.atfloat(i, j) kernel.atfloat(i, j)/sum; } } }5*5 的高斯核那个核数一般是不能超过11 超过11 其效果均值一样了高斯滤波//******************高斯滤波************************* //第一个参数imageSource是待滤波原始图像 //第二个参数imageGaussian是滤波后输出图像 //第三个参数 kernel 是一个指向含有N个double类型数组 //第四个参数size是滤波核的尺寸 //************************************************************* void GaussianFilter(const Mat imageSource, Mat imageGaussian, Mat kernel, int size) { if (!imageSource.data|| imageSource.channels()!1) { return; } imageGaussian Mat::zeros(imageSource.size(),CV_8UC1); float gaussArray[100]; // 将 kernel 的方阵 变成一个一维度数组 这样在循环的时候啊就少了一次内循环 int m 0; for (size_t i 0; i kernel.rows; i) { for (size_t j 0; j kernel.cols; j) { gaussArray[m] kernel.atfloat(i,j); m; } } //滤波 for (int i 0; i imageSource.rows; i) { for (int j 0; j imageSource.cols; j) { int k 0; for (int l -size / 2; l size / 2; l) { for (int g -size / 2; g size / 2; g) { //以下处理针对滤波后图像边界处理为超出边界的值赋值为边界值 int row i l; int col j g; row row 0 ? 0 : row; row row imageSource.rows ? imageSource.rows - 1 : row; col col 0 ? 0 : col; col col imageSource.cols ? imageSource.cols - 1 : col; //卷积和 imageGaussian.atuchar(i, j) gaussArray[k] * imageSource.atuchar(row, col); k; } } } } } void TestGaussian() { Mat kernel; CreateGaussianKernel(5, 1, kernel); // 打印 高斯核 for (int i 0; i kernel.rows; i) { for (int j 0; j kernel.cols; j) { cout kernel.atfloat(i, j); } cout endl; } Mat src imread(C:\\Users\\alber\\Desktop\\opencv_images\\529.jpg); Mat dst, imageGaussian; ConvertRGB2GRAY(src, dst); imwrite(C:\\Users\\alber\\Desktop\\opencv_images\\1\\1.jpg, dst); GaussianFilter(dst, imageGaussian, kernel, 5); imwrite(C:\\Users\\alber\\Desktop\\GaussianFilter.jpg, imageGaussian); }3、实现sobel 算子推导出X Y方向的核【精选】Opencv 笔记5 边缘处理-canny、sobel、Laplacian、Prewitt_opencv 边缘处理_Σίσυφος1900的博客-CSDN博客示例示例3×3 灰度块假想图假设有如下一个局部灰度区域越大越亮10 10 20 10 20 40 20 40 80 Sobel 卷积核示意图Gx检测水平变化[-1 0 1] [-2 0 2] [-1 0 1]Gy检测垂直变化[-1 -2 -1] [ 0 0 0] [1 2 1] 计算示例以中心像素为例1️⃣ 计算 Gx10*(-1) 10*(0) 20*(1) 10*(-2) 20*(0) 40*(2) 20*(-1) 40*(0) 80*(1)计算得Gx (-10 20) (-20 80) (-20 80) Gx 1302️⃣ 计算 Gy10*(-1) 10*(-2) 20*(-1) 10*(0) 20*(0) 40*(0) 20*(1) 40*(2) 80*(1)计算得Gy (-10 -20 -20) (0) (20 80 80) Gy 130 最终梯度幅值梯度强度gradient ||dx||||dy||theta atan(gradY / gradX) * 57.3 注意这里的角度转换//******************Sobel算子计算X、Y方向梯度 以及 梯度方向角******************** //第一个参数imageSourc原始灰度图像 //第二个参数imageSobelX是X方向梯度图像 //第三个参数imageSobelY是Y方向梯度图像 //第四个参数 theta 是梯度方向角数组指针 下一步很重要 就是要用这个值来计算 //************************************************************* void SobelGradDirction(const Mat imageSource, Mat imageX, Mat imageY, Mat gradXY, Mat theta) { imageX Mat::zeros(imageSource.size(), CV_32SC1); imageY Mat::zeros(imageSource.size(), CV_32SC1); gradXY Mat::zeros(imageSource.size(), CV_32SC1); theta Mat::zeros(imageSource.size(), CV_32SC1); int rows imageSource.rows; int cols imageSource.cols; int stepXY imageX.step; int step imageSource.step; /* Mat.step参数指图像的一行实际占用的内存长度 因为opencv中的图像会对每行的长度自动补齐8的倍数 编程时尽量使用指针指针读写像素是速度最快的使用at函数最慢。 */ uchar* PX imageX.data; uchar* PY imageY.data; uchar* P imageSource.data; uchar* XY gradXY.data; for (int i 1; i rows - 1; i) { for (int j 1; j cols - 1; j) { int a00 P[(i - 1) * step j - 1]; int a01 P[(i - 1) * step j]; int a02 P[(i - 1) * step j 1]; int a10 P[i * step j - 1]; int a11 P[i * step j]; int a12 P[i * step j 1]; int a20 P[(i 1) * step j - 1]; int a21 P[(i 1) * step j]; int a22 P[(i 1) * step j 1]; double gradY double(a02 2 * a12 a22 - a00 - 2 * a10 - a20); double gradX double(a00 2 * a01 a02 - a20 - 2 * a21 - a22); imageX.atint(i, j) abs(gradX); imageY.atint(i, j) abs(gradY); if (gradX 0) { gradX 0.000000000001; } theta.atint(i, j) atan(gradY / gradX) * 57.3; theta.atint(i, j) (theta.atint(i, j) 360) % 360; gradXY.atint(i, j) sqrt(gradX * gradX gradY * gradY); //XY[i*stepXY j*(stepXY / step)] sqrt(gradX*gradX gradY*gradY); } } convertScaleAbs(imageX, imageX); convertScaleAbs(imageY, imageY); convertScaleAbs(gradXY, gradXY); }这个不明显所以我打算换个图像test4、局部非极大值抑制局部极大值抑制的作用就是把边缘“变细”只保留最强的那条像素线。局部极大值抑制的作用就是把边缘“变细”只保留最强的那条像素线。局部极大值抑制的作用就是把边缘“变细”只保留最强的那条像素线。沿着 梯度方向 比较邻居像素为什么需要局部极大值抑制只保留梯度方向上最“尖锐”的像素去掉那些不够尖的让边缘从粗变细一像素宽。计算梯度后得到的边缘通常是宽的⟶ 不是清晰的一条线局部极大值抑制可以把边缘“瘦身”只保留最显著的边缘点。这里我们就要用到上面一步在sobel里面计算求得的x y 方向以及梯度方向的那些 东西了。如何做对每个像素看它的梯度方向比如 0°、45°、90°、135°。在该方向上比较该像素的梯度强度与它两侧的像素如果这个像素不是三者中最大 →抑制置为 0如果它最大 →保留计算梯度下面我们看一下梯度的分布[0-45] U[180-225][45-90] U[225-270][90-135] U[270-315][135-180] U[315-360]极大值抑制案例分析确定每个像素的梯度方向假设梯度方向是 45°↗方向逐像素比较邻域值沿梯度方向NMS 后的结果矩阵code:/// summary /// 局部极大值抑制 计算八领域 沿着该点梯度方向比较前后两个点的幅值大小若该点大于前后两点则保留若该点小于前后两点任意一点则置为0 /// /summary /// param nameimageInput 输入的图像/param /// param nameimageOutput/param /// param nametheta/param /// param nameimageX /param /// param nameimageY/param void NonLocalMaxValue(const Mat imageInput, Mat imageOutput, const Mat theta, const Mat imageX, const Mat imageY) { if (!imageInput.data || imageInput.channels() ! 1) { return; } imageOutput imageInput.clone(); int rows imageOutput.rows; int cols imageOutput.cols; int g00, g01, g02, g10, g11, g12, g20, g21, g22; int g1, g2, g3, g4; for (size_t i 1; i rows-1; i) { for (size_t j 1; j cols-1; j) { // 第一行 g00 imageOutput.atuchar(i - 1, j - 1); g01 imageOutput.atuchar(i - 1, j); g02 imageOutput.atuchar(i - 1, j1); // 第二行 g10 imageOutput.atuchar(i , j - 1); g11 imageOutput.atuchar(i , j); g12 imageOutput.atuchar(i, j 1); // 第三行 g20 imageOutput.atuchar(i1, j - 1); g21 imageOutput.atuchar(i1, j); g22 imageOutput.atuchar(i1, j 1); // 当前点的梯度方向 int direction theta.atint(i, j); g1 0; g2 0; g3 0; g4 0; // 保存亚像素点插值得到的灰度值 double t1 0; double t2 0; // 计算权重 double w fabs((double)imageY.atuchar(i,j) / (double)imageX.atuchar(i, j)); if (w0) { w 0.0000001; } if (w1) { w 1 / w; } // g00 g01 g02 // g10 g11 g12 // g20 g21 g22 // if ((0 direction direction 45) || 180 direction direction 225) { t1 g10 * (1 - w) g20 * (w); t2 g02 * (w)g12 * (1 - w); } if ((45 direction direction 90) || 225 direction direction 270) { t1 g01 * (1 - w) g02 * (w); t2 g20 * (w)g21 * (1 - w); } if ((90 direction direction 135) || 270 direction direction 315) { t1 g00 * (w)g01 * (1 - w); t2 g21 * (1 - w) g22 * (w); } if ((135 direction direction 180) || 315 direction direction 360) { t1 g00 * (w)g10 * (1 - w); t2 g12 * (1 - w) g22 * (w); } if (imageInput.atuchar(i,j)t1 || imageInput.atuchar(i, j) t2) { imageOutput.atuchar(i, j) 0; } } } }5、 双阈值连接处理双阈值处理给定一个高阈值high 一个低阈值low, low*[1.5,2]high 这个是给定规则判断条件就是当前currentlow 那么current0lowcurrenthight current 不处理currenthight current255/// summary /// 双阈值原理 /// 制定一个低阈值 L 一个 高阈值 H一般取H为整体图像灰度分布的 7成 并且H为1.5-2L /// 灰度值L gray0, grayH gray255; /// /summary /// param nameimageIn/param /// param namelow/param /// param namehight/param void DoubleThreshold(Mat imageIn, const double low, const double hight) { if (!imageIn.data || imageIn.channels() ! 1) { return; } int rows imageIn.rows; int cols imageIn.cols; double gray; for (size_t i 0; i rows ; i) { for (size_t j 0; j cols ; j) { gray imageIn.atuchar(i, j); gray gray hight ? (255) : (gray low) ? (0) : gray; imageIn.atuchar(i, j) gray; } } }将边缘链接起来经过上每一步的双阈值处理我们基本上已经拿到了边缘点的候选点下一步就是将这些边缘点联合起来组成一个边缘轮廓这里我们再次使用双阈值的机制 low 和 hight 和当前点的灰度值current规则如下 current 的8邻域的灰度值 M介于【low255】中有可能是边缘点这个领域的点M255 并且回退 如果领域类没有 说明这个点是一个孤立的点 不做处理最后判断图像中所有的点不是255 就是0 生成边缘// 在双阈值的时候我们已经将大于高阈值的变成255 了那么在低阈值和255 之间可能有边缘 // 这里主要处理的就是这部分灰度判断 检查其8领域看其邻域中与没有是255 的 // 如果有那么就需要找到这个点将这个点加入边缘中开始回退也就是说要走到上一步行和列都要 //回退1个 的像素 row row-1 colcol-1 void DoubleThresholdLink(Mat imageInput, int lowThreshold, int highThreshold) { for (int i 1; i imageInput.rows - 1; i) { for (int j 1; j imageInput.cols - 1; j) { // 在[lowThreshold ,255 ]区间的进行处理 if (imageInput.atuchar(i, j) lowThreshold imageInput.atuchar(i, j) 255) { // 判断其8 领域中是否有灰度值是255的 if (imageInput.atuchar(i - 1, j - 1) 255 || imageInput.atuchar(i - 1, j) 255 || imageInput.atuchar(i - 1, j 1) 255 || imageInput.atuchar(i, j - 1) 255 || imageInput.atuchar(i, j) 255 || imageInput.atuchar(i, j 1) 255 || imageInput.atuchar(i 1, j - 1) 255 || imageInput.atuchar(i 1, j) 255 || imageInput.atuchar(i 1, j 1) 255) { // 如果领域中有255 的 当前的点是边缘 那么就需要回退 imageInput.atuchar(i, j) 255; DoubleThresholdLink(imageInput, lowThreshold, highThreshold); //递归调用 } else { imageInput.atuchar(i, j) 0; } } } } }opencv 库结果还是用opencv库吧结果比这个好多了canny 算子为什么要采用双阈值处理双阈值的目的是为了利用强边缘确定主结构再用低阈值保留与主结构相连的弱边缘从而同时获得“完整边缘”和“低噪声”的效果。根本原因Canny 要双阈值的根本原因区分“真实弱边缘”和“噪声弱边缘在任何图像中边缘的梯度强度可分为三类类型梯度强度意义强边缘高如光亮、对比强真实、可靠弱边缘中如光照变化、阴影有可能是真边缘也可能是噪声噪声边缘低不应该被保留只用单阈值的两个坏结果① 阈值设太高弱边缘全部被丢掉 → 边缘断裂只保留强边缘 →物体边缘不连续轮廓不完整。例物体边缘有一部分因光照弱梯度只有 4050结果被全部丢掉。② 阈值设太低噪声大量被当成边缘 → 图像非常乱弱噪声点也会被当边缘 →结果全是干扰纹理像“毛糙的杂线”。如果你只用一个阈值不管怎么设定都会出问题。Canny双阈值Canny 使用高阈值 TH确定“强边缘”可靠低阈值 TL可能是真边缘也可能是噪声处理规则梯度类型条件是否保留强边缘≥ TH✔ 必须保留弱边缘TL ≤ 梯度 TH✔ 只有在与强边缘相连时保留噪声 TL✘ 删除示意图三、halcon 效果对比halcon的效果更好coderead_image (Grayimage, C:/Users/alber/Desktop/opencv_images/1/grayImage.jpg) edges_sub_pix (Grayimage, Edges, canny, 1, 20, 40)四、面试经典1 Canny 算子的缺点1️⃣ 参数敏感2️⃣ 对纹理图像容易过检测3️⃣ 边缘断裂4️⃣ 计算量比 Sobel 大2 为什么 Canny 使用高斯滤波因为高斯的一阶导数是最优边缘检测器。John Canny 证明在以下约束下最优1️⃣ 最大信噪比2️⃣ 精确定位3️⃣ 单响应最优解高斯导数滤波器3 Canny 和 Sobel 的区别算法特点Sobel简单梯度检测Canny完整边缘检测 pipeline精度Canny 更高抗噪Canny 更好4 为什么 NMS 沿梯度方向梯度方向垂直于边缘方向沿梯度方向比较才能找到真正的极值点。5 阈值如何选经验Th 2 ~ 3 × Tl或者Th 0.2 × max_gradientTl 0.1 × max_gradient6 Canny 复杂度是多少假设图像大小NM×K复杂度Gaussian O(N)Gradient O(N)NMS O(N)Hysteresis O(N)总体O(N)7 Canny 可以检测亚像素边缘吗标准 Canny不能但可以扩展梯度插值二次曲线拟合实现亚像素边缘。8 Canny 为什么能检测细边缘因为梯度检测非极大值抑制只保留局部最大梯度9 如何改进 Canny常见改进1️⃣ 自适应阈值2️⃣ 多尺度 Canny3️⃣ 亚像素 Canny4️⃣ 深度学习边缘检测10 Canny 为什么是最优边缘检测器John Canny 的优化目标最大化信号噪声\frac{\text{信号}}{\text{噪声}}噪声信号​并满足高检测率高定位精度单边缘响应求解变分问题后得到高斯导数滤波器是最优解。

相关新闻