OpenCvSharp函数:FitEllipse/FitEllipseAMS/FitEllipseDirect椭圆拟合、MinEnclosingCircle最小外接圆

FitEllipse椭圆拟合

函数说明:基于最小二乘法(least-squares sense)计算围绕一组(个数大于等于5个)给定的点集拟合一个椭圆。返回该椭圆的最小外接矩形(如果给定的点是在一条直线上,该矩形的最小边为0)。注意返回的数值可能有负数(大边界之外)。

//函数原型1
RotatedRect FitEllipse(InputArray points)

//函数原型2
RotatedRect FitEllipse(IEnumerable<Point> points)

//函数原型3
RotatedRect FitEllipse(IEnumerable<Point2f> points)

参数

说明

InputArray points

IEnumerable<Point> points

IEnumerable<Point2f> points

待拟合椭圆的点集,个数大于等于5个。

返回值RotatedRect

拟合的椭圆的最小外接矩形,点在一直线上的话,该矩形最小边为0

FitEllipseAMS椭圆拟合(Approximate Mean Square(AMS))

函数说明:基于Approximate Mean Square(近似均方) 方法计算点集的拟合椭圆

//函数原型1
RotatedRect FitEllipseAMS(InputArray points)

//函数原型2
RotatedRect FitEllipseAMS(IEnumerable<Point> points)

//函数原型3
RotatedRect FitEllipseAMS(IEnumerable<Point2f> points)

FitEllipseDirect椭圆拟合(Direct least square(Direct))

函数说明:基于Direct least square(最小二乘法) 方法计算点集的拟合椭圆

扫描二维码关注公众号,回复: 16663770 查看本文章
//函数原型1
RotatedRect FitEllipseDirect(InputArray points)

//函数原型2
RotatedRect FitEllipseDirect(IEnumerable<Point> points)

//函数原型3
RotatedRect FitEllipseDirect(IEnumerable<Point2f> points)

三个函数对比

FitEllipse:据说是,速度快些,但结果不一定是最佳的(如图像示例中的大蓝色框)

FitEllipseAMS:速度慢些,但结果更优。

AMS和Direct算法看不懂,有能讲明白的高人或简明的参考的链接,望评论区被补上,感谢。

MinEnclosingCircle最小外接圆

函数说明:获取一组坐标点集的最小外接圆,返回中心点和半径。

//函数原型1
void MinEnclosingCircle(InputArray points,
    out Point2f center,
    out float radius)

//函数原型2
void MinEnclosingCircle(IEnumerable<Point> points,
    out Point2f center,
    out float radius) 

//函数原型3
void MinEnclosingCircle(IEnumerable<Point2f> points,
    out Point2f center,
    out float radius)

参数

说明

InputArray points

IEnumerable<Point> points

IEnumerable<Point2f> points

待计算最小外接圆的点集。数据类型必须为CV_32SC2 或CV_32FC2

out Point2f center

输出最小外接圆的中心点

out float radius

输出最小外接圆的半径

图像示例

原图

示例

源码示例

private string winName = "FitEllipse";        
private Mat image = new Mat();

private bool fitEllipseQ = true;
private bool fitEllipseAMSQ = true;
private bool fitEllipseDirectQ = true;
private bool minEnclosingCircleColorQ = true;

Scalar minEnclosingCircleColor = new Scalar(255, 255, 0);
Scalar fitEllipseColor = new Scalar(255, 0, 0);
Scalar fitEllipseAMSColor = new Scalar(0, 255, 0);
Scalar fitEllipseDirectColor = new Scalar(0, 0, 255);
Scalar fitEllipseTrueColor = new Scalar(255, 255, 255);

