Trovare un rettangolo di area minima per determinati punti?


72

Come vedi nella figura, la domanda è:

Come trovare il rettangolo di area minima (MAR) montato sui punti indicati?

e una domanda di supporto è:

Esiste una soluzione analitica per il problema?

(Uno sviluppo della domanda sarà quello di adattare una scatola (3D) a un gruppo di punti in una nuvola di punti 3D.)

Come primo stadio, propongo di trovare lo scafo convesso per i punti che riformano il problema (rimuovendo quei punti non coinvolti nella soluzione) per: adattare un MAR ad un poligono. Il metodo richiesto fornirà X ( centro del rettangolo ), D ( due dimensioni ) e A ( angolo ).


La mia proposta di soluzione:

  • Trova il centroide del poligono (vedi Trovare il centro della geometria dell'oggetto? )
  • [S] Montare un rettangolo montato semplice, cioè parallelo agli assi X e Y
    • puoi usare la minmaxfunzione per X e Y dei punti dati (es. vertici del poligono)
  • Memorizza l'area del rettangolo montato
  • Ruota il poligono attorno al centroide, ad esempio di 1 grado
  • Ripetere da [S] fino a quando non viene eseguita una rotazione completa
  • Riporta l'angolo dell'area minima come risultato

Mi sembra promettente, tuttavia esistono i seguenti problemi:

  • scegliere una buona risoluzione per il cambio di angolo potrebbe essere impegnativo,
  • il costo di calcolo è elevato,
  • la soluzione non è analitica ma sperimentale.

inserisci qui la descrizione dell'immagine

Risposte:


45

Sì, esiste una soluzione analitica per questo problema. L'algoritmo che stai cercando è noto nella generalizzazione dei poligoni come "rettangolo circostante più piccolo".

L'algoritmo che descrivi va bene ma per risolvere i problemi che hai elencato, puoi usare il fatto che l'orientamento del MAR è lo stesso di quello di uno dei bordi dello scafo convesso della nuvola di punti . Quindi devi solo testare gli orientamenti dei bordi dello scafo convessi. Dovresti:

  • Calcola lo scafo convesso della nuvola.
  • Per ogni bordo dello scafo convesso:
    • calcola l'orientamento del bordo (con arctan),
    • ruotare lo scafo convesso usando questo orientamento per calcolare facilmente l'area del rettangolo di delimitazione con min / max di x / y dello scafo convesso ruotato,
    • Memorizza l'orientamento corrispondente all'area minima trovata,
  • Restituisce il rettangolo corrispondente all'area minima trovata.

Un esempio di implementazione in Java è disponibile qui .

In 3D, lo stesso vale, tranne:

  • Lo scafo convesso sarà un volume,
  • Gli orientamenti testati saranno gli orientamenti (in 3D) delle facce dello scafo convesse.

In bocca al lupo!


11
+1 Risposta molto bella! Vorrei sottolineare che l'effettiva rotazione del cloud non è necessaria. Innanzitutto - probabilmente intendevi questo - devono essere considerati solo i vertici dello scafo. In secondo luogo, invece di ruotare, rappresentano il lato corrente come coppia di vettori di unità ortogonali. Prendendo i loro prodotti punto con le coordinate del vertice dello scafo (che potrebbe essere fatto come una singola operazione a matrice) si ottengono le coordinate ruotate: nessuna trigonometria necessaria, veloce e perfettamente accurata.
whuber

2
Grazie per i collegamenti. In effetti, ruotando solo per # di spigoli il metodo proposto è molto efficiente. Potrei trovare il documento lo dimostra. Anche se ho contrassegnato questo come la risposta per lealtà alla prima buona risposta (non posso scegliere due / più grandi risposte :() Vorrei raccomandare fortemente di considerare la risposta completa di whuber qui sotto. L'efficienza del metodo dato lì (evitando rotazioni!) È incredibile, e l'intera procedura è solo poche righe di codice. Per me è facilmente traducibile in Python :)
Sviluppatore

Puoi aggiornare il link di implementazione Java?
Myra,

si, è fatto!
luglio

1
Nota che l'estensione in 3D è un po 'più complicata di così. Ogni faccia dello scafo convesso 3D definisce un possibile orientamento di una faccia del rettangolo di selezione, ma non l'orientamento delle facce perpendicolari ad essa. Il problema di come ruotare la casella in quel piano diventa il problema del rettangolo di delimitazione minimo 2D nel piano di quella faccia. Per ogni bordo dello scafo convesso della nuvola proiettato su un determinato piano puoi disegnare un riquadro di delimitazione che ti darà un volume diverso in 3D.
Sarà il

40

Per integrare la grande soluzione di @julien, ecco un'implementazione funzionante in R, che potrebbe servire come pseudocodice per guidare qualsiasi implementazione specifica GIS (o essere applicata direttamente in R, ovviamente). L'input è una matrice di coordinate punto. L'output (il valore di mbr) è una matrice dei vertici del rettangolo di delimitazione minimo (con il primo ripetuto per chiuderlo). Si noti la completa assenza di calcoli trigonometrici.

MBR <- function(p) {
  # Analyze the convex hull edges     
  a <- chull(p)                                   # Indexes of extremal points
  a <- c(a, a[1])                                 # Close the loop
  e <- p[a[-1],] - p[a[-length(a)], ]             # Edge directions
  norms <- sqrt(rowSums(e^2))                     # Edge lengths
  v <- e / norms                                  # Unit edge directions
  w <- cbind(-v[,2], v[,1])                       # Normal directions to the edges

  # Find the MBR
  vertices <- p[a, ]                              # Convex hull vertices
  x <- apply(vertices %*% t(v), 2, range)         # Extremes along edges
  y <- apply(vertices %*% t(w), 2, range)         # Extremes normal to edges
  areas <- (y[1,]-y[2,])*(x[1,]-x[2,])            # Areas
  k <- which.min(areas)                           # Index of the best edge (smallest area)

  # Form a rectangle from the extremes of the best edge
  cbind(x[c(1,2,2,1,1),k], y[c(1,1,2,2,1),k]) %*% rbind(v[k,], w[k,])
}

Ecco un esempio del suo utilizzo:

# Create sample data
set.seed(23)
p <- matrix(rnorm(20*2), ncol=2)                 # Random (normally distributed) points
mbr <- MBR(points)

# Plot the hull, the MBR, and the points
limits <- apply(mbr, 2, range) # Plotting limits
plot(p[(function(x) c(x, x[1]))(chull(p)), ], 
     type="l", asp=1, bty="n", xaxt="n", yaxt="n",
     col="Gray", pch=20, 
     xlab="", ylab="",
     xlim=limits[,1], ylim=limits[,2])                # The hull
lines(mbr, col="Blue", lwd=3)                         # The MBR
points(points, pch=19)                                # The points

MBR

Il tempismo è limitato dalla velocità dell'algoritmo dello scafo convesso, poiché il numero di vertici nello scafo è quasi sempre molto inferiore al totale. La maggior parte degli algoritmi dello scafo convesso sono asintoticamente O (n * log (n)) per n punti: puoi calcolare quasi la stessa velocità con cui puoi leggere le coordinate.


+1 Che soluzione straordinaria! Un'idea del genere viene solo dopo aver avuto lunghe esperienze. Da ora sarò curioso di ottimizzare i miei codici esistenti ispirandomi con questa grande risposta.
Sviluppatore

Vorrei poter votare questo due volte. Sto imparando R e le tue risposte sono una fonte continua di ispirazione.
John Powell,

1
@retrovius Il rettangolo di delimitazione di un insieme di punti (ruotati) è determinato da quattro numeri: la coordinata x più piccola, la coordinata x più grande, la coordinata y più piccola e la coordinata y più grande. Ecco a cosa si riferiscono gli "estremi lungo i bordi".
whuber

1
@retrovius L'origine non ha alcun ruolo in questi calcoli, perché tutto si basa su differenze di coordinate tranne alla fine, dove il rettangolo migliore calcolato in coordinate ruotate viene semplicemente ruotato indietro. Sebbene sia un'idea intelligente utilizzare un sistema di coordinate in cui l'origine è vicina ai punti (per ridurre al minimo la perdita di precisione in virgola mobile), l'origine altrimenti è irrilevante.
whuber

1
@Retrovius Puoi interpretarlo in termini di proprietà delle rotazioni: vale a dire, la matrice di una rotazione è ortogonale. Pertanto, un tipo di risorsa sarebbe uno studio dell'algebra lineare (in genere) o della geometria euclidea analitica (in particolare). Tuttavia, ho scoperto che il modo più semplice per gestire le rotazioni (e le traduzioni e i riscalamenti) nel piano è quello di visualizzare i punti come numeri complessi: le rotazioni vengono semplicemente eseguite moltiplicando i valori per i numeri di unità di lunghezza.
whuber

8

Ho appena implementato questo me stesso e pubblicato la mia risposta su StackOverflow , ma ho pensato che avrei lasciato cadere la mia versione qui affinché gli altri potessero visualizzarla:

import numpy as np
from scipy.spatial import ConvexHull

def minimum_bounding_rectangle(points):
    """
    Find the smallest bounding rectangle for a set of points.
    Returns a set of points representing the corners of the bounding box.

    :param points: an nx2 matrix of coordinates
    :rval: an nx2 matrix of coordinates
    """
    from scipy.ndimage.interpolation import rotate
    pi2 = np.pi/2.

    # get the convex hull for the points
    hull_points = points[ConvexHull(points).vertices]

    # calculate edge angles
    edges = np.zeros((len(hull_points)-1, 2))
    edges = hull_points[1:] - hull_points[:-1]

    angles = np.zeros((len(edges)))
    angles = np.arctan2(edges[:, 1], edges[:, 0])

    angles = np.abs(np.mod(angles, pi2))
    angles = np.unique(angles)

    # find rotation matrices
    # XXX both work
    rotations = np.vstack([
        np.cos(angles),
        np.cos(angles-pi2),
        np.cos(angles+pi2),
        np.cos(angles)]).T
#     rotations = np.vstack([
#         np.cos(angles),
#         -np.sin(angles),
#         np.sin(angles),
#         np.cos(angles)]).T
    rotations = rotations.reshape((-1, 2, 2))

    # apply rotations to the hull
    rot_points = np.dot(rotations, hull_points.T)

    # find the bounding points
    min_x = np.nanmin(rot_points[:, 0], axis=1)
    max_x = np.nanmax(rot_points[:, 0], axis=1)
    min_y = np.nanmin(rot_points[:, 1], axis=1)
    max_y = np.nanmax(rot_points[:, 1], axis=1)

    # find the box with the best area
    areas = (max_x - min_x) * (max_y - min_y)
    best_idx = np.argmin(areas)

    # return the best box
    x1 = max_x[best_idx]
    x2 = min_x[best_idx]
    y1 = max_y[best_idx]
    y2 = min_y[best_idx]
    r = rotations[best_idx]

    rval = np.zeros((4, 2))
    rval[0] = np.dot([x1, y2], r)
    rval[1] = np.dot([x2, y2], r)
    rval[2] = np.dot([x2, y1], r)
    rval[3] = np.dot([x1, y1], r)

    return rval

Ecco quattro diversi esempi di esso in azione. Per ogni esempio, ho generato 4 punti casuali e ho trovato il rettangolo di selezione.

inserisci qui la descrizione dell'immagine

È relativamente veloce anche per questi campioni su 4 punti:

>>> %timeit minimum_bounding_rectangle(a)
1000 loops, best of 3: 245 µs per loop

Ciao JesseBuesking, sei in grado di generare rettangoli con angoli a 90 gradi? Il tuo codice funziona alla grande per ottenere parallelogrammi, ma nel mio caso d'uso specifico sono richiesti angoli di 90 °. Potresti consigliarti come modificare il tuo codice per raggiungerlo? Grazie!
Nader Alexan,

@NaderAlexan Se ti stai chiedendo se è in grado di gestire i quadrati, allora sì, certo che può! L'ho appena provato su un'unità quadrata points = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])e l'output è array([[1.00000000e+00, 6.12323400e-17], [0.00000000e+00, 0.00000000e+00], [6.12323400e-17, 1.00000000e+00], [1.00000000e+00, 1.00000000e+00]])quale è l'unità quadrata stessa (inclusi alcuni errori di arrotondamento in virgola mobile). Nota: un quadrato è solo un rettangolo con lati uguali, quindi suppongo che se può gestire un quadrato si generalizza a tutti i rettangoli.
JesseBuesking,

