Maluj według numerów


42

Otrzymujesz prawdziwy kolorowy obraz. Twoim zadaniem jest wygenerowanie wersji tego obrazu, który wygląda tak, jakby został namalowany farbą po liczbach (aktywność dzieci, a nie nonogramy). Wraz z obrazem podane są dwa parametry: P , maksymalny rozmiar palety kolorów (tj. Maksymalna liczba różnych kolorów do użycia) i N , maksymalna liczba komórek do użycia. Twój algorytm nie musi używać wszystkich kolorów P i komórek N , ale nie może używać więcej. Obraz wyjściowy powinien mieć takie same wymiary jak wejście.

Komórka jest zdefiniowana jako zwarty obszar pikseli, które wszystkie mają ten sam kolor. Piksele dotykające tylko w rogu nie są uważane za ciągłe. Komórki mogą mieć dziury.

Krótko mówiąc, przybliżasz obraz wejściowy tylko za pomocą N płaskich cieniowanych / jednolitych obszarów i P różnych kolorów.

Aby wizualizować parametry, oto bardzo prosty przykład (brak konkretnego obrazu wejściowego; pokazanie moich szalonych umiejętności malowania). Poniższy obraz ma P = 6 i N = 11 :

wprowadź opis zdjęcia tutaj

Oto kilka zdjęć, na których można przetestować algorytm (głównie nasi podejrzani). Kliknij zdjęcia, aby zobaczyć większe wersje.

Wielka Fala Rafa koralowa Tęcza Gwieździsta noc Rzeka Brązowy niedźwiedź Wodospad Mandryl Mgławica Kraba amerykański gotyk Mona Lisa Krzyk

Podaj garść wyników dla różnych parametrów. Jeśli chcesz pokazać dużą liczbę wyników, możesz utworzyć galerię na imgur.com , aby zachować rozsądny rozmiar odpowiedzi. Ewentualnie umieść miniatury w swoim poście i umieść w nich linki do większych zdjęć, tak jak zrobiłem powyżej. Ponadto, jeśli znajdziesz coś fajnego, skorzystaj z innych zdjęć testowych.

Zakładam, że parametry około N ≥ 500 , P ~ 30 byłyby podobne do rzeczywistych szablonów malowania według numerów.

To konkurs popularności, więc wygrywa odpowiedź z największą liczbą głosów netto. Zachęca się wyborców do oceniania odpowiedzi według

  • jak dobrze przybliżone są oryginalne obrazy.
  • jak dobrze algorytm działa na różnych rodzajach obrazów (obrazy są zazwyczaj łatwiejsze niż zdjęcia).
  • jak dobrze algorytm działa z bardzo restrykcyjnymi parametrami.
  • jak wyglądają organiczne / gładkie kształty komórek.

Użyję następującego skryptu Mathematica, aby sprawdzić poprawność wyników:

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 był na tyle miły, że napisał weryfikator w Pythonie 2 za pomocą PIL, który znajduje się w tym pastebinie .


2
Nie jest to najbardziej wydajna rzecz, ale oto weryfikator PIL Python 2 .
Sp3000,

Cóż za cudowne pytanie, ale miałem nadzieję, że zobaczymy również odpowiednią wersję „maluj według numerów”. To znaczy z liczbami na miejscu, więc mógłbym użyć odpowiedzi :)

@Lembik Początkowo chciałem to uwzględnić, ale czułem, że odwróciło to uwagę od interesującej części pytania. Jednak nie powinno być zbyt trudne pobranie wyników jednego z zgłoszeń i przekonwertowanie ich na szablon.
Martin Ender,

To fascynujący post. Czy ktoś poszedł o krok dalej, dodając liczby kolorów jak rzeczywistą farbę według numeru?
B. Blair,

Odpowiedzi:


39

Python 2 z PIL ( galeria )

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)

Czas aktualizacji! Ta aktualizacja zawiera prosty algorytm wygładzania, aby obrazy były mniej rozmyte. Jeśli zaktualizuję ponownie, będę musiał przerobić sporą część mojego kodu, ponieważ robi się niechlujny i mam problem z 2 i więcej.

