1:需求:在图像拍摄或是扫描过程中,获取到不规则的矩形,这样的图像若不预处理,对后期的处理过程中会造成较大的难度,需要通过透视变换来校正图像,得到正确的形状;
2:先看原图,如下所示:
3:处理后结果,如下所示:
4:详细代码如下所示:
public Mat PerspectiveCorrection(Mat src, out string printLines)
{
printLines = string.Empty;
//灰度处理
Mat grayMat = new Mat();
Cv2.CvtColor(src, grayMat, ColorConversionCodes.BGR2GRAY);
Cv2.ImShow("grayMat", grayMat);
//模糊处理 降低噪音
Mat blurMat = new Mat();
//Cv2.MedianBlur(grayMat, blurMat, 3);
Cv2.GaussianBlur(grayMat, blurMat, new Size(3, 3), 9, 9);
//Cv2.BilateralFilter(grayMat, blurMat, 5, 70, 100);
Cv2.ImShow("blurMat", blurMat);
//Mat edgesMat = new Mat();
//Cv2.Canny(blurMat, edgesMat, 50, 50 * 2, 3);
Cv2.Laplacian(blurMat, edgesMat, MatType.CV_8UC3, 3);
//Cv2.ImShow("edgesMat", edgesMat);
//二值处理
Mat binaryMat = new Mat();
Cv2.Threshold(blurMat, binaryMat, 0, 255, ThresholdTypes.Binary | ThresholdTypes.Otsu);
Cv2.ImShow("binaryMat", binaryMat);
//形态学操作
Mat morphMat = new Mat();
Mat kernel = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(9, 9), new Point(-1, -1));
Cv2.MorphologyEx(binaryMat, morphMat, MorphTypes.Close, kernel, new Point(-1, -1), 3);
Cv2.ImShow("morphMat", morphMat);
//轮廓发现
Cv2.BitwiseNot(morphMat, morphMat, new Mat());
Cv2.ImShow("BitwiseNot", morphMat);
Point[][] contours;
HierarchyIndex[] hierarchy;
Cv2.FindContours(morphMat, out contours, out hierarchy, RetrievalModes.Tree, ContourApproximationModes.ApproxSimple, new Point());
#region 寻找最大轮廓下标
//double area = -1;
//double tempArea = 0;
//int index = -1;
//for (int i = 0; i < contours.Length; i++)
//{
// area = Cv2.ContourArea(contours[i]);
// if (tempArea < area)
// {
// tempArea = area;
// index = i;
// }
//}
#endregion
//轮廓绘制
int width = src.Cols;
int height = src.Rows;
Mat drawImg = Mat.Zeros(src.Size(), MatType.CV_8UC3);
for (int i = 0; i < contours.Length; i++)
{
Rect rect = Cv2.BoundingRect(contours[i]);
if (rect.Width > width / 4 && rect.Width < width - 10)
{
Cv2.DrawContours(drawImg, contours, i, new Scalar(0, 0, 255), 2, LineTypes.Link8, hierarchy);
}
}
Cv2.ImShow("contours", drawImg);
//查找直线
Mat contoursImg = new Mat();
int accu = Math.Min(width / 2, height / 2);
Cv2.CvtColor(drawImg, contoursImg, ColorConversionCodes.BGR2GRAY);
LineSegmentPoint[] lines = Cv2.HoughLinesP(contoursImg, 1, Math.PI / 180.0, accu / 5, accu / 5);
Mat linesImg = Mat.Zeros(src.Size(), MatType.CV_8UC3);
for (int i = 0; i < lines.Length; i++)
{
Cv2.Line(linesImg, lines[i].P1, lines[i].P2, new Scalar(0, 0, 255), 1, LineTypes.Link8);
}
Cv2.ImShow("LinesImg", linesImg);
#region 查找坐标点
寻找与定位上下左右4条线 差异最大化 横线两条线的Y值差别大,竖线X防线差值大
int deltah = 0, deltaW = 0;
LineSegmentPoint topLine = new LineSegmentPoint();
LineSegmentPoint bottomLine = new LineSegmentPoint();
LineSegmentPoint leftLine = new LineSegmentPoint();
LineSegmentPoint rightLine = new LineSegmentPoint();
string str = string.Empty;
for (int i = 0; i < lines.Length; i++)
{
//上下横线 判断两点位置坐标差值 并且长度大于最小边的1/10;
deltah = Math.Abs(lines[i].P2.X - lines[i].P1.X);
if (lines[i].P2.Y < (height / 5 * 2) && lines[i].P1.Y < (height / 5 * 2) && deltah > accu / 10)
{
topLine = lines[i];
str += "t";
}
if (lines[i].P2.Y > height / 5 * 3 && lines[i].P1.Y > height / 5 * 3 && deltah > accu / 10)
{
bottomLine = lines[i];
str += "b";
}
//宽度 x方向
deltaW = Math.Abs(lines[i].P2.Y - lines[i].P1.Y);
if (lines[i].P1.X < (width / 5 ) && lines[i].P2.X < (width / 5) && deltaW > accu / 10)
{
leftLine = lines[i];
str += "l";
}
//在x五分之四的位置查找最右边线上的点
if (lines[i].P1.X > (width / 5 * 4) && lines[i].P2.X > (width / 5 * 4) && deltaW > accu / 10)
{
rightLine = lines[i];
str += "r";
}
}
if (!(str.Contains("t") && str.Contains("b") && str.Contains("l") && str.Contains("r")))
{
str = "RecFailed";
}
//打印坐标点显示
printLines = "topLine:p1(x,y)=" + topLine.P1.X + "," + topLine.P1.Y + " p2(x,y)=" + topLine.P2.X + "," + topLine.P2.Y + "\r\n";
printLines += "bottomLine:p1(x,y)=" + bottomLine.P1.X + "," + bottomLine.P1.Y + " p2(x,y)=" + bottomLine.P2.X + "," + bottomLine.P2.Y + "\r\n";
printLines += "leftLine:p1(x,y)=" + leftLine.P1.X + "," + leftLine.P1.Y + " p2(x,y)=" + leftLine.P2.X + "," + leftLine.P2.Y + "\r\n";
printLines += "rightLine:p1(x,y)=" + rightLine.P1.X + "," + rightLine.P1.Y + " p2(x,y)=" + rightLine.P2.X + "," + rightLine.P2.Y + "\r\n";
绘制圆点位置
Cv2.Circle(linesImg, topLine.P1, 2, new Scalar(255, 255, 0), 2, LineTypes.Link8, 0);
Cv2.Circle(linesImg, topLine.P2, 2, new Scalar(255, 255, 0), 2, LineTypes.Link8, 0);
Cv2.Circle(linesImg, bottomLine.P1, 2, new Scalar(255, 255, 0), 2, LineTypes.Link8, 0);
Cv2.Circle(linesImg, bottomLine.P2, 2, new Scalar(255, 255, 0), 2, LineTypes.Link8, 0);
Cv2.Circle(linesImg, leftLine.P1, 2, new Scalar(255, 255, 0), 2, LineTypes.Link8, 0);
Cv2.Circle(linesImg, leftLine.P2, 2, new Scalar(255, 255, 0), 2, LineTypes.Link8, 0);
Cv2.Circle(linesImg, rightLine.P1, 2, new Scalar(255, 255, 0), 2, LineTypes.Link8, 0);
Cv2.Circle(linesImg, rightLine.P2, 2, new Scalar(255, 255, 0), 2, LineTypes.Link8, 0);
Cv2.ImShow("linesImg", linesImg);
#region 计算交点
//y=kx+b 求出两条直线的交点 从而求出四个点
double k1, c1;
//两点法 (y1-y2)/(x1-x2) = k //横线X方向差值大 防止Y方向值相同 分母为零
k1 = Convert.ToDouble(topLine.P2.Y - topLine.P1.Y) / Convert.ToDouble(topLine.P2.X - topLine.P1.X);
c1 = topLine.P1.Y - k1 * topLine.P1.X;
double k2, c2;
k2 = Convert.ToDouble(bottomLine.P2.Y - bottomLine.P1.Y) / Convert.ToDouble(bottomLine.P2.X - bottomLine.P1.X);
c2 = bottomLine.P1.Y - k2 * bottomLine.P1.X;
//竖线Y方向差值大 防止X方向值相同 分母为零
double k3, c3;
if (Convert.ToDouble(leftLine.P2.X - leftLine.P1.X) == 0)//防止分母为0
{
leftLine.P2.X = leftLine.P2.X + 1;
}
k3 = Convert.ToDouble(leftLine.P2.Y - leftLine.P1.Y) / Convert.ToDouble(leftLine.P2.X - leftLine.P1.X);
c3 = leftLine.P1.Y - k3 * leftLine.P1.X;
double k4, c4; //考虑p1点和p2点x或y坐标相同
if (Convert.ToDouble(rightLine.P2.X - rightLine.P1.X) == 0)//防止分母为0
{
rightLine.P2.X = rightLine.P2.X + 1;
}
k4 = Convert.ToDouble(rightLine.P2.Y - rightLine.P1.Y) / Convert.ToDouble(rightLine.P2.X - rightLine.P1.X);
c4 = rightLine.P1.Y - k4 * rightLine.P1.X;
//四条直线的焦点
//左上角
Point p0 = new Point();
p0.X = Convert.ToInt32((c1 - c3) / (k3 - k1));
p0.Y = Convert.ToInt32(k1 * p0.X + c1);
//右上角
Point p1 = new Point();
p1.X = Convert.ToInt32((c1 - c4) / (k4 - k1));
p1.Y = Convert.ToInt32(k1 * p1.X + c1);
//左下角
Point p2 = new Point();
p2.X = Convert.ToInt32((c2 - c3) / (k3 - k2));
p2.Y = Convert.ToInt32(k2 * p2.X + c2);
//右下角
Point p3 = new Point();
p3.X = Convert.ToInt32((c2 - c4) / (k4 - k2));
p3.Y = Convert.ToInt32(k2 * p3.X + c2);
printLines += "p1(x,y)=" + p0.X + "," + p0.Y + "\r\n";
printLines += "p2(x,y)=" + p1.X + "," + p1.Y + "\r\n";
printLines += "p3(x,y)=" + p2.X + "," + p2.Y + "\r\n";
printLines += "p4(x,y)=" + p3.X + "," + p3.Y + "\r\n";
//绘制圆点位置
Cv2.Circle(linesImg, p0, 2, new Scalar(0, 0, 255), 2, LineTypes.Link8, 0);
Cv2.Circle(linesImg, p1, 2, new Scalar(0, 0, 255), 2, LineTypes.Link8, 0);
Cv2.Circle(linesImg, p2, 2, new Scalar(0, 0, 255), 2, LineTypes.Link8, 0);
Cv2.Circle(linesImg, p3, 2, new Scalar(0, 0, 255), 2, LineTypes.Link8, 0);
Cv2.Line(linesImg, topLine.P1, topLine.P2, new Scalar(255, 0, 0), 1, LineTypes.Link8, 0);
Cv2.ImShow("fourCorner", linesImg);
//透视变换
List srcContoursPoints = new List();
srcContoursPoints.Add(p0);
srcContoursPoints.Add(p1);
srcContoursPoints.Add(p2);
srcContoursPoints.Add(p3);
List dstContoursPoints = new List();
dstContoursPoints.Add(new Point2f(0, 0));
dstContoursPoints.Add(new Point2f(width, 0));
dstContoursPoints.Add(new Point2f(0, height));
dstContoursPoints.Add(new Point2f(width, height));
//透视变换矩阵
Mat retImg = new Mat();
Mat warpMat = Cv2.GetPerspectiveTransform(srcContoursPoints, dstContoursPoints);
Cv2.WarpPerspective(src, retImg, warpMat, retImg.Size(), InterpolationFlags.Linear);
Cv2.ImShow("retImg", retImg);
#endregion
#endregion
return linesImg;
}
基本处理过程图像如下:
二值图像
轮廓查找
线段提取
线段交点查找
图像透视变换
该案例透视变换的基本处理步骤如上,主要难点是如何噪声去除和线段交点查找部分的操作;不同图像需要对上述算法中一些参数进行调整(若不理解部分算法参数含义,查找相关资料学习);