La ringrazio per la risposta. Sì, funziona alla grande ma sto cercando di forzarlo a produrre sempre un rettangolo (4 lati con angoli di 90 gradi per ogni lato) su qualsiasi altro poligono a 4 lati, sebbene in alcuni casi produca un rettangolo non sembra per essere un vincolo costante, sai come modificare il codice per aggiungere questo vincolo? Grazie!
Nader Alexan,

Forse gis.stackexchange.com/a/22934/48041 potrebbe guidarti verso una soluzione, dato che la sua risposta sembra avere questo vincolo? Una volta che hai trovato una soluzione, dovresti contribuire perché sono sicuro che altri lo troveranno utile. In bocca al lupo!
JesseBuesking,

7

C'è uno strumento in Whitebox GAT ( http://www.uoguelph.ca/~hydrogeo/Whitebox/ ) chiamato Minimum Bounding Box per risolvere questo esatto problema. C'è anche uno strumento di scafo convesso minimo anche lì. Diversi strumenti nella casella degli strumenti Forma patch, ad esempio l'orientamento e l'allungamento della patch, si basano sulla ricerca del riquadro di delimitazione minimo.

inserisci qui la descrizione dell'immagine


4

Mi sono imbattuto in questo thread mentre cercavo una soluzione Python per un rettangolo di delimitazione dell'area minima.

