Dibuja una imagen como un mapa de Voronoi

170

Créditos a Calvin's Hobbies por empujar mi idea de desafío en la dirección correcta.

Considere un conjunto de puntos en el plano, que llamaremos sitios , y asocie un color con cada sitio. Ahora puede pintar todo el plano coloreando cada punto con el color del sitio más cercano. Esto se llama un mapa de Voronoi (o diagrama de Voronoi ). En principio, los mapas de Voronoi se pueden definir para cualquier métrica de distancia, pero simplemente usaremos la distancia euclidiana habitual r = √(x² + y²). ( Nota: no necesariamente tiene que saber cómo calcular y representar uno de estos para competir en este desafío).

Aquí hay un ejemplo con 100 sitios:

ingrese la descripción de la imagen aquí

Si observa cualquier celda, todos los puntos dentro de esa celda están más cerca del sitio correspondiente que de cualquier otro sitio.

Su tarea es aproximar una imagen dada con dicho mapa de Voronoi. Que le den la imagen en cualquier formato de gráficos de trama conveniente, así como un número entero N . Luego debe producir hasta N sitios y un color para cada sitio, de modo que el mapa de Voronoi basado en estos sitios se parezca lo más posible a la imagen de entrada.

Puede usar el Fragmento de pila al final de este desafío para representar un mapa de Voronoi a partir de su salida, o puede hacerlo usted mismo si lo prefiere.

Usted puede utilizar las funciones built-in o de terceros para calcular un mapa de Voronoi de un conjunto de sitios (si es necesario).

Este es un concurso de popularidad, por lo que gana la respuesta con más votos netos. Se alienta a los votantes a juzgar las respuestas por

  • qué tan bien se aproximan las imágenes originales y sus colores.
  • qué tan bien funciona el algoritmo en diferentes tipos de imágenes.
  • lo bien que el algoritmo funciona para pequeñas N .
  • si el algoritmo agrupa de forma adaptativa los puntos en regiones de la imagen que requieren más detalles.

Imágenes de prueba

Aquí hay algunas imágenes para probar su algoritmo (algunos de nuestros sospechosos habituales, algunos nuevos). Haga clic en las imágenes para versiones más grandes.

Gran ola Erizo playa Cornell Saturno Oso café Yoshi Mandril Nebulosa del Cangrejo El niño de Geobits Cascada Gritar

La playa en la primera fila fue dibujada por Olivia Bell , e incluida con su permiso.

Si quieres un desafío extra, prueba con Yoshi con un fondo blanco y acerta su línea del vientre.

Puede encontrar todas estas imágenes de prueba en esta galería de imágenes donde puede descargarlas todas como un archivo zip. El álbum también contiene un diagrama aleatorio de Voronoi como otra prueba. Como referencia, aquí están los datos que lo generaron .

Incluya diagramas de ejemplo para una variedad de imágenes diferentes y N , por ejemplo, 100, 300, 1000, 3000 (así como pastebins para algunas de las especificaciones de celda correspondientes). Puede usar u omitir bordes negros entre las celdas como mejor le parezca (esto puede verse mejor en algunas imágenes que en otras). Sin embargo, no incluya los sitios (excepto en un ejemplo separado, tal vez si desea explicar cómo funciona la ubicación de su sitio, por supuesto).

Si desea mostrar una gran cantidad de resultados, puede crear una galería en imgur.com , para mantener razonable el tamaño de las respuestas. Alternativamente, coloque miniaturas en su publicación y conviértalas en enlaces a imágenes más grandes, como hice en mi respuesta de referencia . Puede obtener las miniaturas pequeñas agregando sel nombre del archivo en el enlace imgur.com (por ejemplo, I3XrT.png-> I3XrTs.png). Además, siéntase libre de usar otras imágenes de prueba, si encuentra algo bueno.

Renderizador

