From 7ce7a6a84c33dd2c0dbd85effdc380f02cac08d9 Mon Sep 17 00:00:00 2001 From: cadop Date: Mon, 22 May 2023 19:35:56 -0400 Subject: [PATCH] add example for node attributes --- .../source/examples/path/custom_cost.rst | 20 ++ .../source/examples/path_planning.rst | 1 + src/Python/dhart/Examples/NodeAttributes.py | 285 ++++++++++++++++++ src/Python/dhart/Examples/ViewAnalysis3D.py | 15 + 4 files changed, 321 insertions(+) create mode 100644 docs/Python Docs/source/examples/path/custom_cost.rst create mode 100644 src/Python/dhart/Examples/NodeAttributes.py diff --git a/docs/Python Docs/source/examples/path/custom_cost.rst b/docs/Python Docs/source/examples/path/custom_cost.rst new file mode 100644 index 000000000..595cab96a --- /dev/null +++ b/docs/Python Docs/source/examples/path/custom_cost.rst @@ -0,0 +1,20 @@ +=========================== +Path Plan with Custom Costs +=========================== + + +Overview +-------- + +This example uses a simple obj file included in example models. +For context of what the top-down view is based on, see the figure below. + +.. image:: ../imgs/visibility_ex_rendered.png + + +.. currentmodule:: dhart + +.. automodule:: dhart.Examples.NodeAttributes + :members: + + \ No newline at end of file diff --git a/docs/Python Docs/source/examples/path_planning.rst b/docs/Python Docs/source/examples/path_planning.rst index d67015b6a..1d54ef29a 100644 --- a/docs/Python Docs/source/examples/path_planning.rst +++ b/docs/Python Docs/source/examples/path_planning.rst @@ -10,6 +10,7 @@ Path Planning path/visualize.rst path/limit.rst path/multi_cost.rst + path/custom_cost.rst .. seealso:: diff --git a/src/Python/dhart/Examples/NodeAttributes.py b/src/Python/dhart/Examples/NodeAttributes.py new file mode 100644 index 000000000..aae16aa90 --- /dev/null +++ b/src/Python/dhart/Examples/NodeAttributes.py @@ -0,0 +1,285 @@ +""" + +.. plot:: + :context: reset + + + import matplotlib.pyplot as plt + import numpy as np + + import dhart + from dhart.geometry import LoadOBJ + from dhart.graphgenerator import GenerateGraph + from dhart.raytracer import EmbreeBVH + from dhart.pathfinding import DijkstraShortestPath + from dhart.spatialstructures import Direction + from dhart.viewanalysis import SphericalViewAnalysisAggregate, AggregationType + + +.. plot:: + :context: close-figs + + + # Get a sample model path + obj_path = dhart.get_sample_model("VisibilityTestCases.obj") + + # Load the obj file + obj = LoadOBJ(obj_path) + + # Create a BVH + bvh = EmbreeBVH(obj, True) + + # Set the graph parameters + start_point = (1.1 , 1.1, 20) + spacing = (1, 1, 5) + max_nodes = 10000 + up_step, down_step = 0.1, 0.1 + up_slope, down_slope = 1, 1 + max_step_connections = 1 + + # Generate the Graph + graph = GenerateGraph(bvh, start_point, spacing, max_nodes, + up_step,up_slope,down_step,down_slope, + max_step_connections, cores=-1) + # Get Nodes + nodes = graph.getNodes() + print(f"Graph Generated with {len(nodes.array)} nodes") + + +.. plot:: + :context: close-figs + + + # Define a start and end point in x,y + p_desired = np.array([[0,0],[35,35]]) + closest_nodes = graph.get_closest_nodes(p_desired,z=False) + + # Define a start and end node to use for the path (from, to) + start_id, end_id = closest_nodes[0], closest_nodes[1] + + + +.. plot:: + :context: close-figs + + + # Call the shortest path + path = DijkstraShortestPath(graph, start_id, end_id) + + # As the cost array is numpy, simple operations to sum the total cost can be calculated + path_sum = np.sum(path['cost_to_next']) + print('Total path cost: ', path_sum) + + +.. plot:: + :context: close-figs + + + # Get the x,y,z values of the nodes at the given path ids + path_xyz = np.take(nodes[['x','y','z']], path['id']) + + # Extract the xyz locations of the nodes + x_nodes, y_nodes, z_nodes = nodes['x'], nodes['y'], nodes['z'] + + # Extract the xyz locations of the path nodes + x_path, y_path, z_path = path_xyz['x'], path_xyz['y'], path_xyz['z'] + + # Plot the graph + fig = plt.figure(figsize=(6,6)) + plt.scatter(x_nodes, y_nodes, c=z_nodes, alpha=0.5) + plt.plot(x_path,y_path, c="red", marker='.',linewidth=3) + plt.show() + +While energy expenditure is a built-in attribute for path planning, any numerical node attribute +can be used for path planning as well. + +.. plot:: + :context: close-figs + + height = 1.7 # Set a height offset to cast rays from the points + ray_count = 100 # Set the number of rays to use per node + scores = SphericalViewAnalysisAggregate(bvh, nodes, ray_count, height, + upward_fov = 20, downward_fov=20, + agg_type=AggregationType.AVERAGE) + + +.. plot:: + :context: close-figs + + + # add scores to node attributes + # This is our node attribute name, we can use it later to refer to this data + attr_name = "vg_group" + ids = graph.getNodes().array['id'] + edge_cost_name = "vg_group_cost" + + # We need scores to be a string for dhart. + # While this has some overhead, it lets us use node attributes in a flexible way + + str_scores = [str(1.0/(x+0.01)) for x in scores] + # We need an attribute name, the node ids, and an equally sized list of values to assign + graph.add_node_attributes(attr_name, ids, str_scores) + + +This function takes in our node attribute, which we previously called "vg_group" and convert it to +costs of the graph. These costs we will call "vg_group_cost" for this example. +Finally, the third input is a direction. Since these attributes are set on a node and not an edge, +we need to define which node attribute an edge gets. We do this with a directed graph, and set the +node attribute on either the incoming or outgoing edge. + + +.. plot:: + :context: close-figs + + graph.attrs_to_costs(attr_name, edge_cost_name, Direction.INCOMING) + + # We can pick some node ids to define an edge, and query the cost of our custom attribute + sample_cost = graph.GetEdgeCost(1, 2, edge_cost_name) + + +.. testcode:: + + print(f'edge cost {sample_cost}') + + +.. plot:: + :context: close-figs + + # get custom path based on vg + p_desired = np.array([[0,0],[35,35]]) + closest_nodes = graph.get_closest_nodes(p_desired, z=False) + + # Call the shortest path again, with the optional cost type + visibility_path = DijkstraShortestPath(graph, closest_nodes[0], closest_nodes[1], edge_cost_name) + path_xyz = np.take(nodes[['x','y','z']], visibility_path['id']) + + # Extract the xyz locations of the nodes + x_nodes, y_nodes, z_nodes = nodes['x'], nodes['y'], nodes['z'] + # Extract the xyz locations of the path nodes + x_path, y_path, z_path = path_xyz['x'], path_xyz['y'], path_xyz['z'] + + # Plot the graph + fig = plt.figure(figsize=(6,6)) + plt.scatter(x_nodes, y_nodes, c=scores, alpha=0.5) + plt.plot(x_path,y_path, c="red", marker='.',linewidth=3) + plt.show() + +""" + +import matplotlib.pyplot as plt +import numpy as np + +import dhart +from dhart.geometry import LoadOBJ +from dhart.graphgenerator import GenerateGraph +from dhart.raytracer import EmbreeBVH +from dhart.pathfinding import DijkstraShortestPath +from dhart.spatialstructures import Direction +from dhart.viewanalysis import SphericalViewAnalysisAggregate, AggregationType + +# Get a sample model path +obj_path = dhart.get_sample_model("VisibilityTestCases.obj") + +# Load the obj file +obj = LoadOBJ(obj_path) + +# Create a BVH +bvh = EmbreeBVH(obj, True) + +# Set the graph parameters +start_point = (1.1 , 1.1, 20) +spacing = (1, 1, 5) +max_nodes = 10000 +up_step, down_step = 0.1, 0.1 +up_slope, down_slope = 1, 1 +max_step_connections = 1 + +# Generate the Graph +graph = GenerateGraph(bvh, start_point, spacing, max_nodes, + up_step,up_slope,down_step,down_slope, + max_step_connections, cores=-1) +# Get Nodes +nodes = graph.getNodes() +print(f"Graph Generated with {len(nodes.array)} nodes") + +# Define a start and end point in x,y +p_desired = np.array([[0,0],[35,35]]) +closest_nodes = graph.get_closest_nodes(p_desired,z=False) + +# Define a start and end node to use for the path (from, to) +start_id, end_id = closest_nodes[0], closest_nodes[1] + +# Call the shortest path +path = DijkstraShortestPath(graph, start_id, end_id) + +# As the cost array is numpy, simple operations to sum the total cost can be calculated +path_sum = np.sum(path['cost_to_next']) +print('Total path cost: ', path_sum) + +# Get the x,y,z values of the nodes at the given path ids +path_xyz = np.take(nodes[['x','y','z']], path['id']) + +# Extract the xyz locations of the nodes +x_nodes, y_nodes, z_nodes = nodes['x'], nodes['y'], nodes['z'] + +# Extract the xyz locations of the path nodes +x_path, y_path, z_path = path_xyz['x'], path_xyz['y'], path_xyz['z'] + +# Plot the graph using visibility graph as the colors +fig = plt.figure(figsize=(6,6)) +plt.scatter(x_nodes, y_nodes, c=z_nodes, alpha=0.5) +plt.plot(x_path,y_path, c="red", marker='.',linewidth=3) +plt.show() + +# While energy expenditure is a built-in attribute for path planning, any numerical node attribute +# can be used for path planning as well. + +height = 1.7 # Set a height offset to cast rays from the points +ray_count = 100 # Set the number of rays to use per node +scores = SphericalViewAnalysisAggregate(bvh, nodes, ray_count, height, + upward_fov = 20, downward_fov=20, + agg_type=AggregationType.AVERAGE) + +# add scores to node attributes +# This is our node attribute name, we can use it later to refer to this data +attr_name = "vg_group" +ids = graph.getNodes().array['id'] +edge_cost_name = "vg_group_cost" + +# We need scores to be a string for dhart. +# While this has some overhead, it lets us use node attributes in a flexible way + +str_scores = [str(1.0/(x+0.01)) for x in scores] +# We need an attribute name, the node ids, and an equally sized list of values to assign +graph.add_node_attributes(attr_name, ids, str_scores) + +# This function takes in our node attribute, which we previously called "vg_group" and convert it to +# costs of the graph. These costs we will call "vg_group_cost" for this example. +# Finally, the third input is a direction. Since these attributes are set on a node and not an edge, +# we need to define which node attribute an edge gets. We do this with a directed graph, and set the +# node attribute on either the incoming or outgoing edge. +graph.attrs_to_costs(attr_name, edge_cost_name, Direction.INCOMING) + +# We can pick some node ids to define an edge, and query the cost of our custom attribute +sample_cost = graph.GetEdgeCost(1, 2, edge_cost_name) + +print(f'edge cost {sample_cost}') + +# get custom path based on vg +p_desired = np.array([[0,0],[35,35]]) +closest_nodes = graph.get_closest_nodes(p_desired, z=False) + +# Call the shortest path again, with the optional cost type +visibility_path = DijkstraShortestPath(graph, closest_nodes[0], closest_nodes[1], edge_cost_name) +path_xyz = np.take(nodes[['x','y','z']], visibility_path['id']) + +# Extract the xyz locations of the nodes +x_nodes, y_nodes, z_nodes = nodes['x'], nodes['y'], nodes['z'] +# Extract the xyz locations of the path nodes +x_path, y_path, z_path = path_xyz['x'], path_xyz['y'], path_xyz['z'] + + +fig = plt.figure(figsize=(6,6)) +plt.scatter(x_nodes, y_nodes, c=scores, alpha=0.5) +plt.plot(x_path,y_path, c="red", marker='.',linewidth=3) +plt.show() \ No newline at end of file diff --git a/src/Python/dhart/Examples/ViewAnalysis3D.py b/src/Python/dhart/Examples/ViewAnalysis3D.py index 292360aad..7ea1396af 100644 --- a/src/Python/dhart/Examples/ViewAnalysis3D.py +++ b/src/Python/dhart/Examples/ViewAnalysis3D.py @@ -48,7 +48,19 @@ # reshape to get the correct axis hit_pos = hit_dirs_valid * hit_points.reshape(-1,1) + +We can find the percentage of rays that hit the object (bvh). + +.. testcode:: + + print(f'Percent of clear view: {( len(hit_dirs_valid)/ len(hit_dirs) ) * 100}') + + +.. testoutput:: + :options: +NORMALIZE_WHITESPACE + 64.737 + .. plot:: :context: close-figs @@ -151,6 +163,8 @@ # reshape to get the correct axis hit_pos = hit_dirs_valid * hit_points.reshape(-1,1) +print(f'Percent of clear view: {( len(hit_dirs_valid)/ len(hit_dirs) ) * 100}') + # Plot the graph in 3D fig = plt.figure() ax = fig.add_subplot(projection='3d') @@ -184,6 +198,7 @@ # reshape to get the correct axis hit_pos = hit_dirs_valid * hit_points.reshape(-1,1) + # Plot the graph in 3D fig = plt.figure() ax = fig.add_subplot(projection='3d')