Ecco la mia implementazione , per la quale i risultati sono stati verificati con Matlab.

Il codice di prova è incluso per i poligoni semplici e lo sto usando per trovare il riquadro di delimitazione minimo 2D e le direzioni degli assi per un PointCloud 3D.


La tua risposta è stata cancellata?
Paul Richter,

@PaulRichter apparentemente. La fonte era qui github.com/dbworth/minimum-area-bounding-rectangle però
visto il

3

Grazie @ risposta di whuber. È un'ottima soluzione, ma lenta per la grande nuvola di punti. Ho trovato che la convhullnfunzione nel pacchetto R geometryè molto più veloce (138 s contro 0,03 s per 200000 punti). Ho incollato i miei codici qui per chiunque sia interessante per una soluzione più veloce.

library(alphahull)                                  # Exposes ashape()
MBR <- function(points) {
    # Analyze the convex hull edges                       
    a <- ashape(points, alpha=1000)                 # One way to get a convex hull...
    e <- a$edges[, 5:6] - a$edges[, 3:4]            # Edge directions
    norms <- apply(e, 1, function(x) sqrt(x %*% x)) # Edge lengths
    v <- diag(1/norms) %*% e                        # Unit edge directions
    w <- cbind(-v[,2], v[,1])                       # Normal directions to the edges

    # Find the MBR
    vertices <- (points) [a$alpha.extremes, 1:2]    # Convex hull vertices
    minmax <- function(x) c(min(x), max(x))         # Computes min and max
    x <- apply(vertices %*% t(v), 2, minmax)        # Extremes along edges
    y <- apply(vertices %*% t(w), 2, minmax)        # Extremes normal to edges
    areas <- (y[1,]-y[2,])*(x[1,]-x[2,])            # Areas
    k <- which.min(areas)                           # Index of the best edge (smallest area)

    # Form a rectangle from the extremes of the best edge
    cbind(x[c(1,2,2,1,1),k], y[c(1,1,2,2,1),k]) %*% rbind(v[k,], w[k,])
}

