Unity + OpenCVの仮想Quadrocopter(パート3)

CPAP



みなさんこんにちは!



今日は、Unity、C ++、OpenCVを友達にする方法についてのシリーズを続けていきたいと思います。 また、Unityに基づいてコンピュータービジョンのアルゴリズムとドローンのナビゲーションをテストするための仮想環境を取得する方法も。 以前の記事で、 Unityで仮想クアドロコプターを作成する 方法と、C ++プラグインを接続し、そこで仮想カメラから画像を転送し、OpenCVを使用して処理する方法について説明しました 。 この記事では、クアドロコプターで2つの仮想カメラからステレオペアを作成する方法と、画像のピクセル深度を推定するために使用できる視差マップを取得する方法を説明します。



アイデア



3D再構成の作成方法についてはたくさん書かれています。 たとえば、Habrに関するすばらしい記事があります。 あなたが完全にトピックから外れている場合、それを読むことを強くお勧めします。 より数学的に厳密なことはここで読むことができます 。 ここで、私は単純に主要なアイデアを提示しています。 使用する変位マップを取得する手法は、2つの画像を使用した高密度3D再構築と呼ばれます。 横に並んで同じ向きの2台のカメラが、わずかに異なる視点から同じシーンを見るために使用されます。 これはステレオペアと呼ばれます。 通常の水平ステレオペア、つまり、カメラの「ビュー」の方向に対して垂直にオフセットされたカメラを使用します。 最初の画像と2番目の画像で同じシーンポイントを見つけた場合、つまりシーンポイントの2つの投影を見つけた場合、一般的な場合、これら2つの投影の座標が一致しないことがわかります。 つまり、画像が重なっている場合、投影は互いに対してオフセットされます。 これにより、変位の量によってシーンポイントの深さを計算することが可能になります(単純化:変位したポイントは変位した少ないポイントよりも近くなります)。



ステレオペアとポイント深度

www.adept.net.au/news/newsletter/201211-nov/article_3D_stereo.shtmlからの画像



これを行うには、カメラを較正する必要があります。 次に、ステレオペアを調整し、 歪みを除去して、同じ投影点が1本の水平線上にくるように画像をまっすぐにする必要があります。 これは、OpenCVのタイトな3D再構築アルゴリズムの要件であり、対応するポイントの検索を高速化します。 OpenCV-StereoBMの最も単純で最速のアルゴリズムを使用します。 説明APIはこちらです。 始めましょう。



カメラキャリブレーション



Quadrocopterに別のカメラを追加して画像を取得する方法は前の記事で説明されているため、すぐにカメラのキャリブレーションから始めます。 視野角70度のカメラ2台、解像度512 x 512ピクセルのカメラの画像のテクスチャを使用します。 カメラのキャリブレーションとは、内部パラメーターの3x3マトリックスとその歪みパラメーターのベクトルを取得することを意味します。 キャリブレーションは、キャリブレーションサンプルのセットを取得することによって行われます。 1つのキャリブレーションサンプルは、カメラ画像の2次元基準フレーム内の既知のジオメトリを持つキャリブレーションパターンのポイントの座標です。 それを使用するすべてのアルゴリズムの作業は、キャリブレーションの品質に大きく依存するため、カメラを適切にキャリブレーションすることが非常に重要です。 40-50のキャリブレーションサンプルでキャリブレーションを行いますが、キャリブレーションサンプルの大きなばらつきは重要です。つまり、可能であれば、カメラに対するキャリブレーションパターンの向きと位置の最大のばらつきを取得する必要があります。 ほぼ同じ50のキャリブレーションサンプルを提示すると、カメラキャリブレーションの品質が低下します。 私はこの校正ペタルンを使用します 。 Unityで単純にドラッグアンドドロップし、2Dスプライトのテクスチャとして設定できます。 プログラムですべてのサイズをピクセルで設定します。このパターンの正方形の辺のサイズは167ピクセルです。 彼には、OpenCVの画像で彼を見つけるための機能が既にあります。 インスピレーションのために、opencv-source / samples / cpp / calibration.cppの例を使用できます。 実際、私はそれをしました。



