Comment mesurer la similitude des objets SpatialLines

9

J'ai créé deux SpatialLinesobjets R: figure.

Ces objets ont été créés de cette façon:

library(sp)
xy <- cbind(x,y)
xy.sp = sp::SpatialPoints(xy)
spl1 <- sp::SpatialLines(list(Lines(Line(xy.sp), ID="a")))

Maintenant, je veux en quelque sorte conclure qu'il s'agit de la même ligne tournée et inversée, et que la différence entre eux est égale à 0 (c'est-à-dire que la forme est égale).

Pour ce faire, on peut utiliser le maptoolspackage et faire pivoter la ligne # 1, par exemple:

spl180 <- maptools::elide(spl1, rotate=180)

Chaque ligne pivotée doit ensuite être vérifiée par rapport à la ligne n ° 2 à l'aide du rgeospackage, par exemple:

hdist <- rgeos::gDistance(spl180, spl2, byid=FALSE, hausdorff=TRUE)

Cependant, c'est un moyen si coûteux en termes de calcul de faire correspondre des SpatialLinesobjets, surtout si le nombre d'objets est d'environ 1000.

Existe-t-il un moyen intelligent de faire ce travail?

PS De plus, l'approche décrite ci-dessus ne garantit pas toutes les rotations et retournements possibles.

P.S2. Si la ligne # 1 est dézoomée par rapport à la ligne # 2, la différence entre la ligne # 1 et # 2 doit toujours être égale à 0.

MISE À JOUR:

entrez la description de l'image ici

Klausos Klausos
la source

Réponses:

9

Toute méthode efficace vraiment polyvalente normalisera les représentations des formes afin qu'elles ne changent pas lors de la rotation, de la translation, de la réflexion ou de changements triviaux dans la représentation interne.

Une façon de procéder consiste à répertorier chaque forme connectée sous la forme d'une séquence alternée de longueurs de bord et d'angles (signés), en commençant par une extrémité. (La forme doit être "propre" dans le sens de ne pas avoir d'arêtes de longueur nulle ou d'angles droits.) Pour rendre cet invariant en réflexion, annulez tous les angles si le premier non nul est négatif.

(Parce que toute polyligne connectée de n sommets aura n -1 arêtes séparées par n -2 angles, j'ai trouvé pratique dans le Rcode ci-dessous d'utiliser une structure de données composée de deux tableaux, l'un pour les longueurs d'arête $lengthset l'autre pour le angles $angles. Un segment de ligne n'aura aucun angle, il est donc important de gérer des tableaux de longueur nulle dans une telle structure de données.)

Ces représentations peuvent être ordonnées lexicographiquement. Il convient de tenir compte des erreurs à virgule flottante accumulées au cours du processus de normalisation. Une procédure élégante estimerait ces erreurs en fonction des coordonnées d'origine. Dans la solution ci-dessous, une méthode plus simple est utilisée dans laquelle deux longueurs sont considérées comme égales lorsqu'elles diffèrent d'une très petite quantité sur une base relative. Les angles ne peuvent différer que d'une très petite quantité sur une base absolue.

Pour les rendre invariants sous inversion de l'orientation sous-jacente, choisissez la représentation lexicographiquement la plus ancienne entre celle de la polyligne et son inversion.

Pour gérer les polylignes en plusieurs parties, organisez leurs composants dans l'ordre lexicographique.

Pour trouver les classes d'équivalence sous les transformations euclidiennes, alors,

  • Créez des représentations standardisées des formes.

  • Effectuer une sorte lexicographique des représentations standardisées.

  • Effectuez un passage dans l'ordre trié pour identifier des séquences de représentations égales.

Le temps de calcul est proportionnel à O (n * log (n) * N) où n est le nombre d'entités et N est le plus grand nombre de sommets d'une entité. C'est efficace.

Il vaut probablement la peine de mentionner en passant qu'un regroupement préliminaire basé sur des propriétés géométriques invariantes faciles à calculer, telles que la longueur de la polyligne, le centre et les moments autour de ce centre, peut souvent être appliqué pour rationaliser l'ensemble du processus. Il suffit de trouver des sous-groupes de caractéristiques congruentes dans chacun de ces groupes préliminaires. La méthode complète donnée ici serait nécessaire pour des formes qui autrement seraient si remarquablement similaires que de tels invariants simples ne les distingueraient toujours pas. Les entités simples construites à partir de données raster peuvent avoir de telles caractéristiques, par exemple. Cependant, étant donné que la solution donnée ici est si efficace de toute façon, que si l'on va faire l'effort de la mettre en œuvre, cela pourrait très bien fonctionner tout seul.


Exemple

La figure de gauche montre cinq polylignes plus 15 autres qui ont été obtenues à partir de celles-ci par translation aléatoire, rotation, réflexion et inversion de l'orientation interne (qui n'est pas visible). La figure de droite les colore selon leur classe d'équivalence euclidienne: toutes les figures d'une couleur commune sont congruentes; différentes couleurs ne sont pas congruentes.

