# OpenCV卡尺工具

1. 卡尺区域的生成及绘制，基于GDI+完成图形绘制

实现代码：

else if(regionEx?.Region is RotatedRectF)
{
RotatedRectF rrect = (RotatedRectF)regionEx.Region;

using (var graph = new GraphicsPath())
{
PointF Center = new PointF(rrect.cx / sizeratio, rrect.cy / sizeratio);

graph.AddRectangle(new RectangleF( rrect.getrectofangleEqualZero().X / sizeratio,
rrect.getrectofangleEqualZero().Y / sizeratio,
rrect.getrectofangleEqualZero().Width / sizeratio,
rrect.getrectofangleEqualZero().Height / sizeratio));
graph.AddLine(new PointF((rrect.cx - rrect.Width / 2) / sizeratio, rrect.cy / sizeratio),
new PointF((rrect.cx + rrect.Width/2) / sizeratio, rrect.cy / sizeratio));
/
RotatedRectF rotatedRectF = new RotatedRectF((rrect.cx + rrect.Width / 2) / sizeratio,
rrect.cy / sizeratio,20 / sizeratio, 10 / sizeratio, 0);
PointF[] point2Fs = rotatedRectF.getPointF();
graph.AddLine(new PointF((rrect.cx + rrect.Width / 2) / sizeratio,
rrect.cy / sizeratio), new PointF(point2Fs[0].X, point2Fs[0].Y));
graph.AddLine(new PointF((rrect.cx + rrect.Width / 2) / sizeratio,
rrect.cy / sizeratio), new PointF(point2Fs[3].X, point2Fs[3].Y));
/
var a = rrect.angle * (Math.PI / 180);
var n1 = (float)Math.Cos(a);
var n2 = (float)Math.Sin(a);
var n3 = -(float)Math.Sin(a);
var n4 = (float)Math.Cos(a);
var n5 = (float)(Center.X * (1 - Math.Cos(a)) + Center.Y * Math.Sin(a));
var n6 = (float)(Center.Y * (1 - Math.Cos(a)) - Center.X * Math.Sin(a));
graph.Transform(new Matrix(n1, n2, n3, n4, n5, n6));
graphics.DrawPath(new Pen(regionEx.Color, regionEx.Size), graph);

}
}

2. 查找边缘点

a)参照ROI的角度，先将图像进行旋转

public static Mat RotateAffineOfSizeNoChange(this Mat img, double angle, ref List<Point2f> point2Fs)
{

// angle 0-360
while (angle < 0) angle += 360;
if (angle > 360) angle %= 360;
// 计算旋转后的图像尺寸
int w0 = img.Width, h0 = img.Height;
float w =img.Width, h = img.Height;

Point2f center = new Point2f(w0 / 2f, h0 / 2f);
Mat m = Cv2.GetRotationMatrix2D(center, angle, 1.0);
if (angle == 90 || angle == 270)
{
w = img.Height;
h = img.Width;
var mIndex = m.GetGenericIndexer<double>();
mIndex[0, 2] += (w - w0) / 2f;
mIndex[1, 2] += (h - h0) / 2f;
}

//坐标点位集合转换
if (point2Fs.Count>0)
{
Point2f[] point2Fs1 = new CVPointF[point2Fs.Count];
point2Fs.CopyTo(point2Fs1);
for (int i = 0; i < point2Fs.Count; i++)
{
var x = point2Fs1[i].X * m.At<double>(0, 0) + point2Fs1[i].Y * m.At<double>(0, 1) + m.At<double>(0, 2);
var y = point2Fs1[i].X * m.At<double>(1, 0) + point2Fs1[i].Y * m.At<double>(1, 1) + m.At<double>(1, 2);
point2Fs1[i].X = (int)Math.Round(x, 0);
point2Fs1[i].Y = (int)Math.Round(y, 0);

}
point2Fs = point2Fs1.ToList<Point2f>();
}

Mat rotated = new Mat();

Cv2.WarpAffine(img, rotated, m, new CVSize(w, h));
return rotated;
}

b)图像平均灰度投影

static Mat VerticalLine(Mat srcImage)//垂直线条检测
{

List<byte> array=new List<byte> ();

for (int i = 0; i < srcImage.Cols; i++)
{
int sumvalue = 0;
for (int j = 0; j < srcImage.Rows; j++)
{
sumvalue += srcImage.Get<byte>(j, i);//每一列的像素和
}
byte Average_strength =(byte)(sumvalue / srcImage.Rows);//每一列的平均强度

array.Add(Average_strength);

}
Mat lineImage = new Mat(1, array.Count, MatType.CV_8UC1,new Scalar(0,0,0));

int count = array.Count();
//恢复直线
for (int n = 0; n < lineImage.Rows; n++)
{
for (int w = 0; w < count; w++)
{
lineImage.Set<byte>(n, w, array[w]);

}
}

return lineImage;
}

