Fotomosaici o: quanti programmatori ci vogliono per sostituire una lampadina?


33

Ho compilato un mosaico di 2025 colpi alla testa dagli avatar degli utenti principali di Stack Overflow .
(Fare clic sull'immagine per visualizzarla a grandezza naturale.)

Mosaico di colpi alla testa StackOverflow

Il tuo compito è quello di scrivere un algoritmo che creerà un fotomosaico accurato di un'altra immagine usando gli avatar 48 × 48 pixel da questa griglia 45 × 45.

Immagini di prova

Ecco le immagini di prova. La prima è, ovviamente, una lampadina!
(Non sono a grandezza naturale qui. Fai clic su un'immagine per visualizzarla a grandezza naturale. Sono disponibili versioni a mezza grandezza per The Kiss , A Sunday Afternoon ... , Steve Jobs e le sfere .)

lampadina Il bacio Una domenica pomeriggio sull'isola della Grande Jatte Steve Jobs sfere

Grazie a Wikipedia per tutti tranne le sfere raytracing.

A grandezza naturale, tutte queste immagini hanno dimensioni divisibili per 48. Le più grandi dovevano essere JPEG per poter essere sufficientemente compresse da caricare.

punteggio

Questo è un concorso di popolarità. La votazione con i mosaici che descrivono più accuratamente le immagini originali dovrebbe essere votata. Accetterò la risposta più votata tra una settimana o due.

Regole

  • La tua fotomosaica deve essere interamente composta da avatar inalterati di 48 × 48 pixel presi dal mosaico sopra, disposti in una griglia.

  • Puoi riutilizzare un avatar in un mosaico. (In effetti per le immagini di prova più grandi dovrete.)

  • Mostra il tuo output, ma tieni presente che le immagini di prova sono molto grandi e StackExchange consente solo la pubblicazione di immagini fino a 2 MB . Quindi comprimere le immagini o ospitarle altrove e inserire qui versioni più piccole.

  • Per essere confermato il vincitore devi fornire le versioni PNG della tua lampadina o dei mosaici a sfere. Questo è così che posso convalidarli (vedi sotto) per assicurarmi di non aggiungere colori extra agli avatar per rendere i mosaici migliori.

Validator

Questo script Python può essere utilizzato per verificare se un mosaico completato utilizza davvero avatar inalterati. Basta impostare toValidatee allTiles. È improbabile che funzioni per JPEG o altri formati con perdita di dati poiché confronta esattamente le cose, pixel per pixel.

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

Buona fortuna a tutti! Non vedo l'ora di vedere i risultati.

Nota: so che gli algoritmi fotomosaici sono facili da trovare online, ma non sono ancora su questo sito. Spero davvero che vedremo qualcosa di più interessante del solito algoritmo "media ogni riquadro e ogni spazio della griglia e abbinarli" .


1
Non è essenzialmente un duplicato del precedente? Calcola il colore di ognuno, riduci il target a 2025 px e applica l'algoritmo esistente?
John Dvorak,


2
@JanDvorak È simile ma penso che non sia abbastanza per essere un duplicato. L'algoritmo che hai citato è un modo per ottenere un risultato. Esistono tuttavia soluzioni molto più sofisticate.
Howard,

1
Il mio gatto non è presente negli avatar :-(
Joey,

2
Potresti voler cambiare "per creare una lampadina" in "per sostituire una lampadina".
DavidC,

Risposte:


15

Java, distanza 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++;
            }
        }
    }
}

L'algoritmo esegue una ricerca attraverso tutte le tessere avatar per ogni spazio della griglia separatamente. A causa delle dimensioni ridotte, non ho implementato strutture di dati sofisticati o algoritmi di ricerca, ma ho semplicemente forzato l'intero spazio.

Questo codice non modifica le tessere (es. Nessun adattamento ai colori di destinazione).

risultati

Fare clic per l'immagine a dimensione intera.

luce buld sfere
Domenica

Effetto del raggio