Figure

Rle code suit. Lorsque les entrées ont été mises à jour en 500 formes, 500 formes supplémentaires (congruentes), avec une moyenne de 100 sommets par forme, le temps d'exécution sur cette machine était de 3 secondes.

Ce code est incomplet: parce qu'il Rn'a pas de tri lexicographique natif, et que je n'ai pas envie d'en coder un à partir de zéro, j'effectue simplement le tri sur la première coordonnée de chaque forme standardisée. Ce sera bien pour les formes aléatoires créées ici, mais pour le travail de production, un tri lexicographique complet devrait être implémenté. La fonction order.shapeserait la seule affectée par ce changement. Son entrée est une liste de formes normalisées set sa sortie est la séquence d'index dans slaquelle elle serait triée.

#
# 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)))
whuber
la source
Lorsque l'on inclut des dilatations arbitraires (ou "isothéties") dans le groupe de transformations, les classes d'équivalence sont les classes de congruence de la géométrie affine . Cette complication est facile à gérer: standardisez toutes les polylignes pour avoir une longueur totale unitaire, par exemple.
whuber
Merci beaucoup. Une seule question: les formes doivent-elles être représentées comme des lignes spatiales ou des polygones spatiaux?
Klausos Klausos
Les polygones créent une autre complication: leurs limites n'ont pas de point final défini. Il existe de nombreuses façons de gérer cela, telles que la standardisation de la représentation pour commencer (disons) le sommet qui trie en premier dans l'ordre lexicographique xy et en procédant dans le sens antihoraire autour du polygone. (Un polygone connecté topologiquement "propre" n'aura qu'un seul tel sommet.) Le fait qu'une forme soit considérée comme un polygone ou une polyligne dépend du type d'entité qu'elle représente: il n'y a aucun moyen intrinsèque de dire d'une liste fermée de points si elle est destiné à être une polyligne ou un polygone.
whuber
Désolé pour une question simple, mais je devrais lui demander de comprendre votre exemple. Votre objet formes.std a à la fois $ longueurs et $ angles. Si, cependant, j'exécute ce code sur mes données xy (par exemple [1,] 3093.5 -2987.8 [2,] 3072.7 -2991.0 etc.), il n'évalue pas les angles, ni ne dessine de formes. Si je lance l'intrigue (formes [[1]]), alors je peux voir clairement ma polyligne. Alors, comment dois-je enregistrer des polylignes dans R pour pouvoir tester votre code sur mes données?
Klausos Klausos
J'ai commencé avec la même structure de données que vous avez fait: un tableau de coordonnées (x, y). Mes tableaux mettent ces coordonnées en colonnes (comme si vous les aviez utilisées à la rbind(x,y)place de cbind(x,y)). C'est tout ce dont vous avez besoin: la spbibliothèque n'est pas utilisée. Si vous souhaitez suivre ce qui se fait en détail, je vous suggère de commencer avec, par exemple, n.shapes <- 2, n.shapes.new <- 3et p.mean <- 1. Ensuite shapes, shapes.stdetc. sont tous suffisamment petits pour être facilement inspectés. La manière élégante - et "correcte" de gérer tout cela serait de créer une classe de représentations d'entités standardisées.
whuber
1

