Commit
·
d7cb5e4
1
Parent(s):
5cd2bb7
Cleaned-up, and added diameter-based cv cost
Browse files- hoho/wed.py +24 -46
hoho/wed.py
CHANGED
|
@@ -3,12 +3,6 @@ from scipy.optimize import linear_sum_assignment
|
|
| 3 |
import numpy as np
|
| 4 |
|
| 5 |
|
| 6 |
-
def zeromean_normalize(vertices):
|
| 7 |
-
vertices = np.array(vertices)
|
| 8 |
-
vertices = vertices - vertices.mean(axis=0)
|
| 9 |
-
vertices = vertices / (1e-6 + np.linalg.norm(vertices, axis=1)[:, None]) # project all verts to sphere (not what we meant)
|
| 10 |
-
return vertices
|
| 11 |
-
|
| 12 |
def preregister_mean_std(verts_to_transform, target_verts, single_scale=True):
|
| 13 |
mu_target = target_verts.mean(axis=0)
|
| 14 |
mu_in = verts_to_transform.mean(axis=0)
|
|
@@ -34,52 +28,38 @@ def preregister_mean_std(verts_to_transform, target_verts, single_scale=True):
|
|
| 34 |
return transformed_verts
|
| 35 |
|
| 36 |
|
| 37 |
-
def compute_WED(pd_vertices, pd_edges, gt_vertices, gt_edges, cv
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
pd_vertices = np.array(pd_vertices)
|
| 39 |
gt_vertices = np.array(gt_vertices)
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
# Step 0: Prenormalize / preregister
|
| 42 |
-
if prenorm:
|
| 43 |
-
pd_vertices = zeromean_normalize(pd_vertices)
|
| 44 |
-
gt_vertices = zeromean_normalize(gt_vertices)
|
| 45 |
-
|
| 46 |
if preregister:
|
| 47 |
pd_vertices = preregister_mean_std(pd_vertices, gt_vertices, single_scale=single_scale)
|
| 48 |
|
| 49 |
|
| 50 |
pd_edges = np.array(pd_edges)
|
| 51 |
-
gt_edges = np.array(gt_edges)
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
# Step 0.5: Register
|
| 55 |
-
if register:
|
| 56 |
-
# find the optimal rotation, translation, and scale
|
| 57 |
-
from scipy.spatial.transform import Rotation as R
|
| 58 |
-
from scipy.optimize import minimize
|
| 59 |
-
|
| 60 |
-
def transform(x, pd_vertices):
|
| 61 |
-
# x is a 7-element vector, first 3 elements are the rotation vector, next 3 elements are the translation vector, finally scale
|
| 62 |
-
rotation = R.from_rotvec(x[:3])
|
| 63 |
-
translation = x[3:6]
|
| 64 |
-
scale = x[6]
|
| 65 |
-
return scale * rotation.apply(pd_vertices) + translation
|
| 66 |
-
|
| 67 |
-
def cost_function(x, pd_vertices, gt_vertices):
|
| 68 |
-
pd_vertices_transformed = transform(x, pd_vertices)
|
| 69 |
-
distances = cdist(pd_vertices_transformed, gt_vertices, metric='euclidean')
|
| 70 |
-
row_ind, col_ind = linear_sum_assignment(distances)
|
| 71 |
-
translation_costs = np.sum(distances[row_ind, col_ind])
|
| 72 |
-
|
| 73 |
-
return translation_costs
|
| 74 |
-
|
| 75 |
-
x0 = np.array([0, 0, 0, 0, 0, 0, 1])
|
| 76 |
-
# minimize subject to scale > 1e-6
|
| 77 |
-
# res = minimize(cost_function, x0, args=(pd_vertices, gt_vertices), constraints={'type': 'ineq', 'fun': lambda x: x[6] - 1e-6})
|
| 78 |
-
res = minimize(cost_function, x0, args=(pd_vertices, gt_vertices), bounds=[(-np.pi, np.pi), (-np.pi, np.pi), (-np.pi, np.pi), (-500, 500), (-500, 500), (-500, 500), (0.1, 3)])
|
| 79 |
-
# print("scale:", res.x)
|
| 80 |
-
|
| 81 |
-
pd_vertices = transform(res.x, pd_vertices)
|
| 82 |
-
|
| 83 |
|
| 84 |
# Step 1: Bipartite Matching
|
| 85 |
distances = cdist(pd_vertices, gt_vertices, metric='euclidean')
|
|
@@ -106,7 +86,6 @@ def compute_WED(pd_vertices, pd_edges, gt_vertices, gt_edges, cv=1000.0, ce=1.0,
|
|
| 106 |
# Delete edges not in ground truth
|
| 107 |
edges_to_delete = pd_edges_set - gt_edges_set
|
| 108 |
|
| 109 |
-
#deletion_edge_costs = ce * sum(np.linalg.norm(pd_vertices[edge[0]] - pd_vertices[edge[1]]) for edge in edges_to_delete)
|
| 110 |
vert_tf = [np.where(col_ind == v)[0][0] if v in col_ind else 0 for v in range(len(gt_vertices))]
|
| 111 |
deletion_edge_costs = ce * sum(np.linalg.norm(pd_vertices[vert_tf[edge[0]]] - pd_vertices[vert_tf[edge[1]]]) for edge in edges_to_delete)
|
| 112 |
|
|
@@ -117,8 +96,7 @@ def compute_WED(pd_vertices, pd_edges, gt_vertices, gt_edges, cv=1000.0, ce=1.0,
|
|
| 117 |
|
| 118 |
# Step 5: Calculation of WED
|
| 119 |
WED = translation_costs + deletion_costs + insertion_costs + deletion_edge_costs + insertion_edge_costs
|
| 120 |
-
|
| 121 |
-
# print(translation_costs, deletion_costs, insertion_costs, deletion_edge_costs, insertion_edge_costs)
|
| 122 |
|
| 123 |
if normalized:
|
| 124 |
total_length_of_gt_edges = np.linalg.norm((gt_vertices[gt_edges[:, 0]] - gt_vertices[gt_edges[:, 1]]), axis=1).sum()
|
|
|
|
| 3 |
import numpy as np
|
| 4 |
|
| 5 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
def preregister_mean_std(verts_to_transform, target_verts, single_scale=True):
|
| 7 |
mu_target = target_verts.mean(axis=0)
|
| 8 |
mu_in = verts_to_transform.mean(axis=0)
|
|
|
|
| 28 |
return transformed_verts
|
| 29 |
|
| 30 |
|
| 31 |
+
def compute_WED(pd_vertices, pd_edges, gt_vertices, gt_edges, cv=-1, ce=1.0, normalized=True, preregister=True, single_scale=True):
|
| 32 |
+
'''The function computes the Weighted Edge Distance (WED) between two graphs.
|
| 33 |
+
pd_vertices: list of predicted vertices
|
| 34 |
+
pd_edges: list of predicted edges
|
| 35 |
+
gt_vertices: list of ground truth vertices
|
| 36 |
+
gt_edges: list of ground truth edges
|
| 37 |
+
cv: vertex cost
|
| 38 |
+
ce: edge cost
|
| 39 |
+
normalized: if True, the WED is normalized by the total length of the ground truth edges
|
| 40 |
+
preregister: if True, the predicted vertices are pre-registered to the ground truth vertices
|
| 41 |
+
'''
|
| 42 |
+
|
| 43 |
+
# vertex coordinates are in centimeters, so cv and ce are set to 100.0 and 1.0 respectively.
|
| 44 |
+
# This means the missing a vertex is equivanlent predicting it 1 meters off,
|
| 45 |
+
# and that is the same as cv and ce equal to 1.0, if GT is in meters
|
| 46 |
+
|
| 47 |
pd_vertices = np.array(pd_vertices)
|
| 48 |
gt_vertices = np.array(gt_vertices)
|
| 49 |
|
| 50 |
+
diameter = cdist(gt_vertices, gt_vertices).max()
|
| 51 |
+
|
| 52 |
+
if cv < 0:
|
| 53 |
+
cv = diameter / 4.0
|
| 54 |
+
# Cost of addining or deleting a vertex is set to 1/4 of the diameter of the ground truth mesh
|
| 55 |
+
|
| 56 |
# Step 0: Prenormalize / preregister
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
if preregister:
|
| 58 |
pd_vertices = preregister_mean_std(pd_vertices, gt_vertices, single_scale=single_scale)
|
| 59 |
|
| 60 |
|
| 61 |
pd_edges = np.array(pd_edges)
|
| 62 |
+
gt_edges = np.array(gt_edges)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
# Step 1: Bipartite Matching
|
| 65 |
distances = cdist(pd_vertices, gt_vertices, metric='euclidean')
|
|
|
|
| 86 |
# Delete edges not in ground truth
|
| 87 |
edges_to_delete = pd_edges_set - gt_edges_set
|
| 88 |
|
|
|
|
| 89 |
vert_tf = [np.where(col_ind == v)[0][0] if v in col_ind else 0 for v in range(len(gt_vertices))]
|
| 90 |
deletion_edge_costs = ce * sum(np.linalg.norm(pd_vertices[vert_tf[edge[0]]] - pd_vertices[vert_tf[edge[1]]]) for edge in edges_to_delete)
|
| 91 |
|
|
|
|
| 96 |
|
| 97 |
# Step 5: Calculation of WED
|
| 98 |
WED = translation_costs + deletion_costs + insertion_costs + deletion_edge_costs + insertion_edge_costs
|
| 99 |
+
|
|
|
|
| 100 |
|
| 101 |
if normalized:
|
| 102 |
total_length_of_gt_edges = np.linalg.norm((gt_vertices[gt_edges[:, 0]] - gt_vertices[gt_edges[:, 1]]), axis=1).sum()
|