- 使用的是VS调用OpenCvSharp资源库进行一个Winform操作界面编写,网上找了很多开源的程序,发现根本用不了的,用的时候还需要你配置各种电脑系统变量,显得好麻烦。现在弄了个简单的标定助手,可以完美运行,带有棋盘格图像生成工具,操作简单,源码也不复杂。
这里是
using OpenCvSharp;//需要引用OpenCvSharp
using OpenCvSharp.Extensions;
using Size = OpenCvSharp.Size;
public Mat ChessBoardMat;
/// <summary>
/// 生成棋盘格图像
/// </summary>
/// <param name="BoardSize">输入棋盘格角点数量的大小</param>
/// <param name="ImagePixel">输入棋盘格的像素大小</param>
/// <returns></returns>
public Bitmap GenChessBoard(OpenCvSharp.Size BoardSize, OpenCvSharp.Size ImagePixel)
{
int perBoardPixel = ImagePixel.Height / BoardSize.Height;
int basisHeight = (ImagePixel.Height - perBoardPixel * BoardSize.Height) / 2;
int basisWidth = (ImagePixel.Width - perBoardPixel * BoardSize.Width) / 2;
if (basisHeight < 0 || basisWidth < 0)
{
return null;
}
ChessBoardMat = new Mat(ImagePixel, MatType.CV_8UC1, Scalar.All(255));
int flag;
for (int j = 0; j < BoardSize.Height; j++)
{
for (int i = 0; i < BoardSize.Width; i++)
{
flag = (i + j) % 2;
if (flag == 0)
{
for (int n = j * perBoardPixel; n < (j + 1) * perBoardPixel; n++)
for (int m = i * perBoardPixel; m < (i + 1) * perBoardPixel; m++)
ChessBoardMat.At<byte>(n + basisHeight, m + basisWidth) = 0;
}
}
}
return BitmapConverter.ToBitmap(ChessBoardMat);//返回棋盘格图像结果
//Cv2.ImWrite("chessBoard1.bmp", image);//这里是保存生成的图像,可以不使用
}
结果如下:
点击“导入图像”后,选择采集的棋盘格图像所在文件夹,结果如下:
可以在图像列表看到导入结果,在参数设置那里设置正确的棋盘格角点数量后,在图像列表双击图像路径,可以实现棋盘格图像角点的提取显示:
可以提取角点之后就可以标定啦!
标定完成后,可以进行畸变矫正,这个是进行畸变矫正的结果,在这里并没有写保存矫正结果的代码。
所以只是看看就好
然后所有结果都出来了,并且自动保存结果在图像文件里内!
这里是核心源码,至于Winform的操作代码,这里就不放了。感兴趣的朋友可以去下载看看
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using Size = OpenCvSharp.Size;
namespace CvCalibrate
{
public class Calib_Class
{
public Mat ChessBoardMat;
#region 初始化变量
/*内外参数*/
Mat cameraMatrix = new Mat(3, 3, MatType.CV_64FC1, Scalar.All(0)); /* 摄像机内参数矩阵 */
List<int> point_counts = new List<int>(); // 每幅图像中角点的数量
Mat distCoeffs = new Mat(1, 12, MatType.CV_64FC1, Scalar.All(0)); /* 摄像机的5个畸变系数:k1,k2,p1,p2,k3 */ /*加上薄棱镜畸变模型,启用畸变系数S1、S2、S3和S4*/
List<Mat> TranslationMats = new List<Mat>(); /* 每幅图像的旋转向量 */
List<Mat> RotationMats = new List<Mat>(); /* 每幅图像的平移向量 */
/*Mat数据类型转换double的结果*/
public double[,] CameraParameterArray = new double[3, 3];//摄像机内参数矩阵
public double[] DistCoeffsArray = new double[12];//摄像机的5个畸变系数:k1,k2,p1,p2,k3 /*加上薄棱镜畸变模型,启用畸变系数S1、S2、S3和S4*/
public Vec3d[] Rotation; //存放所有图像的3*1旋转向量,每一副图像的旋转向量为一个mat
public Vec3d[] Translation;//存放所有图像的3*1平移向量,每一副图像的平移向量为一个mat
public double MeanError = 0.0; /* 所有图像的平均误差的总和 */
public List<double> TotalError = new List<double>(); /* 每幅图像的平均误差 */
public Size BoardImageSize = new Size();
#endregion
/// <summary>
/// 角点提取,返回Bitmap数据
/// </summary>
/// <param name="imageInput">图像输入</param>
/// <param name="BoardSize">棋盘格角点数量大小</param>
/// <returns></returns>
public Bitmap FindChessboardCorners(Image imageInput, OpenCvSharp.Size BoardSize)
{
try
{
Point2f[] image_points_buf; /* 缓存每幅图像上检测到的角点 */
List<Point2f[]> image_points_seq = new List<Point2f[]>(); /* 保存检测到的所有角点 */
Mat image = BitmapConverter.ToMat(new Bitmap(imageInput));
/* 提取角点 */
Cv2.FindChessboardCorners(image, BoardSize, out image_points_buf);
Mat view_gray = new Mat();
Cv2.CvtColor(image, view_gray, ColorConversionCodes.RGB2GRAY);
/* 亚像素精确化 */
Point2f[] SubPix_points = Cv2.CornerSubPix(view_gray, image_points_buf, new Size(3, 3), new Size(-1, -1), TermCriteria.Both(30, 0.1));
image_points_seq.Add(SubPix_points); //保存亚像素角点
/* 在图像上显示角点位置 */
Cv2.DrawChessboardCorners(view_gray, BoardSize, image_points_buf, true); //用于在图片中标记角点
return BitmapConverter.ToBitmap(view_gray);
}
catch (Exception)
{
return new Bitmap(imageInput);
}
}
public double Fovx/*沿水平传感器轴的视野*/, Fovy/*沿竖直传感器轴的视野*/, FocalLength/*透镜焦距mm*/, AspectRatio/*fy/fx*/;
public Point2d Principal/*主点坐标mm*/;
/// <summary>
/// 运行标定
/// </summary>
/// <param name="ChessBoardFiles">棋盘格图像文件路径</param>
/// <param name="BoardNum">棋盘格角点数量大小</param>
/// <param name="ErrorMessage">返回判断的消息</param>
/// <returns></returns>
public bool Calibrate(List<string> ChessBoardFiles, Size BoardNum, string ErrorMessage)
{
int image_count = 0; /* 图像数量 */
Size image_size = new Size(); /* 图像的尺寸 */
if (ChessBoardFiles.Count() > 0)
{
#region 角点提取
Point2f[] image_points_buf; /* 缓存每幅图像上检测到的角点 */
List<Point2f[]> image_points_seq = new List<Point2f[]>(); /* 保存检测到的所有角点 */
/*读取每一幅图像,从中提取出角点,然后对角点进行亚像素精确化*/
for (int i = 0; i < ChessBoardFiles.Count(); i++)
{
try
{
Mat imageInput = Cv2.ImRead(ChessBoardFiles[i]);
image_count++;
if (image_count == 1) //读入第一张图片时获取图像宽高信息
{
BoardImageSize.Width = image_size.Width = imageInput.Cols;
BoardImageSize.Height = image_size.Height = imageInput.Rows;
}
image_points_buf = FindChessboardCorners(imageInput, BoardNum, ErrorMessage);
if (image_points_buf.Count() > 0)
{
image_points_seq.Add(image_points_buf);
}
else
{
ErrorMessage += "图 " + i + ", ";
}
}
catch (Exception Err)
{
ErrorMessage = " 角点提取失败!" + "\n\r" + Err.Message;
return false;
}
}
#endregion
#region 标定板初始化
//棋盘三维信息
// 初始化标定板上角点的三维坐标
//生成一个标准的标定板角点坐标集合
Size square_size = new Size(20, 20); //初始化标定板上每个棋盘格的大小
List<List<Point3f>> object_points = new List<List<Point3f>>(); // 保存标定板上角点的三维坐标
if (BoardNum.Width > 0)
{
try
{
for (int t = 0; t < image_count; t++)
{
List<Point3f> tempPointSet = new List<Point3f>();
for (int i = 0; i < BoardNum.Height; i++)
{
for (int j = 0; j < BoardNum.Width; j++)
{
Point3f realPoint;
// 假设标定板放在世界坐标系中z=0的平面上
realPoint.X = i * square_size.Width;
realPoint.Y = j * square_size.Height;
realPoint.Z = 0;
tempPointSet.Add(realPoint);
}
}
object_points.Add(tempPointSet);
// 初始化每幅图像中的角点数量,假定每幅图像中都可以看到完整的标定板
point_counts.Add(BoardNum.Width * BoardNum.Height);
}
}
catch (Exception Err)
{
ErrorMessage = "初始化棋盘格失败!" + "\n\r" + Err.Message;
}
}
else
{
ErrorMessage = "棋盘格大小设置错误!";
return false;
}
#endregion
#region 开始标定
try
{
/*薄棱镜畸变模型,启用畸变系数S1、S2、S3和S4*/
/*迭代标准*/
TermCriteria criteria = new TermCriteria(CriteriaTypes.Eps | CriteriaTypes.MaxIter, 30, 0.01);
Cv2.CalibrateCamera(object_points, image_points_seq, image_size, CameraParameterArray, DistCoeffsArray, out Rotation, out Translation, CalibrationFlags.ThinPrismModel, criteria);
}
catch (Exception Err)
{
ErrorMessage = "标定失败!" + "\n\r" + Err.Message;
return false;
}
#endregion
#region 数据类型转换
for (int r = 0; r < CameraParameterArray.GetLength(0); r++)
{
for (int c = 0; c < CameraParameterArray.GetLength(1); c++)
{
cameraMatrix.At<double>(r, c) = CameraParameterArray[r, c];
}
}
for (int r = 0; r < DistCoeffsArray.Length; r++)
{
distCoeffs.At<double>(0, r) = DistCoeffsArray[r];
}
for (int r = 0; r < Rotation.Length; r++)
{
Mat TempRotation = new Mat(3, 1, MatType.CV_64FC1);
TempRotation.At<double>(0, 0) = Rotation[r].Item0;
TempRotation.At<double>(1, 0) = Rotation[r].Item1;
TempRotation.At<double>(2, 0) = Rotation[r].Item2;
RotationMats.Add(TempRotation);
Mat TempTrans = new Mat(3, 1, MatType.CV_64FC1);
TempTrans.At<double>(0, 0) = Translation[r].Item0;
TempTrans.At<double>(1, 0) = Translation[r].Item1;
TempTrans.At<double>(2, 0) = Translation[r].Item2;
TranslationMats.Add(TempTrans);
}
#endregion
#region 重新投影计算,误差计算
double total_err = 0.0; // 所有图像的平均误差的总和
double Error; //每幅图像的平均误差
for (int i = 0; i < image_count; i++)
{
try
{
List<Point3f> tempPointSet = object_points[i];
/* 通过得到的摄像机内外参数,对空间的三维点进行重新投影计算,得到新的投影点 */
Mat NewProjectPoints = new Mat();/* 保存重新计算得到的投影点 */
Mat jacobian = new Mat();//雅可比矩阵
Cv2.ProjectPoints(InputArray.Create<Point3f>(tempPointSet), RotationMats[i], TranslationMats[i], cameraMatrix, distCoeffs, NewProjectPoints, jacobian, 0);
List<Point2f> NewProjectPointsToPoint2f = new List<Point2f>(); /* 保存重新计算得到的投影点,格式转换 */
{
string[] PointsArray = Cv2.Format(NewProjectPoints, FormatType.CSV).Split('\n');//分割出来最后一个回车符号占了一个空间
for (int r = 0; r < PointsArray.Length - 1; r++)
{
Point2f Temp = new Point2f();//临时缓存
string[] PointTemp = PointsArray[r].Split(',');
Temp.X = float.Parse(PointTemp[0]);
Temp.Y = float.Parse(PointTemp[1]);
NewProjectPointsToPoint2f.Add(Temp);
}
}
/* 计算新的投影点和旧的投影点之间的误差*/
Point2f[] OldProjectPoints = image_points_seq[i];
Mat tempImagePointMat = new Mat(1, OldProjectPoints.Length, MatType.CV_32FC2);
Mat image_points2Mat = new Mat(1, NewProjectPoints.Rows, MatType.CV_32FC2);
for (int j = 0; j < OldProjectPoints.Count(); j++)
{
image_points2Mat.At<Vec2f>(0, j) = new Vec2f(NewProjectPointsToPoint2f[j].X, NewProjectPointsToPoint2f[j].Y);
tempImagePointMat.At<Vec2f>(0, j) = new Vec2f(OldProjectPoints[j].X, OldProjectPoints[j].Y);
}
Error = Cv2.Norm(image_points2Mat, tempImagePointMat, NormTypes.L2);//当前图像的投影误差
total_err += Error * Error;
TotalError.Add(Error);
MeanError += Error /= point_counts[i];//总的平均误差
double 重投影误差3 = (total_err / image_count);
}
catch (Exception Err)
{
ErrorMessage = "误差计算失败!" + "\n\r" + Err.Message;
return false;
}
}
#endregion
#region 旋转矩阵计算
List<Mat> AllRotation_Matrix = new List<Mat>(); /* 保存每幅图像的旋转矩阵 */
for (int i = 0; i < image_count; i++)
{
try
{
Mat rotation_matrix = new Mat(3, 3, MatType.CV_64FC1, Scalar.All(0));
/* 将旋转向量转换为相对应的旋转矩阵 */
/*Rodrigues()可以将旋转向量转化为旋转矩阵,也可以将旋转矩阵转化为旋转向量。旋转向量指定了旋转轴,同时它的模长也指定了旋转角度。*/
Cv2.Rodrigues(TranslationMats[i], rotation_matrix);
AllRotation_Matrix.Add(rotation_matrix);
}
catch (Exception Err)
{
ErrorMessage = "旋转矩阵计算失败!" + "\n\r" + Err.Message;
return false;
}
}
#endregion
//Mat Rvec = new Mat();
//Mat Tvec = new Mat();
//Cv2.SolvePnP(InputArray.Create<Point3f>(object_points[0]), InputArray.Create<Point2f>(image_points_seq[0]), cameraMatrix, distCoeffs, Rvec, Tvec);
//double[] R = MatToDouble(Rvec);
//double[] T = MatToDouble(Tvec);
Cv2.CalibrationMatrixValues(cameraMatrix, BoardImageSize, 5.3, 7.2, out Fovx, out Fovy, out FocalLength, out Principal, out AspectRatio);
}
else
{
ErrorMessage = "输入图像路径为空!";
return false;
}
return true;
}
private delegate Point2f[] FindCornersDelegate(Mat Src, Size BoardNum, string ErrorMessage);
/// <summary>
/// 角点提取
/// </summary>
/// <param name="Src">图源</param>
/// <param name="BoardNum">棋盘格角点数量大小</param>
/// <param name="ErrorMessage">返回判断的消息</param>
/// <returns></returns>
private static Point2f[] FindChessboardCorners(Mat Src, Size BoardNum, string ErrorMessage)
{
Point2f[] image_points_buf = new Point2f[BoardNum.Height * BoardNum.Width]; /* 缓存每幅图像上检测到的角点 */
Point2f[] SubPix_points = new Point2f[BoardNum.Height * BoardNum.Width];
try
{
/* 提取角点 */
if (!Cv2.FindChessboardCorners(Src, BoardNum, out image_points_buf))
{
//粗提取角点,若无法提取则返回失败,反之进行精细化提取
ErrorMessage = " 角点提取失败!";
}
else
{
Mat view_gray = new Mat();
Cv2.CvtColor(Src, view_gray, ColorConversionCodes.RGB2GRAY);
SubPix_points = Cv2.CornerSubPix(view_gray, image_points_buf, new Size(3, 3), new Size(-1, -1), TermCriteria.Both(30, 0.1));//对粗提取的角点进行精确化
//Cv2.Find4QuadCornerSubpix(view_gray, image_points_buf, new Size(5, 5)); //对粗提取的角点进行精确化
}
}
catch (Exception Err)
{
ErrorMessage = Err.Message;
}
return SubPix_points;
}
#region 类型转换
static double[] MatToDouble(Mat InMat)
{
if (InMat.Rows >= 1 && InMat.Cols == 1)
{
double[] OutDouble = new double[InMat.Rows];
for (int i = 0; i < InMat.Rows; i++)
{
OutDouble[i] = InMat.At<double>(i);
}
return OutDouble;
}
else if (InMat.Rows == 1 && InMat.Cols >= 1)
{
double[] OutDouble = new double[InMat.Cols];
for (int i = 0; i < InMat.Cols; i++)
{
OutDouble[i] = InMat.At<double>(i);
}
return OutDouble;
}
else
{
return null;
}
}
static double[,] MatToDouble2D(Mat InMat)
{
if (InMat.Rows >= 1 && InMat.Cols >= 1)
{
double[,] OutDouble = new double[InMat.Rows, InMat.Cols];
for (int i = 0; i < InMat.Rows; i++)
{
for (int j = 0; j < InMat.Cols; j++)
{
OutDouble[i, j] = InMat.At<double>(i, j);
}
}
return OutDouble;
}
else
{
return null;
}
}
static Mat DoubleToMat(double[] InDouble)
{
Mat OutMat = new Mat(1, InDouble.Length, MatType.CV_64FC1);
for (int r = 0; r < InDouble.Length; r++)
{
OutMat.At<double>(0, r) = InDouble[r];
}
return OutMat;
}
static Mat Double2DToMat(double[,] InDouble)
{
Mat OutMat = new Mat(InDouble.GetLength(0), InDouble.GetLength(1), MatType.CV_64FC1);
for (int r = 0; r < InDouble.GetLength(0); r++)
{
for (int c = 0; c < InDouble.GetLength(1); c++)
{
OutMat.At<double>(r, c) = InDouble[r, c];
}
}
return OutMat;
}
#endregion
}
}
付费资源,各取所需吧,花了时间和心思做出来的东西让我免费共享是不可能的。
项目资源下载
对了,下载的源码里面有畸变矫正功能的,但是参数那些暂时没设置正确,所以矫正出来的图像似乎没那么准确,有兴趣的朋友可以去研究下,其实所有的代码都是可以参考OpenCv的,只不过是放在了C#下,有些变量类型略有不同,具体怎么样看看它的类就知道了。