Disegna un'immagine come una mappa Voronoi


170

Ringraziamo gli hobby di Calvin per aver spinto la mia idea di sfida nella giusta direzione.

Considera una serie di punti nel piano, che chiameremo siti , e associ un colore a ciascun sito. Ora puoi dipingere l'intero piano colorando ogni punto con il colore del sito più vicino. Questa è chiamata una mappa Voronoi (o diagramma Voronoi ). In linea di principio, le mappe Voronoi possono essere definite per qualsiasi metrica di distanza, ma useremo semplicemente la solita distanza euclidea r = √(x² + y²). ( Nota: non devi necessariamente sapere come calcolare e rendere uno di questi per competere in questa sfida.)

Ecco un esempio con 100 siti:

inserisci qui la descrizione dell'immagine

Se guardi qualsiasi cella, tutti i punti all'interno di quella cella sono più vicini al sito corrispondente che a qualsiasi altro sito.

Il tuo compito è approssimare una data immagine con una tale mappa Voronoi. Si è dato l'immagine in qualsiasi formato grafica raster comodo, così come un intero N . Dovresti quindi produrre fino a N siti e un colore per ciascun sito, in modo tale che la mappa Voronoi basata su questi siti assomigli il più possibile all'immagine di input.

Puoi utilizzare lo Stack Snippet alla fine di questa sfida per eseguire il rendering di una mappa Voronoi dal tuo output, oppure puoi renderla tu stesso se preferisci.

È possibile utilizzare funzioni integrate o di terze parti per calcolare una mappa Voronoi da un set di siti (se necessario).

Questo è un concorso di popolarità, quindi vince la risposta con il maggior numero di voti netti. Gli elettori sono incoraggiati a giudicare le risposte

  • quanto sono approssimate le immagini originali e i loro colori.
  • quanto bene l'algoritmo funziona su diversi tipi di immagini.
  • quanto bene funziona l'algoritmo per N piccolo .
  • se l'algoritmo raggruppa in modo adattivo i punti nelle regioni dell'immagine che richiedono maggiori dettagli.

Immagini di prova

Ecco alcune immagini su cui testare il tuo algoritmo (alcuni dei nostri soliti sospetti, altri nuovi). Fai clic sulle immagini per le versioni più grandi.

Grande onda Riccio Spiaggia Cornell Saturno Orso bruno Yoshi Mandrillo Nebulosa del granchio Geobits 'Kid Cascata Urlare

La spiaggia in prima fila fu disegnata da Olivia Bell e inclusa con il suo permesso.

Se vuoi una sfida in più, prova Yoshi con uno sfondo bianco e ottieni la linea del ventre giusta.

Puoi trovare tutte queste immagini di prova in questa galleria imgur dove puoi scaricarle tutte come file zip. L'album contiene anche un diagramma Voronoi casuale come altro test. Per riferimento, ecco i dati che lo hanno generato .

Si prega di includere diagrammi di esempio per una varietà di immagini diverse e N , ad esempio 100, 300, 1000, 3000 (nonché pastiglie per alcune delle specifiche delle celle corrispondenti). È possibile utilizzare o omettere i bordi neri tra le celle come meglio credi (ciò potrebbe apparire meglio su alcune immagini rispetto ad altre). Tuttavia, non includere i siti (tranne in un esempio separato, forse se si desidera spiegare come funziona il posizionamento del sito, ovviamente).

Se vuoi mostrare un gran numero di risultati, puoi creare una galleria su imgur.com , per mantenere ragionevole la dimensione delle risposte. In alternativa, metti le miniature nel tuo post e rendili collegamenti a immagini più grandi, come ho fatto nella mia risposta di riferimento . Puoi ottenere le miniature piccole aggiungendo sil nome del file nel link imgur.com (ad es. I3XrT.png-> I3XrTs.png). Inoltre, sentiti libero di usare altre immagini di prova, se trovi qualcosa di carino.

Renderer

Incolla l'output nel seguente frammento di stack per visualizzare i risultati. Il formato esatto dell'elenco è irrilevante, purché ogni cella sia specificata da 5 numeri in virgola mobile nell'ordine x y r g b, dove xe ysono le coordinate del sito della cella e r g bsono i canali di colore rosso, verde e blu nell'intervallo 0 ≤ r, g, b ≤ 1.

Lo snippet fornisce opzioni per specificare la larghezza della linea dei bordi della cella e se i siti della cella debbano essere mostrati o meno (quest'ultimo principalmente per scopi di debug). Ma nota che l'output viene ridistribuito solo quando cambiano le specifiche delle celle, quindi se modifichi alcune delle altre opzioni, aggiungi uno spazio alle celle o qualcosa del genere.

Grandi riconoscimenti a Raymond Hill per aver scritto questa bellissima libreria JS Voronoi .

Sfide correlate


5
@frogeyedpeas Guardando i voti che ottieni. ;) Questo è un concorso di popolarità. Non esiste necessariamente il modo migliore per farlo. L'idea è che provi a farlo nel miglior modo possibile e i voti rifletteranno se le persone concordano sul fatto che hai fatto un buon lavoro. C'è una certa dose di soggettività in questi, è vero. Dai un'occhiata alle sfide correlate che ho collegato o a questa . Vedrai che di solito esiste un'ampia varietà di approcci, ma il sistema di voto aiuta le migliori soluzioni a salire in cima e a decidere su un vincitore.
Martin Ender,

3
Olivia approva le approssimazioni della sua spiaggia presentate finora.
Alex A.

3
@AlexA. Devon approva alcune delle approssimazioni del suo viso presentate finora. Non è un grande fan di nessuna delle versioni n = 100;)
Geobits

1
@Geobits: capirà quando sarà più grande.
Alex A.

