/* Copyright (c) 2010-2016, 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. */ #include #include #include #include #include #include #include #include #include #include #include #ifdef WITH_QT #include #include #include #endif using namespace rtabmap; void showUsage() { printf("\nUsage:\n" "rtabmap-report [\"Statistic/Id\"] [options] path\n" #ifndef WITH_QT "[Not built with Qt, statistics cannot be plotted]\n" #endif " path Directory containing rtabmap databases or path of a database.\n" " Options:" " --latex Print table formatted in LaTeX with results.\n" " --kitti Compute error based on KITTI benchmark.\n" " --relative Compute relative motion error between poses.\n" " --loop Compute relative motion error of loop closures.\n" " --scale Find the best scale for the map against the ground truth\n" " and compute error based on the scaled path.\n" " --poses Export odometry to [path]_odom.txt, optimized graph to [path]_slam.txt \n" " and ground truth to [path]_gt.txt in TUM RGB-D format.\n" " --poses_raw Same as --poses, but poses are not aligned to gt." " --gt FILE.txt Use this file as ground truth (TUM RGB-D format). It will\n" " override the ground truth set in database if there is one.\n" " If extension is *.db, the optimized poses of that database will\n" " be used as ground truth.\n" " --gt_max_t # Maximum time interval (sec) to interpolate between a pose and\n" " corresponding ground truth (default 1 sec).\n" " --inc Incremental optimization. \n" " --stats Show available statistics \"Statistic/Id\" to plot or get localization statistics (if path is a file). \n" #ifdef WITH_QT " --invert When reading many databases, put all curves from a same \n" " database in same figure, instead of all same curves from \n" " different database in same figure. When reading a single \n" " database, the inverse will be done. \n" " --ids Use IDs for x axis instead of time in the figures. \n" " --start # Start from this node ID for the figures.\n" " --export Export figures' data to txt files.\n" " --export_prefix Prefix to filenames of exported figures' data (default is \"Stat\").\n" #endif " --report Export all evaluation statistics values in report.txt \n" " --loc [#] Show localization statistics for each \"Statistic/Id\" per\n" " session. Optionally set number 1=min,2=max,4=mean,8=stddev,16=total,32=nonnull%%\n" " to show cumulative results on console (it is a mask, \n" " we can combine those numbers, e.g., 63 for all) \n" " --loc_delay # Delay to split sessions for localization statistics (default 60 seconds).\n" " --ignore_inter_nodes Ignore intermediate poses and statistics.\n" " --udebug Show debug log.\n" " --\"parameter name\" \"value\" Overwrite a specific RTAB-Map's parameter.\n" " --help,-h Show usage\n\n"); exit(1); } struct LocStats { static LocStats from(const std::vector & array) { LocStats values; values.mean = uMean(array); values.stddev = std::sqrt(uVariance(array, values.mean)); uMinMax(array, values.min, values.max); values.total = array.size(); values.nonNull = 0.0f; if(!array.empty()) { for(size_t j=0; j statsToShow; #ifdef WITH_QT std::map figures; #endif ParametersMap overriddenParams = Parameters::parseArguments(argc, argv, true); for(int i=1; i10) { printf("\"--gt_max_t\" option should be between 0 and 10 sec, parsed %f sec.\n", gtMaxInterval); showUsage(); } } else { printf("Missing value for \"--gt\" option.\n"); showUsage(); } } else if(strcmp(argv[i],"--loc") == 0) { ++i; if(ifirst.c_str(), iter->second.c_str()); } } std::string path = argv[argc-1]; path = uReplaceChar(path, '~', UDirectory::homeDir()); if(!UDirectory::exists(path) && UFile::getExtension(path).compare("db") == 0) { invertFigures = !invertFigures; } std::map > > > localizationMultiStats; //> > for(size_t i=0; iresize(QSize(640,480)); fig->setWindowTitle(figureTitle.c_str()); if(useIds) { fig->setXLabel("Node ID"); } else { fig->setXLabel("Time (s)"); } figures.insert(std::make_pair(figureTitle, fig)); #endif } if(showLoc & 0b111111) { localizationMultiStats.insert(std::make_pair(figureTitle, std::vector > >())); } } if(!invertFigures) { statsToShow.clear(); } // External Ground Truth std::map externalGtPoses; if(!gtFile.empty()) { if(UFile::getExtension(gtFile) == "db") { DBDriver * driver = DBDriver::create(); if(driver->openConnection(gtFile)) { std::map poses = driver->loadOptimizedPoses(); for(std::map::iterator iter=poses.begin(); iter!=poses.end(); ++iter) { Transform p, gt; GPS gps; int m=-1, w=-1; std::string l; double s; std::vector v; EnvSensors sensors; if(driver->getNodeInfo(iter->first, p, m, w, l, s, gt, v, gps, sensors)) { externalGtPoses.insert(std::make_pair(s, iter->second)); } } } delete driver; } else { // 10 can import format 1, 10 and 11. std::map poses; std::map stamps; if(rtabmap::graph::importPoses(gtFile, 10, poses, 0, &stamps)) { for(std::map::iterator iter=poses.begin(); iter!=poses.end(); ++iter) { externalGtPoses.insert(std::make_pair(stamps.at(iter->first), iter->second)); } } } if(externalGtPoses.size() < 2) { printf("Failed loading ground truth poses from \"%s\" (\"--gt\" option).\n", gtFile.c_str()); showUsage(); } else { printf("Loading %ld ground truth poses from \"%s\".\n", externalGtPoses.size(), gtFile.c_str()); } } std::string fileName; std::list paths; paths.push_back(path); std::vector > > outputLatexStatistics; std::map > outputLatexStatisticsMap; bool odomRAMSet = false; std::set topDirs; while(paths.size()) { std::string currentPath = paths.front(); UDirectory currentDir(currentPath); paths.pop_front(); bool currentPathIsDatabase = false; if(!currentDir.isValid()) { if(UFile::getExtension(currentPath).compare("db") == 0) { currentPathIsDatabase=true; printf("Database: %s\n", currentPath.c_str()); } else { continue; } } std::list subDirs; if(!currentPathIsDatabase) { printf("Directory: %s\n", currentPath.c_str()); std::list fileNames = currentDir.getFileNames(); if(topDirs.empty()) { for(std::list::iterator iter = fileNames.begin(); iter!=fileNames.end(); ++iter) { topDirs.insert(currentPath+"/"+*iter); } } else { if(topDirs.find(currentPath) != topDirs.end()) { if(outputLatexStatisticsMap.size()) { outputLatexStatistics.push_back(outputLatexStatisticsMap); outputLatexStatisticsMap.clear(); } } } } // For all databases in currentDir while(currentPathIsDatabase || !(fileName = currentDir.getNextFileName()).empty()) { int startIdPerDb = startId; if(currentPathIsDatabase || UFile::getExtension(fileName).compare("db") == 0) { std::string filePath; if(currentPathIsDatabase) { filePath = currentPath; fileName = UFile::getName(currentPath); } else { filePath = currentPath + UDirectory::separator() + fileName; } DBDriver * driver = DBDriver::create(); ParametersMap params; if(driver->openConnection(filePath)) { ULogger::Level logLevel = ULogger::level(); if(ULogger::level() == ULogger::kWarning) { ULogger::setLevel(ULogger::kError); // to suppress parameter warnings } params = driver->getLastParameters(); uInsert(params, overriddenParams); ULogger::setLevel(logLevel); std::set ids; driver->getAllNodeIds(ids, false, false, ignoreInterNodes); std::set allIds = ids; if(ignoreInterNodes) { driver->getAllNodeIds(allIds, false, false, false); } std::map, double> > stats = driver->getAllStatistics(); std::map odomPoses, gtPoses; std::map odomStamps; std::vector cameraTime; cameraTime.reserve(ids.size()); std::vector odomTime; odomTime.reserve(ids.size()); std::vector slamTime; slamTime.reserve(ids.size()); float rmse = -1; float maxRMSE = -1; float maxOdomRAM = -1; float maxMapRAM = -1; #ifdef WITH_QT if(currentPathIsDatabase && showAvailableStats) { std::map availableStats; for(std::set::iterator iter=ids.begin(); iter!=ids.end(); ++iter) { if(stats.find(*iter) != stats.end()) { for(std::map::iterator jter=stats.at(*iter).first.begin(); jter!=stats.at(*iter).first.end(); ++jter) { if(availableStats.find(jter->first) != availableStats.end()) { ++availableStats.at(jter->first); } else { availableStats.insert(std::make_pair(jter->first, 1)); } } } } printf("Showing available statistics in \"%s\":\n", filePath.c_str()); for(std::map::iterator iter=availableStats.begin(); iter!=availableStats.end(); ++iter) { printf("%s (%d)\n", iter->first.c_str(), iter->second); } printf("\n"); exit(1); } std::map curves; if(statsToShow.empty()) { for(std::map::iterator iter=figures.begin(); iter!=figures.end(); ++iter) { curves.insert(std::make_pair(iter->first, iter->second->addCurve(filePath.c_str()))); if(!localizationMultiStats.empty()) localizationMultiStats.at(iter->first).push_back(std::make_pair(fileName, std::vector())); } } else { UPlot * fig = new UPlot(); fig->setWindowTitle(filePath.c_str()); if(useIds) { fig->setXLabel("Node ID"); } else { fig->setXLabel("Time (s)"); } if(!figures.insert(std::make_pair(filePath.c_str(), fig)).second) { delete fig; printf("Figure %s already added!\n", filePath.c_str()); } else { for(size_t i=0; iaddCurve(statsToShow[i].c_str()))); if(!localizationMultiStats.empty()) localizationMultiStats.at(statsToShow[i]).push_back(std::make_pair(fileName, std::vector())); } } } #else for(size_t i=0; i())); } #endif // Find localization sessions and adjust startId std::set mappingSessionIds; if(!localizationMultiStats.empty()) { std::map poses = driver->loadOptimizedPoses(); if(!poses.empty()) { for(std::map::iterator iter=poses.begin(); iter!=poses.end(); ++iter) { Transform p, gt; GPS gps; int m=-1, w=-1; std::string l; double s; std::vector v; EnvSensors sensors; if(driver->getNodeInfo(iter->first, p, m, w, l, s, gt, v, gps, sensors)) { mappingSessionIds.insert(m); } } if(startIdPerDb ==0) { startIdPerDb = poses.rbegin()->first+1; } } } std::map > localizationSessionStats; double previousStamp = 0.0; std::map allWeights; Transform previousPose; int previousMapId = 0; float totalOdomDistance = 0.0f; for(std::set::iterator iter=allIds.begin(); iter!=allIds.end(); ++iter) { Transform p, gt; GPS gps; int m=-1, w=-1; std::string l; double s; std::vector v; EnvSensors sensors; if(driver->getNodeInfo(*iter, p, m, w, l, s, gt, v, gps, sensors)) { if(previousMapId == m) { if(!p.isNull() && !previousPose.isNull()) { totalOdomDistance += p.getDistance(previousPose); } } previousPose = p; previousMapId = m; allWeights.insert(std::make_pair(*iter, w)); if(!ignoreInterNodes || w!=-1) { if(w!=-9) { odomPoses.insert(std::make_pair(*iter, p)); odomStamps.insert(std::make_pair(*iter, s)); if(!externalGtPoses.empty()) { std::map::iterator nextIter = externalGtPoses.upper_bound(s); if(nextIter!=externalGtPoses.end()) { std::map::iterator previousIter = nextIter; --previousIter; if(s == previousIter->first || (nextIter->first-s <= gtMaxInterval && s-previousIter->first <= gtMaxInterval)) { UASSERT(s-previousIter->first >= 0); gtPoses.insert(std::make_pair(*iter, previousIter->second.interpolate((s-previousIter->first)/(nextIter->first-previousIter->first),nextIter->second))); } } } else if(!gt.isNull()) { gtPoses.insert(std::make_pair(*iter, gt)); } } if(!localizationMultiStats.empty() && mappingSessionIds.find(m) != mappingSessionIds.end()) { continue; } if(*iter >= startIdPerDb && uContains(stats, *iter)) { const std::map & stat = stats.at(*iter).first; if(uContains(stat, Statistics::kGtTranslational_rmse())) { rmse = stat.at(Statistics::kGtTranslational_rmse()); if(maxRMSE==-1 || maxRMSE < rmse) { maxRMSE = rmse; } } if(uContains(stat, std::string("Camera/TotalTime/ms"))) { cameraTime.push_back(stat.at(std::string("Camera/TotalTime/ms"))); } if(uContains(stat, std::string("Odometry/TotalTime/ms"))) { odomTime.push_back(stat.at(std::string("Odometry/TotalTime/ms"))); } else if(uContains(stat, std::string("Odometry/TimeEstimation/ms"))) { odomTime.push_back(stat.at(std::string("Odometry/TimeEstimation/ms"))); } if(uContains(stat, std::string("RtabmapROS/TotalTime/ms"))) { if(w!=-1) { slamTime.push_back(stat.at("RtabmapROS/TotalTime/ms")); } } else if(uContains(stat, Statistics::kTimingTotal())) { if(w!=-1) { slamTime.push_back(stat.at(Statistics::kTimingTotal())); } } if(uContains(stat, std::string(Statistics::kMemoryRAM_usage()))) { float ram = stat.at(Statistics::kMemoryRAM_usage()); if(maxMapRAM==-1 || maxMapRAM < ram) { maxMapRAM = ram; } } if(uContains(stat, std::string("Odometry/RAM_usage/MB"))) { float ram = stat.at("Odometry/RAM_usage/MB"); if(maxOdomRAM==-1 || maxOdomRAM < ram) { maxOdomRAM = ram; } } #ifdef WITH_QT for(std::map::iterator jter=curves.begin(); jter!=curves.end(); ++jter) { #else for(std::map > > >::iterator jter=localizationMultiStats.begin(); jter!=localizationMultiStats.end(); ++jter) { #endif if(uContains(stat, jter->first)) { double y = stat.at(jter->first); #ifdef WITH_QT double x = s; if(useIds) { x = *iter; } jter->second->addValue(x,y); #endif if(!localizationMultiStats.empty()) { if(previousStamp > 0 && fabs(s - previousStamp) > locDelay && uContains(localizationSessionStats, jter->first)) { // changed session for(std::map >::iterator kter=localizationSessionStats.begin(); kter!=localizationSessionStats.end(); ++kter) { LocStats values = LocStats::from(localizationSessionStats.at(kter->first)); localizationMultiStats.at(kter->first).rbegin()->second.push_back(values); localizationSessionStats.at(kter->first).clear(); } previousStamp = s; } if(!uContains(localizationSessionStats, jter->first)) { localizationSessionStats.insert(std::make_pair(jter->first, std::vector())); } localizationSessionStats.at(jter->first).push_back(y); } } } previousStamp = s; } } } } for(std::map > > >::iterator jter=localizationMultiStats.begin(); jter!=localizationMultiStats.end(); ++jter) { if(uContains(localizationSessionStats, jter->first) && !localizationSessionStats.at(jter->first).empty()) { // changed session LocStats values = LocStats::from(localizationSessionStats.at(jter->first)); localizationMultiStats.at(jter->first).rbegin()->second.push_back(values); } } std::multimap links; std::multimap allLinks; driver->getAllLinks(allLinks, true, true); if(ignoreInterNodes) { std::multimap allBiLinks; for(std::multimap::iterator jter=allLinks.begin(); jter!=allLinks.end(); ++jter) { if(jter->second.from() != jter->second.to() && (jter->second.type() == Link::kNeighbor || jter->second.type() == Link::kNeighborMerged)) { allBiLinks.insert(std::make_pair(jter->second.to(), jter->second.inverse())); } allBiLinks.insert(*jter); } allLinks=allBiLinks; } std::multimap loopClosureLinks; int landmarks = 0; for(std::multimap::iterator jter=allLinks.begin(); jter!=allLinks.end(); ++jter) { if(jter->second.from() == jter->second.to() || graph::findLink(links, jter->second.from(), jter->second.to(), true) == links.end()) { Link link = jter->second; // remove intermediate nodes? if(link.from() != link.to() && ignoreInterNodes && (link.type() == Link::kNeighbor || link.type() == Link::kNeighborMerged)) { while(uContains(allWeights, link.to()) && allWeights.at(link.to()) < 0) { std::multimap::iterator uter = allLinks.find(link.to()); while(uter != allLinks.end() && uter->first==link.to() && uter->second.from()>uter->second.to()) { ++uter; } if(uter != allLinks.end()) { link = link.merge(uter->second, uter->second.type()); allLinks.erase(uter->first); } else { break; } } } links.insert(std::make_pair(jter->first, link)); } if( jter->second.type() != Link::kNeighbor && jter->second.type() != Link::kNeighborMerged && jter->second.type() != Link::kGravity && jter->second.type() != Link::kLandmark && graph::findLink(loopClosureLinks, jter->second.from(), jter->second.to()) == loopClosureLinks.end()) { loopClosureLinks.insert(*jter); } else if(jter->second.type() == Link::kLandmark) { landmarks++; } } float bestScale = 1.0f; float bestRMSE = -1; float bestRMSEAng = -1; float bestVoRMSE = -1; Transform bestGtToMap = Transform::getIdentity(); Transform bestGtToOdom = Transform::getIdentity(); float kitti_t_err = 0.0f; float kitti_r_err = 0.0f; float relative_t_err = 0.0f; float relative_r_err = 0.0f; float loop_t_err = 0.0f; float loop_r_err = 0.0f; if(ids.size()) { std::map posesOut; std::multimap linksOut; int firstId = *ids.begin(); rtabmap::Optimizer * optimizer = rtabmap::Optimizer::create(params); bool useOdomGravity = Parameters::defaultMemUseOdomGravity(); Parameters::parse(params, Parameters::kMemUseOdomGravity(), useOdomGravity); if(useOdomGravity) { for(std::map::iterator iter=odomPoses.begin(); iter!=odomPoses.end(); ++iter) { links.insert(std::make_pair(iter->first, Link(iter->first, iter->first, Link::kGravity, iter->second))); } } std::map posesWithLandmarks = odomPoses; // Added landmark poses if there are some for(std::multimap::iterator iter=links.begin(); iter!=links.end(); ++iter) { if(iter->second.type() == Link::kLandmark) { UASSERT(iter->second.from() > 0 && iter->second.to() < 0); if(posesWithLandmarks.find(iter->second.from()) != posesWithLandmarks.end() && posesWithLandmarks.find(iter->second.to()) == posesWithLandmarks.end()) { posesWithLandmarks.insert(std::make_pair(iter->second.to(), posesWithLandmarks.at(iter->second.from())*iter->second.transform())); } } } optimizer->getConnectedGraph(firstId, posesWithLandmarks, links, posesOut, linksOut); std::list > intermediateGraphes; std::map poses = optimizer->optimize(firstId, posesOut, linksOut, incrementalOptimization?&intermediateGraphes:0); if(poses.empty()) { // try incremental optimization UWARN("Optimization failed! Try incremental optimization..."); poses = optimizer->optimizeIncremental(firstId, posesOut, linksOut); if(poses.empty()) { UERROR("Incremental optimization also failed! Only original RMSE will be shown."); bestRMSE = rmse; } else { UWARN("Incremental optimization succeeded!"); } } if(poses.size()) { //remove landmarks std::map::iterator iter=poses.begin(); std::map optimizedLandmarks; while(iter!=poses.end() && iter->first < 0) { optimizedLandmarks.insert(*iter); poses.erase(iter++); } if(outputKittiError) { // remove landmarks std::map::iterator iter=posesOut.begin(); while(iter!=posesOut.end() && iter->first < 0) { posesOut.erase(iter++); } } std::map groundTruth; for(std::map::const_iterator iter=poses.begin(); iter!=poses.end(); ++iter) { if(gtPoses.find(iter->first) != gtPoses.end()) { groundTruth.insert(*gtPoses.find(iter->first)); } } outputScaled = outputScaled && groundTruth.size(); for(float scale=outputScaled?0.900f:1.0f; scale<1.100f; scale+=0.001) { std::map scaledPoses; std::map scaledOdomPoses; for(std::map::iterator iter=poses.begin(); iter!=poses.end(); ++iter) { Transform t = iter->second.clone(); t.x() *= scale; t.y() *= scale; t.z() *= scale; scaledPoses.insert(std::make_pair(iter->first, t)); UASSERT(posesOut.find(iter->first)!=posesOut.end()); t = posesOut.at(iter->first).clone(); t.x() *= scale; t.y() *= scale; t.z() *= scale; scaledOdomPoses.insert(std::make_pair(iter->first, t)); } // compute RMSE statistics float translational_rmse = 0.0f; float translational_mean = 0.0f; float translational_median = 0.0f; float translational_std = 0.0f; float translational_min = 0.0f; float translational_max = 0.0f; float rotational_rmse = 0.0f; float rotational_mean = 0.0f; float rotational_median = 0.0f; float rotational_std = 0.0f; float rotational_min = 0.0f; float rotational_max = 0.0f; Transform gtToOdom = graph::calcRMSE( groundTruth, scaledOdomPoses, translational_rmse, translational_mean, translational_median, translational_std, translational_min, translational_max, rotational_rmse, rotational_mean, rotational_median, rotational_std, rotational_min, rotational_max); float translational_rmse_vo = translational_rmse; Transform gtToMap = graph::calcRMSE( groundTruth, scaledPoses, translational_rmse, translational_mean, translational_median, translational_std, translational_min, translational_max, rotational_rmse, rotational_mean, rotational_median, rotational_std, rotational_min, rotational_max); if(bestRMSE!=-1 && translational_rmse > bestRMSE) { break; } bestRMSE = translational_rmse; bestVoRMSE = translational_rmse_vo; bestRMSEAng = rotational_rmse; bestScale = scale; bestGtToMap = gtToMap; bestGtToOdom = gtToOdom; if(!outputScaled) { // just did iteration without any scale, then exit break; } } // Scale/align slam poses for(std::map::iterator iter=poses.begin(); iter!=poses.end(); ++iter) { iter->second.x()*=bestScale; iter->second.y()*=bestScale; iter->second.z()*=bestScale; if(outputPosesAlignedToGt) iter->second = bestGtToMap * iter->second; } for(std::map::iterator iter=optimizedLandmarks.begin(); iter!=optimizedLandmarks.end(); ++iter) { iter->second.x()*=bestScale; iter->second.y()*=bestScale; iter->second.z()*=bestScale; if(outputPosesAlignedToGt) iter->second = bestGtToMap * iter->second; } // Scale/align odom poses for(std::map::iterator iter=posesOut.begin(); iter!=posesOut.end(); ++iter) { iter->second.x()*=bestScale; iter->second.y()*=bestScale; iter->second.z()*=bestScale; if(outputPosesAlignedToGt) iter->second = bestGtToOdom * iter->second; } if(outputRelativeError) { if(groundTruth.size() == poses.size()) { // compute Motion statistics graph::calcRelativeErrors(uValues(groundTruth), uValues(poses), relative_t_err, relative_r_err); } else { std::vector gtPosesTmp; std::vector rPoses; for(std::map::iterator iter=poses.begin(); iter!=poses.end(); ++iter) { if(groundTruth.find(iter->first) != groundTruth.end()) { gtPosesTmp.push_back(groundTruth.at(iter->first)); rPoses.push_back(poses.at(iter->first)); } } if(!gtPosesTmp.empty()) { graph::calcRelativeErrors(gtPosesTmp, rPoses, relative_t_err, relative_r_err); } } } if(outputKittiError) { if(groundTruth.size() == poses.size()) { // compute KITTI statistics graph::calcKittiSequenceErrors(uValues(groundTruth), uValues(poses), kitti_t_err, kitti_r_err); } else { printf("Cannot compute KITTI statistics as optimized poses and ground truth don't have the same size (%d vs %d).\n", (int)poses.size(), (int)groundTruth.size()); } } if(outputPoses) { std::string dir = UDirectory::getDir(filePath); std::string dbName = UFile::getName(filePath); dbName = dbName.substr(0, dbName.size()-3); // remove db std::string path = dir+UDirectory::separator()+dbName+"_slam.txt"; std::multimap dummyLinks; std::map stamps; if(!outputKittiError) { // re-add landmarks uInsert(poses, optimizedLandmarks); } if(!outputKittiError) { for(std::map::iterator iter=poses.begin(); iter!=poses.end(); ++iter) { if(iter->first < 0) { stamps.insert(std::make_pair(iter->first, 0)); } else { UASSERT(odomStamps.find(iter->first) != odomStamps.end()); stamps.insert(*odomStamps.find(iter->first)); } } } if(!graph::exportPoses(path, outputKittiError?2:11, poses, dummyLinks, stamps)) { printf("Could not export the poses to \"%s\"!?!\n", path.c_str()); } //export odom path = dir+UDirectory::separator()+dbName+"_odom.txt"; stamps.clear(); if(!outputKittiError) { for(std::map::iterator iter=posesOut.begin(); iter!=posesOut.end(); ++iter) { if(iter->first < 0) { stamps.insert(std::make_pair(iter->first, 0)); } else { UASSERT(odomStamps.find(iter->first) != odomStamps.end()); stamps.insert(*odomStamps.find(iter->first)); } } } if(!graph::exportPoses(path, outputKittiError?2:11, posesOut, dummyLinks, stamps)) { printf("Could not export the odometry to \"%s\"!?!\n", path.c_str()); } //export ground truth if(groundTruth.size()) { path = dir+UDirectory::separator()+dbName+"_gt.txt"; stamps.clear(); if(!outputKittiError) { for(std::map::iterator iter=groundTruth.begin(); iter!=groundTruth.end(); ++iter) { UASSERT(odomStamps.find(iter->first) != odomStamps.end()); stamps.insert(*odomStamps.find(iter->first)); } } if(!graph::exportPoses(path, outputKittiError?2:11, groundTruth, dummyLinks, stamps)) { printf("Could not export the ground truth to \"%s\"!?!\n", path.c_str()); } } } if (outputReport) { bool fillHeader = false; std::ifstream f("report.csv"); if(!f.good()) { fillHeader = true; } std::ofstream myfile; myfile.open("report.csv", std::fstream::in | std::fstream::out | std::fstream::app); if(fillHeader) { myfile << "Rosbag name"<<";"<<"error linear (m)"<<";"<<"error linear max (m)"<<";"<<"error linear odom (m)"<<";" <<"error angular"<<";" <<"Slam avg (hz)"<<";"<<"Slam max (hz)"<<";" <<"Odom avg (hz)"<<";"<<"Odom max (hz)"<::iterator iter=loopClosureLinks.begin(); iter!=loopClosureLinks.end(); ++iter) { if( groundTruth.find(iter->second.from())!=groundTruth.end() && groundTruth.find(iter->second.to())!=groundTruth.end()) { Transform gtLink = groundTruth.at(iter->second.from()).inverse()*groundTruth.at(iter->second.to()); const Transform & t = iter->second.transform(); Transform scaledLink( t.r11(), t.r12(), t.r13(), t.x()*bestScale, t.r21(), t.r22(), t.r23(), t.y()*bestScale, t.r31(), t.r32(), t.r33(), t.z()*bestScale); sumDist += gtLink.getDistance(scaledLink); sumAngle += gtLink.getAngle(scaledLink); ++count; } } if(count>0) { loop_t_err = sumDist/float(count); loop_r_err = sumAngle/float(count); loop_r_err *= 180/CV_PI; // Rotation error (deg) } } } } printf(" %s (%d, %.1f m%s): RMSE= %.3f m (max=%s, odom=%.3f m) ang=%.1f deg%s%s, %s: avg=%d ms (max=%d ms) loops=%d%s%s%s%s%s%s\n", fileName.c_str(), (int)ids.size(), totalOdomDistance, bestScale != 1.0f?uFormat(", s=%.3f", bestScale).c_str():"", bestRMSE, maxRMSE!=-1?uFormat("%.3fm", maxRMSE).c_str():"NA", bestVoRMSE, bestRMSEAng, !outputKittiError?"":uFormat(", KITTI: t_err=%.2f%% r_err=%.2f deg/100m", kitti_t_err, kitti_r_err*100).c_str(), !outputRelativeError?"":uFormat(", Relative: t_err=%.3fm r_err=%.2f deg", relative_t_err, relative_r_err).c_str(), !localizationMultiStats.empty()?"loc":"slam", (int)uMean(slamTime), (int)uMax(slamTime), (int)loopClosureLinks.size(), landmarks==0?"":uFormat(", landmarks = %d", landmarks).c_str(), !outputLoopAccuracy?"":uFormat(" (t_err=%.3fm r_err=%.2f deg)", loop_t_err, loop_r_err).c_str(), odomTime.empty()?"":uFormat(", odom: avg=%dms (max=%dms)", (int)uMean(odomTime), (int)uMax(odomTime)).c_str(), cameraTime.empty()?"":uFormat(", camera: avg=%dms", (int)uMean(cameraTime)).c_str(), maxOdomRAM!=-1.0f?uFormat(", RAM odom=%dMB ", (int)maxOdomRAM).c_str():"", maxMapRAM!=-1.0f?uFormat(", map=%dMB",(int)maxMapRAM).c_str():""); if(outputLatex) { std::vector stats; stats.push_back(ids.size()); stats.push_back(bestRMSE); stats.push_back(maxRMSE); stats.push_back(bestRMSEAng); stats.push_back(uMean(odomTime)); stats.push_back(uMean(slamTime)); stats.push_back(uMax(slamTime)); stats.push_back(maxOdomRAM); stats.push_back(maxMapRAM); outputLatexStatisticsMap.insert(std::make_pair(filePath, stats)); if(maxOdomRAM != -1.0f) { odomRAMSet = true; } } } driver->closeConnection(); delete driver; } else if(uSplit(fileName, '.').size() == 1) { //sub directory subDirs.push_front(currentPath + UDirectory::separator() + fileName); } currentPathIsDatabase = false; } if(!localizationMultiStats.empty() && showLoc!=-1) { printf("---Localization results---\n"); std::string prefix = "header={"; printf("%s", prefix.c_str()); for(std::vector > >::iterator iter=localizationMultiStats.begin()->second.begin(); iter!=localizationMultiStats.begin()->second.end();) { if(iter!=localizationMultiStats.begin()->second.begin()) { printf("%s", std::string(prefix.size(), ' ').c_str()); } printf("%s", iter->first.c_str()); ++iter; if(iter!=localizationMultiStats.begin()->second.end()) { printf(";\n"); } } printf("}\n"); for(std::map > > >::iterator iter=localizationMultiStats.begin(); iter!=localizationMultiStats.end(); ++iter) { printf("%s\n", iter->first.c_str()); for(int k=0; k<6; ++k) { if(showLoc & (0x1 << k)) { std::string prefix = uFormat(" %s=[", k==0?"min": k==1?"max": k==2?"mean": k==3?"stddev": k==4?"total": "nonnull%"); printf("%s", prefix.c_str()); for(std::vector > >::iterator jter=iter->second.begin(); jter!=iter->second.end();) { if(jter!=iter->second.begin()) { printf("%s", std::string(prefix.size(), ' ').c_str()); } for(size_t j=0; jsecond.size(); ++j) { if(k<4) { printf("%f", k==0?jter->second[j].min: k==1?jter->second[j].max: k==2?jter->second[j].mean: jter->second[j].stddev); } else if(k==4) { printf("%d",jter->second[j].total); } else if(k==5) { printf("%.2f", (jter->second[j].nonNull*100)); } if(j+1 < jter->second.size()) { printf(" "); } } ++jter; if(jter!=iter->second.end()) { printf(";\n"); } } printf("]\n"); } } iter->second.clear(); } } for(std::list::iterator iter=subDirs.begin(); iter!=subDirs.end(); ++iter) { paths.push_front(*iter); } if(outputLatexStatisticsMap.size() && paths.empty()) { outputLatexStatistics.push_back(outputLatexStatisticsMap); outputLatexStatisticsMap.clear(); } } if(outputLatex && outputLatexStatistics.size()) { printf("\nLaTeX output:\n----------------\n"); printf("\\begin{table*}[!t]\n"); printf("\\caption{$t_{end}$ is the absolute translational RMSE value at the end " "of the experiment as $ATE_{max}$ is the maximum during the experiment. " "$r_{end}$ is rotational RMSE value at the end of the experiment. " "$o_{avg}$ and $m_{avg}$ are the average computational time " "for odometry (front-end) and map update (back-end). " "$m_{avg}$ is the maximum computational time for map update. " "$O_{end}$ and $M_{end}$ are the RAM usage at the end of the experiment " "for odometry and map management respectively.}\n"); printf("\\label{}\n"); printf("\\centering\n"); if(odomRAMSet) { printf("\\begin{tabular}{l|c|c|c|c|c|c|c|c|c}\n"); printf("\\cline{2-10}\n"); printf(" & Size & $t_{end}$ & $t_{max}$ & $r_{end}$ & $o_{avg}$ & $m_{avg}$ & $m_{max}$ & $O_{end}$ & $M_{end}$ \\\\\n"); printf(" & (nodes) & (m) & (m) & (deg) & (ms) & (ms) & (ms) & (MB) & (MB) \\\\\n"); } else { printf("\\begin{tabular}{l|c|c|c|c|c|c|c|c}\n"); printf("\\cline{2-9}\n"); printf(" & Size & $t_{end}$ & $t_{max}$ & $r_{end}$ & $o_{avg}$ & $m_{avg}$ & $m_{max}$ & $M_{end}$ \\\\\n"); printf(" & (nodes) & (m) & (m) & (deg) & (ms) & (ms) & (ms) & (MB) \\\\\n"); } printf("\\hline\n"); for(unsigned int j=0; j lowestIndex; if(outputLatexStatistics[j].size() > 1) { std::vector lowestValue(outputLatexStatistics[j].begin()->second.size(),-1); lowestIndex = std::vector(lowestValue.size(),0); int index = 0; for(std::map >::iterator iter=outputLatexStatistics[j].begin(); iter!=outputLatexStatistics[j].end(); ++iter) { UASSERT(lowestValue.size() == iter->second.size()); for(unsigned int i=0; isecond.size(); ++i) { if(lowestValue[i] == -1 || (iter->second[i]>0.0f && lowestValue[i]>iter->second[i])) { lowestValue[i] = iter->second[i]; lowestIndex[i] = index; } } ++index; } } int index = 0; for(std::map >::iterator iter=outputLatexStatistics[j].begin(); iter!=outputLatexStatistics[j].end(); ++iter) { UASSERT(iter->second.size() == 9); printf("%s & ", uReplaceChar(iter->first.c_str(), '_', '-').c_str()); printf("%d & ", (int)iter->second[0]); printf("%s%.3f%s & ", lowestIndex.size()&&lowestIndex[1]==index?"\\textbf{":"", iter->second[1], lowestIndex.size()&&lowestIndex[1]==index?"}":""); printf("%s%.3f%s & ", lowestIndex.size()&&lowestIndex[2]==index?"\\textbf{":"", iter->second[2], lowestIndex.size()&&lowestIndex[2]==index?"}":""); printf("%s%.2f%s & ", lowestIndex.size()&&lowestIndex[3]==index?"\\textbf{":"", iter->second[3], lowestIndex.size()&&lowestIndex[3]==index?"}":""); printf("%s%d%s & ", lowestIndex.size()&&lowestIndex[4]==index?"\\textbf{":"", (int)iter->second[4], lowestIndex.size()&&lowestIndex[4]==index?"}":""); printf("%s%d%s & ", lowestIndex.size()&&lowestIndex[5]==index?"\\textbf{":"", (int)iter->second[5], lowestIndex.size()&&lowestIndex[5]==index?"}":""); printf("%s%d%s & ", lowestIndex.size()&&lowestIndex[6]==index?"\\textbf{":"", (int)iter->second[6], lowestIndex.size()&&lowestIndex[6]==index?"}":""); if(odomRAMSet) { printf("%s%d%s & ", lowestIndex.size()&&lowestIndex[7]==index?"\\textbf{":"", (int)iter->second[7], lowestIndex.size()&&lowestIndex[7]==index?"}":""); } printf("%s%d%s ", lowestIndex.size()&&lowestIndex[8]==index?"\\textbf{":"", (int)iter->second[8], lowestIndex.size()&&lowestIndex[8]==index?"}":""); printf("\\\\\n"); ++index; } printf("\\hline\n"); } } printf("\\end{tabular}\n"); printf("\\end{table*}\n----------------\n"); } #ifdef WITH_QT if(figures.size()) { for(std::map::iterator iter=figures.begin(); iter!=figures.end(); ++iter) { if(!useIds) { iter->second->frameData(); } if(exportFigures) { QString data = iter->second->getAllCurveDataAsText(); if(!data.isEmpty()) { QString filePath = QString(exportPrefix.c_str()) + (exportPrefix.empty()?"":"-") + iter->second->windowTitle().replace('/', "-") + ".txt"; QFile file(filePath); if(file.open(QIODevice::Text | QIODevice::WriteOnly)) { file.write(data.toUtf8()); file.close(); printf("Exported \"%s\".\n", filePath.toStdString().c_str()); } else { printf("ERROR: could not open file \"%s\" for writing!\n", filePath.toStdString().c_str()); } } } else { iter->second->show(); } } if(!exportFigures) { return app.exec(); } } #endif return 0; }