函数说明:基于最小二乘法(least-squares sense)计算围绕一组(个数大于等于5个)给定的点集拟合一个椭圆。返回该椭圆的最小外接矩形(如果给定的点是在一条直线上,该矩形的最小边为0)。注意返回的数值可能有负数(大边界之外)。
//函数原型1
RotatedRect FitEllipse(InputArray points)
//函数原型2
RotatedRect FitEllipse(IEnumerable points)
//函数原型3
RotatedRect FitEllipse(IEnumerable points)
参数 |
说明 |
InputArray points IEnumerable IEnumerable |
待拟合椭圆的点集,个数大于等于5个。 |
返回值RotatedRect |
拟合的椭圆的最小外接矩形,点在一直线上的话,该矩形最小边为0 |
函数说明:基于Approximate Mean Square(近似均方) 方法计算点集的拟合椭圆
//函数原型1
RotatedRect FitEllipseAMS(InputArray points)
//函数原型2
RotatedRect FitEllipseAMS(IEnumerable points)
//函数原型3
RotatedRect FitEllipseAMS(IEnumerable points)
函数说明:基于Direct least square(最小二乘法) 方法计算点集的拟合椭圆
//函数原型1
RotatedRect FitEllipseDirect(InputArray points)
//函数原型2
RotatedRect FitEllipseDirect(IEnumerable points)
//函数原型3
RotatedRect FitEllipseDirect(IEnumerable points)
FitEllipse:据说是,速度快些,但结果不一定是最佳的(如图像示例中的大蓝色框)
FitEllipseAMS:速度慢些,但结果更优。
AMS和Direct算法看不懂,有能讲明白的高人或简明的参考的链接,望评论区被补上,感谢。
函数说明:获取一组坐标点集的最小外接圆,返回中心点和半径。
//函数原型1
void MinEnclosingCircle(InputArray points,
out Point2f center,
out float radius)
//函数原型2
void MinEnclosingCircle(IEnumerable points,
out Point2f center,
out float radius)
//函数原型3
void MinEnclosingCircle(IEnumerable points,
out Point2f center,
out float radius)
参数 |
说明 |
InputArray points IEnumerable IEnumerable |
待计算最小外接圆的点集。数据类型必须为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 text = new List();
List color = new List();
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> points = new List>();
for (int i = 0; i < contours.Length; i++) {
int count = contours[i].Length;
//椭圆拟合至少要5个坐标点
if (count < 6) continue;
List pts = new List();
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() { 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() { 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(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(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好用,难学呀)
还有,C++版的line可以传point2f(通过saturate_cast装饰float转int是四舍五入)画线,而OpenCvSharp的Line需要将Point2f转Point(通过ToPoint()的话,是直接取整)
template<> inline int saturate_cast(float v) { return cvRound(v); }
public readonly Point ToPoint() {
return new Point((int)X, (int)Y);
}
OpenCvSharp函数示例(目录)
参考
https://docs.opencv.org/