Zeichnen Sie ein Bild als Voronoi-Karte

170

Dank an Calvins Hobbys, die meine Herausforderungsidee in die richtige Richtung gelenkt haben.

Betrachten Sie eine Reihe von Punkten in der Ebene, die wir Sites nennen , und ordnen Sie jeder Site eine Farbe zu. Jetzt können Sie die gesamte Ebene malen, indem Sie jeden Punkt mit der Farbe der nächstgelegenen Stelle einfärben. Dies wird als Voronoi-Karte (oder Voronoi-Diagramm ) bezeichnet. Grundsätzlich können Voronoi-Karten für jede Entfernungsmetrik definiert werden, aber wir verwenden einfach die übliche euklidische Entfernung r = √(x² + y²). ( Hinweis: Sie müssen nicht unbedingt wissen, wie man eines davon berechnet und rendert, um in dieser Herausforderung bestehen zu können.)

Hier ist ein Beispiel mit 100 Sites:

Bildbeschreibung hier eingeben

Wenn Sie eine Zelle betrachten, befinden sich alle Punkte in dieser Zelle näher an der entsprechenden Stelle als an einer anderen Stelle.

Ihre Aufgabe ist es, ein bestimmtes Bild mit einer solchen Voronoi-Karte anzunähern. Sie sind das Bild in jedem geeigneten Rastergrafikformat, sowie eine ganze Zahl gegeben N . Sie sollten dann bis zu N Standorte und eine Farbe für jeden Standort erstellen , sodass die auf diesen Standorten basierende Voronoi-Karte dem Eingabebild so nahe wie möglich kommt.

Sie können das Stapel-Snippet unten in dieser Herausforderung verwenden, um eine Voronoi-Karte aus Ihrer Ausgabe zu rendern, oder Sie können sie selbst rendern, wenn Sie dies bevorzugen.

Sie können integrierte Funktionen oder Funktionen von Drittanbietern verwenden, um eine Voronoi-Karte von einer Reihe von Standorten aus zu berechnen (falls erforderlich).

Dies ist ein Beliebtheitswettbewerb, daher gewinnt die Antwort mit den meisten Netto-Stimmen. Die Wähler werden aufgefordert, die Antworten nach zu beurteilen

  • wie gut die Originalbilder und ihre Farben angenähert sind.
  • Wie gut der Algorithmus auf verschiedenen Arten von Bildern funktioniert.
  • wie gut der Algorithmus funktioniert für kleine N .
  • ob der Algorithmus Punkte in Bereichen des Bildes, die mehr Details erfordern, adaptiv gruppiert.

Bilder testen

Hier sind einige Bilder zum Testen Ihres Algorithmus (einige unserer üblichen Verdächtigen, einige neue). Klicken Sie auf die Bilder für größere Versionen.

Große Welle Igel Strand Cornell Saturn Braunbär Yoshi Mandrill Krebsnebel Geobits 'Kind Wasserfall Schrei

Der Strand in der ersten Reihe wurde von Olivia Bell gezeichnet und mit ihrer Erlaubnis eingeschlossen.

Wenn Sie eine zusätzliche Herausforderung möchten, probieren Sie Yoshi mit einem weißen Hintergrund aus und achten Sie darauf, dass seine Bauchlinie stimmt.

Sie finden alle diese Testbilder in dieser Bildergalerie, wo Sie sie alle als zip-Datei herunterladen können. Das Album enthält auch ein zufälliges Voronoi-Diagramm als weiteren Test. Als Referenz hier sind die Daten, die es erzeugt haben .

Bitte fügen Sie Beispieldiagramme für eine Vielzahl verschiedener Bilder und N , z. B. 100, 300, 1000, 3000 (sowie Pastebins zu einigen der entsprechenden Zellenspezifikationen) bei. Sie können schwarze Ränder zwischen den Zellen nach Belieben verwenden oder weglassen (dies kann auf einigen Bildern besser aussehen als auf anderen). Fügen Sie die Websites jedoch nicht hinzu (außer in einem separaten Beispiel, wenn Sie natürlich erläutern möchten, wie Ihre Website-Platzierung funktioniert).

