Dessiner une image sous forme de carte de Voronoï


170

Crédits à Calvin's Hobbies pour avoir poussé mon idée de défi dans la bonne direction.

Considérons un ensemble de points dans le plan, que nous appellerons des sites , et associons une couleur à chaque site. Maintenant, vous pouvez peindre tout le plan en coloriant chaque point avec la couleur du site le plus proche. C'est ce qu'on appelle une carte de Voronoï (ou diagramme de Voronoï ). En principe, les cartes de Voronoï peuvent être définies pour toute métrique de distance, mais nous utiliserons simplement la distance euclidienne habituelle r = √(x² + y²). ( Remarque: vous ne devez pas nécessairement savoir calculer et rendre l'un de ces éléments pour rivaliser dans ce défi.)

Voici un exemple avec 100 sites:

entrez la description de l'image ici

Si vous examinez une cellule, tous les points de cette cellule sont plus proches du site correspondant que de tout autre site.

Votre tâche consiste à approximer une image donnée avec une telle carte de Voronoï. Vous avez donné l'image dans un format graphique raster pratique, ainsi que d' un nombre entier N . Vous devez ensuite créer jusqu'à N sites et une couleur pour chaque site, de sorte que la carte de Voronoï basée sur ces sites ressemble le plus fidèlement possible à l'image d'entrée.

Vous pouvez utiliser l'extrait de pile au bas de ce défi pour rendre une carte de Voronoï à partir de votre sortie, ou vous-même si vous préférez, vous pouvez la restituer vous-même.

Vous pouvez utiliser des fonctions intégrées ou tierces pour calculer une carte Voronoï à partir d'un ensemble de sites (si vous en avez besoin).

Ceci est un concours de popularité, donc la réponse avec le plus grand nombre de votes nets gagne. Les électeurs sont encouragés à juger les réponses par

  • comment bien les images originales et leurs couleurs sont approximées.
  • comment l'algorithme fonctionne sur différents types d'images.
  • comment l'algorithme fonctionne pour les petits N .
  • si l'algorithme regroupe de manière adaptative des points dans des régions de l'image qui nécessitent davantage de détails.

Images d'essai

Voici quelques images pour tester votre algorithme (quelques-uns de nos suspects habituels, d'autres nouveaux). Cliquez sur les images pour les versions plus grandes.

Grande vague Hérisson Plage Cornell Saturne Ours brun Yoshi Mandrill Nébuleuse de crabe Geobits 'Kid Cascade Crier

La plage de la première rangée a été dessinée par Olivia Bell et incluse avec sa permission.

Si vous souhaitez relever un défi supplémentaire, essayez Yoshi avec un arrière - plan blanc et redressez sa ligne de ventre.

Vous pouvez trouver toutes ces images de test dans cette galerie imgur où vous pouvez toutes les télécharger au format zip. L'album contient également un diagramme de Voronoï aléatoire comme autre test. Pour référence, voici les données qui l’ont générée .

Veuillez inclure des exemples de diagrammes pour une variété d’images et de N différents , par exemple 100, 300, 1000, 3000 (ainsi que des corbeilles à coller conformes à certaines des spécifications de cellules correspondantes). Vous pouvez utiliser ou omettre les contours noirs entre les cellules à votre guise (cela peut sembler meilleur sur certaines images que sur d'autres). N'incluez cependant pas les sites (sauf dans un exemple séparé, si vous souhaitez expliquer le fonctionnement de l'emplacement de votre site, bien sûr).

Si vous souhaitez afficher un grand nombre de résultats, vous pouvez créer une galerie sur imgur.com , afin que la taille des réponses reste raisonnable. Vous pouvez également insérer des vignettes dans votre message et les lier à des images plus grandes, comme je l’ai fait dans ma réponse de référence . Vous pouvez obtenir les petites vignettes en ajoutant sle nom de fichier dans le lien imgur.com (par exemple I3XrT.png-> I3XrTs.png). Aussi, n'hésitez pas à utiliser d'autres images de test, si vous trouvez quelque chose de bien.

Renderer

Collez votre sortie dans l'extrait de pile suivant pour rendre vos résultats. Le format de liste exact est sans importance, tant que chaque cellule est spécifiée par 5 nombres à virgule flottante dans l'ordre x y r g b, où xet ysont les coordonnées du site de la cellule, et r g bcorrespondent aux canaux de couleur rouge, vert et bleu de la plage 0 ≤ r, g, b ≤ 1.

L'extrait de code fournit des options permettant de spécifier une largeur de trait des bords de la cellule et d'indiquer si les sites de cellules doivent être affichés (ce dernier principalement pour des fins de débogage). Notez toutefois que la sortie n'est restituée que lorsque les spécifications de la cellule changent. Par conséquent, si vous modifiez certaines des autres options, ajoutez un espace aux cellules ou à un autre élément.

Remerciements à Raymond Hill pour l’écriture de cette très belle bibliothèque de JS Voronoi .

Défis associés


5
@frogeyedpeas En regardant les votes que vous obtenez. ;) Ceci est un concours de popularité. Il n'y a pas nécessairement la meilleure façon de faire. L'idée est que vous essayiez de le faire du mieux possible, et les votes indiqueront si les gens sont d'accord pour dire que vous avez fait du bon travail. Certes, il y a une certaine subjectivité dans celles-ci. Jetez un coup d'œil aux défis connexes que j'ai liés, ou à celui-ci . Vous constaterez qu'il existe généralement une grande variété d'approches, mais le système de vote permet aux meilleures solutions de faire leur apparition et de choisir un gagnant.
Martin Ender

3
Olivia approuve les approximations de sa plage soumises jusqu'à présent.
Alex A.

3
@Alexa. Devon approuve certaines des approximations de son visage présentées jusqu'à présent. Il n'est pas un grand fan des versions n = 100;)
Geobits

1
@ Geobits: Il comprendra quand il sera plus grand.
Alex A.

1
Voici une page sur une technique de pointillé à base de voronoï centroïde . Une bonne source d’inspiration (le mémoire de master associé présente une discussion intéressante sur les améliorations possibles de l’algorithme).
Job

Réponses:


112

Python + scipy + scikit-image , échantillonnage pondéré du disque de Poisson

Ma solution est plutôt complexe. Je fais quelques pré-traitements sur l'image pour supprimer le bruit et obtenir une cartographie de l'intérêt de chaque point (en utilisant une combinaison d'entropie locale et de détection des contours):

Ensuite, je choisis des points d’échantillonnage en utilisant un échantillonnage de Poisson de Poisson avec une torsion: la distance du cercle est déterminée par le poids déterminé précédemment.

Ensuite, une fois que j'ai les points d'échantillonnage, je divise l'image en segments de voronoï et assigne la moyenne L * a * b * des valeurs de couleur à l'intérieur de chaque segment à chaque segment.

J'ai beaucoup d'heuristiques et je dois aussi faire un peu de calcul pour m'assurer que le nombre de points d'échantillon est proche de N. J'obtiens Nexactement en dépassant légèrement , puis en abandonnant certains points avec une heuristique.

En termes de temps d’exécution, ce filtre n’est pas bon marché , mais aucune image ci-dessous ne prend plus de 5 secondes.

Sans plus tarder:

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

Images

Respectivement N100, 300, 1000 et 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
J'aime ça; cela ressemble un peu au verre fumé.
BobTheAwesome

3
Je déconne un peu avec cela, et vous obtenez de meilleurs résultats, en particulier pour les images de triangle bas, si vous remplacez le denoise_bilatteral par un denoise_tv_bregman. Il génère plus de correctifs dans son atténuation, ce qui aide.
LKlevin

@LKlevin Quel poids as-tu utilisé?
Orlp

J'ai utilisé 1,0 comme poids.
LKlevin

65

C ++

Mon approche est assez lente, mais je suis très contente de la qualité des résultats qu’elle donne, notamment en ce qui concerne la préservation des contours. Par exemple, voici Yoshi et la Cornell Box avec seulement 1000 sites chacun:

Il y a deux parties principales qui le font fonctionner. Le premier, incorporé dans la evaluate()fonction, prend un ensemble d'emplacements de sites candidats, y définit les couleurs optimales et renvoie un score pour le PSNR de la tessellation de Voronoï rendue par rapport à l'image cible. Les couleurs de chaque site sont déterminées en faisant la moyenne des pixels d’image cible couverts par la cellule autour du site. J'utilise l'algorithme de Welford pour calculer à la fois la meilleure couleur pour chaque cellule et le PSNR obtenu en un seul passage sur l'image en exploitant la relation entre la variance, la MSE et le PSNR. Cela réduit le problème à celui de trouver le meilleur ensemble d'emplacements de site sans considération particulière pour la couleur.

La deuxième partie, incarnée dans main(), tente de trouver cet ensemble. Il commence par choisir un ensemble de points au hasard. Ensuite, à chaque étape, il supprime un point (va-et-vient) et teste un ensemble de points candidats aléatoires pour le remplacer. Celui qui donne le plus haut PSNR du groupe est accepté et conservé. Effectivement, le site accède à un nouvel emplacement et améliore l’image bit par bit. Notez que l'algorithme ne conserve pas intentionnellement l'emplacement d'origine en tant que candidat. Parfois, cela signifie que le saut diminue la qualité globale de l'image. Permettre que cela se produise aide à éviter de rester bloqué dans les maxima locaux. Il donne également un critère d'arrêt; le programme se termine après avoir franchi un certain nombre d'étapes sans améliorer le meilleur ensemble de sites trouvé jusqu'à présent.

Notez que cette implémentation est assez basique et peut facilement prendre des heures de temps processeur, en particulier à mesure que le nombre de sites augmente. Il recalcule la carte complète de Voronoï pour chaque candidat et la force brute teste la distance à tous les sites pour chaque pixel. Comme chaque opération implique de supprimer un point à la fois et d’en ajouter un autre, les modifications apportées à l’image à chaque étape seront assez locales. Il existe des algorithmes pour la mise à jour incrémentielle efficace d'un diagramme de Voronoï et je pense qu'ils donneraient à cet algorithme une vitesse d'accélération considérable. Pour ce concours, cependant, j'ai choisi de garder les choses simples et brutales.

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

Fonctionnement

Le programme est autonome et n'a pas de dépendances externes au-delà de la bibliothèque standard, mais nécessite que les images soient au format PPM binaire . J'utilise ImageMagick pour convertir les images en PPM, bien que GIMP et quelques autres programmes puissent le faire aussi.

Pour le compiler, enregistrez le programme sous voronoi.cppet exécutez:

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

Je pense que cela fonctionnera probablement sous Windows avec les versions récentes de Visual Studio, bien que je n’aie pas essayé cela. Vous devrez vous assurer que vous compilez avec C ++ 11 ou supérieur et qu'OpenMP est activé si vous le faites. OpenMP n'est pas strictement nécessaire, mais il aide beaucoup à rendre les temps d'exécution plus tolérables.

Pour l'exécuter, faites quelque chose comme:

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

Le fichier ultérieur sera mis à jour au fur et à mesure avec les données du site. La première ligne aura la largeur et la hauteur de l'image, suivies des lignes de valeurs x, y, r, g, b appropriées pour la copie et le collage dans le rendu Javascript dans la description du problème.

Les trois constantes en haut du programme vous permettent de l’accorder entre rapidité et qualité. Le decimationfacteur grossit l'image cible lors de l'évaluation d'un ensemble de sites pour la couleur et le PSNR. Plus il est élevé, plus le programme sera exécuté rapidement. Le réglage sur 1 utilise l’image en pleine résolution. La candidatesconstante contrôle le nombre de candidats à tester à chaque étape. Plus haut donne une meilleure chance de trouver un bon endroit pour sauter mais ralentit le programme. Enfin, il terminationfaut savoir combien d'étapes le programme peut franchir sans améliorer son résultat avant de quitter. L'augmenter peut donner de meilleurs résultats mais le faire prendre un peu plus longtemps.

Images

N = 100, 300, 1000 et 3000:


1
Cela aurait dû gagner IMO - beaucoup mieux que le mien.
Orlp

1
@orlp - Merci! Pour être juste, vous avez posté le vôtre beaucoup plus tôt et le temps est beaucoup plus court. La vitesse compte!
Boojum

1
Eh bien, le mien n’est pas vraiment une réponse de carte voronoi :) C’est un très bon algorithme d’échantillonnage, mais convertir des points d’échantillon en sites voronoi n’est clairement pas optimal.
Orlp

55

IDL, raffinement adaptatif

Cette méthode est inspirée du raffinement de maillage adaptatif issu de simulations astronomiques, ainsi que de la surface de subdivision . C'est le genre de tâche dont IDL est fier, ce que vous pourrez constater grâce au grand nombre de fonctions intégrées que j'ai pu utiliser. :RÉ

J'ai produit certains des éléments intermédiaires de l'image test yoshi sur fond noir, avec n = 1000.

Tout d'abord, nous effectuons une échelle de gris de luminance sur l'image (en utilisant ct_luminance), et appliquons un filtre de Prewitt ( prewitt, voir wikipedia ) pour une bonne détection des contours:

abc abc

Vient ensuite le véritable travail de grognement: nous subdivisons l'image en 4 et mesurons la variance dans chaque quadrant de l'image filtrée. Notre variance est pondérée par la taille de la sous-division (qui dans cette première étape est égale), de sorte que les régions "énervées" à forte variance ne soient pas subdivisées de plus en plus petites. Ensuite, nous utilisons la variance pondérée pour cibler les subdivisions avec plus de détails et subdivisons chaque section riche en détails en 4 divisions supplémentaires, jusqu'à atteindre notre nombre cible de sites (chaque subdivision contient exactement un site). Comme nous ajoutons 3 sites à chaque itération, nous nous retrouvons avec des n - 2 <= N <= nsites.

J'ai créé un fichier .webm du processus de subdivision pour cette image, que je ne peux pas intégrer, mais c'est ici ; la couleur dans chaque sous-section est déterminée par la variance pondérée. (J'ai réalisé le même type de vidéo pour le fond blanc yoshi, à des fins de comparaison, la table des couleurs étant inversée, elle passe au blanc au lieu du noir; c'est ici .) Le produit final de la subdivision ressemble à ceci:

abc

Une fois que nous avons notre liste de subdivisions, nous parcourons chaque subdivision. L'emplacement final du site est la position du minimum de l'image de Prewitt, c'est-à-dire du pixel le moins "énervé", et la couleur de la section est la couleur de ce pixel; voici l'image originale, avec les sites marqués:

abc

Ensuite, nous utilisons l’intégré triangulatepour calculer la triangulation de Delaunay des sites et l’intégré voronoipour définir les sommets de chaque polygone de Voronoï, avant d’attirer chaque polygone dans un tampon d’image de sa couleur respective. Enfin, nous sauvegardons un instantané du tampon d’image.

abc

Le 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

Appelez ceci via voro_map, n, image, output_filename. J'ai également inclus une wrapperprocédure, qui passait en revue chaque image test et fonctionnait avec 100, 500 et 1000 sites.

Résultats collectés ici , et voici quelques vignettes:

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
J'aime beaucoup le fait que cette solution insère plus de points dans des domaines plus complexes, ce qui est selon moi l'objectif, et la distingue des autres pour le moment.
alexander-brett

oui, c'est l'idée de points regroupés dans les détails qui m'a amené au raffinement adaptatif
Sirpercival,

3
Explication très soignée, et les images sont impressionnantes! J'ai une question - Il semble que vous obteniez des images très différentes lorsque Yoshi est sur un fond blanc, où nous avons quelques formes étranges. Qu'est-ce qui pourrait causer ça?
BrainSteel

2
@BrianSteel J'imagine que les contours sont ramassés en tant que zones à forte variance et concentrés inutilement, puis que d'autres zones vraiment très détaillées ont moins de points attribués pour cette raison.
doppelgreener

@BrainSteel, je pense que Doppel a raison - il existe un fort avantage entre la bordure noire et le fond blanc, ce qui demande beaucoup de détails dans l'algorithme. Je ne suis pas sûr que ce soit quelque chose que je puisse (ou, plus important encore, qu'il devrait ) réparer ...
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)