1
Ecco una pagina su una tecnica di punteggiatura centroida basata su voronoi . Una buona fonte di ispirazione (la tesi di laurea correlata ha una bella discussione sui possibili miglioramenti dell'algoritmo).
Giobbe

Risposte:


112

Python + scipy + scikit-image , campionamento ponderato del disco di Poisson

La mia soluzione è piuttosto complessa. Faccio un po 'di preelaborazione sull'immagine per rimuovere il rumore e ottenere una mappatura di quanto "interessante" sia ciascun punto (usando una combinazione di entropia locale e rilevamento dei bordi):

Quindi scelgo i punti di campionamento usando il campionamento del disco di Poisson con una svolta: la distanza del cerchio è determinata dal peso che abbiamo determinato in precedenza.

Quindi, una volta che ho i punti di campionamento, divido l'immagine in segmenti voronoi e assegno la media L * a * b * dei valori di colore all'interno di ciascun segmento a ciascun segmento.

Ho molta euristica e devo anche fare un po 'di matematica per assicurarmi che il numero di punti campione sia vicino N. Ottengo Nesattamente superando leggermente , e poi facendo cadere alcuni punti con un'euristica.

In termini di runtime, questo filtro non è economico , ma nessuna immagine sotto ha richiesto più di 5 secondi per essere realizzata.

Senza ulteriori indugi:

import math
import random
import collections
import os
import sys
import functools
import operator as op
import numpy as np
import warnings

from scipy.spatial import cKDTree as KDTree
from skimage.filters.rank import entropy
from skimage.morphology import disk, dilation
from skimage.util import img_as_ubyte
from skimage.io import imread, imsave
from skimage.color import rgb2gray, rgb2lab, lab2rgb
from skimage.filters import sobel, gaussian_filter
from skimage.restoration import denoise_bilateral
from skimage.transform import downscale_local_mean


# Returns a random real number in half-open range [0, x).
def rand(x):
    r = x
    while r == x:
        r = random.uniform(0, x)
    return r


def poisson_disc(img, n, k=30):
    h, w = img.shape[:2]

    nimg = denoise_bilateral(img, sigma_range=0.15, sigma_spatial=15)
    img_gray = rgb2gray(nimg)
    img_lab = rgb2lab(nimg)

    entropy_weight = 2**(entropy(img_as_ubyte(img_gray), disk(15)))
    entropy_weight /= np.amax(entropy_weight)
    entropy_weight = gaussian_filter(dilation(entropy_weight, disk(15)), 5)

    color = [sobel(img_lab[:, :, channel])**2 for channel in range(1, 3)]
    edge_weight = functools.reduce(op.add, color) ** (1/2) / 75
    edge_weight = dilation(edge_weight, disk(5))

    weight = (0.3*entropy_weight + 0.7*edge_weight)
    weight /= np.mean(weight)
    weight = weight

    max_dist = min(h, w) / 4
    avg_dist = math.sqrt(w * h / (n * math.pi * 0.5) ** (1.05))
    min_dist = avg_dist / 4

    dists = np.clip(avg_dist / weight, min_dist, max_dist)

    def gen_rand_point_around(point):
        radius = random.uniform(dists[point], max_dist)
        angle = rand(2 * math.pi)
        offset = np.array([radius * math.sin(angle), radius * math.cos(angle)])
        return tuple(point + offset)

    def has_neighbours(point):
        point_dist = dists[point]
        distances, idxs = tree.query(point,
                                    len(sample_points) + 1,
                                    distance_upper_bound=max_dist)

        if len(distances) == 0:
            return True

        for dist, idx in zip(distances, idxs):
            if np.isinf(dist):
                break

            if dist < point_dist and dist < dists[tuple(tree.data[idx])]:
                return True

        return False

    # Generate first point randomly.
    first_point = (rand(h), rand(w))
    to_process = [first_point]
    sample_points = [first_point]
    tree = KDTree(sample_points)

    while to_process:
        # Pop a random point.
        point = to_process.pop(random.randrange(len(to_process)))

        for _ in range(k):
            new_point = gen_rand_point_around(point)

            if (0 <= new_point[0] < h and 0 <= new_point[1] < w
                    and not has_neighbours(new_point)):
                to_process.append(new_point)
                sample_points.append(new_point)
                tree = KDTree(sample_points)
                if len(sample_points) % 1000 == 0:
                    print("Generated {} points.".format(len(sample_points)))

    print("Generated {} points.".format(len(sample_points)))

    return sample_points


def sample_colors(img, sample_points, n):
    h, w = img.shape[:2]

    print("Sampling colors...")
    tree = KDTree(np.array(sample_points))
    color_samples = collections.defaultdict(list)
    img_lab = rgb2lab(img)
    xx, yy = np.meshgrid(np.arange(h), np.arange(w))
    pixel_coords = np.c_[xx.ravel(), yy.ravel()]
    nearest = tree.query(pixel_coords)[1]

    i = 0
    for pixel_coord in pixel_coords:
        color_samples[tuple(tree.data[nearest[i]])].append(
            img_lab[tuple(pixel_coord)])
        i += 1

    print("Computing color means...")
    samples = []
    for point, colors in color_samples.items():
        avg_color = np.sum(colors, axis=0) / len(colors)
        samples.append(np.append(point, avg_color))

    if len(samples) > n:
        print("Downsampling {} to {} points...".format(len(samples), n))

    while len(samples) > n:
        tree = KDTree(np.array(samples))
        dists, neighbours = tree.query(np.array(samples), 2)
        dists = dists[:, 1]
        worst_idx = min(range(len(samples)), key=lambda i: dists[i])
        samples[neighbours[worst_idx][1]] += samples[neighbours[worst_idx][0]]
        samples[neighbours[worst_idx][1]] /= 2
        samples.pop(neighbours[worst_idx][0])

    color_samples = []
    for sample in samples:
        color = lab2rgb([[sample[2:]]])[0][0]
        color_samples.append(tuple(sample[:2][::-1]) + tuple(color))

    return color_samples


def render(img, color_samples):
    print("Rendering...")
    h, w = [2*x for x in img.shape[:2]]
    xx, yy = np.meshgrid(np.arange(h), np.arange(w))
    pixel_coords = np.c_[xx.ravel(), yy.ravel()]

    colors = np.empty([h, w, 3])
    coords = []
    for color_sample in color_samples:
        coord = tuple(x*2 for x in color_sample[:2][::-1])
        colors[coord] = color_sample[2:]
        coords.append(coord)

    tree = KDTree(coords)
    idxs = tree.query(pixel_coords)[1]
    data = colors[tuple(tree.data[idxs].astype(int).T)].reshape((w, h, 3))
    data = np.transpose(data, (1, 0, 2))

    return downscale_local_mean(data, (2, 2, 1))


if __name__ == "__main__":
    warnings.simplefilter("ignore")

    img = imread(sys.argv[1])[:, :, :3]

    print("Calibrating...")
    mult = 1.02 * 500 / len(poisson_disc(img, 500))

    for n in (100, 300, 1000, 3000):
        print("Sampling {} for size {}.".format(sys.argv[1], n))

        sample_points = poisson_disc(img, mult * n)
        samples = sample_colors(img, sample_points, n)
        base = os.path.basename(sys.argv[1])
        with open("{}-{}.txt".format(os.path.splitext(base)[0], n), "w") as f:
            for sample in samples:
                f.write(" ".join("{:.3f}".format(x) for x in sample) + "\n")

        imsave("autorenders/{}-{}.png".format(os.path.splitext(base)[0], n),
            render(img, samples))

        print("Done!")

immagini

Rispettivamente Nè 100, 300, 1000 e 3000:

abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc
abc abc abc abc


2
Mi piace questo; sembra un po 'come il vetro affumicato.
BobTheAwesome

3
Ho rovinato un po 'questo, e otterrai risultati migliori, in particolare per le immagini a triangolo basso, se sostituisci denoise_bilatteral con un denoise_tv_bregman. Genera patch più uniformi nel suo denoising, il che aiuta.
LKlevin,

@LKlevin Che peso hai usato?
orlp,

Ho usato 1.0 come peso.
LKlevin,

65

C ++

Il mio approccio è piuttosto lento, ma sono molto contento della qualità dei risultati che dà, in particolare per quanto riguarda la conservazione dei bordi. Ad esempio, ecco Yoshi e la Cornell Box con solo 1000 siti ciascuno:

Ci sono due parti principali che lo fanno spuntare. Il primo, incarnato nella evaluate()funzione, prende un set di posizioni del sito candidato, imposta i colori ottimali su di essi e restituisce un punteggio per il PSNR della tessellazione Voronoi renderizzata rispetto all'immagine di destinazione. I colori per ciascun sito sono determinati calcolando la media dei pixel dell'immagine di destinazione coperti dalla cella attorno al sito. Uso l'algoritmo di Welford per aiutare a calcolare sia il colore migliore per ogni cella sia il PSNR risultante usando un solo passaggio sull'immagine sfruttando la relazione tra varianza, MSE e PSNR. Ciò riduce il problema a quello di trovare il miglior set di posizioni del sito senza particolare riguardo al colore.

La seconda parte, quindi, incarnata main(), cerca di trovare questo set. Inizia scegliendo una serie di punti a caso. Quindi, in ogni passaggio rimuove un punto (andando round-robin) e verifica una serie di punti candidati casuali per sostituirlo. Quello che produce il PSNR più alto del gruppo viene accettato e mantenuto. In effetti, questo fa saltare il sito in una nuova posizione e generalmente migliora l'immagine bit per bit. Si noti che l'algoritmo non mantiene intenzionalmente la posizione originale come candidato. A volte questo significa che il salto riduce la qualità generale dell'immagine. Consentire che ciò accada aiuta ad evitare di rimanere bloccati nei massimi locali. Dà anche un criterio di arresto; il programma termina dopo aver eseguito un certo numero di passaggi senza migliorare il miglior set di siti trovato finora.

Si noti che questa implementazione è abbastanza semplice e può facilmente richiedere ore di CPU-core, soprattutto con l'aumentare del numero di siti. Ricalcola la mappa Voronoi completa per ogni candidato e la forza bruta verifica la distanza da tutti i siti per ciascun pixel. Poiché ogni operazione comporta la rimozione di un punto alla volta e l'aggiunta di un altro, le modifiche effettive all'immagine ad ogni passaggio saranno abbastanza locali. Esistono algoritmi per aggiornare in modo efficiente un diagramma Voronoi e credo che darebbero a questo algoritmo un'enorme velocità. Per questo concorso, tuttavia, ho scelto di mantenere le cose semplici e brutali.

Codice

#include <cstdlib>
#include <cmath>
#include <string>
#include <vector>
#include <fstream>
#include <istream>
#include <ostream>
#include <iostream>
#include <algorithm>
#include <random>

static auto const decimation = 2;
static auto const candidates = 96;
static auto const termination = 200;

using namespace std;

struct rgb {float red, green, blue;};
struct img {int width, height; vector<rgb> pixels;};
struct site {float x, y; rgb color;};

img read(string const &name) {
    ifstream file{name, ios::in | ios::binary};
    auto result = img{0, 0, {}};
    if (file.get() != 'P' || file.get() != '6')
        return result;
    auto skip = [&](){
        while (file.peek() < '0' || '9' < file.peek())
            if (file.get() == '#')
                while (file.peek() != '\r' && file.peek() != '\n')
                    file.get();
    };
     auto maximum = 0;
     skip(); file >> result.width;
     skip(); file >> result.height;
     skip(); file >> maximum;
     file.get();
     for (auto pixel = 0; pixel < result.width * result.height; ++pixel) {
         auto red = file.get() * 1.0f / maximum;
         auto green = file.get() * 1.0f / maximum;
         auto blue = file.get() * 1.0f / maximum;
         result.pixels.emplace_back(rgb{red, green, blue});
     }
     return result;
 }

 float evaluate(img const &target, vector<site> &sites) {
     auto counts = vector<int>(sites.size());
     auto variance = vector<rgb>(sites.size());
     for (auto &site : sites)
         site.color = rgb{0.0f, 0.0f, 0.0f};
     for (auto y = 0; y < target.height; y += decimation)
         for (auto x = 0; x < target.width; x += decimation) {
             auto best = 0;
             auto closest = 1.0e30f;
             for (auto index = 0; index < sites.size(); ++index) {
                 float distance = ((x - sites[index].x) * (x - sites[index].x) +
                                   (y - sites[index].y) * (y - sites[index].y));
                 if (distance < closest) {
                     best = index;
                     closest = distance;
                 }
             }
             ++counts[best];
             auto &pixel = target.pixels[y * target.width + x];
             auto &color = sites[best].color;
             rgb delta = {pixel.red - color.red,
                          pixel.green - color.green,
                          pixel.blue - color.blue};
             color.red += delta.red / counts[best];
             color.green += delta.green / counts[best];
             color.blue += delta.blue / counts[best];
             variance[best].red += delta.red * (pixel.red - color.red);
             variance[best].green += delta.green * (pixel.green - color.green);
             variance[best].blue += delta.blue * (pixel.blue - color.blue);
         }
     auto error = 0.0f;
     auto count = 0;
     for (auto index = 0; index < sites.size(); ++index) {
         if (!counts[index]) {
             auto x = min(max(static_cast<int>(sites[index].x), 0), target.width - 1);
             auto y = min(max(static_cast<int>(sites[index].y), 0), target.height - 1);
             sites[index].color = target.pixels[y * target.width + x];
         }
         count += counts[index];
         error += variance[index].red + variance[index].green + variance[index].blue;
     }
     return 10.0f * log10f(count * 3 / error);
 }

 void write(string const &name, int const width, int const height, vector<site> const &sites) {
     ofstream file{name, ios::out};
     file << width << " " << height << endl;
     for (auto const &site : sites)
         file << site.x << " " << site.y << " "
              << site.color.red << " "<< site.color.green << " "<< site.color.blue << endl;
 }

 int main(int argc, char **argv) {
     auto rng = mt19937{random_device{}()};
     auto uniform = uniform_real_distribution<float>{0.0f, 1.0f};
     auto target = read(argv[1]);
     auto sites = vector<site>{};
     for (auto point = atoi(argv[2]); point; --point)
         sites.emplace_back(site{
             target.width * uniform(rng),
             target.height * uniform(rng)});
     auto greatest = 0.0f;
     auto remaining = termination;
     for (auto step = 0; remaining; ++step, --remaining) {
         auto best_candidate = sites;
         auto best_psnr = 0.0f;
         #pragma omp parallel for
         for (auto candidate = 0; candidate < candidates; ++candidate) {
             auto trial = sites;
             #pragma omp critical
             {
                 trial[step % sites.size()].x = target.width * (uniform(rng) * 1.2f - 0.1f);
                 trial[step % sites.size()].y = target.height * (uniform(rng) * 1.2f - 0.1f);
             }
             auto psnr = evaluate(target, trial);
             #pragma omp critical
             if (psnr > best_psnr) {
                 best_candidate = trial;
                 best_psnr = psnr;
             }
         }
         sites = best_candidate;
         if (best_psnr > greatest) {
             greatest = best_psnr;
             remaining = termination;
             write(argv[3], target.width, target.height, sites);
         }
         cout << "Step " << step << "/" << remaining
              << ", PSNR = " << best_psnr << endl;
     }
     return 0;
 }

In esecuzione

Il programma è autonomo e non ha dipendenze esterne oltre la libreria standard, ma richiede che le immagini siano in formato binario PPM . Uso ImageMagick per convertire le immagini in PPM, anche se GIMP e molti altri programmi possono farlo.

Per compilarlo, salvare il programma come voronoi.cpped eseguire:

g++ -std=c++11 -fopenmp -O3 -o voronoi voronoi.cpp

Mi aspetto che probabilmente funzionerà su Windows con le versioni recenti di Visual Studio, anche se non l'ho provato. Ti consigliamo di compilare con C ++ 11 o superiore e OpenMP abilitato se lo fai. OpenMP non è strettamente necessario, ma aiuta molto a rendere i tempi di esecuzione più tollerabili.

Per eseguirlo, fai qualcosa del tipo:

./voronoi cornell.ppm 1000 cornell-1000.txt

Il file successivo verrà aggiornato con i dati del sito man mano che procede. La prima riga avrà la larghezza e l'altezza dell'immagine, seguite da righe di valori x, y, r, g, b adatte per la copia e l'incollaggio nel renderer Javascript nella descrizione del problema.

Le tre costanti nella parte superiore del programma consentono di ottimizzarlo per la velocità rispetto alla qualità. Il decimationfattore accresce l'immagine di destinazione quando si valuta un set di siti per colore e PSNR. Più è alto, più veloce sarà il programma. Impostandolo su 1 si utilizza l'immagine a piena risoluzione. La candidatescostante controlla il numero di candidati da testare su ogni passaggio. Più in alto offre maggiori possibilità di trovare un buon punto in cui saltare, ma rallenta il programma. Infine, terminationquanti passaggi può eseguire il programma senza migliorare il suo output prima di uscire. L'aumento può dare risultati migliori ma richiedere un tempo leggermente più lungo.

immagini

N = 100, 300, 1000 e 3000:


1
Questo avrebbe dovuto vincere l'IMO - molto meglio del mio.
orlp

1
@orlp - Grazie! Ad essere sinceri, hai pubblicato il tuo molto prima e viene eseguito molto più rapidamente. La velocità conta!
Boojum,

1
Bene, la mia non è proprio una risposta alla mappa di Voronoi :) È davvero un ottimo algoritmo di campionamento, ma trasformare i punti di campionamento in siti di Voronoi non è chiaramente ottimale.
orlp