public void Run(ParamBase paramBase) {

    image = Cv2.ImRead(ImagePath.Ellipses, ImreadModes.Grayscale);
    if (image.Empty()) throw new Exception($"图像打开有误:{ImagePath.Ellipses}");
    Cv2.ImShow("source", image);
    Cv2.NamedWindow(winName, WindowFlags.Normal);
    Cv2.ResizeWindow(winName, image.Width * 2,image.Height*2);
    Cv2.CreateTrackbar("threshold", winName, 255, ProcessImage);
    Cv2.SetTrackbarPos("threshold", winName, 102);
    //ProcessImage(0, IntPtr.Zero);
    while (true) {
        if (Cv2.WaitKey(50) == (int)Keys.Escape) break;
    }
    Cv2.DestroyAllWindows();
    image.Dispose();

}

private void ProcessImage(int pos, IntPtr userData) {
    RotatedRect box = new RotatedRect();
    RotatedRect boxAMS = new RotatedRect();
    RotatedRect boxDirect = new RotatedRect();

    using Mat bImage = image.GreaterThanOrEqual(pos);

    //轮廓发现
    Point[][] contours = Cv2.FindContoursAsArray(bImage, RetrievalModes.List, ContourApproximationModes.ApproxNone);

    Canvas paper = new Canvas();
    paper.Init((int)(0.8 * Math.Min(bImage.Rows, bImage.Cols)), (int)(1.2 * Math.Max(bImage.Rows, bImage.Cols)));
    paper.Stretch(new Point2f(0.0f, 0.0f), new Point2f((float)(bImage.Cols + 2.0), (float)(bImage.Rows + 2.0)));

    List<string> text = new List<string>();
    List<Scalar> color = new List<Scalar>();

    if (minEnclosingCircleColorQ) {
        text.Add("minEnclosingCircle");
        color.Add(minEnclosingCircleColor);
    }

    if (fitEllipseQ) {
        text.Add("OpenCV");
        color.Add(fitEllipseColor);
    }
    if (fitEllipseAMSQ) {
        text.Add("AMS");
        color.Add(fitEllipseAMSColor);
    }
    if (fitEllipseDirectQ) {
        text.Add("Direct");
        color.Add(fitEllipseDirectColor);
    }
    paper.DrawLabels(text.ToArray(), color.ToArray());

    int margin = 2;
    List<List<Point>> points = new List<List<Point>>();
    for (int i = 0; i < contours.Length; i++) {
        int count = contours[i].Length;
        //椭圆拟合至少要5个坐标点
        if (count < 6) continue;
        List<Point> pts = new List<Point>();
        for (int j = 0; j < count; j++) {
            Point pnt = contours[i][j];
            if ((pnt.X > margin && pnt.Y > margin && pnt.X < bImage.Cols - margin && pnt.Y < bImage.Rows - margin)) {
                if (j % 20 == 0) {
                    pts.Add(pnt);
                }
            }
        }
        points.Add(pts);
    }

    //直线的话,FitEllipse返回的最小外接矩形的width为0
    //points.Clear();
    //points.Add(new List<Point>() { new Point(0, 50), new Point(0, 100), new Point(0, 150), new Point(0, 200), new Point(0, 250) });

    //为一个点的话,FitEllipse返回的最小外接矩形的Width和Height都为0
    //points.Clear();
    //points.Add(new List<Point>() { new Point(100, 50), new Point(100, 50), new Point(100, 50), new Point(100, 50), new Point(100, 50) });

    for (int i = 0; i < points.Count(); i++) {
        var pts = points[i];
        //最少5个点
        if (pts.Count() < 5) {
            continue;
        }
        var ptfs = pts.ConvertAll(z => new Point2f(z.X, z.Y));
        bool drawPoint = false;


        if (fitEllipseQ) {
            box = Cv2.FitEllipse(ptfs);
            if (box.Size.Height <= box.Size.Width * 30 &&
                box.Size.Width > 0) {//如果是一条直线或同一点时,Width=0
                paper.DrawEllipseWithBox(box, fitEllipseColor, 3);
                drawPoint = true;
            }
        }
        if (fitEllipseAMSQ) {
            boxAMS = Cv2.FitEllipseAMS(ptfs);
            if (boxAMS.Size.Height <= boxAMS.Size.Width * 30 &&
                boxAMS.Size.Width > 0) {
                paper.DrawEllipseWithBox(boxAMS, fitEllipseAMSColor, 2);
                drawPoint = true;
            }
        }
        if (fitEllipseDirectQ) {
            boxDirect = Cv2.FitEllipseDirect(ptfs);
            if (boxDirect.Size.Height <= boxDirect.Size.Width * 30 &&
                boxDirect.Size.Width > 0) {
                paper.DrawEllipseWithBox(boxDirect, fitEllipseDirectColor, 1);
                drawPoint = true;
            }
        }
        if (!drawPoint) continue;
        if (minEnclosingCircleColorQ) {
            //最小外接圆
            Cv2.MinEnclosingCircle(ptfs, out Point2f center, out float radius);
            paper.DrawCircle(center, radius, minEnclosingCircleColor, 1);
            drawPoint = true;
        }
        paper.DrawPoints(ptfs.ToArray(), fitEllipseTrueColor);
    }

    Cv2.ImShow(winName, paper.img);
    paper.Dispose();
}