L'algorithme

L'idée centrale est que la classification par k-means divise naturellement l'image en cellules de Voronoï, car les points sont liés au centroïde le plus proche. Cependant, nous devons en quelque sorte ajouter les couleurs en tant que contrainte.

Nous convertissons d’abord chaque pixel dans l’ espace colorimétrique Lab , pour une meilleure manipulation des couleurs.

Ensuite, nous faisons une sorte de "détection de bord du pauvre". Pour chaque pixel, nous examinons ses voisins orthogonaux et diagonaux et calculons la différence de couleur au carré moyen. Nous trions ensuite tous les pixels en fonction de cette différence, les pixels les plus similaires à leurs voisins étant en tête de la liste et les pixels les plus dissemblables à leurs voisins à l'arrière (plus susceptibles d'être un point de contour). Voici un exemple pour la planète, plus le pixel est brillant, plus il est différent de ses voisins:

entrez la description de l'image ici

(Il y a un motif clair de type grille sur le rendu ci-dessus. Selon @randomra, cela est probablement dû à un encodage JPG avec perte ou à une compression imgur des images.)

Ensuite, nous utilisons cet ordre de pixels pour échantillonner un grand nombre de points à regrouper. Nous utilisons une distribution exponentielle, en donnant la priorité aux points qui sont plus proches et "intéressants".

