0

I have a 3D depth camera placed above three moving belt lanes and I'm trying to rotate the depth image (or the point cloud) so that the three lanes match the camera's angle. I'm not experienced at all with point clouds, but after some research I've tried to do the following:

  1. Acquire an XYZ cartesian image from the sensor which I turn into a point cloud vector.
  2. Define three points on the point cloud, one on each of the three lanes.
  3. Fit a plane through them by finding the plane coefficients.
  4. Finding the cross product between the plane and the z_normal, and then finding the angle of rotation.
  5. Use the Eigen library to transform the PCL cloud and turn it back into an openCV Mat.

For whatever reason, I always end up with a bad image with max-int values on one side and zeros on the other. I'm not certain anymore if there's something wrong with the code or if the method above is incorrect to start with.

My code so far:

// helper functions
pcl::PointCloud<pcl::PointXYZ>::Ptr MatToPcl(cv::Mat xyzMat);
cv::Mat PclToMat(pcl::PointCloud<pcl::PointXYZ>::Ptr point_cloud_ptr);
void colorize(cv::Mat& src, cv::Mat& dst);
void clip(cv::Mat& m, const uint16_t lowerBound, const uint16_t upperBound);

while(1)
{
    // camera framegrabber object to capture an image
    fg->SWTrigger();
    if (!fg->WaitForFrame(im.get(), 2000))
    {
        throw;
    }

    // openCV Mat declerations
    cv::Mat zDepth, zDepthColor;
    cv::Mat xyz = im->XYZImage();
    vector<cv::Mat> channels(3);
    cv::split(xyz, channels);
    zDepth = channels[0];

    cv::imwrite("xyzMat.png", xyz);
    cv::imwrite("depthImage.png", zDepth);
    clip(zDepth, 1250, 1400);
    colorise(zDepth, zDepthColor);
    cv::imwrite("depthColored.png", zDepthColor);

    // specify a 3D point on each lane
    cv::Point3i p1, p2, p3;

    p1.x = w / 4;
    p1.y = 24;
    p1.z = zDepth.at<uint16_t>(p1.x, p1.y);

    p2.x = w / 2;
    p2.y = 70;
    p2.z = zDepth.at<uint16_t>(p2.x, p2.y);

    p3.x = int(w * 0.75);
    p3.y = 114;
    p3.z = zDepth.at<uint16_t>(p3.x, p3.y);

    auto cross = (p2 - p1).cross(p3 - p1);

    // transform Mats to point clouds
    pcl::PointCloud<pcl::PointXYZ>::Ptr floor_plane, xyzCentered;
    floor_plane = MatToPcl(zDepth);

    Eigen::Matrix<float, 1, 3> floor_plane_normal_vector, xy_plane_normal_vector, rotation_vector;

    floor_plane_normal_vector[0] = cross.x;
    floor_plane_normal_vector[1] = cross.y;
    floor_plane_normal_vector[2] = cross.z;

    // specify the z normal from the xy-plane
    xy_plane_normal_vector[0] = 0.0;
    xy_plane_normal_vector[1] = 0.0;
    xy_plane_normal_vector[2] = 1.0;

    // cross product and normalize vector
    rotation_vector = xy_plane_normal_vector.cross(floor_plane_normal_vector);
    rotation_vector.normalized();

    // angle of rotation
    float theta = -atan2(rotation_vector.norm(), xy_plane_normal_vector.dot(floor_plane_normal_vector));

    // transform plane according to angle
    Eigen::Affine3f transform_2 = Eigen::Affine3f::Identity();

    transform_2.translation() << 0, 0, 30;
    transform_2.rotate(Eigen::AngleAxisf(theta, rotation_vector));

    pcl::transformPointCloud(*floor_plane, *xyzCentered, transform_2);

    // Pointcloud to Mat again
    cv::Mat xyzRot = PclToMat(xyzCentered);

    // clipLow and clipHigh values obtained from trackbars
    clip(xyzRot, clipLow, clipHigh);
    cv::Mat xyzRotColor;
    colorize(xyzRot, xyzRotColor)
    cv::imshow("result", xyzRotColor);
    cv::waitKey(1);
}

pcl::PointCloud<pcl::PointXYZ>::Ptr MatToPcl(cv::Mat xyzMat)
{
    /*
    *  Function: Get from a Mat to pcl pointcloud datatype
    *  In: cv::Mat
    *  Out: pcl::PointCloud
    */

    //char pr=100, pg=100, pb=100;
    pcl::PointCloud<pcl::PointXYZ>::Ptr point_cloud_ptr(new pcl::PointCloud<pcl::PointXYZ>);;

    vector<cv::Mat> channels(3);
    cv::split(xyzMat, channels);

    for (int i = 0; i < ifmXYZ.rows; i++)
    {
        for (int j = 0; j < ifmXYZ.cols; j++)
        {
            pcl::PointXYZ point;
            point.x = channels[0].at<short>(i,j);
            point.y = channels[1].at<short>(i, j);
            point.z = channels[2].at<short>(i, j);

            // when color needs to be added:
            //uint32_t rgb = (static_cast<uint32_t>(pr) << 16 | static_cast<uint32_t>(pg) << 8 | static_cast<uint32_t>(pb));
            //point.rgb = *reinterpret_cast<float*>(&rgb);

            point_cloud_ptr->points.push_back(point);
        }
    }

    point_cloud_ptr->width = (int)point_cloud_ptr->points.size();
    /*point_cloud_ptr->height = 1;*/

    return point_cloud_ptr;

}

