Algoritmo di imballaggio delle trame


52

Che cos'è un buon algoritmo di imballaggio delle trame? Tecnicamente, l' imballaggio del bidone è NP-difficile , quindi un'euristica è ciò che sto davvero cercando.


Supponevo che lo stiate usando per ottimizzare le mappe uv, ma sono curioso di sapere quale sia l'applicazione.
Jonathan Fischoff,

ftgles è una libreria che utilizza opengl e freetype per il rendering dei caratteri. Tuttavia, ogni glifo viene memorizzato nella propria trama. Vorrei metterli in una trama.
deft_code

Risposte:


58

Ho trascorso alcuni mesi in un lavoro elaborando un algoritmo di imballaggio delle trame migliore.

L'algoritmo con cui abbiamo iniziato era semplice. Colleziona tutti gli elementi di input. Ordinali per pixel totali consumati, da grandi a piccoli. Disporli nella trama in ordine di scanline, testando semplicemente le cose dal pixel di inizio sinistro al pixel superiore, spostandosi lungo una linea e ripetendo, ripristinando il pixel di sinistra dopo ogni posizionamento riuscito.

O è necessario codificare in larghezza una larghezza o creare un'altra euristica per questo. Nel tentativo di preservare la quadratura, il nostro algoritmo inizierebbe a 128, per poi aumentare di 128 secondi fino a ottenere un risultato non più profondo di quanto fosse ampio.

Quindi, avevamo quell'algoritmo e ho deciso di migliorarlo. Ho provato un mucchio di euristiche stravaganti, cercando di trovare oggetti che si incastrassero insieme, facendo un po 'di ponderazione su un mucchio di proprietà di imballaggio dello spazio desiderate, ruotando e capovolgendo. Dopo tutto il mio lavoro, letteralmente tre mesi di lavoro, ho finito per risparmiare il 3% di spazio.

Si. 3%.

E dopo aver eseguito la nostra routine di compressione su di essa, in realtà è finita più grande (cosa che non riesco ancora a spiegare), quindi abbiamo buttato giù tutto e siamo tornati al vecchio algoritmo.

Ordinare gli elementi, incepparli nella trama nell'ordine di scanline. C'è il tuo algoritmo. È facile da programmare, veloce da eseguire e non migliorerai molto senza un'incredibile quantità di lavoro. Questo lavoro non vale la pena, a meno che la tua azienda non sia grande almeno 50 persone, e probabilmente di più.

testo alternativo

E come nota a margine, ho appena implementato questo algoritmo (larghezza fissa 512 pixel) per la stessa identica applicazione che stai facendo (niente ftgles, ma glifi di freetype resi opengl.) Ecco il risultato. Sembra sfocato perché il mio sta usando l' algoritmo di rendering del testo basato sul campo di distanza di Valve , che rappresenta anche lo spazio extra tra glifi. Ovviamente, non c'è molto spazio vuoto rimasto e fa un buon lavoro di stipare le cose in punti aperti.

Tutto il codice per questo è concesso in licenza BSD e disponibile su github .


Ho guardato la tua texture e ho pensato "Sono sicuro che il nostro texture packer fa un po 'meglio di così". E poi sono andato a guardarlo, e ho capito che l'ho rotto qualche tempo fa e non me ne sono accorto (perché una volta che funziona, chi guarda le trame di output?) ... Quindi grazie per la pubblicazione - non avrei trovato il bug altrimenti :) (una volta risolto il problema, sembra molto simile - forse una sfumatura migliore, ma è difficile dirlo esattamente. "buono" è probabilmente la descrizione più sicura).
JasonD,

@JasonD, mi piacerebbe sapere cosa fa il tuo algoritmo, se ottiene un output migliore :) Anche se ottiene un output approssimativamente equivalente in un modo diverso.
ZorbaTHut,

1
Grazie per la descrizione dell'algo + il fallimento ammesso + il codice sorgente. Ottimo post.
Calvin1602,

1
La ragione per cui è diventata più grande dopo la compressione è probabilmente a causa dell'algoritmo di compressione. Poiché la compressione si basa spesso sull'hash e sulla ricerca di schemi binari, se l'algoritmo è in grado di identificare abbastanza schemi genererà un intero gruppo di essi che può causare l'espansione della dimensione. un ottimo modo per testarlo semplicemente ri-comprimere un file più e più volte e alla fine ricomincerà a ingrandirsi a causa della mancanza di schemi.
Hanna,

1
Per come cercare l'ultima versione del codice di imballaggio di ZorbaTHut (font_baker.cpp), puoi trovare qui: github.com/zorbathut/glorp/blob/…
mems

20

La tesi di dottorato di Andrea Lodi è intitolata Algorithms for Two Dimensional Bin Packing and Assignment Problems .
La tesi ripercorre alcune delle forme più difficili di questo problema. Fortunatamente, il packaging texture è la versione più semplice. Il miglior algoritmo che ha trovato è stato chiamato Touching Perimeter .

Per citare da pagina 52:

L'algoritmo, chiamato Touching Perimeter (TPRF), inizia ordinando gli articoli in base all'area non in aumento (rompendo i legami con valori min {wj, hj} non in aumento) e orientandoli orizzontalmente. Viene quindi calcolato un limite inferiore L sul valore ottimale della soluzione e vengono inizializzati L scomparti vuoti. (Il limite inferiore continuo L0 definito nella sezione precedente è ovviamente valido anche per 2BP | R | F; i limiti migliori sono proposti da Dell'Amico, Martello e Vigo [56].) L'algoritmo racchiude un oggetto alla volta, in un cestino esistente o inizializzandone uno nuovo. Il primo oggetto imballato in un cestino viene sempre posizionato nell'angolo in basso a sinistra. Ogni articolo successivo viene imballato in una cosiddetta posizione normale (vedi Christo fi des e Whitlock [41]), ovvero
La scelta del cestino e della posizione di imballaggio viene effettuata valutando un punteggio, definito come percentuale del perimetro dell'articolo che tocca il cestino e altri articoli già imballati. Questa strategia favorisce i modelli in cui gli articoli imballati non "intrappolano" piccole aree, che possono essere difficili da usare per ulteriori posizionamenti. Per ogni posizione di imballaggio del candidato, il punteggio viene valutato due volte, per i due orientamenti degli oggetti (se entrambi sono fattibili) e viene selezionato il valore più alto. I legami del punteggio vengono interrotti scegliendo il cestino con l'area massima imballata. L'algoritmo generale è il seguente.

touching_perimeter:
  sort the items by nonincreaseing w,h values, and horizontally orient them;
  comment: Phase 1;
  compute a lower bound L on the optimal solution value, and open L empty bins;
  comment: Phase 2;
  for j := 1 to n do
     score := 0;
     for each normal packing position in an open bin do
        let score1 and score2 be scores with tow orientations;
        score := max{score,score1,score2};
     end for;
     if score > 0 then
        pack item j in the bin, position and orientation corresponding to score;
     else
        open a new bin and horizontally pack item j into i;
     end if;
  end for;
end;

Anche di interesse, il documento descrive un algoritmo per determinare le dimensioni di una mappa delle trame imballata in modo ottimale. Ciò sarebbe utile per determinare se è persino possibile adattare tutte le trame in un atlante 1024x1024.


Questo algoritmo presuppone che le trame siano di forma rettangolare, giusto?
user1767754,

17

Se qualcuno è ancora interessato, ho riscritto completamente la libreria rectpack2D in modo che sia molto più efficiente.

Funziona mantenendo uno std::vectordegli spazi vuoti nell'atlante, iniziando con una dimensione massima iniziale (in genere, la dimensione massima consentita della trama su una particolare GPU), suddividendo il primo spazio vuoto praticabile e salvando le divisioni nel vettore.

Il progresso delle prestazioni è arrivato semplicemente usando un vettore, invece di mantenere un intero albero, come in precedenza.

La procedura è descritta in dettaglio nel README .

La biblioteca è sotto il MIT, quindi sono contento per te se lo trovi utile!

Risultati di esempio:

I test sono stati condotti su una CPU Intel (R) Core (TM) i7-4770K a 3,50 GHz. Il binario è stato creato con clang 6.0.0, usando uno switch -03.

Sprite di gioco arbitrarie + glifi giapponesi: 3264 soggetti in totale.

Durata: 4 millisecondi
Pixel sprecati: 15538 (0,31% - equivalente di un quadrato 125 x 125)

Uscita (2116 x 2382):

3

A colori:
(il nero è spazio sprecato)

4

Glifi giapponesi + alcuni sprite della GUI: 3122 soggetti.

Durata: 3,5 - 7 ms
Pixel sprecati: 9288 (1,23% - equivalente di un quadrato 96 x 96)

Uscita (866 x 871):

5

A colori:
(il nero è spazio sprecato)

6


2
Ho scaricato il tuo codice. Basta leggere le definizioni di struct: CHE COSA È QUESTA MONSTROSITÀ ?! Sembra codice golf.
Akaltar

3
Tuttavia, funziona e mi ha aiutato, quindi grazie. Non volevo essere scortese.
Akaltar

Non so perché ignoro questa risposta poiché è ancora più veloce e più buono del mio algoritmo O_O grazie
GameDeveloper

@akaltar Posso immaginarlo, stavo ancora imparando la lingua durante quel periodo :)
Patryk Czachurski

Approccio abbastanza semplice che è rapido da implementare e ottiene buoni risultati, grazie :)
FlintZA

5

Un buon algoritmo euristico può essere trovato qui . Quando stavo provando qualcosa di simile recentemente, ho trovato questo riferimento come punto di partenza di base per la maggior parte delle implementazioni che ho visto.