Stworzyłem także kolory wagowe k-średnich w oparciu o rozmiary komórek, które tracą detale dla bardziej restrykcyjnych parametrów (np. Środek mgławicy i widły American Gothic), ale sprawiają, że ogólny wybór kolorów jest ostrzejszy i ładniejszy. Co ciekawe, traci on całe tło dla sfer raytraced dla P = 5.

Podsumowanie algorytmu:

  1. Konwertuj piksele na przestrzeń kolorów CIELAB: CIELAB przybliża widzenie człowieka lepiej niż RGB. Pierwotnie użyłem HSL (odcień, nasycenie, jasność), ale miały to dwa problemy - odcień bieli / szarości / czerni jest niezdefiniowany, a odcień mierzony jest w stopniach, które się zawijają, co utrudnia użycie k-średnich.
  2. Podziel obraz na komórki o podobnym kolorze za pomocą wypełnienia zalewowego: wybierz piksel spoza komórki i wykonaj wypełnienie zalewowe z określoną tolerancją. Aby zmierzyć odległość między dwoma kolorami, używam standardowej normy euklidesowej. Bardziej skomplikowane formuły są dostępne w tym artykule wiki .
  3. Scal małe komórki z sąsiadami : wypełnienie zalewowe generuje wiele komórek o wielkości 1 lub 2 pikseli - łączy komórki o rozmiarze mniejszym niż określony z sąsiednią komórką z najbardziej sąsiadującymi pikselami. To znacznie zmniejsza liczbę komórek, skracając czas działania na późniejszych etapach.
  4. Scal razem obszary o podobnym kolorze : przejdź przez komórki w kolejności malejącej wielkości. Jeśli jakakolwiek sąsiednia komórka ma średni kolor mniejszy niż pewna odległość, połącz komórki. Kontynuuj przeglądanie komórek, aż nie będzie można już połączyć.
  5. Scal, dopóki nie będziemy mieć mniej niż 1,5 N komórek (łączenie N) : Scal komórki razem, używając oceny opartej na wielkości komórki i różnicy kolorów, aż do uzyskania maksymalnie 1,5 N komórek. Pozwalamy na odrobinę swobody, ponieważ połączymy się ponownie później.
  6. Scal, aż uzyskamy mniej niż P kolorów, używając k-średnich (P-scalanie) : Użyj algorytmu grupowania k-średnich kilka określonych razy, aby wygenerować grupowanie kolorów komórek, ważąc na podstawie wielkości komórki. Oceniaj poszczególne klastry na podstawie odmiany indeksu Daviesa-Bouldina i wybierz najlepsze klastry do użycia.
  7. Przybliżone wygładzenie gaussowskie : użyj kilku rozmycia liniowego, aby przybliżyć rozmycie gaussowskie ( szczegóły tutaj ). Następnie dla każdego piksela wybierz kolor z siebie i jego sąsiadów na wcześniej zamazanym obrazie najbliższym jego kolorowi na zamazanym obrazie. Ta część może być zoptymalizowana bardziej czasowo, jeśli to konieczne, ponieważ muszę jeszcze wdrożyć optymalny algorytm.
  8. Wykonaj kolejną przepełnienie zalewowe, aby opracować nowe regiony : Jest to konieczne, ponieważ poprzedni krok może faktycznie wygenerować więcej komórek.
  9. Wykonaj kolejną przepustkę scalania małych komórek
  10. Wykonaj kolejne przejście łączące N : Tym razem schodzimy do komórek N zamiast 1,5 N.

Czas przetwarzania każdego obrazu w dużej mierze zależy od jego wielkości i złożoności, przy czasach testowych od 20 sekund do 7 minut.

Ponieważ algorytm wykorzystuje randomizację (np. Scalanie, k-średnie), możesz uzyskać różne wyniki dla różnych przebiegów. Oto porównanie dwóch przebiegów obrazu niedźwiedzia, przy N = 50 i P = 10:

fa M.


Uwaga: Wszystkie poniższe zdjęcia są linkami. Większość z tych obrazów pochodzi od pierwszego uruchomienia, ale jeśli nie podobało mi się wyjście, pozwoliłem sobie na trzy próby uczciwości.

N = 50, P = 10

L. M. za r k re o w n sol o l

N = 500, P = 30