// convert PCL to cv::Mat, taking only the depth values at z. 
cv::Mat PclToMat(pcl::PointCloud<pcl::PointXYZ>::Ptr point_cloud_ptr) 
{

    cv::Mat depth_image; 
    
    if (!depth_image.empty())
        depth_image.release();

    depth_image.create(132, 176, CV_32F);

    int count = 0;
    for (int i = 0; i < 132; i++)
    {
        for (int j = 0; j < 176; j++)
        {
            depth_image.at<float>(i, j) = point_cloud_ptr->points.at(count++).z;
        }
    }

    depth_image.convertTo(depth_image, CV_16UC1);
    return depth_image;
}

/* 
 * For display purposes with cv::imshow, will convert a 16bit depth image to 8bit 3 channel colored image
 * thanks to fmw42 for the function at https://stackoverflow.com/a/67678634/13184944
*/
void colorize(cv::Mat& src, cv::Mat& dst)
{
    // stretch the image by rescaling intensity within the output 8-bit range
    double oldMin;
    double oldMax;
    cv::Point minLoc;
    cv::Point maxLoc;

    cv::minMaxLoc(src, &oldMin, &oldMax, &minLoc, &maxLoc);

    double oldRange = oldMax - oldMin;

    double newMin = 0.0;
    double newMax = 255.0;

    double newRange = newMax - newMin;

    //cout << oldMin << ' ' << oldMax << ' ' << oldRange << '\n';
    

    // clip the values of the image to the required range
    clip(src, oldMin, oldMax);

    //TODO: Look at difference between OpenCV normalization and skimage
    normalize(src, dst, 0, 255, NORM_MINMAX, CV_8UC1);

    //img = (img - cv::Scalar(oldMin)) / (cv::Scalar(oldRange));
    //img = (img * cv::Scalar(newRange)) + cv::Scalar(newMin);

    cv::Mat channels[3] = { dst, dst, dst };
    cv::merge(channels, 3, dst);

    cv::Mat C(1, 6, CV_8UC(3));
    cv::Vec3b color1 = { 0,   0,   255   };
    cv::Vec3b color2 = { 0,   165, 255   };
    cv::Vec3b color3 = { 0,   255, 255   };
    cv::Vec3b color4 = { 255, 255, 0     };
    cv::Vec3b color5 = { 255, 0,   0     };
    cv::Vec3b color6 = { 128, 64,  64    };

    C.at<cv::Vec3b>(0, 0) = color1;
    C.at<cv::Vec3b>(0, 1) = color2;
    C.at<cv::Vec3b>(0, 2) = color3;
    C.at<cv::Vec3b>(0, 3) = color4;
    C.at<cv::Vec3b>(0, 4) = color5;
    C.at<cv::Vec3b>(0, 5) = color6;

    cv::Mat lut; 
    cv::resize(C, lut, cv::Size(256, 1), 0.0, 0.0, cv::INTER_LINEAR);
    //cout << lut.size << '\n';

    cv::LUT(dst, lut, dst);

    return;
}

void clip(cv::Mat& m, const uint16_t lowerBound, const uint16_t upperBound)
{
    m.setTo(lowerBound, m < lowerBound);
    m.setTo(upperBound, m > upperBound);
    return;
}

Apologies if this is really basic or something is obviously wrong but I feel stuck here. I also tried segmentation with ransac but the it never aligns the plane in the way I wanted.

Thanks!

Edit: Updated the code to include additional steps and functions. Only the camera initialization is skipped.

The clip and colorize functions aid in displaying the 16bit depth image. My end goal here is to be able to use trackbars with clip(zImg, low, high) where the three lanes will always be vertically aligns (as in, change color at the same rate) as I change the clip values.

download link with image files: link

Colorized depth image:

colorized 16bit image of three lanes under camera

Grudy
  • 69
  • 1
  • 6
  • 2
    Are you sure you about using integer coordinates? – Pepijn Kramer Dec 21 '21 at 19:00
  • It's how I usually access data in the depth image. I'm not aware if it works differently with Point Clouds. – Grudy Dec 21 '21 at 19:24
  • even if someone told you to use C++, consider python. C++ is awful for prototyping/exploration/learning. – Christoph Rackwitz Dec 21 '21 at 22:33
  • @ChristophRackwitz Totally agree. I prototype all my computer vision algorithms on python first but unfortunately the python bindings for this sensor are bugged (apparently will be fixed in the next release). Until then, I can only use C++ :) – Grudy Dec 21 '21 at 23:01
  • 1
    You might be interested in [Calling C or C++ From Python](https://realpython.com/python-bindings-overview/) to have the interfacing in C++ and doing calculations via Python (e.g. numpy is written in C). – JohanC Dec 21 '21 at 23:37
  • sorry for the tangent. focusing on the question. you should look at (edit your post and add) all the intermediate values in that code (since it's not a [mre]), and also all pictures that could possibly help get an understanding of the situation. – Christoph Rackwitz Dec 22 '21 at 09:17
  • Hey, no problem! I did get permission to add the depth image. I will also add all the functions I used to get to this point. I knew my code wasn't minimally complete, but it's quite the involved thing. I'll update it today. – Grudy Dec 22 '21 at 18:01

0 Answers0