Skip to content

Commit

Permalink
Try to fix urdfs in memory before SDF import (o3de#481)
Browse files Browse the repository at this point in the history
* Fix in memory urdf with errors 2 and 19
* Notify user and added settings
* Apply suggestions from code review

Co-authored-by: Adam Dąbrowski <[email protected]>
Co-authored-by: lumberyard-employee-dm <[email protected]>
Co-authored-by: Mike Balfour <[email protected]>
Signed-off-by: Michał Pełka <[email protected]>
  • Loading branch information
4 people committed Sep 25, 2023
1 parent 577e411 commit ecca4a2
Show file tree
Hide file tree
Showing 12 changed files with 265 additions and 35 deletions.
129 changes: 129 additions & 0 deletions Gems/ROS2/Code/Source/RobotImporter/FixURDF/FixURDF.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/

#include "FixURDF.h"
#include <AzCore/XML/rapidxml.h>
#include <AzCore/XML/rapidxml_print.h>
#include <AzCore/std/containers/unordered_map.h>
#include <iostream>

namespace ROS2::Utils
{
//! Modifies a parsed URDF in memory to add missing inertia to links, which prevents SDF error 19.
//! @param urdf URDF to modify.
//! @returns a list of names of links that were modified.
AZStd::vector<AZStd::string> AddMissingInertiaToLinks(AZ::rapidxml::xml_node<>* urdf)
{
AZStd::vector<AZStd::string> modifiedLinks;
using namespace AZ::rapidxml;
AZStd::vector<xml_node<>*> links;
for (xml_node<>* link = urdf->first_node("link"); link; link = link->next_sibling("link"))
{
if (!link->first_node("inertial"))
{
links.push_back(link);
}
}

for (auto* link : links)
{
xml_node<>* inertial = urdf->document()->allocate_node(node_element, "inertial");

xml_node<>* mass = urdf->document()->allocate_node(node_element, "mass");
mass->append_attribute(urdf->document()->allocate_attribute("value", "1."));
inertial->append_node(mass);
xml_node<>* inertia = urdf->document()->allocate_node(node_element, "inertia");

inertia->append_attribute(urdf->document()->allocate_attribute("ixx", "1."));
inertia->append_attribute(urdf->document()->allocate_attribute("ixy", "0."));
inertia->append_attribute(urdf->document()->allocate_attribute("ixz", "0."));

inertia->append_attribute(urdf->document()->allocate_attribute("iyx", "0."));
inertia->append_attribute(urdf->document()->allocate_attribute("iyy", "1."));
inertia->append_attribute(urdf->document()->allocate_attribute("iyz", "0."));

inertia->append_attribute(urdf->document()->allocate_attribute("izx", "0."));
inertia->append_attribute(urdf->document()->allocate_attribute("izy", "0."));
inertia->append_attribute(urdf->document()->allocate_attribute("izz", "1."));

inertial->append_node(inertia);

xml_node<>* origin = urdf->document()->allocate_node(node_element, "origin");
origin->append_attribute(urdf->document()->allocate_attribute("xyz", "0. 0. 0."));
inertial->append_node(origin);

auto* name = link->first_attribute("name");
if (name)
{
modifiedLinks.push_back(name->value());
}
link->append_node(inertial);
}

return modifiedLinks;
}

//! Handles a case of multiple joints and the link sharing a common names which causes SDF error2 (but is fine in URDF)
//! Function will add a suffix "_dup" to the name of the joint if it is also the name of a link.
//! If there are name collisions in links, this will not be able to fix it, the SDF parser will throw an error.
//! @param urdf URDF to modify.
//! @returns a list of links that were modified
AZStd::vector<AZStd::string> RenameDuplicatedJoints(AZ::rapidxml::xml_node<>* urdf)
{
using namespace AZ::rapidxml;
AZStd::vector<AZStd::string> modifiedLinks;
AZStd::unordered_map<AZStd::string, unsigned int> linkAndJointsName;
for (xml_node<>* link = urdf->first_node("link"); link; link = link->next_sibling("link"))
{
auto* name = link->first_attribute("name");
if (name)
{
linkAndJointsName.insert(AZStd::make_pair(name->value(), 0));
}
}
for (xml_node<>* joint = urdf->first_node("joint"); joint; joint = joint->next_sibling("joint"))
{
auto* name = joint->first_attribute("name");
if (name)
{
if (linkAndJointsName.contains(name->value()))
{
unsigned int& count = linkAndJointsName[name->value()];
auto newName = AZStd::string::format("%s_dup%u", name->value(), count);
name->value(urdf->document()->allocate_string(newName.c_str()));
count++;
modifiedLinks.push_back(AZStd::move(newName));
}
else
{
linkAndJointsName.insert(AZStd::make_pair(name->value(), 0));
}
}
}
return modifiedLinks;
}

AZStd::pair<std::string, AZStd::vector<AZStd::string>> ModifyURDFInMemory(const std::string& data)
{
AZStd::vector<AZStd::string> modifiedElements;
using namespace AZ::rapidxml;
xml_document<> doc;
doc.parse<0>(const_cast<char*>(data.c_str()));
xml_node<>* urdf = doc.first_node("robot");
auto links = AddMissingInertiaToLinks(urdf);
modifiedElements.insert(modifiedElements.end(), AZStd::make_move_iterator(links.begin()), AZStd::make_move_iterator(links.end()));

auto renames = RenameDuplicatedJoints(urdf);
modifiedElements.insert(
modifiedElements.end(), AZStd::make_move_iterator(renames.begin()), AZStd::make_move_iterator(renames.end()));

std::string xmlDocString;
AZ::rapidxml::print(std::back_inserter(xmlDocString), *urdf, 0);
return { xmlDocString, modifiedElements };
}
} // namespace ROS2::Utils
23 changes: 23 additions & 0 deletions Gems/ROS2/Code/Source/RobotImporter/FixURDF/FixURDF.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/

#pragma once

#include <AzCore/std/containers/vector.h>
#include <AzCore/std/string/string.h>

namespace ROS2::Utils
{
//! Modifies in memory URDF to increase chance of successful conversion to SDF.
//! It does the following:
//! - Adds missing inertia to links of mass 1 kg and identity inertia matrix.
//! - Renames joints that have the same name as a link.
//! @param urdf URDF to modify.
//! @returns a modified URDF and a list of XML element that were modified
AZStd::pair<std::string, AZStd::vector<AZStd::string>> ModifyURDFInMemory(const std::string& data);
} // namespace ROS2::Utils
9 changes: 8 additions & 1 deletion Gems/ROS2/Code/Source/RobotImporter/Pages/CheckUrdfPage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ namespace ROS2
CheckUrdfPage::CheckUrdfPage(QWizard* parent)
: QWizardPage(parent)
, m_success(false)
, m_warning(false)
{
m_log = new QTextEdit(this);
setTitle(tr("URDF opening results:"));
Expand All @@ -24,15 +25,21 @@ namespace ROS2
setLayout(layout);
}

void CheckUrdfPage::ReportURDFResult(const QString& status, bool isSuccess)
void CheckUrdfPage::ReportURDFResult(const QString& status, bool isSuccess, bool isWarning)
{
m_log->setMarkdown(status);
m_success = isSuccess;
m_warning = isWarning;
emit completeChanged();
}

bool CheckUrdfPage::isComplete() const
{
return m_success;
}

bool CheckUrdfPage::isWarning() const
{
return m_warning;
}
} // namespace ROS2
4 changes: 3 additions & 1 deletion Gems/ROS2/Code/Source/RobotImporter/Pages/CheckUrdfPage.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ namespace ROS2
Q_OBJECT
public:
explicit CheckUrdfPage(QWizard* parent);
void ReportURDFResult(const QString& result, bool isSuccess);
void ReportURDFResult(const QString& result, bool isSuccess, bool isWarning = false);
bool isComplete() const override;
bool isWarning() const;