Pegue su salida en el siguiente fragmento de pila para representar sus resultados. El formato exacto de la lista es irrelevante, siempre que cada celda esté especificada por 5 números de coma flotante en el orden x y r g b, dónde xy yson las coordenadas del sitio de la celda, y r g bson los canales de color rojo, verde y azul en el rango 0 ≤ r, g, b ≤ 1.

El fragmento proporciona opciones para especificar un ancho de línea de los bordes de la celda, y si los sitios de la celda deben mostrarse o no (esto último principalmente para fines de depuración). Pero tenga en cuenta que la salida solo se vuelve a representar cuando cambian las especificaciones de la celda, por lo que si modifica algunas de las otras opciones, agregue un espacio a las celdas o algo así.

Créditos masivos a Raymond Hill por escribir esta biblioteca realmente agradable de JS Voronoi .

Desafíos relacionados

Martin Ender
fuente
55
@frogeyedpeas Al mirar los votos que obtienes. ;) Este es un concurso de popularidad. No hay necesariamente la mejor manera de hacerlo. La idea es que intentes hacerlo lo mejor que puedas, y los votos reflejarán si la gente está de acuerdo en que has hecho un buen trabajo. Hay una cierta cantidad de subjetividad en estos, es cierto. Echa un vistazo a los desafíos relacionados que he vinculado, o en este . Verá que generalmente hay una amplia variedad de enfoques, pero el sistema de votación ayuda a que las mejores soluciones lleguen a la cima y decidan un ganador.
Martin Ender
3
Olivia aprueba las aproximaciones de su playa presentadas hasta ahora.
Alex A.
3
@AlexA. Devon aprueba algunas de las aproximaciones de su rostro presentadas hasta ahora. No es un gran admirador de ninguna de las versiones n = 100;)
Geobits
1
@Geobits: lo entenderá cuando sea mayor.
Alex A.
1
Aquí hay una página sobre una técnica de punteado basado en voronoi centroidal . Una buena fuente de inspiración (la tesis de maestría relacionada tiene una buena discusión de posibles mejoras al algoritmo).
Trabajo

Respuestas:

112

Python + scipy + scikit-image , muestreo de disco de Poisson ponderado

Mi solución es bastante compleja. Hago un preprocesamiento en la imagen para eliminar el ruido y obtener un mapeo de lo 'interesante' que es cada punto (usando una combinación de entropía local y detección de bordes):

Luego elijo los puntos de muestreo usando el muestreo de disco de Poisson con un giro: la distancia del círculo está determinada por el peso que determinamos anteriormente.

Luego, una vez que tengo los puntos de muestreo, divido la imagen en segmentos voronoi y asigno el promedio L * a * b * de los valores de color dentro de cada segmento a cada segmento.

Tengo muchas heurísticas y también debo hacer un poco de matemática para asegurarme de que el número de puntos de muestra sea cercano N. Obtengo Nexactamente sobrepasando un poco , y luego bajando algunos puntos con una heurística.

En términos de tiempo de ejecución, este filtro no es barato , pero ninguna de las siguientes imágenes tardó más de 5 segundos en crearse.

Sin más preámbulos:

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

Imágenes

Respectivamente Nes 100, 300, 1000 y 3000:

a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C
a B C a B C a B C a B C

orlp
fuente
2
Me gusta esto; se parece un poco al vidrio ahumado.
BobTheAwesome
3
Me equivoqué un poco con esto, y obtienes mejores resultados, particularmente para las imágenes de triángulo bajo, si reemplazas el denoise_bilatteral con un denoise_tv_bregman. Genera parches más uniformes en su eliminación de ruido, lo que ayuda.
LKlevin
@LKlevin ¿Qué peso usaste?
orlp
Usé 1.0 como el peso.
LKlevin
65

C ++

Mi enfoque es bastante lento, pero estoy muy contento con la calidad de los resultados que brinda, particularmente con respecto a la preservación de los bordes. Por ejemplo, aquí están Yoshi y el Cornell Box con solo 1000 sitios cada uno:

Hay dos partes principales que lo hacen funcionar. El primero, incorporado en la evaluate()función, toma un conjunto de ubicaciones de sitios candidatos, establece los colores óptimos en ellas y devuelve una puntuación para el PSNR de la teselación de Voronoi representada frente a la imagen objetivo. Los colores para cada sitio se determinan promediando los píxeles de la imagen objetivo cubiertos por la celda alrededor del sitio. Utilizo el algoritmo de Welford para ayudar a calcular tanto el mejor color para cada celda como el PSNR resultante usando solo un solo paso sobre la imagen explotando la relación entre la varianza, MSE y PSNR. Esto reduce el problema a uno de encontrar el mejor conjunto de ubicaciones del sitio sin tener en cuenta el color.

La segunda parte, incorporada main(), trata de encontrar este conjunto. Comienza eligiendo un conjunto de puntos al azar. Luego, en cada paso, elimina un punto (yendo por turnos) y prueba un conjunto de puntos candidatos aleatorios para reemplazarlo. El que produce el PSNR más alto del grupo es aceptado y mantenido. Efectivamente, esto hace que el sitio salte a una nueva ubicación y generalmente mejora la imagen bit por bit. Tenga en cuenta que el algoritmo intencionalmente no retiene la ubicación original como candidato. A veces esto significa que el salto disminuye la calidad general de la imagen. Permitir que esto suceda ayuda a evitar quedar atrapado en los máximos locales. También da un criterio de detención; el programa termina después de seguir un cierto número de pasos sin mejorar el mejor conjunto de sitios encontrados hasta ahora.

Tenga en cuenta que esta implementación es bastante básica y puede llevar fácilmente horas de tiempo de núcleo de CPU, especialmente a medida que crece el número de sitios. Vuelve a calcular el mapa completo de Voronoi para cada candidato y la fuerza bruta prueba la distancia a todos los sitios para cada píxel. Como cada operación implica eliminar un punto a la vez y agregar otro, los cambios reales en la imagen en cada paso serán bastante locales. Hay algoritmos para actualizar de manera eficiente e incremental un diagrama de Voronoi y creo que le darían a este algoritmo una aceleración tremenda. Para este concurso, sin embargo, he elegido mantener las cosas simples y con fuerza bruta.

Código

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

Corriendo

El programa es autónomo y no tiene dependencias externas más allá de la biblioteca estándar, pero requiere que las imágenes estén en formato PPM binario . Utilizo ImageMagick para convertir imágenes a PPM, aunque GIMP y muchos otros programas también pueden hacerlo.

Para compilarlo, guarde el programa como voronoi.cppy luego ejecute:

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

Espero que probablemente funcione en Windows con versiones recientes de Visual Studio, aunque no lo he probado. Querrás asegurarte de que estás compilando con C ++ 11 o superior y OpenMP habilitado si lo haces. OpenMP no es estrictamente necesario, pero ayuda mucho a hacer que los tiempos de ejecución sean más tolerables.

Para ejecutarlo, haga algo como:

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

El archivo posterior se actualizará con los datos del sitio a medida que avanza. La primera línea tendrá el ancho y la altura de la imagen, seguida de líneas de valores x, y, r, g, b adecuadas para copiar y pegar en el renderizador de Javascript en la descripción del problema.

Las tres constantes en la parte superior del programa le permiten ajustarlo para velocidad versus calidad. El decimationfactor engrosa la imagen de destino al evaluar un conjunto de sitios para color y PSNR. Cuanto más alto sea, más rápido se ejecutará el programa. Establecerlo en 1 usa la imagen de resolución completa. La candidatesconstante controla cuántos candidatos probar en cada paso. Mayor da una mejor oportunidad de encontrar un buen lugar para saltar, pero hace que el programa sea más lento. Finalmente, terminationes cuántos pasos puede seguir el programa sin mejorar su salida antes de que se cierre. Aumentarlo puede dar mejores resultados, pero hace que demore un poco más.