Usando radiuspuoi ridurre la ripetitività delle tessere nel risultato. L'impostazione radius=0non ha alcun effetto. Ad esempio radius=3sopprime la stessa tessera in un raggio di 3 tessere.

luce buld Domenica raggio = 0

luce buld
luce buld
Raggio = 3

Effetto del fattore di ridimensionamento

Usando il scalingfattore possiamo determinare come viene cercata la tessera corrispondente. scaling=1significa cercare una corrispondenza pixel perfetta mentre scaling=48fa una ricerca a tessera media.

ridimensionamento 48
scalatura = 48

ridimensionamento 16
scalatura = 16

ridimensionamento 4
scalatura = 4

ridimensionamento 1
scalatura = 1


1
Wow. Il fattore raggio migliora davvero i risultati. Quelle macchie con lo stesso avatar non erano buone.
John Dvorak,

1
Non sono sicuro se sono io, ma Pictureshack sembra avere una larghezza di banda atroce rispetto a Imgur
Nick T

@NickT Probabilmente, ma Imgur comprime tutto al massimo 1 MB ( imgur.com/faq#size ). :(
Calvin's Hobbies,

Hmm, sono solo io o la risposta di Mathematica di David è di gran lunga migliore di questa risposta con il voto più alto?
solo il

Peccato che tutte quelle foto siano sparite. Puoi ricaricarlo su imgur per caso?
MCMastery,

19

Mathematica, con controllo per granularità

Questo utilizza le foto da 48 x 48 pixel, come richiesto. Per impostazione predefinita, sostituirà quei pixel con un corrispondente quadrato di 48x48 pixel dall'immagine da approssimare.

Tuttavia, la dimensione dei quadrati di destinazione può essere impostata su un valore inferiore a 48 x 48, consentendo una maggiore fedeltà ai dettagli. (vedi gli esempi seguenti).

Preelaborare la tavolozza

collage è l'immagine contenente le foto da utilizzare come tavolozza.

picsColorsè un elenco di singole foto associate al loro valore medio rosso, verde medio e blu medio.

targetColorToPhoto [] `prende il colore medio dell'andana target e trova la foto dalla tavolozza che meglio corrisponde.

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

Esempio

Troviamo la foto che corrisponde meglio a RGBColor [0.640, 0.134, 0.249]:

Esempio 1


fotomosaico

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 prende come input l'immagine grezza di cui creeremo un mosaico fotografico.

targetPic rimuoverà un quarto parametro (di PNG e alcuni JPG), lasciando solo R, G, B.

dimssono le dimensioni di targetPic.

tiles sono i quadratini che insieme compongono l'immagine del bersaglio.

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

tileReplacements sono le foto che corrispondono a ciascuna tessera, nell'ordine corretto.

gallery è l'insieme di sostituzioni di piastrelle (foto) con la corretta dimensionalità (ovvero il numero di righe e colonne che corrispondono alle piastrelle).

ImageAssembly unisce il mosaico in un'immagine di output continuo.


Esempi

Questo sostituisce ogni quadrato 12x12 dall'immagine, domenica, con una corrispondente fotografia di 48 x 48 pixel che meglio si adatta a un colore medio.

photoMosaic[sunday, 12]

sunday2


Domenica (dettaglio)

cappello a cilindro


photoMosaic[lightbulb, 6]

lampadina 6


photoMosaic[stevejobs, 24]

steve jobs 24


Dettaglio, stevejobs.

dettaglio lavori


photoMosaic[kiss, 24]

bacio


Particolare del bacio:

bacio di dettaglio


photoMosaic[spheres, 24]

sfere


1
Mi piace l'idea di granularità. Dà più realismo alle immagini più piccole.
Calvin's Hobbies,

7

JS

Come nel golf precedente: http://jsfiddle.net/eithe/J7jEk/ : D

(questa volta chiamato con unique: false, {pixel_2: {width: 48, height: 48}, pixel_1: {width: 48, height: 48}}) (non trattare la palette per usare un solo pixel una volta, i pixel della palette sono campioni 48x48, i pixel forma sono i campioni 48x48).

Attualmente cerca nell'elenco degli avatar per trovare la corrispondenza più vicina in base al peso dell'algoritmo selezionato, tuttavia non esegue alcuna corrispondenza di uniformità del colore (qualcosa che devo dare un'occhiata.

  • equilibrato
  • laboratorio

Sfortunatamente non sono in grado di giocare con immagini più grandi, perché la mia RAM si esaurisce: D Se possibile apprezzerei immagini di output più piccole. Se si utilizza 1/2 della dimensione dell'immagine fornita, ecco domenica pomeriggio:

  • equilibrato
  • laboratorio

2
Ho appena aggiunto immagini di mezza dimensione che sono ancora divisibili per 48 pixel.
Calvin's Hobbies,

5

GLSL

La differenza tra questa sfida e quella dell'American Gothic nella palette di Mona Lisa: Riorganizzare i pixel mi ha interessato, perché le tessere di mosaico possono essere riutilizzate, mentre i pixel no. Ciò significa che è possibile parallelizzare facilmente l'algoritmo, quindi ho deciso di provare una versione massicciamente parallela. Con "massiccia" intendo usare i 1344 shader core sul mio desktop GTX670 tutti contemporaneamente, tramite GLSL.

Metodo

L'effettivo abbinamento delle piastrelle è semplice: calcolo la distanza RGB tra ciascun pixel in un'area target e l'area della tessera mosaico e scelgo la piastrella con la differenza più bassa (ponderata in base ai valori di luminosità). L'indice della piastrella viene scritto negli attributi di colore rosso e verde del frammento, quindi dopo che tutti i frammenti sono stati resi, rileggo i valori dal framebuffer e costruisco l'immagine di output da quegli indici. L'implementazione effettiva è piuttosto un hack; invece di creare un FBO ho appena aperto una finestra e renderizzata al suo interno, ma GLFW non può aprire finestre con risoluzioni arbitrariamente piccole, quindi creo la finestra più grande del necessario, quindi disegno un piccolo rettangolo delle dimensioni corrette in modo che abbia un frammento per riquadro che si associa all'immagine sorgente. L'intera soluzione MSVC2013 è disponibile all'indirizzohttps://bitbucket.org/Gibgezr/mosaicmaker Richiede la compilazione di GLFW / FreeImage / GLEW / GLM e l'esecuzione di OpenGL 3.3 o migliori driver / scheda video.

Frammento Sorgente Shader

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

risultati

Le immagini vengono renderizzate quasi istantaneamente, quindi la parallelizzazione è stata un successo. Il rovescio della medaglia è che non posso fare in modo che i singoli frammenti facciano affidamento sull'output di altri frammenti, quindi non c'è modo di ottenere il significativo aumento di qualità che si può ottenere non selezionando la stessa tessera due volte entro un certo intervallo. Quindi, risultati veloci, ma qualità limitata a causa delle massicce ripetizioni di piastrelle. Tutto sommato, è stato divertente. http://imgur.com/a/M0Db0 per versioni full-size. inserisci qui la descrizione dell'immagine


4

Pitone

Ecco la prima soluzione Python, usando un approccio medio. Possiamo evolvere da qui. Il resto delle immagini sono qui .

Domenica 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'+'.'))

1

Ancora un'altra soluzione Python - Average Based (RGB vs L a b *)

Risultati (ci sono alcune differenze)

Lampadina - RGB

vista completa

bulb_rgb

Bulb - Lab

vista completa

bulb_lab

Steve - RGB

vista completa

steve_rgb

Steve - Lab

vista completa

steve_lab

Sfere - RGB

vista completa

spheres_rgb

Sfere - Lab

vista completa

spheres_lab

Domenica - RGB

vista completa

sunday_rgb

Domenica - Lab

vista completa

sunday_lab

Bacio - RGB

vista completa

kiss_rgb

Kiss - Lab

vista completa

kiss_lab

Codice

richiede python-colormath per 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)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.