Peindre en chiffres

42

Vous êtes donné une image en vraies couleurs. Votre tâche consiste à générer une version de cette image, qui semble avoir été peinte à l'aide de peinture par numéros (l'activité des enfants, et non les non- graphiques). En plus de l'image, vous avez deux paramètres: P , la taille maximale de la palette de couleurs (c'est-à-dire le nombre maximal de couleurs distinctes à utiliser) et N , le nombre maximal de cellules à utiliser. Votre algorithme ne doit pas nécessairement utiliser toutes les couleurs P et N cellules, mais il ne doit pas en utiliser plus. L'image de sortie doit avoir les mêmes dimensions que l'entrée.

Une cellule est définie comme une zone contiguë de pixels qui ont tous la même couleur. Les pixels se touchant uniquement dans un coin ne sont pas considérés comme contigus. Les cellules peuvent avoir des trous.

En bref, vous devez approximer l’image d’entrée avec seulement N zones d’ombre / couleurs unies et P couleurs différentes.

Juste pour visualiser les paramètres, voici un exemple très simple (sans image d'entrée particulière; montrant mes compétences en peinture folles). L'image suivante a P = 6 et N = 11 :

entrez la description de l'image ici

Voici quelques images pour tester votre algorithme (principalement nos suspects habituels). Cliquez sur les images pour les versions plus grandes.

Grande vague Récif de corail arc en ciel Nuit étoilée rivière Ours brun Cascade Mandrill Nébuleuse de crabe gothique americain Mona Lisa Crier

Veuillez inclure une poignée de résultats pour différents paramètres. Si vous souhaitez afficher un grand nombre de résultats, vous pouvez créer une galerie sur imgur.com , afin que la taille des réponses reste raisonnable. Vous pouvez également placer des vignettes dans votre message et les lier à de plus grandes images, comme je l’ai fait ci-dessus. Aussi, n'hésitez pas à utiliser d'autres images de test, si vous trouvez quelque chose de bien.

Je suppose que les paramètres autour de N ≥ 500 , P ~ 30 seraient similaires à de véritables modèles de peinture par numéro.

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

  • comment bien les images originales sont approchées.
  • l'efficacité de l'algorithme sur différents types d'images (les peintures sont probablement généralement plus simples que les photographies).
  • comment bien l'algorithme fonctionne avec des paramètres très restrictifs.
  • comment organique / lisse les formes des cellules.

J'utiliserai le script Mathematica suivant pour valider les résultats:

image = <pastedimagehere> // ImageData;
palette = Union[Join @@ image];
Print["P = ", Length@palette];
grid = GridGraph[Reverse@Most@Dimensions@image];
image = Flatten[image /. Thread[palette -> Range@Length@palette]];
Print["N = ", 
 Length@ConnectedComponents[
   Graph[Cases[EdgeList[grid], 
     m_ <-> n_ /; image[[m]] == image[[n]]]]]]

Sp3000 a eu la gentillesse d’écrire un vérificateur dans Python 2 à l’aide de PIL, que vous trouverez dans ce pastebin .

Martin Ender
la source
2
Ce n’est pas très efficace, mais voici un vérificateur Python 2 PIL .
Sp3000
Quelle belle question, mais j'espérais que nous verrions aussi la bonne version "peinture par numéros". C’est avec les chiffres en place pour que je puisse utiliser les réponses :)
@ Lembik Au départ, je voulais inclure cela, mais j’ai senti que cela détournait de la partie intéressante de la question. Toutefois, il ne devrait pas être trop difficile de convertir le résultat d’une des soumissions en un modèle.
Martin Ender
Ceci est un post fascinant. Quelqu'un a-t-il pris la mesure supplémentaire d'ajouter les numéros de couleur, comme une peinture par numéro?
B. Blair

Réponses:

39

Python 2 avec PIL ( Galerie )

from __future__ import division
from PIL import Image
import random, math, time
from collections import Counter, defaultdict, namedtuple

"""
Configure settings here
"""

INFILE = "spheres.png"
OUTFILE_STEM = "out"
P = 30
N = 300
OUTPUT_ALL = True # Whether to output the image at each step

FLOOD_FILL_TOLERANCE = 10
CLOSE_CELL_TOLERANCE = 5
SMALL_CELL_THRESHOLD = 10
FIRST_PASS_N_RATIO = 1.5
K_MEANS_TRIALS = 30
BLUR_RADIUS = 2
BLUR_RUNS = 3

"""
Color conversion functions
"""

X = xrange

# 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):rgb=xyz2rgb(lab2xyz(lab));return tuple([int(round(x))for x in rgb])

"""
Stage 1: Read in image and convert to CIELAB
"""

total_time = time.time()

im = Image.open(INFILE)
width, height = im.size

if OUTPUT_ALL:
  im.save(OUTFILE_STEM + "0.png")
  print "Saved image %s0.png" % OUTFILE_STEM

def make_pixlab_map(im):
  width, height = im.size
  pixlab_map = {}

  for i in X(width):
    for j in X(height):
      pixlab_map[(i, j)] = rgb2lab(im.getpixel((i, j)))

  return pixlab_map

pixlab_map = make_pixlab_map(im)

print "Stage 1: CIELAB conversion complete"

"""
Stage 2: Partitioning the image into like-colored cells using flood fill
"""

def d(color1, color2):
  return (abs(color1[0]-color2[0])**2 + abs(color1[1]-color2[1])**2 + abs(color1[2]-color2[2])**2)**.5

def neighbours(pixel):
  results = []

  for neighbour in [(pixel[0]+1, pixel[1]), (pixel[0]-1, pixel[1]),
            (pixel[0], pixel[1]+1), (pixel[0], pixel[1]-1)]:

    if 0 <= neighbour[0] < width and 0 <= neighbour[1] < height:
      results.append(neighbour)

  return results

def flood_fill(start_pixel):
  to_search = {start_pixel}
  cell = set()
  searched = set()
  start_color = pixlab_map[start_pixel]

  while to_search:
    pixel = to_search.pop()

    if d(start_color, pixlab_map[pixel]) < FLOOD_FILL_TOLERANCE:
      cell.add(pixel)
      unplaced_pixels.remove(pixel)

      for n in neighbours(pixel):
        if n in unplaced_pixels and n not in cell and n not in searched:
          to_search.add(n)

    else:
      searched.add(pixel)

  return cell

# These two maps are inverses, pixel/s <-> number of cell containing pixel
cell_sets = {}
pixcell_map = {}
unplaced_pixels = {(i, j) for i in X(width) for j in X(height)}

while unplaced_pixels:
  start_pixel = unplaced_pixels.pop()
  unplaced_pixels.add(start_pixel)
  cell = flood_fill(start_pixel)

  cellnum = len(cell_sets)
  cell_sets[cellnum] = cell

  for pixel in cell:
    pixcell_map[pixel] = cellnum

print "Stage 2: Flood fill partitioning complete, %d cells" % len(cell_sets)

"""
Stage 3: Merge cells with less than a specified threshold amount of pixels to reduce the number of cells
     Also good for getting rid of some noise
"""

def mean_color(cell, color_map):
  L_sum = 0
  a_sum = 0
  b_sum = 0

  for pixel in cell:
    L, a, b = color_map[pixel]
    L_sum += L
    a_sum += a
    b_sum += b

  return L_sum/len(cell), a_sum/len(cell), b_sum/len(cell)

def remove_small(cell_size):
  if len(cell_sets) <= N:
    return

  small_cells = []

  for cellnum in cell_sets:
    if len(cell_sets[cellnum]) <= cell_size:
      small_cells.append(cellnum)

  for cellnum in small_cells:
    neighbour_cells = []

    for cell in cell_sets[cellnum]:
      for n in neighbours(cell):
        neighbour_reg = pixcell_map[n]

        if neighbour_reg != cellnum:
          neighbour_cells.append(neighbour_reg)

    closest_cell = max(neighbour_cells, key=neighbour_cells.count)

    for cell in cell_sets[cellnum]:
      pixcell_map[cell] = closest_cell

    if len(cell_sets[closest_cell]) <= cell_size:
      small_cells.remove(closest_cell)

    cell_sets[closest_cell] |= cell_sets[cellnum]
    del cell_sets[cellnum]

    if len(cell_sets) <= N:
      return

for cell_size in X(1, SMALL_CELL_THRESHOLD):
  remove_small(cell_size)

if OUTPUT_ALL:
  frame_im = Image.new("RGB", im.size)

  for cellnum in cell_sets:
    cell_color = mean_color(cell_sets[cellnum], pixlab_map)

    for pixel in cell_sets[cellnum]:
      frame_im.putpixel(pixel, lab2rgb(cell_color))

  frame_im.save(OUTFILE_STEM + "1.png")
  print "Saved image %s1.png" % OUTFILE_STEM

print "Stage 3: Small cell merging complete, %d cells" % len(cell_sets)

"""
Stage 4: Close color merging
"""

cell_means = {}

for cellnum in cell_sets:
  cell_means[cellnum] = mean_color(cell_sets[cellnum], pixlab_map)

n_graph = defaultdict(set)

for i in X(width):
  for j in X(height):
    pixel = (i, j)
    cell = pixcell_map[pixel]

    for n in neighbours(pixel):
      neighbour_cell = pixcell_map[n]

      if neighbour_cell != cell:
        n_graph[cell].add(neighbour_cell)
        n_graph[neighbour_cell].add(cell)

def merge_cells(merge_from, merge_to):
  merge_from_cell = cell_sets[merge_from]

  for pixel in merge_from_cell:
    pixcell_map[pixel] = merge_to

  del cell_sets[merge_from]
  del cell_means[merge_from]

  n_graph[merge_to] |= n_graph[merge_from]
  n_graph[merge_to].remove(merge_to)

  for n in n_graph[merge_from]:
    n_graph[n].remove(merge_from)

    if n != merge_to:
      n_graph[n].add(merge_to)

  del n_graph[merge_from]

  cell_sets[merge_to] |= merge_from_cell
  cell_means[merge_to] = mean_color(cell_sets[merge_to], pixlab_map)

# Go through the cells from largest to smallest. Keep replenishing the list while we can still merge.
last_time = time.time()
to_search = sorted(cell_sets.keys(), key=lambda x:len(cell_sets[x]), reverse=True)
full_list = True

while len(cell_sets) > N and to_search:
  if time.time() - last_time > 15:
    last_time = time.time()
    print "Close color merging... (%d cells remaining)" % len(cell_sets)

  while to_search:
    cellnum = to_search.pop()
    close_cells = []

    for neighbour_cellnum in n_graph[cellnum]:
      if d(cell_means[cellnum], cell_means[neighbour_cellnum]) < CLOSE_CELL_TOLERANCE:
        close_cells.append(neighbour_cellnum)

    if close_cells:
      for neighbour_cellnum in close_cells:
        merge_cells(neighbour_cellnum, cellnum)

        if neighbour_cellnum in to_search:
          to_search.remove(neighbour_cellnum)

      break

  if full_list == True:
    if to_search:
      full_list = False

  else:
    if not to_search:
      to_search = sorted(cell_sets.keys(), key=lambda x:len(cell_sets[x]), reverse=True)
      full_list = True

if OUTPUT_ALL:
  frame_im = Image.new("RGB", im.size)

  for cellnum in cell_sets:
    cell_color = cell_means[cellnum]

    for pixel in cell_sets[cellnum]:
      frame_im.putpixel(pixel, lab2rgb(cell_color))

  frame_im.save(OUTFILE_STEM + "2.png")
  print "Saved image %s2.png" % OUTFILE_STEM

print "Stage 4: Close color merging complete, %d cells" % len(cell_sets)

"""
Stage 5: N-merging - merge until <= N cells
     Want to merge either 1) small cells or 2) cells close in color
"""

# Weight score between neighbouring cells by 1) size of cell and 2) color difference
def score(cell1, cell2):
  return d(cell_means[cell1], cell_means[cell2]) * len(cell_sets[cell1])**.5

n_scores = {}

for cellnum in cell_sets:
  for n in n_graph[cellnum]:
    n_scores[(n, cellnum)] = score(n, cellnum)

last_time = time.time()

while len(cell_sets) > N * FIRST_PASS_N_RATIO:
  if time.time() - last_time > 15:
    last_time = time.time()
    print "N-merging... (%d cells remaining)" % len(cell_sets)

  merge_from, merge_to = min(n_scores, key=lambda x: n_scores[x])

  for n in n_graph[merge_from]:
    del n_scores[(merge_from, n)]
    del n_scores[(n, merge_from)]

  merge_cells(merge_from, merge_to)

  for n in n_graph[merge_to]:
    n_scores[(n, merge_to)] = score(n, merge_to)
    n_scores[(merge_to, n)] = score(merge_to, n)

if OUTPUT_ALL:
  frame_im = Image.new("RGB", im.size)

  for cellnum in cell_sets:
    cell_color = cell_means[cellnum]

    for pixel in cell_sets[cellnum]:
      frame_im.putpixel(pixel, lab2rgb(cell_color))

  frame_im.save(OUTFILE_STEM + "3.png")
  print "Saved image %s3.png" % OUTFILE_STEM

del n_graph, n_scores

print "Stage 5: N-merging complete, %d cells" % len(cell_sets)

"""
Stage 6: P merging - use k-means
"""

def form_clusters(centroids):
  clusters = defaultdict(set)

  for cellnum in cell_sets:
    # Add cell to closest centroid.
    scores = []

    for centroid in centroids:
      scores.append((d(centroid, cell_means[cellnum]), centroid))

    scores.sort()
    clusters[scores[0][1]].add(cellnum)

  return clusters

def calculate_centroid(cluster):
  L_sum = 0
  a_sum = 0
  b_sum = 0

  weighting = 0

  for cellnum in cluster:
    # Weight based on cell size
    color = cell_means[cellnum]
    cell_weight = len(cell_sets[cellnum])**.5

    L_sum += color[0]*cell_weight
    a_sum += color[1]*cell_weight
    b_sum += color[2]*cell_weight

    weighting += cell_weight

  return (L_sum/weighting, a_sum/weighting, b_sum/weighting)

def db_index(clusters):
  # Davies-Bouldin index
  scatter = {}

  for centroid, cluster in clusters.items():
    scatter_score = 0

    for cellnum in cluster:
      scatter_score += d(cell_means[cellnum], centroid) * len(cell_sets[cellnum])**.5

    scatter_score /= len(cluster)
    scatter[centroid] = scatter_score**2 # Mean squared distance

  index = 0

  for ci, cluster in clusters.items():
    dist_scores = []

    for cj in clusters:
      if ci != cj:
        dist_scores.append((scatter[ci] + scatter[cj])/d(ci, cj))

    index += max(dist_scores)

  return index

best_clusters = None
best_index = None

for i in X(K_MEANS_TRIALS):  
  centroids = {cell_means[cellnum] for cellnum in random.sample(cell_sets, P)}
  converged = False

  while not converged:
    clusters = form_clusters(centroids)
    new_centroids = {calculate_centroid(cluster) for cluster in clusters.values()}

    if centroids == new_centroids:
      converged = True

    centroids = new_centroids

  index = db_index(clusters)

  if best_index is None or index < best_index:
    best_index = index
    best_clusters = clusters

del cell_means
newpix_map = {}

for centroid, cluster in best_clusters.items():
  for cellnum in cluster:
    for pixel in cell_sets[cellnum]:
      newpix_map[pixel] = centroid

if OUTPUT_ALL:
  frame_im = Image.new("RGB", im.size)

  for pixel in newpix_map:
    frame_im.putpixel(pixel, lab2rgb(newpix_map[pixel]))

  frame_im.save(OUTFILE_STEM + "4.png")
  print "Saved image %s4.png" % OUTFILE_STEM

print "Stage 6: P-merging complete"

"""
Stage 7: Approximate Gaussian smoothing
     See http://blog.ivank.net/fastest-gaussian-blur.html
"""

# Hindsight tells me I should have used a class. I hate hindsight.
def vec_sum(vectors):
  assert(vectors and all(len(v) == len(vectors[0]) for v in vectors))
  return tuple(sum(x[i] for x in vectors) for i in X(len(vectors[0])))

def linear_blur(color_list):
  # Can be made faster with an accumulator
  output = []

  for i in X(len(color_list)):
    relevant_pixels = color_list[max(i-BLUR_RADIUS+1, 0):i+BLUR_RADIUS]
    pixsum = vec_sum(relevant_pixels)
    output.append(tuple(pixsum[i]/len(relevant_pixels) for i in X(3)))

  return output

def horizontal_blur():
  for row in X(height):
    colors = [blurpix_map[(i, row)] for i in X(width)]
    colors = linear_blur(colors)

    for i in X(width):
      blurpix_map[(i, row)] = colors[i]

def vertical_blur():
  for column in X(width):
    colors = [blurpix_map[(column, j)] for j in X(height)]
    colors = linear_blur(colors)

    for j in X(height):
      blurpix_map[(column, j)] = colors[j]

blurpix_map = {}

for i in X(width):
  for j in X(height):
    blurpix_map[(i, j)] = newpix_map[(i, j)]

for i in X(BLUR_RUNS):
  vertical_blur()
  horizontal_blur()

# Pixel : color of smoothed image
smoothpix_map = {}

for i in X(width):
  for j in X(height):
    pixel = (i, j)
    blur_color = blurpix_map[pixel]
    nearby_colors = {newpix_map[pixel]}

    for n in neighbours(pixel):
      nearby_colors.add(newpix_map[n])

    smoothpix_map[pixel] = min(nearby_colors, key=lambda x: d(x, blur_color))

del newpix_map, blurpix_map

if OUTPUT_ALL:
  frame_im = Image.new("RGB", im.size)

  for pixel in smoothpix_map:
    frame_im.putpixel(pixel, lab2rgb(smoothpix_map[pixel]))

  frame_im.save(OUTFILE_STEM + "5.png")
  print "Saved image %s5.png" % OUTFILE_STEM

print "Stage 7: Smoothing complete"

"""
Stage 8: Flood fill pass 2
     Code copy-and-paste because I'm lazy
"""

def flood_fill(start_pixel):
  to_search = {start_pixel}
  cell = set()
  searched = set()
  start_color = smoothpix_map[start_pixel]

  while to_search:
    pixel = to_search.pop()

    if start_color == smoothpix_map[pixel]:
      cell.add(pixel)
      unplaced_pixels.remove(pixel)

      for n in neighbours(pixel):
        if n in unplaced_pixels and n not in cell and n not in searched:
          to_search.add(n)

    else:
      searched.add(pixel)

  return cell

cell_sets = {}
pixcell_map = {}
unplaced_pixels = {(i, j) for i in X(width) for j in X(height)}

while unplaced_pixels:
  start_pixel = unplaced_pixels.pop()
  unplaced_pixels.add(start_pixel)
  cell = flood_fill(start_pixel)

  cellnum = len(cell_sets)
  cell_sets[cellnum] = cell

  for pixel in cell:
    pixcell_map[pixel] = cellnum

cell_colors = {}

for cellnum in cell_sets:
  cell_colors[cellnum] = smoothpix_map[next(iter(cell_sets[cellnum]))]

print "Stage 8: Flood fill pass 2 complete, %d cells" % len(cell_sets)

"""
Stage 9: Small cell removal pass 2
"""

def score(cell1, cell2):
  return d(cell_colors[cell1], cell_colors[cell2]) * len(cell_sets[cell1])**.5

def remove_small(cell_size):  
  small_cells = []

  for cellnum in cell_sets:
    if len(cell_sets[cellnum]) <= cell_size:
      small_cells.append(cellnum)

  for cellnum in small_cells:
    neighbour_cells = []

    for cell in cell_sets[cellnum]:
      for n in neighbours(cell):
        neighbour_reg = pixcell_map[n]

        if neighbour_reg != cellnum:
          neighbour_cells.append(neighbour_reg)

    closest_cell = max(neighbour_cells, key=neighbour_cells.count)

    for cell in cell_sets[cellnum]:
      pixcell_map[cell] = closest_cell

    if len(cell_sets[closest_cell]) <= cell_size:
      small_cells.remove(closest_cell)

    cell_color = cell_colors[closest_cell]

    for pixel in cell_sets[cellnum]:
      smoothpix_map[pixel] = cell_color

    cell_sets[closest_cell] |= cell_sets[cellnum]
    del cell_sets[cellnum]
    del cell_colors[cellnum]

for cell_size in X(1, SMALL_CELL_THRESHOLD):
  remove_small(cell_size)

if OUTPUT_ALL:
  frame_im = Image.new("RGB", im.size)

  for pixel in smoothpix_map:
    frame_im.putpixel(pixel, lab2rgb(smoothpix_map[pixel]))

  frame_im.save(OUTFILE_STEM + "6.png")
  print "Saved image %s6.png" % OUTFILE_STEM

print "Stage 9: Small cell removal pass 2 complete, %d cells" % len(cell_sets)

"""
Stage 10: N-merging pass 2
     Necessary as stage 7 might generate *more* cells
"""

def merge_cells(merge_from, merge_to):
  merge_from_cell = cell_sets[merge_from]

  for pixel in merge_from_cell:
    pixcell_map[pixel] = merge_to

  del cell_sets[merge_from]
  del cell_colors[merge_from]

  n_graph[merge_to] |= n_graph[merge_from]
  n_graph[merge_to].remove(merge_to)

  for n in n_graph[merge_from]:
    n_graph[n].remove(merge_from)

    if n != merge_to:
      n_graph[n].add(merge_to)

  del n_graph[merge_from]

  cell_color = cell_colors[merge_to]

  for pixel in merge_from_cell:
    smoothpix_map[pixel] = cell_color

  cell_sets[merge_to] |= merge_from_cell

n_graph = defaultdict(set)

for i in X(width):
  for j in X(height):
    pixel = (i, j)
    cell = pixcell_map[pixel]

    for n in neighbours(pixel):
      neighbour_cell = pixcell_map[n]

      if neighbour_cell != cell:
        n_graph[cell].add(neighbour_cell)
        n_graph[neighbour_cell].add(cell)

n_scores = {}

for cellnum in cell_sets:
  for n in n_graph[cellnum]:
    n_scores[(n, cellnum)] = score(n, cellnum)

last_time = time.time()

while len(cell_sets) > N:
  if time.time() - last_time > 15:
    last_time = time.time()
    print "N-merging (pass 2)... (%d cells remaining)" % len(cell_sets)

  merge_from, merge_to = min(n_scores, key=lambda x: n_scores[x])

  for n in n_graph[merge_from]:
    del n_scores[(merge_from, n)]
    del n_scores[(n, merge_from)]

  merge_cells(merge_from, merge_to)

  for n in n_graph[merge_to]:
    n_scores[(n, merge_to)] = score(n, merge_to)
    n_scores[(merge_to, n)] = score(merge_to, n)

print "Stage 10: N-merging pass 2 complete, %d cells" % len(cell_sets)

"""
Stage last: Output the image!
"""

test_im = Image.new("RGB", im.size)

for i in X(width):
  for j in X(height):
    test_im.putpixel((i, j), lab2rgb(smoothpix_map[(i, j)]))

if OUTPUT_ALL:
  test_im.save(OUTFILE_STEM + "7.png")
else:
  test_im.save(OUTFILE_STEM + ".png")

print "Done! (Time taken: {})".format(time.time() - total_time)

Temps de mise à jour! Cette mise à jour propose un algorithme de lissage simple pour rendre les images moins floues. Si j'effectue une nouvelle mise à jour, je devrai réorganiser une bonne partie de mon code, car cela devient compliqué et que je perds mon temps de parole.

J'ai également créé des couleurs de poids k-means en fonction de la taille des cellules, ce qui perd certains détails pour des paramètres plus restrictifs (par exemple le centre de la nébuleuse et la fourche de l'American Gothic), mais rend le choix de couleur global plus net et plus élégant. Fait intéressant, il perd tout le fond des sphères tracées par rayons pour P = 5.