Imágenes

N = 100, 300, 1000 y 3000:

Boojum
fuente
1
Esto debería haber ganado la OMI, mucho mejor que la mía.
orlp
1
@orlp - ¡Gracias! Sin embargo, para ser justos, publicaste el tuyo mucho antes y se ejecuta mucho más rápido. La velocidad cuenta!
Boojum
1
Bueno, el mío no es realmente una respuesta de mapa voronoi :) Es un algoritmo de muestreo realmente bueno, pero convertir los puntos de muestra en sitios voronoi claramente no es óptimo.
orlp
55

IDL, refinamiento adaptativo

Este método está inspirado en el refinamiento de malla adaptable de simulaciones astronómicas, y también en la superficie de subdivisión . Este es el tipo de tarea de la que se enorgullece IDL, que podrá ver por la gran cantidad de funciones integradas que pude usar. :RE

He mostrado algunos de los intermedios para la imagen de prueba yoshi de fondo negro, con n = 1000.

Primero, realizamos una escala de grises de luminancia en la imagen (usando ct_luminance), y aplicamos un filtro Prewitt ( prewitt, ver wikipedia ) para una buena detección de bordes:

a B C a B C

Luego viene el verdadero trabajo gruñido: subdividimos la imagen en 4 y medimos la varianza en cada cuadrante de la imagen filtrada. Nuestra varianza está ponderada por el tamaño de la subdivisión (que en este primer paso es igual), de modo que las regiones "nerviosas" con alta varianza no se subdividen cada vez más y más. Luego, usamos la varianza ponderada para apuntar subdivisiones con más detalle, y subdividimos iterativamente cada sección rica en detalles en 4 adicionales, hasta alcanzar nuestro número objetivo de sitios (cada subdivisión contiene exactamente un sitio). Como estamos agregando 3 sitios cada vez que iteramos, terminamos con n - 2 <= N <= nsitios.

Hice un .webm del proceso de subdivisión para esta imagen, que no puedo incrustar, pero está aquí ; El color en cada subsección está determinado por la varianza ponderada. (Hice el mismo tipo de video para el yoshi de fondo blanco, en comparación, con la tabla de colores invertida para que vaya hacia el blanco en lugar del negro; está aquí ). El producto final de la subdivisión se ve así:

a B C

Una vez que tenemos nuestra lista de subdivisiones, revisamos cada subdivisión. La ubicación final del sitio es la posición del mínimo de la imagen de Prewitt, es decir, el píxel menos "nervioso", y el color de la sección es el color de ese píxel; Aquí está la imagen original, con sitios marcados:

a B C

Luego, utilizamos el incorporado triangulatepara calcular la triangulación de Delaunay de los sitios, y el incorporado voronoipara definir los vértices de cada polígono Voronoi, antes de dibujar cada polígono en un búfer de imagen en su color respectivo. Finalmente, guardamos una instantánea del búfer de imagen.

a B C

El código:

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

Llame a esto a través de voro_map, n, image, output_filename. También incluí un wrapperprocedimiento, que revisaba cada imagen de prueba y funcionaba con 100, 500 y 1000 sitios.

Salida recopilada aquí , y aquí hay algunas miniaturas:

n = 100

a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C

n = 500

a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C

n = 1000

a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C a B C

