From 471e28bc82f56139e94f0fbbb03d0ef5237bb64d Mon Sep 17 00:00:00 2001 From: Nathan Hughes Date: Mon, 18 Nov 2024 11:35:41 -0500 Subject: [PATCH] Feature/metadata (#21) * add graph metadata object * skip networkx tests if not available * add python metadata interface * add node and edge metadata * use nodesymbol as primary arg type and add implicit conversion * add node and edge metadata bindings --- .flake8 | 2 + CMakeLists.txt | 2 +- include/spark_dsg/dynamic_scene_graph.h | 32 ++--- include/spark_dsg/edge_attributes.h | 3 + include/spark_dsg/node_attributes.h | 3 + .../serialization/attribute_serialization.h | 20 +++ package.xml | 2 +- .../spark_dsg/python/python_layer_view.h | 11 +- python/bindings/src/python_layer_view.cpp | 10 +- python/bindings/src/spark_dsg_bindings.cpp | 117 +++++++++++++----- python/src/spark_dsg/__init__.py | 68 ++++++++-- python/tests/test_bindings.py | 42 ++++++- python/tests/test_networkx.py | 16 ++- src/dynamic_scene_graph.cpp | 3 +- .../graph_binary_serialization.cpp | 16 +++ .../graph_json_serialization.cpp | 5 + 16 files changed, 278 insertions(+), 74 deletions(-) diff --git a/.flake8 b/.flake8 index e4e5eb3..ec6ab8d 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,5 @@ [flake8] max-line-length = 88 extend-ignore = E203, W503 +per-file-ignores = + *__init__.py:F401,F403 diff --git a/CMakeLists.txt b/CMakeLists.txt index ee2e86d..fb23a03 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.14) -project(spark_dsg VERSION 1.0.5) +project(spark_dsg VERSION 1.0.6) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/include/spark_dsg/dynamic_scene_graph.h b/include/spark_dsg/dynamic_scene_graph.h index 4b5bca5..780b68b 100644 --- a/include/spark_dsg/dynamic_scene_graph.h +++ b/include/spark_dsg/dynamic_scene_graph.h @@ -34,6 +34,7 @@ * -------------------------------------------------------------------------- */ #pragma once #include +#include #include #include "spark_dsg/dynamic_scene_graph_layer.h" @@ -410,20 +411,20 @@ class DynamicSceneGraph { /** * @brief Get number of static edges in the graph - * @return number of static edges in the graph + * @return Number of static edges in the graph */ size_t numStaticEdges() const; /** * @brief Get number of dynamic edges in the graph - * @return number of dynamic edges in the graph + * @return Number of dynamic edges in the graph */ size_t numDynamicEdges() const; /** * @brief Get whether or not the scene graph is empty - * @note the scene graph invariants make it so only nodes have to be checked - * @return true if the scene graph is empty + * @note The scene graph invariants make it so only nodes have to be checked + * @return True if the scene graph is empty */ bool empty() const; @@ -435,10 +436,10 @@ class DynamicSceneGraph { Eigen::Vector3d getPosition(NodeId node) const; /** - * @brief merge two nodes - * @param node_from node to remove - * @param node_to node to merge to - * @returns true if operation succeeded + * @brief Merge two nodes + * @param node_from Node to remove + * @param node_to Node to merge to + * @returns True if operation succeeded */ bool mergeNodes(NodeId node_from, NodeId node_to); @@ -457,7 +458,7 @@ class DynamicSceneGraph { * @note Will add the nodes and edges not previously added in current graph * @param other other graph to update from * @param config Configuration controlling merge behavior - * @returns true if merge was successful + * @returns True if merge was successful */ bool mergeGraph(const DynamicSceneGraph& other, const GraphMergeConfig& config = {}); @@ -471,7 +472,7 @@ class DynamicSceneGraph { /** * @brief Get all new nodes in the graph * @param clear_new Clear new status for all new nodes up until this point - * @returns list of all new nodes + * @returns List of all new nodes */ std::vector getNewNodes(bool clear_new = false); @@ -490,12 +491,12 @@ class DynamicSceneGraph { std::vector getNewEdges(bool clear_new = false); /** - * @brief track which edges get used during a serialization update + * @brief Track which edges get used during a serialization update */ void markEdgesAsStale(); /** - * @brief remove edges that do not appear in serialization update + * @brief Remove edges that do not appear in serialization update */ void removeAllStaleEdges(); @@ -514,7 +515,7 @@ class DynamicSceneGraph { void save(std::string filepath, bool include_mesh = true) const; /** - * @brief parse graph from binary or JSON file + * @brief Parse graph from binary or JSON file * @param filepath Complete path to file to read, including extension. * @returns Resulting parsed scene graph */ @@ -526,9 +527,12 @@ class DynamicSceneGraph { std::shared_ptr mesh() const; - //! current static layer ids in the graph + //! Current static layer ids in the graph const LayerIds layer_ids; + //! Any extra information about the graph + nlohmann::json metadata; + protected: BaseLayer& layerFromKey(const LayerKey& key); diff --git a/include/spark_dsg/edge_attributes.h b/include/spark_dsg/edge_attributes.h index 2aa2f8d..358e10d 100644 --- a/include/spark_dsg/edge_attributes.h +++ b/include/spark_dsg/edge_attributes.h @@ -34,6 +34,7 @@ * -------------------------------------------------------------------------- */ #pragma once #include +#include #include #include "spark_dsg/serialization/attribute_registry.h" @@ -76,6 +77,8 @@ struct EdgeAttributes { bool weighted; //! the weight of the edge double weight; + //! Arbitrary metadata about the edge + nlohmann::json metadata; /** * @brief output attribute information diff --git a/include/spark_dsg/node_attributes.h b/include/spark_dsg/node_attributes.h index 051b5c9..899c506 100644 --- a/include/spark_dsg/node_attributes.h +++ b/include/spark_dsg/node_attributes.h @@ -38,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -113,6 +114,8 @@ struct NodeAttributes { bool is_active; //! whether the node was observed by Hydra, or added as a prediction bool is_predicted; + //! Arbitrary node metadata + nlohmann::json metadata; /** * @brief output attribute information diff --git a/include/spark_dsg/serialization/attribute_serialization.h b/include/spark_dsg/serialization/attribute_serialization.h index 2ca14fd..cb5e874 100644 --- a/include/spark_dsg/serialization/attribute_serialization.h +++ b/include/spark_dsg/serialization/attribute_serialization.h @@ -36,9 +36,11 @@ #include #include +#include #include "spark_dsg/serialization/attribute_registry.h" #include "spark_dsg/serialization/binary_serialization.h" +#include "spark_dsg/serialization/versioning.h" namespace spark_dsg::serialization { @@ -170,6 +172,7 @@ void Visitor::to(nlohmann::json& record, const Attrs& attrs) { visitor.type_ = Type::JSON_WRITE; visitor.impl_ = std::make_unique(&record); record["type"] = attrs.registration().name; + record["metadata"] = attrs.metadata; attrs.serialization_info(); visitor.impl_.reset(); } @@ -180,6 +183,12 @@ void Visitor::to(BinarySerializer& serializer, const Attrs& attrs) { visitor.type_ = Type::BINARY_WRITE; visitor.impl_ = std::make_unique(&serializer); serializer.write(attrs.registration().type_id); + + // serialize metadata + std::stringstream ss; + ss << attrs.metadata; + serializer.write(ss.str()); + attrs.serialization_info(); visitor.impl_.reset(); } @@ -196,6 +205,10 @@ std::unique_ptr Visitor::from(const AttributeFactory& factory, return nullptr; } + if (record.contains("metadata")) { + attrs->metadata = record["metadata"]; + } + attrs->serialization_info(); visitor.impl_.reset(); return attrs; @@ -215,6 +228,13 @@ std::unique_ptr Visitor::from(const AttributeFactory& factory, return nullptr; } + const auto& header = io::GlobalInfo::loadedHeader(); + if (header.version >= io::Version(1, 0, 6)) { + std::string metadata_json; + deserializer.read(metadata_json); + attrs->metadata = nlohmann::json::parse(metadata_json); + } + attrs->serialization_info(); visitor.impl_.reset(); return attrs; diff --git a/package.xml b/package.xml index bedb74f..cd5ec38 100644 --- a/package.xml +++ b/package.xml @@ -1,7 +1,7 @@ spark_dsg - 1.0.5 + 1.0.6 Dynamic Scene Graph (DSG) type definitions and core library code Nathan Hughes diff --git a/python/bindings/include/spark_dsg/python/python_layer_view.h b/python/bindings/include/spark_dsg/python/python_layer_view.h index 513bd7d..dba1aa5 100644 --- a/python/bindings/include/spark_dsg/python/python_layer_view.h +++ b/python/bindings/include/spark_dsg/python/python_layer_view.h @@ -34,6 +34,7 @@ * -------------------------------------------------------------------------- */ #pragma once #include +#include #include "spark_dsg/python/scene_graph_iterators.h" @@ -46,11 +47,11 @@ class LayerView { EdgeIter edges() const; size_t numNodes() const; size_t numEdges() const; - bool hasNode(NodeId node_id) const; - bool hasEdge(NodeId source, NodeId target) const; - const SceneGraphNode& getNode(NodeId node_id) const; - const SceneGraphEdge& getEdge(NodeId source, NodeId target) const; - Eigen::Vector3d getPosition(NodeId node_id) const; + bool hasNode(NodeSymbol node_id) const; + bool hasEdge(NodeSymbol source, NodeSymbol target) const; + const SceneGraphNode& getNode(NodeSymbol node_id) const; + const SceneGraphEdge& getEdge(NodeSymbol source, NodeSymbol target) const; + Eigen::Vector3d getPosition(NodeSymbol node_id) const; const LayerId id; diff --git a/python/bindings/src/python_layer_view.cpp b/python/bindings/src/python_layer_view.cpp index 7e1fc4b..4c178e3 100644 --- a/python/bindings/src/python_layer_view.cpp +++ b/python/bindings/src/python_layer_view.cpp @@ -48,21 +48,21 @@ size_t LayerView::numNodes() const { return layer_ref_.numNodes(); } size_t LayerView::numEdges() const { return layer_ref_.numEdges(); } -bool LayerView::hasNode(NodeId node_id) const { return layer_ref_.hasNode(node_id); } +bool LayerView::hasNode(NodeSymbol node_id) const { return layer_ref_.hasNode(node_id); } -bool LayerView::hasEdge(NodeId source, NodeId target) const { +bool LayerView::hasEdge(NodeSymbol source, NodeSymbol target) const { return layer_ref_.hasEdge(source, target); } -const SceneGraphNode& LayerView::getNode(NodeId node_id) const { +const SceneGraphNode& LayerView::getNode(NodeSymbol node_id) const { return layer_ref_.getNode(node_id); } -const SceneGraphEdge& LayerView::getEdge(NodeId source, NodeId target) const { +const SceneGraphEdge& LayerView::getEdge(NodeSymbol source, NodeSymbol target) const { return layer_ref_.getEdge(source, target); } -Eigen::Vector3d LayerView::getPosition(NodeId node_id) const { +Eigen::Vector3d LayerView::getPosition(NodeSymbol node_id) const { return layer_ref_.getNode(node_id).attributes().position; } diff --git a/python/bindings/src/spark_dsg_bindings.cpp b/python/bindings/src/spark_dsg_bindings.cpp index 59e7e0a..dc505d1 100644 --- a/python/bindings/src/spark_dsg_bindings.cpp +++ b/python/bindings/src/spark_dsg_bindings.cpp @@ -154,6 +154,8 @@ PYBIND11_MODULE(_dsg_bindings, module) { .def(pybind11::self == pybind11::self) .def(pybind11::self != pybind11::self); + py::implicitly_convertible(); + py::class_(module, "NearestVertexInfo") .def(py::init<>()) .def_property( @@ -267,6 +269,16 @@ PYBIND11_MODULE(_dsg_bindings, module) { .def_readwrite("last_update_time_ns", &NodeAttributes::last_update_time_ns) .def_readwrite("is_active", &NodeAttributes::is_active) .def_readwrite("is_predicted", &NodeAttributes::is_predicted) + .def("_get_metadata", + [](const NodeAttributes& node) { + std::stringstream ss; + ss << node.metadata; + return ss.str(); + }) + .def("_set_metadata", + [](NodeAttributes& node, const std::string& data) { + node.metadata = nlohmann::json::parse(data); + }) .def("__repr__", [](const NodeAttributes& attrs) { std::stringstream ss; ss << attrs; @@ -393,7 +405,16 @@ PYBIND11_MODULE(_dsg_bindings, module) { py::class_(module, "EdgeAttributes") .def(py::init<>()) .def_readwrite("weighted", &EdgeAttributes::weighted) - .def_readwrite("weight", &EdgeAttributes::weight); + .def_readwrite("weight", &EdgeAttributes::weight) + .def("_get_metadata", + [](const EdgeAttributes& edge) { + std::stringstream ss; + ss << edge.metadata; + return ss.str(); + }) + .def("_set_metadata", [](EdgeAttributes& edge, const std::string& data) { + edge.metadata = nlohmann::json::parse(data); + }); /************************************************************************************** * Scene graph node @@ -531,19 +552,18 @@ PYBIND11_MODULE(_dsg_bindings, module) { py::class_>( module, "SceneGraphLayer") .def(py::init()) - .def( - "add_node", - [](IsolatedSceneGraphLayer& layer, NodeId node, const NodeAttributes& attrs) { - layer.emplaceNode(node, attrs.clone()); - }) + .def("add_node", + [](IsolatedSceneGraphLayer& layer, + NodeSymbol node, + const NodeAttributes& attrs) { layer.emplaceNode(node, attrs.clone()); }) .def("insert_edge", - [](IsolatedSceneGraphLayer& layer, NodeId source, NodeId target) { + [](IsolatedSceneGraphLayer& layer, NodeSymbol source, NodeSymbol target) { return layer.insertEdge(source, target); }) .def("insert_edge", [](IsolatedSceneGraphLayer& layer, - NodeId source, - NodeId target, + NodeSymbol source, + NodeSymbol target, const EdgeAttributes& info) { return layer.insertEdge(source, target, info.clone()); }) @@ -565,7 +585,7 @@ PYBIND11_MODULE(_dsg_bindings, module) { .def("num_nodes", &IsolatedSceneGraphLayer::numNodes) .def("num_edges", &IsolatedSceneGraphLayer::numEdges) .def("get_position", - [](const IsolatedSceneGraphLayer& layer, NodeId node) { + [](const IsolatedSceneGraphLayer& layer, NodeSymbol node) { return layer.getNode(node).attributes().position; }) .def_readonly("id", &IsolatedSceneGraphLayer::id) @@ -648,9 +668,9 @@ PYBIND11_MODULE(_dsg_bindings, module) { .def("add_node", [](DynamicSceneGraph& graph, LayerId layer_id, - NodeId node_id, + NodeSymbol node_id, const NodeAttributes& attrs) { - graph.emplaceNode(layer_id, node_id, attrs.clone()); + return graph.emplaceNode(layer_id, node_id, attrs.clone()); }) .def( "add_node", @@ -660,7 +680,7 @@ PYBIND11_MODULE(_dsg_bindings, module) { std::chrono::nanoseconds timestamp, const NodeAttributes& attrs, bool add_edge_to_previous) { - graph.emplaceNode( + return graph.emplaceNode( layer_id, prefix, timestamp, attrs.clone(), add_edge_to_previous); }, "layer_id"_a, @@ -671,8 +691,8 @@ PYBIND11_MODULE(_dsg_bindings, module) { .def( "insert_edge", [](DynamicSceneGraph& graph, - NodeId source, - NodeId target, + NodeSymbol source, + NodeSymbol target, bool enforce_single_parent) { return enforce_single_parent ? graph.insertParentEdge(source, target) : graph.insertEdge(source, target); @@ -683,8 +703,8 @@ PYBIND11_MODULE(_dsg_bindings, module) { .def( "insert_edge", [](DynamicSceneGraph& graph, - NodeId source, - NodeId target, + NodeSymbol source, + NodeSymbol target, const EdgeAttributes& info, bool enforce_single_parent) { return enforce_single_parent @@ -701,9 +721,14 @@ PYBIND11_MODULE(_dsg_bindings, module) { .def("has_layer", static_cast( &DynamicSceneGraph::hasLayer)) - .def("has_node", &DynamicSceneGraph::hasNode) + .def("has_node", + [](const DynamicSceneGraph& graph, NodeSymbol node_id) { + return graph.hasNode(node_id); + }) .def("has_edge", - py::overload_cast(&DynamicSceneGraph::hasEdge, py::const_)) + [](const DynamicSceneGraph& graph, NodeSymbol source, NodeSymbol target) { + return graph.hasEdge(source, target); + }) .def("has_mesh", &DynamicSceneGraph::hasMesh) .def( "get_layer", @@ -723,20 +748,36 @@ PYBIND11_MODULE(_dsg_bindings, module) { return DynamicLayerView(graph.getLayer(layer_id, prefix)); }, py::return_value_policy::reference_internal) - .def("get_node", - &DynamicSceneGraph::getNode, - py::return_value_policy::reference_internal) - .def("find_node", - &DynamicSceneGraph::findNode, - py::return_value_policy::reference_internal) - .def("get_edge", - &DynamicSceneGraph::getEdge, - py::return_value_policy::reference_internal) - .def("find_edge", - &DynamicSceneGraph::findEdge, - py::return_value_policy::reference_internal) - .def("remove_node", &DynamicSceneGraph::removeNode) - .def("remove_edge", &DynamicSceneGraph::removeEdge) + .def( + "get_node", + [](const DynamicSceneGraph& graph, NodeSymbol node) -> const SceneGraphNode& { + return graph.getNode(node); + }, + py::return_value_policy::reference_internal) + .def( + "find_node", + [](const DynamicSceneGraph& graph, NodeSymbol node) -> const SceneGraphNode* { + return graph.findNode(node); + }, + py::return_value_policy::reference_internal) + .def( + "get_edge", + [](const DynamicSceneGraph& graph, NodeSymbol source, NodeSymbol target) + -> const SceneGraphEdge& { return graph.getEdge(source, target); }, + py::return_value_policy::reference_internal) + .def( + "find_edge", + [](const DynamicSceneGraph& graph, NodeSymbol source, NodeSymbol target) + -> const SceneGraphEdge* { return graph.findEdge(source, target); }, + py::return_value_policy::reference_internal) + .def("remove_node", + [](DynamicSceneGraph& graph, NodeSymbol node) -> bool { + return graph.removeNode(node); + }) + .def("remove_edge", + [](DynamicSceneGraph& graph, NodeSymbol source, NodeSymbol target) -> bool { + return graph.removeEdge(source, target); + }) .def("is_dynamic", &DynamicSceneGraph::isDynamic) .def("num_layers", &DynamicSceneGraph::numLayers) .def("num_dynamic_layers_of_type", &DynamicSceneGraph::numDynamicLayersOfType) @@ -768,6 +809,16 @@ PYBIND11_MODULE(_dsg_bindings, module) { [](const std::filesystem::path& filepath) { return DynamicSceneGraph::load(filepath); }) + .def("_get_metadata", + [](const DynamicSceneGraph& graph) { + std::stringstream ss; + ss << graph.metadata; + return ss.str(); + }) + .def("_set_metadata", + [](DynamicSceneGraph& graph, const std::string& data) { + graph.metadata = nlohmann::json::parse(data); + }) .def_property( "layers", [](const DynamicSceneGraph& graph) { diff --git a/python/src/spark_dsg/__init__.py b/python/src/spark_dsg/__init__.py index 478a806..24b2f1e 100644 --- a/python/src/spark_dsg/__init__.py +++ b/python/src/spark_dsg/__init__.py @@ -33,18 +33,19 @@ # # """The Spark-DSG package.""" -from spark_dsg._dsg_bindings import * # NOQA -from spark_dsg._dsg_bindings import ( - compute_ancestor_bounding_box, - DsgLayers, - BoundingBoxType, - DynamicSceneGraph, - SceneGraphLayer, - LayerView, -) -from spark_dsg.torch_conversion import scene_graph_to_torch, scene_graph_layer_to_torch -from spark_dsg.visualization import plot_scene_graph # NOQA -from spark_dsg.open3d_visualization import render_to_open3d # NOQA +import json +import types + +from spark_dsg._dsg_bindings import * +from spark_dsg._dsg_bindings import (BoundingBoxType, DsgLayers, + DynamicSceneGraph, EdgeAttributes, + LayerView, NodeAttributes, + SceneGraphLayer, + compute_ancestor_bounding_box) +from spark_dsg.open3d_visualization import render_to_open3d +from spark_dsg.torch_conversion import (scene_graph_layer_to_torch, + scene_graph_to_torch) +from spark_dsg.visualization import plot_scene_graph def add_bounding_boxes_to_layer( @@ -68,6 +69,49 @@ def add_bounding_boxes_to_layer( node.attributes.bounding_box = bbox +def _get_metadata(obj): + """Get graph metadata.""" + data_str = obj._get_metadata() + metadata = json.loads(data_str) + metadata = dict() if metadata is None else metadata + return types.MappingProxyType(metadata) + + +def _set_metadata(obj, data): + """Serialize and set graph metadata.""" + obj._set_metadata(json.dumps(data)) + + +def _update_nested(contents, other): + for key, value in other.items(): + if key not in contents: + contents[key] = {} + + if isinstance(value, dict): + _update_nested(contents[key], value) + else: + contents[key] = value + + +def _add_metadata(obj, data): + """Serialize and update metadata from passed object.""" + data_str = obj._get_metadata() + metadata = json.loads(data_str) + metadata = dict() if metadata is None else metadata + _update_nested(metadata, data) + obj._set_metadata(json.dumps(metadata)) + + +def _add_metadata_interface(obj): + obj.metadata = property(_get_metadata) + obj.set_metadata = _set_metadata + obj.add_metadata = _add_metadata + + +_add_metadata_interface(DynamicSceneGraph) +_add_metadata_interface(NodeAttributes) +_add_metadata_interface(EdgeAttributes) + DynamicSceneGraph.to_torch = scene_graph_to_torch SceneGraphLayer.to_torch = scene_graph_layer_to_torch LayerView.to_torch = scene_graph_layer_to_torch diff --git a/python/tests/test_bindings.py b/python/tests/test_bindings.py index dc01623..f057233 100644 --- a/python/tests/test_bindings.py +++ b/python/tests/test_bindings.py @@ -33,8 +33,8 @@ # # """Test that the bindings are working appropriately.""" -import spark_dsg as dsg import numpy as np +import spark_dsg as dsg def test_empty_graph(): @@ -302,3 +302,43 @@ def test_node_counts(resource_dir): assert node_type_counts["R"] == G.get_layer(dsg.DsgLayers.ROOMS).num_nodes() assert "B" in node_type_counts assert node_type_counts["B"] == G.get_layer(dsg.DsgLayers.BUILDINGS).num_nodes() + + +def test_graph_metadata(tmp_path): + """Test that graph metadata works as expected.""" + G = dsg.DynamicSceneGraph() + G.add_metadata({"foo": 5}) + G.add_metadata({"bar": [1, 2, 3, 4, 5]}) + G.add_metadata({"something": {"a": 13, "b": 42.0, "c": "world"}}) + + graph_path = tmp_path / "graph.json" + G.save(graph_path) + G_new = dsg.DynamicSceneGraph.load(graph_path) + assert G_new.metadata == { + "foo": 5, + "bar": [1, 2, 3, 4, 5], + "something": {"a": 13, "b": 42.0, "c": "world"}, + } + + G.add_metadata({"something": {"b": 643.0, "other": "foo"}}) + assert G.metadata == { + "foo": 5, + "bar": [1, 2, 3, 4, 5], + "something": {"a": 13, "b": 643.0, "c": "world", "other": "foo"}, + } + + +def test_attribute_metadata(tmp_path): + """Test that attribute metadata works as expected.""" + G = dsg.DynamicSceneGraph() + + attrs = dsg.ObjectNodeAttributes() + attrs.add_metadata({"test": {"a": 5, "c": "hello"}}) + attrs.add_metadata({"test": {"a": 6, "b": 42.0}}) + G.add_node(dsg.DsgLayers.OBJECTS, dsg.NodeSymbol("O", 1), attrs) + + graph_path = tmp_path / "graph.json" + G.save(graph_path) + G_new = dsg.DynamicSceneGraph.load(graph_path) + new_attrs = G_new.get_node(dsg.NodeSymbol("O", 1)).attributes + assert new_attrs.metadata == {"test": {"a": 6, "b": 42.0, "c": "hello"}} diff --git a/python/tests/test_networkx.py b/python/tests/test_networkx.py index e990344..c2ad110 100644 --- a/python/tests/test_networkx.py +++ b/python/tests/test_networkx.py @@ -33,9 +33,20 @@ # # """Test that networkx conversion works as expected.""" +import importlib + +import numpy as np +import pytest import spark_dsg as dsg import spark_dsg.networkx as dsg_nx -import numpy as np + + +def _has_networkx(): + try: + importlib.import_module("networkx") + return True + except ImportError: + return False def _check_attribute_validity(G_nx): @@ -49,6 +60,7 @@ def _check_attribute_validity(G_nx): assert "weighted" in G_nx[edge[0]][edge[1]] +@pytest.mark.skipif(not _has_networkx(), reason="requires networkx") def test_static_graph_conversion(resource_dir): """Test that graph conversion is exact.""" dsg_path = resource_dir / "apartment_dsg.json" @@ -62,6 +74,7 @@ def test_static_graph_conversion(resource_dir): _check_attribute_validity(G_nx) +@pytest.mark.skipif(not _has_networkx(), reason="requires networkx") def test_full_graph_conversion(resource_dir): """Test that graph conversion is exact.""" dsg_path = resource_dir / "apartment_dsg.json" @@ -81,6 +94,7 @@ def test_full_graph_conversion(resource_dir): _check_attribute_validity(G_nx) +@pytest.mark.skipif(not _has_networkx(), reason="requires networkx") def test_layer_conversion(resource_dir): """Test that layer conversion is exact.""" dsg_path = resource_dir / "apartment_dsg.json" diff --git a/src/dynamic_scene_graph.cpp b/src/dynamic_scene_graph.cpp index 4cb04db..852140d 100644 --- a/src/dynamic_scene_graph.cpp +++ b/src/dynamic_scene_graph.cpp @@ -56,7 +56,8 @@ DynamicSceneGraph::LayerIds getDefaultLayerIds() { DynamicSceneGraph::DynamicSceneGraph() : DynamicSceneGraph(getDefaultLayerIds()) {} -DynamicSceneGraph::DynamicSceneGraph(const LayerIds& layer_ids) : layer_ids(layer_ids) { +DynamicSceneGraph::DynamicSceneGraph(const LayerIds& layer_ids) + : layer_ids(layer_ids), metadata(nlohmann::json::object()) { for (const auto layer_id : layer_ids) { if (layer_id == DsgLayers::UNKNOWN) { throw std::domain_error("cannot instantiate layer with unknown layer id!"); diff --git a/src/serialization/graph_binary_serialization.cpp b/src/serialization/graph_binary_serialization.cpp index 70573eb..cf6fe67 100644 --- a/src/serialization/graph_binary_serialization.cpp +++ b/src/serialization/graph_binary_serialization.cpp @@ -36,6 +36,7 @@ #include "spark_dsg/dynamic_scene_graph.h" #include "spark_dsg/edge_attributes.h" +#include "spark_dsg/logging.h" #include "spark_dsg/node_attributes.h" #include "spark_dsg/serialization/attribute_registry.h" #include "spark_dsg/serialization/attribute_serialization.h" @@ -136,6 +137,11 @@ void writeGraph(const DynamicSceneGraph& graph, serializer.write(serialization::AttributeRegistry::names()); serializer.write(serialization::AttributeRegistry::names()); + // dump metadata to serialized json and write + std::stringstream ss; + ss << graph.metadata; + serializer.write(ss.str()); + serializer.startDynamicArray(); for (const auto& id_layer_pair : graph.layers()) { for (const auto& id_node_pair : id_layer_pair.second->nodes()) { @@ -211,6 +217,16 @@ bool updateGraph(DynamicSceneGraph& graph, const BinaryDeserializer& deserialize const auto node_factory = loadFactory(header, deserializer); const auto edge_factory = loadFactory(header, deserializer); + if (header.version >= io::Version(1, 0, 6)) { + std::string metadata_json; + deserializer.read(metadata_json); + try { + graph.metadata = nlohmann::json::parse(metadata_json); + } catch (const std::exception& e) { + SG_LOG(WARNING) << "Invalid json metadata: " << e.what(); + } + } + std::unordered_set stale_nodes; for (const auto& id_key_pair : graph.node_lookup()) { stale_nodes.insert(id_key_pair.first); diff --git a/src/serialization/graph_json_serialization.cpp b/src/serialization/graph_json_serialization.cpp index 477a165..f57adc6 100644 --- a/src/serialization/graph_json_serialization.cpp +++ b/src/serialization/graph_json_serialization.cpp @@ -116,6 +116,7 @@ std::string writeGraph(const DynamicSceneGraph& graph, bool include_mesh) { record["nodes"] = nlohmann::json::array(); record["edges"] = nlohmann::json::array(); record["layer_ids"] = graph.layer_ids; + record["metadata"] = graph.metadata; for (const auto& id_layer_pair : graph.layers()) { for (const auto& id_node_pair : id_layer_pair.second->nodes()) { @@ -178,6 +179,10 @@ DynamicSceneGraph::Ptr readGraph(const std::string& contents) { const auto layer_ids = record.at("layer_ids").get(); auto graph = std::make_shared(layer_ids); + if (record.contains("metadata")) { + graph->metadata = record["metadata"]; + } + for (const auto& node : record.at("nodes")) { read_node_from_json(node_factory, node, *graph); }