Funziona particolarmente bene con molti oggetti dalla forma regolare, di dimensioni simili o con un buon mix di immagini piccole e di dimensioni minori. Il miglior consiglio per ottenere buoni risultati è di ricordare di ordinare i tuoi input in termini di dimensioni dell'immagine, quindi impacchettare dal più grande al più piccolo poiché le immagini più piccole si impacchettano nello spazio attorno alle immagini più grandi. In che modo fai questo ordinamento e dipende dai tuoi obiettivi. Ho usato il perimetro piuttosto che l'area come approssimazione del 1 ° ordine poiché ho considerato che le immagini alte + sottili / corte + larghe (che avrebbero un'area bassa) in realtà sono molto difficili da posizionare in seguito in un pacchetto, quindi usando il perimetro si spinge queste strane forme verso la parte anteriore dell'ordine.

Ecco un esempio di visualizzazione dell'output per il mio packer su un set casuale di immagini dalla directory del dump delle immagini del mio sito Web :). Uscita packer

I numeri nei quadrati sono gli ID dei blocchi contenenti nella struttura, in modo da darti un'idea dell'ordine delle inserzioni. Il primo è l'ID "3" perché è il primo nodo foglia (solo le foglie contengono immagini) e di conseguenza ha 2 genitori).

        Root[0]
       /      \
   Child[1]  Child[2]
      |
    Leaf[3]

3

Personalmente, uso solo un avido primo blocco più grande che si adatta. Non è ottimale, ma fa il trucco OK.

Si noti che, se si dispone di una quantità ragionevole di blocchi di trama, è possibile cercare in modo esauriente l'ordine migliore anche se il problema in sé è NP.


3

Qualcosa che ho usato, che funziona bene anche con le mappe UV irregolari, è trasformare il cerotto UV in una maschera bitmap e mantenere una maschera per la trama stessa, cercando la prima posizione in cui il cerotto UV si adatterà. Ordino i blocchi secondo una semplice euristica (altezza, larghezza, dimensione, qualunque cosa) e consento alle rotazioni dei blocchi di minimizzare o massimizzare l'euristica scelta. Ciò fornisce uno spazio di ricerca gestibile per la forza bruta.

Se riesci quindi a ripetere quella prova di diverse euristiche e / o applicare un fattore casuale nella scelta dell'ordine e a ripetere fino a quando non scade il tempo limite.

Con questo schema otterrai piccole isole UV racchiuse negli spazi vuoti creati da quelle grandi e persino nei buchi lasciati all'interno di singole patch UV.


1

Di recente abbiamo rilasciato uno script Python che impacchetterà le trame in più file di immagini di una determinata dimensione.

Citato dal nostro blog:

"Mentre ci sono numerosi packer che possono essere trovati online, la nostra difficoltà era trovare qualcuno che potesse gestire un gran numero di immagini in più directory. Così è nato il nostro packer di atlante!

Così com'è, il nostro piccolo script inizierà nella directory di base e caricherà tutti i .PNG in un atlante. Se quell'atlante è pieno, ne crea uno nuovo. Quindi, proverà ad adattare il resto delle immagini in tutti gli atlanti precedenti prima di trovare un posto in quello nuovo. In questo modo, ogni atlante è imballato il più stretto possibile. Gli atlanti sono denominati in base alla cartella da cui provengono le loro immagini.

È possibile modificare le dimensioni dell'atlante (riga 65), il formato delle immagini che si desidera comprimere (riga 67), la directory di caricamento (riga 10) e la directory di salvataggio (riga 13) abbastanza facilmente senza esperienza in Python. Come un piccolo disclaimer, questo è stato montato insieme in pochi giorni per funzionare specificamente con il nostro motore. Ti incoraggio a richiedere funzionalità, a commentare con le tue varianti e a segnalare eventuali errori, ma qualsiasi modifica allo script avverrà nel mio tempo libero. "

Sentiti libero di consultare il codice sorgente completo qui: http://www.retroaffect.com/blog/159/Image_Atlas_Packer/#b


1

È abbastanza facile impacchettare i caratteri perché tutte (o la grande maggioranza) delle trame degli glifi hanno quasi le stesse dimensioni. Fai la cosa più semplice che ti viene in mente e sarà molto vicina all'ottimale.

L'intelligenza diventa più importante quando si confezionano immagini di dimensioni molto diverse. Quindi vuoi essere in grado di colmare lacune, ecc. Anche allora, tuttavia, un semplice algoritmo come la ricerca dell'ordine di scanline discusso in precedenza produrrà risultati molto ragionevoli.

Nessuno degli algos avanzati è magico. Non saranno più efficaci del 50% rispetto a un semplice algoritmo e non otterrai benefici costanti da essi a meno che tu non abbia un numero impressionante di fogli di trama. questo perché i piccoli miglioramenti apportati da algoritmi migliori verranno visualizzati solo in forma aggregata.

Diventa semplice e passa a qualcosa in cui i tuoi sforzi saranno ricompensati meglio


0

Se è specifico per le trame dei caratteri, probabilmente fai qualcosa di non ottimale ma bello e semplice:

Ordina i caratteri per altezza, prima il più alto

Inizia da 0,0 Posiziona il primo carattere nelle coordinate attuali, avanza di X, posiziona il successivo, ripeti fino a quando non riusciamo a inserirne un altro

Ripristina X su 0, fai avanzare Y verso il basso dell'altezza del carattere più alto nella riga e riempi un'altra riga

Ripeti fino a quando non abbiamo esaurito i personaggi o non possiamo adattarci a un'altra riga.

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.