Java 在图中查找人脸的算法
Java algorithm for finding faces in a graph
我有一个平面图,是我自己创建的。我想找到这张图的面孔,但找不到可行的算法。到目前为止,我所做的是使用一种算法来查找图中的所有循环,但这给了我所有可能的循环,我已经尝试但没有找到一种方法来只对面孔进行排序。我的一个想法是使用 Path2Ds contains
方法来查看另一个形状是否重叠,但是由于面共享节点,所以这是行不通的。下图展示了我想要的,之后的代码展示了我的可重现示例。
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class PolygonFinder {
// Graph modeled as list of edges
static int[][] graph
= {
{1, 2}, {1, 6}, {1, 5}, {2, 6},
{2, 3}, {3, 7}, {7, 4}, {3, 4},
{5, 4}, {6, 5}
};
static List<int[]> cycles = new ArrayList<>();
/**
* @param args
*/
public static void main(String[] args) {
for (int[] graph1 : graph) {
for (int j = 0; j < graph1.length; j++) {
findNewCycles(new int[]{graph1[j]});
}
}
cycles.stream().map(cy -> {
String s = "" + cy[0];
for (int i = 1; i < cy.length; i++) {
s += "," + cy[i];
}
return s;
}).forEachOrdered(s -> {
System.out.println(s);
});
}
static void findNewCycles(int[] path) {
int n = path[0];
int x;
int[] sub = new int[path.length + 1];
for (int[] graph1 : graph) {
for (int y = 0; y <= 1; y++) {
if (graph1[y] == n) {
x = graph1[(y + 1) % 2];
if (!visited(x, path)) // neighbor node not on path yet
{
sub[0] = x;
System.arraycopy(path, 0, sub, 1, path.length);
// explore extended path
findNewCycles(sub);
} else if ((path.length > 2) && (x == path[path.length - 1])) // cycle found
{
int[] p = normalize(path);
int[] inv = invert(p);
if (isNew(p) && isNew(inv)) {
cycles.add(p);
}
}
}
}
}
}
// check of both arrays have same lengths and contents
static Boolean equals(int[] a, int[] b) {
Boolean ret = (a[0] == b[0]) && (a.length == b.length);
for (int i = 1; ret && (i < a.length); i++) {
if (a[i] != b[i]) {
ret = false;
}
}
return ret;
}
// create a path array with reversed order
static int[] invert(int[] path) {
int[] p = new int[path.length];
for (int i = 0; i < path.length; i++) {
p[i] = path[path.length - 1 - i];
}
return normalize(p);
}
// rotate cycle path such that it begins with the smallest node
static int[] normalize(int[] path) {
int[] p = new int[path.length];
int x = smallest(path);
int n;
System.arraycopy(path, 0, p, 0, path.length);
while (p[0] != x) {
n = p[0];
System.arraycopy(p, 1, p, 0, p.length - 1);
p[p.length - 1] = n;
}
return p;
}
// compare path against known cycles
// return true, iff path is not a known cycle
static Boolean isNew(int[] path) {
Boolean ret = true;
for (int[] p : cycles) {
if (equals(p, path)) {
ret = false;
break;
}
}
return ret;
}
// return the int of the array which is the smallest
static int smallest(int[] path) {
int min = path[0];
for (int p : path) {
if (p < min) {
min = p;
}
}
return min;
}
// check if vertex n is contained in path
static Boolean visited(int n, int[] path) {
Boolean ret = false;
for (int p : path) {
if (p == n) {
ret = true;
break;
}
}
return ret;
}
}
以上代码运行后的结果为:
1,6,2
1,5,6,2
1,5,4,7,3,2
1,6,5,4,7,3,2
1,5,4,3,2
1,6,5,4,3,2
1,5,4,7,3,2,6
1,5,4,3,2,6
1,5,6
2,3,7,4,5,6
2,3,4,5,6
3,4,7
我解决这个问题的最佳尝试之一是使用以下代码。坐标来自上图
List<Polygon> polys = new LinkedList<>();
Polygon p1 = new Polygon();
p1.addPoint(new Point2D.Double(-4, 4));
p1.addPoint(new Point2D.Double(-1, 3));
p1.addPoint(new Point2D.Double(-1, 5));
Polygon p2 = new Polygon();
p2.addPoint(new Point2D.Double(-4, 4));
p2.addPoint(new Point2D.Double(0, -2));
p2.addPoint(new Point2D.Double(-1, 3));
p2.addPoint(new Point2D.Double(-1, 5));
Polygon p3 = new Polygon();
p3.addPoint(new Point2D.Double(-4, 4));
p3.addPoint(new Point2D.Double(0, -2));
p3.addPoint(new Point2D.Double(4, 1));
p3.addPoint(new Point2D.Double(2, 2));
p3.addPoint(new Point2D.Double(3, 4));
p3.addPoint(new Point2D.Double(-1, 5));
Polygon p4 = new Polygon();
p4.addPoint(new Point2D.Double(-4, 4));
p4.addPoint(new Point2D.Double(-1, 3));
p4.addPoint(new Point2D.Double(0, -2));
p4.addPoint(new Point2D.Double(4, 1));
p4.addPoint(new Point2D.Double(2, 2));
p4.addPoint(new Point2D.Double(3, 4));
p4.addPoint(new Point2D.Double(-1, 5));
Polygon p5 = new Polygon();
p5.addPoint(new Point2D.Double(-4, 4));
p5.addPoint(new Point2D.Double(0, -2));
p5.addPoint(new Point2D.Double(4, 1));
p5.addPoint(new Point2D.Double(3, 4));
p5.addPoint(new Point2D.Double(-1, 5));
Polygon p6 = new Polygon();
p6.addPoint(new Point2D.Double(-4, 4));
p6.addPoint(new Point2D.Double(-1, 3));
p6.addPoint(new Point2D.Double(0, -2));
p6.addPoint(new Point2D.Double(4, 1));
p6.addPoint(new Point2D.Double(3, 4));
p6.addPoint(new Point2D.Double(-1, 5));
Polygon p7 = new Polygon();
p7.addPoint(new Point2D.Double(-4, 4));
p7.addPoint(new Point2D.Double(0, -2));
p7.addPoint(new Point2D.Double(4, 1));
p7.addPoint(new Point2D.Double(2, 2));
p7.addPoint(new Point2D.Double(3, 4));
p7.addPoint(new Point2D.Double(-1, 5));
p7.addPoint(new Point2D.Double(-1, 3));
Polygon p8 = new Polygon();
p8.addPoint(new Point2D.Double(-4, 4));
p8.addPoint(new Point2D.Double(0, -2));
p8.addPoint(new Point2D.Double(4, 1));
p8.addPoint(new Point2D.Double(3, 4));
p8.addPoint(new Point2D.Double(-1, 5));
p8.addPoint(new Point2D.Double(-1, 3));
Polygon p9 = new Polygon();
p9.addPoint(new Point2D.Double(-4, 4));
p9.addPoint(new Point2D.Double(0, -2));
p9.addPoint(new Point2D.Double(-1, 3));
Polygon p10 = new Polygon();
p10.addPoint(new Point2D.Double(-1, 5));
p10.addPoint(new Point2D.Double(3, 4));
p10.addPoint(new Point2D.Double(2, 2));
p10.addPoint(new Point2D.Double(4, 1));
p10.addPoint(new Point2D.Double(0, -2));
p10.addPoint(new Point2D.Double(-1, 3));
Polygon p11 = new Polygon();
p11.addPoint(new Point2D.Double(-1, 5));
p11.addPoint(new Point2D.Double(3, 4));
p11.addPoint(new Point2D.Double(4, 1));
p11.addPoint(new Point2D.Double(0, -2));
p11.addPoint(new Point2D.Double(-1, 3));
Polygon p12 = new Polygon();
p12.addPoint(new Point2D.Double(3, 4));
p12.addPoint(new Point2D.Double(4, 1));
p12.addPoint(new Point2D.Double(2, 2));
polys.add(p1);
polys.add(p2);
polys.add(p3);
polys.add(p4);
polys.add(p5);
polys.add(p6);
polys.add(p7);
polys.add(p8);
polys.add(p9);
polys.add(p10);
polys.add(p11);
polys.add(p12);
Set<Integer> toRemove = new HashSet<>();
for (Polygon polyI : polys) {
for (Polygon polyJ : polys) {
if (polyI.equals(polyJ)) {
continue;
}
if (polyI.contains(polyJ)) {
toRemove.add(polys.indexOf(polyI));
}
}
}
List<Integer> list = new LinkedList<>(toRemove);
Collections.sort(list);
Collections.reverse(list);
list.forEach((t) -> {
polys.remove(t.intValue());
});
System.out.println("");
polys.forEach((t) -> {
System.out.println(t.getPoints());
});
此处列出了使用的多边形方法。
@Override
public boolean contains(Point2D point) {
return getPath().contains(point);
}
@Override
public boolean contains(IPolygon polygon) {
List<Point2D> p2Points = polygon.getPoints();
for (Point2D point : p2Points) {
if (getPath().contains(point)) {
if (!points.contains(point)) {
return true;
}
}
}
return false;
}
private Path2D getPath() {
Path2D path = new Path2D.Double();
path.moveTo(points.get(0).getX(), points.get(0).getY());
for (int i = 1; i < points.size(); i++) {
path.lineTo(points.get(i).getX(), points.get(i).getY());
}
path.closePath();
return path;
}
此代码给出了以下结果,不需要第 2-4 个。
[Point2D.Double[-4.0, 4.0], Point2D.Double[-1.0, 3.0], Point2D.Double[-1.0, 5.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[0.0, -2.0], Point2D.Double[-1.0, 3.0], Point2D.Double[-1.0, 5.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[-1.0, 3.0], Point2D.Double[0.0, -2.0], Point2D.Double[4.0, 1.0], Point2D.Double[2.0, 2.0], Point2D.Double[3.0, 4.0], Point2D.Double[-1.0, 5.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[0.0, -2.0], Point2D.Double[4.0, 1.0], Point2D.Double[2.0, 2.0], Point2D.Double[3.0, 4.0], Point2D.Double[-1.0, 5.0], Point2D.Double[-1.0, 3.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[0.0, -2.0], Point2D.Double[-1.0, 3.0]]
[Point2D.Double[-1.0, 5.0], Point2D.Double[3.0, 4.0], Point2D.Double[2.0, 2.0], Point2D.Double[4.0, 1.0], Point2D.Double[0.0, -2.0], Point2D.Double[-1.0, 3.0]]
[Point2D.Double[3.0, 4.0], Point2D.Double[4.0, 1.0], Point2D.Double[2.0, 2.0]]
在仅由直线组成的平面嵌入中,在顶点相交的面的边必须在该节点的所有边中相邻。
因此,如果给定这样一个嵌入,并根据每个顶点的方向对边进行排序,我们就可以轻松地遍历人脸的周边,让边上的每个顶点紧靠边的右侧我们到达了。
作为数据结构,我可能会选择这样的东西:
class Vertex {
Edge edges;
}
class Edge {
Vertex source;
Vertex target;
Edge reverse; // the same edge, seen from the other end
Edge next; // forms a circular linked list, sorted in order of direction
}
然后我们可以像这样迭代人脸的周长:
Edge startingEdge = ...;
Edge currentEdge = startingEdge;
do {
currentEdge = currentEdge.reverse.next;
} while (currentEdge != startingEdge);
要按方向对边进行排序,我们可以利用以下事实:如果 a 在 b 的左侧(从坐标系的原点看),a x b
为负。
boolean left(Point2D.Double a, Point2D.Double b) {
return a.x * b.y - a.y * b.x < 0;
}
我们可以使用简单的插入排序来按方向对边进行排序(这将足够快,因为平面图具有有界的平均节点度,因此边列表将很短)。
这里有一个基于half-edges 思想的人脸识别选项。在高层次上,该方法如下所示:
- 用有向边 (u, v) 和 (v, u) 替换连接两点 u 和 v 的每条边。这些被称为 half-edges.
- 将半边链接在一起,使一条半边链完美地描绘出平面图的一个面。
- 遍历这些链以识别平面图的所有面。
从视觉上看,它看起来像这样。我们将从如下图开始:
并以如下所示的图表结束:
一旦我们有了第二张图,走彩色链就会识别出所有的面孔。
那么,问题是如何准确地确定如何将半边链接在一起。基本思想如下:我们想将边缘链接在一起,以便
- 所有内面的半边沿逆时针方向缠绕(或逆时针方向,或 widdershins,取决于你来自池塘的哪一侧),
- 外表面的半边沿顺时针方向缠绕。
只要我们能想出一个方便的策略来链接这样的东西,我们就可以轻松地将半边粘合在一起以获得我们想要的 属性。有很多方法可以做到这一点,但我想重点介绍的方法是在本地查看每个节点。
假设您有一些节点 X,其邻居是 A、B、C 和 D,如下所示。
在这里,我用纯蓝色标记了离开 X 的半边,用橙色虚线标记了进入 X 的半边。
现在,关注此图中的外向半边 (X, A)。当我们将所有内容连接在一起时,其他一些半边 (_, X) 需要链接到 (X, A) 中。它是哪个边缘?从图中可以看出是半边(B,X),形成部分链(B,X),(X,A)。
同理,关注图中的半边(X,B)。和以前一样,当我们将所有半边连接成链时,我们需要某种方法来确定哪个半边 (_, X) 应该出现在它之前。通过检查,我们可以看到它是 (C, X).
更一般地说,请注意
- (X,A)之前的半边是(B,X)。
- (X,B)之前的半边是(C,X)。
- (X,C)之前的半边是(D,X)。
- (X,D)之前的半边是(A,X)。
看到规律了吗?如果我们按逆时针方向(逆时针方向)对该节点周围的邻居进行排序,则可以找到一条边 (X, Y) 之前的半边,如下所示:假设 Z 是该节点周围逆时针方向的下一个邻居,则半边在 (X, Y) 之前的是半边 (Z, X)。
这为我们提供了一个非常好的策略,可以在满足上述要求的同时将边连接到链中。这是一些伪代码:
For each node v:
Get v's neighbors sorted anticlockwise as u_1, u_2, u_3, ..., u_n
For each half-edge (v, u_i):
Update half-edge (u_{i+1 mod n}, v) to chain to (v, u_i)
至此,我们已将所有内容连接到链中,大功告成!
这里有一些我略过的技术细节需要在您编写代码之前解决。例如:
- 如何逆时针排序节点 v 的邻居?这可以通过使用
Math.atan2(dy, dx)
计算 v 的每个邻居与 v 的角度并根据这些值进行排序来完成。
- 您如何跟踪什么链接到什么?如果您所做的只是识别面孔,则可以创建一个
Map<HalfEdge, HalfEdge>
将每个半边与其后的下一个半边相关联。如果您计划在未来保留链条,您可能希望使链表的每个 HalfEdge
部分引用序列中的下一个半边。
- 如何从一对节点映射到它们之间的半边 运行?这可以通过从节点对到半边的
Map
之类的东西来完成。您还可以构造半边,并让每个半边存储一个指向另一个方向的半边 运行 的指针。
归因:我最初是从this related question on the Computer Science Stack Exchange that asks how to build a doubly-connected edge list(DECL)中从线段集合中学习到这个算法的。我的贡献是简化算法,仅返回识别面部所需的链,并添加一些视觉效果以更好地激发概念。
对于每条边,获取边顶点嵌入中的坐标,并使用它们使用三角函数计算边的角度。
例如从(x1,y1)到(x2, y2) 从正 x 轴逆时针测量由 Math.atan2(y2-y1,x2-x1)
.
给出
对于每个顶点,通过按角度对边进行排序来创建循环边排序。这可以存储为数组,也可以使用循环列表数据结构。
选择一条边,跟随它到相邻顶点,然后跟随下一个相邻的顺时针边,重复跟随边到下一个顶点,然后是下一个顺时针边,直到回到起始边;那么你已经找到了图形的一个面。
重复第 3 步,沿与之前相反的方向选取未访问的边或已访问的边,然后沿相同的顺时针方向跟随它以找到下一个面。重复此操作,直到所有边都被访问过两次(每个方向一次)然后你就找到了所有的面。
在 Java 中,即:
import java.awt.geom.Point2D;
import java.awt.Polygon;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.stream.Collectors;
import java.text.MessageFormat;
public class GraphFaces
{
static class Vertex
{
final int index;
final Point2D point;
final ArrayList<Edge> outboundEdges = new ArrayList<>();
public Vertex( final int index, final Point2D point )
{
this.index = index;
this.point = point;
}
public void addEdge( final Edge edge )
{
this.outboundEdges.add( edge );
}
public void sortEdges()
{
this.outboundEdges.sort((e1,e2)->Double.compare(e1.angle,e2.angle));
Edge prev = this.outboundEdges.get(this.outboundEdges.size() - 1);
for ( final Edge edge: this.outboundEdges )
{
edge.setNextEdge( prev );
prev = edge;
}
}
@Override
public String toString()
{
return Integer.toString(this.index);
// return MessageFormat.format("({0},{1})",this.point.getX(),this.point.getY());
}
}
static class Edge
{
final Vertex from;
final Vertex to;
final double angle;
boolean visited = false;
Edge next = null;
Edge reverse = null;
public Edge( final Vertex from, final Vertex to )
{
this.from = from;
this.to = to;
this.angle = Math.atan2(to.point.getY() - from.point.getY(), to.point.getX() - from.point.getX());
from.addEdge( this );
}
public Vertex getFrom()
{
return this.from;
}
public Vertex getTo()
{
return this.to;
}
public void setNextEdge( final Edge edge )
{
this.next = edge;
}
public void setReverseEdge( final Edge edge )
{
this.reverse = edge;
}
@Override
public String toString()
{
return MessageFormat.format("{0} -> {1}", this.from, this.to);
}
}
public static void main(final String[] args)
{
final Vertex[] vertices = {
new Vertex( 1, new Point2D.Double(-4,+4) ),
new Vertex( 2, new Point2D.Double(-1,+5) ),
new Vertex( 3, new Point2D.Double(+3,+4) ),
new Vertex( 4, new Point2D.Double(+4,+1) ),
new Vertex( 5, new Point2D.Double(+0,-2) ),
new Vertex( 6, new Point2D.Double(-1,+3) ),
new Vertex( 7, new Point2D.Double(+2,+2) )
};
final int[][] graph = {
{1, 2}, {1, 6}, {1, 5}, {2, 6}, {2, 3}, {3, 7}, {7, 4}, {3, 4}, {5, 4}, {6, 5}
};
final Edge[] edges = new Edge[2 * graph.length];
for ( int i = 0; i < graph.length; i++ )
{
final Vertex from = vertices[graph[i][0]-1];
final Vertex to = vertices[graph[i][1]-1];
edges[2*i] = new Edge( from, to );
edges[2*i+1] = new Edge( to, from );
edges[2*i].setReverseEdge(edges[2*i+1]);
edges[2*i+1].setReverseEdge(edges[2*i]);
}
for ( final Vertex vertex: vertices )
{
vertex.sortEdges();
}
final ArrayList<ArrayList<Edge>> faces = new ArrayList<>();
for ( final Edge edge: edges )
{
if ( edge.visited )
{
continue;
}
final ArrayList<Edge> face = new ArrayList<>();
faces.add( face );
Edge e = edge;
do
{
face.add(e);
e.visited = true;
e = e.reverse.next;
}
while (e != edge);
System.out.println( face.stream().map(Edge::getFrom).collect(Collectors.toList()) );
}
}
}
输出:
[1, 2, 3, 4, 5]
[2, 1, 6]
[6, 1, 5]
[2, 6, 5, 4, 7, 3]
[3, 7, 4]
注意:这包括图形的外表面。
或者,如果您想要:测试图形的平面度;生成(双连通)图的所有可能嵌入;并为这些嵌入中的一个(或多个)生成循环边排序,然后您可以使用博士论文 Planarity Testing by Path Addition,其中在附录中包含完整的 Java 源代码。
我有一个平面图,是我自己创建的。我想找到这张图的面孔,但找不到可行的算法。到目前为止,我所做的是使用一种算法来查找图中的所有循环,但这给了我所有可能的循环,我已经尝试但没有找到一种方法来只对面孔进行排序。我的一个想法是使用 Path2Ds contains
方法来查看另一个形状是否重叠,但是由于面共享节点,所以这是行不通的。下图展示了我想要的,之后的代码展示了我的可重现示例。
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
public class PolygonFinder {
// Graph modeled as list of edges
static int[][] graph
= {
{1, 2}, {1, 6}, {1, 5}, {2, 6},
{2, 3}, {3, 7}, {7, 4}, {3, 4},
{5, 4}, {6, 5}
};
static List<int[]> cycles = new ArrayList<>();
/**
* @param args
*/
public static void main(String[] args) {
for (int[] graph1 : graph) {
for (int j = 0; j < graph1.length; j++) {
findNewCycles(new int[]{graph1[j]});
}
}
cycles.stream().map(cy -> {
String s = "" + cy[0];
for (int i = 1; i < cy.length; i++) {
s += "," + cy[i];
}
return s;
}).forEachOrdered(s -> {
System.out.println(s);
});
}
static void findNewCycles(int[] path) {
int n = path[0];
int x;
int[] sub = new int[path.length + 1];
for (int[] graph1 : graph) {
for (int y = 0; y <= 1; y++) {
if (graph1[y] == n) {
x = graph1[(y + 1) % 2];
if (!visited(x, path)) // neighbor node not on path yet
{
sub[0] = x;
System.arraycopy(path, 0, sub, 1, path.length);
// explore extended path
findNewCycles(sub);
} else if ((path.length > 2) && (x == path[path.length - 1])) // cycle found
{
int[] p = normalize(path);
int[] inv = invert(p);
if (isNew(p) && isNew(inv)) {
cycles.add(p);
}
}
}
}
}
}
// check of both arrays have same lengths and contents
static Boolean equals(int[] a, int[] b) {
Boolean ret = (a[0] == b[0]) && (a.length == b.length);
for (int i = 1; ret && (i < a.length); i++) {
if (a[i] != b[i]) {
ret = false;
}
}
return ret;
}
// create a path array with reversed order
static int[] invert(int[] path) {
int[] p = new int[path.length];
for (int i = 0; i < path.length; i++) {
p[i] = path[path.length - 1 - i];
}
return normalize(p);
}
// rotate cycle path such that it begins with the smallest node
static int[] normalize(int[] path) {
int[] p = new int[path.length];
int x = smallest(path);
int n;
System.arraycopy(path, 0, p, 0, path.length);
while (p[0] != x) {
n = p[0];
System.arraycopy(p, 1, p, 0, p.length - 1);
p[p.length - 1] = n;
}
return p;
}
// compare path against known cycles
// return true, iff path is not a known cycle
static Boolean isNew(int[] path) {
Boolean ret = true;
for (int[] p : cycles) {
if (equals(p, path)) {
ret = false;
break;
}
}
return ret;
}
// return the int of the array which is the smallest
static int smallest(int[] path) {
int min = path[0];
for (int p : path) {
if (p < min) {
min = p;
}
}
return min;
}
// check if vertex n is contained in path
static Boolean visited(int n, int[] path) {
Boolean ret = false;
for (int p : path) {
if (p == n) {
ret = true;
break;
}
}
return ret;
}
}
以上代码运行后的结果为:
1,6,2
1,5,6,2
1,5,4,7,3,2
1,6,5,4,7,3,2
1,5,4,3,2
1,6,5,4,3,2
1,5,4,7,3,2,6
1,5,4,3,2,6
1,5,6
2,3,7,4,5,6
2,3,4,5,6
3,4,7
我解决这个问题的最佳尝试之一是使用以下代码。坐标来自上图
List<Polygon> polys = new LinkedList<>();
Polygon p1 = new Polygon();
p1.addPoint(new Point2D.Double(-4, 4));
p1.addPoint(new Point2D.Double(-1, 3));
p1.addPoint(new Point2D.Double(-1, 5));
Polygon p2 = new Polygon();
p2.addPoint(new Point2D.Double(-4, 4));
p2.addPoint(new Point2D.Double(0, -2));
p2.addPoint(new Point2D.Double(-1, 3));
p2.addPoint(new Point2D.Double(-1, 5));
Polygon p3 = new Polygon();
p3.addPoint(new Point2D.Double(-4, 4));
p3.addPoint(new Point2D.Double(0, -2));
p3.addPoint(new Point2D.Double(4, 1));
p3.addPoint(new Point2D.Double(2, 2));
p3.addPoint(new Point2D.Double(3, 4));
p3.addPoint(new Point2D.Double(-1, 5));
Polygon p4 = new Polygon();
p4.addPoint(new Point2D.Double(-4, 4));
p4.addPoint(new Point2D.Double(-1, 3));
p4.addPoint(new Point2D.Double(0, -2));
p4.addPoint(new Point2D.Double(4, 1));
p4.addPoint(new Point2D.Double(2, 2));
p4.addPoint(new Point2D.Double(3, 4));
p4.addPoint(new Point2D.Double(-1, 5));
Polygon p5 = new Polygon();
p5.addPoint(new Point2D.Double(-4, 4));
p5.addPoint(new Point2D.Double(0, -2));
p5.addPoint(new Point2D.Double(4, 1));
p5.addPoint(new Point2D.Double(3, 4));
p5.addPoint(new Point2D.Double(-1, 5));
Polygon p6 = new Polygon();
p6.addPoint(new Point2D.Double(-4, 4));
p6.addPoint(new Point2D.Double(-1, 3));
p6.addPoint(new Point2D.Double(0, -2));
p6.addPoint(new Point2D.Double(4, 1));
p6.addPoint(new Point2D.Double(3, 4));
p6.addPoint(new Point2D.Double(-1, 5));
Polygon p7 = new Polygon();
p7.addPoint(new Point2D.Double(-4, 4));
p7.addPoint(new Point2D.Double(0, -2));
p7.addPoint(new Point2D.Double(4, 1));
p7.addPoint(new Point2D.Double(2, 2));
p7.addPoint(new Point2D.Double(3, 4));
p7.addPoint(new Point2D.Double(-1, 5));
p7.addPoint(new Point2D.Double(-1, 3));
Polygon p8 = new Polygon();
p8.addPoint(new Point2D.Double(-4, 4));
p8.addPoint(new Point2D.Double(0, -2));
p8.addPoint(new Point2D.Double(4, 1));
p8.addPoint(new Point2D.Double(3, 4));
p8.addPoint(new Point2D.Double(-1, 5));
p8.addPoint(new Point2D.Double(-1, 3));
Polygon p9 = new Polygon();
p9.addPoint(new Point2D.Double(-4, 4));
p9.addPoint(new Point2D.Double(0, -2));
p9.addPoint(new Point2D.Double(-1, 3));
Polygon p10 = new Polygon();
p10.addPoint(new Point2D.Double(-1, 5));
p10.addPoint(new Point2D.Double(3, 4));
p10.addPoint(new Point2D.Double(2, 2));
p10.addPoint(new Point2D.Double(4, 1));
p10.addPoint(new Point2D.Double(0, -2));
p10.addPoint(new Point2D.Double(-1, 3));
Polygon p11 = new Polygon();
p11.addPoint(new Point2D.Double(-1, 5));
p11.addPoint(new Point2D.Double(3, 4));
p11.addPoint(new Point2D.Double(4, 1));
p11.addPoint(new Point2D.Double(0, -2));
p11.addPoint(new Point2D.Double(-1, 3));
Polygon p12 = new Polygon();
p12.addPoint(new Point2D.Double(3, 4));
p12.addPoint(new Point2D.Double(4, 1));
p12.addPoint(new Point2D.Double(2, 2));
polys.add(p1);
polys.add(p2);
polys.add(p3);
polys.add(p4);
polys.add(p5);
polys.add(p6);
polys.add(p7);
polys.add(p8);
polys.add(p9);
polys.add(p10);
polys.add(p11);
polys.add(p12);
Set<Integer> toRemove = new HashSet<>();
for (Polygon polyI : polys) {
for (Polygon polyJ : polys) {
if (polyI.equals(polyJ)) {
continue;
}
if (polyI.contains(polyJ)) {
toRemove.add(polys.indexOf(polyI));
}
}
}
List<Integer> list = new LinkedList<>(toRemove);
Collections.sort(list);
Collections.reverse(list);
list.forEach((t) -> {
polys.remove(t.intValue());
});
System.out.println("");
polys.forEach((t) -> {
System.out.println(t.getPoints());
});
此处列出了使用的多边形方法。
@Override
public boolean contains(Point2D point) {
return getPath().contains(point);
}
@Override
public boolean contains(IPolygon polygon) {
List<Point2D> p2Points = polygon.getPoints();
for (Point2D point : p2Points) {
if (getPath().contains(point)) {
if (!points.contains(point)) {
return true;
}
}
}
return false;
}
private Path2D getPath() {
Path2D path = new Path2D.Double();
path.moveTo(points.get(0).getX(), points.get(0).getY());
for (int i = 1; i < points.size(); i++) {
path.lineTo(points.get(i).getX(), points.get(i).getY());
}
path.closePath();
return path;
}
此代码给出了以下结果,不需要第 2-4 个。
[Point2D.Double[-4.0, 4.0], Point2D.Double[-1.0, 3.0], Point2D.Double[-1.0, 5.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[0.0, -2.0], Point2D.Double[-1.0, 3.0], Point2D.Double[-1.0, 5.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[-1.0, 3.0], Point2D.Double[0.0, -2.0], Point2D.Double[4.0, 1.0], Point2D.Double[2.0, 2.0], Point2D.Double[3.0, 4.0], Point2D.Double[-1.0, 5.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[0.0, -2.0], Point2D.Double[4.0, 1.0], Point2D.Double[2.0, 2.0], Point2D.Double[3.0, 4.0], Point2D.Double[-1.0, 5.0], Point2D.Double[-1.0, 3.0]]
[Point2D.Double[-4.0, 4.0], Point2D.Double[0.0, -2.0], Point2D.Double[-1.0, 3.0]]
[Point2D.Double[-1.0, 5.0], Point2D.Double[3.0, 4.0], Point2D.Double[2.0, 2.0], Point2D.Double[4.0, 1.0], Point2D.Double[0.0, -2.0], Point2D.Double[-1.0, 3.0]]
[Point2D.Double[3.0, 4.0], Point2D.Double[4.0, 1.0], Point2D.Double[2.0, 2.0]]
在仅由直线组成的平面嵌入中,在顶点相交的面的边必须在该节点的所有边中相邻。
因此,如果给定这样一个嵌入,并根据每个顶点的方向对边进行排序,我们就可以轻松地遍历人脸的周边,让边上的每个顶点紧靠边的右侧我们到达了。
作为数据结构,我可能会选择这样的东西:
class Vertex {
Edge edges;
}
class Edge {
Vertex source;
Vertex target;
Edge reverse; // the same edge, seen from the other end
Edge next; // forms a circular linked list, sorted in order of direction
}
然后我们可以像这样迭代人脸的周长:
Edge startingEdge = ...;
Edge currentEdge = startingEdge;
do {
currentEdge = currentEdge.reverse.next;
} while (currentEdge != startingEdge);
要按方向对边进行排序,我们可以利用以下事实:如果 a 在 b 的左侧(从坐标系的原点看),a x b
为负。
boolean left(Point2D.Double a, Point2D.Double b) {
return a.x * b.y - a.y * b.x < 0;
}
我们可以使用简单的插入排序来按方向对边进行排序(这将足够快,因为平面图具有有界的平均节点度,因此边列表将很短)。
这里有一个基于half-edges 思想的人脸识别选项。在高层次上,该方法如下所示:
- 用有向边 (u, v) 和 (v, u) 替换连接两点 u 和 v 的每条边。这些被称为 half-edges.
- 将半边链接在一起,使一条半边链完美地描绘出平面图的一个面。
- 遍历这些链以识别平面图的所有面。
从视觉上看,它看起来像这样。我们将从如下图开始:
并以如下所示的图表结束:
一旦我们有了第二张图,走彩色链就会识别出所有的面孔。
那么,问题是如何准确地确定如何将半边链接在一起。基本思想如下:我们想将边缘链接在一起,以便
- 所有内面的半边沿逆时针方向缠绕(或逆时针方向,或 widdershins,取决于你来自池塘的哪一侧),
- 外表面的半边沿顺时针方向缠绕。
只要我们能想出一个方便的策略来链接这样的东西,我们就可以轻松地将半边粘合在一起以获得我们想要的 属性。有很多方法可以做到这一点,但我想重点介绍的方法是在本地查看每个节点。
假设您有一些节点 X,其邻居是 A、B、C 和 D,如下所示。
在这里,我用纯蓝色标记了离开 X 的半边,用橙色虚线标记了进入 X 的半边。
现在,关注此图中的外向半边 (X, A)。当我们将所有内容连接在一起时,其他一些半边 (_, X) 需要链接到 (X, A) 中。它是哪个边缘?从图中可以看出是半边(B,X),形成部分链(B,X),(X,A)。
同理,关注图中的半边(X,B)。和以前一样,当我们将所有半边连接成链时,我们需要某种方法来确定哪个半边 (_, X) 应该出现在它之前。通过检查,我们可以看到它是 (C, X).
更一般地说,请注意
- (X,A)之前的半边是(B,X)。
- (X,B)之前的半边是(C,X)。
- (X,C)之前的半边是(D,X)。
- (X,D)之前的半边是(A,X)。
看到规律了吗?如果我们按逆时针方向(逆时针方向)对该节点周围的邻居进行排序,则可以找到一条边 (X, Y) 之前的半边,如下所示:假设 Z 是该节点周围逆时针方向的下一个邻居,则半边在 (X, Y) 之前的是半边 (Z, X)。
这为我们提供了一个非常好的策略,可以在满足上述要求的同时将边连接到链中。这是一些伪代码:
For each node v:
Get v's neighbors sorted anticlockwise as u_1, u_2, u_3, ..., u_n
For each half-edge (v, u_i):
Update half-edge (u_{i+1 mod n}, v) to chain to (v, u_i)
至此,我们已将所有内容连接到链中,大功告成!
这里有一些我略过的技术细节需要在您编写代码之前解决。例如:
- 如何逆时针排序节点 v 的邻居?这可以通过使用
Math.atan2(dy, dx)
计算 v 的每个邻居与 v 的角度并根据这些值进行排序来完成。 - 您如何跟踪什么链接到什么?如果您所做的只是识别面孔,则可以创建一个
Map<HalfEdge, HalfEdge>
将每个半边与其后的下一个半边相关联。如果您计划在未来保留链条,您可能希望使链表的每个HalfEdge
部分引用序列中的下一个半边。 - 如何从一对节点映射到它们之间的半边 运行?这可以通过从节点对到半边的
Map
之类的东西来完成。您还可以构造半边,并让每个半边存储一个指向另一个方向的半边 运行 的指针。
归因:我最初是从this related question on the Computer Science Stack Exchange that asks how to build a doubly-connected edge list(DECL)中从线段集合中学习到这个算法的。我的贡献是简化算法,仅返回识别面部所需的链,并添加一些视觉效果以更好地激发概念。
对于每条边,获取边顶点嵌入中的坐标,并使用它们使用三角函数计算边的角度。
例如从(x1,y1)到(x2, y2) 从正 x 轴逆时针测量由
给出Math.atan2(y2-y1,x2-x1)
.对于每个顶点,通过按角度对边进行排序来创建循环边排序。这可以存储为数组,也可以使用循环列表数据结构。
选择一条边,跟随它到相邻顶点,然后跟随下一个相邻的顺时针边,重复跟随边到下一个顶点,然后是下一个顺时针边,直到回到起始边;那么你已经找到了图形的一个面。
重复第 3 步,沿与之前相反的方向选取未访问的边或已访问的边,然后沿相同的顺时针方向跟随它以找到下一个面。重复此操作,直到所有边都被访问过两次(每个方向一次)然后你就找到了所有的面。
在 Java 中,即:
import java.awt.geom.Point2D;
import java.awt.Polygon;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.stream.Collectors;
import java.text.MessageFormat;
public class GraphFaces
{
static class Vertex
{
final int index;
final Point2D point;
final ArrayList<Edge> outboundEdges = new ArrayList<>();
public Vertex( final int index, final Point2D point )
{
this.index = index;
this.point = point;
}
public void addEdge( final Edge edge )
{
this.outboundEdges.add( edge );
}
public void sortEdges()
{
this.outboundEdges.sort((e1,e2)->Double.compare(e1.angle,e2.angle));
Edge prev = this.outboundEdges.get(this.outboundEdges.size() - 1);
for ( final Edge edge: this.outboundEdges )
{
edge.setNextEdge( prev );
prev = edge;
}
}
@Override
public String toString()
{
return Integer.toString(this.index);
// return MessageFormat.format("({0},{1})",this.point.getX(),this.point.getY());
}
}
static class Edge
{
final Vertex from;
final Vertex to;
final double angle;
boolean visited = false;
Edge next = null;
Edge reverse = null;
public Edge( final Vertex from, final Vertex to )
{
this.from = from;
this.to = to;
this.angle = Math.atan2(to.point.getY() - from.point.getY(), to.point.getX() - from.point.getX());
from.addEdge( this );
}
public Vertex getFrom()
{
return this.from;
}
public Vertex getTo()
{
return this.to;
}
public void setNextEdge( final Edge edge )
{
this.next = edge;
}
public void setReverseEdge( final Edge edge )
{
this.reverse = edge;
}
@Override
public String toString()
{
return MessageFormat.format("{0} -> {1}", this.from, this.to);
}
}
public static void main(final String[] args)
{
final Vertex[] vertices = {
new Vertex( 1, new Point2D.Double(-4,+4) ),
new Vertex( 2, new Point2D.Double(-1,+5) ),
new Vertex( 3, new Point2D.Double(+3,+4) ),
new Vertex( 4, new Point2D.Double(+4,+1) ),
new Vertex( 5, new Point2D.Double(+0,-2) ),
new Vertex( 6, new Point2D.Double(-1,+3) ),
new Vertex( 7, new Point2D.Double(+2,+2) )
};
final int[][] graph = {
{1, 2}, {1, 6}, {1, 5}, {2, 6}, {2, 3}, {3, 7}, {7, 4}, {3, 4}, {5, 4}, {6, 5}
};
final Edge[] edges = new Edge[2 * graph.length];
for ( int i = 0; i < graph.length; i++ )
{
final Vertex from = vertices[graph[i][0]-1];
final Vertex to = vertices[graph[i][1]-1];
edges[2*i] = new Edge( from, to );
edges[2*i+1] = new Edge( to, from );
edges[2*i].setReverseEdge(edges[2*i+1]);
edges[2*i+1].setReverseEdge(edges[2*i]);
}
for ( final Vertex vertex: vertices )
{
vertex.sortEdges();
}
final ArrayList<ArrayList<Edge>> faces = new ArrayList<>();
for ( final Edge edge: edges )
{
if ( edge.visited )
{
continue;
}
final ArrayList<Edge> face = new ArrayList<>();
faces.add( face );
Edge e = edge;
do
{
face.add(e);
e.visited = true;
e = e.reverse.next;
}
while (e != edge);
System.out.println( face.stream().map(Edge::getFrom).collect(Collectors.toList()) );
}
}
}
输出:
[1, 2, 3, 4, 5] [2, 1, 6] [6, 1, 5] [2, 6, 5, 4, 7, 3] [3, 7, 4]
注意:这包括图形的外表面。
或者,如果您想要:测试图形的平面度;生成(双连通)图的所有可能嵌入;并为这些嵌入中的一个(或多个)生成循环边排序,然后您可以使用博士论文 Planarity Testing by Path Addition,其中在附录中包含完整的 Java 源代码。