Wenn Sie eine große Anzahl von Ergebnissen anzeigen möchten , können Sie auf imgur.com eine Galerie erstellen , um die Größe der Antworten angemessen zu halten. Alternativ können Sie Thumbnails in Ihren Beitrag einfügen und sie mit größeren Bildern verknüpfen, wie ich es in meiner Referenzantwort getan habe . Sie können die kleinen Thumbnails erhalten, indem Sie san den Dateinamen im Link imgur.com anhängen (z . B. I3XrT.png-> I3XrTs.png). Sie können auch andere Testbilder verwenden, wenn Sie etwas Schönes finden.

Renderer

Fügen Sie Ihre Ausgabe in das folgende Stapel-Snippet ein, um Ihre Ergebnisse zu rendern. Das genaue Listenformat ist irrelevant, solange jede Zelle durch 5 Gleitkommazahlen in der Reihenfolge angegeben wird x y r g b, in der xund ydie Koordinaten des Zellstandorts und r g bdie roten, grünen und blauen Farbkanäle im Bereich sind 0 ≤ r, g, b ≤ 1.

Das Snippet bietet Optionen zum Angeben einer Linienbreite der Zellenkanten und zum Anzeigen der Zellenstandorte (letztere hauptsächlich zu Debugging-Zwecken). Beachten Sie jedoch, dass die Ausgabe nur dann neu gerendert wird, wenn sich die Zellenspezifikationen ändern. Wenn Sie also einige der anderen Optionen ändern, fügen Sie den Zellen ein Leerzeichen hinzu.

Dank an Raymond Hill für das Schreiben dieser wirklich schönen JS Voronoi-Bibliothek .

Verwandte Herausforderungen

Martin Ender
quelle
5
@frogeyedpeas Durch einen Blick auf die Stimmen, die Sie erhalten. ;) Dies ist ein Beliebtheitswettbewerb. Es gibt nicht unbedingt den besten Weg. Die Idee ist, dass Sie versuchen, es so gut wie möglich zu machen, und die Abstimmungen zeigen, ob die Leute der Meinung sind, dass Sie gute Arbeit geleistet haben. Zugegeben, darin liegt eine gewisse Subjektivität. Werfen Sie einen Blick auf die damit verbundenen Herausforderungen, die ich verbunden habe, oder auf diese . Sie werden sehen, dass es in der Regel eine Vielzahl von Ansätzen gibt, aber das Abstimmungssystem hilft, die besseren Lösungen nach oben zu bringen und über einen Gewinner zu entscheiden.
Martin Ender
3
Olivia billigt die bisher eingereichten Annäherungen an ihren Strand.
Alex A.
3
@AlexA. Devon billigt einige der bisher eingereichten Näherungen seines Gesichts. Er ist kein großer Fan einer der n = 100 Versionen;)
Geobits
1
@ Geobits: Er wird verstehen, wenn er älter ist.
Alex A.
1
Hier ist eine Seite über eine Centroidal Voronoi-basierte Punktiertechnik . Eine gute Inspirationsquelle (in der zugehörigen Masterarbeit werden mögliche Verbesserungen des Algorithmus ausführlich besprochen).
Job

Antworten:

112

Python + Scipy + Scikit-Image , gewichtetes Poisson-Disc-Sampling

Meine Lösung ist ziemlich komplex. Ich bearbeite das Bild vorab, um Rauschen zu entfernen und eine Abbildung zu erhalten, wie 'interessant' jeder Punkt ist (unter Verwendung einer Kombination aus lokaler Entropie und Kantenerkennung):

Dann wähle ich Abtastpunkte mit einer Poissonscheibensampling mit einer Verdrehung: Die Entfernung des Kreises wird durch das zuvor bestimmte Gewicht bestimmt.

Sobald ich die Abtastpunkte habe, teile ich das Bild in voronoi Segmente auf und ordne jedem Segment den L * a * b * Durchschnitt der Farbwerte in jedem Segment zu.

Ich habe viele Heuristiken und ich muss auch ein bisschen rechnen, um sicherzustellen, dass die Anzahl der Stichprobenpunkte in der Nähe liegt N. Ich bekomme Ngenau durch ein leichtes Überschießen und dann einige Punkte mit einer Heuristik fallen.

