// This file is part of OpenCV project. // It is subject to the license terms in the LICENSE file found in the top-level directory // of this distribution and at http://opencv.org/license.html #include "precomp.hpp" namespace cv { /////////////////////////////////////////////////////////////////////////////////// // Our three input types have a different value for a depth pixel with no depth template inline DepthDepth noDepthSentinelValue() { return 0; } template<> inline float noDepthSentinelValue() { return std::numeric_limits::quiet_NaN(); } template<> inline double noDepthSentinelValue() { return std::numeric_limits::quiet_NaN(); } /////////////////////////////////////////////////////////////////////////////////// // Testing for depth pixels with no depth isn't straightforward for NaN values. We // need to specialize the equality check for floats and doubles. template inline bool isEqualToNoDepthSentinelValue(const DepthDepth& value) { return value == noDepthSentinelValue(); } template<> inline bool isEqualToNoDepthSentinelValue(const float& value) { return cvIsNaN(value) != 0; } template<> inline bool isEqualToNoDepthSentinelValue(const double& value) { return cvIsNaN(value) != 0; } /////////////////////////////////////////////////////////////////////////////////// // When using the unsigned short representation, we'd like to round the values to the nearest // integer value. The float/double representations don't need to be rounded template inline DepthDepth floatToInputDepth(const float& value) { return (DepthDepth)value; } template<> inline unsigned short floatToInputDepth(const float& value) { return (unsigned short)(value + 0.5); } /////////////////////////////////////////////////////////////////////////////////// /** Computes a registered depth image from an unregistered image. * * @param unregisteredDepth the input depth data * @param unregisteredCameraMatrix the camera matrix of the depth camera * @param registeredCameraMatrix the camera matrix of the external camera * @param registeredDistCoeffs the distortion coefficients of the external camera * @param rbtRgb2Depth the rigid body transform between the cameras. * @param outputImagePlaneSize the image plane dimensions of the external camera (width, height) * @param depthDilation whether or not the depth is dilated to avoid holes and occlusion errors * @param inputDepthToMetersScale the scale needed to transform the input depth units to meters * @param registeredDepth the result of transforming the depth into the external camera */ template void performRegistration(const Mat_& unregisteredDepth, const Matx33f& unregisteredCameraMatrix, const Matx33f& registeredCameraMatrix, const Mat_& registeredDistCoeffs, const Matx44f& rbtRgb2Depth, const Size outputImagePlaneSize, const bool depthDilation, const float inputDepthToMetersScale, Mat& registeredDepth) { // Create output Mat of the correct type, filled with an initial value indicating no depth registeredDepth = Mat_(outputImagePlaneSize, noDepthSentinelValue()); // Figure out whether we'll have to apply a distortion bool hasDistortion = (countNonZero(registeredDistCoeffs) > 0); // A point (i,j,1) will have to be converted to 3d first, by multiplying it by K.inv() // It will then be transformed by rbtRgb2Depth. // Finally, it will be projected into the external camera via registeredCameraMatrix and // its distortion coefficients. If there is no distortion in the external camera, we // can linearly chain all three operations together. Matx44f K = Matx44f::zeros(); for (unsigned char j = 0; j < 3; ++j) for (unsigned char i = 0; i < 3; ++i) { K(j, i) = unregisteredCameraMatrix(j, i); } K(3, 3) = 1; Matx44f initialProjection; if (hasDistortion) { // The projection into the external camera will be done separately with distortion initialProjection = rbtRgb2Depth * K.inv(); } else { // No distortion, so all operations can be chained initialProjection = Matx44f::zeros(); for (unsigned char j = 0; j < 3; ++j) for (unsigned char i = 0; i < 3; ++i) initialProjection(j, i) = registeredCameraMatrix(j, i); initialProjection(3, 3) = 1; initialProjection = initialProjection * rbtRgb2Depth * K.inv(); } // Apply the initial projection to the input depth Mat_ transformedCloud; { Mat_ point_tmp(outputImagePlaneSize, Point3f(0., 0., 0.)); for (int j = 0; j < unregisteredDepth.rows; ++j) { const DepthDepth* unregisteredDepthPtr = unregisteredDepth[j]; Point3f* point = point_tmp[j]; for (int i = 0; i < unregisteredDepth.cols; ++i, ++unregisteredDepthPtr, ++point) { float rescaled_depth = float(*unregisteredDepthPtr) * inputDepthToMetersScale; // If the DepthDepth is of type unsigned short, zero is a sentinel value to indicate // no depth. CV_32F and CV_64F should already have NaN for no depth values. if (rescaled_depth == 0) { rescaled_depth = std::numeric_limits::quiet_NaN(); } point->x = i * rescaled_depth; point->y = j * rescaled_depth; point->z = rescaled_depth; } } perspectiveTransform(point_tmp, transformedCloud, initialProjection); } std::vector transformedAndProjectedPoints(transformedCloud.cols); const float metersToInputUnitsScale = 1 / inputDepthToMetersScale; const Rect registeredDepthBounds(Point(), outputImagePlaneSize); for (int y = 0; y < transformedCloud.rows; y++) { if (hasDistortion) { // Project an entire row of points with distortion. // Doing this for the entire image at once would require more memory. projectPoints(transformedCloud.row(y), Vec3f(0, 0, 0), Vec3f(0, 0, 0), registeredCameraMatrix, registeredDistCoeffs, transformedAndProjectedPoints); } else { // With no distortion, we just have to dehomogenize the point since all major transforms // already happened with initialProjection. Point2f* point2d = &transformedAndProjectedPoints[0]; const Point2f* point2d_end = point2d + transformedAndProjectedPoints.size(); const Point3f* point3d = transformedCloud[y]; for (; point2d < point2d_end; ++point2d, ++point3d) { point2d->x = point3d->x / point3d->z; point2d->y = point3d->y / point3d->z; } } const Point2f* outputProjectedPoint = &transformedAndProjectedPoints[0]; const Point3f* p = transformedCloud[y], * p_end = p + transformedCloud.cols; for (; p < p_end; ++outputProjectedPoint, ++p) { // Skip this one if there isn't a valid depth const Point2f projectedPixelFloatLocation = *outputProjectedPoint; if (cvIsNaN(projectedPixelFloatLocation.x)) continue; //Get integer pixel location const Point2i projectedPixelLocation = projectedPixelFloatLocation; // Ensure that the projected point is actually contained in our output image if (!registeredDepthBounds.contains(projectedPixelLocation)) continue; // Go back to our original scale, since that's what our output will be // The templated function is to ensure that integer values are rounded to the nearest integer const DepthDepth cloudDepth = floatToInputDepth(p->z * metersToInputUnitsScale); DepthDepth& outputDepth = registeredDepth.at(projectedPixelLocation.y, projectedPixelLocation.x); // Occlusion check if (isEqualToNoDepthSentinelValue(outputDepth) || (outputDepth > cloudDepth)) outputDepth = cloudDepth; // If desired, dilate this point to avoid holes in the final image if (depthDilation) { // Choosing to dilate in a 2x2 region, where the original projected location is in the bottom right of this // region. This is what's done on PrimeSense devices, but a more accurate scheme could be used. const Point2i dilatedProjectedLocations[3] = { Point2i(projectedPixelLocation.x - 1, projectedPixelLocation.y), Point2i(projectedPixelLocation.x , projectedPixelLocation.y - 1), Point2i(projectedPixelLocation.x - 1, projectedPixelLocation.y - 1) }; for (int i = 0; i < 3; i++) { const Point2i& dilatedCoordinates = dilatedProjectedLocations[i]; if (!registeredDepthBounds.contains(dilatedCoordinates)) continue; DepthDepth& outputDilatedDepth = registeredDepth.at(dilatedCoordinates.y, dilatedCoordinates.x); // Occlusion check if (isEqualToNoDepthSentinelValue(outputDilatedDepth) || (outputDilatedDepth > cloudDepth)) outputDilatedDepth = cloudDepth; } } // depthDilation } // iterate cols } // iterate rows } void registerDepth(InputArray unregisteredCameraMatrix, InputArray registeredCameraMatrix, InputArray registeredDistCoeffs, InputArray Rt, InputArray unregisteredDepth, const Size& outputImagePlaneSize, OutputArray registeredDepth, bool depthDilation) { CV_Assert(unregisteredCameraMatrix.depth() == CV_64F || unregisteredCameraMatrix.depth() == CV_32F); CV_Assert(registeredCameraMatrix.depth() == CV_64F || registeredCameraMatrix.depth() == CV_32F); CV_Assert(registeredDistCoeffs.empty() || registeredDistCoeffs.depth() == CV_64F || registeredDistCoeffs.depth() == CV_32F); CV_Assert(Rt.depth() == CV_64F || Rt.depth() == CV_32F); CV_Assert(unregisteredDepth.cols() > 0 && unregisteredDepth.rows() > 0 && (unregisteredDepth.depth() == CV_32F || unregisteredDepth.depth() == CV_64F || unregisteredDepth.depth() == CV_16U)); CV_Assert(outputImagePlaneSize.height > 0 && outputImagePlaneSize.width > 0); // Implicitly checking dimensions of the InputArrays Matx33f _unregisteredCameraMatrix = unregisteredCameraMatrix.getMat(); Matx33f _registeredCameraMatrix = registeredCameraMatrix.getMat(); Mat_ _registeredDistCoeffs = registeredDistCoeffs.getMat(); Matx44f _rbtRgb2Depth = Rt.getMat(); Mat& registeredDepthMat = registeredDepth.getMatRef(); switch (unregisteredDepth.depth()) { case CV_16U: { performRegistration(unregisteredDepth.getMat(), _unregisteredCameraMatrix, _registeredCameraMatrix, _registeredDistCoeffs, _rbtRgb2Depth, outputImagePlaneSize, depthDilation, .001f, registeredDepthMat); break; } case CV_32F: { performRegistration(unregisteredDepth.getMat(), _unregisteredCameraMatrix, _registeredCameraMatrix, _registeredDistCoeffs, _rbtRgb2Depth, outputImagePlaneSize, depthDilation, 1.0f, registeredDepthMat); break; } case CV_64F: { performRegistration(unregisteredDepth.getMat(), _unregisteredCameraMatrix, _registeredCameraMatrix, _registeredDistCoeffs, _rbtRgb2Depth, outputImagePlaneSize, depthDilation, 1.0f, registeredDepthMat); break; } default: { CV_Error(Error::StsUnsupportedFormat, "Input depth must be unsigned short, float, or double."); } } } } /* namespace cv */