entrez la description de l'image ici

Pour le clustering, nous choisissons d’abord des Ncentroïdes, choisis au hasard en utilisant la même distribution exponentielle que ci-dessus. Une itération initiale est effectuée et, pour chacune des grappes résultantes, nous attribuons une couleur moyenne et un seuil de variance de couleur. Ensuite, pour un certain nombre d'itérations, nous:

  • Construisez la triangulation de Delaunay des centroïdes pour pouvoir interroger facilement les voisins des centroïdes.
  • Utilisez la triangulation pour supprimer les centroïdes dont la couleur est proche de celle de la plupart (> 4/5) de leurs voisins et des voisins du voisin combinés. Tous les points d'échantillonnage associés sont également supprimés et de nouveaux centroïdes de remplacement et des points d'échantillonnage sont ajoutés. Cette étape tente de forcer l'algorithme à placer plus de clusters là où des détails sont nécessaires.
  • Construisez un arbre kd pour les nouveaux centroïdes, afin de pouvoir facilement interroger les centroïdes les plus proches de n'importe quel point de l'échantillon.
  • Utilisez l’arbre pour assigner chaque point d’échantillon à l’un des 6 centroïdes les plus proches (6 choisis de manière empirique). Un centre de gravité n'acceptera un point d'échantillonnage que si sa couleur est comprise dans le seuil de variance de couleur du centre de gravité. Nous essayons d’attribuer chaque point d’échantillon au premier centre de gravité accepté, mais si ce n’est pas possible, nous l’attribuons simplement au centre de gravité le plus proche. Le "flou" de l'algorithme provient de cette étape, car il est possible que des clusters se chevauchent.
  • Recalculer les centroïdes.