In Bezug auf die Laufzeit ist dieser Filter nicht billig , aber es dauerte mehr als 5 Sekunden, bis ein Bild unten erstellt wurde.

Ohne weiteres:

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!")

Bilder

Jeweils N100, 300, 1000 und 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

orlp
quelle
2
Ich mag das; es sieht ein bisschen aus wie Rauchglas.
BobTheAwesome
3
Ich habe ein bisschen rumgespielt, und Sie erhalten bessere Ergebnisse, insbesondere bei Bildern mit niedrigem Dreieck, wenn Sie denoise_bilatteral durch denoise_tv_bregman ersetzen. Es erzeugt mehr gleichmäßige Flecken in seiner Rauschunterdrückung, was hilft.
LKlevin
@LKlevin Welches Gewicht hast du benutzt?
Orlp
Ich habe 1,0 als Gewicht verwendet.
LKlevin,
65

C ++

Mein Ansatz ist ziemlich langsam, aber ich bin sehr zufrieden mit der Qualität der erzielten Ergebnisse, insbesondere in Bezug auf die Kantenschonung. Hier sind zum Beispiel Yoshi und die Cornell Box mit jeweils nur 1000 Standorten:

Es gibt zwei Hauptteile, die es ticken lassen. Die erste Funktion, die in der evaluate()Funktion enthalten ist, nimmt eine Reihe von Standortkandidaten auf, legt die optimalen Farben für sie fest und gibt eine Punktzahl für das PSNR der gerenderten Voronoi-Tesselation im Vergleich zum Zielbild zurück. Die Farben für jede Site werden durch Mitteln der Zielbildpixel bestimmt, die von der Zelle um die Site abgedeckt werden. Ich verwende den Welford-Algorithmus , um sowohl die beste Farbe für jede Zelle als auch das resultierende PSNR mit nur einem Durchgang über das Bild zu berechnen, indem ich die Beziehung zwischen Varianz, MSE und PSNR ausnütze. Dies reduziert das Problem auf die Suche nach den besten Standorten ohne besondere Berücksichtigung der Farbe.

Der zweite Teil, verkörpert in main(), versucht diese Menge zu finden. Es beginnt mit der zufälligen Auswahl einer Reihe von Punkten. Dann wird in jedem Schritt ein Punkt entfernt (Round-Robin) und ein Satz zufälliger Kandidatenpunkte getestet, um ihn zu ersetzen. Derjenige, der den höchsten PSNR des Bündels ergibt, wird akzeptiert und beibehalten. Tatsächlich bewirkt dies, dass die Site an eine neue Position springt und das Bild im Allgemeinen Stück für Stück verbessert. Beachten Sie, dass der Algorithmus absichtlich nicht die ursprüngliche Position als Kandidat beibehält. Manchmal bedeutet dies, dass der Sprung die Gesamtbildqualität verringert. Wenn Sie dies zulassen, können Sie vermeiden, dass lokale Maxima eingehalten werden. Es gibt auch ein Anhaltekriterium; Das Programm wird beendet, nachdem eine bestimmte Anzahl von Schritten ausgeführt wurde, ohne die bisher besten gefundenen Websites zu verbessern.

Beachten Sie, dass diese Implementierung ziemlich einfach ist und Stunden CPU-Kernzeit in Anspruch nehmen kann, insbesondere wenn die Anzahl der Sites zunimmt. Es berechnet die komplette Voronoi-Karte für jeden Kandidaten neu und Brute Force testet die Entfernung zu allen Standorten für jedes Pixel. Da bei jeder Operation jeweils ein Punkt entfernt und ein anderer hinzugefügt wird, sind die tatsächlichen Änderungen am Bild bei jedem Schritt ziemlich lokal. Es gibt Algorithmen, mit denen ein Voronoi-Diagramm effizient inkrementell aktualisiert werden kann, und ich glaube, sie würden diesem Algorithmus eine enorme Geschwindigkeit verleihen. Für diesen Wettbewerb habe ich mich jedoch entschieden, die Dinge einfach und brachial zu halten.

Code

#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;
 }

Laufen

