由于需要實(shí)現(xiàn)一個(gè)物體的測(cè)量,但是已有QT程序,最后的整體功能需要在C#集成實(shí)現(xiàn)。
首先有兩個(gè)方案:(1)利用已有的QT程序以及界面,直接在C#中調(diào)用QT,或者C++程序,但是經(jīng)過(guò)嘗試,發(fā)現(xiàn)兩者之間進(jìn)行調(diào)用不是那么的簡(jiǎn)單,涉及到許多變量定義的不用以及數(shù)據(jù)結(jié)構(gòu)的不同。因此決定方案(2),在C#里重新實(shí)現(xiàn)該功能。
由于也是第一次接觸相機(jī)的使用,因此就借此記錄一下。
一、首先是相機(jī)的標(biāo)定,這個(gè)很簡(jiǎn)單,也有大量的相關(guān)參考:
相機(jī)標(biāo)定(一)——內(nèi)參標(biāo)定與程序?qū)崿F(xiàn)_相機(jī)內(nèi)參標(biāo)定_white_Learner的博客-CSDN博客相機(jī)標(biāo)定(一)——內(nèi)參標(biāo)定與程序?qū)崿F(xiàn)相機(jī)標(biāo)定(二)——圖像坐標(biāo)與世界坐標(biāo)轉(zhuǎn)換相機(jī)標(biāo)定(三)——手眼標(biāo)定一、張正友標(biāo)定算法實(shí)現(xiàn)流程1.1 準(zhǔn)備棋盤(pán)格備注:棋盤(pán)格黑白間距已知,可采用打印紙或者購(gòu)買(mǎi)黑白棋盤(pán)標(biāo)定板(精度要求高)1.2 針對(duì)棋盤(pán)格拍攝若干張圖片此處分兩種情況(1)標(biāo)定畸變系數(shù)和相機(jī)內(nèi)參,拍攝照片需要包含完整棋盤(pán),同時(shí)需要不同距離,不同方位...https://blog.csdn.net/Kalenee/article/details/80672785
OpenCvSharp 棋盤(pán)格標(biāo)定助手_opencvsharp 標(biāo)定_YT - Chow的博客-CSDN博客使用的是VS調(diào)用OpenCvSharp資源庫(kù)進(jìn)行一個(gè)Winform操作界面編寫(xiě),網(wǎng)上找了很多開(kāi)源的程序,發(fā)現(xiàn)根本用不了的,用的時(shí)候還需要你配置各種電腦系統(tǒng)變量,顯得好麻煩?,F(xiàn)在弄了個(gè)簡(jiǎn)單的標(biāo)定助手,可以完美運(yùn)行,帶有棋盤(pán)格圖像生成工具,操作簡(jiǎn)單,源碼也不復(fù)雜。使用了OpenCvSharp資源開(kāi)發(fā)包,在VS下做了一個(gè)棋盤(pán)格圖像下的相機(jī)標(biāo)定助手小Demo,顯然,C#也可以用OpenCv了。這是一個(gè)比較好的案例,可以參考下。鄙人不才,也用它做了一個(gè)SFM三維重建的Demo,這里就不放了。using Op.https://blog.csdn.net/Yoto_Jo/article/details/117574528?utm_medium=distribute.pc_feed_404.none-task-blog-2~default~BlogCommendFromBaidu~Rate-11-117574528-blog-null.pc_404_mixedpudn&depth_1-utm_source=distribute.pc_feed_404.none-task-blog-2~default~BlogCommendFromBaidu~Rate-11-117574528-blog-null.pc_404_mixedpud
因?yàn)镺pencvSharp本來(lái)也是由Opencv封裝成的C#的動(dòng)態(tài)鏈接庫(kù),因此本質(zhì)上使用的方法和基本的函數(shù)實(shí)現(xiàn)是差不多的,可以直接參考C++版本的相機(jī)標(biāo)定。只需要注意個(gè)人使用的時(shí)候修改標(biāo)定格的具體物理大小和標(biāo)定格的個(gè)數(shù)。
二、像素坐標(biāo)轉(zhuǎn)世界坐標(biāo)
當(dāng)標(biāo)定好相機(jī)后,就需要實(shí)現(xiàn)如何通過(guò)像素坐標(biāo)轉(zhuǎn)到世界坐標(biāo),理論和實(shí)現(xiàn)過(guò)程可以參考C++版本
相機(jī)標(biāo)定(二)——圖像坐標(biāo)與世界坐標(biāo)轉(zhuǎn)換_標(biāo)定將圖像坐標(biāo)轉(zhuǎn)換成世界坐標(biāo)_white_Learner的博客-CSDN博客因本文存在錯(cuò)誤與模糊之處,為此進(jìn)行重寫(xiě)修改,但因CSDN不支持富文本轉(zhuǎn)換Markdown,所以重寫(xiě)發(fā)布新的博文:https://blog.csdn.net/Kalenee/article/details/99207102一、坐標(biāo)變換詳解1.1 坐標(biāo)關(guān)系相機(jī)中有四個(gè)坐標(biāo)系,分別為world,camera,image,pixelworld為世界坐標(biāo)系,可以任意指定軸和軸,...https://blog.csdn.net/Kalenee/article/details/80659489
public static Point3d GetWorldPoints(Point2f inPoints)
{
double s;
int zConst = 0;
Mat r = new Mat(3, 3, MatType.CV_64FC1);
Mat t = new Mat(3, 1, MatType.CV_64FC1);
using (var fs1 = new FileStorage("RT.yaml", FileStorage.Mode.Read))
{
r = (Mat)fs1["R"];
t = (Mat)fs1["t"];
}
Mat R_invert = new Mat(3, 3, MatType.CV_64FC1);
Mat cameraMatrix_invert = new Mat(3, 3, MatType.CV_64FC1);
Mat imagePoint = new Mat(3, 1, MatType.CV_64FC1);
imagePoint.At<double>(0, 0) = inPoints.X;
imagePoint.At<double>(1, 0) = inPoints.Y;
imagePoint.At<double>(2, 0) = 1;
Mat cameraMatrix = new Mat(3, 3, MatType.CV_64FC1, new double[3, 3] {
{ 1473.819, 0, 615.859 },
{ 0, 1474.14, 467.697 },
{ 0, 0, 1 } });
Cv2.Invert(r, R_invert, DecompTypes.SVD);
Cv2.Invert(cameraMatrix, cameraMatrix_invert, DecompTypes.SVD);
Mat tempMat = R_invert * cameraMatrix_invert * imagePoint;
Mat tempMat2 = R_invert * t;
s = zConst + tempMat2.At<double>(2, 0);
s /= tempMat.At<double>(2, 0);
//計(jì)算世界坐標(biāo)
Mat wcPoint;
wcPoint = R_invert * ( cameraMatrix_invert*s * imagePoint - t);
Point3d world;
world.X = wcPoint.At<double>(0, 0);
world.Y = wcPoint.At<double>(1, 0);
world.Z = wcPoint.At<double>(2, 0);
//Point3f worldPoint(wcPoint.at<double>(0, 0), wcPoint.at<double>(1, 0), wcPoint.at<double>(2, 0));
return world;
}
?可以看出來(lái)幾乎用C++的版本就能實(shí)現(xiàn)。
這里需要注意的是Cv2.Invert()矩陣求逆的函數(shù),他的第三個(gè)參數(shù)?DecompTypes的不同,求逆的方法也不同。但是我自己的測(cè)試得到的結(jié)果卻只有?DecompTypes.SVD這種方法比較準(zhǔn)確,不知道各位有沒(méi)有什么經(jīng)驗(yàn)對(duì)于求逆的方法選擇。
然而在函數(shù)寫(xiě)好后測(cè)試像素坐標(biāo)到世界坐標(biāo)卻始終達(dá)不到要求,經(jīng)過(guò)師兄指導(dǎo)是因?yàn)樽约哼@里用到的外參(旋轉(zhuǎn)矩陣和平移向量)是不對(duì)的,原因在于我是直接用的Cv2.CalibrateCamera()求解得到的外參進(jìn)行計(jì)算的,而我在實(shí)驗(yàn)的時(shí)候拍攝條件已經(jīng)變了,當(dāng)時(shí)的外參已經(jīng)不適用了。因而在求解之前還需要得到當(dāng)前相機(jī)的外參。犯這個(gè)錯(cuò)誤也是沒(méi)有整體掌握好對(duì)于整個(gè)實(shí)現(xiàn)過(guò)程的理解,沒(méi)有對(duì)相機(jī)坐標(biāo)轉(zhuǎn)換理論的理解,導(dǎo)致外參與物體的對(duì)應(yīng)有問(wèn)題,因此下面就是求解當(dāng)前相機(jī)的外參。
三、外參求解
由于需要通過(guò)已知的物體像素坐標(biāo)轉(zhuǎn)換為世界坐標(biāo),因此就需要該相機(jī)和該物體此時(shí)的位置姿態(tài)的變化。因?yàn)榍蠼獬鐾鈪⒕湍軌蚍赐瞥鑫矬w的實(shí)際世界坐標(biāo)。
需要注意的是,我的實(shí)際環(huán)境是相機(jī)處于不動(dòng)的情況,即相機(jī)始終與地面保持平行,固定相機(jī)的位置。因此若需要在相機(jī)運(yùn)動(dòng)或者相對(duì)位置隨時(shí)發(fā)生改變的情況下使用,需要有所修改。
所用到的函數(shù)也很簡(jiǎn)單,用到的是OpencvSharp自帶的SolvePnP進(jìn)行求解
?從函數(shù)的參數(shù)中可以看到,需要我們準(zhǔn)備的有objectPoints, imagePoints, Intrinsic, distCoeffs, rvec, tvec,? SolvePnPFlags,我認(rèn)為這幾個(gè)是比較關(guān)鍵的參數(shù)。
首先是objectPoints:需要提供一組世界坐標(biāo)
? ? ? ? ? imagePoints:需要提供一組與世界坐標(biāo)對(duì)應(yīng)的像素坐標(biāo)
? ? ? ? ? Intrinsic, distCoeffs:相機(jī)的內(nèi)參和畸變系數(shù)
? ? ? ? ? rvec, tvec:輸出一個(gè)旋轉(zhuǎn)向量和平移向量,通常情況下旋轉(zhuǎn)向量需要用Cv2.Rodrigues()轉(zhuǎn)換為旋轉(zhuǎn)矩陣
? ? ? ? ??SolvePnPFlags:求解的方法選擇,不同參數(shù)選擇要求的輸入點(diǎn)也有所要求,可以參考相關(guān)文章進(jìn)行選擇選擇
這里對(duì)于objectPoints和imagePoints點(diǎn)的取值我用了兩種方法。
其一是利用標(biāo)定紙,通過(guò)程序?qū)ふ覙?biāo)定紙的角點(diǎn)坐標(biāo),然后在用尺子實(shí)際測(cè)出各個(gè)角點(diǎn)的世界坐標(biāo),這里需要注意的是對(duì)于世界坐標(biāo)的X,Y軸的選取,以角點(diǎn)最左上角為原點(diǎn),向右形成X軸,向下形成Y軸。一定要注意世界坐標(biāo)點(diǎn)與像素坐標(biāo)點(diǎn)的對(duì)應(yīng)關(guān)系。
? ? ? ? ? ?
?從得到的角點(diǎn)可以看出,我的標(biāo)定紙有橫向五個(gè)角點(diǎn)縱向四個(gè)角點(diǎn),而通過(guò)Cv2.FindChessboardCorners()得到的角點(diǎn)坐標(biāo)是以圖片的最左上角為坐標(biāo)原點(diǎn),向右形成X軸,向下形成Y軸。且存放的點(diǎn)的順序是以左上角第一個(gè)點(diǎn)開(kāi)始,從左向右,從上至下,即第一排角點(diǎn)的索引以此為0,1,2.....,第二排開(kāi)始為5。
下面是求解過(guò)程
public static void GetRvec()
{
double meanDistance = 0;
double sumDistance = 0;
int numPairs = 0;
Mat Intrinsic = new Mat(3, 3, MatType.CV_32FC1, new double[] { 1473.81, 0, 615.85 , 0, 1474.14, 467.69 , 0, 0, 1 });
Mat distCoeffs = new Mat(5,1, MatType.CV_32FC1, new double[] { 0.051, 0.44, -0.01, -0.009, -2.22 });
string calibImagesPath = "標(biāo)定圖片/"; // 標(biāo)定圖片所在目錄
int boardWidth = 5; // 棋盤(pán)格寬度(內(nèi)角點(diǎn)個(gè)數(shù))
int boardHeight = 4; // 棋盤(pán)格高度(內(nèi)角點(diǎn)個(gè)數(shù))
float squareSize = 36.1F; // 棋盤(pán)格單個(gè)方格的邊長(zhǎng)(毫米)
int k=0;
Point3d[] objectPoints = new Point3d[20];
Point2d[] ww = new Point2d[20] ;
for (int i = 0; i < boardHeight; i++)
{
for (int j = 0; j < boardWidth; j++)
{
// objectPoints.Add(new Point3d(j * squareSize, i * squareSize, 0));
objectPoints[k].X = j * squareSize;
objectPoints[k].Y = i * squareSize;
objectPoints[k].Z = 0;
ww[k].X = j * squareSize;
ww[k].Y = i * squareSize;
k++;
}
}
// 提取圖像中的角點(diǎn)
Mat gray = new Mat();
Cv2.CvtColor(calibImage, gray, ColorConversionCodes.BGR2GRAY);
Point2f[] corners;
Point2f[] imagepoint = new Point2f[4];
bool found = Cv2.FindChessboardCorners(gray, new Size(boardWidth, boardHeight), out corners);
if (found)
{
TermCriteria criteria = new TermCriteria(CriteriaType.MaxIter | CriteriaType.Eps, 30, 0.001);
Cv2.CornerSubPix(gray, corners, new Size(11, 11), new Size(-1, -1), criteria);
Cv2.Find4QuadCornerSubpix(gray, corners, new Size(5, 5));
}
foreach (Point2f p in corners)
{
Cv2.Circle(gray, (int)p.X, (int)p.Y, 5, new Scalar(0, 0, 255), 2);
}
Cv2.ImShow("Image with Corners", gray);
Mat rvec = new Mat(); // 旋轉(zhuǎn)向量
Mat tvec = new Mat(); // 平移向量
InputArray imagePoints = InputArray.Create(corners);
InputArray objectPoints1 = InputArray.Create(objectPoints);
Cv2.SolvePnP(objectPoints1, imagePoints, Intrinsic, distCoeffs, rvec, tvec, false, SolvePnPFlags.Iterative);
Mat rotMatrix = new Mat(3, 3, MatType.CV_32FC1);
Cv2.Rodrigues(rvec, rotMatrix);
using (var fs1 = new FileStorage("test5.yaml", FileStorage.Mode.Write))
{
fs1.Add("rvecsMat").Add(rotMatrix);
fs1.Add("tvecsMat").Add(tvec);
}
}
方法二是通過(guò)手動(dòng)獲取圖片的像素坐標(biāo),這個(gè)就可以使用任何圖片,通過(guò)手動(dòng)點(diǎn)擊獲取坐標(biāo),世界坐標(biāo)任然通過(guò)實(shí)際測(cè)量得到。
private void button6_Click(object sender, EventArgs e)
{
MouseCallback draw = new MouseCallback(draw_circle);
Mat src = Cv2.ImRead(@"你自己的圖片", ImreadModes.AnyColor);
Cv2.ImShow("src image", src);
tempMat = new Mat(src.Size(), src.Type());
Cv2.CopyTo(src, tempMat);
System.Runtime.InteropServices.GCHandle handle = System.Runtime.InteropServices.GCHandle.Alloc(src);
IntPtr ptr = System.Runtime.InteropServices.GCHandle.ToIntPtr(handle);
Cv2.SetMouseCallback("src image", draw, ptr);
}
?
static Mat tempMat;
static Point2f[] a = new Point2f[4];
static int i = 0;
public static void draw_circle(MouseEventTypes @event, int x, int y,
MouseEventFlags flags, IntPtr userData)
{
System.Runtime.InteropServices.GCHandle handle =
System.Runtime.InteropServices.GCHandle.FromIntPtr(userData);
Mat src = (Mat)handle.Target;
if (@event == MouseEventTypes.LButtonDown)
{
a[i].X = x;
a[i].Y = y;
i++;
}
}
上述代碼是在按鍵觸發(fā)時(shí)執(zhí)行讀取照片并創(chuàng)建鼠標(biāo)的點(diǎn)擊事件,對(duì)于鼠標(biāo)的操作自己不太熟悉,因?yàn)橐褂镁碗S便找了個(gè)例程改改這是能實(shí)現(xiàn)基本的鼠標(biāo)點(diǎn)擊功能。這里由于采用的手動(dòng)點(diǎn)擊的方式,因此點(diǎn)的個(gè)數(shù)就設(shè)置得比較少,只采用了四個(gè)點(diǎn),根據(jù)自己實(shí)際情況做調(diào)整,最好點(diǎn)得個(gè)數(shù)不少于4個(gè)。
得到了相機(jī)得外參,當(dāng)我將已知像素坐標(biāo)帶入其中得時(shí)候,卻發(fā)現(xiàn)并沒(méi)有得到我想要得結(jié)果,經(jīng)過(guò)幾周的調(diào)試加計(jì)算,最后通過(guò)C++版本的求解過(guò)程一一對(duì)比,每一步我都輸出結(jié)果進(jìn)行比對(duì),發(fā)現(xiàn)在SolvePnP()這兒出現(xiàn)了問(wèn)題,在帶入?yún)?shù)之前,一切都與C++版本的結(jié)果已知,但是通過(guò)SolvePnP()求解的外參就對(duì)不上,嘗試改變輸入的數(shù)據(jù)格式,以及數(shù)據(jù)的個(gè)數(shù),還有求解方法,始終與C++版本對(duì)不上,于是我用C++求得的外參帶入我自己的后續(xù)程序中,最后能夠比較準(zhǔn)確的得到世界坐標(biāo)。說(shuō)明就是SolvePnP()函數(shù)的問(wèn)題。
但是很遺憾,我仍然沒(méi)有找到解決的方法,不知道各位大佬有沒(méi)有什么辦法或者我哪里出現(xiàn)了問(wèn)題。
為了實(shí)現(xiàn)這一步,我最后采用的是用C++來(lái)調(diào)用C++版本的SolvePnP()函數(shù),將其封裝為Dill動(dòng)態(tài)連接庫(kù),讓C#進(jìn)行調(diào)用。
四、C++封裝DILL給C#調(diào)用
參考其他文章的C++封裝過(guò)程,基本上跟著步驟來(lái)就沒(méi)啥大問(wèn)題。
C#調(diào)用OpenCV(C++原版)思路和實(shí)現(xiàn)方法(小白教程)_c# opencv_SteveDraw的博客-CSDN博客為什么要本地安裝呢?因?yàn)榧热徽{(diào)用那么必須是要獲得相應(yīng)OpenCV接口的調(diào)頭文件或者C++文件!c#和C++雖然兩者衍生自C語(yǔ)言爸爸,兩者更是有多個(gè)類(lèi)似的地方,但是終究語(yǔ)言環(huán)境的差異,這兩者并不能互通,但是做好接口和生成和調(diào)用.dll(動(dòng)態(tài)鏈接庫(kù))就可以無(wú)縫連接,這也是目前C#做視覺(jué)應(yīng)用的一個(gè)常用點(diǎn)!既然要用到第一種方法那么就要建立一個(gè)C++空項(xiàng)目來(lái)生成.dll文件!點(diǎn)擊頭文件夾,右鍵點(diǎn)擊添加->添加新建項(xiàng)(或者點(diǎn)擊頭文件后,快捷鍵Ctrl+Shift+A)接著添加demo.h內(nèi)容:2.添加cpp文https://blog.csdn.net/SteveZhou212/article/details/125103432?
#include <iostream>
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgcodecs/legacy/constants_c.h"
#include <opencv2/opencv.hpp>
#include <time.h>
#include"getRT.h" //這里對(duì)應(yīng)你新建的那個(gè)頭文件
#include <vector>
using namespace std;
using namespace cv;
void toCV()
{
vector<Point2d> imagepoint;
Mat imagepoint1 = Mat(4, 2, CV_64FC1);
Mat objectpoint1 = Mat(4, 3, CV_64FC1);
cv::FileStorage fs("point.yaml", FileStorage::READ);
fs.open("point.yaml", cv::FileStorage::READ);
fs["point"]>>imagepoint1;
fs["worldpoint"] >> objectpoint1;
fs.release();
//imagepoint1.
/* vector<Point3d> objP;
objP.clear();
objP.push_back(Point3d(0, 0, 0));
objP.push_back(Point3d(212.0f, 0, 0));
objP.push_back(Point3d(212.0f, 298.3f, 0));
objP.push_back(Point3d(0, 298.3f, 0));
*/
Mat intrinsic = (Mat_<double>(3, 3) << 1473.819, 0, 615.859 ,
0, 1474.14, 467.697,
0, 0, 1 );
Mat dis = (Mat_<double>(5,1) << 0.051, 0.44, -0.01, -0.009, -2.22);
Mat rvec = Mat(3, 1, CV_64FC1, Scalar::all(0));
Mat tvec = Mat(3, 1, CV_64FC1, Scalar::all(0));
Mat rotM = Mat(3, 3, CV_64FC1, Scalar::all(0));
solvePnP(objectpoint1, imagepoint1, intrinsic, dis, rvec, tvec,false,SOLVEPNP_EPNP );
Rodrigues(rvec, rotM); //將旋轉(zhuǎn)向量變換成旋轉(zhuǎn)矩陣
cv::FileStorage fd("RT.yaml", FileStorage::WRITE);
fd << "R" << rotM;
fd << "t" << tvec;
fd << "point" << imagepoint1;
fd.release();
}
void main()
{
toCV();
}
只需要在C#工程中引用該函數(shù)就行,記得添加這兩行代碼
[DllImport("getRT.dll")]
private extern static void toCV();
最后在需要用到PNP求解的地方調(diào)用toCV就行。
下面是最后實(shí)現(xiàn)的效果,首先是對(duì)物體進(jìn)行檢測(cè),找到最小外接圓,得到圓心與半徑
?最后通過(guò)計(jì)算得到我的手機(jī)對(duì)角線(xiàn)長(zhǎng)為166mm,通過(guò)屏幕尺寸6.4英寸換算為162.56mm,考慮到手機(jī)并不是全面屏,上下巴加上一點(diǎn),誤差應(yīng)該在5mm以?xún)?nèi)。
第一次接觸C#,可能很多地方表達(dá)不太清楚,也是順便記錄一下整個(gè)過(guò)程,希望能夠幫助到有需要的人。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-705541.html
另外就是SolvePnP()求解的問(wèn)題,不知道有沒(méi)有大佬知道為啥我的求解始終不對(duì)。文章有不足或者不清楚的地方希望大家指正。文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-705541.html
到了這里,關(guān)于C#實(shí)現(xiàn)物體尺寸測(cè)量(利用坐標(biāo)轉(zhuǎn)換)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!