Sir zach

专注于算法,AI, Android以及音视频领域 欢迎关注我的最新博客: zachliu.cn

导航

【原创】判断点是否在多边形内的实际应用

Posted on 2021-12-01 14:17  SirZach  阅读(174)  评论(0编辑  收藏  举报

背景介绍

在最近的车载设备项目中,交通部808协议中有一个功能是判断当前车辆是否行驶在多边形区域中,如果超出区域需要进行报警。这里的位置是通过GPS实时获得。实际上这是一个判断点是否在多边形内的一个典型应用。

808协议描述:
image

解法1: 射线法

由于此场景只需要判断单点是否在区域内,可以使用经典的射线法,此算法不需考虑精度误差和多边形点给出的顺序。算法复杂度为O(N)

此算法的思路是:
从检测点引一条射线,查看射线和多边形所有边的交点数目,如果左边和右边的交点数均为奇数,则检测点在多边形内; 如果左右两边的交点数均为偶数,则检测点在多边形外。

image
在图1中,射线左边有5个交点,右边有3个交点,故检测点在多边形内。

特别的,存在以下几种特殊情况:

image image

image

为了处理这些特殊情况,我们定义相交点肯定落在射线的上方,所以图4中边a 会产生一个交点,因为它的一个端点在射线上面,另一个在下边,而边b 不会产生交点,因为根据定义,它的两个端点都在射线上方,不算相交。
图5中,边c会产生一个交点,边d, e都不会产生交点。图6的情况类似。

另外,测试点刚好落在边上的情况需要单独讨论。

测试

为了测试代码的正确性,可以直接使用 HDU 1756 Cupid's Arrow 或者 Hrbust 1429 凸多边形

HDU 1756 注意坐标点要定义为double, 否则无法AC

点击查看代码
import java.io.IOException;
import java.util.Scanner;


public class HDU1756 { // OJ提交的时候要改成Main
    private static double eps = 1e-6;

    private static Point[] polygons;  // 存储多边形顶点集

    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            int n = sc.nextInt();  // 多边形顶点数
            polygons = new Point[n];

            for (int i = 0; i < n; i++) {
                double x = sc.nextDouble();
                double y = sc.nextDouble();
                polygons[i] = new Point(x, y);
            }

            n = sc.nextInt();  // 待测试点个数
            for (int i = 0; i < n; i++) {
                double x = sc.nextDouble();
                double y = sc.nextDouble();
                if (isInPolygon(new Point(x, y))) {
                    System.out.println("Yes");
                } else {
                    System.out.println("No");
                }
            }
        }
    }

    private static int dcmp(double x) {
        if (Math.abs(x) < eps) {
            return 0;
        }
        return x < 0 ? -1 : 1;
    }

    /**
     * 判断点P是否在A,B所在的边上
     */
    private static boolean onSegment(Point P, Point A, Point B) {
        double crossRet = new Point(A.minus(P)).crossMulti(B.minus(P));  
        double dotRet = new Point(A.minus(P)).dotMulti(B.minus(P));   
		// 判断P在A,B所在直线上  以及 判断P在AB 范围内
        return dcmp(crossRet) == 0 && dcmp(dotRet) <= 0;
    }


    private static boolean isInPolygon(Point q) {
        boolean flag = false;
        if (polygons.length == 0) return flag;

        int n = polygons.length;
        int j = n - 1;
        for (int i = 0; i < n; i++) {
            Point p1 = polygons[i];
            Point p2 = polygons[j];

            if (onSegment(q, p1, p2)) { // 测试点Q在边上
                return true;
            }

            double x = q.x;
            double y = q.y;

            // 280ms
            if (p1.y < y && p2.y >= y || p2.y < y && p1.y >= y) {
                if (p1.x + (y - p1.y) / (p2.y - p1.y) * (p2.x - p1.x) < x) {
                    flag = !flag;
                }
            }

            // 312 ms
//            double slope;
//            if (p2.x - p1.x == 0) {
//                slope = 0;
//            } else {
//                slope = (p2.y - p1.y) / (p2.x - p1.x);
//            }
//
//            boolean cond1 = (p1.x <= x) && (x < p2.x);
//            boolean cond2 = (p2.x <= x) && (x < p1.x);
//            boolean above = (y < slope * (x - p1.x) + p1.y);
//
//            if ((cond1 || cond2) && above) {
//                flag = !flag;
//            }

            j = i;
        }
        return flag;
    }


    static class Point {
        private double x, y;

        public Point(double x, double y) {
            this.x = x;
            this.y = y;
        }

        public Point(Point p) {
            this.x = p.x;
            this.y = p.y;
        }

        private Point minus(Point p) {
            return new Point(x - p.x, y - p.y);
        }

        private double dotMulti(Point p) {
            return x * p.x + y * p.y;
        }

        private double crossMulti(Point p) {
            return x * p.y - y * p.x;
        }
    }
}

两条直线是否相交的推导:
image

解法2: 二分法

算法思想:
1、选择多边形其中一个点O为起点,向多边形的其他顶点做射线
2、判断测试点是否在所有射线所包围的区域内。 如果点在最左侧向量左侧或最右侧向量右侧,则不在多边形内
3、用二分法找到点在哪两条向量之间,也就是找出点所在的大体区域
4、用两条向量v1, v2之间的顶点所形成的边a来判断测试点是否在多边形内

image

复杂度为O(logN)

使用下面的代码HDU 1756一直过不了,暂时不清楚问题在哪,这个OJ不好的地方就是没给出错误用例. Hrbust又注册不了。