基本的なキャリブレーション機能のコード
/** @brief             . Size boardSize (9, 6) -    ,      ,  sampleFound        */ void CameraCalibrator::findSample (const cv::Mat& img) { currentSamplePoints.clear(); int chessBoardFlags = CALIB_CB_ADAPTIVE_THRESH | CALIB_CB_NORMALIZE_IMAGE; sampleFound = findChessboardCorners( img, boardSize, currentSamplePoints, chessBoardFlags); currentImage = &img; } bool CameraCalibrator::isSampleFound () { return sampleFound; } /** @brief     */ void CameraCalibrator::acceptSample () { //      Mat viewGray; cvtColor(*currentImage, viewGray, COLOR_BGR2GRAY); cornerSubPix( viewGray, currentSamplePoints, Size(11,11), Size(-1,-1), TermCriteria( TermCriteria::EPS+TermCriteria::COUNT, 30, 0.1 )); //      (     ) drawChessboardCorners(*currentImage, boardSize, Mat(currentSamplePoints), sampleFound); //  samplesPoints.push_back(currentSamplePoints); } /** @brief     */ void CameraCalibrator::makeCalibration () { vector<Mat> rvecs, tvecs; vector<float> reprojErrs; double totalAvgErr = 0; //  ,       //     , //   -   ,   //     ,  //   float aspectRatio = 1.0; int flags = CV_CALIB_FIX_ASPECT_RATIO; bool ok = runCalibration(samplesPoints, imageSize, boardSize, squareSize, aspectRatio, flags, cameraMatrix, distCoeffs, rvecs, tvecs, reprojErrs, totalAvgErr); //      stringstream sstr; sstr << "--- calib result: " << (ok ? "Calibration succeeded" : "Calibration failed") << ". avg reprojection error = " << totalAvgErr; DebugLog(sstr.str()); saveCameraParams(imageSize, boardSize, squareSize, aspectRatio, flags, cameraMatrix, distCoeffs, rvecs, tvecs, reprojErrs, samplesPoints, totalAvgErr); } /** @brief      */ void calcChessboardCorners(Size boardSize, float squareSize, vector<Point3f>& corners) { corners.resize(0); for( int i = 0; i < boardSize.height; ++i ) for( int j = 0; j < boardSize.width; ++j ) corners.push_back(Point3f(j*squareSize, i*squareSize, 0)); } /** @brief    */ bool runCalibration( vector<vector<Point2f> > imagePoints, Size imageSize, Size boardSize, float squareSize, float aspectRatio, int flags, Mat& cameraMatrix, Mat& distCoeffs, vector<Mat>& rvecs, vector<Mat>& tvecs, vector<float>& reprojErrs, double& totalAvgErr ) { //     cameraMatrix = Mat::eye(3, 3, CV_64F); if( flags & CALIB_FIX_ASPECT_RATIO ) cameraMatrix.at<double>(0,0) = aspectRatio; //   distCoeffs = Mat::zeros(8, 1, CV_64F); //      //    //    vector<vector<Point3f> > objectPoints(1); calcChessboardCorners(boardSize, squareSize, objectPoints[0]); objectPoints.resize(imagePoints.size(),objectPoints[0]); // OpenCV   //objectPoints -      //imagePoints -   //imageSize -    double rms = calibrateCamera(objectPoints, imagePoints, imageSize, cameraMatrix, distCoeffs, rvecs, tvecs, flags|CALIB_FIX_K4|CALIB_FIX_K5); ///*|CALIB_FIX_K3*/|CALIB_FIX_K4|CALIB_FIX_K5); //rms -   ,   , //     ,   1 printf("RMS error reported by calibrateCamera: %g\n", rms); bool ok = checkRange(cameraMatrix) && checkRange(distCoeffs); totalAvgErr = computeReprojectionErrors(objectPoints, imagePoints, rvecs, tvecs, cameraMatrix, distCoeffs, reprojErrs); return ok; }
      
      







ステレオペア



次に、2台のカメラからステレオペアを作成する必要があります。 これを行うには、 stereoCalibrate関数を使用します。 彼女には、両方のカメラのサンプルが必要です。 カメラのキャリブレーション手順から取得したサンプルを使用します。 したがって、キャリブレーションボールパターンが2つの画像で同時に見つかったサンプルのみを保存することが重要です。 カメラ行列とその歪みパラメーターは、適切な初期近似として必要です。



ステレオペアキャリブレーションコード
 /** @brief ,    */ void StereoCalibrator::makeCalibration ( const std::vector<std::vector<cv::Point2f>>& camera1SamplesPoints, const std::vector<std::vector<cv::Point2f>>& camera2SamplesPoints, cv::Mat& camera1Matrix, cv::Mat& camera1DistCoeffs, cv::Mat& camera2Matrix, cv::Mat& camera2DistCoeffs ) { //       std::vector<vector<Point3f>> objectPoints; for( int i = 0; i < camera1SamplesPoints.size(); i++ ) { objectPoints.push_back(chessboardCorners); } double rms = stereoCalibrate( objectPoints, camera1SamplesPoints, camera2SamplesPoints, //        camera1Matrix, camera1DistCoeffs, camera2Matrix, camera2DistCoeffs, //   imageSize, //         rotationMatrix, //        translationVector, //    essentialMatrix, //    fundamentalMatrix, //,        //       CV_CALIB_USE_INTRINSIC_GUESS, TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 100, 1e-5) ); //      - // , ,  ,      1 stringstream outs; outs << "--- stereo calib: done with RMS error=" << rms; DebugLog(outs.str()); }
      
      







Unityのステレオペア



私の校正パラメーター
  //512, 70 deg Mat cam1 = (Mat_<double>(3, 3) << 355.3383989449604, 0, 258.0008490063121, 0, 354.5068750418187, 255.7252273330564, 0, 0, 1); Mat dist1 = (Mat_<double>(5, 1) << -0.02781875153957544, 0.05084431574408409, 0.0003262438299225566, 0.0005420218184546293, -0.06711413339515834); Mat cam2 = (Mat_<double>(3, 3) << 354.8366825622115, 0, 255.7668702403205, 0, 353.9950515096826, 254.3218524455621, 0, 0, 1); Mat dist2 = (Mat_<double>(12, 1) << -0.03429254591232522, 0.04304840389703278, -0.0005799461588668822, 0.0005396568753307817, -0.01867317550268149); Mat R = (Mat_<double>(3, 3) << 0.9999698145104303, 3.974878365893637e-06, 0.007769816740176146, -3.390471048492443e-05, 0.9999925806915616, 0.003851936175643478, -0.00776974378253147, -0.003852083336451321, 0.9999623955607145); Mat T = (Mat_<double>(3, 1) << 498.2890078004688, 0.3317087752736566, -6.137837861924672);
      
      







変位マップ



次に、変位マップを計算する番です。 前の手順をすべて正しく行ったとしても、すぐに計算できるという事実ではありません。 実際には、変位マップを計算するアルゴリズムには約10個のパラメーターがあり、適切な画像を得るには正しく選択する必要があります。 ここで何が問題なのかを理解するのに役立ちます。このビデオ







この記事から。 パラメーターの設定に関して、TextureThresholdパラメーターを50以上に上げることはお勧めしません。この場合、StereoBMアルゴリズムが自然に落ち始めることがわかっているからです。



オフセットマップ受信コード
 //     DisparityMapCalculator::DisparityMapCalculator () { bm = StereoBM::create(16,9); } /** @brief         */ void DisparityMapCalculator::set ( cv::Mat camera1Matrix, cv::Mat camera2Matrix, cv::Mat camera1distCoeff, cv::Mat camera2distCoeff, cv::Mat rotationMatrix, cv::Mat translationVector, cv::Size imageSize ) { this->camera1Matrix = camera1Matrix; this->camera2Matrix = camera2Matrix; this->camera1distCoeff = camera1distCoeff; this->camera2distCoeff = camera2distCoeff; this->rotationMatrix = rotationMatrix; this->translationVector = translationVector; //   ()  stereoRectify( camera1Matrix, camera1distCoeff, camera2Matrix, camera2distCoeff, imageSize, rotationMatrix, translationVector, R1, R2, P1, P2, Q, /*CALIB_ZERO_DISPARITY*/0, -1, imageSize, &roi1, &roi2 ); //    //     initUndistortRectifyMap(camera1Matrix, camera1distCoeff, R1, P1, imageSize, CV_16SC2, map11, map12); initUndistortRectifyMap(camera2Matrix, camera2distCoeff, R2, P2, imageSize, CV_16SC2, map21, map22); bm->setROI1(roi1); bm->setROI2(roi2); } /** @brief        */ void DisparityMapCalculator::setBMParameters ( int preFilterSize, int preFilterCap, int blockSize, int minDisparity, int numDisparities, int textureThreshold, int uniquenessRatio, int speckleWindowSize, int speckleRange, int disp12maxDiff ) { bm->setPreFilterSize(preFilterSize); bm->setPreFilterCap(preFilterCap); bm->setBlockSize(blockSize); bm->setMinDisparity(minDisparity); bm->setNumDisparities(numDisparities); bm->setTextureThreshold(textureThreshold); bm->setUniquenessRatio(uniquenessRatio); bm->setSpeckleWindowSize(speckleWindowSize); bm->setSpeckleRange(speckleRange); bm->setDisp12MaxDiff(disp12maxDiff); } /** @brief    */ void DisparityMapCalculator::compute ( const cv::Mat& image1, const cv::Mat& image2, cv::Mat& image1recified, cv::Mat& image2recified, cv::Mat& disparityMap ) { //   remap(image1, image1recified, map11, map12, INTER_LINEAR); remap(image2, image2recified, map21, map22, INTER_LINEAR); //          . //     OpenGL  ,   //   , //      OpenGL  OpenCV //      ,     flip(image1recified, L, 1); flip(image2recified, R, 1); // stereo bm -     sgbm, //      // StereoBM        cv::cvtColor(L, image1gray, CV_RGBA2GRAY, 1); cv::cvtColor(R, image2gray, CV_RGBA2GRAY, 1); int numberOfDisparities = bm->getNumDisparities(); //   bm->compute(image1gray, image2gray, disp); //    ,    disp.convertTo(disp8bit, CV_8U, 255/(numberOfDisparities*16.)); // ,       Unity flip (disp8bit, disp, 1); //     4   //      // 4  cv::cvtColor(disp, disparityMap, CV_GRAY2RGBA, 4); }
      
      







また、ビデオが私にとってどのように機能するかを見ることができます。







ディスプレイスメントマップの計算に使用されるアルゴリズムは非常に古いため、結果はあまり美しくありません。 もっと新しいアルゴリズムを使用することで改善できると思いますが、このためにはOpenCVでは十分ではありません。



トーマスコールに感謝します

コードはgithub 、ブランチhabr_part3_disparity_map_opencv_stereobm から取得できます



ご清聴ありがとうございました。



All Articles