entrez la description de l'image ici

(Cliquez pour agrandir)

Enfin, nous échantillonnons un grand nombre de points, cette fois en utilisant une distribution uniforme. En utilisant un autre arbre kd, nous assignons chaque point au centroïde le plus proche, formant des clusters. Nous estimons ensuite la couleur médiane de chaque groupe en utilisant un algorithme d’escalade en donnant les couleurs finales de nos cellules (idée de cette étape grâce à @PhiNotPi et @ MartinBüttner).

entrez la description de l'image ici

Remarques

En plus de générer un fichier texte pour l'extrait de code ( OUTFILE), if DEBUGest défini sur Truele programme permet également d'afficher et de remplacer les images ci-dessus. L'algorithme prend quelques minutes pour chaque image, c'est donc un bon moyen de contrôler les progrès sans ajouter beaucoup à la durée d'exécution.

Exemples de sorties

N = 32:

entrez la description de l'image ici

N = 100:

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

N = 1000:

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

N = 3000:

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici


1
J'aime vraiment à quel point tes Yoshis sur blanc sont bien sortis.
Max

26

Mathematica, cellules aléatoires

C'est la solution de base, de sorte que vous obtenez une idée du minimum que je vous demande. Étant donné le nom du fichier (localement ou sous forme d'URL) in fileet N in n, le code suivant sélectionne simplement N pixels aléatoires et utilise les couleurs trouvées au niveau de ces pixels. C’est vraiment naïf et cela ne fonctionne pas incroyablement bien, mais je veux que vous réussissiez à le battre après tout. :)

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

