// Copyright Epic Games, Inc. All Rights Reserved. #include "OpenCVHelper.h" #include "Engine/Texture2D.h" #include "TextureResource.h" #include "UObject/Package.h" #if WITH_OPENCV #include "PreOpenCVHeaders.h" // IWYU pragma: keep #include "opencv2/aruco.hpp" #include "opencv2/calib3d.hpp" #include "opencv2/imgproc.hpp" #include "PostOpenCVHeaders.h" // IWYU pragma: keep #endif // WITH_OPENCV DEFINE_LOG_CATEGORY_STATIC(LogOpenCVHelper, Log, All); namespace UE::Aruco::Private { #if WITH_OPENCV cv::Ptr GetArucoDictionary(EArucoDictionary InDictionary) { switch (InDictionary) { case EArucoDictionary::DICT_4X4_50: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_4X4_50); case EArucoDictionary::DICT_4X4_100: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_4X4_100); case EArucoDictionary::DICT_4X4_250: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_4X4_250); case EArucoDictionary::DICT_4X4_1000: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_4X4_1000); case EArucoDictionary::DICT_5X5_50: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_5X5_50); case EArucoDictionary::DICT_5X5_100: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_5X5_100); case EArucoDictionary::DICT_5X5_250: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_5X5_250); case EArucoDictionary::DICT_5X5_1000: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_5X5_1000); case EArucoDictionary::DICT_6X6_50: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_50); case EArucoDictionary::DICT_6X6_100: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_100); case EArucoDictionary::DICT_6X6_250: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_250); case EArucoDictionary::DICT_6X6_1000: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_6X6_1000); case EArucoDictionary::DICT_7X7_50: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_7X7_50); case EArucoDictionary::DICT_7X7_100: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_7X7_100); case EArucoDictionary::DICT_7X7_250: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_7X7_250); case EArucoDictionary::DICT_7X7_1000: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_7X7_1000); case EArucoDictionary::DICT_ARUCO_ORIGINAL: return cv::aruco::getPredefinedDictionary(cv::aruco::DICT_ARUCO_ORIGINAL); default: ensureMsgf(false, TEXT("Unhandled EArucoDictionary type. Update this switch statement.")); break; // Do nothing } return nullptr; } #endif } void FOpenCVHelper::ConvertCoordinateSystem(FTransform& Transform, const EAxis SrcXInDstAxis, const EAxis SrcYInDstAxis, const EAxis SrcZInDstAxis) { // Unreal Engine: // Front : X // Right : Y // Up : Z // // OpenCV: // Front : Z // Right : X // Up : Yn FMatrix M12 = FMatrix::Identity; M12.SetColumn(0, UnitVectorFromAxisEnum(SrcXInDstAxis)); M12.SetColumn(1, UnitVectorFromAxisEnum(SrcYInDstAxis)); M12.SetColumn(2, UnitVectorFromAxisEnum(SrcZInDstAxis)); Transform.SetFromMatrix(M12.GetTransposed() * Transform.ToMatrixWithScale() * M12); } void FOpenCVHelper::ConvertUnrealToOpenCV(FTransform& Transform) { ConvertCoordinateSystem(Transform, EAxis::Y, EAxis::Zn, EAxis::X); } void FOpenCVHelper::ConvertOpenCVToUnreal(FTransform& Transform) { ConvertCoordinateSystem(Transform, EAxis::Z, EAxis::X, EAxis::Yn); } FVector FOpenCVHelper::ConvertUnrealToOpenCV(const FVector& Vector) { return FVector(Vector.Y, -Vector.Z, Vector.X); } FVector FOpenCVHelper::ConvertOpenCVToUnreal(const FVector& Vector) { return FVector(Vector.Z, Vector.X, -Vector.Y); } #if WITH_OPENCV UTexture2D* FOpenCVHelper::TextureFromCvMat(cv::Mat& Mat, const FString* PackagePath, const FName* TextureName) { if ((Mat.cols <= 0) || (Mat.rows <= 0)) { return nullptr; } // Currently we only support G8 and BGRA8 if (Mat.depth() != CV_8U) { return nullptr; } EPixelFormat PixelFormat; ETextureSourceFormat SourceFormat; switch (Mat.channels()) { case 1: PixelFormat = PF_G8; SourceFormat = TSF_G8; break; case 4: PixelFormat = PF_B8G8R8A8; SourceFormat = TSF_BGRA8; break; default: return nullptr; } UTexture2D* Texture = nullptr; #if WITH_EDITOR if (PackagePath && TextureName) { Texture = NewObject(CreatePackage(**PackagePath), *TextureName, RF_Standalone | RF_Public); if (!Texture) { return nullptr; } const int32 NumSlices = 1; const int32 NumMips = 1; Texture->Source.Init(Mat.cols, Mat.rows, NumSlices, NumMips, SourceFormat, Mat.data); auto IsPowerOfTwo = [](int32 Value) { return (Value > 0) && ((Value & (Value - 1)) == 0); }; if (!IsPowerOfTwo(Mat.cols) || !IsPowerOfTwo(Mat.rows)) { Texture->MipGenSettings = TMGS_NoMipmaps; } Texture->SRGB = 0; FTextureFormatSettings FormatSettings; if (Mat.channels() == 1) { Texture->CompressionSettings = TextureCompressionSettings::TC_Grayscale; Texture->CompressionNoAlpha = true; } Texture->SetLayerFormatSettings(0, FormatSettings); Texture->SetPlatformData(new FTexturePlatformData()); Texture->GetPlatformData()->SizeX = Mat.cols; Texture->GetPlatformData()->SizeY = Mat.rows; Texture->GetPlatformData()->PixelFormat = PixelFormat; Texture->UpdateResource(); Texture->MarkPackageDirty(); } else #endif //WITH_EDITOR { Texture = UTexture2D::CreateTransient(Mat.cols, Mat.rows, PixelFormat); if (!Texture) { return nullptr; } #if WITH_EDITORONLY_DATA Texture->MipGenSettings = TMGS_NoMipmaps; #endif Texture->NeverStream = true; Texture->SRGB = 0; if (Mat.channels() == 1) { Texture->CompressionSettings = TextureCompressionSettings::TC_Grayscale; #if WITH_EDITORONLY_DATA Texture->CompressionNoAlpha = true; #endif } // Copy the pixels from the OpenCV Mat to the Texture FTexture2DMipMap& Mip0 = Texture->GetPlatformData()->Mips[0]; void* TextureData = Mip0.BulkData.Lock(LOCK_READ_WRITE); const int32 PixelStride = Mat.channels(); FMemory::Memcpy(TextureData, Mat.data, Mat.cols * Mat.rows * SIZE_T(PixelStride)); Mip0.BulkData.Unlock(); Texture->UpdateResource(); } return Texture; } UTexture2D* FOpenCVHelper::TextureFromCvMat(cv::Mat& Mat, UTexture2D* InTexture) { if (!InTexture) { return TextureFromCvMat(Mat); } if ((Mat.cols <= 0) || (Mat.rows <= 0)) { return nullptr; } // Currently we only support G8 and BGRA8 if (Mat.depth() != CV_8U) { return nullptr; } EPixelFormat PixelFormat; switch (Mat.channels()) { case 1: PixelFormat = PF_G8; break; case 4: PixelFormat = PF_B8G8R8A8; break; default: return nullptr; } if ((InTexture->GetSizeX() != Mat.cols) || (InTexture->GetSizeY() != Mat.rows) || (InTexture->GetPixelFormat() != PixelFormat)) { return TextureFromCvMat(Mat); } // Copy the pixels from the OpenCV Mat to the Texture FTexture2DMipMap& Mip0 = InTexture->GetPlatformData()->Mips[0]; void* TextureData = Mip0.BulkData.Lock(LOCK_READ_WRITE); const int32 PixelStride = Mat.channels(); FMemory::Memcpy(TextureData, Mat.data, Mat.cols * Mat.rows * SIZE_T(PixelStride)); Mip0.BulkData.Unlock(); InTexture->UpdateResource(); return InTexture; } void FOpenCVHelper::MakeCameraPoseFromObjectVectors(const cv::Mat& InRotation, const cv::Mat& InTranslation, FTransform& OutTransform) { // The input rotation and translation vectors are both 3x1 vectors that are computed by OpenCV in functions such as calibrateCamera() and solvePnP() // Together, they perform a change of basis from object coordinate space to camera coordinate space // The desired output transform is the pose of the camera in world space, which can be obtained by inverting/transposing the rotation and translation vectors // [R|t]' = [R'|-R'*t] // Where R is a 3x3 rotation matrix and t is a 3x1 translation vector // Convert input rotation from rodrigues notation to a 3x3 rotation matrix cv::Mat RotationMatrix3x3; cv::Rodrigues(InRotation, RotationMatrix3x3); // Invert/transpose to get the camera orientation cv::Mat CameraRotation = RotationMatrix3x3.t(); cv::Mat CameraTranslation = -RotationMatrix3x3.t() * InTranslation; FMatrix TransformationMatrix = FMatrix::Identity; // Add the rotation matrix to the transformation matrix for (int32 Column = 0; Column < 3; ++Column) { TransformationMatrix.SetColumn(Column, FVector(CameraRotation.at(Column, 0), CameraRotation.at(Column, 1), CameraRotation.at(Column, 2))); } // Add the translation vector to the transformation matrix TransformationMatrix.M[3][0] = CameraTranslation.at(0); TransformationMatrix.M[3][1] = CameraTranslation.at(1); TransformationMatrix.M[3][2] = CameraTranslation.at(2); OutTransform.SetFromMatrix(TransformationMatrix); // Convert the output FTransform to UE's coordinate system FOpenCVHelper::ConvertOpenCVToUnreal(OutTransform); } void FOpenCVHelper::MakeObjectVectorsFromCameraPose(const FTransform& InTransform, cv::Mat& OutRotation, cv::Mat& OutTranslation) { // Convert the input FTransform to OpenCV's coordinate system FTransform CvTransform = InTransform; ConvertUnrealToOpenCV(CvTransform); const FMatrix TransformationMatrix = CvTransform.ToMatrixNoScale(); // Extract the translation vector from the transformation matrix cv::Mat CameraTranslation = cv::Mat(3, 1, CV_64FC1); CameraTranslation.at(0) = TransformationMatrix.M[3][0]; CameraTranslation.at(1) = TransformationMatrix.M[3][1]; CameraTranslation.at(2) = TransformationMatrix.M[3][2]; // Extract the rotation matrix from the transformation matrix cv::Mat CameraRotation = cv::Mat(3, 3, CV_64FC1); for (int32 Column = 0; Column < 3; ++Column) { const FVector ColumnVector = TransformationMatrix.GetColumn(Column); CameraRotation.at(Column, 0) = ColumnVector.X; CameraRotation.at(Column, 1) = ColumnVector.Y; CameraRotation.at(Column, 2) = ColumnVector.Z; } // Invert/transpose to get the rotation and translation that perform a change of basis from object coordinate space to camera coordinate space cv::Mat RotationMatrix3x3 = CameraRotation.t(); OutTranslation = -CameraRotation.inv() * CameraTranslation; // Convert the 3x3 rotation matrix to rodrigues notation cv::Rodrigues(RotationMatrix3x3, OutRotation); } #endif // WITH_OPENCV bool FOpenCVHelper::IdentifyArucoMarkers(TArray& Image, FIntPoint ImageSize, EArucoDictionary DictionaryName, TArray& OutMarkers) { #if WITH_OPENCV // Initialize an OpenCV matrix header to point at the input image data. // The format for FColor is B8G8R8A8, so the corresponding OpenCV format is CV_8UC4 (8-bit per channel, 4 channels) cv::Mat ImageMat = cv::Mat(ImageSize.Y, ImageSize.X, CV_8UC4, Image.GetData()); // Convert the image to grayscale before attempting to detect markers cv::Mat GrayImage; cv::cvtColor(ImageMat, GrayImage, cv::COLOR_RGBA2GRAY); // We do not know the number of markers expected in the image, so we cannot yet reserve the necessary space for our marker data and create a convenient matrix header for that data. // Instead, we will pass unitialized vectors to opencv and copy the data into our final data structure after the markers have been identified. std::vector MarkerIds; std::vector> Corners; cv::Ptr Dictionary = UE::Aruco::Private::GetArucoDictionary(DictionaryName); if (!Dictionary) { return false; } cv::Ptr DetectorParameters = cv::aruco::DetectorParameters::create(); DetectorParameters->cornerRefinementMethod = cv::aruco::CORNER_REFINE_SUBPIX; cv::aruco::detectMarkers(GrayImage, Dictionary, Corners, MarkerIds, DetectorParameters); OutMarkers.Empty(); const int32 NumMarkers = MarkerIds.size(); if (NumMarkers < 1) { return false; } OutMarkers.Reserve(NumMarkers); // Copy the detected marker data into the final data structure for (int32 MarkerIndex = 0; MarkerIndex < NumMarkers; ++MarkerIndex) { FArucoMarker NewMarker; NewMarker.MarkerID = MarkerIds[MarkerIndex]; const std::vector& MarkerCorners = Corners[MarkerIndex]; NewMarker.Corners[0] = FVector2f(MarkerCorners[0].x, MarkerCorners[0].y); // TopLeft NewMarker.Corners[1] = FVector2f(MarkerCorners[1].x, MarkerCorners[1].y); // TopRight NewMarker.Corners[2] = FVector2f(MarkerCorners[2].x, MarkerCorners[2].y); // BottomRight NewMarker.Corners[3] = FVector2f(MarkerCorners[3].x, MarkerCorners[3].y); // BottomLeft OutMarkers.Add(NewMarker); } return true; #else return false; #endif // WITH_OPENCV } bool FOpenCVHelper::DrawArucoMarkers(const TArray& Markers, UTexture2D* DebugTexture) { #if WITH_OPENCV if (!DebugTexture || Markers.IsEmpty()) { return false; } const int32 NumMarkers = Markers.Num(); std::vector MarkerIds; MarkerIds.reserve(NumMarkers); std::vector> Corners; Corners.reserve(NumMarkers); // Copy the detected marker data into the final data structure for (int32 MarkerIndex = 0; MarkerIndex < NumMarkers; ++MarkerIndex) { const FArucoMarker& Marker = Markers[MarkerIndex]; MarkerIds.push_back(Marker.MarkerID); std::vector MarkerCorners; MarkerCorners.reserve(4); MarkerCorners.push_back(cv::Point2f(Marker.Corners[0].X, Marker.Corners[0].Y)); MarkerCorners.push_back(cv::Point2f(Marker.Corners[1].X, Marker.Corners[1].Y)); MarkerCorners.push_back(cv::Point2f(Marker.Corners[2].X, Marker.Corners[2].Y)); MarkerCorners.push_back(cv::Point2f(Marker.Corners[3].X, Marker.Corners[3].Y)); Corners.push_back(MarkerCorners); } FTexture2DMipMap& Mip0 = DebugTexture->GetPlatformData()->Mips[0]; void* TextureData = Mip0.BulkData.Lock(LOCK_READ_WRITE); // Create a matrix header pointing to the raw texture data, copy the debug marker outlines into it, and update the texture resource cv::Mat TextureMat = cv::Mat(DebugTexture->GetSizeY(), DebugTexture->GetSizeX(), CV_8UC4, TextureData); // We need to convert from RGBA to RGB temporarily because cv::aruco::drawDetectedMarkers only supports images with 1 or 3 color channels (not 4) // We use a second matrix header because cvtColor will make a copy of the original data each time it changes the color format and we do not want to lose the reference to the original TextureData pointer. cv::Mat DebugMat; TextureMat.copyTo(DebugMat); cv::cvtColor(DebugMat, DebugMat, cv::COLOR_RGBA2RGB); cv::aruco::drawDetectedMarkers(DebugMat, Corners, MarkerIds); cv::cvtColor(DebugMat, DebugMat, cv::COLOR_RGB2RGBA); DebugMat.copyTo(TextureMat); Mip0.BulkData.Unlock(); DebugTexture->UpdateResource(); return true; #else return false; #endif // WITH_OPENCV } bool FOpenCVHelper::IdentifyCheckerboard(TArray& Image, FIntPoint ImageSize, FIntPoint CheckerboardDimensions, TArray& OutCorners) { FIntRect FullImageRect = FIntRect(FIntPoint(0), ImageSize); return IdentifyCheckerboard(Image, ImageSize, FullImageRect, CheckerboardDimensions, OutCorners); } bool FOpenCVHelper::IdentifyCheckerboard(TArray& Image, FIntPoint ImageSize, FIntRect RegionOfInterest, FIntPoint CheckerboardDimensions, TArray& OutCorners) { #if WITH_OPENCV // Initialize an OpenCV matrix header to point at the input image data. // The format for FColor is B8G8R8A8, so the corresponding OpenCV format is CV_8UC4 (8-bit per channel, 4 channels) const cv::Mat WholeImageMat = cv::Mat(ImageSize.Y, ImageSize.X, CV_8UC4, Image.GetData()); // Sanitize the ROI to ensure that it lies completely within the bounds of the full size image RegionOfInterest.Min.X = FMath::Clamp(RegionOfInterest.Min.X, 0, ImageSize.X); RegionOfInterest.Min.Y = FMath::Clamp(RegionOfInterest.Min.Y, 0, ImageSize.Y); RegionOfInterest.Max.X = FMath::Clamp(RegionOfInterest.Max.X, 0, ImageSize.X); RegionOfInterest.Max.Y = FMath::Clamp(RegionOfInterest.Max.Y, 0, ImageSize.Y); if ((RegionOfInterest.Width() <= 0) || (RegionOfInterest.Height() <= 0)) { OutCorners.Empty(); return false; } // Create a header for the region of interest const cv::Rect CvROI = cv::Rect(RegionOfInterest.Min.X, RegionOfInterest.Min.Y, RegionOfInterest.Size().X, RegionOfInterest.Size().Y); const cv::Mat ImageMat = cv::Mat(WholeImageMat, CvROI); // Convert the image to grayscale before attempting to detect checkerboard corners cv::Mat GrayImage; cv::cvtColor(ImageMat, GrayImage, cv::COLOR_RGBA2GRAY); const cv::Size CheckerboardSize = cv::Size(CheckerboardDimensions.X, CheckerboardDimensions.Y); const int32 NumExpectedCorners = CheckerboardDimensions.X * CheckerboardDimensions.Y; // ADAPTIVE_THRESH and NORMALIZE_IMAGE are both default options to aid in corner detection. // FAST_CHECK will quickly determine if there is any checkerboard in the image and early-out quickly if not const int32 FindCornersFlags = cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE | cv::CALIB_CB_FAST_CHECK; // Initialize the output array to hold the number of expected corners, then create a matrix header to the data to pass to OpenCV OutCorners.Init(FVector2f(), NumExpectedCorners); cv::Mat CornerMat = cv::Mat(NumExpectedCorners, 1, CV_32FC2, (void*)OutCorners.GetData()); const bool bFoundCorners = cv::findChessboardCorners(GrayImage, CheckerboardSize, CornerMat, FindCornersFlags); if (!bFoundCorners || OutCorners.Num() != NumExpectedCorners) { OutCorners.Empty(); return false; } // Attempt to refine the corner detection to subpixel accuracy. Algorithm settings are all defaults from the OpenCV documentation. const cv::TermCriteria Criteria = cv::TermCriteria(cv::TermCriteria::Type::EPS | cv::TermCriteria::Type::COUNT, 30, 0.001); const cv::Size HalfSearchWindowSize = cv::Size(11, 11); const cv::Size HalfDeadZoneSize = cv::Size(-1, -1); cv::cornerSubPix(GrayImage, CornerMat, HalfSearchWindowSize, HalfDeadZoneSize, Criteria); // The detected corner array can begin with either the TopLeft or BottomRight corner. If the BottomRight corner is first, reverse the order. if ((NumExpectedCorners >= 2) && (OutCorners[0].Y > OutCorners[NumExpectedCorners - 1].Y)) { Algo::Reverse(OutCorners); } // Adjust the detected corners to be relative to the full image, not the ROI for (FVector2f& Corner : OutCorners) { Corner += RegionOfInterest.Min; } return true; #else return false; #endif // WITH_OPENCV } bool FOpenCVHelper::DrawCheckerboardCorners(const TArray& Corners, FIntPoint CheckerboardDimensions, UTexture2D* DebugTexture) { #if WITH_OPENCV if (!DebugTexture || Corners.IsEmpty()) { return false; } cv::Mat CornerMat(Corners.Num(), 1, CV_32FC2, (void*)Corners.GetData()); FTexture2DMipMap& Mip0 = DebugTexture->GetPlatformData()->Mips[0]; void* TextureData = Mip0.BulkData.Lock(LOCK_READ_WRITE); // Create a matrix header pointing to the raw texture data, draw the debug pattern, and update the texture resource const FIntPoint TextureSize = FIntPoint(DebugTexture->GetSizeX(), DebugTexture->GetSizeY()); cv::Mat TextureMat = cv::Mat(TextureSize.Y, TextureSize.X, CV_8UC4, TextureData); constexpr bool bFoundPattern = true; cv::drawChessboardCorners(TextureMat, cv::Size(CheckerboardDimensions.X, CheckerboardDimensions.Y), CornerMat, bFoundPattern); Mip0.BulkData.Unlock(); DebugTexture->UpdateResource(); return true; #else return false; #endif // WITH_OPENCV } bool FOpenCVHelper::DrawCheckerboardCorners(const TArray& Corners, FIntPoint CheckerboardDimensions, UTexture2D* DebugTexture) { TArray CornersFloat; CornersFloat.Reserve(Corners.Num()); for (const FVector2D& Corner : Corners) { CornersFloat.Add(FVector2f(Corner.X, Corner.Y)); } return DrawCheckerboardCorners(CornersFloat, CheckerboardDimensions, DebugTexture); } bool FOpenCVHelper::SolvePnP(const TArray& ObjectPoints, const TArray& ImagePoints, const FVector2D& FocalLength, const FVector2D& ImageCenter, const TArray& DistortionParameters, FTransform& OutCameraPose) { #if WITH_OPENCV const int32 NumPoints = ObjectPoints.Num(); if (!ensureMsgf((ImagePoints.Num() == NumPoints), TEXT("The number of 3D object points must match the number of 2D image points being passed to solvePnP()"))) { return false; } // For non-planar sets of 3D points, solvePnP requires a minimum of 6 points to compute the direct linear transformation (DLT) constexpr int32 MinimumPoints = 6; if (NumPoints < MinimumPoints) { UE_LOG(LogOpenCVHelper, Error, TEXT("SolvePnP requires a minimum of 6 3D/2D point correspondences, but only %d were provided"), NumPoints); return false; } // cv::solvePnP() will only accept spherical distortion parameters, but it accepts a variable number of parameters based on how much of the distortion model is used // We need to guard against an incorrect number of parameters to avoid crashing in the opencv module const int32 NumDistortionParameters = DistortionParameters.Num(); const bool bCorrectNumDistortionParameters = (NumDistortionParameters == 0) || (NumDistortionParameters == 4) || (NumDistortionParameters == 5) || (NumDistortionParameters == 8) || (NumDistortionParameters == 12) || (NumDistortionParameters == 14); if (!ensureMsgf(bCorrectNumDistortionParameters, TEXT("The number of distortion parameters being passed to solvePnP() is invalid"))) { return false; } // Convert from UE coordinates to OpenCV coordinates TArray CvObjectPoints; CvObjectPoints.Reserve(NumPoints); for (const FVector& ObjectPoint : ObjectPoints) { CvObjectPoints.Add(FOpenCVHelper::ConvertUnrealToOpenCV(ObjectPoint)); } cv::Mat ObjectPointsMat(NumPoints, 1, CV_64FC3, (void*)CvObjectPoints.GetData()); cv::Mat ImagePointsMat(NumPoints, 1, CV_32FC2, (void*)ImagePoints.GetData()); cv::Mat DistortionMat(NumDistortionParameters, 1, CV_32FC1, (void*)DistortionParameters.GetData()); // Initialize the camera matrix that will be used in each call to projectPoints() cv::Mat CameraMatrix = cv::Mat::eye(3, 3, CV_64F); CameraMatrix.at(0, 0) = FocalLength.X; CameraMatrix.at(1, 1) = FocalLength.Y; CameraMatrix.at(0, 2) = ImageCenter.X; CameraMatrix.at(1, 2) = ImageCenter.Y; // Solve for camera position cv::Mat Rotation; cv::Mat Translation; // We send no distortion parameters, because Points2d was manually undistorted already if (!cv::solvePnP(ObjectPointsMat, ImagePointsMat, CameraMatrix, DistortionMat, Rotation, Translation)) { return false; } // Convert the OpenCV rotation and translation vectors into an FTransform in UE's coordinate system MakeCameraPoseFromObjectVectors(Rotation, Translation, OutCameraPose); return true; #else return false; #endif // WITH_OPENCV } bool FOpenCVHelper::ProjectPoints(const TArray& ObjectPoints, const FVector2D& FocalLength, const FVector2D& ImageCenter, const TArray& DistortionParameters, const FTransform& CameraPose, TArray& OutImagePoints) { TArray OutImagePointsDoublePrecision; if (ProjectPoints(ObjectPoints, FocalLength, ImageCenter, DistortionParameters, CameraPose, OutImagePointsDoublePrecision)) { for (const FVector2D& Point : OutImagePointsDoublePrecision) { OutImagePoints.Add(FVector2f(Point.X, Point.Y)); } return true; } return false; } bool FOpenCVHelper::ProjectPoints(const TArray& ObjectPoints, const FVector2D& FocalLength, const FVector2D& ImageCenter, const TArray& DistortionParameters, const FTransform& CameraPose, TArray& OutImagePoints) { #if WITH_OPENCV const int32 NumPoints = ObjectPoints.Num(); // Convert from UE coordinates to OpenCV coordinates TArray CvObjectPoints; CvObjectPoints.Reserve(NumPoints); for (const FVector& ObjectPoint : ObjectPoints) { CvObjectPoints.Add(FOpenCVHelper::ConvertUnrealToOpenCV(ObjectPoint)); } cv::Mat ObjectPointsMat = cv::Mat(NumPoints, 1, CV_64FC3, (void*)CvObjectPoints.GetData()); cv::Mat Rotation; cv::Mat Translation; FOpenCVHelper::MakeObjectVectorsFromCameraPose(CameraPose, Rotation, Translation); // Initialize the camera matrix that will be used in each call to projectPoints() cv::Mat CameraMatrix = cv::Mat::eye(3, 3, CV_64F); CameraMatrix.at(0, 0) = FocalLength.X; CameraMatrix.at(1, 1) = FocalLength.Y; CameraMatrix.at(0, 2) = ImageCenter.X; CameraMatrix.at(1, 2) = ImageCenter.Y; cv::Mat DistortionParametersMat = cv::Mat(DistortionParameters.Num(), 1, CV_32FC1, (void*)DistortionParameters.GetData()); OutImagePoints.Init(FVector2D(0.0, 0.0), NumPoints); cv::Mat ImagePointsMat = cv::Mat(NumPoints, 1, CV_64FC2, (void*)OutImagePoints.GetData()); cv::projectPoints(ObjectPointsMat, Rotation, Translation, CameraMatrix, DistortionParametersMat, ImagePointsMat); return true; #else return false; #endif // WITH_OPENCV } bool FOpenCVHelper::FitLine3D(const TArray& InPoints, FVector& OutLine, FVector& OutPointOnLine) { #if WITH_OPENCV const int32 NumPoints = InPoints.Num(); if (NumPoints < 2) { return false; } cv::Mat PointsMat(NumPoints, 1, CV_64FC3, (void*)InPoints.GetData()); // Find a best fit line between the 3D points, producing a line (the optical axis) and a point on that line constexpr int CParam = 0; // fitline parameter of 0 will try to find an optimal value for the simple euclidean distance method of DIST_L2 constexpr double RadiusAccuracy = 0.01; // cv documentation specifies 0.01 as a good default value constexpr double AngleAccuracy = 0.01; // cv documentation specifies 0.01 as a good default value cv::Vec6f LineAndPoint; cv::fitLine(PointsMat, LineAndPoint, cv::DIST_L2, CParam, RadiusAccuracy, AngleAccuracy); OutLine = FVector(LineAndPoint[0], LineAndPoint[1], LineAndPoint[2]); OutPointOnLine = FVector(LineAndPoint[3], LineAndPoint[4], LineAndPoint[5]); return true; #else return false; #endif // WITH_OPENCV } #if WITH_OPENCV double FOpenCVHelper::ComputeReprojectionError(const FTransform& CameraPose, const cv::Mat& CameraIntrinsicMatrix, const std::vector& Points3d, const std::vector& Points2d) { // Ensure that the number of point correspondences is valid const int32 NumPoints3d = Points3d.size(); const int32 NumPoints2d = Points2d.size(); if ((NumPoints3d == 0) || (NumPoints2d == 0) || (NumPoints3d != NumPoints2d)) { return -1.0; } const FMatrix CameraPoseMatrix = CameraPose.ToMatrixNoScale(); const cv::Mat Tcam = (cv::Mat_(3, 1) << CameraPoseMatrix.M[3][0], CameraPoseMatrix.M[3][1], CameraPoseMatrix.M[3][2]); cv::Mat Rcam = cv::Mat::zeros(3, 3, cv::DataType::type); for (int32 Column = 0; Column < 3; ++Column) { FVector ColVec = CameraPoseMatrix.GetColumn(Column); Rcam.at(Column, 0) = ColVec.X; Rcam.at(Column, 1) = ColVec.Y; Rcam.at(Column, 2) = ColVec.Z; } const cv::Mat Robj = Rcam.t(); cv::Mat Rrod; cv::Rodrigues(Robj, Rrod); const cv::Mat Tobj = -Rcam.inv() * Tcam; std::vector ReprojectedPoints2d; // The 2D points will be compared against the undistorted 2D points, so the distortion coefficients can be ignored cv::projectPoints(Points3d, Rrod, Tobj, CameraIntrinsicMatrix, cv::noArray(), ReprojectedPoints2d); if (ReprojectedPoints2d.size() != NumPoints2d) { return -1.0; } // Compute euclidean distance between captured 2D points and reprojected 2D points to measure reprojection error double ReprojectionError = 0.0; for (int32 Index = 0; Index < NumPoints2d; ++Index) { const cv::Point2f& A = Points2d[Index]; const cv::Point2f& B = ReprojectedPoints2d[Index]; const cv::Point2f Diff = A - B; ReprojectionError += (Diff.x * Diff.x) + (Diff.y * Diff.y); // cv::norm with NORM_L2SQR } return ReprojectionError; } #endif double FOpenCVHelper::ComputeReprojectionError(const TArray& ObjectPoints, const TArray& ImagePoints, const FVector2D& FocalLength, const FVector2D& ImageCenter, const FTransform& CameraPose) { #if WITH_OPENCV // Ensure that the number of point correspondences is valid const int32 NumPoints3d = ObjectPoints.Num(); const int32 NumPoints2d = ImagePoints.Num(); if ((NumPoints3d == 0) || (NumPoints2d == 0) || (NumPoints3d != NumPoints2d)) { return -1.0; } // Initialize the camera matrix that will be used in each call to projectPoints() cv::Mat CameraMatrix = cv::Mat::eye(3, 3, CV_64F); CameraMatrix.at(0, 0) = FocalLength.X; CameraMatrix.at(1, 1) = FocalLength.Y; CameraMatrix.at(0, 2) = ImageCenter.X; CameraMatrix.at(1, 2) = ImageCenter.Y; cv::Mat Rotation; cv::Mat Translation; FOpenCVHelper::MakeObjectVectorsFromCameraPose(CameraPose, Rotation, Translation); // cv::projectPoints requires that the 3D points and 2D points have the same bit depth, so we have to make a copy of the ObjectPoints with only 32-bits of precision TArray CvObjectPoints; for (const FVector& Point : ObjectPoints) { FVector CvPoint = ConvertUnrealToOpenCV(Point); CvObjectPoints.Add(FVector3f(CvPoint.X, CvPoint.Y, CvPoint.Z)); } TArray ProjectedPoints; ProjectedPoints.Init(FVector2f(), NumPoints2d); cv::Mat ObjectPointsMat = cv::Mat(ObjectPoints.Num(), 1, CV_32FC3, (void*)CvObjectPoints.GetData()); cv::Mat ProjectedPointsMat = cv::Mat(ProjectedPoints.Num(), 1, CV_32FC2, (void*)ProjectedPoints.GetData()); // The 2D points will be compared against the undistorted 2D points, so the distortion coefficients can be ignored cv::projectPoints(ObjectPointsMat, Rotation, Translation, CameraMatrix, cv::noArray(), ProjectedPointsMat); if (ProjectedPoints.Num() != NumPoints2d) { return -1.0; } // Compute euclidean distance between captured 2D points and reprojected 2D points to measure reprojection error double ReprojectionError = 0.0; for (int32 Index = 0; Index < NumPoints2d; ++Index) { const FVector2f Diff = ProjectedPoints[Index] - ImagePoints[Index]; ReprojectionError += (Diff.X * Diff.X) + (Diff.Y * Diff.Y); // cv::norm with NORM_L2SQR } return ReprojectionError; #else return -1.0f; #endif // WITH_OPENCV } #if WITH_OPENCV cv::Mat FOpenCVLensDistortionParametersBase::ConvertToOpenCVDistortionCoefficients() const { if (bUseFisheyeModel) { cv::Mat DistortionCoefficients(1, 4, CV_64F); DistortionCoefficients.at(0) = K1; DistortionCoefficients.at(1) = K2; DistortionCoefficients.at(2) = K3; DistortionCoefficients.at(3) = K4; return DistortionCoefficients; } else { cv::Mat DistortionCoefficients(1, 8, CV_64F); DistortionCoefficients.at(0) = K1; DistortionCoefficients.at(1) = K2; DistortionCoefficients.at(2) = P1; DistortionCoefficients.at(3) = P2; DistortionCoefficients.at(4) = K3; DistortionCoefficients.at(5) = K4; DistortionCoefficients.at(6) = K5; DistortionCoefficients.at(7) = K6; return DistortionCoefficients; } } cv::Mat FOpenCVLensDistortionParametersBase::CreateOpenCVCameraMatrix(const FVector2D& InImageSize) const { cv::Mat CameraMatrix = cv::Mat::eye(3, 3, CV_64F); CameraMatrix.at(0, 0) = F.X * InImageSize.X; CameraMatrix.at(1, 1) = F.Y * InImageSize.Y; CameraMatrix.at(0, 2) = C.X * InImageSize.X; CameraMatrix.at(1, 2) = C.Y * InImageSize.Y; return CameraMatrix; } #endif // WITH_OPENCV