Vous en demandez beaucoup avec une rotation et une dilatation arbitraires! Je ne sais pas à quel point la distance Hausdorff serait utile, mais vérifiez-la. Mon approche consisterait à réduire le nombre de cas à vérifier via des données bon marché. Par exemple, vous pouvez ignorer les comparaisons coûteuses si la longueur des deux chaînes de lignes n'est pas un rapport entier (en supposant une mise à l'échelle entière / graduée ). Vous pouvez également vérifier si la zone du cadre de délimitation ou leurs zones de coque convexes sont dans un bon rapport. Je suis sûr qu'il y a beaucoup de contrôles bon marché que vous pouvez faire contre le centroïde, comme les distances ou les angles de début / fin.

Alors seulement, si vous détectez la mise à l'échelle, annulez-la et effectuez les vérifications vraiment coûteuses.

Clarification: je ne connais pas les packages que vous utilisez. Par rapport entier, je voulais dire que vous devriez diviser les deux distances, vérifier si le résultat est un entier, sinon, inverser cette valeur (vous pourriez avoir choisi le mauvais ordre) et revérifier. Si vous obtenez un entier ou suffisamment proche, vous pouvez en déduire qu'il y avait peut-être une mise à l'échelle en cours. Ou il pourrait simplement s'agir de deux formes différentes.

En ce qui concerne le cadre de sélection, vous avez probablement obtenu des points opposés du rectangle qui le représente, donc en extraire la zone est une simple arithmétique. Le principe derrière la comparaison des ratios est le même, juste que le résultat serait au carré. Ne vous embêtez pas avec des coques convexes si vous ne pouvez pas les retirer de ce package R, c'était juste une idée (probablement pas assez bon marché de toute façon).

lynxlynxlynx
la source
Merci beaucoup. Pourriez-vous expliquer comment détecter si la longueur des deux chaînes de lignes n'est pas un rapport entier? En outre, j'apprécie beaucoup si vous pouvez donner un exemple de vérification "si la zone du cadre de délimitation ou les zones de coque convexes sont dans un bon rapport"
Klausos Klausos
Par exemple, si j'extrais un cadre de délimitation spatiale à partir de données spatiales, je ne reçois alors que deux points: spl <- sp :: SpatialLines (list (Lines (Line (Line (xy.sp), ID = i))) b <- bbox ( spl)
Klausos Klausos
Extension du poste principal.
lynxlynxlynx
"Si vous obtenez un nombre entier ou assez proche, vous pouvez en déduire qu'il y avait peut-être une mise à l'échelle en cours." Un utilisateur n'aurait-il pas pu appliquer une échelle de 1,4 environ?
Germán Carrillo
Bien sûr, mais mon hypothèse était claire, surtout avec les modifications ultérieures. J'imaginais un zoom de style webmap, où l'un est bien limité.
lynxlynxlynx
1

Une bonne méthode pour comparer ces polylignes serait de s'appuyer sur une représentation comme une séquence de (distances, angles de virage) à chaque sommet: pour une ligne composée de points P1, P2, ..., PN, une telle séquence serait:

(distance (P1P2), angle (P1, P2, P3), distance (P2P3), ..., angle (P (N-2), P (N-1), PN), distance (P (N-1 ) PN)).

Selon vos besoins, deux lignes sont égales si et seulement si leurs séquences correspondantes sont identiques (modulez l'ordre et la direction de l'angle). La comparaison des séquences de nombres est triviale.

En calculant chaque séquence de polyligne une seule fois et, comme suggéré par lynxlynxlynx, en testant la similitude de séquence uniquement pour une polyligne ayant les mêmes caractéristiques triviales (longueur, nombre de sommets ...), le calcul devrait être vraiment rapide!

julien
la source
C'est la bonne idée. Pour que cela fonctionne réellement, cependant, de nombreux détails doivent être abordés, tels que la gestion des réflexions, l'orientation interne, la possibilité de plusieurs composants connectés et l'erreur d'arrondi à virgule flottante. Ils sont discutés dans la solution que j'ai fournie.
whuber
Oui, je n'ai décrit que l'idée principale. Votre réponse est remarquablement plus complète (comme souvent :-)
julien