Voici toutes les images de test pour N = 100 (toutes les images sont liées à des versions plus grandes):

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

Comme vous pouvez le constater, ils sont essentiellement inutiles. Bien qu'elles puissent avoir une certaine valeur artistique, d'une manière expressionniste, les images originales sont à peine reconnaissables.

Pour N = 500 , la situation s’est un peu améliorée, mais il reste encore des artefacts très étranges, les images paraissent délavées et de nombreuses cellules sont gaspillées dans des régions sans détail:

entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici entrez la description de l'image ici

À ton tour!


Je ne suis pas un bon codeur mais mon dieu, ces images sont magnifiques. Idée géniale!
Faraz Masroor

Toute raison pour Dimensions@ImageDataet pas ImageDimensions? Vous pouvez éviter le lent ImageDatacomplètement en utilisant PixelValue.
2012rcampion

@ 2012rcampion Aucune raison, je ne savais pas que l'une ou l'autre fonction existait. Je pourrais peut-être remédier à cela plus tard et changer également les exemples d'images en valeurs N recommandées.
Martin Ender

23

Mathematica

Nous savons tous que Martin aime Mathematica, alors laissez-moi essayer avec Mathematica.

Mon algorithme utilise des points aléatoires à partir des bords de l'image pour créer un diagramme de voronoï initial. Le diagramme est ensuite prettifié par un ajustement itératif du maillage avec un simple filtre moyen. Cela donne des images avec une densité cellulaire élevée près des régions à contraste élevé et des cellules visuellement agréables sans angles fous.