c)二阶求导零点寻找边缘点

static public Mat FindEdges(Mat srcImage, byte edgeThreshold, ref List<Point2f> zeroPList,
EumProjection_direction eumProjection_Direction= EumProjection_direction.vertical )
{
Rect boundary = srcImage.BoundingRect();
Mat GaussMat = Filter.ImageFilter.GaussImage(srcImage, new Size(3, 3), 0);
if (eumProjection_Direction== EumProjection_direction.vertical)
{
float cenY = (boundary.Y + boundary.Height) / 2;
Mat projectMat = VerticalLine(GaussMat);//平均值灰度投影
Mat thdMat = new Mat();
Cv2.Threshold(projectMat,thdMat, edgeThreshold,255,ThresholdTypes.Binary);
Mat CannyMat=on_Canny(thdMat, edgeThreshold, 255);//二阶求导零点
//Mat mat = CannyMat.FindNonZero();
//CannyMat.GetArray<byte>(out byte[] bytearray);
zeroPList = new List<Point2f>();//极值点坐标集合
for (int i = 0; i < CannyMat.Cols; i++)
{
if (CannyMat.Get<byte>(0, i) == 255
&&i>3&&i< CannyMat.Cols-2)//去掉首位干扰点
zeroPList.Add(new Point2f(i, cenY));
}
return CannyMat;
}
else
{
float cenX = (boundary.X + boundary.Width) / 2;
Mat projectMat = HorizonLine(srcImage);//平均值灰度投影
//projectMat.GetArray<byte>(out byte[] values);
Mat thdMat = new Mat();
Cv2.Threshold(projectMat, thdMat, edgeThreshold, 255, ThresholdTypes.Binary);
//thdMat.GetArray<byte>(out byte[] values);
Mat CannyMat = on_Canny(thdMat, edgeThreshold, 255);//二阶求导零点
//Mat mat = CannyMat.FindNonZero();
zeroPList = new List<Point2f>();//极值点坐标集合
for (int i = 0; i < CannyMat.Rows; i++)
{
//byte ss = CannyMat.Get<byte>(0, i);
if (CannyMat.Get<byte>(i, 0) == 255)
zeroPList.Add(new Point2f(cenX, i));
}
return CannyMat;
}
}

3. 直线拟合

#1：最小二乘法

static public M_LINE fitLine( Mat srcImage,   List<Point2f> fitPList)
{
if (fitPList.Count < 2) return default;
//Cv2.FitLine(fitPList, DistanceTypes.L2, 0, 0.01, 0.01);
var line = Cv2.FitLine(fitPList, DistanceTypes.L1, 0, 0, 0);
line.FitSize(srcImage.Width, srcImage.Height, out var p1, out var p2);
return new M_LINE { sp = p1, ep = p2 };
}

#2： 随机抽样一致性法

public static M_LINE FitLineRansac( Mat srcImage, Vector2[] fitPList,
int iterations=1000, double sigma=1.0)
{
int bestScore = -1;
Line2D result = null;
Random random = new Random();
for (int i = 0; i < iterations; i++)
{
if (bestScore > fitPList.Length * 0.5)
break;

var indexes = GetRandomIndexes(random, 0, fitPList.Length);
var p1 = fitPList[indexes.Item0];
var p2 = fitPList[indexes.Item1];

var dir = Vector2.Normalize(p2 - p1);
var line = new Line2D(dir.X, dir.Y, p2.X, p2.Y);
int score = 0;
for (int j = 0; j < fitPList.Length; j++)
{
if (line.Distance(fitPList[j].X, fitPList[j].Y) < sigma)
score += 1;
}

if (score > bestScore)
{
bestScore = score;
result = line;
}
}

result.FitSize(srcImage.Width, srcImage.Height, out var pa, out var pb);
return new M_LINE { sp = pa, ep = pb };
}

4. 亚像素处理

P1、P2、P3、P4为邻近四个像素，a、b、c、d为待插值坐标距离邻近像素中心位置距离，绿点所表示的像素值可由下面公式计算得出

5. 检测效果图

PS：因为在网上opencvsharp开发的用例和参考资料相对较少，所以把这些分享出来和大家一起学习和探讨，有不足之处可以一起沟通交流！

THE END