Résumé de l'algorithme:

  1. Convertissez les pixels dans l' espace colorimétrique CIELAB : CIELAB rapproche mieux la vision humaine que le mode RVB. À l’origine, j’utilisais le HSL (teinte, saturation, luminosité), mais cela posait deux problèmes: la teinte blanc / gris / noir n’est pas définie et la teinte est mesurée en degrés, ce qui rend le k-mean difficile à utiliser.
  2. Diviser l'image en cellules de même couleur à l'aide du remplissage: choisissez un pixel qui ne se trouve pas dans une cellule et effectuez un remplissage à l'aide d'une tolérance spécifiée. Pour mesurer la distance entre deux couleurs, j'utilise la norme euclidienne standard. Des formules plus compliquées sont disponibles sur cet article du wiki .
  3. Fusionnez de petites cellules avec leurs voisins : le remplissage génère de nombreuses cellules de 1 ou 2 pixels - fusionnez des cellules inférieures à une taille spécifiée avec la cellule voisine avec les pixels les plus adjacents. Cela réduit considérablement le nombre de cellules, améliorant le temps d'exécution pour les étapes ultérieures.
  4. Fusionnez des régions de même couleur : parcourez les cellules par ordre décroissant de taille. Si une cellule voisine a une couleur moyenne inférieure à une certaine distance, fusionnez les cellules. Continuez à parcourir les cellules jusqu'à ce qu'il ne soit plus possible de les fusionner.
  5. Fusionner jusqu'à ce que nous ayons moins de 1,5 N cellules (fusion de N) : Fusionner les cellules ensemble, en utilisant une notation basée sur la taille de la cellule et la différence de couleur, jusqu'à obtenir au plus 1,5 N cellules. Nous laissons un peu de marge de manœuvre car nous fusionnerons à nouveau plus tard.
  6. Fusionner jusqu'à obtenir moins de P couleurs, à l'aide de k-moyennes (P-fusion) : utilisez un certain nombre de fois l' algorithme de classification de k-moyennes pour générer des regroupements de couleurs de cellules, pondérés en fonction de la taille de la cellule. Attribuez un score à chaque regroupement en fonction d’une variation de l’ indice de Davies-Bouldin et choisissez le meilleur regroupement à utiliser.
  7. Lissage gaussien approximatif : Utilisez plusieurs flous linéaires pour approximer le flou gaussien ( détails ici ). Ensuite, pour chaque pixel, sélectionnez la couleur de lui-même et de ses voisins dans l'image pré-floue la plus proche de sa couleur dans l'image floue. Cette partie peut être optimisée plus rapidement si nécessaire car je n'ai pas encore implémenté l'algorithme optimal.
  8. Effectuez une autre passe d'inondation pour définir les nouvelles régions : cela est nécessaire car l'étape précédente peut en réalité générer davantage de cellules.
  9. Faire une autre passe de fusion de petites cellules
  10. Faites une autre passe N-fusion : Cette fois, nous descendons à N cellules plutôt qu’à 1,5 N.

