-
Notifications
You must be signed in to change notification settings - Fork 200
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Topology-preserving VW #143
Conversation
417ffbb
to
e327958
Compare
Some progress (?) |
And now for the bad news: |
I need to dig into this more, but it doesn't seem to be thrashing anywhere obvious; the naïve version runs the main loop 12.9k times, whereas the new version runs it 13.5k times (Only a 4.5% increase). This is somewhat complicated by not being able to run perf (bc macOS). |
Cool! I can take a look at this over the weekend |
I hooked up the same test to Instruments. 427 ms total with The JS |
c2c72fc
to
0dcb7c9
Compare
OK, this is beginning to feel somewhat more coherent. I've refactored the intersection-removal logic, which has made it a lot simpler: |
Progress:
Removing the point denoted by the dot causes a self-intersection. Ordinarily, that could be fixed by removing the previous point too, but that's impossible because that wouldn't leave enough points to form a ring, so we keep the exterior as is, and end the simplification. This doesn't affect simplification of the inner ring(s), and there's a test for that. |
OK! I think this death march is over for now. I'm pretty happy with the tests (although ideally I'd like some more weird adversarial examples) and the docs. Could some people have a look at it / try it out / try to break it / comment on the code? @frewsxcv @pka @mbattifarano @jdroenner? I'm still not delighted about the Spade R* tree performance – someone has suggested using SQLite's R* functionality, but AFAIK it's not included in the standard SQLite distribution, and SQLite isn't available everywhere anyway, and I have no experience using it for spatial queries (Spatialite uses it, of course, so it certainly works). Other points in Spade's favour are the fact that we need an R or R* tree for a Polygon distance variant, so we're not pulling it in as a dependency just for this. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks good after a first pass! i need to do a little more reading as i'm not too familiar with some of the logic here. if someone else in the community is, would love your feedback, otherwise i'll try to take a closer look into this in the coming days
{ | ||
fn partial_cmp(&self, other: &VScore<T>) -> Option<Ordering> { | ||
Some(self.cmp(other)) | ||
} | ||
} | ||
|
||
impl<T> Eq for VScore<T> where T: Float {} | ||
impl<T> Eq for VScore<T> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you might be able to derive Eq
but keep the explicit PartialEq
impl
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The compiler complains about this for some reason, which is (IIRC) why I had to implement the blank Eq
.
src/algorithm/simplifyvw.rs
Outdated
} | ||
|
||
// Geometries that can be simplified using the topology-preserving variant | ||
#[derive(Debug)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[derive(Copy)]
src/algorithm/simplifyvw.rs
Outdated
// have min_points left, stop: since a self-intersection causes removal of the spatially previous | ||
// point, THAT could lead to a further self-intersection without the possibility of removing | ||
// more points, potentially leaving the geometry in an invalid state. | ||
#[derive(Debug)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[derive(Copy)]
src/algorithm/simplifyvw.rs
Outdated
let interiors = match simplified.is_empty() { | ||
true => vec![], | ||
false => simplified.into_iter().map(LineString).collect(), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
seems fine to just do let interiors = simplified.into_iter().map(LineString).collect();
here. an empty iterator will result in an empty Vec
src/algorithm/simplifyvw.rs
Outdated
let candidates = tree.lookup_in_rectangle(&BoundingRect::from_corners(&br, &tl)); | ||
candidates | ||
.iter() | ||
.map(|c| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you can get rid of the map
here i think
.iter()
.any(|c| {
...
})
0003d01
to
2530e2f
Compare
I just read through this again one more time, and everything seems good enough to land. I'm still not very knowledgeable about the algorithm (yet), but I don't think it makes sense to hold up merging this. If I learn more about it in the future, I can always revisit the code. I noticed the title of the PR has 'WIP'. Is this ready to go or do you still have more changes? |
I don't think there's anything left for me to do – edited. I'm happy to label this as "experimental" in the docs for a while, maybe? I'm just going to squash my commits, and then it's ready to go, apart from the possible doc change. |
This is based on the technique outlined at https://www.jasondavies.com/simplify/ It uses an R* tree to restrict the intersection search to only those segments that fall within a given triangle's bounding box. If an intersection is detected, the previous point (i.e. the triangle's "left" member is also marked for removal by setting its area to negative epsilon), removing the self-intersection.
2530e2f
to
ee0f105
Compare
up to you if you want to do this, but i don't think it's necessary. i don't see any reason to consider this much more experimental than other algorithms in rust-geo. thanks for doing this! your comments you added to this PR along the way were very helpful! bors r+ |
143: Topology-preserving VW r=frewsxcv a=urschrei This is a first attempt at topology-preserving VW, based on https://www.jasondavies.com/simplify/. **Update**: see subsequent comments for a refinement of the approach. <s>It's WIP for two reasons: - I need a longer test input which produces self-intersections for a given epsilon. This is surprisingly difficult to cook up.</s> - I'm not sure the approach in this fork is correct: 1. If an intersection is found, move the triangle (`a`) into a vec, and exit the loop early 2. pop the next triangle (`b`) and start processing it 3. `peek_mut()` at the _next_ triangle (`c`) in the queue, and set its area to `a`s area. This guarantees its removal 4. push `a` back onto the heap. If we modified the correct `c` triangle, `a` will be subsequently skipped in the main loop, as one of its support points was removed 5. if it wasn't the triangle we needed to remove, `a` will be re-detected as causing an intersection. <s>I've got an alternate implementation [here](https://github.com/urschrei/rust-geo/blob/vw_topology_preserve/src/algorithm/simplifyvw.rs#L168). The difference: 1. If an intersection is found, move the triangle (`a`) into a vec, and exit the loop early 2. pop the next triangle (`b`) and start processing it 3. `peek` at the _next_ triangle (`c`) in the queue, **and increase the epsilon to its area. This guarantees its removal** 4. push `a` back onto the heap. If the `c` triangle we remove was the correct one, `a` will be subsequently skipped in the main loop, as one of its support points was removed 5. If it wasn't, `a` will be re-detected as causing an intersection, and the epsilon will be increased again. The drawback of the second approach is that it permanently increases the user-defined epsilon, leading to the removal of more points overall than the first approach. Also, both of these approaches could be wrong. More complex test inputs will _really_ help here.</s>
Build succeeded |
This is a first attempt at topology-preserving VW, based on https://www.jasondavies.com/simplify/.
Update: see subsequent comments for a refinement of the approach.
It's WIP for two reasons:I need a longer test input which produces self-intersections for a given epsilon. This is surprisingly difficult to cook up.a
) into a vec, and exit the loop earlyb
) and start processing itpeek_mut()
at the next triangle (c
) in the queue, and set its area toa
s area. This guarantees its removala
back onto the heap. If we modified the correctc
triangle,a
will be subsequently skipped in the main loop, as one of its support points was removeda
will be re-detected as causing an intersection.I've got an alternate implementation here. The difference:a
) into a vec, and exit the loop earlyb
) and start processing itpeek
at the next triangle (c
) in the queue, and increase the epsilon to its area. This guarantees its removala
back onto the heap. If thec
triangle we remove was the correct one,a
will be subsequently skipped in the main loop, as one of its support points was removeda
will be re-detected as causing an intersection, and the epsilon will be increased again.The drawback of the second approach is that it permanently increases the user-defined epsilon, leading to the removal of more points overall than the first approach.Also, both of these approaches could be wrong. More complex test inputs will really help here.