C# implémente la mesure de la taille des objets (en utilisant la conversion de coordonnées)

Étant donné que la mesure d'un objet doit être implémentée, mais qu'il existe déjà un programme QT, la fonction globale finale doit être intégrée et implémentée en C#.

Il existe d'abord deux options : (1) Utiliser les programmes et interfaces QT existants pour appeler directement des programmes QT ou C++ en C#. Cependant, après avoir essayé, nous avons constaté que l'appel entre les deux n'est pas si simple et implique de nombreuses définitions de variables. Non utilisé et la structure des données est différente. Par conséquent, nous avons opté pour l’option (2) et réimplémenté cette fonction en C#.

Comme c'est la première fois que j'utilise un appareil photo, j'en profite pour l'enregistrer.

1. Le premier est l’étalonnage de la caméra. C’est très simple et il existe de nombreuses références associées :

Calibrage de la caméra (1) - Calibrage des paramètres internes et mise en œuvre du programme_Calibrage des paramètres internes de la caméra_white_Learner's Blog-Blog CSDNCalibrage de la caméra (1) - Calibrage des paramètres internes et mise en œuvre du programme de calibrage de la caméra ( 2) - Conversion des coordonnées de l'image et des coordonnées mondiales Calibrage de la caméra (3) - Calibrage œil-main 1. Processus de mise en œuvre de l'algorithme de calibrage de Zhang Zhengyou 1.1 Préparer le damier Remarque : L'espacement noir et blanc du damier est connu, vous pouvez utiliser du papier d'impression ou achetez un damier noir et blanc pour le calibrage (exigences de haute précision) 1.2 Prenez plusieurs photos du motif de l'échiquier. Il y a deux situations ici (1) Calibrage du coefficient de distorsion et des paramètres internes de l'appareil photo. Les photos doivent inclure l'échiquier complet, et en même temps, différentes distances et différentes orientations sont nécessaires... https://blog.csdn.net/Kalenee/article/details/80672785

OpenCvSharp Checkerboard Calibration Assistant_opencvsharp Calibration_YT - Chow's Blog-CSDN Blog a utilisé VS pour appeler la bibliothèque de ressources OpenCvSharp afin d'écrire une interface d'opération Winform. J'en ai trouvé beaucoup sur Internet. que les programmes open source ne peuvent pas être utilisés du tout. Lors de leur utilisation, vous devez configurer diverses variables du système informatique, ce qui semble très gênant. Maintenant, j'ai un assistant de calibrage simple, qui fonctionne parfaitement. Il dispose d'un outil de génération d'images en damier. Il est facile à utiliser et le code source n'est pas compliqué. À l'aide du package de développement de ressources OpenCvSharp, j'ai fait une petite démo de l'assistant d'étalonnage de la caméra sous l'image en damier sous VS. Évidemment, C# peut également utiliser OpenCv. C'est un meilleur cas, vous pouvez vous y référer. Je n'ai pas de talent, et je l'ai également utilisé pour faire une démo de reconstruction tridimensionnelle SFM, que je n'inclurai pas ici. en utilisant 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 &

Parce qu'OpencvSharp est à l'origine une bibliothèque de liens dynamiques C# encapsulée par Opencv, la méthode utilisée est essentiellement la même que l'implémentation de la fonction de base. Vous pouvez directement vous référer à la version C++ de l'étalonnage de la caméra. Il vous suffit de faire attention à la modification de la taille physique spécifique de la grille d'étalonnage et du nombre de grilles d'étalonnage lorsque vous l'utilisez personnellement.

2. Convertissez les coordonnées de pixels en coordonnées mondiales

Après avoir calibré la caméra, vous devez mettre en œuvre comment convertir les coordonnées des pixels en coordonnées mondiales. Pour la théorie et le processus de mise en œuvre, veuillez vous référer à la version C++.

Calibrage de la caméra (2) - Conversion des coordonnées de l'image et des coordonnées mondiales_Calibration convertit les coordonnées de l'image en coordonnées mondiales_white_Learner's blog-CSDN blogParce que cet article contient des erreurs et des ambiguïtés, réécrivez et modifiez pour cet objectif, mais comme CSDN ne prend pas en charge la conversion de texte enrichi en Markdown, alors réécrivez et publiez un nouveau billet de blog : https://blog.csdn.net/Kalenee/article/details/99207102 1. Explication détaillée de la transformation des coordonnées 1.1 Coordonnées relation Il existe quatre systèmes de coordonnées dans la caméra, à savoir le monde, la caméra et l'image. Pixelworld est le système de coordonnées du monde. Vous pouvez spécifier les axes et les axes arbitrairement...https://blog. csdn.net/Kalene/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);

            //计算世界坐标
            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;
        }

 On voit que cela est presque réalisable avec la version C++.

