Fotomosaicos o: ¿Cuántos programadores se necesitan para reemplazar una bombilla?

33

He compilado un mosaico de 2025 disparos a la cabeza de los avatares de los principales usuarios de Stack Overflow .
(Haga clic en la imagen para verla en tamaño completo).

Mosaico de disparos en la cabeza de StackOverflow

Su tarea es escribir un algoritmo que cree un fotomosaico preciso de otra imagen utilizando los avatares de 48 × 48 píxeles de esta cuadrícula de 45 × 45 de ellos.

Imágenes de prueba

Aquí están las imágenes de prueba. ¡El primero es, por supuesto, una bombilla!
(No son de tamaño completo aquí. Haga clic en una imagen para verla en tamaño completo. Hay versiones de tamaño medio disponibles para The Kiss , A Sunday Afternoon ... , Steve Jobs y las esferas ).

bombilla El beso Un domingo por la tarde en la isla de La Grande Jatte Steve Jobs esferas

Gracias a Wikipedia por todas menos las esferas de trazado de rayos.

A tamaño completo, todas estas imágenes tienen dimensiones divisibles por 48. Las más grandes tenían que ser JPEG para que pudieran comprimirse lo suficiente como para cargarlas.

Tanteo

Este es un concurso de popularidad. La presentación con mosaicos que representan con mayor precisión las imágenes originales debe ser votada. Aceptaré la respuesta más votada en una o dos semanas.

Reglas

  • Sus fotomosaicos deben estar completamente compuestos por avatares inalterados de 48 × 48 píxeles tomados del mosaico de arriba, dispuestos en una cuadrícula.

  • Puedes reutilizar un avatar en un mosaico. (De hecho, para las imágenes de prueba más grandes tendrá que hacerlo).

  • Muestre su salida, pero tenga en cuenta que las imágenes de prueba son muy grandes y que StackExchange solo permite publicar imágenes de hasta 2 MB . Así que comprima sus imágenes o guárdelas en otro lugar y ponga versiones más pequeñas aquí.

  • Para ser confirmado el ganador, debe proporcionar versiones PNG de su bombilla o mosaicos de esferas. Esto es para que pueda validarlos (ver más abajo) para asegurarme de que no agregue colores adicionales a los avatares para que los mosaicos se vean mejor.

Validador

Este script de Python se puede usar para verificar si un mosaico completo realmente usa avatares inalterados. Solo establece toValidatey allTiles. Es poco probable que funcione para JPEG u otros formatos con pérdida, ya que compara las cosas exactamente, píxel por píxel.

from PIL import Image, ImageChops

toValidate = 'test.png' #test.png is the mosaic to validate
allTiles = 'avatars.png' #avatars.png is the grid of 2025 48x48 avatars

def equal(img1, img2):
    return ImageChops.difference(img1, img2).getbbox() is None

def getTiles(mosaic, (w, h)):
    tiles = {}
    for i in range(mosaic.size[0] / w):
        for j in range(mosaic.size[1] / h):
            x, y = i * w, j * h
            tiles[(i, j)] = mosaic.crop((x, y, x + w, y + h))
    return tiles

def validateMosaic(mosaic, allTiles, tileSize):
    w, h = tileSize
    if mosaic.size[0] % w != 0 or mosaic.size[1] % h != 0:
        print 'Tiles do not fit mosaic.'
    elif allTiles.size[0] % w != 0 or allTiles.size[1] % h != 0:
        print 'Tiles do not fit allTiles.'
    else:
        pool = getTiles(allTiles, tileSize)
        tiles = getTiles(mosaic, tileSize)
        matches = lambda tile: equal(tiles[pos], tile)
        success = True
        for pos in tiles:
            if not any(map(matches, pool.values())):
                print 'Tile in row %s, column %s was not found in allTiles.' % (pos[1] + 1, pos[0] + 1)
                success = False
        if success:
            print 'Mosaic is valid.'
            return
    print 'MOSAIC IS INVALID!'

