905 lines
31 KiB
C++
905 lines
31 KiB
C++
// 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<cv::aruco::Dictionary> 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<UTexture2D>(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<double>(Column, 0), CameraRotation.at<double>(Column, 1), CameraRotation.at<double>(Column, 2)));
|
|
}
|
|
|
|
// Add the translation vector to the transformation matrix
|
|
TransformationMatrix.M[3][0] = CameraTranslation.at<double>(0);
|
|
TransformationMatrix.M[3][1] = CameraTranslation.at<double>(1);
|
|
TransformationMatrix.M[3][2] = CameraTranslation.at<double>(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<double>(0) = TransformationMatrix.M[3][0];
|
|
CameraTranslation.at<double>(1) = TransformationMatrix.M[3][1];
|
|
CameraTranslation.at<double>(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<double>(Column, 0) = ColumnVector.X;
|
|
CameraRotation.at<double>(Column, 1) = ColumnVector.Y;
|
|
CameraRotation.at<double>(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<FColor>& Image, FIntPoint ImageSize, EArucoDictionary DictionaryName, TArray<FArucoMarker>& 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<int> MarkerIds;
|
|
std::vector<std::vector<cv::Point2f>> Corners;
|
|
|
|
cv::Ptr<cv::aruco::Dictionary> Dictionary = UE::Aruco::Private::GetArucoDictionary(DictionaryName);
|
|
if (!Dictionary)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
cv::Ptr<cv::aruco::DetectorParameters> 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<cv::Point2f>& 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<FArucoMarker>& Markers, UTexture2D* DebugTexture)
|
|
{
|
|
#if WITH_OPENCV
|
|
if (!DebugTexture || Markers.IsEmpty())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const int32 NumMarkers = Markers.Num();
|
|
|
|
std::vector<int> MarkerIds;
|
|
MarkerIds.reserve(NumMarkers);
|
|
|
|
std::vector<std::vector<cv::Point2f>> 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<cv::Point2f> 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<FColor>& Image, FIntPoint ImageSize, FIntPoint CheckerboardDimensions, TArray<FVector2f>& OutCorners)
|
|
{
|
|
FIntRect FullImageRect = FIntRect(FIntPoint(0), ImageSize);
|
|
return IdentifyCheckerboard(Image, ImageSize, FullImageRect, CheckerboardDimensions, OutCorners);
|
|
}
|
|
|
|
bool FOpenCVHelper::IdentifyCheckerboard(TArray<FColor>& Image, FIntPoint ImageSize, FIntRect RegionOfInterest, FIntPoint CheckerboardDimensions, TArray<FVector2f>& 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<FVector2f>& 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<FVector2D>& Corners, FIntPoint CheckerboardDimensions, UTexture2D* DebugTexture)
|
|
{
|
|
TArray<FVector2f> 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<FVector>& ObjectPoints, const TArray<FVector2f>& ImagePoints, const FVector2D& FocalLength, const FVector2D& ImageCenter, const TArray<float>& 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<FVector> 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<double>(0, 0) = FocalLength.X;
|
|
CameraMatrix.at<double>(1, 1) = FocalLength.Y;
|
|
CameraMatrix.at<double>(0, 2) = ImageCenter.X;
|
|
CameraMatrix.at<double>(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<FVector>& ObjectPoints, const FVector2D& FocalLength, const FVector2D& ImageCenter, const TArray<float>& DistortionParameters, const FTransform& CameraPose, TArray<FVector2f>& OutImagePoints)
|
|
{
|
|
TArray<FVector2D> 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<FVector>& ObjectPoints, const FVector2D& FocalLength, const FVector2D& ImageCenter, const TArray<float>& DistortionParameters, const FTransform& CameraPose, TArray<FVector2D>& OutImagePoints)
|
|
{
|
|
#if WITH_OPENCV
|
|
const int32 NumPoints = ObjectPoints.Num();
|
|
|
|
// Convert from UE coordinates to OpenCV coordinates
|
|
TArray<FVector> 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<double>(0, 0) = FocalLength.X;
|
|
CameraMatrix.at<double>(1, 1) = FocalLength.Y;
|
|
CameraMatrix.at<double>(0, 2) = ImageCenter.X;
|
|
CameraMatrix.at<double>(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<FVector>& 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<cv::Point3f>& Points3d, const std::vector<cv::Point2f>& 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_<double>(3, 1) << CameraPoseMatrix.M[3][0], CameraPoseMatrix.M[3][1], CameraPoseMatrix.M[3][2]);
|
|
|
|
cv::Mat Rcam = cv::Mat::zeros(3, 3, cv::DataType<double>::type);
|
|
for (int32 Column = 0; Column < 3; ++Column)
|
|
{
|
|
FVector ColVec = CameraPoseMatrix.GetColumn(Column);
|
|
Rcam.at<double>(Column, 0) = ColVec.X;
|
|
Rcam.at<double>(Column, 1) = ColVec.Y;
|
|
Rcam.at<double>(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<cv::Point2f> 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<FVector>& ObjectPoints, const TArray<FVector2f>& 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<double>(0, 0) = FocalLength.X;
|
|
CameraMatrix.at<double>(1, 1) = FocalLength.Y;
|
|
CameraMatrix.at<double>(0, 2) = ImageCenter.X;
|
|
CameraMatrix.at<double>(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<FVector3f> CvObjectPoints;
|
|
for (const FVector& Point : ObjectPoints)
|
|
{
|
|
FVector CvPoint = ConvertUnrealToOpenCV(Point);
|
|
CvObjectPoints.Add(FVector3f(CvPoint.X, CvPoint.Y, CvPoint.Z));
|
|
}
|
|
|
|
TArray<FVector2f> 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<double>(0) = K1;
|
|
DistortionCoefficients.at<double>(1) = K2;
|
|
DistortionCoefficients.at<double>(2) = K3;
|
|
DistortionCoefficients.at<double>(3) = K4;
|
|
return DistortionCoefficients;
|
|
}
|
|
else
|
|
{
|
|
cv::Mat DistortionCoefficients(1, 8, CV_64F);
|
|
DistortionCoefficients.at<double>(0) = K1;
|
|
DistortionCoefficients.at<double>(1) = K2;
|
|
DistortionCoefficients.at<double>(2) = P1;
|
|
DistortionCoefficients.at<double>(3) = P2;
|
|
DistortionCoefficients.at<double>(4) = K3;
|
|
DistortionCoefficients.at<double>(5) = K4;
|
|
DistortionCoefficients.at<double>(6) = K5;
|
|
DistortionCoefficients.at<double>(7) = K6;
|
|
return DistortionCoefficients;
|
|
}
|
|
}
|
|
|
|
cv::Mat FOpenCVLensDistortionParametersBase::CreateOpenCVCameraMatrix(const FVector2D& InImageSize) const
|
|
{
|
|
cv::Mat CameraMatrix = cv::Mat::eye(3, 3, CV_64F);
|
|
CameraMatrix.at<double>(0, 0) = F.X * InImageSize.X;
|
|
CameraMatrix.at<double>(1, 1) = F.Y * InImageSize.Y;
|
|
CameraMatrix.at<double>(0, 2) = C.X * InImageSize.X;
|
|
CameraMatrix.at<double>(1, 2) = C.Y * InImageSize.Y;
|
|
return CameraMatrix;
|
|
}
|
|
#endif // WITH_OPENCV
|