前段时间在b站发布了关于二维平面下一些计算几何学知识的讲解,有许多小伙伴私戳我说能不能出个代码实现,所以这段时间就抽个时间用c++实现下视频里面讲的内容。
注: 本篇博客不再具体讲解理论内容,而是实现相关算法。想要进一步深入了解理论内容的小伙伴可以去回顾之前的视频讲解:bilibili。
代码仓库:https://github.com/CHH3213/Math_Geometry/
point的struct定义如下:
// Define point struct.
struct Point {
Point()=default;
Point(double x_in, double y_in) : x(x_in), y(y_in) {}
Point(const Point& p) : x(p.x), y(p.y) {}
Point& operator=(const Point& p) {
x = p.x;
y = p.y;
return *this;
}
Point operator+(const Point& p) const{
return {x + p.x, y + p.y};
}
Point operator-(const Point& p) const{
return {x - p.x, y - p.y};
}
double operator*(const Point& p) const{
return x * p.x+ y * p.y;
}
Point operator*(double k)const {
return {x *k, y * k};
}
friend Point operator*(double k, const Point& p) {
return {p.x * k, p.y * k};
}
bool operator==(const Point& p)const{
return p.x==x&&p.y==y;
}
bool operator!=(const Point& p)const{
return !(p.x==x&&p.y==y);
}
double modulus()const {
return sqrt(x * x + y * y);
}
double DistanceTo(const Point& other)const {
double dx = x - other.x;
double dy = y - other.y;
return sqrt(dx * dx + dy * dy);
}
friend std::ostream& operator<<(std::ostream& out, const Point& p) {
out << "(" << p.x << ", " << p.y << ")";
return out;
}
double x;
double y;
};
line就是直线,直线的struct我们可以定义成:
//Define line segment.
struct Line {
Line()=default;
Line(Point p1_in, Point p2_in) : p1(p1_in), p2(p2_in),direction(p2_in-p1_in) {
}
friend std::ostream& operator<<(std::ostream& out, const Line& line) {
out << "Line: " << line.p1 << " ---> " << line.p2;
return out;
}
// A point in Line.
Point p1;
// Another point in line.
Point p2;
Point direction;
};
线段的struct如下:
// Define segment struct.
struct Segment {
Segment()=default;
Segment(Point start_in, Point end_in) : start(start_in), end(end_in),direction(end-start) {
}
Segment(const Segment& s) : start(s.start), end(s.end),direction(end-start) {}
Segment& operator=(const Segment& s) {
start = s.start;
end = s.end;
return *this;
}
Segment operator+(const Segment& rhs)const {
return {start + rhs.start, end + rhs.end};
}
Segment operator-(const Segment& rhs)const {
return {start - rhs.start, end - rhs.end};
}
double length() const{
return direction.modulus();
}
Point unit_direction() const{
double len = length();
if (len != 0) {
return {direction.x / len, direction.y / len};
} else {
// Handle the case where the length is zero (avoid division by zero).
throw std::runtime_error("Cannot calculate unit direction for a segment with zero length.");
}
}
friend std::ostream& operator<<(std::ostream& out, const Segment& s) {
out << "Segment: " << s.start << " ---> " << s.end;
return out;
}
Point start;
Point end;
Point direction;
};
线段彼此相连便组成了折线段,也就是polyline,我们可以这样定义struct
// Define polyline struct.
// Note that a polyline we can use points vector or segments vector to represent.
struct Polyline {
Polyline()=default;
Polyline(const std::vector<Point>& pts):points(pts){
for(int i = 1;i<points.size();++i){
segs.push_back(Segment(points[i-1],points[i]));
}
}
Polyline(const std::vector<Segment>& segs_) : segs(segs_) {
for(int i = 0;i<segs.size();++i){
points.push_back(segs[i].start);
}
points.push_back(segs[segs.size()-1].end);
}
void append(const Segment& seg) {
if(!segs.empty() && segs.back().end != seg.start) {
throw std::invalid_argument("Disconnected Segment");
}
segs.push_back(seg);
points.push_back(seg.end);
}
void append(const Point& p) {
const auto seg = Segment(points.back(),p);
points.push_back(p);
segs.push_back(seg);
}
Polyline operator+(const Polyline& other) const {
Polyline result;
result.segs = this->segs;
result.points = this->points;
for(auto& seg : other.segs) {
result.append(seg);
}
return result;
}
Segment GetSegmentByIndex(int index) {
if(index < 0 || index >= segs.size()) {
throw std::out_of_range("Index out of range");
}
return segs[index];
}
std::vector<Point> Points() const{
return points;
}
std::vector<Segment> Segments()const{
return segs;
}
private:
std::vector<Segment> segs;
std::vector<Point> points;
};
点积
// Calculates dot product.
double DotProduct(const Vec& v1,const Vec& v2){
return v1.x*v2.x+v1.y*v2.y;
}
叉积
// Calculates cross product.
double CrossProduct(const Vec& v1,const Vec& v2) {
return v1.x*v2.y-v2.x*v1.y;
}
投影这里指的是求点到线段的投影点、投影长度。
点到线段的投影长度
// Compute projection length of point p.
double ComputeProjectionLength(const Point& p,const Segment& segment){
const auto& p1p = p-segment.start;
return DotProduct(p1p,segment.unit_direction());
}
点到线段的投影点
// Compute projection point of point p.
Point ComputeProjection(const Point& p,const Segment& segment){
double projection_length = ComputeProjectionLength(p,segment);
return segment.start + segment.unit_direction()*projection_length;
}
距离包括点-点,点-直线,点-线段,线段-线段等之间的距离。
point to point
// Get distance between point p1 and point p2.
double GetDistance(const Point& p1, const Point& p2){
return p1.DistanceTo(p2);
}
point to line
// Get distance between point p and a straight line.
double GetDistance(const Point& p, const Line& line){
Segment p1p2(line.p1,line.p2);
Segment p1p(line.p1,p);
return std::abs(CrossProduct(p1p2.direction,p1p.direction))/p1p2.length();
}
point to segment
// Get distance between point p and segment(p1,p2).
double GetDistance(const Point& p, const Segment& segment){
Segment p1p(segment.start,p);
Segment p2p(segment.end,p);
const auto c1 = DotProduct(p1p.direction,segment.direction);
const auto c2 = DotProduct(p2p.direction,segment.direction);
if(c1<=0){
//distance(p,segment)=distacne(p1,p).
return GetDistance(segment.start,p);
}
if(c2>=0){
//distance(p,segment)=distacne(p2,p).
return GetDistance(segment.end,p);
}
return std::abs(CrossProduct(segment.direction,p1p.direction))/segment.length();
}
segment to segment
// Get distance between segment and segment (method 1), method 2 is to be determined.
double GetDistance(const Segment& s1,const Segment& s2){
const double d1 = GetDistance(s1.start, s2);
const double d2 = GetDistance(s1.end, s2);
const double d3 = GetDistance(s2.start, s1);
const double d4 = GetDistance(s2.end, s1);
return std::min(std::min(d1, d2), std::min(d3, d4));
}
注:视频中讲解的另外一种方法暂时未实现,留个todo。
对于一个点与线段之间的位置关系,我们可以定义一个enum来表示
enum class Side{
// When the segment's length is zero, it's useless to determine the side, so we use DEFAULT_NO_SIDE to show.
DEFAULT_NO_SIDE=0,
LEFT,
RIGHT,
// The three below states mean that the point is in line.
ON_AFTER,
ON_BEFORE,
WITHIN
};
也就是说点与线段的相对位置关系包括以下几种:
判断点跟一条线段的相对位置关系
// Determine which side of the segment the point is.
Side GetSide(const Point& p,const Segment& s){
const auto& p0 = s.start;
const auto& p2 = s.end;
const auto& a = p-p0;
const auto& b = p2-p0;
const auto cross_product = CrossProduct(a,b);
if(cross_product!=0){
// Returns LEFT if p0,p,p2 are clockwise position, returns RIGHT means p0,p,p2 are counter-clockwise position.
return cross_product<0?Side::LEFT:Side::RIGHT;
}
const auto dot_product = DotProduct(a,b);
if(dot_product<0){
return Side::ON_BEFORE;
}else if(dot_product>0){
if(b.modulus()<a.modulus()){
return Side::ON_AFTER;
}else{
return Side::WITHIN;
}
}else{
if(a.modulus()==0){
return Side::WITHIN;
}
return Side::DEFAULT_NO_SIDE;
}
}
判断点与两条相连的线段的相对位置关系——法一
// Determine which side of two segments the point is.
//Method 1: directly use cross product to determine and only have left/right options.
Side GetSide(const Point& p, const Segment& s1,const Segment& s2) {
//DCHECK(s1.end==s2.start)<<"please ensure the two segments are connected.";
if (s1.end != s2.start) {
throw std::runtime_error("Error: The two segments are not connected.");
}
const auto& p0p = p-s1.start;
const auto& p1p = p-s2.start;
const auto c1 = CrossProduct(s1.direction,p0p);
const auto c2 = CrossProduct(s2.direction,p1p);
if(c1>0&&c2>0){
return Side::LEFT;
}
if(c1<0&&c2<0){
return Side::RIGHT;
}
return std::abs(c1)>std::abs(c2)?Side::LEFT:Side::RIGHT;
}
判断点与两条相连的线段的相对位置关系——法二
// Determine which side of two segments the point is.
// Method 2: through the side of p to one segment to determine, and except left/right side, we also provide other options.
Side GetSide(const Point& p, const Segment& s1,const Segment& s2) {
//DCHECK(s1.end==s2.start)<<"please ensure the two segments are connected.";
if (s1.end != s2.start) {
throw std::runtime_error("Error: The two segments are not connected.");
}
const auto side_1 = GetSide(p,s1);
const auto side_2 = GetSide(p,s2);
if(side_1==side_2){
return side_1;
}
if(side_1==Side::WITHIN||side_2==Side::WITHIN){
return Side::WITHIN;
}
const auto& p0p = p-s1.start;
const auto& p1p = p-s2.start;
const auto c1 = CrossProduct(s1.direction,p0p);
const auto c2 = CrossProduct(s2.direction,p1p);
return std::abs(c2) > std::abs(c1) ? side_2 : side_1;
}
这里相交主要指的是线段与线段之间的相交。很显然相交分为两步:
判断是否相交
// Determine whether segment 1 intersects segment 2.
bool IsIntersection(const Segment& s1, const Segment& s2){
const double o1 = CrossProduct(s2.start - s1.start, s1.direction);
const double o2 = CrossProduct(s2.end - s1.start, s1.direction);
const double o3 = CrossProduct(s1.start - s2.start, s2.direction);
const double o4 = CrossProduct(s1.end - s2.start, s2.direction);
// Segments are considered intersecting if they have different orientations.
if(o1 * o2 < 0 && o3 * o4 < 0){
return true;
}
auto on_segment = [](const Point &p, const Point &q, const Point &r){
return (q.x <= std::max(p.x, r.x) && q.x >= std::min(p.x, r.x) &&
q.y <= std::max(p.y, r.y) && q.y >= std::min(p.y, r.y));
};
// Additional checks for collinear points.
if(o1 == 0 && on_segment(s1.start, s2.start, s1.end)){
return true;
}
if(o2 == 0 && on_segment(s1.start, s2.end, s1.end)){
return true;
}
if(o3 == 0 && on_segment(s2.start, s1.start, s2.end)){
return true;
}
if(o4 == 0 && on_segment(s2.start, s1.end, s2.end)){
return true;
}
return false;
}
若相交则求出相交点
//Calculate the intersection between segment 𝑝0𝑝1 and segment 𝑝2𝑝3.
Point GetIntersectionPoint(const Segment& s1, const Segment& s2){
if(!IsIntersection(s1, s2)){
std::cout << "No intersection, return a deafult point value:(0,0)!";
return Point(0, 0);
}
const auto& u = s1.direction;
const auto& v = s2.direction;
const auto& w = s1.start - s2.end;
const auto c1 = CrossProduct(w, v);
const auto c2 = CrossProduct(v, u);
if(c2 != 0){
const double t = std::abs(c1 / c2);
return s1.start + t * u;
}
// Address collinear and overlapping situation. If so, we return overlaping start or end.
const auto side_1 = GetSide(s1.start, s2);
const auto side_2 = GetSide(s1.end, s2);
const auto side_3 = GetSide(s2.start, s1);
const auto side_4 = GetSide(s2.end, s1);
if(side_1 == Side::WITHIN){
return s1.start;
}
if(side_2 == Side::WITHIN){
return s1.end;
}
if(side_3 == Side::WITHIN){
return s2.start;
}
if(side_4 == Side::WITHIN){
return s2.end;
}
throw std::runtime_error("Something is wrong, please check.");
}
通过三点求曲率:
// Obtain curvature according to p1,p2,p3.
// NOTE : curvature has a sign, not just an unsigned value.
double GetCurvature(const Point& p1, const Point& p2, const Point& p3){
const auto& p1p2 = p2 - p1;
const auto& p2p3 = p3 - p2;
const auto& p1p3 = p3 - p1;
const auto& a = p1p2.modulus();
const auto& b = p2p3.modulus();
const auto& c = p1p3.modulus();
const auto cross_product = CrossProduct(p1p2, p2p3);
return 2 * cross_product / (a * b * c);
}
求距离给定点最近的线段。
实现方式1
// Find the given point's closest segment in polyline using linear search.
// Option 1.
Segment FindClosestSegmentByLinearSearch(const Point& point, const Polyline& polyline){
const auto points = polyline.Points();
const auto segments = polyline.Segments();
//Compute the square distance between given point and first point in polyline.
double min_dist_sq = GetDistance(point,points[0]);
int closest_segment_index = 0;
for(int i=1;i<points.size();++i){
const auto& p1 = points[i-1];
const auto& p2 = points[i];
const auto& seg = segments[i-1];
const auto& v = seg.unit_direction();
const auto& w = point to p1;
const double c1 = DotProduct(w,v);
if(c1<=0){
continue;
}
double dist_sq= 0.0;
const double c2 = seg.length();
if(c2<=c1){
dist_sq = GetDistance(point,p2);
}else{
dist_sq = GetDistance(point,seg);
}
if(dist_sq<min_dist_sq){
min_dist_sq = dist_sq;
closest_segment_index=i-1;
if(min_dist_sq<Epsilon){
break;
}
}
}
return segments[closest_segment_index];
}
实现方式2
// Find the given point's closest segment in polyline using linear search.
// Option 2: since we have implemented distance function, we can directly use it.
Segment FindClosestSegmentByLinearSearch(const Point& point, const Polyline& polyline){
const auto& points = polyline.Points();
const auto& segments = polyline.Segments();
//Compute the square distance between given point and first point in polyline.
double min_dist_sq = GetDistance(point,points[0]);
int closest_segment_index = 0;
for(int i=0;i<segments.size();++i){
const auto& seg = segments[i];
const double dist_sq = GetDistance(point,seg);
if(dist_sq<min_dist_sq){
min_dist_sq = dist_sq;
closest_segment_index=i;
if(min_dist_sq<Epsilon){
break;
}
}
}
return segments[closest_segment_index];
}
视频中主要涉及的内容实现基本完成了,还有一些额外的没有实现,后续会把它完善。
以上所有代码均存放于github仓库中,欢迎访问:https://github.com/CHH3213/Math_Geometry/