Skip to content

Commit b0a0548

Browse files
authored
Merge pull request #217 from nzfeng/fast-marching
Signed Fast Marching Method
2 parents 73da1d8 + 9df4338 commit b0a0548

4 files changed

Lines changed: 205 additions & 32 deletions

File tree

docs/docs/surface/algorithms/geodesic_distance.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,50 @@ Computing exact geodesic paths also allows one to compute exact [log maps](/surf
174174

175175
Returns the gradient of the distance function at `v` (i.e. the unit tangent vector at `v` which points away from the closest source)
176176

177+
## Fast Marching Method for Distance
178+
179+
These routines implement the _fast marching method_ (FMM) for geodesic distance on triangle meshes, as described by [Kimmel and Sethian 1998](https://www.pnas.org/doi/pdf/10.1073/pnas.95.15.8431).
180+
181+
This implementation allows you to obtain unsigned distance to a source set of points, or signed distance to a source set of curves, where the distance values on the source set may be initialized to any value. (When initial distances are not 0, "signed" means that the gradient of distance is continuous across the source curves.)
182+
183+
Note that as a wave-based propagation method, if the source curves are not closed, then signed distance output is not guaranteed to be well-behaved; if you're looking for robust signed distance computation, consider the [Signed Heat Method routines](/surface/algorithms/signed_heat_method). If your curves are closed, then FMM will in general be more efficient.
184+
185+
`#include "geometrycentral/surface/fast_marching_method.h"`
186+
187+
??? func "`#!cpp VertexData<double> FMMDistance(IntrinsicGeometryInterface& geometry, const std::vector<std::vector<std::pair<SurfacePoint, double>>>& initialDistances, bool sign = false)`"
188+
189+
Compute the distance from a source set of [Surface Points](/surface/utilities/surface_point/), initialized with the given distance values.
190+
191+
If `sign` is false, computes unsigned distance. If `sign` is true, computes signed distance by interpreting the source points as a set of curves and propagating their orientation.
192+
193+
??? func "`#!cpp VertexData<double> FMMDistance(IntrinsicGeometryInterface& geometry, const std::vector<std::pair<Vertex, double>>& initialDistances, bool sign = false)`"
194+
195+
Compute the distance from a set of source vertices, initialized with the given distance values.
196+
197+
If `sign` is false, computes unsigned distance. If `sign` is true, computes signed distance by interpreting the source vertices as a set of curves and propagating their orientation.
198+
199+
Example
200+
```cpp
201+
#include "geometrycentral/surface/fast_marching_method.h"
202+
#include "geometrycentral/surface/meshio.h"
203+
204+
// Load a mesh
205+
std::unique_ptr<SurfaceMesh> mesh;
206+
std::unique_ptr<VertexPositionGeometry> geometry;
207+
std::tie(mesh, geometry) = loadMesh(filename);
208+
209+
// Pick some vertices and initial distances.
210+
std::vector<std::pair<Vertex, double>> initialDistances;
211+
initialDistances.emplace_back(mesh->vertex(7), 0.);
212+
initialDistances.emplace_back(mesh->vertex(8), 0.);
213+
214+
// Compute distance
215+
VertexData<double> dist = FMMDistance(*geometry, initialDistances);
216+
/* do something useful */
217+
```
218+
219+
If computing signed distance, distances should be initialized to 0 on the source points.
220+
177221
## Heat Method for Distance
178222
179223
These routines implement the [Heat Method for Geodesic Distance](http://www.cs.cmu.edu/~kmcrane/Projects/HeatMethod/paper.pdf). This algorithm uses short time heat flow to compute distance on surfaces. Because the main burden is simply solving linear systems of equations, it tends to be faster than polyhedral schemes, especially when computing distance multiple times on the same surface. In the computational geometry sense, this method is an approximation, as the result is not precisely equal to the polyhedral distance on the surface; nonetheless it is fast and well-suited for many applications.

include/geometrycentral/surface/barycentric_vector.ipp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -531,8 +531,8 @@ inline Face sharedFace(const BarycentricVector& u, const BarycentricVector& w) {
531531
case BarycentricVectorType::Vertex:
532532
for (Vertex v : u.face.adjacentVertices()) {
533533
if (v == w.vertex) return u.face;
534-
break;
535534
}
535+
break;
536536
}
537537
break;
538538
}

include/geometrycentral/surface/fast_marching_method.h

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#pragma once
22

3+
#include "geometrycentral/surface/barycentric_vector.h"
34
#include "geometrycentral/surface/intrinsic_geometry_interface.h"
5+
#include "geometrycentral/surface/surface_point.h"
46
#include "geometrycentral/utilities/utilities.h"
57

68
#include <cmath>
@@ -12,7 +14,11 @@ namespace geometrycentral {
1214
namespace surface {
1315

1416
VertexData<double> FMMDistance(IntrinsicGeometryInterface& geometry,
15-
const std::vector<std::pair<Vertex, double>>& initialDistances);
17+
const std::vector<std::vector<std::pair<SurfacePoint, double>>>& initialDistances,
18+
bool sign = false);
19+
20+
VertexData<double> FMMDistance(IntrinsicGeometryInterface& geometry,
21+
const std::vector<std::pair<Vertex, double>>& initialDistances, bool sign = false);
1622

1723

1824
} // namespace surface

src/surface/fast_marching_method.cpp

Lines changed: 153 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace {
1111

1212
// The super fun quadratic distance function in the Fast Marching Method on triangle meshes
1313
// TODO parameter c isn't actually defined in paper, so I guessed that it was an error
14-
double eikonalDistanceSubroutine(double a, double b, double theta, double dA, double dB) {
14+
double eikonalDistanceSubroutine(double a, double b, double theta, double dA, double dB, int sign) {
1515

1616

1717
if (theta <= PI / 2.0) {
@@ -24,55 +24,165 @@ double eikonalDistanceSubroutine(double a, double b, double theta, double dA, do
2424
double quadB = 2 * b * u * (a * cTheta - b);
2525
double quadC = b * b * (u * u - a * a * sTheta2);
2626
double sqrtVal = std::sqrt(quadB * quadB - 4 * quadA * quadC);
27-
// double tVals[] = {(-quadB + sqrtVal) / (2*quadA), // seems to always be the first one
28-
// (-quadB - sqrtVal) / (2*quadA)};
29-
30-
double t = (-quadB + sqrtVal) / (2 * quadA);
31-
if (u < t && a * cTheta < b * (t - u) / t && b * (t - u) / t < a / cTheta) {
27+
double tVals[] = {(-quadB + sqrtVal) / (2 * quadA), (-quadB - sqrtVal) / (2 * quadA)};
28+
double t = sign > 0 ? tVals[0] : tVals[1];
29+
double y = b * (t - u) / t;
30+
if (u < sign * t && a * cTheta < y && y < a / cTheta) {
3231
return dA + t;
3332
} else {
34-
return std::min(b + dA, a + dB);
33+
return std::min(b * sign + dA, a * sign + dB);
3534
}
3635

3736
}
3837
// Custom by Nick to get acceptable results in obtuse triangles without fancy unfolding
3938
else {
40-
41-
double maxDist = std::max(dA, dB); // all points on base are less than this far away, by convexity
39+
double maxDist = std::max(sign * dA, sign * dB); // all points on base are less than this far away, by convexity
4240
double c = std::sqrt(a * a + b * b - 2 * a * b * std::cos(theta));
4341
double area = 0.5 * std::sin(theta) * a * b;
4442
double altitude = 2 * area / c; // distance to base, must be inside triangle since obtuse
4543
double baseDist = maxDist + altitude;
4644

47-
return std::min({b + dA, a + dB, baseDist});
45+
return std::min({b * sign + dA, a * sign + dB, sign * baseDist});
4846
}
4947
}
5048

5149

5250
} // namespace
5351

54-
5552
VertexData<double> FMMDistance(IntrinsicGeometryInterface& geometry,
56-
const std::vector<std::pair<Vertex, double>>& initialDistances) {
53+
const std::vector<std::vector<std::pair<SurfacePoint, double>>>& initialDistances,
54+
bool sign) {
5755

5856
typedef std::pair<double, Vertex> Entry;
5957

6058
SurfaceMesh& mesh = geometry.mesh;
6159
geometry.requireEdgeLengths();
6260
geometry.requireCornerAngles();
63-
61+
6462
// TODO this could handle nonmanifold geometry with a few small tweaks
65-
if(!mesh.isManifold()) {
63+
if (!mesh.isManifold()) {
6664
throw std::runtime_error("handling of nonmanifold mesh not yet implemented");
6765
}
6866

6967
// Initialize
7068
VertexData<double> distances(mesh, std::numeric_limits<double>::infinity());
69+
VertexData<int> signs(mesh, 1);
7170
VertexData<char> finalized(mesh, false);
71+
VertexData<char> isSource(mesh, false);
7272

73-
std::priority_queue<Entry, std::vector<Entry>, std::greater<Entry>> frontierPQ;
74-
for (auto& x : initialDistances) {
75-
frontierPQ.push(std::make_pair(x.second, x.first));
73+
auto cmp = [&signs](Entry left, Entry right) {
74+
if (signs[left.second] != signs[right.second]) {
75+
// We're looking at an edge that intersects the source, which may not have initial distance 0.
76+
// I *think* this can only happen if the positive vertex lies on the source, in which case it's the "closer" one.
77+
return signs[left.second] != 1;
78+
}
79+
return signs[left.second] * left.first > signs[right.second] * right.first;
80+
};
81+
std::priority_queue<Entry, std::vector<Entry>, decltype(cmp)> frontierPQ(cmp);
82+
// Initialize signs
83+
if (sign) {
84+
for (Vertex v : mesh.vertices()) signs[v] = 0;
85+
for (auto& curve : initialDistances) {
86+
size_t nNodes = curve.size();
87+
for (size_t i = 0; i < nNodes - 1; i++) {
88+
const SurfacePoint& pA = curve[i].first;
89+
const SurfacePoint& pB = curve[i + 1].first;
90+
Edge commonEdge = sharedEdge(pA, pB);
91+
if (commonEdge != Edge()) {
92+
// Assign +/- signs to the "third" vertices of each face straddling this edge.
93+
// These vertices might themselves lie on the curve, in which case we overwrite them below.
94+
Halfedge he = commonEdge.halfedge();
95+
signs[he.next().tipVertex()] = (he.vertex() == pA.vertex) ? 1 : -1;
96+
signs[he.twin().next().tipVertex()] = -signs[he.next().tipVertex()];
97+
} else {
98+
Face commonFace = sharedFace(pA, pB);
99+
if (commonFace == Face()) {
100+
throw std::logic_error("For signed fast marching distance, each curve segment must share a common face.");
101+
}
102+
BarycentricVector tangent(pA, pB);
103+
BarycentricVector normal = tangent.rotate90(geometry);
104+
for (Vertex v : commonFace.adjacentVertices()) {
105+
BarycentricVector u(SurfacePoint(v), pA);
106+
signs[v] = (dot(geometry, normal, u) > 0) ? 1 : -1;
107+
}
108+
}
109+
}
110+
}
111+
// Vertices on the curve are always assumed to have positive sign.
112+
for (auto& curve : initialDistances) {
113+
size_t nNodes = curve.size();
114+
for (size_t i = 0; i < nNodes; i++) {
115+
const SurfacePoint& p = curve[i].first;
116+
if (p.type != SurfacePointType::Vertex) continue;
117+
signs[p.vertex] = 1;
118+
}
119+
}
120+
// Fill in the signs of faces around the fan of any vertices.
121+
for (auto& curve : initialDistances) {
122+
size_t nNodes = curve.size();
123+
for (size_t i = 0; i < nNodes; i++) {
124+
const SurfacePoint& p = curve[i].first;
125+
if (p.type != SurfacePointType::Vertex) continue;
126+
Halfedge startHe = p.vertex.halfedge();
127+
Halfedge currHe = startHe;
128+
while (true) {
129+
if (signs[currHe.tipVertex()] != 0 && signs[currHe.tipVertex()] != signs[p.vertex] &&
130+
signs[currHe.next().tipVertex()] == 0) {
131+
signs[currHe.next().tipVertex()] = -1;
132+
}
133+
currHe = currHe.next().next().twin();
134+
if (currHe == startHe) break;
135+
}
136+
}
137+
}
138+
}
139+
// Initialize distances.
140+
for (auto& curve : initialDistances) {
141+
for (auto& x : curve) {
142+
const SurfacePoint& p = x.first;
143+
switch (p.type) {
144+
case (SurfacePointType::Vertex): {
145+
frontierPQ.push(std::make_pair(x.second, p.vertex));
146+
isSource[p.vertex] = true;
147+
break;
148+
}
149+
case (SurfacePointType::Edge): {
150+
const Vertex& vA = p.edge.firstVertex();
151+
const Vertex& vB = p.edge.secondVertex();
152+
double l = geometry.edgeLengths[p.edge];
153+
frontierPQ.push(std::make_pair(x.second + signs[vA] * p.tEdge * l, vA));
154+
frontierPQ.push(std::make_pair(x.second + signs[vB] * (1. - p.tEdge) * l, vB));
155+
isSource[vA] = true;
156+
isSource[vB] = true;
157+
break;
158+
}
159+
case (SurfacePointType::Face): {
160+
Halfedge he = p.face.halfedge();
161+
const Vertex& vA = he.vertex();
162+
const Vertex& vB = he.next().vertex();
163+
const Vertex& vC = he.next().next().vertex();
164+
double lAB = geometry.edgeLengths[he.edge()];
165+
double lBC = geometry.edgeLengths[he.next().edge()];
166+
double lCA = geometry.edgeLengths[he.next().next().edge()];
167+
double lAB2 = lAB * lAB;
168+
double lBC2 = lBC * lBC;
169+
double lCA2 = lCA * lCA;
170+
double u = p.faceCoords[0];
171+
double v = p.faceCoords[1];
172+
double w = p.faceCoords[2];
173+
double dist2_A = lAB2 * (v * (1. - u)) + lCA2 * (w * (1. - u)) - lAB2 * v * w; // squared distance from p to vA
174+
double dist2_B = lAB2 * (u * (1. - v)) + lBC2 * (w * (1. - v)) - lCA2 * u * w; // squared distance from p to vB
175+
double dist2_C = lCA2 * (u * (1. - w)) + lBC2 * (v * (1. - w)) - lBC2 * u * v; // squared distance from p to vC
176+
frontierPQ.push(std::make_pair(x.second + signs[vA] * std::sqrt(dist2_A), vA));
177+
frontierPQ.push(std::make_pair(x.second + signs[vB] * std::sqrt(dist2_B), vB));
178+
frontierPQ.push(std::make_pair(x.second + signs[vC] * std::sqrt(dist2_C), vC));
179+
isSource[vA] = true;
180+
isSource[vB] = true;
181+
isSource[vC] = true;
182+
break;
183+
}
184+
}
185+
}
76186
}
77187
size_t nFound = 0;
78188
size_t nVert = mesh.nVertices();
@@ -95,16 +205,16 @@ VertexData<double> FMMDistance(IntrinsicGeometryInterface& geometry,
95205
finalized[currV] = true;
96206
nFound++;
97207

98-
99208
// Add any eligible neighbors
100209
for (Halfedge he : currV.incomingHalfedges()) {
101210
Vertex neighVert = he.vertex();
102211

103212
// Add with length
104-
if (!finalized[neighVert]) {
105-
double newDist = currDist + geometry.edgeLengths[he.edge()];
106-
if (newDist < distances[neighVert]) {
107-
frontierPQ.push(std::make_pair(currDist + geometry.edgeLengths[he.edge()], neighVert));
213+
if (!finalized[neighVert] && !isSource[neighVert]) {
214+
if (signs[neighVert] == 0) signs[neighVert] = signs[currV];
215+
double newDist = currDist + signs[neighVert] * geometry.edgeLengths[he.edge()];
216+
if (signs[neighVert] * newDist < signs[neighVert] * distances[neighVert] || std::isinf(distances[neighVert])) {
217+
frontierPQ.push(std::make_pair(newDist, neighVert));
108218
distances[neighVert] = newDist;
109219
}
110220
continue;
@@ -113,17 +223,17 @@ VertexData<double> FMMDistance(IntrinsicGeometryInterface& geometry,
113223
// Check the third point of the "left" triangle straddling this edge
114224
if (he.isInterior()) {
115225
Vertex newVert = he.next().next().vertex();
116-
if (!finalized[newVert]) {
226+
if (!finalized[newVert] && !isSource[newVert]) {
117227

118228
// Compute the distance
119229
double lenB = geometry.edgeLengths[he.next().next().edge()];
120230
double distB = currDist;
121231
double lenA = geometry.edgeLengths[he.next().edge()];
122232
double distA = distances[neighVert];
123233
double theta = geometry.cornerAngles[he.next().next().corner()];
124-
double newDist = eikonalDistanceSubroutine(lenA, lenB, theta, distA, distB);
125-
126-
if (newDist < distances[newVert]) {
234+
if (signs[newVert] == 0) signs[newVert] = (signs[currV] != 0) ? signs[currV] : signs[he.next().vertex()];
235+
double newDist = eikonalDistanceSubroutine(lenA, lenB, theta, distA, distB, signs[newVert]);
236+
if (signs[newVert] * newDist < signs[newVert] * distances[newVert] || std::isinf(distances[newVert])) {
127237
frontierPQ.push(std::make_pair(newDist, newVert));
128238
distances[newVert] = newDist;
129239
}
@@ -134,17 +244,17 @@ VertexData<double> FMMDistance(IntrinsicGeometryInterface& geometry,
134244
Halfedge heT = he.twin();
135245
if (heT.isInterior()) {
136246
Vertex newVert = heT.next().next().vertex();
137-
if (!finalized[newVert]) {
247+
if (!finalized[newVert] && !isSource[newVert]) {
138248

139249
// Compute the distance
140250
double lenB = geometry.edgeLengths[heT.next().edge()];
141251
double distB = currDist;
142252
double lenA = geometry.edgeLengths[heT.next().next().edge()];
143253
double distA = distances[neighVert];
144254
double theta = geometry.cornerAngles[heT.next().next().corner()];
145-
double newDist = eikonalDistanceSubroutine(lenA, lenB, theta, distA, distB);
146-
147-
if (newDist < distances[newVert]) {
255+
if (signs[newVert] == 0) signs[newVert] = (signs[currV] != 0) ? signs[currV] : signs[he.next().vertex()];
256+
double newDist = eikonalDistanceSubroutine(lenA, lenB, theta, distA, distB, signs[newVert]);
257+
if (signs[newVert] * newDist < signs[newVert] * distances[newVert] || std::isinf(distances[newVert])) {
148258
frontierPQ.push(std::make_pair(newDist, newVert));
149259
distances[newVert] = newDist;
150260
}
@@ -157,5 +267,18 @@ VertexData<double> FMMDistance(IntrinsicGeometryInterface& geometry,
157267
}
158268

159269

270+
VertexData<double> FMMDistance(IntrinsicGeometryInterface& geometry,
271+
const std::vector<std::pair<Vertex, double>>& initialDistances, bool sign) {
272+
273+
std::vector<std::vector<std::pair<SurfacePoint, double>>> initialConditions;
274+
initialConditions.emplace_back();
275+
for (const auto& x : initialDistances) {
276+
initialConditions.back().emplace_back(SurfacePoint(x.first), x.second);
277+
}
278+
VertexData<double> distances = FMMDistance(geometry, initialConditions, sign);
279+
return distances;
280+
}
281+
282+
160283
} // namespace surface
161284
} // namespace geometrycentral

0 commit comments

Comments
 (0)