Ce qu'il faut noter ici, c'est la fonction d'inversion matricielle Cv2.Invert(). En fonction de son troisième paramètre DecompTypes, la méthode d'inversion est également différente. Cependant, les résultats obtenus par mon propre test ne sont que plus précis en utilisant la méthode DecompTypes.SVD. Je me demande si vous avez de l'expérience dans le choix de la méthode inverse.

Cependant, après l'écriture de la fonction, le test des coordonnées de pixel avec les coordonnées du monde n'a toujours pas répondu aux exigences. Après les conseils de mon frère aîné, c'est parce que les paramètres externes (matrice de rotation et vecteur de translation) que j'ai utilisés ici étaient erronés. La raison était que j'ai utilisé directement Cv2. Les paramètres externes obtenus par .CalibrateCamera() sont utilisés pour le calcul. Cependant, les conditions de prise de vue ont changé au cours de mon expérience et les paramètres externes à ce moment-là ne sont plus applicables. Par conséquent, il est nécessaire d’obtenir les paramètres externes de la caméra actuelle avant de résoudre le problème. Faire cette erreur signifie ne pas avoir une bonne compréhension globale de l'ensemble du processus de mise en œuvre et de la théorie de la conversion des coordonnées de la caméra, ce qui entraîne des problèmes de correspondance des paramètres externes avec les objets.Par conséquent, ce qui suit est de résoudre les paramètres externes de la caméra actuelle.

3. Résolution des paramètres externes

Étant donné que les coordonnées connues des pixels de l'objet doivent être converties en coordonnées mondiales, les changements de position et de posture de la caméra et de l'objet à ce moment sont nécessaires. Parce que la résolution des paramètres externes peut déduire les coordonnées mondiales réelles de l'objet.

Il convient de noter que mon environnement réel est celui où la caméra est stationnaire, c'est-à-dire que la caméra est toujours parallèle au sol et que la position de la caméra est fixe. Par conséquent, s’il doit être utilisé lorsque la caméra bouge ou que la position relative change à tout moment, il doit être modifié.

Les fonctions utilisées sont également très simples. SolvePnP fourni avec OpencvSharp est utilisé pour la solution.

 Comme vous pouvez le voir à partir des paramètres de la fonction, nous devons préparer objectPoints, imagePoints, Intrinsic, distCoeffs, rvec, tvec, SolvePnPFlags. Je pense que ce sont les paramètres les plus critiques.

Le premier est objectPoints : un ensemble de coordonnées mondiales doit être fourni

          ImagePoints : un ensemble de coordonnées de pixels correspondant aux coordonnées du monde doit être fourni

          ​ ​ ​ ​ Intrinsèques, distCoeffs : paramètres internes de la caméra et coefficients de distorsion

          rvec, tvec : génère un vecteur de rotation et un vecteur de translation. Habituellement, le vecteur de rotation doit être converti en une matrice de rotation à l'aide de Cv2.Rodrigues()

          SolvePnPFlags : sélection de la méthode de solution, les points d'entrée pour différentes exigences de sélection de paramètres ont également des exigences, vous pouvez vous référer aux articles connexes pour effectuer des sélections

Ici, j'utilise deux méthodes pour obtenir les valeurs de objectPoints et imagePoints.

La première consiste à utiliser du papier d'étalonnage, à trouver les coordonnées des coins du papier d'étalonnage via un programme, puis à utiliser une règle pour mesurer réellement les coordonnées mondiales de chaque point d'angle. Ce qu'il faut noter ici, c'est la sélection des axes X et Y. des coordonnées du monde, basées sur les points d'angle. Le coin supérieur gauche est l'origine, formant l'axe X vers la droite et l'axe Y vers le bas. Assurez-vous de faire attention à la correspondance entre les points de coordonnées mondiales et les points de coordonnées pixel.

           

 Il peut être vu à partir des points d'angle obtenus que mon papier d'étalonnage a cinq points d'angle horizontaux et quatre points d'angle verticaux, et les coordonnées des points d'angle obtenues via Cv2.FindChessboardCorners() sont basées sur le coin supérieur gauche de l'image comme origine de les coordonnées, à droite Formez l'axe X et descendez pour former l'axe Y. Et l'ordre des points stockés commence à partir du premier point dans le coin supérieur gauche, de gauche à droite, de haut en bas, c'est-à-dire que l'indice de la première rangée de points de coin est 0, 1, 2..., le deuxième peloton commence à 17 heures.

Voici le processus de solution

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 = "标定图片/"; // 标定图片所在目录
            int boardWidth = 5; // 棋盘格宽度(内角点个数)
            int boardHeight = 4; // 棋盘格高度(内角点个数)
            float squareSize = 36.1F; // 棋盘格单个方格的边长(毫米)
            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++;

                }
            }

         
            // 提取图像中的角点
            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(); // 旋转向量
            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);

            }
           
        }

La deuxième méthode consiste à obtenir manuellement les coordonnées en pixels de l'image. Cela peut utiliser n'importe quelle image et obtenir les coordonnées en cliquant manuellement. Les coordonnées du monde sont toujours obtenues par mesure réelle.

 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++;

              
            }
         }