Das Programm ist eigenständig und hat keine externen Abhängigkeiten über die Standardbibliothek hinaus, erfordert jedoch Bilder im binären PPM- Format. Ich verwende ImageMagick , um Bilder in PPM zu konvertieren, obwohl es auch GIMP und einige andere Programme können.

Speichern Sie das Programm zum Kompilieren unter voronoi.cppund führen Sie dann Folgendes aus:

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

Ich erwarte, dass es wahrscheinlich unter Windows mit den neuesten Versionen von Visual Studio funktioniert, obwohl ich dies nicht ausprobiert habe. Sie sollten sicherstellen, dass Sie mit C ++ 11 oder höher kompilieren und OpenMP aktiviert ist, wenn Sie dies tun. OpenMP ist nicht zwingend erforderlich, trägt aber wesentlich dazu bei, die Ausführungszeiten erträglicher zu machen.

Um es auszuführen, mache etwas wie:

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

Die spätere Datei wird mit den Site-Daten aktualisiert. Die erste Zeile enthält die Breite und Höhe des Bildes, gefolgt von Zeilen mit x-, y-, r-, g- und b-Werten, die zum Kopieren und Einfügen in den Javascript-Renderer in der Problembeschreibung geeignet sind.

Mit den drei Konstanten oben im Programm können Sie die Geschwindigkeit im Verhältnis zur Qualität einstellen. Der decimationFaktor vergröbert das Zielbild bei der Bewertung einer Reihe von Standorten für Farbe und PSNR. Je höher der Wert, desto schneller wird das Programm ausgeführt. Wenn Sie den Wert auf 1 setzen, wird das Bild in voller Auflösung verwendet. Die candidatesKonstante steuert, wie viele Kandidaten für jeden Schritt getestet werden sollen. Je höher, desto besser ist die Chance, eine gute Stelle zum Springen zu finden, aber desto langsamer wird das Programm. Schließlich terminationist es die Anzahl der Schritte, die das Programm ausführen kann, ohne seine Ausgabe zu verbessern, bevor es beendet wird. Erhöhen kann zu besseren Ergebnissen führen, dauert jedoch geringfügig länger.

Bilder

N = 100, 300, 1000 und 3000:

Boojum
quelle
1
Das hätte IMO gewinnen sollen - viel besser als meins.
Orlp
1
@orlp - Danke! Um fair zu sein, hast du deine viel früher gepostet und es läuft viel schneller. Geschwindigkeit zählt!
Boojum
1
Nun, meins ist keine wirkliche Antwort auf eine Voronoi-Karte :) Es ist ein wirklich sehr guter Abtastalgorithmus, aber die Umwandlung von Abtastpunkten in Voronoi-Sites ist eindeutig nicht optimal.
Orlp
55

IDL, adaptive Verfeinerung

Diese Methode ist inspiriert von Adaptive Mesh Refinement aus astronomischen Simulationen sowie Subdivision Surface . Dies ist die Art von Aufgabe, auf die sich IDL rühmt, was Sie an der großen Anzahl integrierter Funktionen erkennen können, die ich verwenden konnte. : D

Ich habe einige der Zwischenprodukte für das Yoshi-Testbild mit schwarzem Hintergrund ausgegeben n = 1000.

Zuerst führen wir eine Graustufendarstellung für das Bild durch (mithilfe von ct_luminance) und wenden einen Prewitt-Filter an ( prewittsiehe Wikipedia ), um eine gute Kantenerkennung zu erzielen:

ABC ABC

Dann kommt die eigentliche Grunzarbeit: Wir unterteilen das Bild in 4 und messen die Varianz in jedem Quadranten im gefilterten Bild. Unsere Varianz wird durch die Größe der Unterteilung (die in diesem ersten Schritt gleich ist) gewichtet, damit "kantige" Regionen mit hoher Varianz nicht immer kleiner und kleiner unterteilt werden. Anschließend verwenden wir die gewichtete Varianz, um Unterteilungen mit mehr Details anzustreben, und unterteilen jeden detailreichen Abschnitt iterativ in weitere vier Abschnitte, bis wir die Zielanzahl von Standorten erreicht haben (jede Unterteilung enthält genau einen Standort). Da wir jedes Mal, wenn wir iterieren, 3 Sites hinzufügen, erhalten wir letztendlich n - 2 <= N <= nSites.

