任何真正通用的有效方法都会标准化形状的表示,以使形状不会随着内部表示的旋转,平移,反射或微不足道的变化而改变。
一种方法是从一端开始,将每个连接的形状列出为边长和(有符号)角的交替序列。(在没有零长度的边缘或直角的意义上,形状应该是“干净的”。)要使该常数在反射下不变,如果第一个非零角为负,则将所有角取反。
(由于任何连接的n个顶点的折线将具有n -1个以n -2角分隔的边,我发现在R
下面的代码中使用由两个数组组成的数据结构比较方便,一个数组用于边长,另一个数组用于边长$lengths
。角度$angles
。线段将没有角度在所有的,所以它来处理零长度数组在这样的数据结构是很重要的。)
这样的表示可以按字典顺序排序。 应该对在标准化过程中累积的浮点错误留有余地。一个优雅的过程将根据原始坐标来估计这些误差。在下面的解决方案中,使用了一种更简单的方法,其中两个长度在相对基础上相差很小时,被视为相等。 角度在绝对基础上可能仅相差很小。
要使它们在基本方向的反转下保持不变,请在折线的折线及其反转之间选择词典上最早的表示形式。
要处理多部分折线,请按字典顺序排列其组成部分。
为了找到欧几里得变换下的等价类,
创建形状的标准化表示。
对标准表示法进行字典编排。
遍历排序顺序以标识相等表示的序列。
计算时间与O(n * log(n)* N)成比例,其中n是要素数量,N是任何要素中最大的顶点数量。 这是有效的。
值得一提的是,基于容易计算的不变几何属性(例如折线长度,中心和围绕该中心的弯矩)的初步分组通常可用于简化整个过程。只需在每个这样的初步组中找到全等特征的子组。对于形状将非常需要此处给出的完整方法,否则形状将非常相似,以至于这种简单的不变式仍然无法区分它们。例如,由栅格数据构造的简单要素可能具有此类特征。但是,由于这里给出的解决方案仍然非常有效,因此如果要努力实现它,那么它本身就可以很好地工作。
例
左图显示了五条折线,外加15条折线,这些折线是通过随机平移,旋转,反射和内部方向反转(不可见)获得的。右图根据欧几里得当量类别为它们着色:所有相同颜色的图都相同。不同的颜色不一致。
R
代码如下。 当输入更新为500个形状,500个额外的(全等)形状(每个形状平均100个顶点)时,此机器上的执行时间为3秒。
这段代码是不完整的:因为R
没有本地的字典排序,而且我不希望自己从头开始编码,所以我只是对每个标准化形状的第一个坐标进行排序。这对于此处创建的随机形状将是很好的,但是对于生产工作,应实施完整的词典编目排序。该功能order.shape
将是唯一受此更改影响的功能。它的输入是标准化形状的列表,s
其输出是将其排序的索引序列s
。
#
# Create random shapes.
#
n.shapes <- 5 # Unique shapes, up to congruence
n.shapes.new <- 15 # Additional congruent shapes to generate
p.mean <- 5 # Expected number of vertices per shape
set.seed(17) # Create a reproducible starting point
shape.random <- function(n) matrix(rnorm(2*n), nrow=2, ncol=n)
shapes <- lapply(2+rpois(n.shapes, p.mean-2), shape.random)
#
# Randomly move them around.
#
move.random <- function(xy) {
a <- runif(1, 0, 2*pi)
reflection <- sign(runif(1, -1, 1))
translation <- runif(2, -8, 8)
m <- matrix(c(cos(a), sin(a), -sin(a), cos(a)), 2, 2) %*%
matrix(c(reflection, 0, 0, 1), 2, 2)
m <- m %*% xy + translation
if (runif(1, -1, 0) < 0) m <- m[ ,dim(m)[2]:1]
return (m)
}
i <- sample(length(shapes), n.shapes.new, replace=TRUE)
shapes <- c(shapes, lapply(i, function(j) move.random(shapes[[j]])))
#
# Plot the shapes.
#
range.shapes <- c(min(sapply(shapes, min)), max(sapply(shapes, max)))
palette(gray.colors(length(shapes)))
par(mfrow=c(1,2))
plot(range.shapes, range.shapes, type="n",asp=1, bty="n", xlab="", ylab="")
invisible(lapply(1:length(shapes), function(i) lines(t(shapes[[i]]), col=i, lwd=2)))
#
# Standardize the shape description.
#
standardize <- function(xy) {
n <- dim(xy)[2]
vectors <- xy[ ,-1, drop=FALSE] - xy[ ,-n, drop=FALSE]
lengths <- sqrt(colSums(vectors^2))
if (which.min(lengths - rev(lengths))*2 < n) {
lengths <- rev(lengths)
vectors <- vectors[, (n-1):1]
}
if (n > 2) {
vectors <- vectors / rbind(lengths, lengths)
perps <- rbind(-vectors[2, ], vectors[1, ])
angles <- sapply(1:(n-2), function(i) {
cosine <- sum(vectors[, i+1] * vectors[, i])
sine <- sum(perps[, i+1] * vectors[, i])
atan2(sine, cosine)
})
i <- min(which(angles != 0))
angles <- sign(angles[i]) * angles
} else angles <- numeric(0)
list(lengths=lengths, angles=angles)
}
shapes.std <- lapply(shapes, standardize)
#
# Sort lexicographically. (Not implemented: see the text.)
#
order.shape <- function(s) {
order(sapply(s, function(s) s$lengths[1]))
}
i <- order.shape(shapes.std)
#
# Group.
#
equal.shape <- function(s.0, s.1) {
same.length <- function(a,b) abs(a-b) <= (a+b) * 1e-8
same.angle <- function(a,b) min(abs(a-b), abs(a-b)-2*pi) < 1e-11
r <- function(u) {
a <- u$angles
if (length(a) > 0) {
a <- rev(u$angles)
i <- min(which(a != 0))
a <- sign(a[i]) * a
}
list(lengths=rev(u$lengths), angles=a)
}
e <- function(u, v) {
if (length(u$lengths) != length(v$lengths)) return (FALSE)
all(mapply(same.length, u$lengths, v$lengths)) &&
all(mapply(same.angle, u$angles, v$angles))
}
e(s.0, s.1) || e(r(s.0), s.1)
}
g <- rep(1, length(shapes.std))
for (j in 2:length(i)) {
i.0 <- i[j-1]
i.1 <- i[j]
if (equal.shape(shapes.std[[i.0]], shapes.std[[i.1]]))
g[j] <- g[j-1] else g[j] <- g[j-1]+1
}
palette(rainbow(max(g)))
plot(range.shapes, range.shapes, type="n",asp=1, bty="n", xlab="", ylab="")
invisible(lapply(1:length(i), function(j) lines(t(shapes[[i[j]]]), col=g[j], lwd=2)))