55

IDL, raffinatezza adattativa

Questo metodo è ispirato al perfezionamento della mesh adattiva da simulazioni astronomiche e anche alla superficie di suddivisione . Questo è il tipo di attività su cui IDL è orgoglioso, che potrai riconoscere dal gran numero di funzioni integrate che sono stato in grado di utilizzare. : D

Ho prodotto alcuni degli intermedi per l'immagine del test yoshi con sfondo nero, con n = 1000.

Innanzitutto, eseguiamo una scala di grigi di luminanza sull'immagine (usando ct_luminance) e applichiamo un filtro Prewitt ( prewitt, vedi wikipedia ) per un buon rilevamento dei bordi:

abc abc

Poi arriva il vero grugnito: suddividiamo l'immagine in 4 e misuriamo la varianza in ciascun quadrante dell'immagine filtrata. La nostra varianza è ponderata dalla dimensione della suddivisione (che in questo primo passaggio è uguale), in modo che le regioni "spigolose" con varianza elevata non vengano suddivise sempre più piccole. Quindi, utilizziamo la varianza ponderata per indirizzare le suddivisioni con maggiori dettagli e suddividere iterativamente ogni sezione ricca di dettagli in 4 sezioni aggiuntive, fino a quando non raggiungiamo il nostro numero target di siti (ogni suddivisione contiene esattamente un sito). Dal momento che stiamo aggiungendo 3 siti ogni volta che ripetiamo, finiamo con i n - 2 <= N <= nsiti.

