C#+OpenCV实战(一)_图片简易角度矫正
本文介绍了一个自动矫正倾斜纸张图像的代码实现。该功能通过四个模块完成:1)图像预处理(降噪、强化边缘);2)轮廓检测与筛选(找出纸张轮廓);3)定位四角(拟合四边形顶点);4)透视变换(将倾斜图像矫正为正面矩形)。算法适用于扫描或拍摄的文档图像矫正场景,能有效处理因纸张放置不当导致的图像倾斜问题。代码通过边缘检测、凸包计算和透视变换等技术,最终输出经过几何矫正的平整文档图像。
·
1. 功能和使用场景
这段代码的核心功能是自动矫正倾斜的纸张图像,通过检测纸张边缘、定位四角并进行透视变换,让倾斜的纸张变得平整。
使用场景:例如扫描文档时纸张放歪了、拍摄的合同照片角度不正等,需要将图像中的纸张矫正为正视图的场景。

模块 1:图像预处理(降噪与强化边缘)
- 比喻:就像处理一张模糊的草稿纸 —— 先把纸上的小破洞补好(闭运算),再用橡皮擦淡化无关的污渍(高斯滤波),最后用马克笔把纸张的边缘描清晰(Canny 检测)。
- 作用:预处理后的图像能更清晰地显示纸张的轮廓,为后续检测做准备。
/// <summary>
/// 图像角度矫正-纸张
/// </summary>
/// <param name="src">图片</param>
/// <returns>结果图片</returns>
/// <exception cref="Exception"></exception>
public static Mat ImageAngle_Correction(Mat src)
{
// 复制原始图像备用
Mat src2 = new Mat();
src.CopyTo(src2); // 调用CopyTo方法,将输入图像src的内容复制到src2(相当于“备份原图”,避免后续处理破坏原图)
// 形态学闭运算(填充小缝隙)
InputArray kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3, 3));
// InputArray:OpenCV中用于传递输入数据的类型;kernel是“结构元素”(类似“小刷子”)
// Cv2.GetStructuringElement:创建结构元素;MorphShapes.Rect表示刷子是3x3的矩形(Size(3,3))
// 类比:用3x3的小方块刷子填补纸张边缘的小孔洞
Cv2.MorphologyEx(src, src, MorphTypes.Close, kernel, new Point(-1, -1), 3);
// Cv2.MorphologyEx:形态学操作函数;src, src表示“输入图像和输出图像都是src”(原地修改)
// MorphTypes.Close:闭运算(先膨胀后腐蚀,作用是填充小缝隙);kernel是上面创建的“刷子”
// new Point(-1, -1):锚点(刷子的中心点,-1,-1表示默认中心);3:操作执行3次(增强效果)
// 高斯滤波(模糊降噪)
Cv2.GaussianBlur(src, src, new Size(11, 11), 2, 2);
// Cv2.GaussianBlur:高斯模糊函数(类似“用毛玻璃盖在图像上,让细节变模糊”)
// new Size(11,11):模糊核大小(11x11的区域内计算模糊,数值越大越模糊)
// 后两个2:高斯函数的标准差(控制模糊程度,越大越模糊)
// 作用:过滤图像中的小噪点(比如纸张上的灰尘斑点)
// Canny边缘检测(提取轮廓线条)
Mat canny_Image = new Mat();// 声明一个新的Mat变量canny_Image,用于存储边缘检测结果
Cv2.Canny(src, canny_Image, 10, 30, 3, false);
// Cv2.Canny:边缘检测函数(类似“用马克笔勾勒出图像中明暗变化明显的线条”)
// src:输入图像;canny_Image:输出边缘图像(黑白图,边缘是白色线条)
// 10,30:阈值(低于10的不算边缘,高于30的肯定是边缘,中间的看是否和边缘相连)
// 3: Sobel算子大小(用于计算梯度,3表示3x3的算子);false:不使用L2范数(边缘强度计算方式)
模块 2:轮廓检测与筛选(找到纸张的轮廓)
- 比喻:相当于在一堆杂乱的线条中,找出最粗、最长的那一条(因为纸张通常是图像中面积最大的物体)。
- 作用:从所有轮廓中定位出纸张的边缘轮廓,排除其他小物体(如文字、污渍)的干扰。
// 找轮廓
/*
* 找轮廓(输入图像,out 轮廓集合,out 级别,轮廓检索模式,近似法,偏移量)
* 输入图像:单通道图像矩阵,可以是灰度图,但更常用的是二值图像,一般是经过Canny、拉普拉斯等边缘检测算子处理过的二值图像;
* 轮廓集合:contours
* 历史轮廓:hierarchy:0:后一个轮廓,1:前一个轮廓,2:父轮廓,3:内嵌轮廓
* 轮廓检索模式:轮廓的检索模式
* 取值一:CV_RETR_EXTERNAL 只检测最外围轮廓,包含在外围轮廓内的内围轮廓被忽略
* 取值二:CV_RETR_LIST 检测所有的轮廓,包括内围、外围轮廓,但是检测到的轮廓不建立等级关系,彼此之间独立,没有等级关系,这就意味着这个检索模式下不存在父轮廓或内嵌轮廓,所以hierarchy向量内所有元素的第3、第4个分量都会被置为-1,具体下文会讲到
* 取值三:CV_RETR_CCOMP 检测所有的轮廓,但所有轮廓只建立两个等级关系,外围为顶层,若外围内的内围轮廓还包含了其他的轮廓信息,则内围内的所有轮廓均归属于顶层
* 取值四:CV_RETR_TREE 检测所有轮廓,所有轮廓建立一个等级树结构。外层轮廓包含内层轮廓,内层轮廓还可以继续包含内嵌轮廓。
* 近似方法:轮廓的近似方法
* 取值一:CV_CHAIN_APPROX_NONE 保存物体边界上所有连续的轮廓点到contours向量内
* 取值二:CV_CHAIN_APPROX_SIMPLE 仅保存轮廓的拐点信息,把所有轮廓拐点处的点保存入contours向量内,拐点与拐点之间直线段上的信息点不予保留
* 取值三和四:CV_CHAIN_APPROX_TC89_L1,CV_CHAIN_APPROX_TC89_KCOS使用teh-Chinl chain 近似算法
* 偏移量:Point偏移量,所有的轮廓信息相对于原始图像对应点的偏移量,相当于在每一个检测出的轮廓点上加上该偏移量,且Point可以是负值。不填为默认不偏移Point()
*/
// 检测所有外围轮廓
Cv2.FindContours(canny_Image, out Point[][] contours, out HierarchyIndex[] hierarchly,
RetrievalModes.External,
ContourApproximationModes.ApproxSimple,
new Point(0, 0));
// 2. 检查是否检测到轮廓,没检测到则抛异常
if (contours.Length == 0)// 若轮廓集合为空(没有找到任何轮廓)
{
throw new Exception("边缘检测失败");// 抛出异常(程序中断并提示错误)
}
// 3. 绘制所有轮廓(可视化调试用,实际运行可注释)
Random rnd = new Random();// 创建随机数生成器(用于生成随机颜色)
Scalar color;// 声明Scalar类型变量color(Scalar是OpenCV中表示颜色的类型,BGR格式)
color = new Scalar(0, 255, 0);// 初始化颜色为绿色(B=0, G=255, R=0)
for (int i = 0; i < contours.Length; i++)// 循环遍历所有轮廓
{
color = new Scalar(rnd.Next(0, 255), rnd.Next(0, 255), rnd.Next(0, 255));// 生成随机颜色(每个轮廓颜色不同)
Cv2.DrawContours(src, contours, i, color, 2, LineTypes.Link4);// 在src图像上绘制第i个轮廓
// src:绘制目标图像;i:要绘制的轮廓索引;color:线条颜色;2:线条粗细;LineTypes.Link4:4连通线(线条更平滑)
}
//Cv2.ImShow("contours", src);// 显示绘制了轮廓的图像(调试用,注释掉则不显示)
//求出面积最大的轮廓(大概率是纸张)
double max_area = 0.0; // 存储最大轮廓面积(初始为0)
double currentArea = 0.0;// 存储当前轮廓的面积
Point[] max_contour = null;// 存储面积最大的轮廓(初始为null)
for (int i = 0; i < contours.Length; i++)// 遍历所有轮廓
{
currentArea = Cv2.ContourArea(contours[i]);// 计算第i个轮廓的面积(Cv2.ContourArea是计算轮廓面积的函数)
if (currentArea > max_area)// 若当前轮廓面积大于已知最大面积
{
max_area = currentArea;// 更新最大面积
max_contour = contours[i];// 更新最大轮廓
}
}
模块 3:定位纸张四角(拟合四边形顶点)
- 比喻:把纸张的边缘想象成一个不规则的线圈,先用力把线圈绷紧(凸包),再把多余的弯曲部分剪掉,最终得到一个四边形(因为纸张是矩形的)。
- 作用:精确定位纸张的四个角点,为后续矫正提供基准。
// 1. 提取轮廓的凸包(最外层边界)
//多边形拟合凸包的四个顶点
Point[] hull = Cv2.ConvexHull(max_contour);// Cv2.ConvexHull:计算轮廓的凸包(类似“用橡皮筋绷紧轮廓,得到最外层的凸多边形”)
// 作用:去除轮廓中的凹陷(比如纸张边缘轻微褶皱导致的凹陷)
// 多边形拟合(将凸包简化为四边形)
double epsilon = 0.02 * Cv2.ArcLength(max_contour, true);// 计算拟合精度(轮廓周长的2%,控制简化程度)
Point[] approx = Cv2.ApproxPolyDP(hull, epsilon, true);// 多边形拟合(将凸包简化为边数更少的多边形)
// hull:输入轮廓;epsilon:拟合精度(值越小越接近原轮廓);true:输出闭合多边形
// 3. 检查是否拟合出4个顶点(纸张是矩形,应有4个角)
if (approx.Length != 4)// 若拟合出的顶点数不是4
{
throw new Exception("拟合凸包的四个顶点失败");// 抛异常(说明没找到矩形纸张)
}
// 4. 绘制拟合出的四边形(可视化四角位置)
Scalar scalar2 = new Scalar(0, 255, 255);// 定义颜色为黄色(B=0, G=255, R=255)
// 依次连接4个顶点,绘制四边形
Cv2.Line(src, approx[0], approx[1], scalar2, 1, LineTypes.Link4);// 从第0个顶点到第1个顶点画直线
Cv2.Line(src, approx[1], approx[2], scalar2, 1, LineTypes.Link4);// 从第1个到第2个
Cv2.Line(src, approx[2], approx[3], scalar2, 1, LineTypes.Link4);// 从第2个到第3个
Cv2.Line(src, approx[3], approx[0], scalar2, 1, LineTypes.Link4);// 从第3个到第0个(闭合)
// 参数:目标图像、起点、终点、颜色、线宽、线类型
模块 4:透视变换(矫正倾斜角度)
- 比喻:如同把一张斜放的纸(四边形)“贴” 到一个端正的矩形框里 —— 先确定纸上四个角的位置,再计算如何拉伸、旋转这张纸,让四个角刚好对准矩形框的四个角。
- 作用:通过数学变换,将倾斜的纸张转换为正面平视的矩形图像,完成角度矫正。
// 1. 排序四角点(确定上下左右位置)
Array.Sort(approx, (cs1, cs2) =>// 用Array.Sort排序approx数组,自定义比较规则(lambda表达式)
{
if (cs1.Y > cs2.Y)// 若点cs1的Y坐标(垂直位置)大于cs2,说明cs1在下方
{
return 1;// cs1排在cs2后面(下方的点靠后)
}
else if (cs1.Y == cs2.Y)// 若Y坐标相同(同一水平线)
{
return cs1.X < cs2.X ? 1 : -1;// X坐标小的(左边)排在后面
}
else// cs1在上方
{
return -1;// cs1排在cs2前面(上方的点靠前)
}
});
// 排序后:approx[0]和approx[1]是上方两点,approx[2]和approx[3]是下方两点
// 2. 定义原始四角点(srcPt)和目标四角点(dstPt)
//算法找出的角点
Point2f[] srcPt = [approx[0], approx[1], approx[3], approx[2]];// 原始图像中纸张的四个角(按顺序:上左、上右、下右、下左)
// Point2f:浮点型坐标(比Point更精确,适合变换计算)
// 3. 计算目标矩形(矫正后的位置)
RotatedRect rect = Cv2.MinAreaRect(srcPt); // 计算srcPt的最小外接矩形(能包围4个点的最小矩形,可能倾斜)
Rect box = rect.BoundingRect();// 计算该矩形的轴对齐边界框(不倾斜的矩形,用于确定矫正后的位置)
Point2f[] dstPt = new Point2f[4];// 声明目标点数组(矫正后纸张的四个角)
// 定义目标点坐标(轴对齐矩形的四个角,按上左、上右、下右、下左顺序)
dstPt[0].X = box.X;// 上左X:边界框左上角X
dstPt[0].Y = box.Y;// 上左Y:边界框左上角Y
dstPt[1].X = box.X + box.Width;// 上右X:左上角X + 宽度
dstPt[1].Y = box.Y;// 上右Y:左上角Y
dstPt[2].X = box.X + box.Width;// 下右X:左上角X + 宽度
dstPt[2].Y = box.Y + box.Height;// 下右Y:左上角Y + 高度
dstPt[3].X = box.X;// 下左X:左上角X
dstPt[3].Y = box.Y + box.Height;// 下左Y:左上角Y + 高度
// 类比:确定一个“标准矩形框”,让纸张的四个角刚好对齐这个框的四个角
// 4. 执行透视变换(将倾斜纸张“贴”到目标矩形上)
Mat final = new Mat();// 声明final变量,存储矫正后的图像
Mat warpmatrix = Cv2.GetPerspectiveTransform(srcPt, dstPt); // 计算透视变换矩阵(描述如何从原始点变换到目标点的数学公式)
Cv2.WarpPerspective(src2, final, warpmatrix, src.Size()); // 应用透视变换
// src2:原始图像(用一开始备份的原图,避免预处理对内容的破坏);final:输出矫正后的图像
// warpmatrix:变换矩阵;src.Size():输出图像的大小(和原图一致)
return final;// 返回矫正后的图像
}
火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。
更多推荐
所有评论(0)