From 3f5438364943e9e8e59369efff43fa60e614e678 Mon Sep 17 00:00:00 2001 From: Rainer Kuemmerle Date: Sat, 24 Aug 2024 16:33:58 +0200 Subject: [PATCH 1/4] Add script to validate g2o graph JSON --- python/examples/validate_graph_json.py | 54 ++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100755 python/examples/validate_graph_json.py diff --git a/python/examples/validate_graph_json.py b/python/examples/validate_graph_json.py new file mode 100755 index 000000000..a8eba6d82 --- /dev/null +++ b/python/examples/validate_graph_json.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 + +import argparse +import errno +import json +import sys +from typing import Any, Optional + +from jsonschema import ValidationError, validate + + +def read_json(filename) -> Optional[Any]: + """Reads the content of filename as JSON and returns the dict + + Args: + filename: Filename of the file to read + + Returns: + Optional[Any]: The JSON dict stored in the file, None in case of Exception. + """ + try: + with open(filename) as schema_file: + content = json.load(schema_file) + return content + except Exception: + return None + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("schema", type=str, help="JSON schema") + parser.add_argument("input", type=str, help="input JSON g2o graph") + args = parser.parse_args() + + schema = read_json(args.schema) + if schema is None: + print("Error while reading schema") + sys.exit(errno.EIO) + + json_data = read_json(args.input) + if json_data is None: + print("Error while graph input for validation") + sys.exit(errno.EIO) + + try: + validate(instance=json_data, schema=schema) + print("Success") + except ValidationError as ex: + print("Validation failed") + print(ex) + + +if __name__ == "__main__": + main() From 0b010d7881d04faf1c4fcab434820ba353c9bc81 Mon Sep 17 00:00:00 2001 From: Rainer Kuemmerle Date: Sat, 24 Aug 2024 17:19:17 +0200 Subject: [PATCH 2/4] Guess also input format in g2o CLI --- g2o/apps/g2o_cli/g2o.cpp | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/g2o/apps/g2o_cli/g2o.cpp b/g2o/apps/g2o_cli/g2o.cpp index af7ea7517..b92885879 100644 --- a/g2o/apps/g2o_cli/g2o.cpp +++ b/g2o/apps/g2o_cli/g2o.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include "dl_wrapper.h" #include "g2o/config.h" @@ -65,6 +66,15 @@ void sigquit_handler(int sig) { } } } + +g2o::io::Format guessIOFormat(const std::string_view filename) { + const std::string file_extension = g2o::getFileExtension(filename); + if (file_extension.empty()) { + return g2o::io::Format::kG2O; + } + return g2o::io::formatForFileExtension(file_extension) + .value_or(g2o::io::Format::kG2O); +} } // namespace using g2o::HyperGraph; @@ -274,13 +284,15 @@ int main(int argc, char** argv) { return 2; } } else { - cerr << "Read input from " << inputFilename << '\n'; + const g2o::io::Format input_format = guessIOFormat(inputFilename); + cerr << "Read input from " << inputFilename << " as " + << g2o::io::to_string(input_format) << '\n'; std::ifstream ifs(inputFilename.c_str()); if (!ifs) { cerr << "Failed to open file\n"; return 1; } - if (!optimizer.load(ifs)) { + if (!optimizer.load(ifs, input_format)) { cerr << "Error loading graph\n"; return 2; } @@ -693,12 +705,7 @@ int main(int argc, char** argv) { cerr << "saving to stdout"; optimizer.save(cout); } else { - const std::string file_extension = g2o::getFileExtension(outputfilename); - g2o::io::Format output_format = g2o::io::Format::kG2O; - if (!file_extension.empty()) { - output_format = g2o::io::formatForFileExtension(file_extension) - .value_or(output_format); - } + const g2o::io::Format output_format = guessIOFormat(outputfilename); cerr << "saving " << outputfilename << " in " << g2o::io::to_string(output_format) << " ... "; optimizer.save(outputfilename.c_str(), output_format); From 1c193d468e231176a3d97f25054ca7e35698d21a Mon Sep 17 00:00:00 2001 From: Rainer Kuemmerle Date: Sat, 24 Aug 2024 17:19:53 +0200 Subject: [PATCH 3/4] Store optional container only if not empty Store vertex / edge data and edge param_ids only if not empty. Reading the JSON treads those elements as optional. This reduces the data size a bit and focuses on required information. --- CMakeLists.txt | 2 +- g2o/core/io/io_json.cpp | 2 + g2o/core/io/io_wrapper_json.h | 71 +++++++++++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e08a6e09..e8ae15ef5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -421,7 +421,7 @@ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${g2o_CXX_FLAGS}") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${g2o_C_FLAGS}") find_package(Eigen3 3.3 REQUIRED) -find_package(nlohmann_json 3.2.0) +find_package(nlohmann_json 3.3.0) # Generate config.h set(G2O_OPENGL_FOUND ${OPENGL_FOUND}) diff --git a/g2o/core/io/io_json.cpp b/g2o/core/io/io_json.cpp index 7406e4862..e759798aa 100644 --- a/g2o/core/io/io_json.cpp +++ b/g2o/core/io/io_json.cpp @@ -27,6 +27,7 @@ #include "io_json.h" #include +#include #include #include "g2o/config.h" @@ -56,6 +57,7 @@ std::optional IoJson::load(std::istream& input) { bool IoJson::save(std::ostream& output, const AbstractGraph& graph) { try { + output << std::setw(2); output << json::toJson(graph); } catch (const std::exception& e) { G2O_ERROR("Exception while saving: {}", e.what()); diff --git a/g2o/core/io/io_wrapper_json.h b/g2o/core/io/io_wrapper_json.h index e140b03d4..916265685 100644 --- a/g2o/core/io/io_wrapper_json.h +++ b/g2o/core/io/io_wrapper_json.h @@ -35,14 +35,77 @@ #include "g2o/core/abstract_graph.h" namespace g2o { +namespace internal { +/** + * @brief Get the object from the JSON if key exists + * + * @tparam T type of the target value + * @param j a JSON + * @param key key for retrieving the value + * @param target Where to store the value + */ +template +void get_to_if_exists(const nlohmann::json& j, const char* key, T& target) { + auto it = j.find(key); + if (it == j.end()) return; + it->get_to(target); +} + +/** + * @brief Store a container into the JSON if not empty + * + * @tparam T type of the container + * @param j a JSON + * @param key key for storing the container + * @param value the container to store + */ +template +void store_if_not_empty(nlohmann::json& j, const char* key, const T& value) { + if (value.empty()) return; + j[key] = value; +} +} // namespace internal NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AbstractGraph::AbstractParameter, tag, id, value); NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AbstractGraph::AbstractData, tag, data); -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AbstractGraph::AbstractVertex, tag, id, - estimate, data); -NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(AbstractGraph::AbstractEdge, tag, ids, - param_ids, measurement, information, data); + +// VERTEX +inline void to_json(nlohmann::json& j, + const AbstractGraph::AbstractVertex& vertex) { + j = nlohmann::json{ + {"tag", vertex.tag}, {"id", vertex.id}, {"estimate", vertex.estimate}}; + internal::store_if_not_empty(j, "data", vertex.data); +} + +inline void from_json(const nlohmann::json& j, + AbstractGraph::AbstractVertex& vertex) { + j.at("tag").get_to(vertex.tag); + j.at("id").get_to(vertex.id); + j.at("estimate").get_to(vertex.estimate); + internal::get_to_if_exists(j, "data", vertex.data); +} + +// EDGE +inline void to_json(nlohmann::json& j, + const AbstractGraph::AbstractEdge& edge) { + j = nlohmann::json{{"tag", edge.tag}, + {"ids", edge.ids}, + {"measurement", edge.measurement}, + {"information", edge.information}}; + internal::store_if_not_empty(j, "data", edge.data); + internal::store_if_not_empty(j, "param_ids", edge.param_ids); +} + +inline void from_json(const nlohmann::json& j, + AbstractGraph::AbstractEdge& edge) { + j.at("tag").get_to(edge.tag); + j.at("ids").get_to(edge.ids); + j.at("measurement").get_to(edge.measurement); + j.at("information").get_to(edge.information); + internal::get_to_if_exists(j, "data", edge.data); + internal::get_to_if_exists(j, "param_ids", edge.param_ids); +} namespace json { From 96e18193ef5fdfbb43e3be4b751c5a235c074be4 Mon Sep 17 00:00:00 2001 From: Rainer Kuemmerle Date: Sat, 24 Aug 2024 17:44:18 +0200 Subject: [PATCH 4/4] Tread graph's fixed and parameters as optional data --- g2o/core/io/io_wrapper_json.h | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/g2o/core/io/io_wrapper_json.h b/g2o/core/io/io_wrapper_json.h index 916265685..bfe075b2c 100644 --- a/g2o/core/io/io_wrapper_json.h +++ b/g2o/core/io/io_wrapper_json.h @@ -124,23 +124,20 @@ enum class IoVersions { inline AbstractGraph fromJson(const nlohmann::json& json) { const nlohmann::json& json_graph = json["graph"]; AbstractGraph graph; - graph.fixed() = json_graph["fixed"].get>(); - graph.parameters() = - json_graph["params"].get>(); - graph.vertices() = - json_graph["vertices"].get>(); - graph.edges() = - json_graph["edges"].get>(); + json_graph["vertices"].get_to(graph.vertices()); + json_graph["edges"].get_to(graph.edges()); + internal::get_to_if_exists(json_graph, "fixed", graph.fixed()); + internal::get_to_if_exists(json_graph, "params", graph.parameters()); return graph; } inline nlohmann::json toJson(const AbstractGraph& graph) { nlohmann::json json; nlohmann::json& json_graph = json["graph"]; - json_graph["fixed"] = graph.fixed(); - json_graph["params"] = graph.parameters(); json_graph["vertices"] = graph.vertices(); json_graph["edges"] = graph.edges(); + g2o::internal::store_if_not_empty(json_graph, "fixed", graph.fixed()); + g2o::internal::store_if_not_empty(json_graph, "params", graph.parameters()); return json; }