Ich habe für dieses Bild ein .webm des Unterteilungsprozesses erstellt, das ich nicht einbetten kann, aber es ist hier . Die Farbe in jedem Unterabschnitt wird durch die gewichtete Varianz bestimmt. (Ich habe zum Vergleich die gleiche Art von Video für das Weiß-Hintergrund-Yoshi gemacht, wobei die Farbtabelle umgekehrt wurde, sodass es in Richtung Weiß statt Schwarz geht. Es ist hier .) Das Endprodukt der Unterteilung sieht folgendermaßen aus:

ABC

Sobald wir unsere Unterteilungsliste haben, durchlaufen wir jede Unterteilung. Der endgültige Standort ist die Position des Minimums des Prewitt-Bildes, dh des am wenigsten "kantigen" Pixels, und die Farbe des Abschnitts ist die Farbe dieses Pixels; Hier ist das Originalbild mit den markierten Websites:

ABC

Dann verwenden wir die integrierte Funktion triangulate, um die Delaunay-Triangulation der Sites zu berechnen, und die integrierte Funktion voronoi, um die Eckpunkte jedes Voronoi-Polygons zu definieren, bevor jedes Polygon in seiner jeweiligen Farbe in einen Bildpuffer gezeichnet wird. Schließlich speichern wir einen Schnappschuss des Bildpuffers.

ABC

Der Code:

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

Rufen Sie dies über voro_map, n, image, output_filename. Ich habe auch eine wrapperProzedur eingefügt, die jedes Testbild durchlief und mit 100, 500 und 1000 Sites lief.

Die hier gesammelten Ausgaben und hier einige Miniaturansichten:

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

sirpercival
quelle
9
Ich mag die Tatsache wirklich, dass diese Lösung mehr Punkte in komplexeren Bereichen setzt, was meiner Meinung nach die Absicht ist, und sie an dieser Stelle von den anderen unterscheidet.
Alexander-Brett
Ja, die Idee der
detailgruppierten
3
Sehr ordentliche Erklärung, und die Bilder sind beeindruckend! Ich habe eine Frage - Es sieht so aus, als würden Sie sehr unterschiedliche Bilder erhalten, wenn Yoshi auf einem weißen Hintergrund ist, auf dem wir einige seltsame Formen haben. Was könnte das verursachen?
BrainSteel
2
@BrianSteel Ich stelle mir vor, dass die Umrisse als Bereiche mit hoher Varianz aufgefasst und unnötigerweise fokussiert werden, und dass dann anderen wirklich detailreichen Bereichen weniger Punkte zugewiesen werden.
Doppelgreener
@BrainSteel Ich denke, doppel ist richtig - es gibt eine starke Kante zwischen dem schwarzen Rand und dem weißen Hintergrund, die nach vielen Details im Algorithmus fragt. Ich bin nicht sicher, ob dies etwas ist, das ich reparieren kann (oder was noch wichtiger ist ) ...
Sirpercival
47

Python 3 + PIL + SciPy, Fuzzy k-means

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)

Der Algorithmus

Die Kernidee ist, dass k-means Clustering das Bild auf natürliche Weise in Voronoi-Zellen aufteilt, da Punkte an den nächstgelegenen Schwerpunkt gebunden sind. Allerdings müssen wir die Farben irgendwie als Einschränkung hinzufügen.

Zuerst konvertieren wir jedes Pixel in den Lab-Farbraum , um eine bessere Farbmanipulation zu erreichen.

Dann machen wir eine Art "Randerkennung für Arme". Für jedes Pixel betrachten wir seine orthogonalen und diagonalen Nachbarn und berechnen die mittlere quadratische Farbdifferenz. Wir sortieren dann alle Pixel nach diesem Unterschied, wobei die Pixel am ähnlichsten zu ihren Nachbarn am Anfang der Liste sind und die Pixel am verschiedensten zu ihren Nachbarn am Ende (dh mit größerer Wahrscheinlichkeit ein Randpunkt). Hier ist ein Beispiel für den Planeten: Je heller das Pixel ist, desto mehr unterscheidet es sich von seinen Nachbarn:

Bildbeschreibung hier eingeben

(Die oben wiedergegebene Ausgabe weist ein klares gitterartiges Muster auf. Laut @randomra liegt dies wahrscheinlich an der verlustbehafteten JPG-Codierung oder an der imgur-Komprimierung der Bilder.)

Als Nächstes verwenden wir diese Pixelreihenfolge, um eine große Anzahl von Punkten abzutasten, die gruppiert werden sollen. Wir verwenden eine Exponentialverteilung, bei der Punkte, die kantenähnlicher und "interessanter" sind, Vorrang haben.

Bildbeschreibung hier eingeben

Für das Clustering wählen wir zunächst die NZentroide aus, die zufällig unter Verwendung derselben Exponentialverteilung wie oben ausgewählt werden. Eine anfängliche Iteration wird durchgeführt, und für jeden der resultierenden Cluster weisen wir eine mittlere Farbe und eine Farbvarianzschwelle zu. Dann für eine Reihe von Iterationen, wir:

  • Erstellen Sie die Delaunay-Triangulation der Zentroide, damit wir Nachbarn leicht nach Zentroiden fragen können.
  • Verwenden Sie die Triangulation, um Zentroide zu entfernen, deren Farbe den meisten (> 4/5) der Nachbarn und der Nachbarn des Nachbarn zusammen nahe kommt. Alle zugehörigen Abtastpunkte werden ebenfalls entfernt, und neue Ersatzschwerpunkte und Abtastpunkte werden hinzugefügt. Dieser Schritt versucht, den Algorithmus zu zwingen, mehr Cluster dort zu platzieren, wo Details benötigt werden.
  • Konstruieren Sie einen kd-Baum für die neuen Zentroide, damit wir die nächsten Zentroide zu jedem Stichprobenpunkt leicht abfragen können.
  • Verwenden Sie den Baum, um jeden Stichprobenpunkt einem der 6 nächsten Zentroide zuzuordnen (6 empirisch ausgewählt). Ein Schwerpunkt akzeptiert nur dann einen Stichprobenpunkt, wenn die Farbe des Punkts innerhalb des Schwellenwerts für die Farbvarianz des Schwerpunkts liegt. Wir versuchen, jeden Abtastpunkt dem ersten akzeptierenden Schwerpunkt zuzuweisen, aber wenn dies nicht möglich ist, weisen wir ihn einfach dem nächstgelegenen Schwerpunkt zu. Die "Unschärfe" des Algorithmus ergibt sich aus diesem Schritt, da es möglich ist, dass sich Cluster überlappen.
  • Berechnen Sie die Zentroide neu.

Bildbeschreibung hier eingeben

(Klicken für volle Größe)

Schließlich wird eine große Anzahl von Punkten unter Verwendung einer gleichmäßigen Verteilung abgetastet. Mit einem anderen kd-Baum ordnen wir jeden Punkt seinem nächsten Schwerpunkt zu und bilden Cluster. Wir approximieren dann die Medianfarbe jedes Clusters mit einem Hill-Climbing-Algorithmus und geben die endgültigen Zellfarben an (Idee für diesen Schritt dank @PhiNotPi und @ MartinBüttner).

Bildbeschreibung hier eingeben

Anmerkungen

Neben einer Textdatei für das Snippet Ausgeben ( OUTFILE), wenn DEBUGgesetzt ist Truedas Programm wird auch ausgegeben , und die Bilder oben überschreiben. Der Algorithmus benötigt für jedes Bild ein paar Minuten, daher ist dies eine gute Möglichkeit, den Fortschritt zu überprüfen, ohne die Laufzeit zu verkürzen.

Beispielausgaben

N = 32:

Bildbeschreibung hier eingeben

N = 100:

Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben

N = 1000:

Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben

N = 3000:

Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben

Sp3000
quelle
1
Mir gefällt sehr gut, wie gut deine weißen Yoshis geworden sind.
Max
26

Mathematica, Random Cells

Dies ist die Grundlösung, damit Sie eine Vorstellung davon bekommen, welches Minimum ich von Ihnen verlange. Mit dem Dateinamen (lokal oder als URL) in fileund N in nwählt der folgende Code einfach N zufällige Pixel aus und verwendet die bei diesen Pixeln gefundenen Farben. Das ist wirklich naiv und funktioniert nicht besonders gut, aber ich möchte, dass ihr das doch besiegt. :)

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