点击查看代码
import java.io.IOException;
import java.util.Scanner;


public class Main {
    private static double eps = 1e-6;

    private static Point[] polygons;  // 存储多边形顶点集

    public static void main(String[] args) throws Exception {
        Scanner sc = new Scanner(System.in);
        while (sc.hasNext()) {
            int n = sc.nextInt();  // 多边形顶点数
            polygons = new Point[n];

            for (int i = 0; i < n; i++) {
                double x = sc.nextDouble();
                double y = sc.nextDouble();
                polygons[i] = new Point(x, y);
            }

            n = sc.nextInt();  // 待测试点个数
            for (int i = 0; i < n; i++) {
                double x = sc.nextDouble();
                double y = sc.nextDouble();
                if (isInPolygon(new Point(x, y))) {
                    System.out.println("Yes");
                } else {
                    System.out.println("No");
                }
            }
        }
    }

    private static int dcmp(double x) {
        if (Math.abs(x) < eps) {
            return 0;
        }
        return x < 0 ? -1 : 1;
    }

    /**
     * 判断点P是否在A,B所在的边上
     */
    private static boolean onSegment(Point P, Point A, Point B) {
        double crossRet = new Point(A.minus(P)).crossMulti(B.minus(P));
        double dotRet = new Point(A.minus(P)).dotMulti(B.minus(P));

        // 判断P在A,B所在直线上 ,并且判断P在AB 范围内
        return dcmp(crossRet) == 0 && dcmp(dotRet) <= 0;
    }


    /**
     * 判断P 是否在AB所在直线上, 计算叉乘 (A - P) ^ (B - P)
     * @return 0 在直线上
     */
    private static double cross(Point P, Point A, Point B) {
        return new Point(A.minus(P)).crossMulti(B.minus(P));
    }


    private static boolean isInPolygon(Point q) {
        if (polygons.length == 0) return false;

        int n = polygons.length;
        int j = n - 1;
        for (int i = 0; i < n; i++) {
            Point p1 = polygons[i];
            Point p2 = polygons[j];

            if (onSegment(q, p1, p2)) { // 测试点Q在边上
                return true;
            }
            j = i;

            /// 1、判断测试点是否在最外侧两条向量外面
            // dcmp((polygon[n - 1]-polygon[0])^(Q-polygon[0]))<=0 || dcmp((polygon[1]-polygon[0])^(P-polygon[0])) >= 0
            if (dcmp(polygons[n - 1].minus(polygons[0]).crossMulti(q.minus(polygons[0]))) <= 0 ||
                    dcmp(polygons[1].minus(polygons[0]).crossMulti(q.minus(polygons[0]))) >= 0) {
                return false;
            }


            // 2、二分查找测试点在哪个三角形中
            int low = 2, high = n;
            while (low < high) {
                int mid = (low + high + 1) >> 1;
                // dcmp((polygon[mid]-polygon[1])^(P-polygon[1]))<=0
                if (dcmp(polygons[mid - 1].minus(polygons[0]).crossMulti(q.minus(polygons[0]))) <= 0) {
                    low = mid;
                } else {
                    high = mid - 1;
                }
            }

            // 3、判断测试点是否在第三条边外部
            // dcmp((polygon[l+1]-polygon[l])^(P-polygon[l]))>=0
            if (dcmp(polygons[low].minus(polygons[low - 1]).crossMulti(q.minus(polygons[low - 1]))) >= 0) {
                return false;
            }
        }
        return true;
    }


    static class Point {
        private double x, y;

        public Point(double x, double y) {
            this.x = x;
            this.y = y;
        }

        public Point(Point p) {
            this.x = p.x;
            this.y = p.y;
        }

        private Point minus(Point p) {
            return new Point(x - p.x, y - p.y);
        }

        private double dotMulti(Point p) {
            return x * p.x + y * p.y;
        }

        private double crossMulti(Point p) { // p^q > 0, P在Q的顺时针方向;  < 0, P在Q的逆时针方向; = 0, p,q共线,可能同向或反向
            return x * p.y - y * p.x;
        }
    }
}

另外一种写法也是过不了

点击查看代码
private static boolean isInPolygon(Point q) {
	if (polygons.length == 0) return false;

	int n = polygons.length;
	int j = n - 1;
	for (int i = 0; i < n; i++) {
		Point p1 = polygons[i];
		Point p2 = polygons[j];

		if (onSegment(q, p1, p2)) { // 测试点Q在边上
			return true;
		}
		j = i;

		// 1、判断测试点是否在最外侧两条向量外面
		if (cross(polygons[0], polygons[1], q) >= 0 || cross(polygons[0], polygons[n - 1], q) <= 0) {
			return false;
		}

		// 2、二分查找测试点在哪个三角形中
		int low = 2, high = n - 1;
		while (low < high) {
			int mid = low + (high - low) / 2;
			if (cross(polygons[0], polygons[mid], q) > 0) {
				high = mid;
			} else {
				low = mid + 1;
			}
		}

		// 3、判断测试点是否在第三条边外部
		if (cross(polygons[low], polygons[low - 1], q) <= 0) {
			return false;
		}
	}
	return true;
}

具体原因等有时间再研究一下,如果哪位朋友看出哪里有问题,帮忙指出

参考

Point-In-Polygon Algorithm — Determining Whether A Point Is Inside A Complex Polygon
Point in polygon
详谈判断点在多边形内的七种方法
hrbust 1429:凸多边形(计算几何,判断点是否在多边形内,二分法)
ACM-计算几何之凸多边形——hrbust1429