internal class Canvas : IDisposable {
    public bool setupQ { get; set; }
    public Point origin { get; set; }
    public Point corner { get; set; }
    public int minDims { get; set; }
    public int maxDims { get; set; }
    public double scale { get; set; }
    public int rows { get; set; }
    public int cols { get; set; }

    public Mat img { get; set; } = new Mat();
    public void Init(int minD, int maxD) {
        // Initialise the canvas with minimum and maximum rows and column sizes.
        minDims = minD; maxDims = maxD;
        origin = new Point(0, 0);
        corner = new Point(0, 0);
        scale = 1.0;
        rows = 0;
        cols = 0;
        setupQ = false;
    }

    public void Stretch(Point2f min, Point2f max) {
        // Stretch the canvas to include the points min and max.
        if (setupQ) {
            if (corner.X < max.X) { corner = new Point((int)(max.X + 1.0), corner.Y); };
            if (corner.Y < max.Y) { corner = new Point(corner.X, (int)(max.Y + 1.0)); };
            if (origin.X > min.X) { origin = new Point((int)min.X, origin.Y); };
            if (origin.Y > min.Y) { origin = new Point(origin.X, (int)min.Y); };
        }
        else {
            origin = new Point((int)min.X, (int)min.Y);
            corner = new Point((int)(max.X + 1.0), (int)(max.Y + 1.0));
        }
        int c = (int)(scale * ((corner.X + 1.0) - origin.X));
        if (c < minDims) {
            scale = scale * (double)minDims / (double)c;
        }
        else {
            if (c > maxDims) {
                scale = scale * (double)maxDims / (double)c;
            }
        }
        int r = (int)(scale * ((corner.Y + 1.0) - origin.Y));
        if (r < minDims) {
            scale = scale * (double)minDims / (double)r;
        }
        else {
            if (r > maxDims) {
                scale = scale * (double)maxDims / (double)r;
            }
        }
        cols = (int)(scale * ((corner.X + 1.0) - origin.X));
        rows = (int)(scale * ((corner.Y + 1.0) - origin.Y));
        setupQ = true;
    }

    public void Stretch(Point2f[] pts) {
        // Stretch the canvas so all the points pts are on the canvas.
        Point2f min = pts[0];
        Point2f max = pts[0];
        for (int i = 1; i < pts.Length; i++) {
            Point2f pnt = pts[i];
            if (max.X < pnt.X) { max.X = pnt.X; };
            if (max.Y < pnt.Y) { max.Y = pnt.Y; };
            if (min.X > pnt.X) { min.X = pnt.X; };
            if (min.Y > pnt.Y) { min.Y = pnt.Y; };
        };
        Stretch(min, max);
    }