private:
QTextEdit* m_log;
QString m_fileName;
bool m_success;
bool m_warning;
};
} // namespace ROS2
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ namespace ROS2
sdf::ParserConfig parserConfig;
parserConfig.URDFSetPreserveFixedJoint(sdfBuilderSettings.m_urdfPreserveFixedJoints);

auto parsedUrdfOutcome = UrdfParser::ParseFromFile(filePath, parserConfig);
auto parsedUrdfOutcome = UrdfParser::ParseFromFile(filePath, parserConfig, sdfBuilderSettings);
if (!parsedUrdfOutcome)
{
const AZStd::string log = Utils::JoinSdfErrorsToString(parsedUrdfOutcome.GetSdfErrors());
Expand Down
43 changes: 31 additions & 12 deletions Gems/ROS2/Code/Source/RobotImporter/RobotImporterWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@
#include <AzCore/Utils/Utils.h>

#include "RobotImporterWidget.h"
#include <QApplication>
#include <QScreen>
#include <QTranslator>
#include <SdfAssetBuilder/SdfAssetBuilderSettings.h>
#include <URDF/URDFPrefabMaker.h>
#include <URDF/UrdfParser.h>
#include <Utils/ErrorUtils.h>
#include <Utils/FilePath.h>
#include <Utils/RobotImporterUtils.h>
#include <Utils/ErrorUtils.h>
#include <QApplication>
#include <QScreen>
#include <QTranslator>

namespace ROS2
{
Expand Down Expand Up @@ -119,8 +119,7 @@ namespace ROS2
report += "```\n";
if (outcome.m_logErrorOutput.size())
{
report +=
QString::fromUtf8(outcome.m_logErrorOutput.data(), static_cast<int>(outcome.m_logErrorOutput.size()));
report += QString::fromUtf8(outcome.m_logErrorOutput.data(), static_cast<int>(outcome.m_logErrorOutput.size()));
}
else
{
Expand All @@ -131,8 +130,8 @@ namespace ROS2
report += "```\n";
if (outcome.m_logStandardOutput.size())
{
report += QString::fromUtf8(
outcome.m_logStandardOutput.data(), static_cast<int>(outcome.m_logStandardOutput.size()));
report +=
QString::fromUtf8(outcome.m_logStandardOutput.data(), static_cast<int>(outcome.m_logStandardOutput.size()));
}
else
{
Expand All @@ -147,21 +146,36 @@ namespace ROS2
else if (Utils::IsFileUrdf(m_urdfPath))
{
// standard URDF
parsedUrdfOutcome = UrdfParser::ParseFromFile(m_urdfPath, parserConfig);
parsedUrdfOutcome = UrdfParser::ParseFromFile(m_urdfPath, parserConfig, sdfBuilderSettings);
}
else
{
AZ_Assert(false, "Unknown file extension : %s \n", m_urdfPath.c_str());
}
AZStd::string log;
bool urdfParsedSuccess{ parsedUrdfOutcome };
const bool urdfParsedSuccess{ parsedUrdfOutcome };
const bool urdfParsedWithWarnings{ parsedUrdfOutcome.UrdfParsedWithModifiedContent() };
if (urdfParsedSuccess)
{
if (urdfParsedWithWarnings)
{
report += "# " + tr("The URDF was parsed, though results were modified to be compatible with SDFormat") + "\n";
report += tr("Modified tags in URDF:") + "\n";
for (const auto& modifiedTag : parsedUrdfOutcome.m_modifiedURDFTags)
{
report += " - " + QString::fromUtf8(modifiedTag.data(), static_cast<int>(modifiedTag.size())) + "\n";
}
report += "\n# "+tr("The modified URDF code:") + "\n";
report += "```\n" + QString::fromStdString(parsedUrdfOutcome.m_modifiedURDFContent) + "```\n";
}
else
{
report += "# " + tr("The URDF was parsed and opened successfully") + "\n";
AZ_Printf("Wizard", "Wizard skips m_checkUrdfPage since there is no errors in URDF\n");
}
m_parsedUrdf = AZStd::move(parsedUrdfOutcome.GetRoot());
report += "# " + tr("The URDF was parsed and opened successfully") + "\n";
m_prefabMaker.reset();
// Report the status of skipping this page
AZ_Printf("Wizard", "Wizard skips m_checkUrdfPage since there is no errors in URDF\n");
m_meshNames = Utils::GetMeshesFilenames(&m_parsedUrdf, true, true);
m_assetPage->ClearAssetsList();
}
Expand All @@ -177,6 +191,7 @@ namespace ROS2
report += QString::fromUtf8(log.data(), int(log.size()));
report += "`";
}
m_checkUrdfPage->ReportURDFResult(report, urdfParsedSuccess, urdfParsedWithWarnings);
if (parsedUrdfOutcome.m_parseMessages.size() > 0)
{
report += "\n\n";
Expand Down Expand Up @@ -358,6 +373,10 @@ namespace ROS2
{
if ((currentPage() == m_fileSelectPage && m_params.empty()) || currentPage() == m_xacroParamsPage)
{
if (m_parsedUrdf.Model() != nullptr && !m_checkUrdfPage->isWarning())
{
return m_xacroParamsPage->nextId();
}
if (m_parsedUrdf.Model() != nullptr && m_checkUrdfPage->isComplete())
{
if (m_meshNames.size() == 0)
Expand Down
36 changes: 28 additions & 8 deletions Gems/ROS2/Code/Source/RobotImporter/URDF/UrdfParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
#include <AzCore/Debug/Trace.h>
#include <AzCore/std/string/regex.h>
#include <AzCore/std/string/string.h>
#include <RobotImporter/FixURDF/FixURDF.h>
#include <RobotImporter/Utils/ErrorUtils.h>
#include <RobotImporter/Utils/FilePath.h>

namespace ROS2::UrdfParser
{
Expand All @@ -26,8 +28,7 @@ namespace ROS2::UrdfParser
// @param consoleStream Reference to sdf::Console::ConsoleStream whose
// output will be redirected
// @param redirectStream reference to stream where console stream output is redirected to
RedirectSDFOutputStream(sdf::Console::ConsoleStream& consoleStream,
std::ostream& redirectStream)
RedirectSDFOutputStream(sdf::Console::ConsoleStream& consoleStream, std::ostream& redirectStream)
: m_consoleStreamRef(consoleStream)
, m_origConsoleStream(consoleStream)
{
Expand All @@ -51,7 +52,7 @@ namespace ROS2::UrdfParser
{
return m_root;
}
const sdf::Root& ParseResult::GetRoot() const &
const sdf::Root& ParseResult::GetRoot() const&
{
return m_root;
}
Expand Down Expand Up @@ -85,6 +86,11 @@ namespace ROS2::UrdfParser
return m_sdfErrors.empty();
}

bool ParseResult::UrdfParsedWithModifiedContent() const
{
return m_modifiedURDFTags.size() > 0;
}

RootObjectOutcome Parse(AZStd::string_view xmlString, const sdf::ParserConfig& parserConfig)
{
return Parse(std::string(xmlString.data(), xmlString.size()), parserConfig);
Expand All @@ -110,7 +116,8 @@ namespace ROS2::UrdfParser
return parseResult;
}

RootObjectOutcome ParseFromFile(AZ::IO::PathView filePath, const sdf::ParserConfig& parserConfig)
RootObjectOutcome ParseFromFile(
AZ::IO::PathView filePath, const sdf::ParserConfig& parserConfig, const SdfAssetBuilderSettings& settings)
{
// Store path in a AZ::IO::FixedMaxPath which is stack based structure that provides memory
// for the path string and is null terminated.
Expand All @@ -121,13 +128,26 @@ namespace ROS2::UrdfParser
{
auto fileNotFoundMessage = AZStd::fixed_string<1024>::format("File %.*s does not exist", AZ_PATH_ARG(urdfFilePath));
ParseResult fileNotFoundResult;
fileNotFoundResult.m_sdfErrors.emplace_back(sdf::ErrorCode::FILE_READ,
std::string{ fileNotFoundMessage.c_str(), fileNotFoundMessage.size() },
std::string{ urdfFilePath.c_str(), urdfFilePath.Native().size() });
fileNotFoundResult.m_sdfErrors.emplace_back(
sdf::ErrorCode::FILE_READ,
std::string{ fileNotFoundMessage.c_str(), fileNotFoundMessage.size() },
std::string{ urdfFilePath.c_str(), urdfFilePath.Native().size() });
return fileNotFoundResult;
}

std::string xmlStr((std::istreambuf_iterator<char>(istream)), std::istreambuf_iterator<char>());
if (Utils::IsFileUrdf(filePath) && settings.m_fixURDF)
{
// modify in memory
auto [modifiedXmlStr, modifiedElements] = (ROS2::Utils::ModifyURDFInMemory(xmlStr));

auto result = Parse(modifiedXmlStr, parserConfig);
result.m_modifiedURDFTags = AZStd::move(modifiedElements);
result.m_modifiedURDFContent = AZStd::move(modifiedXmlStr);
return result;
}
return Parse(xmlStr, parserConfig);
}
} // namespace ROS2


} // namespace ROS2::UrdfParser
Loading

0 comments on commit ecca4a2

Please sign in to comment.