mirror of
https://github.com/introlab/rtabmap.git
synced 2026-10-06 01:57:45 +08:00
1258 lines
49 KiB
C++
1258 lines
49 KiB
C++
/*
|
|
Copyright (c) 2010-2026, Mathieu Labbe - IntRoLab - Universite de Sherbrooke
|
|
All rights reserved.
|
|
|
|
Redistribution and use in source and binary forms, with or without
|
|
modification, are permitted provided that the following conditions are met:
|
|
* Redistributions of source code must retain the above copyright
|
|
notice, this list of conditions and the following disclaimer.
|
|
* Redistributions in binary form must reproduce the above copyright
|
|
notice, this list of conditions and the following disclaimer in the
|
|
documentation and/or other materials provided with the distribution.
|
|
* Neither the name of the Universite de Sherbrooke nor the
|
|
names of its contributors may be used to endorse or promote products
|
|
derived from this software without specific prior written permission.
|
|
|
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY
|
|
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// Refines the transform between a camera and a lidar without a target, from a database
|
|
// in which nodes hold an image and a lidar scan taken together, by aligning the lidar's
|
|
// depth discontinuities with the image's edges (as in J. Levinson and S. Thrun,
|
|
// "Automatic Online Calibration of Cameras and Lasers", RSS 2013).
|
|
//
|
|
// The result is a correction X of the camera's mount, in the camera's body frame (x
|
|
// forward, y left, z up): the robot base -> camera body transform B becomes B * X. It is
|
|
// estimated for the nodes all together, so that a node's own errors (time sync, odometry
|
|
// over the scan) average out. Internally, the solvers search for the same correction in
|
|
// the camera's optical frame, C = R^-1 * X * R (R: the optical rotation), in which the
|
|
// lidar is projected: each node's camera local transform T becomes T * C.
|
|
|
|
#include "CalibrationProblem.h"
|
|
#include "CorrectionSolver.h"
|
|
|
|
#include <rtabmap/core/DBDriver.h>
|
|
#include <rtabmap/core/Signature.h>
|
|
#include <rtabmap/core/Version.h>
|
|
#include <rtabmap/core/util3d.h>
|
|
#include <rtabmap/core/util3d_filtering.h>
|
|
#include <rtabmap/core/util3d_surface.h>
|
|
#include <rtabmap/core/util3d_transforms.h>
|
|
#include <rtabmap/utilite/ULogger.h>
|
|
#include <rtabmap/utilite/UDirectory.h>
|
|
#include <rtabmap/utilite/UConversion.h>
|
|
#include <rtabmap/utilite/UStl.h>
|
|
#include <rtabmap/utilite/UTimer.h>
|
|
|
|
#include <opencv2/imgproc.hpp>
|
|
#include <opencv2/imgcodecs.hpp>
|
|
#include <Eigen/Geometry>
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstring>
|
|
#include <functional>
|
|
#include <iterator>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <stdio.h>
|
|
#include <vector>
|
|
|
|
using namespace rtabmap;
|
|
|
|
void showUsage(const char * exec)
|
|
{
|
|
const std::vector<std::string> solvers = availableSolvers();
|
|
printf("\nUsage:\n"
|
|
"%s [Options] database.db\n"
|
|
" Refine the camera-lidar extrinsics of a database whose nodes have an image and\n"
|
|
" a lidar scan taken together, by aligning the lidar's edges (depth discontinuities,\n"
|
|
" creases and, if the scans have intensity, intensity edges) with the images' edges.\n"
|
|
" Prints a correction X of the camera's mount, in the camera's body frame (x forward,\n"
|
|
" y left, z up): the robot base -> camera transform B (e.g., base_link ->\n"
|
|
" camera_link) becomes B * X.\n"
|
|
"Options:\n"
|
|
" --solver \"name\" How the correction is searched for: \"simplex\" (OpenCV's\n"
|
|
" Nelder-Mead, default), \"pattern\" (one parameter at a\n"
|
|
" time, with decreasing steps), \"g2o\" or \"gtsam\" (least\n"
|
|
" squares on the distances to the image edges, if rtabmap is\n"
|
|
" built with them). Available in this build: %s.\n"
|
|
" --verbose Also print the sensitivity: how much the score drops with\n"
|
|
" the result off by 1 deg or 2 cm on each axis.\n"
|
|
" --translation Also estimate the translation. Off by default: unless the\n"
|
|
" scene is close compared to the lever arm between the\n"
|
|
" sensors, it is not observable (check the split result).\n"
|
|
" --images \"dir\" Save, for every node, its image with its edges (green) and\n"
|
|
" the lidar edges projected (depth: red, crease: blue,\n"
|
|
" intensity: yellow), and the lidar's intensity and surface\n"
|
|
" orientation (yellow: facing the camera, red: edge on),\n"
|
|
" before and after the correction, in this directory.\n"
|
|
" --decimation # Image decimation at which the lidar's depth discontinuities\n"
|
|
" are found, so that the projected scan is dense (default 4).\n"
|
|
" --jump #.# Relative depth jump for a lidar point to be on a\n"
|
|
" discontinuity, as a fraction of its depth: 0.15 (default)\n"
|
|
" means a neighbor at least 15% of this point's depth farther\n"
|
|
" than where this point's surface would continue.\n"
|
|
" --intensity_jump #.# Relative intensity change for a lidar point to be on an\n"
|
|
" intensity edge, as a fraction: 0.4 (default) means a\n"
|
|
" neighbor at least 1.4 times brighter or darker.\n"
|
|
" --intensity_weight #.# Weight of intensity edges relative to depth\n"
|
|
" discontinuities (default 1).\n"
|
|
" --crease_angle #.# Also use creases: lidar points where the surface normals\n"
|
|
" differ by this angle (deg), e.g., between a wall and the\n"
|
|
" floor (default 45, 0 disables).\n"
|
|
" --crease_voxel #.# Voxel size (m) of the scans on which normals are\n"
|
|
" computed, smoother than at full resolution (default 0.1).\n"
|
|
" --no_intensity Use only depth discontinuities, even if the scans have\n"
|
|
" intensity.\n"
|
|
" --sigma #.# Fall off (pixels) of the image edges' score (default 3).\n"
|
|
" --min_depth #.# Ignore lidar points closer to the camera (m, default 0.5).\n"
|
|
" --initial_rotation #.# #.# #.# Start from this correction (roll, pitch, yaw in\n"
|
|
" deg, in the camera's body frame) instead of none, e.g., to\n"
|
|
" see from how far the result is found again (default 0 0 0).\n"
|
|
" --max_angular_speed #.# Skip the nodes rotating faster than this (deg/s, mean\n"
|
|
" since the previous node, over which an assembled scan is\n"
|
|
" taken, from odometry; default 0: all).\n"
|
|
" --max_linear_speed #.# Skip the nodes moving faster than this (m/s; default\n"
|
|
" 0: all).\n"
|
|
" --voxel #.# Voxel filter the scans first (m, default 0: as stored), to\n"
|
|
" compare the result at several lidar densities.\n"
|
|
"\n", exec, uJoin(std::list<std::string>(solvers.begin(), solvers.end()), ", ").c_str());
|
|
exit(1);
|
|
}
|
|
|
|
void computeEdgeMaps(Frame & f, float sigma)
|
|
{
|
|
cv::Mat blurred;
|
|
cv::GaussianBlur(f.gray, blurred, cv::Size(5, 5), 1.5);
|
|
cv::Canny(blurred, f.edges, 40, 100);
|
|
cv::distanceTransform(f.edges == 0, f.edgeDistance, cv::DIST_L2, 3);
|
|
// Smooth enough for a local search to follow, highest on the edges.
|
|
cv::exp(-f.edgeDistance / sigma, f.edgeScore);
|
|
}
|
|
|
|
// The scan as seen by the camera at scanToCam * C, at the image's resolution divided by
|
|
// decimation, where the projected scan is dense.
|
|
struct CellMaps
|
|
{
|
|
cv::Mat depth; // CV_32F, the nearest point's depth, 0 where none
|
|
cv::Mat index; // CV_32S, the nearest point's index in the cloud, -1 where none
|
|
cv::Mat logIntensity; // CV_32F, empty if not computed
|
|
cv::Mat normals; // CV_32FC3, in the camera frame, not normalized, zero where unknown; empty if not computed
|
|
};
|
|
|
|
CellMaps computeCellMaps(const Frame & f, const Transform & C, int decimation, bool withIntensity,
|
|
bool withNormals, float normalsVoxel, float minDepth)
|
|
{
|
|
CellMaps maps;
|
|
const int w = f.gray.cols / decimation, h = f.gray.rows / decimation;
|
|
const double fx = f.K.at<double>(0,0) / decimation, fy = f.K.at<double>(1,1) / decimation;
|
|
const double cx = f.K.at<double>(0,2) / decimation, cy = f.K.at<double>(1,2) / decimation;
|
|
const Transform scanInCam = (f.scanToCam * C).inverse();
|
|
|
|
// Nearest point per pixel, and which one it is.
|
|
cv::Mat depth(h, w, CV_32F, cv::Scalar(0));
|
|
cv::Mat index(h, w, CV_32S, cv::Scalar(-1));
|
|
for(int i = 0; i < f.cloud.rows; ++i)
|
|
{
|
|
const float * p = f.cloud.ptr<float>(i);
|
|
const cv::Point3f pc = util3d::transformPoint(cv::Point3f(p[0], p[1], p[2]), scanInCam);
|
|
if(pc.z < minDepth)
|
|
{
|
|
continue;
|
|
}
|
|
const int u = int(fx * pc.x / pc.z + cx), v = int(fy * pc.y / pc.z + cy);
|
|
if(u < 0 || v < 0 || u >= w || v >= h)
|
|
{
|
|
continue;
|
|
}
|
|
float & d = depth.at<float>(v, u);
|
|
if(d == 0 || pc.z < d)
|
|
{
|
|
d = pc.z;
|
|
index.at<int>(v, u) = i;
|
|
}
|
|
}
|
|
|
|
// Log-intensity, so that a change is relative and intensity's fall off with range
|
|
// matters less. A cell's is the mean over all the points of the surface it sees
|
|
// (within 5% of its nearest point's depth), not its nearest point's alone: the
|
|
// beams of a lidar do not return the same intensity from the same surface, and a
|
|
// node's scan assembles several sweeps, so neighboring cells seen by different beams
|
|
// would otherwise differ, making false edges along the beams' traces. Then median
|
|
// filtered against what speckle remains.
|
|
cv::Mat logIntensity;
|
|
if(withIntensity && f.hasIntensity)
|
|
{
|
|
cv::Mat sum(h, w, CV_32F, cv::Scalar(0));
|
|
cv::Mat count(h, w, CV_32S, cv::Scalar(0));
|
|
for(int i = 0; i < f.cloud.rows; ++i)
|
|
{
|
|
const float * p = f.cloud.ptr<float>(i);
|
|
const cv::Point3f pc = util3d::transformPoint(cv::Point3f(p[0], p[1], p[2]), scanInCam);
|
|
if(pc.z < minDepth)
|
|
{
|
|
continue;
|
|
}
|
|
const int u = int(fx * pc.x / pc.z + cx), v = int(fy * pc.y / pc.z + cy);
|
|
if(u < 0 || v < 0 || u >= w || v >= h || pc.z > depth.at<float>(v, u) * 1.05f)
|
|
{
|
|
continue;
|
|
}
|
|
sum.at<float>(v, u) += std::log(1.0f + std::max(0.0f, p[3]));
|
|
++count.at<int>(v, u);
|
|
}
|
|
logIntensity = cv::Mat(h, w, CV_32F, cv::Scalar(0));
|
|
for(int v = 0; v < h; ++v)
|
|
{
|
|
for(int u = 0; u < w; ++u)
|
|
{
|
|
const int n = count.at<int>(v, u);
|
|
if(n > 0)
|
|
{
|
|
logIntensity.at<float>(v, u) = sum.at<float>(v, u) / n;
|
|
}
|
|
}
|
|
}
|
|
cv::medianBlur(logIntensity, logIntensity, 3);
|
|
}
|
|
|
|
// Surface normals, in the camera frame, turned toward it: a cell's is the mean over
|
|
// the (voxelized) points of the surface it sees, as for intensity. A voxel stands for
|
|
// the surface over its whole size, which can span several cells: its normal is spread
|
|
// over the cells it covers, else most cells would have none where the voxels are
|
|
// larger than the cells, and creases (which need all the neighbors' normals) be missed.
|
|
cv::Mat cellNormals; // CV_32FC3, zero where unknown
|
|
if(withNormals && !f.normals.empty())
|
|
{
|
|
const Eigen::Matrix3f rotation = scanInCam.toEigen3f().linear();
|
|
cellNormals = cv::Mat(h, w, CV_32FC3, cv::Scalar(0, 0, 0));
|
|
for(int i = 0; i < f.normals.rows; ++i)
|
|
{
|
|
const float * p = f.normals.ptr<float>(i);
|
|
const cv::Point3f pc = util3d::transformPoint(cv::Point3f(p[0], p[1], p[2]), scanInCam);
|
|
if(pc.z < minDepth || !std::isfinite(p[3]))
|
|
{
|
|
continue;
|
|
}
|
|
Eigen::Vector3f n = rotation * Eigen::Vector3f(p[3], p[4], p[5]);
|
|
if(n.dot(Eigen::Vector3f(pc.x, pc.y, pc.z)) > 0.0f)
|
|
{
|
|
n = -n; // toward the camera
|
|
}
|
|
const double pu = fx * pc.x / pc.z + cx, pv = fy * pc.y / pc.z + cy;
|
|
const double halfSize = 0.5 * normalsVoxel * fx / pc.z; // in cells
|
|
for(int v = std::max(0, int(std::floor(pv - halfSize))); v <= std::min(h - 1, int(std::floor(pv + halfSize))); ++v)
|
|
{
|
|
for(int u = std::max(0, int(std::floor(pu - halfSize))); u <= std::min(w - 1, int(std::floor(pu + halfSize))); ++u)
|
|
{
|
|
const float d = depth.at<float>(v, u);
|
|
if(d == 0 || pc.z > d * 1.05f || pc.z < d * 0.95f)
|
|
{
|
|
continue; // not the surface this cell sees
|
|
}
|
|
cellNormals.at<cv::Vec3f>(v, u) += cv::Vec3f(n.x(), n.y(), n.z());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
maps.depth = depth;
|
|
maps.index = index;
|
|
maps.logIntensity = logIntensity;
|
|
maps.normals = cellNormals;
|
|
return maps;
|
|
}
|
|
|
|
// Edge points of a cell map located at the image's full resolution, as Canny locates edges:
|
|
// the map (1 or 3 channels) is interpolated and smoothed at full resolution, and an edge is
|
|
// where its change is the largest across it (non-maximum suppression along the direction of
|
|
// largest change of its channels, as for a color image), in the cells where cellWeight > 0
|
|
// (the cells found on an edge at cell resolution, and how strong). At cell resolution, an
|
|
// edge is a band of a few cells (any cell differing enough from a neighbor), and the
|
|
// nearest point of a cell is anywhere in it. These edges are on continuous surfaces (not
|
|
// depth discontinuities): the depth is interpolated where they are found (inverse depth,
|
|
// linear on a plane), and they are turned back into 3D. One point per cell, the strongest.
|
|
void addFullResolutionEdges(Frame & f, const Transform & C, int decimation, const cv::Mat & depth,
|
|
const cv::Mat & map, const cv::Mat & cellWeight, unsigned char type)
|
|
{
|
|
const int w = depth.cols, h = depth.rows;
|
|
const int W = w * decimation, H = h * decimation;
|
|
const double FX = f.K.at<double>(0,0), FY = f.K.at<double>(1,1);
|
|
const double CX = f.K.at<double>(0,2), CY = f.K.at<double>(1,2);
|
|
const Transform camToScan = f.scanToCam * C;
|
|
cv::Mat smooth, dx, dy;
|
|
cv::resize(map, smooth, cv::Size(W, H), 0, 0, cv::INTER_LINEAR);
|
|
cv::GaussianBlur(smooth, smooth, cv::Size(0, 0), decimation / 2.0);
|
|
cv::Sobel(smooth, dx, CV_32F, 1, 0, 3);
|
|
cv::Sobel(smooth, dy, CV_32F, 0, 1, 3);
|
|
const int channels = smooth.channels();
|
|
// Largest rate of change and its direction (Di Zenzo).
|
|
cv::Mat magnitude(H, W, CV_32F, cv::Scalar(0));
|
|
cv::Mat direction(H, W, CV_32F, cv::Scalar(0));
|
|
for(int y = 0; y < H; ++y)
|
|
{
|
|
const float * gx = dx.ptr<float>(y);
|
|
const float * gy = dy.ptr<float>(y);
|
|
for(int x = 0; x < W; ++x)
|
|
{
|
|
float gxx = 0, gyy = 0, gxy = 0;
|
|
for(int c = 0; c < channels; ++c)
|
|
{
|
|
const float a = gx[x * channels + c], b = gy[x * channels + c];
|
|
gxx += a * a; gyy += b * b; gxy += a * b;
|
|
}
|
|
magnitude.at<float>(y, x) = 0.5f * (gxx + gyy + std::sqrt((gxx - gyy) * (gxx - gyy) + 4.0f * gxy * gxy));
|
|
direction.at<float>(y, x) = 0.5f * std::atan2(2.0f * gxy, gxx - gyy);
|
|
}
|
|
}
|
|
std::vector<float> best(w * h, 0.0f);
|
|
std::vector<cv::Point2f> bestPixel(w * h);
|
|
for(int y = 1; y < H - 1; ++y)
|
|
{
|
|
for(int x = 1; x < W - 1; ++x)
|
|
{
|
|
const int u = x / decimation, v = y / decimation;
|
|
if(cellWeight.at<float>(v, u) <= 0.0f)
|
|
{
|
|
continue;
|
|
}
|
|
const float m = magnitude.at<float>(y, x);
|
|
const float a = direction.at<float>(y, x);
|
|
const int sx = int(std::round(std::cos(a))), sy = int(std::round(std::sin(a)));
|
|
if(m <= best[v * w + u] || m < magnitude.at<float>(y + sy, x + sx) || m < magnitude.at<float>(y - sy, x - sx))
|
|
{
|
|
continue;
|
|
}
|
|
// Sub-pixel position across the edge, from a parabola through the strengths: a
|
|
// pixel's center would put all these points on the image's pixel centers when
|
|
// projected with the correction they were selected with, where the image edge
|
|
// score (interpolated between pixel centers) peaks, making that correction a
|
|
// false optimum.
|
|
const float mm = magnitude.at<float>(y - sy, x - sx), mp = magnitude.at<float>(y + sy, x + sx);
|
|
const float den = mm - 2.0f * m + mp;
|
|
const float offset = den < 0.0f ? std::max(-0.5f, std::min(0.5f, 0.5f * (mm - mp) / den)) : 0.0f;
|
|
best[v * w + u] = m;
|
|
bestPixel[v * w + u] = cv::Point2f(x + offset * sx, y + offset * sy);
|
|
}
|
|
}
|
|
for(int v = 0; v < h; ++v)
|
|
{
|
|
for(int u = 0; u < w; ++u)
|
|
{
|
|
if(best[v * w + u] <= 0.0f)
|
|
{
|
|
continue;
|
|
}
|
|
const cv::Point2f & px = bestPixel[v * w + u];
|
|
// Bilinear inverse depth between the 4 nearest cell centers, all on the same
|
|
// surface (within 5% of each other's depth).
|
|
const float cu = (px.x + 0.5f) / decimation - 0.5f, cv_ = (px.y + 0.5f) / decimation - 0.5f;
|
|
const int u0 = std::max(0, std::min(w - 2, int(std::floor(cu)))), v0 = std::max(0, std::min(h - 2, int(std::floor(cv_))));
|
|
const float au = std::max(0.0f, std::min(1.0f, cu - u0)), av = std::max(0.0f, std::min(1.0f, cv_ - v0));
|
|
const float d00 = depth.at<float>(v0, u0), d01 = depth.at<float>(v0, u0 + 1);
|
|
const float d10 = depth.at<float>(v0 + 1, u0), d11 = depth.at<float>(v0 + 1, u0 + 1);
|
|
const float dMin = std::min(std::min(d00, d01), std::min(d10, d11)), dMax = std::max(std::max(d00, d01), std::max(d10, d11));
|
|
if(dMin <= 0.0f || dMax > dMin * 1.05f)
|
|
{
|
|
continue;
|
|
}
|
|
const float inverse = (1 - av) * ((1 - au) / d00 + au / d01) + av * ((1 - au) / d10 + au / d11);
|
|
const float z = 1.0f / inverse;
|
|
const cv::Point3f pc(float((px.x - CX) / FX) * z, float((px.y - CY) / FY) * z, z);
|
|
f.edgePoints.push_back(util3d::transformPoint(pc, camToScan));
|
|
f.edgeWeights.push_back(cellWeight.at<float>(v, u));
|
|
f.edgeTypes.push_back(type);
|
|
}
|
|
}
|
|
}
|
|
|
|
// The lidar's edges as seen by the camera at scanToCam * C: points in front of a depth
|
|
// discontinuity, found at a resolution where the projected scan is dense, then points on a
|
|
// crease or an intensity edge (a change of the surface's reflectance, e.g., paint or
|
|
// material, which the image is likely to show too, where there is no depth discontinuity),
|
|
// found at that resolution and located at the image's.
|
|
void selectEdgePoints(Frame & f, const Transform & C, int decimation, float jumpThreshold,
|
|
float intensityJumpThreshold, float intensityWeight, float creaseAngle, float creaseVoxel, float minDepth)
|
|
{
|
|
const CellMaps maps = computeCellMaps(f, C, decimation, intensityJumpThreshold > 0.0f, creaseAngle > 0.0f,
|
|
creaseVoxel, minDepth);
|
|
const cv::Mat & depth = maps.depth;
|
|
const cv::Mat & index = maps.index;
|
|
const cv::Mat & logIntensity = maps.logIntensity;
|
|
const cv::Mat & cellNormals = maps.normals;
|
|
const int w = depth.cols, h = depth.rows;
|
|
const float logIntensityJump = std::log(1.0f + intensityJumpThreshold);
|
|
|
|
f.edgePoints.clear();
|
|
f.edgeWeights.clear();
|
|
f.edgeTypes.clear();
|
|
|
|
// Depth discontinuities, and which cells are (no crease or intensity edge there).
|
|
cv::Mat depthEdge(h, w, CV_8U, cv::Scalar(0));
|
|
for(int v = 1; v < h - 1; ++v)
|
|
{
|
|
for(int u = 1; u < w - 1; ++u)
|
|
{
|
|
const float d = depth.at<float>(v, u);
|
|
if(d == 0)
|
|
{
|
|
continue;
|
|
}
|
|
// Largest relative jump to a farther neighbor, beyond where this point's surface
|
|
// would continue: this point is in front of it. On a plane, inverse depth is
|
|
// linear in the image, so the surface's continuation at a neighbor is predicted
|
|
// from the opposite neighbor. A surface seen at a grazing angle (the floor ahead
|
|
// of a low camera) has a steep depth gradient but follows its continuation: it
|
|
// is not a discontinuity, whereas a background behind an object's edge is far
|
|
// beyond the object's continuation.
|
|
float jump = 0.0f;
|
|
for(int dv = -1; dv <= 1; ++dv)
|
|
{
|
|
for(int du = -1; du <= 1; ++du)
|
|
{
|
|
if(du == 0 && dv == 0)
|
|
{
|
|
continue;
|
|
}
|
|
const float n = depth.at<float>(v + dv, u + du);
|
|
const float o = depth.at<float>(v - dv, u - du);
|
|
if(n <= 0 || o <= 0)
|
|
{
|
|
continue; // no opposite neighbor: no prediction
|
|
}
|
|
const float predictedInverse = 2.0f / d - 1.0f / o;
|
|
if(predictedInverse <= 0.0f)
|
|
{
|
|
continue; // the surface recedes beyond the horizon there
|
|
}
|
|
jump = std::max(jump, (n - 1.0f / predictedInverse) / d);
|
|
}
|
|
}
|
|
if(jump > jumpThreshold)
|
|
{
|
|
depthEdge.at<unsigned char>(v, u) = 1;
|
|
const float * p = f.cloud.ptr<float>(index.at<int>(v, u));
|
|
f.edgePoints.emplace_back(p[0], p[1], p[2]);
|
|
f.edgeWeights.push_back(std::min(1.0f, jump));
|
|
f.edgeTypes.push_back(kEdgeDepth);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Creases: cells where the surface normals of two opposite neighbors differ by more than
|
|
// creaseAngle, where the cell and all its neighbors have a normal, weighted by the angle.
|
|
cv::Mat creaseWeight(h, w, CV_32F, cv::Scalar(0));
|
|
if(creaseAngle > 0.0f && !cellNormals.empty())
|
|
{
|
|
static const int directions[4][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; // (du, dv)
|
|
cv::Mat unit(h, w, CV_32FC3, cv::Scalar(0, 0, 0)); // unit normals, zero where unknown
|
|
cv::Mat known(h, w, CV_8U, cv::Scalar(0));
|
|
for(int v = 0; v < h; ++v)
|
|
{
|
|
for(int u = 0; u < w; ++u)
|
|
{
|
|
const cv::Vec3f & n = cellNormals.at<cv::Vec3f>(v, u);
|
|
const double nn = cv::norm(n);
|
|
if(nn > 0)
|
|
{
|
|
unit.at<cv::Vec3f>(v, u) = n / nn;
|
|
known.at<unsigned char>(v, u) = 1;
|
|
}
|
|
}
|
|
}
|
|
for(int v = 1; v < h - 1; ++v)
|
|
{
|
|
for(int u = 1; u < w - 1; ++u)
|
|
{
|
|
bool complete = !depthEdge.at<unsigned char>(v, u);
|
|
for(int dv = -1; dv <= 1 && complete; ++dv)
|
|
{
|
|
for(int du = -1; du <= 1 && complete; ++du)
|
|
{
|
|
complete = known.at<unsigned char>(v + dv, u + du) != 0;
|
|
}
|
|
}
|
|
if(!complete)
|
|
{
|
|
continue;
|
|
}
|
|
float strongest = 0.0f;
|
|
for(int k = 0; k < 4; ++k)
|
|
{
|
|
const int du = directions[k][0], dv = directions[k][1];
|
|
const float c = unit.at<cv::Vec3f>(v + dv, u + du).dot(unit.at<cv::Vec3f>(v - dv, u - du));
|
|
strongest = std::max(strongest, std::acos(std::max(-1.0f, std::min(1.0f, c))) * 180.0f / float(M_PI));
|
|
}
|
|
if(strongest > creaseAngle)
|
|
{
|
|
creaseWeight.at<float>(v, u) = std::min(1.0f, strongest / (2.0f * creaseAngle));
|
|
}
|
|
}
|
|
}
|
|
addFullResolutionEdges(f, C, decimation, depth, unit, creaseWeight, kEdgeCrease);
|
|
}
|
|
|
|
// Intensity edges: cells whose log-intensity differs from a neighbor's by more than
|
|
// logIntensityJump, where all of them are known (no edge against a hole), and not
|
|
// already on a depth discontinuity or a crease, weighted by the change.
|
|
if(!logIntensity.empty())
|
|
{
|
|
cv::Mat intensityWeightMap(h, w, CV_32F, cv::Scalar(0));
|
|
for(int v = 1; v < h - 1; ++v)
|
|
{
|
|
for(int u = 1; u < w - 1; ++u)
|
|
{
|
|
if(depth.at<float>(v, u) == 0 || depthEdge.at<unsigned char>(v, u) || creaseWeight.at<float>(v, u) > 0.0f)
|
|
{
|
|
continue;
|
|
}
|
|
bool complete = true;
|
|
float change = 0.0f;
|
|
const float center = logIntensity.at<float>(v, u);
|
|
for(int dv = -1; dv <= 1 && complete; ++dv)
|
|
{
|
|
for(int du = -1; du <= 1; ++du)
|
|
{
|
|
if(depth.at<float>(v + dv, u + du) == 0)
|
|
{
|
|
complete = false;
|
|
break;
|
|
}
|
|
change = std::max(change, std::fabs(logIntensity.at<float>(v + dv, u + du) - center));
|
|
}
|
|
}
|
|
if(complete && change > logIntensityJump)
|
|
{
|
|
intensityWeightMap.at<float>(v, u) = intensityWeight * std::min(1.0f, change / (2.0f * logIntensityJump));
|
|
}
|
|
}
|
|
}
|
|
addFullResolutionEdges(f, C, decimation, depth, logIntensity, intensityWeightMap, kEdgeIntensity);
|
|
}
|
|
}
|
|
|
|
// The node's id and speeds, in the image's top left corner, one per line.
|
|
void drawNodeInfo(cv::Mat & image, const Frame & f)
|
|
{
|
|
std::vector<std::string> lines(1, uFormat("Node %d", f.id));
|
|
if(f.meanAngularSpeed >= 0.0f)
|
|
{
|
|
lines.push_back(uFormat("mean over %.1f s (%d poses): %.2f m/s %.1f deg/s", f.meanWindow, f.meanPoses + 1, f.meanLinearSpeed, f.meanAngularSpeed));
|
|
}
|
|
if(f.angularSpeed >= 0.0f)
|
|
{
|
|
lines.push_back(uFormat("instant: %.2f m/s %.1f deg/s", f.linearSpeed, f.angularSpeed));
|
|
}
|
|
const double scale = 0.35 * image.cols / 640.0;
|
|
const int lineHeight = std::max(10, int(30.0 * scale + 0.5));
|
|
for(size_t i = 0; i < lines.size(); ++i)
|
|
{
|
|
const cv::Point origin(6, lineHeight * int(i + 1));
|
|
cv::putText(image, lines[i], origin, cv::FONT_HERSHEY_SIMPLEX, scale, cv::Scalar(0, 0, 0), 3, cv::LINE_AA);
|
|
cv::putText(image, lines[i], origin, cv::FONT_HERSHEY_SIMPLEX, scale, cv::Scalar(255, 255, 255), 1, cv::LINE_AA);
|
|
}
|
|
}
|
|
|
|
void saveOverlay(const Frame & f, const Transform & C, float minDepth, const std::string & path)
|
|
{
|
|
// The image darkened, its edges in green, the lidar edge points over them: depth
|
|
// discontinuities in red, creases in blue, intensity edges in yellow.
|
|
cv::Mat out;
|
|
cv::cvtColor(f.gray / 2, out, cv::COLOR_GRAY2BGR);
|
|
out.setTo(cv::Scalar(0, 200, 0), f.edges);
|
|
const Transform scanInCam = (f.scanToCam * C).inverse();
|
|
const double fx = f.K.at<double>(0,0), fy = f.K.at<double>(1,1);
|
|
const double cx = f.K.at<double>(0,2), cy = f.K.at<double>(1,2);
|
|
for(size_t i = 0; i < f.edgePoints.size(); ++i)
|
|
{
|
|
const cv::Point3f pc = util3d::transformPoint(f.edgePoints[i], scanInCam);
|
|
if(pc.z >= minDepth)
|
|
{
|
|
// One pixel each, so that the image edges under them stay visible.
|
|
const int u = int(fx * pc.x / pc.z + cx + 0.5), v = int(fy * pc.y / pc.z + cy + 0.5);
|
|
if(u >= 0 && v >= 0 && u < out.cols && v < out.rows)
|
|
{
|
|
static const cv::Vec3b colors[3] = {cv::Vec3b(0, 0, 255), cv::Vec3b(255, 128, 0), cv::Vec3b(0, 255, 255)};
|
|
out.at<cv::Vec3b>(v, u) = colors[f.edgeTypes[i]];
|
|
}
|
|
}
|
|
}
|
|
drawNodeInfo(out, f);
|
|
cv::imwrite(path, out);
|
|
}
|
|
|
|
// The cell maps the lidar edges are found from, half transparent over the image, with the
|
|
// image's edges in green and the map's own in blue, from red (0) to yellow (1): the lidar's intensity (as used, log
|
|
// and median filtered, contrast stretched per image: red dark, yellow bright), and how the
|
|
// surface faces the camera (red: seen edge on, at a grazing angle, yellow: facing it).
|
|
void saveCellMapOverlays(const Frame & f, const Transform & C, int decimation, float normalsVoxel,
|
|
float minDepth, const std::string & prefix, const std::string & suffix)
|
|
{
|
|
const CellMaps maps = computeCellMaps(f, C, decimation, true, true, normalsVoxel, minDepth);
|
|
const int w = maps.depth.cols, h = maps.depth.rows;
|
|
const double fx = f.K.at<double>(0,0) / decimation, fy = f.K.at<double>(1,1) / decimation;
|
|
const double cx = f.K.at<double>(0,2) / decimation, cy = f.K.at<double>(1,2) / decimation;
|
|
|
|
// values: CV_32F in [0,1] per cell, negative where unknown.
|
|
auto save = [&](const cv::Mat & values, const std::string & path)
|
|
{
|
|
cv::Mat cells(h, w, CV_8UC3, cv::Scalar(0, 0, 0));
|
|
cv::Mat known(h, w, CV_8U, cv::Scalar(0));
|
|
for(int v = 0; v < h; ++v)
|
|
{
|
|
for(int u = 0; u < w; ++u)
|
|
{
|
|
const float x = values.at<float>(v, u);
|
|
if(x >= 0.0f)
|
|
{
|
|
cells.at<cv::Vec3b>(v, u) = cv::Vec3b(0, (unsigned char)(255.0f * std::min(1.0f, x)), 255);
|
|
known.at<unsigned char>(v, u) = 255;
|
|
}
|
|
}
|
|
}
|
|
cv::Mat out, up, upKnown, blended;
|
|
cv::cvtColor(f.gray, out, cv::COLOR_GRAY2BGR);
|
|
const cv::Rect roi(0, 0, w * decimation, h * decimation);
|
|
cv::resize(cells, up, roi.size(), 0, 0, cv::INTER_NEAREST);
|
|
cv::resize(known, upKnown, roi.size(), 0, 0, cv::INTER_NEAREST);
|
|
cv::addWeighted(out(roi), 0.5, up, 0.5, 0.0, blended);
|
|
blended.copyTo(out(roi), upKnown);
|
|
out.setTo(cv::Scalar(0, 200, 0), f.edges);
|
|
|
|
// The map's own edges, as the image's (Canny), in blue: smoothly upsampled so that
|
|
// they do not follow the cells' outline, and not against holes.
|
|
cv::Mat value8(h, w, CV_8U, cv::Scalar(0));
|
|
for(int v = 0; v < h; ++v)
|
|
{
|
|
for(int u = 0; u < w; ++u)
|
|
{
|
|
value8.at<unsigned char>(v, u) = (unsigned char)(255.0f * std::max(0.0f, std::min(1.0f, values.at<float>(v, u))));
|
|
}
|
|
}
|
|
cv::Mat smooth, mapEdges, inside;
|
|
cv::resize(value8, smooth, roi.size(), 0, 0, cv::INTER_LINEAR);
|
|
cv::GaussianBlur(smooth, smooth, cv::Size(5, 5), 1.5);
|
|
cv::Canny(smooth, mapEdges, 40, 100);
|
|
cv::erode(upKnown, inside, cv::Mat(), cv::Point(-1, -1), decimation);
|
|
mapEdges &= inside;
|
|
out(roi).setTo(cv::Scalar(255, 0, 0), mapEdges);
|
|
drawNodeInfo(out, f);
|
|
cv::imwrite(path, out);
|
|
};
|
|
|
|
if(!maps.logIntensity.empty())
|
|
{
|
|
std::vector<float> known;
|
|
for(int v = 0; v < h; ++v)
|
|
{
|
|
for(int u = 0; u < w; ++u)
|
|
{
|
|
if(maps.depth.at<float>(v, u) > 0)
|
|
{
|
|
known.push_back(maps.logIntensity.at<float>(v, u));
|
|
}
|
|
}
|
|
}
|
|
if(!known.empty())
|
|
{
|
|
std::sort(known.begin(), known.end());
|
|
const float low = known[known.size() * 2 / 100], high = known[known.size() * 98 / 100];
|
|
cv::Mat values(h, w, CV_32F, cv::Scalar(-1));
|
|
for(int v = 0; v < h; ++v)
|
|
{
|
|
for(int u = 0; u < w; ++u)
|
|
{
|
|
if(maps.depth.at<float>(v, u) > 0)
|
|
{
|
|
const float x = high > low ? (maps.logIntensity.at<float>(v, u) - low) / (high - low) : 0.5f;
|
|
values.at<float>(v, u) = std::min(1.0f, std::max(0.0f, x));
|
|
}
|
|
}
|
|
}
|
|
save(values, prefix + "_intensity" + suffix);
|
|
}
|
|
}
|
|
|
|
if(!maps.normals.empty())
|
|
{
|
|
cv::Mat values(h, w, CV_32F, cv::Scalar(-1));
|
|
for(int v = 0; v < h; ++v)
|
|
{
|
|
for(int u = 0; u < w; ++u)
|
|
{
|
|
const cv::Vec3f & n = maps.normals.at<cv::Vec3f>(v, u);
|
|
const double nn = cv::norm(n);
|
|
if(nn > 0)
|
|
{
|
|
// Cosine between the normal and the ray to the cell: 1 facing, 0 edge on.
|
|
const cv::Vec3f ray((u + 0.5 - cx) / fx, (v + 0.5 - cy) / fy, 1.0);
|
|
values.at<float>(v, u) = float(std::fabs(n.dot(ray)) / (nn * cv::norm(ray)));
|
|
}
|
|
}
|
|
}
|
|
save(values, prefix + "_normals" + suffix);
|
|
}
|
|
}
|
|
|
|
// The correction of the camera's mount X (camera body frame: x forward, y left, z up) for
|
|
// the solvers' parameters p (the correction C in the camera's optical frame), and back.
|
|
Transform mountCorrection(const double p[6])
|
|
{
|
|
return CameraModel::opticalRotation() * correctionFrom(p) * CameraModel::opticalRotation().inverse();
|
|
}
|
|
|
|
void parametersFromMountCorrection(const Transform & X, double p[6])
|
|
{
|
|
const Transform C = CameraModel::opticalRotation().inverse() * X * CameraModel::opticalRotation();
|
|
float x, y, z, roll, pitch, yaw;
|
|
C.getTranslationAndEulerAngles(x, y, z, roll, pitch, yaw);
|
|
p[0] = x; p[1] = y; p[2] = z;
|
|
p[3] = roll * 180.0 / M_PI; p[4] = pitch * 180.0 / M_PI; p[5] = yaw * 180.0 / M_PI;
|
|
}
|
|
|
|
void printCorrection(const char * label, const double p[6])
|
|
{
|
|
float x, y, z, roll, pitch, yaw;
|
|
mountCorrection(p).getTranslationAndEulerAngles(x, y, z, roll, pitch, yaw);
|
|
printf("%s xyz=(%.3f, %.3f, %.3f) m rpy=(%.2f, %.2f, %.2f) deg\n", label, x, y, z,
|
|
roll * 180.0f / float(M_PI), pitch * 180.0f / float(M_PI), yaw * 180.0f / float(M_PI));
|
|
}
|
|
|
|
int main(int argc, char * argv[])
|
|
{
|
|
ULogger::setType(ULogger::kTypeConsole);
|
|
ULogger::setLevel(ULogger::kWarning);
|
|
|
|
if(argc < 2)
|
|
{
|
|
showUsage(argv[0]);
|
|
}
|
|
|
|
bool translation = false;
|
|
bool verbose = false;
|
|
std::string solverName = "simplex";
|
|
std::string imagesDir;
|
|
int decimation = 4;
|
|
float jump = 0.15f;
|
|
float intensityJump = 0.4f;
|
|
float intensityWeight = 1.0f;
|
|
float creaseAngle = 45.0f;
|
|
float creaseVoxel = 0.1f;
|
|
float sigma = 3.0f;
|
|
float voxel = 0.0f;
|
|
float minDepth = 0.5f;
|
|
float maxAngularSpeed = 0.0f;
|
|
float maxLinearSpeed = 0.0f;
|
|
double initialRotation[3] = {0, 0, 0};
|
|
for(int i = 1; i < argc - 1; ++i)
|
|
{
|
|
if(std::strcmp(argv[i], "--help") == 0)
|
|
{
|
|
showUsage(argv[0]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--solver") == 0 && i + 1 < argc - 1)
|
|
{
|
|
solverName = argv[++i];
|
|
}
|
|
else if(std::strcmp(argv[i], "--verbose") == 0)
|
|
{
|
|
verbose = true;
|
|
}
|
|
else if(std::strcmp(argv[i], "--translation") == 0)
|
|
{
|
|
translation = true;
|
|
}
|
|
else if(std::strcmp(argv[i], "--images") == 0 && i + 1 < argc - 1)
|
|
{
|
|
imagesDir = argv[++i];
|
|
}
|
|
else if(std::strcmp(argv[i], "--decimation") == 0 && i + 1 < argc - 1)
|
|
{
|
|
decimation = std::max(1, uStr2Int(argv[++i]));
|
|
}
|
|
else if(std::strcmp(argv[i], "--jump") == 0 && i + 1 < argc - 1)
|
|
{
|
|
jump = uStr2Float(argv[++i]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--intensity_jump") == 0 && i + 1 < argc - 1)
|
|
{
|
|
intensityJump = uStr2Float(argv[++i]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--intensity_weight") == 0 && i + 1 < argc - 1)
|
|
{
|
|
intensityWeight = uStr2Float(argv[++i]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--crease_angle") == 0 && i + 1 < argc - 1)
|
|
{
|
|
creaseAngle = uStr2Float(argv[++i]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--crease_voxel") == 0 && i + 1 < argc - 1)
|
|
{
|
|
creaseVoxel = uStr2Float(argv[++i]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--no_intensity") == 0)
|
|
{
|
|
intensityJump = 0.0f;
|
|
}
|
|
else if(std::strcmp(argv[i], "--sigma") == 0 && i + 1 < argc - 1)
|
|
{
|
|
sigma = uStr2Float(argv[++i]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--voxel") == 0 && i + 1 < argc - 1)
|
|
{
|
|
voxel = uStr2Float(argv[++i]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--min_depth") == 0 && i + 1 < argc - 1)
|
|
{
|
|
minDepth = uStr2Float(argv[++i]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--max_angular_speed") == 0 && i + 1 < argc - 1)
|
|
{
|
|
maxAngularSpeed = uStr2Float(argv[++i]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--max_linear_speed") == 0 && i + 1 < argc - 1)
|
|
{
|
|
maxLinearSpeed = uStr2Float(argv[++i]);
|
|
}
|
|
else if(std::strcmp(argv[i], "--initial_rotation") == 0 && i + 3 < argc - 1)
|
|
{
|
|
for(int k = 0; k < 3; ++k)
|
|
{
|
|
initialRotation[k] = uStr2Double(argv[++i]);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
printf("Unknown option \"%s\"\n", argv[i]);
|
|
showUsage(argv[0]);
|
|
}
|
|
}
|
|
const std::unique_ptr<CorrectionSolver> solver = createSolver(solverName, sigma);
|
|
if(!solver)
|
|
{
|
|
printf("Unknown solver \"%s\".\n", solverName.c_str());
|
|
showUsage(argv[0]);
|
|
}
|
|
{
|
|
// The least-squares solvers' double precision correction must be correctionFrom()'s.
|
|
const double t[6] = {0.01, -0.02, 0.03, 1.5, -2.5, 3.5};
|
|
const double diff = (correctionFrom(t).toEigen3d().matrix() - correctionFromDouble(t).matrix()).cwiseAbs().maxCoeff();
|
|
UASSERT_MSG(diff < 1e-5, uFormat("correctionFromDouble() differs from correctionFrom() by %g", diff).c_str());
|
|
double back[6];
|
|
parametersFromMountCorrection(mountCorrection(t), back);
|
|
for(int k = 0; k < 6; ++k)
|
|
{
|
|
UASSERT_MSG(std::fabs(back[k] - t[k]) < 1e-4, "parametersFromMountCorrection() is not mountCorrection()'s inverse");
|
|
}
|
|
}
|
|
const std::string databasePath = argv[argc - 1];
|
|
if(std::string(databasePath).rfind("--", 0) == 0)
|
|
{
|
|
showUsage(argv[0]);
|
|
}
|
|
|
|
DBDriver * driver = DBDriver::create();
|
|
if(!driver->openConnection(databasePath, false, /*readOnly=*/true))
|
|
{
|
|
printf("Cannot open database \"%s\".\n", databasePath.c_str());
|
|
delete driver;
|
|
return 1;
|
|
}
|
|
// All the nodes, with the intermediate ones (only odometry poses, recorded between the
|
|
// nodes with data when the map was made with Rtabmap/CreateIntermediateNodes), for the
|
|
// speeds; the nodes with data for the calibration.
|
|
UTimer totalTimer;
|
|
UTimer stepTimer;
|
|
std::vector<std::pair<std::string, double> > times; // step, s
|
|
double normalsTime = 0.0;
|
|
std::set<int> idSet;
|
|
driver->getAllNodeIds(idSet, true);
|
|
std::list<int> ids(idSet.begin(), idSet.end());
|
|
std::list<Signature *> allSignatures;
|
|
driver->loadSignatures(ids, allSignatures);
|
|
struct OdometrySample
|
|
{
|
|
Transform pose;
|
|
double stamp;
|
|
bool intermediate;
|
|
};
|
|
std::map<int, OdometrySample> odometry;
|
|
std::list<Signature *> signatures;
|
|
int intermediateNodes = 0;
|
|
for(Signature * s : allSignatures)
|
|
{
|
|
const bool intermediate = s->getWeight() == -1;
|
|
odometry.insert(std::make_pair(s->id(), OdometrySample{s->getPose(), s->getStamp(), intermediate}));
|
|
if(intermediate)
|
|
{
|
|
++intermediateNodes;
|
|
delete s;
|
|
}
|
|
else
|
|
{
|
|
signatures.push_back(s);
|
|
}
|
|
}
|
|
driver->loadNodeData(signatures, true, true, false, false);
|
|
|
|
std::vector<Frame> frames;
|
|
int skipped = 0;
|
|
int tooFast = 0;
|
|
int noVelocity = 0;
|
|
for(Signature * s : signatures)
|
|
{
|
|
// The node's speeds. The instantaneous one is odometry's when the node was added,
|
|
// at the end of its scan. An assembled scan is taken since the previous node: its
|
|
// mean speed over that time is along the odometry poses from the previous node to
|
|
// this one, through the intermediate nodes if any (else only the net motion between
|
|
// both, less than the path if the robot went back and forth). While the robot
|
|
// moves fast, an error in the sensors' time synchronization, motion within the
|
|
// assembled scan, and motion blur or rolling shutter in the image move the edges more.
|
|
float linearSpeed = -1.0f, angularSpeed = -1.0f;
|
|
float meanLinearSpeed = -1.0f, meanAngularSpeed = -1.0f, meanWindow = 0.0f;
|
|
int meanPoses = 0;
|
|
const std::vector<float> & velocity = s->getVelocity();
|
|
if(velocity.size() == 6)
|
|
{
|
|
linearSpeed = std::sqrt(velocity[0]*velocity[0] + velocity[1]*velocity[1] + velocity[2]*velocity[2]);
|
|
angularSpeed = std::sqrt(velocity[3]*velocity[3] + velocity[4]*velocity[4] + velocity[5]*velocity[5]) * 180.0f / float(M_PI);
|
|
}
|
|
std::map<int, OdometrySample>::const_iterator current = odometry.find(s->id());
|
|
std::map<int, OdometrySample>::const_iterator start = current;
|
|
while(start != odometry.begin())
|
|
{
|
|
--start;
|
|
if(!start->second.intermediate)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
if(start != current && !start->second.intermediate)
|
|
{
|
|
const double dt = current->second.stamp - start->second.stamp;
|
|
if(dt > 0.0 && dt < 10.0)
|
|
{
|
|
double distance = 0.0, angle = 0.0;
|
|
bool valid = true;
|
|
for(std::map<int, OdometrySample>::const_iterator iter = start; iter != current && valid; ++iter)
|
|
{
|
|
const Transform & from = iter->second.pose;
|
|
const Transform & to = std::next(iter)->second.pose;
|
|
valid = !from.isNull() && !to.isNull();
|
|
if(valid)
|
|
{
|
|
const Transform motion = from.inverse() * to;
|
|
distance += motion.getNorm();
|
|
angle += Eigen::AngleAxisf(motion.toEigen3f().linear()).angle();
|
|
++meanPoses;
|
|
}
|
|
}
|
|
if(valid)
|
|
{
|
|
meanLinearSpeed = distance / dt;
|
|
meanAngularSpeed = angle * 180.0 / M_PI / dt;
|
|
meanWindow = dt;
|
|
}
|
|
}
|
|
}
|
|
if(maxAngularSpeed > 0.0f || maxLinearSpeed > 0.0f)
|
|
{
|
|
// The mean speed over the scan, else the instantaneous one.
|
|
const float linear = meanLinearSpeed >= 0.0f ? meanLinearSpeed : linearSpeed;
|
|
const float angular = meanAngularSpeed >= 0.0f ? meanAngularSpeed : angularSpeed;
|
|
if(angular < 0.0f)
|
|
{
|
|
++noVelocity;
|
|
}
|
|
else if((maxAngularSpeed > 0.0f && angular > maxAngularSpeed) || (maxLinearSpeed > 0.0f && linear > maxLinearSpeed))
|
|
{
|
|
++tooFast;
|
|
delete s;
|
|
continue;
|
|
}
|
|
}
|
|
SensorData & data = s->sensorData();
|
|
cv::Mat image, depth;
|
|
LaserScan scan;
|
|
data.uncompressData(&image, &depth, &scan);
|
|
if(voxel > 0.0f && !scan.isEmpty())
|
|
{
|
|
scan = util3d::commonFiltering(scan, 1, 0.0f, 0.0f, voxel);
|
|
}
|
|
if(!image.empty() && !scan.isEmpty() && data.cameraModels().size() == 1 &&
|
|
data.cameraModels()[0].isValidForProjection())
|
|
{
|
|
const CameraModel & model = data.cameraModels()[0];
|
|
Frame f;
|
|
f.id = s->id();
|
|
f.linearSpeed = linearSpeed;
|
|
f.angularSpeed = angularSpeed;
|
|
f.meanLinearSpeed = meanLinearSpeed;
|
|
f.meanAngularSpeed = meanAngularSpeed;
|
|
f.meanWindow = meanWindow;
|
|
f.meanPoses = meanPoses;
|
|
if(image.channels() == 3)
|
|
{
|
|
cv::cvtColor(image, f.gray, cv::COLOR_BGR2GRAY);
|
|
}
|
|
else
|
|
{
|
|
f.gray = image;
|
|
}
|
|
computeEdgeMaps(f, sigma);
|
|
f.K = model.K().clone();
|
|
f.scanToCam = scan.localTransform().inverse() * model.localTransform();
|
|
// Scan frame, as stored.
|
|
f.hasIntensity = scan.hasIntensity();
|
|
f.cloud = cv::Mat(scan.size(), 4, CV_32F, cv::Scalar(0));
|
|
for(int i = 0; i < scan.size(); ++i)
|
|
{
|
|
const float * p = scan.data().ptr<float>(0, i);
|
|
f.cloud.at<float>(i, 0) = p[0];
|
|
f.cloud.at<float>(i, 1) = p[1];
|
|
f.cloud.at<float>(i, 2) = scan.is2d() ? 0.0f : p[2];
|
|
if(f.hasIntensity)
|
|
{
|
|
f.cloud.at<float>(i, 3) = p[scan.getIntensityOffset()];
|
|
}
|
|
}
|
|
if((creaseAngle > 0.0f || !imagesDir.empty()) && !scan.is2d())
|
|
{
|
|
// Normals on a voxelized copy: smoother than at full resolution, and much
|
|
// faster. Oriented toward the lidar (the scan frame's origin). Also for the
|
|
// images, which show them.
|
|
UTimer normalsTimer;
|
|
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
|
|
cloud->resize(f.cloud.rows);
|
|
for(int i = 0; i < f.cloud.rows; ++i)
|
|
{
|
|
cloud->at(i) = pcl::PointXYZ(f.cloud.at<float>(i, 0), f.cloud.at<float>(i, 1), f.cloud.at<float>(i, 2));
|
|
}
|
|
if(creaseVoxel > 0.0f)
|
|
{
|
|
cloud = util3d::voxelize(cloud, creaseVoxel);
|
|
}
|
|
pcl::PointCloud<pcl::Normal>::Ptr normals = util3d::computeNormals(cloud, 20);
|
|
f.normals = cv::Mat(cloud->size(), 6, CV_32F);
|
|
for(size_t i = 0; i < cloud->size(); ++i)
|
|
{
|
|
float * n = f.normals.ptr<float>(i);
|
|
n[0] = cloud->at(i).x; n[1] = cloud->at(i).y; n[2] = cloud->at(i).z;
|
|
n[3] = normals->at(i).normal_x; n[4] = normals->at(i).normal_y; n[5] = normals->at(i).normal_z;
|
|
}
|
|
normalsTime += normalsTimer.elapsed();
|
|
}
|
|
frames.push_back(f);
|
|
}
|
|
else
|
|
{
|
|
++skipped;
|
|
}
|
|
delete s;
|
|
}
|
|
driver->closeConnection(false);
|
|
delete driver;
|
|
times.push_back(std::make_pair(std::string("Loading the nodes (image edges, scans)"), stepTimer.ticks() - normalsTime));
|
|
if(normalsTime > 0.0)
|
|
{
|
|
times.push_back(std::make_pair(std::string("Normals of the scans"), normalsTime));
|
|
}
|
|
size_t points = 0;
|
|
for(const Frame & f : frames)
|
|
{
|
|
points += f.cloud.rows;
|
|
}
|
|
printf("%d nodes with an image (one camera) and a lidar scan, %d skipped. %d lidar points per node on average%s (%.1f s).\n",
|
|
(int)frames.size(), skipped, frames.empty() ? 0 : int(points / frames.size()),
|
|
voxel > 0.0f ? uFormat(" after a %g m voxel filter", voxel).c_str() : "", totalTimer.elapsed());
|
|
if(intermediateNodes)
|
|
{
|
|
printf("%d intermediate nodes (odometry poses only), for the nodes' mean speeds.\n", intermediateNodes);
|
|
}
|
|
if(maxAngularSpeed > 0.0f || maxLinearSpeed > 0.0f)
|
|
{
|
|
printf("%d nodes moving too fast skipped (rotating faster than %s, moving faster than %s)%s.\n", tooFast,
|
|
maxAngularSpeed > 0.0f ? uFormat("%g deg/s", maxAngularSpeed).c_str() : "any",
|
|
maxLinearSpeed > 0.0f ? uFormat("%g m/s", maxLinearSpeed).c_str() : "any",
|
|
noVelocity ? uFormat(", %d nodes without a speed kept", noVelocity).c_str() : "");
|
|
}
|
|
if(frames.size() < 2)
|
|
{
|
|
printf("Not enough nodes.\n");
|
|
return 1;
|
|
}
|
|
|
|
std::vector<int> all, even, odd;
|
|
for(size_t i = 0; i < frames.size(); ++i)
|
|
{
|
|
all.push_back(i);
|
|
(i % 2 ? odd : even).push_back(i);
|
|
}
|
|
|
|
const CalibrationProblem problem(frames, all, minDepth);
|
|
printf("Solver: %s\n", solver->name());
|
|
|
|
// Discontinuities as seen with the current extrinsics (or with the initial rotation
|
|
// given), then again with the result.
|
|
double initial[6] = {0, 0, 0, 0, 0, 0};
|
|
parametersFromMountCorrection(Transform(0, 0, 0, initialRotation[0] * M_PI / 180.0,
|
|
initialRotation[1] * M_PI / 180.0, initialRotation[2] * M_PI / 180.0), initial);
|
|
if(initialRotation[0] != 0 || initialRotation[1] != 0 || initialRotation[2] != 0)
|
|
{
|
|
printCorrection("Starting from:", initial);
|
|
}
|
|
double p[6];
|
|
std::copy(initial, initial + 6, p);
|
|
for(int pass = 0; pass < 2; ++pass)
|
|
{
|
|
size_t points = 0;
|
|
size_t byType[3] = {0, 0, 0};
|
|
stepTimer.start();
|
|
for(Frame & f : frames)
|
|
{
|
|
selectEdgePoints(f, correctionFrom(p), decimation, jump, intensityJump, intensityWeight, creaseAngle, creaseVoxel, minDepth);
|
|
points += f.edgePoints.size();
|
|
for(unsigned char t : f.edgeTypes)
|
|
{
|
|
++byType[t];
|
|
}
|
|
}
|
|
const double selectionTime = stepTimer.ticks();
|
|
const double before = problem.score(correctionFrom(p));
|
|
const int evaluations = problem.evaluations();
|
|
solver->solve(problem, translation, p);
|
|
const double solverTime = stepTimer.ticks();
|
|
times.push_back(std::make_pair(uFormat("Pass %d: lidar edge points", pass + 1), selectionTime));
|
|
times.push_back(std::make_pair(uFormat("Pass %d: solver", pass + 1), solverTime));
|
|
printf("Pass %d: %d lidar edge points (depth %d, crease %d, intensity %d), score %.4f -> %.4f (%d score evaluations, %.1f s + %.1f s)\n",
|
|
pass + 1, (int)points, (int)byType[kEdgeDepth], (int)byType[kEdgeCrease], (int)byType[kEdgeIntensity],
|
|
before, problem.score(correctionFrom(p)), problem.evaluations() - evaluations - 1, selectionTime, solverTime);
|
|
}
|
|
|
|
{
|
|
// How often each kind of lidar edge lands on an image edge once corrected: what
|
|
// each brings, and how much of it is noise.
|
|
size_t near[3] = {0, 0, 0}, total[3] = {0, 0, 0};
|
|
for(const Frame & f : frames)
|
|
{
|
|
for(unsigned char t : f.edgeTypes)
|
|
{
|
|
++total[t];
|
|
}
|
|
}
|
|
problem.project(correctionFrom(p), [&](const Frame & f, size_t i, float u, float v) {
|
|
if(f.edgeDistance.at<float>(int(v + 0.5f), int(u + 0.5f)) <= 2.0f)
|
|
{
|
|
++near[f.edgeTypes[i]];
|
|
}
|
|
});
|
|
printf("\nLidar edge points within 2 pixels of an image edge, once corrected: depth %.0f%%, crease %.0f%%, intensity %.0f%%\n",
|
|
total[kEdgeDepth] ? 100.0 * near[kEdgeDepth] / total[kEdgeDepth] : 0.0,
|
|
total[kEdgeCrease] ? 100.0 * near[kEdgeCrease] / total[kEdgeCrease] : 0.0,
|
|
total[kEdgeIntensity] ? 100.0 * near[kEdgeIntensity] / total[kEdgeIntensity] : 0.0);
|
|
}
|
|
|
|
printf("\nConsistency, each half of the nodes on its own:\n");
|
|
stepTimer.start();
|
|
for(int h = 0; h < 2; ++h)
|
|
{
|
|
double q[6];
|
|
std::copy(initial, initial + 6, q);
|
|
solver->solve(CalibrationProblem(frames, h ? odd : even, minDepth), translation, q);
|
|
printCorrection(h ? " odd nodes: " : " even nodes:", q);
|
|
}
|
|
times.push_back(std::make_pair(std::string("Halves of the nodes"), stepTimer.ticks()));
|
|
|
|
if(verbose)
|
|
{
|
|
// Sensitivity: how much the score drops with the result off by 1 deg or 2 cm along
|
|
// or about each of the camera's body axes (mean of both directions). A large drop:
|
|
// the data determines that axis well; almost none: it is not observable from it.
|
|
const double best = problem.score(correctionFrom(p));
|
|
const char * names[6] = {"x", "y", "z", "roll", "pitch", "yaw"};
|
|
const double probe[6] = {0.02, 0.02, 0.02, 1.0, 1.0, 1.0};
|
|
double drop[6];
|
|
for(int k = 0; k < 6; ++k)
|
|
{
|
|
double sum = 0.0;
|
|
for(int sign = 0; sign < 2; ++sign)
|
|
{
|
|
double delta[6] = {0, 0, 0, 0, 0, 0};
|
|
delta[k] = sign ? -probe[k] : probe[k];
|
|
const Transform moved = mountCorrection(p) * Transform(delta[0], delta[1], delta[2],
|
|
delta[3] * M_PI / 180.0, delta[4] * M_PI / 180.0, delta[5] * M_PI / 180.0);
|
|
double q[6];
|
|
parametersFromMountCorrection(moved, q);
|
|
sum += problem.score(correctionFrom(q));
|
|
}
|
|
drop[k] = best > 0.0 ? 100.0 * (best - sum / 2.0) / best : 0.0;
|
|
}
|
|
printf("\nSensitivity (score drop with the result off by 1 deg or 2 cm; little: not observable):\n"
|
|
" %s %.1f%% %s %.1f%% %s %.1f%% | %s %.1f%% %s %.1f%% %s %.1f%%\n",
|
|
names[3], drop[3], names[4], drop[4], names[5], drop[5], names[0], drop[0], names[1], drop[1], names[2], drop[2]);
|
|
times.push_back(std::make_pair(std::string("Sensitivity"), stepTimer.ticks()));
|
|
}
|
|
|
|
if(!imagesDir.empty())
|
|
{
|
|
UDirectory::makeDir(imagesDir);
|
|
for(size_t i = 0; i < frames.size(); ++i)
|
|
{
|
|
const std::string prefix = imagesDir + "/" + uFormat("%04d", frames[i].id);
|
|
// Before: from where the search started (with the initial rotation, if any).
|
|
saveOverlay(frames[i], correctionFrom(initial), minDepth, prefix + "_edges_1_before.png");
|
|
saveOverlay(frames[i], correctionFrom(p), minDepth, prefix + "_edges_2_after.png");
|
|
saveCellMapOverlays(frames[i], correctionFrom(initial), decimation, creaseVoxel, minDepth, prefix, "_1_before.png");
|
|
saveCellMapOverlays(frames[i], correctionFrom(p), decimation, creaseVoxel, minDepth, prefix, "_2_after.png");
|
|
}
|
|
printf("\nImage overlays saved to %s\n", imagesDir.c_str());
|
|
times.push_back(std::make_pair(std::string("Images"), stepTimer.ticks()));
|
|
}
|
|
|
|
printf("\n");
|
|
const Transform X = mountCorrection(p);
|
|
{
|
|
float x, y, z, roll, pitch, yaw;
|
|
X.getTranslationAndEulerAngles(x, y, z, roll, pitch, yaw);
|
|
const Eigen::Quaternionf q = X.getQuaternionf();
|
|
printf("Correction of the camera's mount X, in the camera's body frame (x forward, y left, z up):\n"
|
|
" xyz (m): %.6f %.6f %.6f\n"
|
|
" roll pitch yaw (rad): %.6f %.6f %.6f (deg: %.4f %.4f %.4f)\n"
|
|
" quaternion (x y z w): %.6f %.6f %.6f %.6f\n\n",
|
|
x, y, z, roll, pitch, yaw, roll * 180.0f / float(M_PI), pitch * 180.0f / float(M_PI), yaw * 180.0f / float(M_PI),
|
|
q.x(), q.y(), q.z(), q.w());
|
|
}
|
|
if(initialRotation[0] != 0 || initialRotation[1] != 0 || initialRotation[2] != 0)
|
|
{
|
|
// As if the database's camera transform were off by the initial rotation: what
|
|
// undoes it, to see that the result was found again from there.
|
|
const Transform X0(0, 0, 0, initialRotation[0] * M_PI / 180.0, initialRotation[1] * M_PI / 180.0, initialRotation[2] * M_PI / 180.0);
|
|
float x, y, z, roll, pitch, yaw;
|
|
(X0.inverse() * X).getTranslationAndEulerAngles(x, y, z, roll, pitch, yaw);
|
|
printf(" relative to the initial rotation (%g, %g, %g deg): rpy (deg): %.4f %.4f %.4f\n\n",
|
|
initialRotation[0], initialRotation[1], initialRotation[2],
|
|
roll * 180.0f / float(M_PI), pitch * 180.0f / float(M_PI), yaw * 180.0f / float(M_PI));
|
|
}
|
|
printf("The robot base -> camera transform B (e.g., base_link -> camera_link) becomes B * X.\n"
|
|
"Or insert X after B in TF: base_link -[B]-> camera_link_measured -[X]-> camera_link,\n"
|
|
"the rest unchanged.\n");
|
|
|
|
printf("\nTime:\n");
|
|
for(const std::pair<std::string, double> & t : times)
|
|
{
|
|
printf(" %-40s %7.1f s\n", t.first.c_str(), t.second);
|
|
}
|
|
printf(" %-40s %7.1f s\n", "Total", totalTimer.elapsed());
|
|
return 0;
|
|
}
|