Tips
-
For standard use of texture mapping, you can refer to the masking example: Mapping2DImageToDepthMap · GitHub. If you need to obtain a point cloud that matches the 2D image resolution and want RGB information in areas without point cloud data, you can refer to the method in this document (this method is slightly slower than the masking example due to interpolation steps).
-
This document example is based on the Mech-Eye SDK 2.1.0 interface. You can refer to the user manual: C++ (Windows) Mech-Eye SDK 2.1.0.
-
If you are using Mech-Eye SDK 2.2.0 or later versions, there are changes in interface names and header file names. For specific modifications, refer to: Migration Guide (from version 2.1.0 to 2.2.0) (mech-mind.net). You can also refer to the user manual for Mech-Eye SDK 2.2.0: C++ (Windows) Mech-Eye SDK 2.2.0.
-
After version 2.2.0 of Mech-Eye SDK, you can obtain a depth source 2D image that fully corresponds with the depth map. Refer to the example (using C++ as an example): CaptureStereo2DImages (github.com).
Background
In deep learning model training, when the resolution of the camera used to generate depth maps does not match the resolution of the color image used for texture mapping, it becomes challenging to find corresponding depth map coordinates in the color image’s reference frame. This, in turn, affects the acquisition of point cloud coordinates.
To solve this problem, we can perform transformations and interpolation operations by reading the camera intrinsic parameters, allowing us to calculate the corresponding point cloud coordinates.
Solution
Below is an sample in C++.
This sample ultimately saves the original depth map, the original 2D color texture image, the depth map in the 2D color texture camera reference frame, and the corresponding point cloud for the 2D texture image while outputting the corresponding coordinates.
#include <iostream>
#include "MechEyeApi.h"
#include "SampleUtil.h"
#include "OpenCVUtil.h"
#include <opencv2/opencv.hpp>
#include <fstream>
#include <vector>
// Define a data structure for point clouds, including 3D coordinates and color information
struct PointCloud {
cv::Point3f point;
cv::Vec3b color;
};
int main() {
// Create a MechEyeDevice object and connect to the camera
mmind::api::MechEyeDevice device;
mmind::api::DeviceIntri deviceIntri;
if (!findAndConnect(device))
return -1;
// Capture depth maps and color images
mmind::api::DepthMap depth;
mmind::api::ColorMap color;
showError(device.captureColorMap(color));
showError(device.captureDepthMap(depth));
// Save the depth maps and color images to files
const std::string depthFile = "depth.tiff";
const std::string colorFile = "color.png";
saveMap(depth, depthFile);
saveMap(color, colorFile);
// Load the depth maps and color images
cv::Mat depthImage = cv::imread("depth.tiff", cv::IMREAD_ANYDEPTH);
cv::Mat textureImage = cv::imread("color.png");
// Get and define camera matrices
showError(device.getDeviceIntri(deviceIntri));
//printCalibParams(deviceIntri);
cv::Mat textureCameraMatrix = cv::Mat::zeros(3, 3, CV_64FC1);
cv::Mat depthCameraMatrix = cv::Mat::zeros(3, 3, CV_64FC1);
cv::Mat rotationMatrix = cv::Mat::zeros(3, 3, CV_64FC1);
cv::Mat translationVector = cv::Mat::zeros(3, 1, CV_64FC1);
// Extract camera matrices from camera intrinsic parameters
textureCameraMatrix.at<double>(0, 0) = deviceIntri.textureCameraIntri.cameraMatrix[0];
textureCameraMatrix.at<double>(1, 1) = deviceIntri.textureCameraIntri.cameraMatrix[1];
textureCameraMatrix.at<double>(0, 2)= deviceIntri.textureCameraIntri.cameraMatrix[2];
textureCameraMatrix.at<double>(1, 2)= deviceIntri.textureCameraIntri.cameraMatrix[3];
textureCameraMatrix.at<double>(2, 2) = 1;
depthCameraMatrix.at<double>(0, 0) = deviceIntri.depthCameraIntri.cameraMatrix[0];
depthCameraMatrix.at<double>(1, 1) = deviceIntri.depthCameraIntri.cameraMatrix[1];
depthCameraMatrix.at<double>(0, 2) = deviceIntri.depthCameraIntri.cameraMatrix[2];
depthCameraMatrix.at<double>(1, 2) = deviceIntri.depthCameraIntri.cameraMatrix[3];
depthCameraMatrix.at<double>(2, 2) = 1;
// Extract rotation matrix and translation vector
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
rotationMatrix.at<double>(i, j)=deviceIntri.depthToTexture.rotation[i][j];
}
translationVector.at<double>(i, 0)= deviceIntri.depthToTexture.translation[i];
}
// Create a buffer to store point cloud mapped to the texture image
cv::Mat mappedImageBuffer(textureImage.size(), CV_32FC2, cv::Scalar(0, 0, 0));
int countprint = 0;
// Iterate through each pixel in the depth map, calculate point cloud, and map it to the texture image
for (int y = 0; y < depthImage.rows; ++y) {
for (int x = 0; x < depthImage.cols; ++x) {
// Get the depth value
float depthValue = depthImage.at<float>(y, x);
if (depthValue <= 0)
continue;
// Calculate the depth point in the depth camera reference frame
cv::Mat depthPoint = (cv::Mat_<double>(3, 1) <<
depthValue * (x - depthCameraMatrix.at<double>(0, 2)) / depthCameraMatrix.at<double>(0, 0),
depthValue * (y - depthCameraMatrix.at<double>(1, 2)) / depthCameraMatrix.at<double>(1, 1),
depthValue);
// Use rotation matrix and translation vector to map the depth point to the texture camera reference frame
cv::Mat texturePoint = rotationMatrix * depthPoint + translationVector;
// Project the point cloud onto the texture image using the texture camera matrix
cv::Mat textureImagePoint = textureCameraMatrix * texturePoint;
cv::Point2f textureImageCoords(
textureImagePoint.at<double>(0, 0) / textureImagePoint.at<double>(2, 0),
textureImagePoint.at<double>(1, 0) / textureImagePoint.at<double>(2, 0));
// Check if the projected coordinates are within the range of the texture image
if (textureImageCoords.x >= 0 && textureImageCoords.x < textureImage.cols &&
textureImageCoords.y >= 0 && textureImageCoords.y < textureImage.rows) {
// Average nearby pixels to reduce sampling error
int xCenter = std::round(textureImageCoords.x);
int yCenter = std::round(textureImageCoords.y);
for (int i = -1; i < 2; i++) {
for (int j = -1; j < 2; j++) {
if (xCenter + j >= 0 && xCenter + j < textureImage.cols &&
yCenter + i >= 0 && yCenter + i < textureImage.rows) {
mappedImageBuffer.at<cv::Vec2f>(yCenter + i, xCenter + j)[0] += texturePoint.at<double>(2,0);
mappedImageBuffer.at<cv::Vec2f>(yCenter + i, xCenter + j)[1] += 1;
}
}
}
}
}
}
// Create the mapping result for the depth map
cv::Mat mappedImage(textureImage.size(), CV_32FC1, cv::Scalar(0, 0, 0));
// Convert the point cloud mapping result to the final depth map
for (int row = 0; row < mappedImageBuffer.rows; ++row) {
for (int col = 0; col < mappedImageBuffer.cols; ++col) {
if (mappedImageBuffer.at<cv::Vec2f>(row, col)[1] != 0)
mappedImage.at<float>(row, col) = mappedImageBuffer.at<cv::Vec2f>(row, col)[0] / mappedImageBuffer.at<cv::Vec2f>(row, col)[1];
}
}
// Display and save the mapped depth map
cv::imshow("depthintexture", mappedImage);
cv::imwrite("depthintexture.tiff", mappedImage);
std::cout << "Save depth image : depthintexture.tiff" << std::endl;
// Create a container to store point cloud data
std::vector<PointCloud> pointCloudData;
cv::Mat inverseRotationMatrix;
bool invertable = cv::invert(rotationMatrix, inverseRotationMatrix);
if (!invertable)
{
printf("rotation not invertable");
return 0;
}
bool hasPrintedX = false;
// Iterate through each pixel in the interpolated depth map
for (int y = 0; y < mappedImage.rows; ++y) {
for (int x = 0; x < mappedImage.cols; ++x) {
// Get the depth value
float depthValueInTexture = mappedImage.at<float>(y, x);
// Get color information corresponding to the point in the color image
cv::Vec3b color = textureImage.at<cv::Vec3b>(y, x);
if (depthValueInTexture <= 0)
continue;
// Calculate the depth point's coordinates in the depth camera reference frame
cv::Mat depthPointInTexture = (cv::Mat_<double>(3, 1) <<
depthValueInTexture * (x - textureCameraMatrix.at<double>(0, 2)) / textureCameraMatrix.at<double>(0, 0),
depthValueInTexture * (y - textureCameraMatrix.at<double>(1, 2)) / textureCameraMatrix.at<double>(1, 1),
depthValueInTexture);
// Convert from millimeters (mm) to meters (m)
/*0.001 * depthValueInTexture * (x - textureCameraMatrix.at<double>(0, 2)) / textureCameraMatrix.at<double>(0, 0),
0.001 * depthValueInTexture * (y - textureCameraMatrix.at<double>(1, 2)) / textureCameraMatrix.at<double>(1, 1),
0.001 * depthValueInTexture);*/
// Use the rotation matrix and translation vector to map the point to the depth camera reference frame
cv::Mat pointInPattern = inverseRotationMatrix * (depthPointInTexture - translationVector);
// Create a new point cloud and save the point cloud information
PointCloud pointCloud;
pointCloud.point = cv::Point3f(pointInPattern.at<double>(0, 0),
pointInPattern.at<double>(1, 0),
pointInPattern.at<double>(2, 0));
pointCloud.color = color;
pointCloudData.push_back(pointCloud);
// Find point cloud coordinates corresponding to a specific pixel (e.g., 1000, 1000)
if (x == 1000 && y == 1000 && !hasPrintedX) {
std::cout << pointCloud.point.x << std::endl;
std::cout << pointCloud.point.y << std::endl;
std::cout << pointCloud.point.z << std::endl;
hasPrintedX = true;
}
}
}
// Save the point cloud data as a PLY file
std::ofstream outFile("PointCloud.ply");
if (outFile.is_open()) {
outFile << "ply\n";
outFile << "format ascii 1.0\n";
outFile << "element vertex " << pointCloudData.size() << "\n";
outFile << "property float x\n";
outFile << "property float y\n";
outFile << "property float z\n";
outFile << "property uchar red\n";
outFile << "property uchar green\n";
outFile << "property uchar blue\n";
outFile << "end_header\n";
for (const auto& point : pointCloudData) {
outFile << point.point.x << " " << point.point.y << " " << point.point.z << " ";
outFile << static_cast<int>(point.color[2]) << " " << static_cast<int>(point.color[1]) << " " << static_cast<int>(point.color[0]) << "\n";
}
std::cout << "Save PointCloud : PointCloud.ply" << std::endl;
outFile.close();
}
else {
std::cerr << "Failed to save the point cloud to the file.\n";
}
cv::waitKey(0);
return 0;
}