Le temps de traitement de chaque image dépend fortement de sa taille et de sa complexité, avec des durées allant de 20 secondes à 7 minutes pour les images testées.

Étant donné que l'algorithme utilise la randomisation (par exemple, la fusion, k-moyennes), vous pouvez obtenir des résultats différents sur différentes exécutions. Voici une comparaison de deux parcours pour l'image de l'ours, avec N = 50 et P = 10:

F M


Remarque: toutes les images ci-dessous sont des liens. La plupart de ces images sont directement issues de la première utilisation, mais si je n'aimais pas le résultat, je me permettais jusqu'à trois tentatives pour être juste.

N = 50, P = 10

L M une r k ré o w n g o l

N = 500, P = 30

F . . . : ( une une une une une une

Mais je suis assez paresseux quand il s'agit de peindre par couleurs, alors juste pour le plaisir ...

N = 20, P = 5

une une une une une une une une une une une une

De plus, il est amusant de voir ce qui se passe lorsque vous essayez de presser 1 million de couleurs dans N = 500, P = 30:

une

Voici une procédure pas à pas de l'algorithme pour l'image sous-marine avec N = 500 et P = 30, sous forme GIF animé:

une


J'ai également fait une galerie pour les versions précédentes de l'algorithme ici . Voici quelques-uns de mes favoris de la dernière version (à partir du moment où la nébuleuse avait plus d'étoiles et que l'ours avait l'air fourreur):

une une

Sp3000
la source
Si quelqu'un reçoit une exception lorsque le programme tente de décompresser les pixels, il semblerait que im = im.convert("RGB")certaines images soient nécessaires. Je mettrai cela après avoir restructuré le code un peu.
Sp3000
15

Python 2 avec PIL

Également une solution Python et probablement un travail en cours:

from PIL import Image, ImageFilter
import random

def draw(file_name, P, N, M=3):
    img = Image.open(file_name, 'r')
    pixels = img.load()
    size_x, size_y = img.size

    def dist(c1, c2):
        return (c1[0]-c2[0])**2+(c1[1]-c2[1])**2+(c1[2]-c2[2])**2

    def mean(colours):
        n = len(colours)
        r = sum(c[0] for c in colours)//n
        g = sum(c[1] for c in colours)//n
        b = sum(c[2] for c in colours)//n
        return (r,g,b)

    def colourize(colour, palette):
        return min(palette, key=lambda c: dist(c, colour))

    def cluster(colours, k, max_n=10000, max_i=10):
        colours = random.sample(colours, max_n)
        centroids = random.sample(colours, k)
        i = 0
        old_centroids = None
        while not(i>max_i or centroids==old_centroids):
            old_centroids = centroids
            i += 1
            labels = [colourize(c, centroids) for c in colours]
            centroids = [mean([c for c,l in zip(colours, labels)
                               if l is cen]) for cen in centroids]
        return centroids

    all_coords = [(x,y) for x in xrange(size_x) for y in xrange(size_y)]
    all_colours = [pixels[x,y] for x,y in all_coords]
    palette = cluster(all_colours, P)
    print 'clustered'

    for x,y in all_coords:
        pixels[x,y] = colourize(pixels[x,y], palette)
    print 'colourized'

    median_filter = ImageFilter.MedianFilter(size=M)
    img = img.filter(median_filter)
    pixels = img.load()
    for x,y in all_coords:
        pixels[x,y] = colourize(pixels[x,y], palette)
    print 'median filtered'

    def neighbours(edge, outer, colour=None):
        return set((x+a,y+b) for x,y in edge
                   for a,b in ((1,0), (-1,0), (0,1), (0,-1))
                   if (x+a,y+b) in outer
                   and (colour==None or pixels[(x+a,y+b)]==colour))

    def cell(centre, rest):
        colour = pixels[centre]
        edge = set([centre])
        region = set()
        while edge:
            region |= edge
            rest = rest-edge
            edge = set(n for n in neighbours(edge, rest, colour))
        return region, rest

    print 'start segmentation:'
    rest = set(all_coords)
    cells = []
    while rest:
        centre = random.sample(rest, 1)[0]
        region, rest = cell(centre, rest-set(centre))
        cells += [region]
        print '%d pixels remaining'%len(rest)
    cells = sorted(cells, key=len, reverse=True)
    print 'segmented (%d segments)'%len(cells)

    print 'start merging:'
    while len(cells)>N:
        small_cell = cells.pop()
        n = neighbours(small_cell, set(all_coords)-small_cell)
        for big_cell in cells:
            if big_cell & n:
                big_cell |= small_cell
                break
        print '%d segments remaining'%len(cells)
    print 'merged'

    for cell in cells:
        colour = colourize(mean([pixels[x,y] for x,y in cell]), palette)
        for x,y in cell:
            pixels[x,y] = colour
    print 'colorized again'

    img.save('P%d N%d '%(P,N)+file_name)
    print 'saved'

draw('a.png', 11, 500, 1)

L'algorithme suit une approche différente de celle de SP3000, en commençant par les couleurs:

  • Recherchez une palette de couleurs de P couleurs par regroupement k-signifie et peignez l'image dans cette palette réduite.

  • Appliquez un léger filtre médian pour éliminer le bruit.

  • Faites une liste de toutes les cellules monochromatiques et triez-les par taille.

  • Fusionnez les plus petites cellules avec leur plus grand voisin respectif jusqu'à ce qu'il ne reste plus que N cellules.

Il y a encore beaucoup à faire, à la fois en termes de rapidité et de qualité des résultats. En particulier, l'étape de fusion de cellules peut prendre plusieurs minutes et donner des résultats loin d'être optimaux.


P = 5, N = 45

P = 5, N = 45P = 5, N = 45

P = 10, N = 50

P = 10, N = 50P = 10, N = 50P = 10, N = 50P = 10, N = 50

P = 4, N = 250

P = 4, N = 250P = 4, N = 250

P = 11, N = 500

P = 11, N = 500P = 11, N = 500

Emil
la source
J'ai d'abord essayé d'utiliser à peu près la même approche (essayé de le faire en Javascript sur un canevs) mais finalement j'ai abandonné parce que cela prenait trop de temps, donc c'est vraiment agréable de voir à quoi cela pourrait ressembler, beau travail!
mardi
Très bon travail. J'ai adoré l'ours avec 20 cellules.
DavidC
15

Mathematica

Pour le moment, cela prend le nombre de couleurs et le rayon gaussien à utiliser dans le filtre gaussien. Plus le rayon est grand, plus le flou et la fusion des couleurs sont importants.

Parce qu'il ne permet pas de saisir le nombre de cellules, il ne répond à aucune des exigences de base du défi.

La sortie inclut le nombre de cellules pour chaque couleur ainsi que le nombre total de cellules.

quantImg[img_,nColours_,gaussR_]:=ColorQuantize[GaussianFilter[img,gaussR],nColours,
Dithering-> False]

colours[qImg_]:=Union[Flatten[ImageData[qImg],1]]

showColors[image_,nColors_,gaussR_]:=
   Module[{qImg,colors,ca,nCells},
   qImg=quantImg[image,nColors,gaussR];
   colors=colours[qImg];
   ca=ConstantArray[0,Reverse@ImageDimensions[image]];
   nCells[qImgg_,color_]:=
   Module[{r},
   r=ReplacePart[ca,Position[ImageData@qImg,color]/.{a_,b_}:> ({a,b}->1)];
   (*ArrayPlot[r,ColorRules->{1\[Rule]RGBColor[color],0\[Rule]White}];*)
   m=MorphologicalComponents[r];
   {RGBColor@color,Max[Union@Flatten[m,1]]}];
   s=nCells[qImg,#]&/@colors;
   Grid[{
    {Row[{s}]}, {Row[{"cells:\t\t",Tr[s[[All,2]]]}]},{Row[{"colors:\t\t",nColors}]},
    {Row[{"Gauss. Radius: ", gaussR}]}},Alignment->Left]]

Mise à jour

quantImage2permet de spécifier le nombre de cellules souhaité en entrée. Il détermine le meilleur rayon gaussien en effectuant une boucle dans des scénarios avec des rayons plus importants jusqu'à ce qu'une correspondance étroite soit trouvée.

quantImage2 sorties (image, cellules demandées, cellules utilisées, erreur, rayon gaussien utilisé).

C'est cependant très lent. Pour gagner du temps, vous pouvez commencer par un rayon initial dont la valeur par défaut est 0.

gaussianRadius[img_,nCol_,nCells_,initialRadius_:0]:=
Module[{radius=initialRadius,nc=10^6,results={},r},
While[nc>nCells,(nc=numberOfCells[ape,nColors,radius]);
results=AppendTo[results,{nColors,radius,nc}];radius++];
r=results[[{-2,-1}]];
Nearest[r[[All,3]],200][[1]];
Cases[r,{_,_,Nearest[r[[All,3]],nCells][[1]]}][[1,2]]
]

quantImg2[img_,nColours_,nCells1_,initialRadius_:0]:={ColorQuantize[GaussianFilter[img,
g=gaussianRadius[img,nColours,nCells1,initialRadius]],nColours,Dithering->False],
nCells1,nn=numberOfCells[img,nColours,g],N[(nn-nCells1)/nCells1],g}

Exemple pour lequel nous spécifions le nombre de cellules souhaité dans la sortie.

Exemple demandant 90 cellules avec 25 couleurs. La solution renvoie 88 cellules, erreur de 2%. La fonction a choisi le rayon gaussien de 55. (Beaucoup de distorsion).

Ape X


Exemples pour lesquels l'entrée comprend le rayon gaussien, mais pas le nombre de cellules.

25 couleurs, rayon gaussien de 5 pixels

nColors = 25;
gR = 5;
quantImg[balls, nColors, gR]

des balles


Trois couleurs, rayon de 17 pixels

nColors=3;gaussianRadius=17;
showColors[wave,nColors,gaussianRadius]
quantImg[wave,nColors,gaussianRadius]

vague 3 17


Vingt couleurs, rayon de 17 pixels

Nous avons augmenté le nombre de couleurs mais pas la mise au point. Notez l'augmentation du nombre de cellules.

vague 2


Six couleurs, rayon de 4 pixels

nColors=6;gaussianRadius=4;
showColors[wave,nColors,gaussianRadius]
quantImg[wave,nColors,gaussianRadius]

wave3


nColors = 6; gaussianRadius = 17;
showColors[ape, nColors, gaussianRadius]
quantImg[ape, nColors, gaussianRadius]

singe 1


nColors = 6; gaussianRadius = 3;
showColors[ape, nColors, gaussianRadius]
quantImg[ape, nColors, gaussianRadius]

singe 2


Nuit étoilée

Avec seulement 6 couleurs et 60 cellules. Les couleurs utilisées dans les showColorsrevendications ne correspondent pas aux couleurs . (Le jaune n'apparaît pas parmi les 5 couleurs mais il est utilisé dans le dessin.) Je verrai si je peux comprendre cela.

nuit étoilée 1

DavidC
la source
C'est absolument magnifique et fonctionne vraiment bien pour les paramètres restrictifs. Une chance de transformer le nombre de cellules en paramètre? (Je suppose que vous pouvez toujours trouver une estimation du rayon à partir du nombre de cellules, l'appliquer, puis fusionner de petites cellules jusqu'à ce que vous soyez au-dessous de la limite.)
Martin Ender
Il est possible de créer un tableau de showColors, en parcourant une plage de nombres de couleurs et de rayons et en choisissant la combinaison la plus proche du nombre de cellules souhaité. Pas sûr si j'ai le gaz pour le faire pour le moment. Peut-être plus tard.
DavidC
Bien sûr, laissez-moi savoir si vous le faites. (J'aimerais aussi voir d'autres résultats pour les autres images. :))
Martin Ender
2
C'est très bien. Merci d'avoir respecté les règles. ;)
Martin Ender
1
J'aime les sphères! Ils sont gentils et rond
Sp3000
9

Python 2 avec PIL

C'est encore un peu un travail en cours. En outre, le code ci-dessous est un horrible fouillis de spaghettis et ne doit pas être utilisé comme une inspiration. :)

from PIL import Image, ImageFilter
from math import sqrt
from copy import copy
from random import shuffle, choice, seed

IN_FILE = "input.png"
OUT_FILE = "output.png"

LOGGING = True
GRAPHICAL_LOGGING = False
LOG_FILE_PREFIX = "out"
LOG_FILE_SUFFIX = ".png"
LOG_ROUND_INTERVAL = 150
LOG_FLIP_INTERVAL = 40000

N = 500
P = 30
BLUR_RADIUS = 3
FILAMENT_ROUND_INTERVAL = 5
seed(0) # Random seed

print("Opening input file...")

image = Image.open(IN_FILE).filter(ImageFilter.GaussianBlur(BLUR_RADIUS))
pixels = {}
width, height = image.size

for i in range(width):
    for j in range(height):
        pixels[(i, j)] = image.getpixel((i, j))

def dist_rgb((a,b,c), (d,e,f)):
    return (a-d)**2 + (b-e)**2 + (c-f)**2

def nbors((x,y)):
    if 0 < x:
        if 0 < y:
            yield (x-1,y-1)
        if y < height-1:
            yield (x-1,y+1)
    if x < width - 1:
        if 0 < y:
            yield (x+1,y-1)
        if y < height-1:
            yield (x+1,y+1)

def full_circ((x,y)):
    return ((x+1,y), (x+1,y+1), (x,y+1), (x-1,y+1), (x-1,y), (x-1,y-1), (x,y-1), (x+1,y-1))

class Region:

    def __init__(self):
        self.points = set()
        self.size = 0
        self.sum = (0,0,0)

    def flip_point(self, point):
        sum_r, sum_g, sum_b = self.sum
        r, g, b = pixels[point]
        if point in self.points:
            self.sum = (sum_r - r, sum_g - g, sum_b - b)
            self.size -= 1
            self.points.remove(point)
        else:
            self.sum = (sum_r + r, sum_g + g, sum_b + b)
            self.size += 1
            self.points.add(point)

    def mean_with(self, color):
        if color is None:
            s = float(self.size)
            r, g, b = self.sum
        else:
            s = float(self.size + 1)
            r, g, b = map(lambda a,b: a+b, self.sum, color)
        return (r/s, g/s, b/s)

print("Initializing regions...")

aspect_ratio = width / float(height)
a = int(sqrt(N)*aspect_ratio)
b = int(sqrt(N)/aspect_ratio)

num_components = a*b
owners = {}
regions = [Region() for i in range(P)]
borders = set()

nodes = [(i,j) for i in range(a) for j in range(b)]
shuffle(nodes)
node_values = {(i,j):None for i in range(a) for j in range(b)}

for i in range(P):
    node_values[nodes[i]] = regions[i]

for (i,j) in nodes[P:]:
    forbiddens = set()
    for node in (i,j-1), (i,j+1), (i-1,j), (i+1,j):
        if node in node_values and node_values[node] is not None:
            forbiddens.add(node_values[node])
    node_values[(i,j)] = choice(list(set(regions) - forbiddens))

for (i,j) in nodes:
    for x in range((width*i)/a, (width*(i+1))/a):
        for y in range((height*j)/b, (height*(j+1))/b):
            owner = node_values[(i,j)]
            owner.flip_point((x,y))
            owners[(x,y)] = owner

def recalc_borders(point = None):
    global borders
    if point is None:
        borders = set()
        for i in range(width):
            for j in range(height):
                if (i,j) not in borders:
                    owner = owner_of((i,j))
                    for pt in nbors((i,j)):
                        if owner_of(pt) != owner:
                            borders.add((i,j))
                            borders.add(pt)
                            break
    else:
        for pt in nbors(point):
            owner = owner_of(pt)
            for pt2 in nbors(pt):
                if owner_of(pt2) != owner:
                    borders.add(pt)
                    break
            else:
                borders.discard(pt)

def owner_of(point):
    if 0 <= point[0] < width and 0 <= point[1] < height:
        return owners[point]
    else:
        return None

# Status codes for analysis
SINGLETON = 0
FILAMENT = 1
SWAPPABLE = 2
NOT_SWAPPABLE = 3

def analyze_nbors(point):
    owner = owner_of(point)
    circ = a,b,c,d,e,f,g,h = full_circ(point)
    oa,ob,oc,od,oe,of,og,oh = map(owner_of, circ)
    nbor_owners = set([oa,oc,oe,og])
    if owner not in nbor_owners:
        return SINGLETON, owner, nbor_owners - set([None])
    if oc != oe == owner == oa != og != oc:
        return FILAMENT, owner, set([og, oc]) - set([None])
    if oe != oc == owner == og != oa != oe:
        return FILAMENT, owner, set([oe, oa]) - set([None])
    last_owner = oa
    flips = {last_owner:0}
    for (corner, side, corner_owner, side_owner) in (b,c,ob,oc), (d,e,od,oe), (f,g,of,og), (h,a,oh,oa):
        if side_owner not in flips:
            flips[side_owner] = 0
        if side_owner != corner_owner or side_owner != last_owner:
            flips[side_owner] += 1
            flips[last_owner] += 1
        last_owner = side_owner
    candidates = set(own for own in flips if flips[own] == 2 and own is not None)
    if owner in candidates:
        return SWAPPABLE, owner, candidates - set([owner])
    return NOT_SWAPPABLE, None, None

print("Calculating borders...")

recalc_borders()

print("Deforming regions...")

def assign_colors():
    used_colors = {}
    for region in regions:
        r, g, b = region.mean_with(None)
        r, g, b = int(round(r)), int(round(g)), int(round(b))
        if (r,g,b) in used_colors:
            for color in sorted([(r2, g2, b2) for r2 in range(256) for g2 in range(256) for b2 in range(256)], key=lambda color: dist_rgb(color, (r,g,b))):
                if color not in used_colors:
                    used_colors[color] = region.points
                    break
        else:
            used_colors[(r,g,b)] = region.points
    return used_colors

def make_image(colors):
    img = Image.new("RGB", image.size)
    for color in colors:
        for point in colors[color]:
            img.putpixel(point, color)
    return img

# Round status labels
FULL_ROUND = 0
NEIGHBOR_ROUND = 1
FILAMENT_ROUND = 2

max_filament = None
next_search = set()
rounds = 0
points_flipped = 0
singletons = 0
filaments = 0
flip_milestone = 0
logs = 0

while True:
    if LOGGING and (rounds % LOG_ROUND_INTERVAL == 0 or points_flipped >= flip_milestone):
        print("Round %d of deformation:\n %d edit(s) so far, of which %d singleton removal(s) and %d filament cut(s)."%(rounds, points_flipped, singletons, filaments))
        while points_flipped >= flip_milestone: flip_milestone += LOG_FLIP_INTERVAL
        if GRAPHICAL_LOGGING:
            make_image(assign_colors()).save(LOG_FILE_PREFIX + str(logs) + LOG_FILE_SUFFIX)
            logs += 1
    if max_filament is None or (round_status == NEIGHBOR_ROUND and rounds%FILAMENT_ROUND_INTERVAL != 0):
        search_space, round_status = (next_search & borders, NEIGHBOR_ROUND) if next_search else (copy(borders), FULL_ROUND)
        next_search = set()
        max_filament = None
    else:
        round_status = FILAMENT_ROUND
        search_space = set([max_filament[0]]) & borders
    search_space = list(search_space)
    shuffle(search_space)
    for point in search_space:
        status, owner, takers = analyze_nbors(point)
        if (status == FILAMENT and num_components < N) or status in (SINGLETON, SWAPPABLE):
            color = pixels[point]
            takers_list = list(takers)
            shuffle(takers_list)
            for taker in takers_list:
                dist = dist_rgb(color, owner.mean_with(None)) - dist_rgb(color, taker.mean_with(color))
                if dist > 0:
                    if status != FILAMENT or round_status == FILAMENT_ROUND:
                        found = True
                        owner.flip_point(point)
                        taker.flip_point(point)
                        owners[point] = taker
                        recalc_borders(point)
                        next_search.add(point)
                        for nbor in full_circ(point):
                            next_search.add(nbor)
                        points_flipped += 1
                    if status == FILAMENT:
                        if round_status == FILAMENT_ROUND:
                            num_components += 1
                            filaments += 1
                        elif max_filament is None or max_filament[1] < dist:
                            max_filament = (point, dist)
                    if status == SINGLETON:
                        num_components -= 1
                        singletons += 1
                    break
    rounds += 1
    if round_status == FILAMENT_ROUND:
        max_filament = None
    if round_status == FULL_ROUND and max_filament is None and not next_search:
        break

print("Deformation completed after %d rounds:\n %d edit(s), of which %d singleton removal(s) and %d filament cut(s)."%(rounds, points_flipped, singletons, filaments))

print("Assigning colors...")

used_colors = assign_colors()

print("Producing output...")

make_image(used_colors).save(OUT_FILE)

print("Done!")

Comment ça marche

Le programme divise la toile en Prégions, chacune d'entre elles étant composée d'un certain nombre de cellules sans trous. Initialement, la toile est simplement divisée en carrés approximatifs, attribués de manière aléatoire aux régions. Ensuite, ces régions sont "déformées" dans un processus itératif, où un pixel donné peut changer de région si

  1. le changement diminuerait la distance RVB du pixel par rapport à la couleur moyenne de la région qui le contient, et
  2. il ne brise pas, ne fusionne pas les cellules et n'introduit pas de trous dans celles-ci.

Cette dernière condition peut être appliquée localement, le processus est donc un peu comme un automate cellulaire. De cette façon, nous n’avons pas à faire de cheminement ou autre, ce qui accélère considérablement le processus. Cependant, comme les cellules ne peuvent pas être brisées, certaines d’entre elles se transforment en longs "filaments" qui bordent d’autres cellules et inhibent leur croissance. Pour résoudre ce problème, il existe un processus appelé "coupe de filament", qui casse parfois une cellule en forme de filament en deux, s'il y en a moins que les Ncellules à ce moment-là. Les cellules peuvent également disparaître si leur taille est égale à 1, ce qui laisse de la place pour les coupes de filaments.

Le processus se termine lorsqu'aucun pixel n'est incité à changer de région. Après cela, chaque région est simplement colorée par sa couleur moyenne. Habituellement, il restera quelques filaments dans la sortie, comme on peut le voir dans les exemples ci-dessous, en particulier dans la nébuleuse.

P = 30, N = 500

Mona Lisa Babouin Boules colorées Nébuleuse

Plus de photos plus tard.

Certaines propriétés intéressantes de mon programme sont qu’il est probabiliste, de sorte que les résultats peuvent varier d’une exécution à l’autre, à moins que vous utilisiez la même valeur de départ pseudo-aléatoire bien sûr. Le caractère aléatoire n’est pas essentiel, cependant, je voulais simplement éviter tout artefact accidentel pouvant résulter de la façon dont Python parcourt un ensemble de coordonnées ou quelque chose de similaire. Le programme a tendance à utiliser toutes les Pcouleurs et presque toutes les Ncellules, et les cellules ne contiennent jamais de trous par leur conception. En outre, le processus de déformation est assez lent. Les boules colorées ont pris presque 15 minutes pour produire sur ma machine. Sur le dessus, si vous allumez leGRAPHICAL_LOGGINGEn option, vous obtiendrez une série d'images intéressantes du processus de déformation. J'ai transformé celles de Mona Lisa en animations GIF (réduites de 50% pour réduire la taille du fichier). Si vous regardez attentivement son visage et ses cheveux, vous pouvez voir le processus de coupe de filament en action.

entrez la description de l'image ici

Zgarb
la source
Wow, ces résultats sont vraiment beaux (bien que ce ne soit pas tout à fait semblable aux chiffres, mais quand même très agréable :))
Martin Ender