Sirpercival
fuente
99
Realmente me gusta el hecho de que esta solución pone más puntos en áreas más complejas, que creo que es la intención, y lo distingue de los demás en este momento.
alexander-brett
Sí, la idea de los puntos agrupados en detalle es lo que me llevó a un refinamiento adaptativo
Sirpercival
3
Muy buena explicación, ¡y las imágenes son impresionantes! Tengo una pregunta: parece que obtienes imágenes muy diferentes cuando Yoshi está sobre un fondo blanco, donde tenemos algunas formas extrañas. ¿Qué podría estar causando eso?
BrainSteel
2
@BrianSteel Me imagino que los contornos se recogen como áreas de alta varianza y se centran innecesariamente, y luego otras áreas verdaderamente de alto detalle tienen menos puntos asignados debido a eso.
doppelgreener
@BrainSteel, creo que doppel tiene razón: hay un borde fuerte entre el borde negro y el fondo blanco, que requiere muchos detalles en el algoritmo. No estoy seguro de si esto es algo que puedo (o, lo que es más importante, debería ) arreglar ...
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)

El algoritmo

La idea central es que k-significa agrupamiento divide naturalmente la imagen en celdas de Voronoi, ya que los puntos están vinculados al centroide más cercano. Sin embargo, necesitamos agregar de alguna manera los colores como una restricción.

Primero convertimos cada píxel al espacio de color Lab , para una mejor manipulación del color.

Luego hacemos una especie de "detección de bordes del pobre". Para cada píxel, observamos sus vecinos ortogonales y diagonales, y calculamos la diferencia cuadrática media en color. Luego clasificamos todos los píxeles por esta diferencia, con los píxeles más similares a sus vecinos al principio de la lista, y los píxeles más diferentes a sus vecinos en la parte posterior (es decir, más probable que sea un punto de borde). Aquí hay un ejemplo para el planeta, donde cuanto más brillante es el píxel, más diferente es de sus vecinos:

ingrese la descripción de la imagen aquí

(Hay un patrón claro similar a una cuadrícula en la salida representada arriba. Según @randomra, esto probablemente se deba a la codificación JPG con pérdida o a la compresión de las imágenes).

A continuación, usamos este orden de píxeles para muestrear una gran cantidad de puntos que se agruparán. Utilizamos una distribución exponencial, dando prioridad a los puntos que son más similares a los bordes e "interesantes".

ingrese la descripción de la imagen aquí

Para la agrupación, primero seleccionamos los Ncentroides, elegidos al azar utilizando la misma distribución exponencial que la anterior. Se realiza una iteración inicial y para cada uno de los grupos resultantes asignamos un color medio y un umbral de variación de color. Luego, para una serie de iteraciones, nosotros:

  • Construya la triangulación de Delaunay de los centroides, de modo que podamos consultar fácilmente vecinos a los centroides.
  • Use la triangulación para eliminar los centroides que son de color cercano a la mayoría (> 4/5) de sus vecinos y vecinos vecinos combinados. También se eliminan los puntos de muestra asociados y se agregan nuevos centroides de reemplazo y puntos de muestra. Este paso intenta forzar al algoritmo a colocar más clústeres donde se necesitan detalles.
  • Construya un árbol kd para los nuevos centroides, de modo que podamos consultar fácilmente los centroides más cercanos a cualquier punto de muestra.
  • Use el árbol para asignar cada punto de muestra a uno de los 6 centroides más cercanos (6 elegidos empíricamente). Un centroide solo aceptará un punto de muestra si el color del punto está dentro del umbral de variación de color del centroide. Intentamos asignar cada punto de muestra al primer centroide de aceptación, pero si eso no es posible, simplemente lo asignamos al centroide más cercano. La "confusión" del algoritmo proviene de este paso, ya que es posible que los clústeres se superpongan.
  • Recalcule los centroides.

ingrese la descripción de la imagen aquí

(Haga clic para tamaño completo)

Finalmente, muestreamos una gran cantidad de puntos, esta vez usando una distribución uniforme. Usando otro árbol kd, asignamos cada punto a su centroide más cercano, formando grupos. Luego, aproximamos el color medio de cada grupo utilizando un algoritmo de escalada, dando nuestros colores finales de celda (idea para este paso gracias a @PhiNotPi y @ MartinBüttner).

ingrese la descripción de la imagen aquí

Notas

