C#-Winform 海康网络摄像机Opencv去畸变显示
- 1 标定原理及C++Opencv相机标定
- 1.1 世界坐标系->相机坐标系 三维点到三维点
- 1.2 相机坐标系->图像坐标系 三维点到二维点
- 1.3 图像坐标系->像素坐标系
- 1.4 相机畸变模型
- 1.5 C++ 相机标定
- 2 C#-Winform去畸变显示
1 标定原理及C++Opencv相机标定
相机标定:世界坐标系到像素坐标系的映射关系。
世界坐标系:代表物体在真实世界里的三维坐标。
相机坐标系:代表以相机光学中心为远点的坐标系,光轴于z轴重合。
图像坐标系:代表相机拍摄图像的坐标系,原点为相机光轴与成像平面的交点,是图像的中心。
像素坐标系:坐标原点在左上角。
1.1 世界坐标系->相机坐标系 三维点到三维点
刚性变换旋转矩阵R和平移矩阵t,在世界坐标系下的点p在相机坐标系下的位置。
1.2 相机坐标系->图像坐标系 三维点到二维点
将相机的成像模型近似为小孔模型,忽略相机镜片对成像的畸变影响,相机视场中任意一点P(Xc,Yc,Zc),与图像坐标系下坐标p(x,y)之间的关系可表达为:
其中f为相机的焦距,Zc为P在相机坐标系的纵坐标值。
1.3 图像坐标系->像素坐标系
设图像x方向上每毫米有fx个像素,y方向每毫米有fy个像素,则图像坐标系到像素坐标系的映射关系为:
1.4 相机畸变模型
一般只考虑径向畸变k和切向畸变p
1.5 C++ 相机标定
- (1)利用相机拍摄标定板图像
- (2)寻找标定板角点
- (3)获取标定结果,将标定结果保存至TXT文件
图像坐标系->像素坐标系映射矩阵
962.686 0 691.86
0 820.579 344.445
0 0 1
畸变参数(k1,k2,k3,p1,p2)
-0.375764 0.0277159 -0.00834643 -0.00988833 0.0815268
#include <opencv.hpp>
#include <string>
#include <iostream>
#include <fstream>
using namespace std;
using namespace cv;
#define PATH "img//"
#define NUM 20
void get_camera_param()
{
// 定义用来保存导入的图片
Mat image_in;
// 定义用来保存文件路径的容器
vector<Mat> rvecs, tvecs;
// 定义相机矩阵,畸变矩阵
Mat cameraMatrix;
Mat distCoeffs;
int flags = 0;
// 定义保存图像二维角点的容器
vector<Point2f> corners;
// 定义保存图像三维角点的容器
vector<vector<Point2f> > corners2;
// 定义保存图像二维和三维角点的容器
vector<Point3f> worldPoints;
vector<vector<Point3f>> worldPoints2;
//***************读取一个文件夹中的所有图片(所有标定图片)**********************
for (int j = 0; j < 7; j++) {
for (int k = 0; k < 7; k++) {
worldPoints.push_back(Point3f(j*1.0, k*1.0, 0.0f));
}
}
for (int i = 1; i <= NUM; i++) {
string str = PATH + to_string(i) + ".jpg";
image_in = imread(str);
bool found = findChessboardCorners(image_in, Size(7, 7), corners, CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE);
// 将找到的角点放入容器中;
corners2.push_back(corners);
//画出角点
drawChessboardCorners(image_in, Size(7, 7), corners, found);
//显示图像
imshow("test", image_in);
//图像刷新等待时间,单位ms
waitKey(0);
// 世界坐标系的二维vector 放入三维vector
worldPoints2.push_back(worldPoints);
cout << str << endl;
}
calibrateCamera(worldPoints2, corners2, image_in.size(), cameraMatrix, distCoeffs, rvecs, tvecs);
//*************************************保存参数至txt*****************************************
ofstream outfile("CameraParam.txt");
cout << cameraMatrix.type() << endl;
outfile << cameraMatrix.at<double>(0, 0) << " " << cameraMatrix.at<double>(0, 1) << " " << cameraMatrix.at<double>(0, 2) << endl;
outfile << cameraMatrix.at<double>(1, 0) << " " << cameraMatrix.at<double>(1, 1) << " " << cameraMatrix.at<double>(1, 2) << endl;
outfile << cameraMatrix.at<double>(2, 0) << " " << cameraMatrix.at<double>(2, 1) << " " << cameraMatrix.at<double>(2, 2) << endl;
for (int i = 0; i < distCoeffs.cols; i++)
{
outfile << distCoeffs.at<double>(0, i) << " ";
}
outfile.close();
}
2 C#-Winform去畸变显示
- (1) 搭建C# OpencvSharp环境。
- (2) 使用Opencv的VideoCapture对接海康威视网络摄像机的rtsp视频流。
- (3) 读取TXT文件获取相机标定生成的相机变换矩阵和畸变参数
- (4) 开启一个子线程读取视频流,使用Opencv的Undistort方法去畸变,然后用PictureBox显示,整个去畸变算法时间为30ms,海康网络摄像机的最大采样频率为25帧/秒,去畸变显示和用SDK显示实时性上相差无几。
- (5) 加载实时录像和抓图功能,视频和图像均已去畸变,可与海康SDK的摄像机位移控制等功能结合使用。
using System;
using System.Drawing;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;
using System.IO;
using System.Threading;
namespace Delete_Camera_Distortion_CSharp
{
public partial class Form1 : Form
{
Mat cameraMatrix = new Mat(3, 3, MatType.CV_64F);//相机外参矩阵
Mat distCoeffs = new Mat(1, 5, MatType.CV_64F);//相机内参即畸变系数
Thread thread;//开辟新线程利用rtsp读取视频流
bool keep_video = false;//判断是否需要实时录像
bool keep_image = false;//判断是否需要抓图
VideoWriter writer;//保存录像视频流对象
VideoCapture capture1;//opencv视频流对象
public Form1()
{
InitializeComponent();
get_txt_info();
}
/// <summary>
/// 读取TXT文件的相机矩阵和畸变系数
/// </summary>
private void get_txt_info()
{
int rows = 0;
try
{
using (StreamReader sr = new StreamReader("CameraParam.txt"))
{
string strline;
// 从文件读取并显示行,直到文件的末尾
while ((strline = sr.ReadLine()) != null)
{
string[] line = System.Text.RegularExpressions.Regex.Replace(strline.Trim(), @"[\s]+", " ").Split(" ".ToCharArray());
if (rows < 3)
{
for (int cols = 0; cols < line.Length; cols++)
{
cameraMatrix.At<double>(rows, cols) = Convert.ToDouble(line[cols]);
}
rows++;
}
else
{
for (int cols = 0; cols < 5; cols++)
{
distCoeffs.At<double>(0, cols) = Convert.ToDouble(line[cols]);
}
}
}
}
}
catch (Exception e)
{
Console.WriteLine("The file could not be read:");
Console.WriteLine(e.Message);
}
}
/// <summary>
/// 子线程读网络摄像机视频流
/// </summary>
private void MyThread()
{
string url = "rtsp://admin:123456@192.168.1.64:554/Streaming/Channels/101?transportmode=unicast";
capture1 = new VideoCapture(url);
while(capture1.IsOpened())
{
while (!keep_video&&!keep_image)
{
Mat image = new Mat();
bool ret = capture1.Read(image);
if (ret)
{
Bitmap bit_img = image.ToBitmap();
this.pictureBox1.Image = bit_img;
Mat result = new Mat();
Cv2.Undistort(image, result, cameraMatrix, distCoeffs);
string Text = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss");
Cv2.PutText(result, Text, new OpenCvSharp.Point(40, 70), HersheyFonts.HersheyPlain, 3.3, new Scalar(0, 0, 255), 4);
Bitmap bit_img1 = result.ToBitmap();
this.pictureBox2.Image = bit_img1;
}
image.Release();
System.GC.Collect();
}
while (keep_video)
{
Mat image = new Mat();
bool ret = capture1.Read(image);
if (ret)
{
Bitmap bit_img = image.ToBitmap();
this.pictureBox1.Image = bit_img;
Mat result = new Mat();
Cv2.Undistort(image, result, cameraMatrix, distCoeffs);
string Text = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss");
Cv2.PutText(result, Text, new OpenCvSharp.Point(40, 70), HersheyFonts.HersheyPlain, 3.3, new Scalar(0, 0, 255), 5);
Bitmap bit_img1 = result.ToBitmap();
this.pictureBox2.Image = bit_img1;
writer.Write(result);
}
image.Release();
System.GC.Collect();
}
while (keep_image)
{
Mat image = new Mat();
bool ret = capture1.Read(image);
if (ret)
{
Bitmap bit_img = image.ToBitmap();
this.pictureBox1.Image = bit_img;
Mat result = new Mat();
Cv2.Undistort(image, result, cameraMatrix, distCoeffs);
Bitmap bit_img1 = result.ToBitmap();
string Text = DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss");
Cv2.PutText(result, Text, new OpenCvSharp.Point(40, 70), HersheyFonts.HersheyPlain, 3.3, new Scalar(255, 255, 255), 4);
this.pictureBox2.Image = bit_img1;
string path = "..//image//" + DateTime.Now.ToString("yyyy-MM-dd--hh-mm-ss") + ".jpg";
Cv2.ImWrite(path, result);
keep_image = !keep_image;
}
image.Release();
System.GC.Collect();
}
}
}
/// <summary>
/// 开始预览按钮点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click_1(object sender, EventArgs e)
{
Control.CheckForIllegalCrossThreadCalls = false;
thread = new Thread(new ThreadStart(MyThread));
thread.IsBackground = true;
thread.Start();
button1.Enabled = false;
button2.Enabled = true;
}
/// <summary>
/// 结束预览按钮点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button2_Click_1(object sender, EventArgs e)
{
thread.Abort();
while (thread.ThreadState != ThreadState.Aborted)
{
Thread.Sleep(100);
}
button1.Enabled = true;
button2.Enabled = false;
this.pictureBox1.Refresh();
this.pictureBox2.Refresh();
}
/// <summary>
/// 开始实时录像按钮点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button4_Click(object sender, EventArgs e)
{
keep_video = !keep_video;
writer = new VideoWriter();
string path = @"..//video//" + DateTime.Now.ToString("yyyy-MM-dd--hh-mm-ss") +".mp4";
Console.WriteLine(path);
writer.Open(path, FourCC.AVC, 20, new OpenCvSharp.Size(1280, 720), true);
button4.Enabled = false;
button3.Enabled = true;
}
/// <summary>
/// 结束实时录像点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button3_Click(object sender, EventArgs e)
{
keep_video = !keep_video;
writer.Release();
button4.Enabled = true;
button3.Enabled = false;
}
/// <summary>
/// 抓图按钮点击事件
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button6_Click(object sender, EventArgs e)
{
keep_image = true;
}
}
}