fa . . . : ( za za za za za za

Ale jestem dość leniwy, jeśli chodzi o malowanie kolorami, więc dla zabawy ...

N = 20, P = 5

za za za za za za za za za za za za

Dodatkowo zabawnie jest zobaczyć, co się stanie, gdy spróbujesz wycisnąć 1 milion kolorów na N = 500, P = 30:

za

Oto krok po kroku algorytm dla obrazu podwodnego o N = 500 i P = 30, w animowanej formie GIF:

za


Zrobiłem też galerię dla poprzednich wersji algorytmu tutaj . Oto niektóre z moich ulubionych z ostatniej wersji (od kiedy mgławica miała więcej gwiazd, a niedźwiedź wyglądał na bardziej puszystego):

za za


Jeśli ktoś dostaje wyjątek, gdy program próbuje rozpakować piksele, wygląda na to, że im = im.convert("RGB")jest potrzebny do niektórych zdjęć. Włożę to po tym, jak trochę zrestrukturyzuję kod.
Sp3000,

15

Python 2 z PIL

Również rozwiązanie w języku Python i prawdopodobnie bardzo dużo pracy w toku:

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)

Algorytm stosuje inne podejście niż SP3000, zaczynając od kolorów najpierw:

  • Znajdź paletę kolorów P za pomocą k-oznacza grupowania i pomaluj obraz w tej zmniejszonej palecie.

  • Zastosuj lekki filtr środkowy, aby pozbyć się szumu.

  • Zrób listę wszystkich komórek monochromatycznych i posortuj ją według rozmiaru.

  • Scal najmniejsze komórki z ich odpowiednimi największymi sąsiadami, aż pozostanie tylko N komórek.

Jest sporo miejsca na ulepszenia, zarówno pod względem szybkości, jak i jakości wyników. Zwłaszcza etap łączenia komórek może potrwać do wielu minut i daje dalekie od optymalnych rezultatów.


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


Najpierw próbowałem zastosować to samo podejście (próbowałem to zrobić w JavaScript na canvs), ale ewidentnie się poddałem, ponieważ trwało to zbyt długo, więc naprawdę fajnie jest zobaczyć, jak mogłoby to wyglądać, dobra robota!
flawr

Bardzo dobra robota. Kochałem misia z 20 komórkami.
DavidC,

15

Matematyka

W tej chwili bierze to liczbę kolorów i promień Gaussa do zastosowania w filtrze Gaussa. Im większy promień, tym większe rozmycie i scalenie kolorów.

Ponieważ nie pozwala na wprowadzenie liczby komórek, nie spełnia jednego z podstawowych wymagań wyzwania.

Dane wyjściowe obejmują liczbę komórek dla każdego koloru, a także całkowitą liczbę komórek.

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

Aktualizacja

quantImage2pozwala określić żądaną liczbę komórek jako dane wejściowe. Określa najlepszy promień gaussowski, przechodząc przez scenariusze z większymi promieniami, aż do znalezienia ścisłego dopasowania.

quantImage2 dane wyjściowe (obraz, wymagane komórki, wykorzystane komórki, błąd, zastosowany promień gaussa).

Jest jednak bardzo powolny. Aby zaoszczędzić czas, możesz zacząć od początkowego promienia, którego domyślną wartością jest 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}

Przykład, dla którego określamy liczbę komórek pożądanych w danych wyjściowych.

Przykład z prośbą o 90 komórek w 25 kolorach. Rozwiązanie zwraca 88 komórek, błąd 2%. Funkcja wybrała promień Gaussa na 55. (Dużo zniekształceń).

Ape X


Przykłady, dla których dane wejściowe obejmują promień Gaussa, ale nie liczbę komórek.

25 kolorów, promień Gaussa 5 pikseli

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

kulki


Trzy kolory, promień 17 pikseli

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

fala 3 17


Dwadzieścia kolorów, promień 17 pikseli

Zwiększyliśmy liczbę kolorów, ale nie skupiliśmy się. Zwróć uwagę na wzrost liczby komórek.

fala 2


Sześć kolorów, promień 4 pikseli

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

fala3


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

małpa 1


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

małpa 2


Gwieździsta noc

Tylko 6 kolorów i 60 komórek. Występuje niedopasowanie kolorów w kolorach, których showColorsużywa roszczenia. (Żółty nie pojawia się wśród 5 kolorów, ale jest używany na rysunku.) Zobaczę, czy mogę to rozgryźć.