Además de generar un archivo de texto para el fragmento ( OUTFILE), si DEBUGestá configurado para Trueel programa también generará y sobrescribirá las imágenes de arriba. El algoritmo toma un par de minutos para cada imagen, por lo que es una buena forma de verificar el progreso que no agrega mucho al tiempo de ejecución.

Resultados de muestra

N = 32:

ingrese la descripción de la imagen aquí

N = 100:

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

N = 1000:

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

N = 3000:

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

Sp3000
fuente
1
Realmente me gusta lo bien que resultó tu Yoshis blanco.
Max
26

Mathematica, celdas aleatorias

Esta es la solución de referencia, por lo que tiene una idea del mínimo que le pido. Dado el nombre del archivo (localmente o como una URL) en filey N en n, el siguiente código simplemente selecciona N píxeles aleatorios y usa los colores encontrados en esos píxeles. Esto es realmente ingenuo y no funciona increíblemente bien, pero quiero que lo superen después de todo. :)

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

Aquí están todas las imágenes de prueba para N = 100 (todas las imágenes enlazan a versiones más grandes):

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

Como puede ver, estos son esencialmente inútiles. Si bien pueden tener algún valor artístico, de manera expresionista, las imágenes originales son apenas reconocibles.

Para N = 500 , la situación mejora un poco, pero todavía hay artefactos muy extraños, las imágenes se ven borrosas y se desperdician muchas células en regiones sin detalles:

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

¡Tu turno!

Martin Ender
fuente
No soy un buen programador, pero Dios mío, estas imágenes son hermosas. ¡Idea increíble!
Faraz Masroor
¿Alguna razón para Dimensions@ImageDatay no ImageDimensions? Puede evitar la lentitud ImageDatapor completo usando PixelValue.
2012 Arcampion
@ 2012rcampion No hay razón, simplemente no sabía que existía ninguna función. Podría arreglar eso más tarde y también cambiar las imágenes de ejemplo a los valores N recomendados.
Martin Ender
23

Mathematica

Todos sabemos que a Martin le encanta Mathematica, así que déjame probarlo con Mathematica.

Mi algoritmo usa puntos aleatorios de los bordes de la imagen para crear un diagrama de voronoi inicial. El diagrama se embellece mediante un ajuste iterativo de la malla con un filtro medio simple. Esto proporciona imágenes con alta densidad celular cerca de regiones de alto contraste y células visualmente agradables sin ángulos extraños.

Las siguientes imágenes muestran un ejemplo del proceso en acción. La diversión está un poco estropeada por el mal Antialiasing de Mathematicas, pero obtenemos gráficos vectoriales, que deben valer algo.

Este algoritmo, sin el muestreo aleatorio, se puede encontrar en la VoronoiMeshdocumentación aquí .

ingrese la descripción de la imagen aquí ingrese la descripción de la imagen aquí

Imágenes de prueba (100,300,1000,3000)

Código

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]}]]
   ];
pata
fuente
Buen trabajo para un primer post! :) Es posible que desee probar la imagen de prueba de Voronoi con 32 celdas (ese es el número de celdas en la imagen en sí).
Martin Ender
¡Gracias! Supongo que mi algoritmo funcionará terriblemente en este ejemplo. Las semillas se inicializarán en los bordes de la celda y la recursividad no lo hará mucho mejor;)
pata
A pesar de que la velocidad de convergencia a la imagen original es más lenta, ¡encuentro que su algoritmo produce un resultado muy artístico! (como una versión mejorada de Georges Seurat funciona). ¡Gran trabajo!
neizod
También puede obtener colores poligonales interpolados con aspecto vidrioso cambiando sus líneas finales aGraphics@Table[ Append[mp, VertexColors -> RGBColor /@ ImageValue[img, First[mp]]], {mp, MeshPrimitives[voronoiRelaxed, 2]}]
Histogramas
13

Python + SciPy + maestro de ceremonias