Les images suivantes montrent un exemple du processus en action. Le plaisir est un peu gâché par le mauvais antialiasing de Mathematicas, mais nous obtenons des graphiques vectoriels, cela doit valoir quelque chose.

Cet algorithme, sans échantillonnage aléatoire, peut être trouvé dans la VoronoiMeshdocumentation ici .

entrez la description de l'image ici entrez la description de l'image ici

Images de test (100 300 100 000 000)

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

Beau travail pour un premier post! :) Vous voudrez peut-être essayer l'image test de Voronoï avec 32 cellules (c'est le nombre de cellules dans l'image elle-même).
Martin Ender

Merci! Je suppose que mon algorithme fonctionnera terriblement sur cet exemple. Les graines seront initialisées sur les bords des cellules et la récurrence ne le rendra pas beaucoup mieux;)
patte

Malgré le lent ralentissement de la convergence vers l'image d'origine, je trouve que votre algorithme donne un résultat très artistique! (comme une version améliorée de Georges Seurat fonctionne). Bon travail!
neizod

Vous pouvez également obtenir des couleurs de polygone interpolées d'aspect vitreux en modifiant vos dernières lignes enGraphics@Table[ Append[mp, VertexColors -> RGBColor /@ ImageValue[img, First[mp]]], {mp, MeshPrimitives[voronoiRelaxed, 2]}]
Histogrammes