Ho creato un .webm del processo di suddivisione per questa immagine, che non posso incorporare, ma è qui ; il colore in ogni sottosezione è determinato dalla varianza ponderata. (Ho fatto lo stesso tipo di video per lo yoshi con sfondo bianco, per confronto, con la tabella dei colori invertita in modo che vada verso il bianco anziché il nero; è qui .) Il prodotto finale della suddivisione è simile al seguente:

abc

Una volta che abbiamo il nostro elenco di suddivisioni, esaminiamo ogni suddivisione. La posizione finale del sito è la posizione del minimo dell'immagine Prewitt, ovvero il pixel meno "spigoloso" e il colore della sezione è il colore di quel pixel; ecco l'immagine originale, con i siti contrassegnati:

abc

Quindi, usiamo il built-in triangulateper calcolare la triangolazione Delaunay dei siti e il built-in voronoiper definire i vertici di ciascun poligono Voronoi, prima di disegnare ogni poligono in un buffer di immagine nel suo rispettivo colore. Infine, salviamo un'istantanea del buffer dell'immagine.

abc

Il codice:

function subdivide, image, bounds, vars
  ;subdivide a section into 4, and return the 4 subdivisions and the variance of each
  division = list()
  vars = list()
  nx = bounds[2] - bounds[0]
  ny = bounds[3] - bounds[1]
  for i=0,1 do begin
    for j=0,1 do begin
      x = i * nx/2 + bounds[0]
      y = j * ny/2 + bounds[1]
      sub = image[x:x+nx/2-(~(nx mod 2)),y:y+ny/2-(~(ny mod 2))]
      division.add, [x,y,x+nx/2-(~(nx mod 2)),y+ny/2-(~(ny mod 2))]
      vars.add, variance(sub) * n_elements(sub)
    endfor
  endfor
  return, division
end

pro voro_map, n, image, outfile
  sz = size(image, /dim)
  ;first, convert image to greyscale, and then use a Prewitt filter to pick out edges
  edges = prewitt(reform(ct_luminance(image[0,*,*], image[1,*,*], image[2,*,*])))
  ;next, iteratively subdivide the image into sections, using variance to pick
  ;the next subdivision target (variance -> detail) until we've hit N subdivisions
  subdivisions = subdivide(edges, [0,0,sz[1],sz[2]], variances)
  while subdivisions.count() lt (n - 2) do begin
    !null = max(variances.toarray(),target)
    oldsub = subdivisions.remove(target)
    newsub = subdivide(edges, oldsub, vars)
    if subdivisions.count(newsub[0]) gt 0 or subdivisions.count(newsub[1]) gt 0 or subdivisions.count(newsub[2]) gt 0 or subdivisions.count(newsub[3]) gt 0 then stop
    subdivisions += newsub
    variances.remove, target
    variances += vars
  endwhile
  ;now we find the minimum edge value of each subdivision (we want to pick representative 
  ;colors, not edge colors) and use that as the site (with associated color)
  sites = fltarr(2,n)
  colors = lonarr(n)
  foreach sub, subdivisions, i do begin
    slice = edges[sub[0]:sub[2],sub[1]:sub[3]]
    !null = min(slice,target)
    sxy = array_indices(slice, target) + sub[0:1]
    sites[*,i] = sxy
    colors[i] = cgcolor24(image[0:2,sxy[0],sxy[1]])
  endforeach
  ;finally, generate the voronoi map
  old = !d.NAME
  set_plot, 'Z'
  device, set_resolution=sz[1:2], decomposed=1, set_pixel_depth=24
  triangulate, sites[0,*], sites[1,*], tr, connectivity=C
  for i=0,n-1 do begin
    if C[i] eq C[i+1] then continue
    voronoi, sites[0,*], sites[1,*], i, C, xp, yp
    cgpolygon, xp, yp, color=colors[i], /fill, /device
  endfor
  !null = cgsnapshot(file=outfile, /nodialog)
  set_plot, old
end

pro wrapper
  cd, '~/voronoi'
  fs = file_search()
  foreach f,fs do begin
    base = strsplit(f,'.',/extract)
    if base[1] eq 'png' then im = read_png(f) else read_jpeg, f, im
    voro_map,100, im, base[0]+'100.png'
    voro_map,500, im, base[0]+'500.png'
    voro_map,1000,im, base[0]+'1000.png'
  endforeach
end

Chiamalo via voro_map, n, image, output_filename. Ho incluso anche una wrapperprocedura, che ha esaminato ciascuna immagine di prova ed è stata eseguita con 100, 500 e 1000 siti.

Output raccolto qui , e qui ci sono alcune miniature:

n = 100

abc abc abc abc abc abc abc abc abc abc abc abc abc

n = 500

abc abc abc abc abc abc abc abc abc abc abc abc abc

n = 1000

abc abc abc abc abc abc abc abc abc abc abc abc abc


9
Mi piace molto il fatto che questa soluzione metta più punti in aree più complesse, il che è secondo me l'intento e lo distingue dagli altri a questo punto.
alexander-brett,

sì, l'idea di punti raggruppati in dettaglio è ciò che mi ha portato alla raffinatezza adattativa
sirpercival

3
Spiegazione molto accurata e le immagini sono impressionanti! Ho una domanda: sembra che tu abbia immagini molto diverse quando Yoshi è su uno sfondo bianco, dove abbiamo alcune forme strane. Cosa potrebbe causare questo?
BrainSteel,

2
@BrianSteel Immagino i contorni ottenere raccolti come zone ad alta varianza e concentrati sulla inutilmente, e poi altri veramente aree ad alto dettaglio hanno meno punti assegnati a causa di questo.
doppelgreener,