Hier sind alle Testbilder für N = 100 (alle Bilder verweisen auf größere Versionen):

Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben

Wie Sie sehen können, sind diese im Wesentlichen nutzlos. Während sie auf expressionistische Weise einen künstlerischen Wert haben mögen, sind die Originalbilder kaum wiederzuerkennen.

Für N = 500 hat sich die Situation etwas verbessert, aber es gibt immer noch sehr merkwürdige Artefakte, die Bilder sehen verwaschen aus und viele Zellen werden auf Regionen ohne Detail verschwendet:

Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben

Du bist dran!

Martin Ender
quelle
Ich bin kein guter Programmierer, aber mein Gott, diese Bilder sehen wunderschön aus. Tolle Idee!
Faraz Masroor
Irgendein Grund dafür Dimensions@ImageDataund nicht ImageDimensions? Sie können die Langsamkeit ImageDatainsgesamt vermeiden, indem Sie verwenden PixelValue.
2012rcampion
@ 2012rcampion Kein Grund, ich wusste einfach nicht, dass es eine Funktion gibt. Ich könnte das später beheben und auch die Beispielbilder auf die empfohlenen N-Werte ändern.
Martin Ender
23

Mathematica

Wir alle wissen, dass Martin Mathematica liebt. Lassen Sie mich es mit Mathematica versuchen.

Mein Algorithmus verwendet zufällige Punkte von den Bildrändern, um ein erstes Voronoi-Diagramm zu erstellen. Das Diagramm wird dann durch eine iterative Anpassung des Netzes mit einem einfachen Mittelwertfilter verschönert. Dies ergibt Bilder mit hoher Zelldichte in der Nähe kontrastreicher Bereiche und optisch ansprechenden Zellen ohne verrückte Winkel.

Die folgenden Bilder zeigen ein Beispiel für den Vorgang. Der Spaß wird durch Mathematicas schlechtes Antialiasing etwas verdorben, aber wir bekommen Vektorgrafiken, die etwas wert sein müssen.

Dieser Algorithmus ohne die Zufallsstichprobe ist in der VoronoiMeshDokumentation hier zu finden .

Bildbeschreibung hier eingeben Bildbeschreibung hier eingeben

Testbilder (100,300,1000,3000)

Code

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]}]]
   ];
Pfote
quelle
Gute Arbeit für einen ersten Beitrag! :) Vielleicht möchten Sie das Voronoi-Testbild mit 32 Zellen testen (das ist die Anzahl der Zellen im Bild selbst).
Martin Ender
Vielen Dank! Ich vermute, mein Algorithmus wird in diesem Beispiel furchtbar funktionieren. Die Samen werden an den Zellrändern initialisiert und die Rekursion wird es nicht viel besser machen;)
Pfote
Trotz der langsameren Konvergenzrate zum Originalbild finde ich, dass Ihr Algorithmus ein sehr kunstvolles Ergebnis liefert! (Wie eine verbesserte Version von Georges Seurat). Gut gemacht!
Neizod
Sie können auch glasig aussehende interpolierte Polygonfarben erhalten, indem Sie Ihre endgültigen Linien aufGraphics@Table[ Append[mp, VertexColors -> RGBColor /@ ImageValue[img, First[mp]]], {mp, MeshPrimitives[voronoiRelaxed, 2]}]
Histogramme
13

Python + SciPy + Emcee

Der von mir verwendete Algorithmus ist der folgende:

  1. Bilder verkleinern (~ 150 Pixel)
  2. Machen Sie ein unscharfes Bild der maximalen Kanalwerte (dies hilft, weiße Bereiche nicht zu stark aufzunehmen).
  3. Nimm den absoluten Wert.
  4. Wähle zufällige Punkte mit einer Wahrscheinlichkeit, die proportional zu diesem Bild ist. Dies wählt Punkte auf beiden Seiten von Diskontinuitäten aus.
  5. Verfeinern Sie die ausgewählten Punkte, um eine Kostenfunktion zu senken. Die Funktion ist das Maximum der Summe der quadratischen Abweichungen in den Kanälen (was wiederum dazu beiträgt, eine Tendenz zu Volltonfarben und nicht nur zu Volltonweiß zu erzielen). Ich habe Markov Chain Monte Carlo mit dem Emcee-Modul (sehr empfehlenswert) als Optimierer missbraucht. Die Prozedur wird beendet, wenn nach N-Ketteniterationen keine neue Verbesserung gefunden wird.