MBR2 <- function(points) {
    tryCatch({
        a2 <- geometry::convhulln(points, options = 'FA')

        e <- points[a2$hull[,2],] - points[a2$hull[,1],]            # Edge directions
        norms <- apply(e, 1, function(x) sqrt(x %*% x)) # Edge lengths

        v <- diag(1/norms) %*% as.matrix(e)                        # Unit edge directions


        w <- cbind(-v[,2], v[,1])                       # Normal directions to the edges

        # Find the MBR
        vertices <- as.matrix((points) [a2$hull, 1:2])    # Convex hull vertices
        minmax <- function(x) c(min(x), max(x))         # Computes min and max
        x <- apply(vertices %*% t(v), 2, minmax)        # Extremes along edges
        y <- apply(vertices %*% t(w), 2, minmax)        # Extremes normal to edges
        areas <- (y[1,]-y[2,])*(x[1,]-x[2,])            # Areas
        k <- which.min(areas)                           # Index of the best edge (smallest area)

        # Form a rectangle from the extremes of the best edge
        as.data.frame(cbind(x[c(1,2,2,1,1),k], y[c(1,1,2,2,1),k]) %*% rbind(v[k,], w[k,]))
    }, error = function(e) {
        assign('points', points, .GlobalEnv)
        stop(e)  
    })
}


# Create sample data
#set.seed(23)
points <- matrix(rnorm(200000*2), ncol=2)                 # Random (normally distributed) points
system.time(mbr <- MBR(points))
system.time(mmbr2 <- MBR2(points))


# Plot the hull, the MBR, and the points
limits <- apply(mbr, 2, function(x) c(min(x),max(x))) # Plotting limits
plot(ashape(points, alpha=1000), col="Gray", pch=20, 
     xlim=limits[,1], ylim=limits[,2])                # The hull
lines(mbr, col="Blue", lwd=10)                         # The MBR
lines(mbr2, col="red", lwd=3)                         # The MBR2
points(points, pch=19)   

Due metodi ottengono la stessa risposta (esempio per 2000 punti):

inserisci qui la descrizione dell'immagine


È possibile estendere questa implementazione allo spazio 3d (ovvero trovare un riquadro del volume minimo che includa tutti i punti dati nello spazio 3d)?
Sasha,

0

Consiglio semplicemente la funzione incorporata di OpenCV minAreaRect, che trova un rettangolo ruotato dell'area minima che racchiude il set di punti 2D di input. Per vedere come utilizzare questa funzione, è possibile fare riferimento a questo tutorial .

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.