@BrainSteel penso che doppel abbia ragione: c'è un forte vantaggio tra il bordo nero e lo sfondo bianco, che richiede molti dettagli nell'algoritmo. non sono sicuro che sia qualcosa che posso (o, soprattutto, dovrei ) risolvere ...
sirpercival

47

Python 3 + PIL + SciPy, Fuzzy k-significa

from collections import defaultdict
import itertools
import random
import time

from PIL import Image
import numpy as np
from scipy.spatial import KDTree, Delaunay

INFILE = "planet.jpg"
OUTFILE = "voronoi.txt"
N = 3000

DEBUG = True # Outputs extra images to see what's happening
FEATURE_FILE = "features.png"
SAMPLE_FILE = "samples.png"
SAMPLE_POINTS = 20000
ITERATIONS = 10
CLOSE_COLOR_THRESHOLD = 15

"""
Color conversion functions
"""

start_time = time.time()

# http://www.easyrgb.com/?X=MATH
def rgb2xyz(rgb):
  r, g, b = rgb
  r /= 255
  g /= 255
  b /= 255

  r = ((r + 0.055)/1.055)**2.4 if r > 0.04045 else r/12.92
  g = ((g + 0.055)/1.055)**2.4 if g > 0.04045 else g/12.92
  b = ((b + 0.055)/1.055)**2.4 if b > 0.04045 else b/12.92

  r *= 100
  g *= 100
  b *= 100

  x = r*0.4124 + g*0.3576 + b*0.1805
  y = r*0.2126 + g*0.7152 + b*0.0722
  z = r*0.0193 + g*0.1192 + b*0.9505

  return (x, y, z)

def xyz2lab(xyz):
  x, y, z = xyz
  x /= 95.047
  y /= 100
  z /= 108.883

  x = x**(1/3) if x > 0.008856 else 7.787*x + 16/116
  y = y**(1/3) if y > 0.008856 else 7.787*y + 16/116
  z = z**(1/3) if z > 0.008856 else 7.787*z + 16/116

  L = 116*y - 16
  a = 500*(x - y)
  b = 200*(y - z)

  return (L, a, b)

def rgb2lab(rgb):
  return xyz2lab(rgb2xyz(rgb))

def lab2xyz(lab):
  L, a, b = lab
  y = (L + 16)/116
  x = a/500 + y
  z = y - b/200

  y = y**3 if y**3 > 0.008856 else (y - 16/116)/7.787
  x = x**3 if x**3 > 0.008856 else (x - 16/116)/7.787
  z = z**3 if z**3 > 0.008856 else (z - 16/116)/7.787

  x *= 95.047
  y *= 100
  z *= 108.883

  return (x, y, z)

def xyz2rgb(xyz):
  x, y, z = xyz
  x /= 100
  y /= 100
  z /= 100

  r = x* 3.2406 + y*-1.5372 + z*-0.4986
  g = x*-0.9689 + y* 1.8758 + z* 0.0415
  b = x* 0.0557 + y*-0.2040 + z* 1.0570

  r = 1.055 * (r**(1/2.4)) - 0.055 if r > 0.0031308 else 12.92*r
  g = 1.055 * (g**(1/2.4)) - 0.055 if g > 0.0031308 else 12.92*g
  b = 1.055 * (b**(1/2.4)) - 0.055 if b > 0.0031308 else 12.92*b

  r *= 255
  g *= 255
  b *= 255

  return (r, g, b)

def lab2rgb(lab):
  return xyz2rgb(lab2xyz(lab))

"""
Step 1: Read image and convert to CIELAB
"""

im = Image.open(INFILE)
im = im.convert("RGB")
width, height = prev_size = im.size

pixlab_map = {}

for x in range(width):
    for y in range(height):
        pixlab_map[(x, y)] = rgb2lab(im.getpixel((x, y)))

print("Step 1: Image read and converted")

"""
Step 2: Get feature points
"""

def euclidean(point1, point2):
    return sum((x-y)**2 for x,y in zip(point1, point2))**.5


def neighbours(pixel):
    x, y = pixel
    results = []

    for dx, dy in itertools.product([-1, 0, 1], repeat=2):
        neighbour = (pixel[0] + dx, pixel[1] + dy)

        if (neighbour != pixel and 0 <= neighbour[0] < width
            and 0 <= neighbour[1] < height):
            results.append(neighbour)

    return results

def mse(colors, base):
    return sum(euclidean(x, base)**2 for x in colors)/len(colors)

features = []

for x in range(width):
    for y in range(height):
        pixel = (x, y)
        col = pixlab_map[pixel]
        features.append((mse([pixlab_map[n] for n in neighbours(pixel)], col),
                         random.random(),
                         pixel))

features.sort()
features_copy = [x[2] for x in features]

if DEBUG:
    test_im = Image.new("RGB", im.size)

    for i in range(len(features)):
        pixel = features[i][1]
        test_im.putpixel(pixel, (int(255*i/len(features)),)*3)

    test_im.save(FEATURE_FILE)

print("Step 2a: Edge detection-ish complete")

def random_index(list_):
    r = random.expovariate(2)

    while r > 1:
         r = random.expovariate(2)

    return int((1 - r) * len(list_))

sample_points = set()

while features and len(sample_points) < SAMPLE_POINTS:
    index = random_index(features)
    point = features[index][2]
    sample_points.add(point)
    del features[index]

print("Step 2b: {} feature samples generated".format(len(sample_points)))

if DEBUG:
    test_im = Image.new("RGB", im.size)

    for pixel in sample_points:
        test_im.putpixel(pixel, (255, 255, 255))

    test_im.save(SAMPLE_FILE)

"""
Step 3: Fuzzy k-means
"""

def euclidean(point1, point2):
    return sum((x-y)**2 for x,y in zip(point1, point2))**.5

def get_centroid(points):
    return tuple(sum(coord)/len(points) for coord in zip(*points))

def mean_cell_color(cell):
    return get_centroid([pixlab_map[pixel] for pixel in cell])

def median_cell_color(cell):
    # Pick start point out of mean and up to 10 pixels in cell
    mean_col = get_centroid([pixlab_map[pixel] for pixel in cell])
    start_choices = [pixlab_map[pixel] for pixel in cell]

    if len(start_choices) > 10:
        start_choices = random.sample(start_choices, 10)

    start_choices.append(mean_col)

    best_dist = None
    col = None

    for c in start_choices:
        dist = sum(euclidean(c, pixlab_map[pixel])
                       for pixel in cell)

        if col is None or dist < best_dist:
            col = c
            best_dist = dist

    # Approximate median by hill climbing
    last = None

    while last is None or euclidean(col, last) < 1e-6:
        last = col

        best_dist = None
        best_col = None

        for deviation in itertools.product([-1, 0, 1], repeat=3):
            new_col = tuple(x+y for x,y in zip(col, deviation))
            dist = sum(euclidean(new_col, pixlab_map[pixel])
                       for pixel in cell)

            if best_dist is None or dist < best_dist:
                best_col = new_col

        col = best_col

    return col

def random_point():
    index = random_index(features_copy)
    point = features_copy[index]

    dx = random.random() * 10 - 5
    dy = random.random() * 10 - 5

    return (point[0] + dx, point[1] + dy)

centroids = np.asarray([random_point() for _ in range(N)])
variance = {i:float("inf") for i in range(N)}
cluster_colors = {i:(0, 0, 0) for i in range(N)}

# Initial iteration
tree = KDTree(centroids)
clusters = defaultdict(set)

for point in sample_points:
    nearest = tree.query(point)[1]
    clusters[nearest].add(point)