13

Python + SciPy + Emcee

L'algorithme que j'ai utilisé est le suivant:

  1. Redimensionner les images à une taille réduite (~ 150 pixels)
  2. Créez une image non masquée des valeurs maximales du canal (cela aide à ne pas capter trop fort les régions blanches).
  3. Prenez la valeur absolue.
  4. Choisissez des points aléatoires avec une probabilité proportionnelle à cette image. Ceci choisit des points des deux côtés des discontinuités.
  5. Affinez les points choisis pour réduire une fonction de coût. La fonction est le maximum de la somme des déviations au carré dans les canaux (ce qui aide encore à favoriser les couleurs unies et pas seulement le blanc). J'ai mal utilisé Markov Chain Monte Carlo avec le module emcee (hautement recommandé) comme optimiseur. La procédure échoue lorsqu'aucune nouvelle amélioration n'est trouvée après les itérations de la chaîne N.

L'algorithme semble très bien fonctionner. Malheureusement, il ne peut raisonnablement fonctionner que sur de petites images. Je n'ai pas eu le temps de prendre les points de Voronoï et de les appliquer aux images plus grandes. Ils pourraient être raffinés à ce stade. J'aurais aussi pu utiliser MCMC plus longtemps pour obtenir de meilleurs minima. Le point faible de l’algorithme est qu’il est plutôt coûteux. Je n’ai pas eu le temps d’augmenter au-delà de 1000 points et quelques images à 1000 points sont encore en train d’être affinées.

(clic droit et voir l'image pour obtenir une version plus grande)

Miniatures pour 100, 300 et 1000 points

Les liens vers des versions plus grandes sont http://imgur.com/a/2IXDT#9 (100 points), http://imgur.com/a/bBQ7q (300 points) et http://imgur.com/a/rr8wJ (1000 points)

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

Les images masquées non masquées ressemblent à ce qui suit. Des points aléatoires sont sélectionnés dans l'image si un nombre aléatoire est inférieur à la valeur de l'image (normée à 1):

Image de Saturne masquée non flouée

Je posterai des images plus grandes et les points de Voronoï si je dispose de plus de temps.

Éditer: si vous augmentez le nombre de marcheurs à 100 * npts, modifiez la fonction de coût pour qu’elle soit l’un des carrés des déviations dans tous les canaux, et attendez longtemps (augmentation du nombre d’itérations pour sortir de la boucle). 200), il est possible de faire de bonnes images avec seulement 100 points:

Image 11, 100 points Image 2, 100 points Image 4, 100 points Image 10, 100 points


3

Utiliser l'énergie de l'image comme une carte de poids ponctuel

Dans mon approche de ce défi, je cherchais un moyen de cartographier la "pertinence" d'une zone d'image donnée par rapport à la probabilité qu'un point particulier soit choisi comme centre de Vroidoïro. Cependant, je voulais toujours préserver la sensation artistique du mosaïquage de Voronoï en choisissant au hasard des points d’image. De plus, je voulais utiliser de grandes images pour ne rien perdre du processus de sous-échantillonnage. Mon algorithme est à peu près comme ça:

  1. Pour chaque image, créez une carte de netteté. La carte de netteté est définie par l'énergie d'image normalisée (ou le carré du signal haute fréquence de l'image). Un exemple ressemble à ceci:

Carte de netteté

  1. Générez un nombre de points à partir de l'image, en prenant 70% des points de la carte de netteté et 30% de tous les autres points. Cela signifie que les points sont échantillonnés de manière plus dense dans les parties les plus détaillées de l'image.
  2. Couleur!

Résultats

N = 100, 500, 1000, 3000

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

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

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

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

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

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

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

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

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

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

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

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

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

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


14
Cela vous dérangerait-il a) d'inclure le code source utilisé pour générer cela, et b) de lier chaque vignette à l'image en taille réelle?
Martin Ender
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.