gwiaździsta noc 1


Jest to absolutnie wspaniałe i działa naprawdę dobrze w przypadku restrykcyjnych parametrów. Czy jest jakaś szansa na przekształcenie liczby komórek w parametr? (Przypuszczam, że zawsze można znaleźć oszacowanie promienia na podstawie liczby komórek, zastosować je, a następnie scalić małe komórki, dopóki nie osiągniesz limitu.)
Martin Ender,

Możliwe jest utworzenie Tabeli z showColorszapętleniem zakresu kolorów i promieni oraz wybranie kombinacji najbliższej pożądanej liczbie komórek. Nie jestem pewien, czy mam teraz na to benzynę. Może później.
DavidC,

Jasne, daj mi znać, jeśli tak. (Chciałbym również zobaczyć więcej wyników dla innych zdjęć. :))
Martin Ender

2
W porządku. Dzięki za grę według zasad. ;)
Martin Ender,

1
Lubię kule! Są ładne i okrągłe
Sp3000

9

Python 2 z PIL

To wciąż jest trochę w toku. Ponadto poniższy kod jest okropnym bałaganem spaghetti i nie powinien być wykorzystywany jako inspiracja. :)

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

Jak to działa

Program dzieli płótno na Pregiony, z których każdy składa się z pewnej liczby komórek bez otworów. Początkowo płótno jest po prostu podzielone na przybliżone kwadraty, które są losowo przypisywane do regionów. Następnie obszary te są „zdeformowane” w procesie iteracyjnym, w którym dany piksel może zmienić swój region, jeśli

  1. zmiana zmniejszyłaby odległość RGB piksela od średniego koloru regionu, który go zawiera, oraz
  2. nie rozbija ani nie łączy komórek ani nie wprowadza do nich dziur.

Ten ostatni warunek można egzekwować lokalnie, więc proces jest trochę jak automat komórkowy. W ten sposób nie musimy wykonywać żadnych poszukiwań ścieżek, co znacznie przyspiesza proces. Ponieważ jednak komórek nie można rozbić, niektóre z nich kończą się jako długie „włókna”, które graniczą z innymi komórkami i hamują ich wzrost. Aby to naprawić, istnieje proces zwany „cięciem filamentu”, który czasami rozbija komórkę w kształcie filamentu na dwie części, jeśli Nw tym czasie jest mniej niż komórek. Komórki mogą również zniknąć, jeśli ich rozmiar wynosi 1, co powoduje miejsce na cięcia włókien.

Proces kończy się, gdy żaden piksel nie ma motywacji do przełączania regionów, a następnie każdy region jest po prostu pokolorowany przez swój średni kolor. Zwykle na wyjściu pozostaną pewne włókna, co można zobaczyć w poniższych przykładach, zwłaszcza w mgławicy.

P = 30, N = 500

Mona Lisa Pawian Kolorowe kulki Mgławica

Więcej zdjęć później.

Niektóre interesujące właściwości mojego programu są takie, że jest on probabilistyczny, więc wyniki mogą się różnić między różnymi uruchomieniami, chyba że użyjesz oczywiście tego samego pseudolosowego ziarna. Losowość nie jest jednak istotna, chciałem tylko uniknąć przypadkowych artefaktów, które mogą wynikać ze szczególnego sposobu, w jaki Python przemierza zbiór współrzędnych lub coś podobnego. Program zwykle używa wszystkich Pkolorów i prawie wszystkich Nkomórek, a komórki nigdy nie zawierają otworów z założenia. Ponadto proces deformacji jest dość powolny. Wyprodukowanie kolorowych kulek zajęło prawie 15 minut na mojej maszynie. Z drugiej strony, włączaszGRAPHICAL_LOGGINGopcja, otrzymasz fajną serię zdjęć procesu deformacji. Zrobiłem te Mona Lisa w animację GIF (zmniejszyłem 50%, aby zmniejszyć rozmiar pliku). Jeśli przyjrzysz się uważnie jej twarzy i włosom, zobaczysz proces cięcia filamentu w akcji.

wprowadź opis zdjęcia tutaj


Wow, te wyniki wyglądają naprawdę pięknie (choć nie całkiem tak, jakby były pomalowane cyframi, ale nadal bardzo ładne :)).
Martin Ender
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.