El algoritmo que he usado es el siguiente:

  1. Cambiar el tamaño de las imágenes a un tamaño más pequeño (~ 150 píxeles)
  2. Haga una imagen desenfocada de los valores máximos del canal (esto ayuda a no captar regiones blancas con demasiada fuerza).
  3. Toma el valor absoluto.
  4. Elija puntos aleatorios con una probabilidad proporcional a esta imagen. Esto elige puntos a ambos lados de las discontinuidades.
  5. Refine los puntos elegidos para reducir una función de costo. La función es el máximo de la suma de las desviaciones al cuadrado en los canales (de nuevo ayuda a sesgar los colores sólidos y no solo el blanco sólido). He usado mal Markov Chain Monte Carlo con el módulo maestro de ceremonias (muy recomendado) como optimizador. El procedimiento se rescata cuando no se encuentra una nueva mejora después de N iteraciones de cadena.

El algoritmo parece funcionar muy bien. Desafortunadamente, solo puede ejecutarse sensiblemente en imágenes más pequeñas. No he tenido tiempo de tomar los puntos Voronoi y aplicarlos a las imágenes más grandes. Podrían ser refinados en este punto. También podría haber ejecutado el MCMC durante más tiempo para obtener mejores mínimos. El punto débil del algoritmo es que es bastante costoso. No he tenido tiempo de aumentar más allá de 1000 puntos y un par de las imágenes de 1000 puntos todavía se están refinando.

(haga clic derecho y vea la imagen para obtener una versión más grande)

Miniaturas para 100, 300 y 1000 puntos

Los enlaces a versiones más grandes son http://imgur.com/a/2IXDT#9 (100 puntos), http://imgur.com/a/bBQ7q (300 puntos) y http://imgur.com/a/rr8wJ (1000 puntos)

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

Las imágenes enmascaradas sin aspecto se parecen a las siguientes. Se seleccionan puntos aleatorios de la imagen si un número aleatorio es menor que el valor de la imagen (normalizado a 1):

Imagen de Saturno enmascarada sin enfoque

Publicaré imágenes más grandes y los puntos Voronoi si tengo más tiempo.

Editar: si aumenta el número de caminantes a 100 * npts, cambie la función de costo para que sea uno de los cuadrados de las desviaciones en todos los canales y espere mucho tiempo (aumentando el número de iteraciones para salir del bucle a 200), es posible hacer algunas buenas imágenes con solo 100 puntos:

Imagen 11, 100 puntos. Imagen 2, 100 puntos Imagen 4, 100 puntos Imagen 10, 100 puntos.

xioxox
fuente
3

Usando la energía de la imagen como un mapa de peso en puntos

En mi enfoque de este desafío, quería una forma de mapear la "relevancia" de un área de imagen en particular con la probabilidad de que un punto en particular fuera elegido como un centroide de Voronoi. Sin embargo, todavía quería preservar la sensación artística del mosaico de Voronoi eligiendo puntos de imagen al azar. Además, quería operar en imágenes grandes, para no perder nada en el proceso de disminución de resolución. Mi algoritmo es más o menos así:

  1. Para cada imagen, cree un mapa de nitidez. El mapa de nitidez se define por la energía de imagen normalizada (o el cuadrado de la señal de alta frecuencia de la imagen). Un ejemplo se ve así:

Mapa de nitidez

  1. Genere varios puntos de la imagen, tomando el 70 por ciento de los puntos en el mapa de nitidez y el 30 por ciento de todos los demás puntos. Esto significa que los puntos se muestrean más densamente desde partes de alto detalle de la imagen.
  2. ¡Color!

Resultados

N = 100, 500, 1000, 3000

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

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

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

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

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

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

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

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

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

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

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

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

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

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

mprat
fuente
14
¿Le importaría a) incluir el código fuente utilizado para generar esto, yb) vincular cada miniatura a la imagen a tamaño completo?
Martin Ender