目录
1. Opencvsharp介绍
2. NuGet安装OpenCvSharp4和OpenCvSharp4.runtime.win
3. 边缘检测
3.1 Canny算子
3.2 Laplacian拉普拉斯算子
3.3 Sobel算子
3.4 Scharr算子
4. 轮廓提取
5. 实际应用
6. 检测结果
OpenCvSharp 是一个OpenCV的.Net wrapper,应用最新的OpenCV库开发,使用习惯比EmguCV更接近原始的OpenCV --- 百度百科
OpenCVSharp特点
在OpenCvSharp里面,图像一般使用Mat对象存储。Mat即矩阵,矩阵的每一项是一个MatType结构。MatType有CV_16SC4、CV_8UC3、CV_8UC1等,可以看到,类型皆由四部分组成,定义如下:
8 | U | C | 3 |
每通道的字节数 | 每通道的数据类型 | 固定为C,指Channel | 通道数 |
例如,一般的无透明彩色图,每一格像素包含了RGB三个通道,所以其类型为8UC3,如果还包含透明度信息,则选用8UC4类型。
添加引用
using OpenCvSharp;
using Point = OpenCvSharp.Point;
using Size = OpenCvSharp.Size;
using Rect = OpenCvSharp.Rect;
private Mat CannyOperator(Mat srcImage)
{
Mat src_gray = new Mat();
//转换为灰度图
Cv2.CvtColor(srcImage, src_gray, ColorConversionCodes.RGB2GRAY);
//均值滤波: 利用区域内像素的均值替换掉原有的像素值,进行去噪
/*src:输入图像
dst: 输出图像
ksize:内核大小,size(X, Y),假如size(3, 3)则表示3 * 3的内核大小
anchor: 锚点。默认值Point(-1,-1)表示锚位于内核中心
borderType: 输出图像的边框样式,一般用默认样式*/
Cv2.Blur(src_gray, src_gray, new OpenCvSharp.Size(2, 2)); //滤波
Mat canny_Image = new Mat();
//Canny边缘检测
/*src:输入图像
edges:输出图像
threshold1:滞后阈值低阈值
threshold2:滞后阈值高阈值
一般高低阈值的比例在2:1到3:1之间
apertureSize: Sobel操作符的孔径大小[默认为aperturresizize . size3]
L2gradient :计算图像梯度幅值的标志
*/
Cv2.Canny(src_gray, canny_Image, 100, 200);
return canny_Image;
}
private Mat LaplacianOperator(Mat srcImg)
{
Mat LaplacianImg = new Mat();
Mat gussImage = new Mat();
//高斯滤波: 每个像素点的值都由本身与和邻近区域的其他像素值经过加权平均后得到,加权系数越靠近中心越大,越远离中心越小
/* src:输入图像
dst:输出图像
ksize:高斯核的大小。ksize。宽度和高度可以不同,但它们都必须是正的和奇数的。或者,它们可以是0然后用sigma来计算
sigmaX:表示高斯核在X轴方向的标准偏差
sigmaY :表示高斯核在Y轴方向的标准偏差值,如果sigmaY 为0,则sigmaY =sigmaX,如果两个sigma都为零,则用ksize计算
borderType :一般用默认值
*/
Cv2.GaussianBlur(srcImg, gussImage, new OpenCvSharp.Size(3, 3), 0, 0, BorderTypes.Default);
Mat grayImage = new Mat();
Cv2.CvtColor(gussImage, grayImage, ColorConversionCodes.RGB2GRAY); //灰度图
//Laplacian运算, 计算二阶导数
/*src 源图像
dst 输出图像,将具有与src相同的大小和相同数量的通道
ddepth 目标图像的所需深度 默认填 -1,与源图一致
ksize 用于计算二阶导数滤波器的孔径大小,卷积核大小,奇数
scale 计算的拉普拉斯值的可选缩放因子(默认情况下不应用缩放)
delta 可选的增量值,在将结果存储到dst之前添加到结果中
borderType 边缘处理方法
*/
Cv2.Laplacian(grayImage, LaplacianImg, -1, 3); //参数:1,源图像;2,输出图像;3,目标图像的所需深度 默认填 -1,与源图一致;4,用于计算二阶导数滤波器的卷积核大小,需奇数。
//阈值操作:可根据灰度的差异来分割图像
/* src:输入图像
dst:输出图像
thresh:阈值
maxval:阈值最大
type:阈值类型,详解见下
Binary:阈值二值化(大于阈值的让它等于最大值,小于的等于最小值)
BinaryInv:阈值反二值化(二值化阈值相反,大于阈值为最小值,小于阈值为最大值)
Trunc:截断(大于阈值的就等于阈值,小的不变)
ToZero:阈值归零(当大于阈值的不变,小于阈值的归零)
ToZeroIv:阈值归零取反(与阈值取零相反,大于时为最小值,小于时保持不变)
*/
Mat dst = new Mat();
Cv2.Threshold(LaplacianImg, dst, 10, 255, ThresholdTypes.Binary);
return dst;
}
//Sobel算子主要用来检测离散微分边缘算子,Sobel算子对噪声灰常敏感,一般需要先把图片进行高斯降噪
private Mat SobelOperator(Mat src_img)
{
Mat dst = new Mat();
//高斯滤波
Cv2.GaussianBlur(src_img, dst, new OpenCvSharp.Size(3, 3), 0, 0, BorderTypes.Default);
Mat grayImage = new Mat();
Cv2.CvtColor(dst, grayImage, ColorConversionCodes.BGR2GRAY); //转换为灰度图
Mat X = new Mat();
Mat Y = new Mat();
/*src:输入图像
dst:输出图像
ddepth:输出图像深度
xorder:X方向的差分阶数
yorder:Y方向的差分阶数
ksize :表示Sobel核大小,只能为奇数
scale: 计算导数值时候的缩放因子,默认为1
delta :表示存入目标图前可选的delta值
borderType :边界模式,一般为默认
*/
Cv2.Sobel(grayImage, X, MatType.CV_16S, 1, 0, 3); //Sobel边缘查找,参数:1,输入;2,输出X方向梯度图像;3,输出图像的深度;4,X方向几阶导数;5,Y方向几阶导数;6,卷积核大小,必须为奇数。
Cv2.Sobel(grayImage, Y, MatType.CV_16S, 0, 1, 3); //输出Y方向梯度图像
#region 方式1:像素操作进行相加
int width = X.Cols;
int hight = Y.Rows;
Mat output = new Mat(X.Size(), X.Type());
for (int x = 0; x < hight; x++) //合并X和Y,G= (Gx*Gx +Gy*Gy)的开平方根
{
for (int y = 0; y < width; y++)
{
int xg = X.At(x, y); //获取像素点的值
int yg = Y.At(x, y);
double v1 = Math.Pow(xg, 2); //平方
double v2 = Math.Pow(yg, 2);
int val = (int)Math.Sqrt(v1 + v2); //开平方根
if (val > 255) //确保像素值在 0至255之间
{
val = 255;
}
if (val < 0)
{
val = 0;
}
byte xy = (byte)val;
output.Set(x, y, xy); //为图像设置像素值
}
}
Mat tmp = new Mat(output.Size(), MatType.CV_8UC1);
#endregion
#region 方式2:利用现有API实现(X梯度+Y梯度)
Mat Abs_X = new Mat();
Mat Abs_Y = new Mat();
Mat Result = new Mat();
Cv2.ConvertScaleAbs(X, Abs_X, 1.0);//缩放,计算绝对值并将结果转换为8位。
Cv2.ConvertScaleAbs(Y, Abs_Y, 1.0);//缩放,计算绝对值并将结果转换为8位。
Cv2.AddWeighted(Abs_X, 0.5, Abs_Y, 0.5, 0, Result);//以不同的权重将两幅图片叠加
#endregion
//阈值
Mat result = new Mat();
Cv2.Threshold(tmp, result, 10, 250, ThresholdTypes.Binary);
return result;
}
//Scharr算子是对Sobel算子的优化,特别在核为3*3时
private Mat ScharrOperator(Mat srcImg)
{
Mat dst = new Mat();
Cv2.GaussianBlur(srcImg, dst, new OpenCvSharp.Size(3, 3), 0, 0, BorderTypes.Default);
Mat grayImage = new Mat();
Cv2.CvtColor(dst, grayImage, ColorConversionCodes.BGR2GRAY); //转换为灰度图
Mat grad_x = new Mat();
Mat grad_x2 = new Mat();
Mat grad_y = new Mat();
Mat grad_y2 = new Mat();
Cv2.Scharr(grayImage, grad_x, MatType.CV_16S, 1, 0);
Cv2.Scharr(grayImage, grad_y, MatType.CV_16S, 0, 1);
Cv2.ConvertScaleAbs(grad_x, grad_x2);
Cv2.ConvertScaleAbs(grad_y, grad_y2);
Mat result = new Mat();
Cv2.AddWeighted(grad_x2, 0.5, grad_y2, 0.5, 0, result);
//阈值
Cv2.Threshold(result, result, 10, 250, ThresholdTypes.Binary);
//Cv2.ImShow("Scharr", result);
return result;
}
public Mat FindContours(Mat srcImage)
{
//轮廓提取
/*image,灰度图片输入
contours,轮廓结果输出
mode,轮廓检索模式
External,只检测外层轮廓
List,提取所有轮廓,并放置在list中,检测的轮廓不建立等级关系
CComp,提取所有轮廓,并将轮廓组织成双层结构(two-level hierarchy),顶层为连通域的外围边界,次层位内层边界
Tree,提取所有轮廓并重新建立网状轮廓结构
FloodFill,官网没有介绍,应该是洪水填充法
method,轮廓近似方法
ApproxNone,获取每个轮廓的每个像素,相邻的两个点的像素位置差不超过1
ApproxSimple,压缩水平方向,垂直方向,对角线方向的元素,值保留该方向的重点坐标,如果一个矩形轮廓只需4个点来保存轮廓信息
ApproxTC89L1,使用Teh-Chinl链逼近算法中的一种
ApproxTC89KCOS,使用Teh-Chinl链逼近算法中的一种
*/
OpenCvSharp.Point[][] contours;
HierarchyIndex[] hierarchly;
Cv2.FindContours(srcImage, out contours, out hierarchly, RetrievalModes.Tree, ContourApproximationModes.ApproxSimple, new OpenCvSharp.Point(0, 0)); //获得轮廓
}
需求:待测矩形区域应在图像的中心,计算其offset
步骤:① 边缘检测 ② 提取轮廓 ③ 多边形逼近 ④ 条件判断,剔除非目标元素 ⑤ 获取图像的中心&矩形的中心 ⑥ 计算offset
private void detection()
{
stopwatch.Restart();
//读取原始图
Mat srcImage = Cv2.ImRead(ImagePath);
Mat dst_Image;
if ((dst_Image = FindContours(CannyOperator(srcImage))) == null)
{
Debug.WriteLine("Canny failed");
if ((dst_Image = FindContours(LaplacianOperator(srcImage)))==null)
{
Debug.WriteLine("Laplacian failed");
if ((dst_Image = FindContours(SobelOperator(srcImage)))==null)
{
Debug.WriteLine("Sobel failed");
if ((dst_Image = FindContours(ScharrOperator(srcImage))) == null)
{
MessageBox.Show("图像检测失败!");
return;
}
}
}
}
stopwatch.Stop();
Debug.WriteLine($"检测耗时:{stopwatch.ElapsedMilliseconds} ms");
Cv2.ImShow("原始图:", srcImage);
Cv2.ImShow("轮廓图", dst_Image);
}
private Point2f Result = new Point2f();
//查找轮廓
public Mat FindContours(Mat srcImage)
{
//轮廓提取
OpenCvSharp.Point[][] contours;
HierarchyIndex[] hierarchly;
Cv2.FindContours(srcImage, out contours, out hierarchly, RetrievalModes.Tree, ContourApproximationModes.ApproxSimple, new OpenCvSharp.Point(0, 0)); //获得轮廓
//FindRectangle(contours);
Mat dst_Image = Mat.Zeros(srcImage.Size(), srcImage.Type());
//标记出图像的中心点坐标
List keyPoints = new List();
Point2f imageCenter = new Point2f(dst_Image.Width / 2, dst_Image.Height / 2);
keyPoints.Add(new KeyPoint(imageCenter, 5));
Cv2.DrawKeypoints(dst_Image, keyPoints, dst_Image, Scalar.Blue);
Debug.WriteLine($"图像的中心点坐标:X:{imageCenter.X} Y:{imageCenter.Y}");
Random rnd = new Random();
double cnt_len;
Point[] cnt;
RotatedRect rect;
Point2f[] box=null;
for (int i = 0; i < contours.Length; i++)
{
//判断区分的数据根据实测图像细调
//要识别的矩形的点位总数范围
if (contours[i].Length < 30 || contours[i].Length >60)
{
continue;
}
cnt_len = Cv2.ArcLength(contours[i], true);//计算轮廓周长
cnt = Cv2.ApproxPolyDP(contours[i], 0.02 * cnt_len, true);//多边形逼近
//条件判断逼近边的数量不能小于4,轮廓面积不能小于1500,轮廓周长范围
if (cnt.Length <4 || Cv2.ContourArea(cnt) < 1500 || cnt_len<400 || cnt_len>550)
{
continue;
}
//求出在点集下的最小外接矩形
rect = Cv2.MinAreaRect(contours[i]);
Debug.WriteLine($"矩形的中心点坐标:{rect.Center}");
box = Cv2.BoxPoints(rect); //box的矩形四个顶点的坐标,依次为:左下、左上、右上、右下
foreach (var item in box)
{
Debug.WriteLine(item);
}
Debug.WriteLine($"{i}: {contours[i].Length}+area:{Cv2.ContourArea(contours[i], false)}+ len: {cnt_len}");
//仅获取要识别的矩形区域
//var centerPoint= GetCenterPoint(box); //自己计算的
var centerPoint = rect.Center; //矩形的中心点坐标,自带属性
if (Judge(centerPoint, imageCenter, ref Result))
{
//在图中标记出矩形轮廓的中心点坐标
keyPoints.Clear();
keyPoints.Add(new KeyPoint(centerPoint, 5));
Cv2.DrawKeypoints(dst_Image, keyPoints, dst_Image, Scalar.Red);
//颜色对象
Scalar color = new Scalar(rnd.Next(0, 255), rnd.Next(0, 255), rnd.Next(0, 255));
//画出轮廓
Cv2.DrawContours(dst_Image, contours, i, color, 2, LineTypes.Link8, hierarchly);
Debug.WriteLine($"offset: x:{Result.X} y:{Result.Y}");
}
}
if (box==null)
{
return null;
}
if (box.Length>1)
{
}
return dst_Image; //返回结果
}
//因有的图像中有两个几乎一样的矩形,因此通过中心点的偏移来排除掉另一个,仅留下实际带识别的
private bool Judge(Point2f point, Point2f center,ref Point2f result)
{
if ((center.X- point.X)>100 || (center.Y-point.Y)>100)
{
return false;
}
result= new Point2f(center.X - point.X, center.Y - point.Y);
return true;
}
结果1
结果2
结果3
结果4