Der Algorithmus scheint sehr gut zu funktionieren. Leider kann es nur sinnvoll auf kleineren Bildern laufen. Ich hatte keine Zeit, die Voronoi-Punkte auf die größeren Bilder anzuwenden. Sie könnten an dieser Stelle verfeinert werden. Ich hätte die MCMC auch länger laufen lassen können, um bessere Minima zu erzielen. Der Schwachpunkt des Algorithmus ist, dass er ziemlich teuer ist. Ich hatte keine Zeit, über 1000 Punkte hinauszuwachsen, und einige der 1000-Punkte-Bilder werden noch verfeinert.

(Klicken Sie mit der rechten Maustaste und zeigen Sie das Bild an, um eine größere Version zu erhalten.)

Thumbnails für 100, 300 und 1000 Punkte

Links zu größeren Versionen sind http://imgur.com/a/2IXDT#9 (100 Punkte), http://imgur.com/a/bBQ7q (300 Punkte) und http://imgur.com/a/rr8wJ (1000 Punkte)

#!/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()

Unscharf maskierte Bilder sehen wie folgt aus. Zufällige Punkte werden aus dem Bild ausgewählt, wenn eine Zufallszahl kleiner als der Wert des Bildes ist (normiert auf 1):

Unscharfes, maskiertes Saturn-Bild

Ich poste größere Bilder und die Voronoi-Punkte, wenn ich mehr Zeit habe.

Bearbeiten: Wenn Sie die Anzahl der Wanderer auf 100 * Npts erhöhen, ändern Sie die Kostenfunktion so, dass sie zu den Quadraten der Abweichungen in allen Kanälen gehört, und warten Sie lange (und erhöhen Sie die Anzahl der Iterationen, auf die die Schleife abgebrochen werden soll) 200) ist es möglich, einige gute Bilder mit nur 100 Punkten zu machen:

Bild 11, 100 Punkte Bild 2, 100 Punkte Bild 4, 100 Punkte Bild 10, 100 Punkte

Xioxox
quelle
3

Verwenden der Bildenergie als Punktgewichtskarte

Bei meiner Herangehensweise an diese Herausforderung wollte ich eine Möglichkeit finden, die "Relevanz" eines bestimmten Bildbereichs auf die Wahrscheinlichkeit abzubilden, dass ein bestimmter Punkt als Voronoi-Schwerpunkt ausgewählt wird. Ich wollte jedoch immer noch das künstlerische Gefühl des Voronoi-Mosaiks bewahren, indem ich zufällig Bildpunkte auswählte. Außerdem wollte ich große Bilder bearbeiten, damit ich beim Downsampling nichts verliere. Mein Algorithmus sieht ungefähr so ​​aus:

  1. Erstellen Sie für jedes Bild eine Schärfekarte. Die Schärfekarte wird durch die normalisierte Bildenergie (oder das Quadrat des Hochfrequenzsignals des Bildes) definiert. Ein Beispiel sieht so aus:

Schärfe-Karte

  1. Generieren Sie eine Reihe von Punkten aus dem Bild, wobei Sie 70 Prozent von den Punkten in der Schärfekarte und 30 Prozent von allen anderen Punkten übernehmen. Dies bedeutet, dass Punkte aus detailreichen Teilen des Bildes dichter abgetastet werden.
  2. Farbe!

Ergebnisse

N = 100, 500, 1000, 3000

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

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

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

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

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

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

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

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

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

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

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

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

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

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

mprat
quelle
14
Würde es Ihnen etwas ausmachen, a) den Quellcode einzuschließen, mit dem dies generiert wurde, und b) jedes Vorschaubild mit dem Bild in voller Größe zu verknüpfen?
Martin Ender