# Cluster!
for iter_num in range(ITERATIONS):
    if DEBUG:
        test_im = Image.new("RGB", im.size)

        for n, pixels in clusters.items():
            color = 0xFFFFFF * (n/N)
            color = (int(color//256//256%256), int(color//256%256), int(color%256))

            for p in pixels:
                test_im.putpixel(p, color)

        test_im.save(SAMPLE_FILE)

    for cluster_num in clusters:
        if clusters[cluster_num]:
            cols = [pixlab_map[x] for x in clusters[cluster_num]]

            cluster_colors[cluster_num] = mean_cell_color(clusters[cluster_num])
            variance[cluster_num] = mse(cols, cluster_colors[cluster_num])

        else:
            cluster_colors[cluster_num] = (0, 0, 0)
            variance[cluster_num] = float("inf")

    print("Clustering (iteration {})".format(iter_num))

    # Remove useless/high variance
    if iter_num < ITERATIONS - 1:
        delaunay = Delaunay(np.asarray(centroids))
        neighbours = defaultdict(set)

        for simplex in delaunay.simplices:
            n1, n2, n3 = simplex

            neighbours[n1] |= {n2, n3}
            neighbours[n2] |= {n1, n3}
            neighbours[n3] |= {n1, n2}

        for num, centroid in enumerate(centroids):
            col = cluster_colors[num]

            like_neighbours = True

            nns = set() # neighbours + neighbours of neighbours

            for n in neighbours[num]:
                nns |= {n} | neighbours[n] - {num}

            nn_far = sum(euclidean(col, cluster_colors[nn]) > CLOSE_COLOR_THRESHOLD
                         for nn in nns)

            if nns and nn_far / len(nns) < 1/5:
                sample_points -= clusters[num]

                for _ in clusters[num]:
                    if features and len(sample_points) < SAMPLE_POINTS:
                        index = random_index(features)
                        point = features[index][3]
                        sample_points.add(point)
                        del features[index]

                clusters[num] = set()

    new_centroids = []

    for i in range(N):
        if clusters[i]:
            new_centroids.append(get_centroid(clusters[i]))
        else:
            new_centroids.append(random_point())

    centroids = np.asarray(new_centroids)
    tree = KDTree(centroids)

    clusters = defaultdict(set)

    for point in sample_points:
        nearest = tree.query(point, k=6)[1]
        col = pixlab_map[point]

        for n in nearest:
            if n < N and euclidean(col, cluster_colors[n])**2 <= variance[n]:
                clusters[n].add(point)
                break

        else:
            clusters[nearest[0]].add(point)

print("Step 3: Fuzzy k-means complete")

"""
Step 4: Output
"""

for i in range(N):
    if clusters[i]:
        centroids[i] = get_centroid(clusters[i])

centroids = np.asarray(centroids)
tree = KDTree(centroids)
color_clusters = defaultdict(set)

# Throw back on some sample points to get the colors right
all_points = [(x, y) for x in range(width) for y in range(height)]

for pixel in random.sample(all_points, int(min(width*height, 5 * SAMPLE_POINTS))):
    nearest = tree.query(pixel)[1]
    color_clusters[nearest].add(pixel)

with open(OUTFILE, "w") as outfile:
    for i in range(N):
        if clusters[i]:
            centroid = tuple(centroids[i])          
            col = tuple(x/255 for x in lab2rgb(median_cell_color(color_clusters[i] or clusters[i])))
            print(" ".join(map(str, centroid + col)), file=outfile)

print("Done! Time taken:", time.time() - start_time)

L'algoritmo

L'idea di base è che k-cluster raggruppa naturalmente le partizioni dell'immagine in celle Voronoi, poiché i punti sono legati al centroide più vicino. Tuttavia, dobbiamo in qualche modo aggiungere i colori come vincolo.

Innanzitutto convertiamo ogni pixel nello spazio colore Lab , per una migliore manipolazione del colore.

Quindi facciamo una sorta di "rilevamento dei bordi dei poveri". Per ogni pixel, osserviamo i suoi vicini ortogonali e diagonali e calcoliamo la differenza di colore al quadrato medio. Ordiniamo quindi tutti i pixel in base a questa differenza, con i pixel più simili ai loro vicini nella parte anteriore dell'elenco e i pixel più dissimili dai loro vicini nella parte posteriore (ovvero, è più probabile che siano un punto limite). Ecco un esempio per il pianeta, dove più è luminoso il pixel, più è diverso dai suoi vicini:

inserisci qui la descrizione dell'immagine

(C'è un chiaro modello a griglia sull'output di rendering sopra. Secondo @randomra, ciò è probabilmente dovuto alla codifica JPG con perdita di dati o alla compressione delle immagini imgur.)

Quindi, usiamo questo ordinamento di pixel per campionare un gran numero di punti da raggruppare. Usiamo una distribuzione esponenziale, dando priorità ai punti che sono più simili e "interessanti".

inserisci qui la descrizione dell'immagine

Per il raggruppamento, selezioniamo prima i Ncentroidi, scelti a caso usando la stessa distribuzione esponenziale di cui sopra. Viene eseguita un'iterazione iniziale e per ciascuno dei cluster risultanti assegniamo un colore medio e una soglia di variazione del colore. Quindi per una serie di iterazioni, noi:

  • Costruisci la triangolazione Delaunay dei centroidi, in modo da poter facilmente interrogare i vicini ai centroidi.
  • Usa la triangolazione per rimuovere i centroidi che sono di colore vicino alla maggior parte (> 4/5) dei loro vicini e vicini del vicino messi insieme. Vengono rimossi anche tutti i punti campione associati e vengono aggiunti nuovi centroidi di sostituzione e punti campione. Questo passaggio tenta di forzare l'algoritmo a posizionare più cluster dove sono necessari dettagli.
  • Costruisci un albero kd per i nuovi centroidi, in modo da poter facilmente interrogare i centroidi più vicini a qualsiasi punto di campionamento.
  • Utilizzare l'albero per assegnare ciascun punto campione a uno dei 6 centroidi più vicini (6 scelti empiricamente). Un centroide accetterà un punto campione solo se il colore del punto rientra nella soglia di variazione colore del centroide. Cerchiamo di assegnare ciascun punto campione al primo centroide accettante, ma se ciò non è possibile, lo assegniamo semplicemente al centroide più vicino. La "sfocatura" dell'algoritmo deriva da questo passaggio, poiché è possibile che i cluster si sovrappongano.
  • Ricalcola i centroidi.

inserisci qui la descrizione dell'immagine

(Fare clic per ingrandire)

Infine, campioniamo un gran numero di punti, questa volta usando una distribuzione uniforme. Usando un altro kd-tree, assegniamo ogni punto al suo centroide più vicino, formando cluster. Quindi approssimiamo il colore mediano di ogni cluster usando un algoritmo di arrampicata in collina, dando i colori finali delle nostre cellule (idea per questo passaggio grazie a @PhiNotPi e @ MartinBüttner).

inserisci qui la descrizione dell'immagine

Appunti

Oltre a produrre un file di testo per lo snippet ( OUTFILE), se DEBUGimpostato Truesul programma, verranno anche emessi e sovrascritti le immagini sopra. L'algoritmo richiede un paio di minuti per ogni immagine, quindi è un buon modo per controllare i progressi che non aggiunge molto al tempo di esecuzione.

Output di esempio

N = 32:

inserisci qui la descrizione dell'immagine

N = 100:

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

N = 1000:

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

N = 3000:

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine


1
Mi piace davvero quanto sono riusciti i tuoi Yoshis bianchi.
Max

26

Mathematica, Celle casuali

Questa è la soluzione di base, quindi hai un'idea del minimo che ti sto chiedendo. Dato il nome del file (localmente o come URL) in filee N in n, il codice seguente seleziona semplicemente N pixel casuali e utilizza i colori trovati in quei pixel. Questo è davvero ingenuo e non funziona incredibilmente bene, ma voglio che voi ragazzi lo battiate dopo tutto. :)

data = ImageData@Import@file;
dims = Dimensions[data][[1 ;; 2]]
{Reverse@#, data[[##]][[1 ;; 3]] & @@ Floor[1 + #]} &[dims #] & /@ 
 RandomReal[1, {n, 2}]

Ecco tutte le immagini di prova per N = 100 (tutte le immagini si collegano a versioni più grandi):

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

Come puoi vedere, questi sono essenzialmente inutili. Sebbene possano avere un certo valore artistico, in modo espressionista, le immagini originali sono appena riconoscibili.

Per N = 500 , la situazione è migliorata un po ', ma ci sono ancora manufatti molto strani, le immagini sembrano sbiadite e molte celle vengono sprecate in regioni senza dettagli:

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

Il tuo turno!


Non sono un buon programmatore, ma mio dio queste immagini sono bellissime. Idea fantastica!
Faraz Masroor,

Qualche motivo per Dimensions@ImageDatae no ImageDimensions? Puoi evitare del ImageDatatutto il rallentamento usando PixelValue.
2012campo

@ 2012rcampion Nessun motivo, non sapevo che entrambe le funzioni esistessero. Potrei risolverlo più tardi e anche cambiare le immagini di esempio con i valori N consigliati.
Martin Ender,

23

matematica

Sappiamo tutti che Martin ama Mathematica, quindi fatemi provare con Mathematica.

Il mio algoritmo utilizza punti casuali dai bordi dell'immagine per creare un diagramma voronoi iniziale. Il diagramma viene quindi impostato da una regolazione iterativa della mesh con un semplice filtro medio. Ciò fornisce immagini con un'alta densità cellulare vicino a regioni ad alto contrasto e cellule visivamente gradevoli senza angoli folli.

Le seguenti immagini mostrano un esempio del processo in azione. Il divertimento è un po 'rovinato dall'antialiasing di Mathematicas, ma otteniamo grafica vettoriale, che deve valere la pena.

Questo algoritmo, senza il campionamento casuale, può essere trovato nella VoronoiMeshdocumentazione qui .

inserisci qui la descrizione dell'immagine inserisci qui la descrizione dell'immagine

Immagini di prova (100.300.1000.3000)

Codice

VoronoiImage[img_, nSeeds_, iterations_] := Module[{
    i = img,
    edges = EdgeDetect@img,
    voronoiRegion = Transpose[{{0, 0}, ImageDimensions[img]}],
    seeds, voronoiInitial, voronoiRelaxed
    },
   seeds = RandomChoice[ImageValuePositions[edges, White], nSeeds];
   voronoiInitial = VoronoiMesh[seeds, voronoiRegion];
   voronoiRelaxed = 
    Nest[VoronoiMesh[Mean @@@ MeshPrimitives[#, 2], voronoiRegion] &, 
     voronoiInitial, iterations];
   Graphics[Table[{RGBColor[ImageValue[img, Mean @@ mp]], mp}, 
     {mp,MeshPrimitives[voronoiRelaxed, 2]}]]
   ];

Bel lavoro per un primo post! :) Potresti provare l'immagine di prova Voronoi con 32 celle (che è il numero di celle nell'immagine stessa).
Martin Ender,

Grazie! Suppongo che il mio algoritmo funzionerà terribilmente su questo esempio. I semi verranno inizializzati sui bordi delle cellule e la ricorsione non renderà molto meglio;)
zampa

Nonostante il tasso più lento di convergere all'immagine originale, trovo che il tuo algoritmo produca un risultato molto artistico! (come una versione migliorata delle opere di Georges Seurat). Ottimo lavoro!
Neizod,

Puoi anche ottenere colori poligonali interpolati dall'aspetto vetroso cambiando le tue linee finali inGraphics@Table[ Append[mp, VertexColors -> RGBColor /@ ImageValue[img, First[mp]]], {mp, MeshPrimitives[voronoiRelaxed, 2]}]
Istogrammi

13

Python + SciPy + emcee

L'algoritmo che ho usato è il seguente:

  1. Ridimensiona le immagini a dimensioni ridotte (~ 150 pixel)
  2. Crea un'immagine non mascherata dei valori massimi del canale (questo aiuta a non raccogliere le aree bianche in modo troppo forte).
  3. Prendi il valore assoluto.
  4. Scegli punti casuali con una probabilità proporzionale a questa immagine. Questo sceglie i punti su entrambi i lati delle discontinuità.
  5. Perfeziona i punti scelti per abbassare una funzione di costo. La funzione è il massimo della somma delle deviazioni quadrate nei canali (aiutando nuovamente i colori solidi e non solo il bianco solido). Ho usato male Markov Chain Monte Carlo con il modulo emcee (altamente raccomandato) come ottimizzatore. La procedura si interrompe quando non si riscontra alcun nuovo miglioramento dopo le iterazioni della catena N.

L'algoritmo sembra funzionare molto bene. Sfortunatamente può funzionare sensibilmente solo su immagini di dimensioni ridotte. Non ho avuto il tempo di prendere i punti Voronoi e applicarli alle immagini più grandi. Potrebbero essere raffinati a questo punto. Avrei anche potuto eseguire MCMC più a lungo per ottenere minimi migliori. Il punto debole dell'algoritmo è che è piuttosto costoso. Non ho avuto il tempo di aumentare oltre i 1000 punti e un paio di immagini dei 1000 punti sono ancora in fase di perfezionamento.

(tasto destro del mouse e visualizza l'immagine per ottenere una versione più grande)

Miniature per 100, 300 e 1000 punti

I collegamenti alle versioni più grandi sono http://imgur.com/a/2IXDT#9 (100 punti), http://imgur.com/a/bBQ7q (300 punti) e http://imgur.com/a/rr8wJ (1000 punti)

#!/usr/bin/env python

import glob
import os

import scipy.misc
import scipy.spatial
import scipy.signal
import numpy as N
import numpy.random as NR
import emcee

def compute_image(pars, rimg, gimg, bimg):
    npts = len(pars) // 2
    x = pars[:npts]
    y = pars[npts:npts*2]
    yw, xw = rimg.shape

    # exit if points are too far away from image, to stop MCMC
    # wandering off
    if(N.any(x > 1.2*xw) or N.any(x < -0.2*xw) or
       N.any(y > 1.2*yw) or N.any(y < -0.2*yw)):
        return None

    # compute tesselation
    xy = N.column_stack( (x, y) )
    tree = scipy.spatial.cKDTree(xy)

    ypts, xpts = N.indices((yw, xw))
    queryxy = N.column_stack((N.ravel(xpts), N.ravel(ypts)))

    dist, idx = tree.query(queryxy)

    idx = idx.reshape(yw, xw)
    ridx = N.ravel(idx)

    # tesselate image
    div = 1./N.clip(N.bincount(ridx), 1, 1e99)
    rav = N.bincount(ridx, weights=N.ravel(rimg)) * div
    gav = N.bincount(ridx, weights=N.ravel(gimg)) * div
    bav = N.bincount(ridx, weights=N.ravel(bimg)) * div

    rout = rav[idx]
    gout = gav[idx]
    bout = bav[idx]
    return rout, gout, bout

def compute_fit(pars, img_r, img_g, img_b):
    """Return fit statistic for parameters."""
    # get model
    retn = compute_image(pars, img_r, img_g, img_b)
    if retn is None:
        return -1e99
    model_r, model_g, model_b = retn

    # maximum squared deviation from one of the chanels
    fit = max( ((img_r-model_r)**2).sum(),
               ((img_g-model_g)**2).sum(),
               ((img_b-model_b)**2).sum() )

    # return fake log probability
    return -fit

def convgauss(img, sigma):
    """Convolve image with a Gaussian."""
    size = 3*sigma
    kern = N.fromfunction(
        lambda y, x: N.exp( -((x-size/2)**2+(y-size/2)**2)/2./sigma ),
        (size, size))
    kern /= kern.sum()
    out = scipy.signal.convolve2d(img.astype(N.float64), kern, mode='same')
    return out

def process_image(infilename, outroot, npts):
    img = scipy.misc.imread(infilename)
    img_r = img[:,:,0]
    img_g = img[:,:,1]
    img_b = img[:,:,2]

    # scale down size
    maxdim = max(img_r.shape)
    scale = int(maxdim / 150)
    img_r = img_r[::scale, ::scale]
    img_g = img_g[::scale, ::scale]
    img_b = img_b[::scale, ::scale]

    # make unsharp-masked image of input
    img_tot = N.max((img_r, img_g, img_b), axis=0)
    img1 = convgauss(img_tot, 2)
    img2 = convgauss(img_tot, 32)
    diff = N.abs(img1 - img2)
    diff = diff/diff.max()
    diffi = (diff*255).astype(N.int)
    scipy.misc.imsave(outroot + '_unsharp.png', diffi)

    # create random points with a probability distribution given by
    # the unsharp-masked image
    yw, xw = img_r.shape
    xpars = []
    ypars = []
    while len(xpars) < npts:
        ypar = NR.randint(int(yw*0.02),int(yw*0.98))
        xpar = NR.randint(int(xw*0.02),int(xw*0.98))
        if diff[ypar, xpar] > NR.rand():
            xpars.append(xpar)
            ypars.append(ypar)

    # initial parameters to model
    allpar = N.concatenate( (xpars, ypars) )

    # set up MCMC sampler with parameters close to each other
    nwalkers = npts*5  # needs to be at least 2*number of parameters+2
    pos0 = []
    for i in xrange(nwalkers):
        pos0.append(NR.normal(0,1,allpar.shape)+allpar)

    sampler = emcee.EnsembleSampler(
        nwalkers, len(allpar), compute_fit,
        args=[img_r, img_g, img_b],
        threads=4)

    # sample until we don't find a better fit
    lastmax = -N.inf
    ct = 0
    ct_nobetter = 0
    for result in sampler.sample(pos0, iterations=10000, storechain=False):
        print ct
        pos, lnprob = result[:2]
        maxidx = N.argmax(lnprob)

        if lnprob[maxidx] > lastmax:
            # write image
            lastmax = lnprob[maxidx]
            mimg = compute_image(pos[maxidx], img_r, img_g, img_b)
            out = N.dstack(mimg).astype(N.int32)
            out = N.clip(out, 0, 255)
            scipy.misc.imsave(outroot + '_binned.png', out)

            # save parameters
            N.savetxt(outroot + '_param.dat', scale*pos[maxidx])

            ct_nobetter = 0
            print(lastmax)

        ct += 1
        ct_nobetter += 1
        if ct_nobetter == 60:
            break

def main():
    for npts in 100, 300, 1000:
        for infile in sorted(glob.glob(os.path.join('images', '*'))):
            print infile
            outroot = '%s/%s_%i' % (
                'outdir',
                os.path.splitext(os.path.basename(infile))[0], npts)

            # race condition!
            lock = outroot + '.lock'
            if os.path.exists(lock):
                continue
            with open(lock, 'w') as f:
                pass

            process_image(infile, outroot, npts)

if __name__ == '__main__':
    main()

Le immagini non mascherate sembrano le seguenti. I punti casuali vengono selezionati dall'immagine se un numero casuale è inferiore al valore dell'immagine (normato a 1):

Immagine di Saturno mascherata non affilata

Pubblicherò immagini più grandi e i punti Voronoi se avrò più tempo.

Modifica: se aumenti il ​​numero di walker a 100 * npti, modifica la funzione di costo in modo che diventi uno dei quadrati delle deviazioni in tutti i canali e attendi a lungo (aumentando il numero di iterazioni per interrompere il ciclo in 200), è possibile fare delle belle immagini con soli 100 punti:

Immagine 11, 100 punti Immagine 2, 100 punti Immagine 4, 100 punti Immagine 10, 100 punti


3

Utilizzo dell'energia dell'immagine come una mappa a peso corporeo

Nel mio approccio a questa sfida, volevo un modo per mappare la "pertinenza" di una particolare area dell'immagine alla probabilità che un particolare punto sarebbe stato scelto come centroide Voronoi. Tuttavia, volevo ancora preservare l'atmosfera artistica del mosaico Voronoi scegliendo casualmente punti immagine. Inoltre, volevo operare su immagini di grandi dimensioni, quindi non perdo nulla nel processo di downsampling. Il mio algoritmo è più o meno così:

  1. Per ogni immagine, crea una mappa di nitidezza. La mappa di nitidezza è definita dall'energia normalizzata dell'immagine (o dal quadrato del segnale ad alta frequenza dell'immagine). Un esempio è simile al seguente:

Mappa della nitidezza

  1. Genera un numero di punti dall'immagine, prendendo il 70 percento dai punti nella mappa della nitidezza e il 30 percento da tutti gli altri punti. Ciò significa che i punti vengono campionati in modo più denso da parti dell'immagine ad alto dettaglio.
  2. Colore!

risultati

N = 100, 500, 1000, 3000

Immagine 1, N = 100 Immagine 1, N = 500 Immagine 1, N = 1000 Immagine 1, N = 3000

Immagine 2, N = 100 Immagine 2, N = 500 Immagine 2, N = 1000 Immagine 2, N = 3000

Immagine 3, N = 100 Immagine 3, N = 500 Immagine 3, N = 1000 Immagine 3, N = 3000

Immagine 4, N = 100 Immagine 4, N = 500 Immagine 4, N = 1000 Immagine 4, N = 3000

Immagine 5, N = 100 Immagine 5, N = 500 Immagine 5, N = 1000 Immagine 5, N = 3000

Immagine 6, N = 100 Immagine 6, N = 500 Immagine 6, N = 1000 Immagine 6, N = 3000

Immagine 7, N = 100 Immagine 7, N = 500 Immagine 7, N = 1000 Immagine 7, N = 3000

Immagine 8, N = 100 Immagine 8, N = 500 Immagine 8, N = 1000 Immagine 8, N = 3000

Immagine 9, N = 100 Immagine 9, N = 500 Immagine 9, N = 1000 Immagine 9, N = 3000

Immagine 10, N = 100 Immagine 10, N = 500 Immagine 10, N = 1000 Immagine 10, N = 3000

Immagine 11, N = 100 Immagine 11, N = 500 Immagine 11, N = 1000 Immagine 11, N = 3000

Immagine 12, N = 100 Immagine 12, N = 500 Immagine 12, N = 1000 Immagine 12, N = 3000

Immagine 13, N = 100 Immagine 13, N = 500 Immagine 13, N = 1000 Immagine 13, N = 3000

Immagine 14, N = 100 Immagine 14, N = 500 Immagine 14, N = 1000 Immagine 14, N = 3000


14
Ti dispiacerebbe a) incluso il codice sorgente utilizzato per generare questo, e b) collegare ciascuna miniatura all'immagine a dimensione intera?
Martin Ender,
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.