    public void Stretch(RotatedRect box) {   // Stretch the canvas so that the rectangle box is on the canvas.
        Point2f min = box.Center;
        Point2f max = box.Center;
        Point2f[] vtx = box.Points();
        for (int i = 0; i < 4; i++) {
            Point2f pnt = vtx[i];
            if (max.X < pnt.X) { max.X = pnt.X; };
            if (max.Y < pnt.Y) { max.Y = pnt.Y; };
            if (min.X > pnt.X) { min.X = pnt.X; };
            if (min.Y > pnt.Y) { min.Y = pnt.Y; };
        }
        Stretch(min, max);
    }

    private Point ToPoint(Point2f point2f) {
        //注意OpenCV的Point2f转Point的是使用 template<> inline int saturate_cast<int>(float v)            { return cvRound(v); }
        //而OpenCvSharpe的point2f.ToPoint();是向下取整
        return new Point(Math.Round(point2f.X), Math.Round(point2f.Y));
    }

    public void DrawEllipseWithBox(RotatedRect box, Scalar color, int lineThickness) {
        if (img.Empty()) {
            Stretch(box);
            img = Mat.Zeros(rows, cols, MatType.CV_8UC3);
        }
        box.Center = new Point2f(box.Center.X - origin.X, box.Center.Y - origin.Y) * scale;
        box.Size.Width = (float)(scale * box.Size.Width);
        box.Size.Height = (float)(scale * box.Size.Height);
        Cv2.Ellipse(img, box, color, lineThickness, LineTypes.AntiAlias);
        Point2f[] vtx = box.Points();
        for (int j = 0; j < 4; j++) {
            Cv2.Line(img, ToPoint(vtx[j]), ToPoint(vtx[(j + 1) % 4]), color, lineThickness, LineTypes.AntiAlias);
        }
    }
    public void DrawCircle(Point2f center,float radius, Scalar color, int lineThickness) {
        var Center = new Point2f(center.X - origin.X, center.Y - origin.Y) * scale;

        Cv2.Circle(img, (int)((center.X - origin.X) * scale), (int)((center.Y - origin.Y) * scale), (int)(radius * scale), color, lineThickness);
    }
    public void DrawPoints(Point2f[] pts, Scalar color) {
        if (img.Empty()) {
            Stretch(pts);
            img = Mat.Zeros(rows, cols, MatType.CV_8UC3);
        }
        for (int i = 0; i < pts.Length; i++) {
            Point pnt = new Point(pts[i].X - origin.X, pts[i].Y - origin.Y) * scale;
            img.At<Vec3b>(pnt.Y, pnt.X) = color.ToVec3b();
        }
    }
    public void DrawLabels(string[] texts, Scalar[] colors) {
        if (img.Empty()) {
            img = Mat.Zeros(rows, cols, MatType.CV_8UC3);
        }
        int vPos = 0;
        for (int i = 0; i < texts.Length; i++) {
            Size textsize = Cv2.GetTextSize(texts[i], HersheyFonts.HersheyComplex, 1, 1, out int baseLine);
            vPos += (int)(textsize.Height * 1.3);
            Point org = new Point((img.Cols - textsize.Width), vPos);
            Cv2.PutText(img, texts[i], org, HersheyFonts.HersheyComplex, 1, colors[i], 1, LineTypes.Link8);
        }            
    }

    public void Dispose() {
        if (img != null) { img.Dispose(); }
        img = null;
    }
}

官网C++源码https://github.com/opencv/opencv/blob/4.x/samples/cpp/fitellipse.cpp

官网源码有一点点笔误小BUG,前一个PR还没处理完呢,这个PR不知如何操作(Git好用,难学呀)

避坑,Point2f转Point

还有,C++版的line可以传point2f(通过saturate_cast装饰float转int是四舍五入)画线,而OpenCvSharp的Line需要将Point2f转Point(通过ToPoint()的话,是直接取整)

template<> inline int saturate_cast<int>(float v)            { return cvRound(v); }
public readonly Point ToPoint() {
            return new Point((int)X, (int)Y);
        }

OpenCvSharp函数示例(目录)

参考

https://docs.opencv.org/

猜你喜欢

转载自blog.csdn.net/TyroneKing/article/details/129407646