Le code ci-dessus consiste à lire la photo et à créer un événement de clic de souris lorsque la touche est déclenchée. Je ne suis pas familier avec le fonctionnement de la souris, j'ai donc juste trouvé une routine et l'ai modifiée pour obtenir la fonction de base du clic de souris. En raison de la méthode de clic manuel utilisée ici, le nombre de points est défini relativement petit. Seuls quatre points sont utilisés. Ajustez en fonction de votre situation réelle. Il est préférable d'avoir au moins 4 points.

Après avoir récupéré les paramètres externes de la caméra, lorsque j'y ai introduit les coordonnées de pixels connues, j'ai constaté que je n'obtenais pas les résultats souhaités. Après plusieurs semaines de débogage et de calcul, je les ai finalement comparés un par un via la version C++. du processus de solution. J'ai affiché les résultats à chaque étape pour comparaison et j'ai découvert qu'il y avait un problème avec SolvePnP(). Avant d'introduire les paramètres, tout était connu des résultats de la version C++, mais les paramètres externes résolus par SolvePnP() ne correspondait pas. J'ai essayé de changer le format des données d'entrée, le nombre de données et la méthode de solution, mais c'était toujours incohérent avec la version C++, j'ai donc utilisé les paramètres externes obtenus par C++ et les ai introduits dans mon propre programme de suivi, et finalement j'ai pu obtenir des coordonnées mondiales plus précises. L'explication est un problème avec la fonction SolvePnP().

Mais malheureusement, je n'ai toujours pas trouvé de solution. Je ne sais pas si vous avez des solutions ni où est mon problème.

Afin de réaliser cette étape, j'ai finalement utilisé C++ pour appeler la version C++ de la fonction SolvePnP(), je l'ai encapsulée dans une bibliothèque de liens dynamiques Dill et j'ai laissé C# effectuer l'appel.

4. C++ encapsule DILL et l'appelle en C#

Reportez-vous au processus d'encapsulation C++ dans d'autres articles. Fondamentalement, il n'y aura pas de gros problèmes si vous suivez les étapes.

Idées et méthodes d'implémentation d'OpenCV (version originale C++) en C# (tutoriel débutant)_c# blog opencv_SteveDraw-blog CSDNPourquoi devez-vous l'installer localement ? Car puisqu'il est appelé, vous devez obtenir le fichier Demi-tour ou fichier C++ de l'interface OpenCV correspondante ! Bien que C# et C++ soient dérivés du langage C et présentent de nombreuses similitudes, après tout, en raison des différences dans les environnements linguistiques, les deux ne peuvent pas communiquer entre eux. Cependant, ils doivent faire du bon travail dans les interfaces et dans la génération et l'appel . dll (lien dynamique) Library) peuvent être connectés de manière transparente, ce qui est également un point commun pour les applications visuelles en C# ! Puisque vous souhaitez utiliser la première méthode, vous devez créer un projet C++ vide pour générer le fichier .dll ! Cliquez sur le dossier d'en-tête, cliquez avec le bouton droit sur Ajouter -> Ajouter un nouvel élément (ou cliquez sur le fichier d'en-tête, raccourci Ctrl+Maj+A), puis ajoutez le contenu demo.h : 2. Ajoutez un fichier 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"  //这里对应你新建的那个头文件
#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);  //将旋转向量变换成旋转矩阵
  
    cv::FileStorage fd("RT.yaml", FileStorage::WRITE);
    fd << "R" << rotM;
    fd << "t" << tvec;
    fd << "point" << imagepoint1;
    fd.release();
}
void main()
{
    toCV();
}

Référencez simplement la fonction dans le projet C#, pensez à ajouter ces deux lignes de code

[DllImport("getRT.dll")]
private extern static void toCV();

Enfin, appelez simplement toCV là où une solution PNP est nécessaire.

Voici l'effet final : tout d'abord, l'objet est détecté, le plus petit cercle circonscrit est trouvé, et le centre et le rayon du cercle sont obtenus.

 Enfin, grâce au calcul, j'ai constaté que la longueur diagonale de mon téléphone mobile est de 166 mm, ce qui est converti en 162,56 mm sur la base de la taille de l'écran de 6,4 pouces. Considérant que le téléphone mobile n'est pas un plein écran, en ajoutant un peu à la partie supérieure et le bas du menton, l'erreur doit être inférieure à 5 mm.

C'est la première fois que j'entre en contact avec C#, donc je ne pourrai peut-être pas m'exprimer clairement dans de nombreux endroits, je vais donc enregistrer tout le processus en passant, dans l'espoir d'aider ceux qui en ont besoin.

Un autre problème est SolvePnP(). Je me demande si quelqu'un sait pourquoi ma solution est toujours fausse. J'espère que vous pourrez me corriger s'il y a des lacunes ou des aspects peu clairs dans l'article.

Je suppose que tu aimes

Origine blog.csdn.net/weixin_44566773/article/details/129399417
conseillé
Classement