validateMosaic(Image.open(toValidate).convert('RGB'), Image.open(allTiles).convert('RGB'), (48, 48))

¡Buena suerte a todos! No puedo esperar para ver los resultados.

Nota: Sé que los algoritmos fotomosaicos son fáciles de encontrar en línea, pero aún no están en este sitio. Realmente espero que veamos algo más interesante que el algoritmo habitual "promedio de cada mosaico y cada espacio de cuadrícula y unirlos" .

Pasatiempos de Calvin
fuente
1
¿No es esencialmente un duplicado del anterior? Calcular el color de cada uno, reducir el objetivo a 2025 px y aplicar el algoritmo existente?
John Dvorak
2
@ JanDvorak Es similar, pero creo que no es suficiente para ser un duplicado. El algoritmo que mencionó es una forma de obtener un resultado. Sin embargo, hay soluciones mucho más sofisticadas.
Howard
1
Mi gato no está en los avatares :-(
Joey
2
Es posible que desee cambiar " hacer una bombilla" a " reemplazar una bombilla".
DavidC

Respuestas:

15

Java, distancia media

package photomosaic;

import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import javax.imageio.ImageIO;

public class MainClass {

    static final String FILE_IMAGE = "45148_sunday.jpg";
    static final String FILE_AVATARS = "25745_avatars.png";
    static final String FILE_RESULT = "mosaic.png";

    static final int BLOCKSIZE = 48;

    static final int SCALING = 4;

    static final int RADIUS = 3;

    public static void main(String[] args) throws IOException {
        BufferedImage imageAvatars = ImageIO.read(new File(FILE_AVATARS));
        int[] avatars = deBlock(imageAvatars, BLOCKSIZE);

        BufferedImage image = ImageIO.read(new File(FILE_IMAGE));
        int[] data = deBlock(image, BLOCKSIZE);

        // perform the mosaic search on a downscaled version
        int[] avatarsScaled = scaleDown(avatars, BLOCKSIZE, SCALING);
        int[] dataScaled = scaleDown(data, BLOCKSIZE, SCALING);
        int[] bests = mosaicize(dataScaled, avatarsScaled, (BLOCKSIZE / SCALING) * (BLOCKSIZE / SCALING), image.getWidth() / BLOCKSIZE);

        // rebuild the image from the mosaic tiles
        reBuild(bests, data, avatars, BLOCKSIZE);

        reBlock(image, data, BLOCKSIZE);
        ImageIO.write(image, "png", new File(FILE_RESULT));
    }

    // a simple downscale function using simple averaging
    private static int[] scaleDown(int[] data, int size, int scale) {
        int newsize = size / scale;
        int newpixels = newsize * newsize;
        int[] result = new int[data.length / scale / scale];
        for (int r = 0; r < result.length; r += newpixels) {
            for (int y = 0; y < newsize; y++) {
                for (int x = 0; x < newsize; x++) {
                    int avgR = 0;
                    int avgG = 0;
                    int avgB = 0;
                    for (int sy = 0; sy < scale; sy++) {
                        for (int sx = 0; sx < scale; sx++) {
                            int dt = data[r * scale * scale + (y * scale + sy) * size + (x * scale + sx)];
                            avgR += (dt & 0xFF0000) >> 16;
                            avgG += (dt & 0xFF00) >> 8;
                            avgB += (dt & 0xFF) >> 0;
                        }
                    }
                    avgR /= scale * scale;
                    avgG /= scale * scale;
                    avgB /= scale * scale;
                    result[r + y * newsize + x] = 0xFF000000 + (avgR << 16) + (avgG << 8) + (avgB << 0);
                }
            }
        }
        return result;
    }

    // the mosaicize algorithm: take the avatar with least pixel-wise distance
    private static int[] mosaicize(int[] data, int[] avatars, int pixels, int tilesPerLine) {
        int tiles = data.length / pixels;

        // use random order for tile search
        List<Integer> ts = new ArrayList<Integer>();
        for (int t = 0; t < tiles; t++) {
            ts.add(t);
        }
        Collections.shuffle(ts);

        // result array
        int[] bests = new int[tiles];
        Arrays.fill(bests, -1);

        // make list of offsets to be ignored
        List<Integer> ignores = new ArrayList<Integer>();
        for (int sy = -RADIUS; sy <= RADIUS; sy++) {
            for (int sx = -RADIUS; sx <= RADIUS; sx++) {
                if (sx * sx + sy * sy <= RADIUS * RADIUS) {
                    ignores.add(sy * tilesPerLine + sx);
                }
            }
        }

        for (int t : ts) {
            int b = t * pixels;
            int bestsum = Integer.MAX_VALUE;
            for (int at = 0; at < avatars.length / pixels; at++) {
                int a = at * pixels;
                int sum = 0;
                for (int i = 0; i < pixels; i++) {
                    int r1 = (avatars[a + i] & 0xFF0000) >> 16;
                    int g1 = (avatars[a + i] & 0xFF00) >> 8;
                    int b1 = (avatars[a + i] & 0xFF) >> 0;

                    int r2 = (data[b + i] & 0xFF0000) >> 16;
                    int g2 = (data[b + i] & 0xFF00) >> 8;
                    int b2 = (data[b + i] & 0xFF) >> 0;

                    int dr = (r1 - r2) * 30;
                    int dg = (g1 - g2) * 59;
                    int db = (b1 - b2) * 11;

                    sum += Math.sqrt(dr * dr + dg * dg + db * db);
                }
                if (sum < bestsum) {
                    boolean ignore = false;
                    for (int io : ignores) {
                        if (t + io >= 0 && t + io < bests.length && bests[t + io] == at) {
                            ignore = true;
                            break;
                        }
                    }
                    if (!ignore) {
                        bestsum = sum;
                        bests[t] = at;
                    }
                }
            }
        }
        return bests;
    }

    // build image from avatar tiles
    private static void reBuild(int[] bests, int[] data, int[] avatars, int size) {
        for (int r = 0; r < bests.length; r++) {
            System.arraycopy(avatars, bests[r] * size * size, data, r * size * size, size * size);
        }
    }

    // splits the image into blocks and concatenates all the blocks
    private static int[] deBlock(BufferedImage image, int size) {
        int[] result = new int[image.getWidth() * image.getHeight()];
        int r = 0;
        for (int fy = 0; fy < image.getHeight(); fy += size) {
            for (int fx = 0; fx < image.getWidth(); fx += size) {
                for (int l = 0; l < size; l++) {
                    image.getRGB(fx, fy + l, size, 1, result, r * size * size + l * size, size);
                }
                r++;
            }
        }
        return result;
    }

    // unsplits the block version into the original image format
    private static void reBlock(BufferedImage image, int[] data, int size) {
        int r = 0;
        for (int fy = 0; fy < image.getHeight(); fy += size) {
            for (int fx = 0; fx < image.getWidth(); fx += size) {
                for (int l = 0; l < size; l++) {
                    image.setRGB(fx, fy + l, size, 1, data, r * size * size + l * size, size);
                }
                r++;
            }
        }
    }
}

El algoritmo realiza una búsqueda a través de todas las fichas de avatar para cada espacio de cuadrícula por separado. Debido a los pequeños tamaños, no implementé estructuras de datos sofisticadas o algoritmos de búsqueda, sino que simplemente forcé la fuerza bruta en todo el espacio.

Este código no realiza ninguna modificación en los mosaicos (por ejemplo, no se adapta a los colores de destino).

Resultados

Haga clic para obtener una imagen a tamaño completo.

bulbo de luz esferas
domingo

Efecto de radio

Usarlo radiuspuede reducir la repetitividad de los mosaicos en el resultado. La configuración radius=0no tiene ningún efecto. Por ejemplo, radius=3suprime el mismo mosaico dentro de un radio de 3 mosaicos.

bulbo de luz domingo radio = 0

bulbo de luz
bulbo de luz
radio = 3

Efecto del factor de escala

Usando el scalingfactor podemos determinar cómo se busca el mosaico coincidente. scaling=1significa buscar una combinación perfecta de píxeles mientrasscaling=48 hace una búsqueda de mosaico promedio.

escala 48
escala = 48

escala 16
escala = 16

escala 4
escala = 4

escala 1
escala = 1

Howard
fuente
1
Guau. El factor radio realmente mejora los resultados. Esas manchas del mismo avatar no eran buenas.
John Dvorak
1
No estoy seguro de si soy yo, pero Pictureshack parece tener un ancho de banda atroz en comparación con Imgur
Nick T
@NickT Probablemente, pero Imgur comprime todo a lo sumo 1 MB ( imgur.com/faq#size ). :(
Calvin's Hobbies
Hmm, ¿soy solo yo o la respuesta de Mathematica de David es mucho mejor que esta respuesta de los más votados?
justhalf
Lástima que todas esas fotos se hayan ido. ¿Puedes volver a cargar a imgur por casualidad?
MCMastery
19

Mathematica, con control de granularidad.

Utiliza las fotos de 48 x 48 píxeles, según sea necesario. Por defecto, intercambiará esos píxeles por un cuadrado de 48x48 píxeles correspondiente de la imagen que se aproximará.

Sin embargo, el tamaño de los cuadrados de destino se puede configurar para que sea menor que 48 x 48, lo que permite una mayor fidelidad a los detalles. (ver los ejemplos a continuación).

Preprocesar la paleta

collage es la imagen que contiene las fotos para servir como paleta.

picsColorses una lista de fotos individuales combinadas con sus valores medios rojo, verde medio y azul medio.

targetColorToPhoto [] `toma el color promedio de la franja objetivo y encuentra la foto de la paleta que mejor se adapta a ella.

parts=Flatten[ImagePartition[collage,48],1];
picsColors={#,c=Mean[Flatten[ImageData[#],1]]}&/@parts;
nf=Nearest[picsColors[[All,2]]];

targetColorToPhoto[p_]:=Cases[picsColors,{pic_,nf[p,1][[1]]}:>pic][[1]]

Ejemplo

Busquemos la foto que mejor coincida con RGBColor [0.640, 0.134, 0.249]:

example1


photoMosaic

photoMosaic[rawPic_, targetSwathSize_: 48] :=
 Module[{targetPic, data, dims, tiles, tileReplacements, gallery},
  targetPic = Image[data = ImageData[rawPic] /. {r_, g_, b_, _} :> {r, g, b}];
  dims = Dimensions[data];
  tiles = ImagePartition[targetPic, targetSwathSize];
  tileReplacements = targetColorToPhoto /@ (Mean[Flatten[ImageData[#], 1]] & /@Flatten[tiles, 1]);
  gallery = ArrayReshape[tileReplacements, {dims[[1]]/targetSwathSize,dims[[2]]/targetSwathSize}];
  ImageAssemble[gallery]]

`photoMosaic toma como entrada la imagen en bruto de la que haremos un mosaico fotográfico.

targetPic eliminará un cuarto parámetro (de PNG y algunos JPG), dejando solo R, G, B.

dims son las dimensiones de targetPic .

tiles son los pequeños cuadrados que juntos forman la imagen objetivo.

targetSwathSize is the granularity parameter; it defaults at 48 (x48).

tileReplacements son las fotos que coinciden con cada mosaico, en el orden correcto.

gallery es el conjunto de reemplazos de mosaicos (fotos) con la dimensionalidad adecuada (es decir, el número de filas y columnas que coinciden con los mosaicos).

ImageAssembly une el mosaico en una imagen de salida continua.


Ejemplos

Esto reemplaza cada cuadrado de 12x12 de la imagen, el domingo, con una fotografía correspondiente de 48 x 48 píxeles que mejor se adapta al color promedio.

photoMosaic[sunday, 12]

sunday2


Domingo (detalle)

top hat


photoMosaic[lightbulb, 6]

lightbulb 6


photoMosaic[stevejobs, 24]

steve jobs 24


Detalle, stevejobs.

jobs detail


photoMosaic[kiss, 24]

kiss


Detalle del beso:

detail kiss


photoMosaic[spheres, 24]

spheres

DavidC
fuente
1
Me gusta la idea de granularidad. Da más realismo a las imágenes más pequeñas.
Aficiones de Calvin
7

JS

Igual que en el golf anterior: http://jsfiddle.net/eithe/J7jEk/ : D

(esta vez llamado con unique: false, {pixel_2: {width: 48, height: 48}, pixel_1: {width: 48, height: 48}} ) (no trate que la paleta use un píxel una vez, los píxeles de la paleta son muestras de 48x48, los píxeles de forma son muestras de 48x48).

Actualmente busca en la lista de avatares para encontrar la coincidencia más cercana por peso del algoritmo seleccionado, sin embargo, no realiza ninguna coincidencia de uniformidad de color (algo que necesito echar un vistazo.

  • balanced
  • lab

Desafortunadamente, no puedo jugar con imágenes más grandes, porque mi RAM se agota: D Si es posible, agradecería imágenes de salida más pequeñas. Si usa la mitad del tamaño de la imagen, aquí está el domingo por la tarde:

  • balanced
  • lab
eithed
fuente
2
Acabo de agregar imágenes de medio tamaño que aún son divisibles por 48 píxeles.
Hobbies de Calvin
5

GLSL

La diferencia entre este desafío y el de American Gothic en la paleta de Mona Lisa: reorganizar los píxeles me interesó, porque los mosaicos se pueden reutilizar, mientras que los píxeles no. Esto significa que es posible paralelizar fácilmente el algoritmo, así que decidí intentar una versión paralela masiva. Por "masivamente" me refiero a usar los núcleos de sombreador 1344 en el GTX670 de mi escritorio simultáneamente, a través de GLSL.

Método

La coincidencia real de mosaicos es simple: calculo la distancia RGB entre cada píxel en un área objetivo y el área de mosaicos, y elijo el mosaico con la menor diferencia (ponderado por los valores de brillo). El índice de mosaico se escribe en los atributos de color rojo y verde del fragmento, luego, después de haber procesado todos los fragmentos, leí los valores del framebuffer y construí la imagen de salida a partir de esos índices. La implementación real es un gran truco; en lugar de crear un FBO, acabo de abrir una ventana y la rendericé, pero GLFW no puede abrir ventanas con resoluciones arbitrariamente pequeñas, por lo que creo la ventana más grande de lo necesario, luego dibujo un pequeño rectángulo del tamaño correcto para que tenga un fragmento por mosaico que se asigna a la imagen de origen. Toda la solución MSVC2013 está disponible enhttps://bitbucket.org/Gibgezr/mosaicmaker Requiere GLFW / FreeImage / GLEW / GLM para compilar, y OpenGL 3.3 o mejores controladores / tarjeta de video para funcionar.

Fragmento Shader Source

#version 330 core

uniform sampler2D sourceTexture;
uniform sampler2D mosaicTexture;

in vec2 v_texcoord;

out vec4 out_Color;

void main(void)
{   
    ivec2 sourceSize = textureSize(sourceTexture, 0);
    ivec2 mosaicSize = textureSize(mosaicTexture, 0);

    float num_pixels = mosaicSize.x/45.f;
    vec4 myTexel;
    float difference = 0.f;

    //initialize smallest difference to a large value
    float smallest_difference = 255.0f*255.0f*255.0f;
    int closest_x = 0, closest_y = 0;

    vec2 pixel_size_src = vec2( 1.0f/sourceSize.x, 1.0f/sourceSize.y);
    vec2 pixel_size_mosaic = vec2( 1.0f/mosaicSize.x , 1.0f/mosaicSize.y);

    vec2 incoming_texcoord;
    //adjust incoming uv to bottom corner of the tile space
    incoming_texcoord.x =  v_texcoord.x - 1.0f/(sourceSize.x / num_pixels * 2.0f);
    incoming_texcoord.y =  v_texcoord.y - 1.0f/(sourceSize.y / num_pixels * 2.0f);

    vec2 texcoord_mosaic;
    vec2 pixelcoord_src, pixelcoord_mosaic;
    vec4 pixel_src, pixel_mosaic;

    //loop over all of the mosaic tiles
    for(int i = 0; i < 45; ++i)
    {
        for(int j = 0; j < 45; ++j)
        {
            difference = 0.f;
            texcoord_mosaic = vec2(j * pixel_size_mosaic.x * num_pixels, i * pixel_size_mosaic.y * num_pixels);

            //loop over all of the pixels in the images, adding to the difference
            for(int y = 0; y < num_pixels; ++y)
            {
                for(int x = 0; x < num_pixels; ++x)
                {
                    pixelcoord_src = vec2(incoming_texcoord.x + x * pixel_size_src.x, incoming_texcoord.y + y * pixel_size_src.y);                  
                    pixelcoord_mosaic = vec2(texcoord_mosaic.x + x * pixel_size_mosaic.x, texcoord_mosaic.y + y * pixel_size_mosaic.y); 
                    pixel_src = texture(sourceTexture, pixelcoord_src);
                    pixel_mosaic = texture(mosaicTexture, pixelcoord_mosaic);

                    pixel_src *= 255.0f;
                    pixel_mosaic *= 255.0f;

                    difference += (pixel_src.x - pixel_mosaic.x) * (pixel_src.x - pixel_mosaic.x) * 0.5f+
                        (pixel_src.y - pixel_mosaic.y) * (pixel_src.y - pixel_mosaic.y) +
                        (pixel_src.z - pixel_mosaic.z) * (pixel_src.z - pixel_mosaic.z) * 0.17f;
                }

            }

            if(difference < smallest_difference)
            {
                smallest_difference = difference;
                closest_x = j;
                closest_y = i;
            }               
        }
    }

    myTexel.x = float(closest_x)/255.f;
    myTexel.y = float(closest_y)/255.f;
    myTexel.z = 0.f;
    myTexel.w = 0.f;    

    out_Color = myTexel;
}

Resultados

Las imágenes se reproducen casi al instante, por lo que la paralelización fue un éxito. La desventaja es que no puedo hacer que los fragmentos individuales dependan de la salida de cualquier otro fragmento, por lo que no hay forma de obtener el aumento de calidad significativo que puede obtener al no elegir el mismo mosaico dos veces dentro de un cierto rango. Entonces, resultados rápidos, pero calidad limitada debido a repeticiones masivas de mosaicos. En general, fue divertido. http://imgur.com/a/M0Db0 para versiones de tamaño completo. enter image description here

Darren
fuente
4

Pitón

Aquí va la primera solución de Python, utilizando un enfoque medio. Podemos evolucionar desde aquí. El resto de las imágenes están aquí .

sunday steve

from PIL import Image
import numpy as np

def calcmean(b):
    meansum = 0
    for k in range(len(b)):
        meansum = meansum + (k+1)*b[k]
    return meansum/sum(b)    

def gettiles(imageh,w,h):
    k = 0 
    tiles = {}
    for x in range(0,imageh.size[0],w):
        for y in range(0,imageh.size[1],h):
            a=imageh.crop((x, y, x + w, y + h))
            b=a.resize([1,1], Image.ANTIALIAS)
            tiles[k] = [a,x,y,calcmean(b.histogram()[0:256]) \
                             ,calcmean(b.histogram()[256:256*2]) \
                             ,calcmean(b.histogram()[256*2:256*3])]
            k = k + 1
    return tiles

w = 48 
h = 48

srch = Image.open('25745_avatars.png').convert('RGB')
files = ['21104_spheres.png', '45148_sunday.jpg', '47824_steve.jpg', '61555_kiss.jpg', '77388_lightbulb.png']
for f in range(len(files)):
    desh = Image.open(files[f]).convert('RGB')

    srctiles = gettiles(srch,w,h)
    destiles = gettiles(desh,w,h)

    #build proximity matrix 
    pm = np.zeros((len(destiles),len(srctiles)))
    for d in range(len(destiles)):
        for s in range(len(srctiles)):
            pm[d,s] = (srctiles[s][3]-destiles[d][3])**2 + \
                      (srctiles[s][4]-destiles[d][4])**2 + \
                      (srctiles[s][5]-destiles[d][5])**2

    for k in range(len(destiles)):
        j = list(pm[k,:]).index(min(pm[k,:]))
        desh.paste(srctiles[j][0], (destiles[k][1], destiles[k][2]))

    desh.save(files[f].replace('.','_m'+'.'))
Willem
fuente
1

Otra solución más de Python: basada en el promedio (RGB vs L a b *)

Resultados (hay algunas diferencias ligeramente)

Bombilla - RGB

vista completa

bulb_rgb

Bombilla - Laboratorio

vista completa

bulb_lab

Steve - RGB

vista completa

steve_rgb

Steve - Laboratorio

vista completa

steve_lab

Esferas - RGB

vista completa

spheres_rgb

Esferas - Laboratorio

vista completa

spheres_lab

Domingo - RGB

vista completa

sunday_rgb

Domingo - laboratorio

vista completa

sunday_lab

Beso - RGB

vista completa

kiss_rgb

Beso - laboratorio

vista completa

kiss_lab

Código

requiere python-colormath para Lab

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from PIL import Image
from colormath.color_objects import LabColor,sRGBColor
from colormath.color_conversions import convert_color
from colormath.color_diff import delta_e_cie1976

def build_photomosaic_ils(mosaic_im,target_im,block_width,block_height,colordiff,new_filename):

    mosaic_width=mosaic_im.size[0]              #dimensions of the target image
    mosaic_height=mosaic_im.size[1]

    target_width=target_im.size[0]              #dimensions of the target image
    target_height=target_im.size[1]

    target_grid_width,target_grid_height=get_grid_dimensions(target_width,target_height,block_width,block_height)       #dimensions of the target grid
    mosaic_grid_width,mosaic_grid_height=get_grid_dimensions(mosaic_width,mosaic_height,block_width,block_height)       #dimensions of the mosaic grid

    target_nboxes=target_grid_width*target_grid_height
    mosaic_nboxes=mosaic_grid_width*mosaic_grid_height

    print "Computing the average color of each photo in the mosaic..."
    mosaic_color_averages=compute_block_avg(mosaic_im,block_width,block_height)
    print "Computing the average color of each block in the target photo ..."
    target_color_averages=compute_block_avg(target_im,block_width,block_height)

    print "Computing photomosaic ..."
    photomosaic=[0]*target_nboxes
    for n in xrange(target_nboxes):
        print "%.2f " % (n/float(target_nboxes)*100)+"%"
        for z in xrange(mosaic_nboxes):
            current_diff=colordiff(target_color_averages[n],mosaic_color_averages[photomosaic[n]])
            candidate_diff=colordiff(target_color_averages[n],mosaic_color_averages[z])

            if(candidate_diff<current_diff):
                photomosaic[n]=z

    print "Building final image ..."
    build_final_solution(photomosaic,mosaic_im,target_nboxes,target_im,target_grid_width,block_height,block_width,new_filename)

def build_initial_solution(target_nboxes,mosaic_nboxes):
    candidate=[0]*target_nboxes

    for n in xrange(target_nboxes):
        candidate[n]=random.randint(0,mosaic_nboxes-1)

    return candidate

def build_final_solution(best,mosaic_im,target_nboxes,target_im,target_grid_width,block_height,block_width,new_filename):

    for n in xrange(target_nboxes):

        i=(n%target_grid_width)*block_width             #i,j -> upper left point of the target image
        j=(n/target_grid_width)*block_height

        box = (i,j,i+block_width,j+block_height)        

        #get the best photo from the mosaic
        best_photo_im=get_block(mosaic_im,best[n],block_width,block_height)

        #paste the best photo found back into the image
        target_im.paste(best_photo_im,box)

    target_im.save(new_filename);


#get dimensions of the image grid
def get_grid_dimensions(im_width,im_height,block_width,block_height):
    grid_width=im_width/block_width     #dimensions of the target image grid
    grid_height=im_height/block_height
    return grid_width,grid_height

#compute the fitness of given candidate solution
def fitness(candidate,mosaic_color_averages,mosaic_nboxes,target_color_averages,target_nboxes):
    error=0.0
    for i in xrange(target_nboxes):
        error+=colordiff_rgb(mosaic_color_averages[candidate[i]],target_color_averages[i])
    return error

#get a list of color averages, i.e, the average color of each block in the given image
def compute_block_avg(im,block_height,block_width):

    width=im.size[0]
    height=im.size[1]

    grid_width_dim=width/block_width                    #dimension of the grid
    grid_height_dim=height/block_height

    nblocks=grid_width_dim*grid_height_dim              #number of blocks

    avg_colors=[]
    for i in xrange(nblocks):
        avg_colors+=[avg_color(get_block(im,i,block_width,block_height))]
    return avg_colors

#returns the average RGB color of a given image
def avg_color(im):
    avg_r=avg_g=avg_b=0.0
    pixels=im.getdata()
    size=len(pixels)
    for p in pixels:
        avg_r+=p[0]/float(size)
        avg_g+=p[1]/float(size)
        avg_b+=p[2]/float(size)

    return (avg_r,avg_g,avg_b)

#get the nth block of the image
def get_block(im,n,block_width,block_height):

    width=im.size[0]

    grid_width_dim=width/block_width                        #dimension of the grid

    i=(n%grid_width_dim)*block_width                        #i,j -> upper left point of the target block
    j=(n/grid_width_dim)*block_height

    box = (i,j,i+block_width,j+block_height)
    block_im = im.crop(box)
    return block_im


#calculate color difference of two pixels in the RGB space
#less is better
def colordiff_rgb(pixel1,pixel2):

    delta_red=pixel1[0]-pixel2[0]
    delta_green=pixel1[1]-pixel2[1]
    delta_blue=pixel1[2]-pixel2[2]

    fit=delta_red**2+delta_green**2+delta_blue**2
    return fit

#http://python-colormath.readthedocs.org/en/latest/index.html
#calculate color difference of two pixels in the L*ab space
#less is better
def colordiff_lab(pixel1,pixel2):

    #convert rgb values to L*ab values
    rgb_pixel_1=sRGBColor(pixel1[0],pixel1[1],pixel1[2],True)
    lab_1= convert_color(rgb_pixel_1, LabColor)

    rgb_pixel_2=sRGBColor(pixel2[0],pixel2[1],pixel2[2],True)
    lab_2= convert_color(rgb_pixel_2, LabColor)

    #calculate delta e
    delta_e = delta_e_cie1976(lab_1, lab_2)
    return delta_e


if __name__ == '__main__':
    mosaic="images/25745_avatars.png"
    targets=["images/lightbulb.png","images/steve.jpg","images/sunday.jpg","images/spheres.png","images/kiss.jpg"]
    target=targets[0]
    mosaic_im=Image.open(mosaic)
    target_im=Image.open(target)
    new_filename=target.split(".")[0]+"_photomosaic.png"
    colordiff=colordiff_rgb

    build_photomosaic_ils(mosaic_im,target_im,48,48,colordiff,new_filename)
AlexPnt
fuente