숫자로 페인트


42

트루 컬러 이미지가 제공됩니다. 당신의 임무는이 이미지의 버전을 생성하는 것입니다.이 그림은 숫자로 페인트 칠한 것처럼 보입니다 (비 그램이 아닌 어린이 활동 ). : 이미지와 함께, 당신은 두 개의 매개 변수를 제공하고 P , 색상 팔레트의 최대 크기 (즉, 최대 사용에 서로 다른 색상의 수), 및 N 의 최대 번호 세포 사용에 있습니다. 귀하의 알고리즘은 않습니다 하지 모두 사용해야하는 P의 색상과 N의 세포를하지만, 더 그 이상 사용하지 않아야합니다. 출력 이미지는 입력과 크기가 같아야합니다.

셀이 모두 동일한 색상을 가지고 픽셀의 연속 영역으로서 정의된다. 모퉁이에서만 만지는 픽셀은 연속적인 것으로 간주 되지 않습니다 . 세포에는 구멍이있을 수 있습니다.

즉, N 평평한 색조 / 단색 영역과 P 가지 색상만으로 입력 이미지를 근사화해야합니다.

매개 변수를 시각화하기 위해 여기에 매우 간단한 예가 있습니다 (특별한 입력 이미지가 아니라 미친 페인트 기술을 과시하는 경우). 다음 이미지는 P = 6N = 11입니다 .

여기에 이미지 설명을 입력하십시오

다음은 알고리즘을 테스트하기위한 몇 가지 이미지입니다 (주로 일반적인 용의자). 더 큰 버전의 사진을 클릭하십시오.

큰 파도 산호초 무지개 별이 빛나는 밤 강 갈색 곰 폭포 맨드릴 게 성운 미국 고딕 모나리자 비명

다른 매개 변수에 대한 소수의 결과를 포함하십시오. 많은 수의 결과를 표시하려면 imgur.com 에서 갤러리를 작성 하여 답변의 크기를 합리적으로 유지할 수 있습니다 . 또는 게시물에 미리보기 이미지를 넣고 위와 같이 더 큰 이미지로 연결되도록합니다. 또한 멋진 것을 발견하면 다른 테스트 이미지를 자유롭게 사용하십시오.

N ≥ 500 , P ~ 30 주위의 매개 변수 는 실제 페인트 번호 템플릿과 유사 하다고 가정합니다 .

이것은 인기 경연 대회이므로 가장 많은 순 투표 수를 얻은 답이 이깁니다. 유권자들은 다음과 같이 답변을 판단하도록 권장됩니다

  • 원본 이미지의 근사치
  • 알고리즘이 다른 종류의 이미지에서 얼마나 잘 작동하는지 (그림은 일반적으로 사진보다 쉽습니다).
  • 알고리즘이 매우 제한적인 매개 변수와 얼마나 잘 작동하는지
  • 세포 모양이 어떻게 유기적 / 부드럽게 보이는지

다음 Mathematica 스크립트를 사용하여 결과를 검증합니다.

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은 PIL을 사용하여 Python 2에서 검증기를 작성하기에 충분히 친절 했습니다.이 pastebin에서 찾을 수 있습니다.


2
가장 효율적인 것은 아니지만 다음 은 Python 2 PIL verifier 입니다.
Sp3000

정말 멋진 질문이지만, 우리는 또한 "숫자 별 페인트"버전도 볼 수 있기를 바랐습니다. 그 자리에 숫자가 있으므로 대답을 사용할 수 있습니다 :)

@Lembik 나는 원래 그것을 포함하고 싶었지만 질문의 흥미로운 부분에서 산만하다고 느꼈습니다. 제출물 중 하나의 출력을 가져 와서 템플릿으로 변환하는 것은 너무 어렵지 않아야합니다.
Martin Ender

이것은 매혹적인 게시물입니다. 실제 페인트 번호로 색상 번호를 추가하는 추가 단계를 수행 한 사람이 있습니까?
B. 블레어

답변:


39

PIL이 포함 된 Python 2 ( 갤러리 )

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)

업데이트 시간! 이 업데이트에는 간단한 스무딩 알고리즘이있어 이미지가 덜 흐릿하게 보입니다. 다시 업데이트하면 내 코드의 공정한 부분을 개선해야합니다. 왜냐하면 코드가 지저분 해지 기 때문입니다.

또한 셀 크기를 기준으로 k- 평균 무게 색상을 만들었습니다. 이는 더 제한적인 매개 변수 (예 : 성운의 중심과 미국 고딕의 갈퀴)에 대한 세부 정보가 손실되지만 전반적인 색상 선택이 더 선명하고 멋지게 만듭니다. 흥미롭게도 P = 5 인 경우 광선 추적 구체의 전체 배경이 사라집니다.

알고리즘 요약 :

  1. 픽셀을 CIELAB 색상 공간으로 변환합니다 . CIELAB은 RGB보다 사람의 시력과 유사합니다. 원래 HSL (색조, 채도, 명도)을 사용했지만 두 가지 문제가있었습니다. 흰색 / 회색 / 검정의 색조는 정의되지 않았으며 색조는 감겨 진 각도로 측정되어 k- 평균을 사용하기 어렵습니다.
  2. 플러드 필을 사용하여 이미지를 같은 색의 셀로 나눕니다 : 셀에없는 픽셀을 선택하고 지정된 공차를 사용하여 플러드 필을 수행합니다. 두 색상 사이의 거리를 측정하기 위해 표준 유클리드 표준을 사용합니다. 이 위키 기사 에서보다 복잡한 공식을 볼 수있다 .
  3. 작은 셀을 이웃과 병합 : 플러드 필은 지정된 크기보다 작은 셀을 지정된 픽셀보다 적은 셀을 가장 인접한 픽셀이있는 이웃 셀과 병합합니다. 이렇게하면 셀 수가 상당히 줄어들어 이후 단계의 실행 시간이 향상됩니다.
  4. 유사한 색상의 영역을 병합합니다 . 크기가 작아지는 순서대로 셀을 통과합니다. 인접한 셀의 색상이 일정 거리보다 작은 경우 셀을 병합합니다. 더 이상 병합 할 수 없을 때까지 셀을 계속 진행하십시오.
  5. 1.5N 미만의 셀이있을 때까지 병합 (N- 병합) : 셀 크기와 색상 차이에 따른 점수를 사용하여 최대 1.5N 셀이 될 때까지 셀을 병합합니다. 나중에 다시 병합 할 때 약간의 여유가 있습니다.
  6. k- 평균 (P- 병합)을 사용하여 P 색상 미만이 될 때까지 병합 : 셀 크기를 기준으로 가중치를 적용 하여 k- 평균 군집 알고리즘을 지정된 횟수만큼 사용하여 셀 색상의 군집을 생성합니다. Davies-Bouldin 지수 의 변형을 기반으로 각 군집을 채점 하고 사용하기에 가장 적합한 군집을 선택하십시오.
  7. 대략적인 가우스 평활화 : 여러 선형 흐림 효과를 사용하여 가우시안 흐림 효과를 근사화합니다 ( 자세한 내용은 여기 참조 ). 그런 다음 각 픽셀에 대해 흐리게 처리 된 이미지의 색상과 가장 가까운 사전 흐리게 처리 된 이미지에서 자체와 주변 색상을 선택합니다. 이 부분은 아직 최적의 알고리즘을 구현하지 않았기 때문에 필요한 경우 더 시간에 맞게 최적화 할 수 있습니다.
  8. 새로운 지역을 해결하기 위해 또 다른 플러드 필 패스를 수행 하십시오. 이전 단계에서 실제로 더 많은 셀을 생성 할 수 있으므로 필요 합니다.
  9. 다른 소형 셀 병합 패스를 수행하십시오.
  10. 다른 N- 병합 통과 : 이번에는 1.5N이 아닌 N 개의 셀로 내려갑니다.

각 이미지의 처리 시간은 크기와 복잡도에 따라 달라지며 테스트 이미지의 경우 20 초에서 7 분 사이의 시간이 소요됩니다.

알고리즘은 임의 화 (예 : 병합, k- 평균)를 사용하므로 실행마다 다른 결과를 얻을 수 있습니다. 다음은 곰 이미지에 대한 두 개의 실행을 비교 한 것입니다. N = 50이고 P = 10입니다.

에프 엠


참고 : 아래의 모든 이미지는 링크입니다. 이 이미지의 대부분은 첫 번째 실행에서 나온 것이지만 출력이 마음에 들지 않으면 최대 세 번의 시도가 공정했습니다.

N = 50, P = 10

엘 엠 에이 아르 자형 케이 디 영형 승 엔 지 영형 엘

N = 500, P = 30

에프 . . . : ( 에이 에이 에이 에이 에이 에이

그러나 나는 색상으로 페인트 할 때 꽤 게 으르므로 재미를 위해 ...

N = 20, P = 5

에이 에이 에이 에이 에이 에이 에이 에이 에이 에이 에이 에이

또한 백만 가지 색상 을 N = 500, P = 30으로 짜려고 할 때 어떤 일이 발생하는지 보는 것도 재미 있습니다 .

에이

애니메이션 GIF 형식으로 N = 500, P = 30 인 수중 이미지 알고리즘에 대한 단계별 연습은 다음과 같습니다.

에이


또한 이전 버전의 알고리즘에 대한 갤러리를 여기에 만들었습니다 . 마지막 버전 (내 성운에 별이 더 많고 곰이 더 화려하게 보일 때부터)에서 가장 좋아하는 것들이 있습니다.

에이 에이


프로그램이 픽셀을 풀려고 할 때 예외가 발생하면 im = im.convert("RGB")일부 사진에 필요한 것처럼 보입니다 . 코드를 약간 재구성 한 후에 넣겠습니다.
Sp3000

15

PIL이 포함 된 Python 2

또한 파이썬 솔루션과 아마도 매우 많은 작업이 진행되고 있습니다.

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)

이 알고리즘은 먼저 색상부터 시작하여 SP3000과 다른 접근 방식을 따릅니다.

  • k- 평균 군집화를 통해 P 색상 의 색상 팔레트를 찾고이 축소 된 팔레트에서 이미지를 페인트하십시오.

  • 약간의 중간 필터를 적용하여 약간의 노이즈를 제거하십시오.

  • 모든 단색 세포의 목록을 만들고 크기별로 정렬하십시오.

  • N 개의 셀만 남을 때까지 가장 작은 셀을 각각 가장 큰 이웃과 병합합니다 .

결과의 속도와 품질 측면에서 개선의 여지가 꽤 있습니다. 특히 셀 병합 단계는 최대 몇 분이 걸릴 수 있으며 최적의 결과와는 거리가 멀습니다.


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


나는 같은 접근법 (canvs에서 Javascript로 시도하려고 시도했다)에 대해 먼저 사용하려고 시도했지만 너무 오래 걸리기 때문에 eventaully는 포기했기 때문에 그것이 어떻게 생겼는지 볼 수있어서 정말 좋습니다!
flawr

아주 좋은 일입니다. 나는 20 개의 세포로 곰을 좋아했습니다.
DavidC

15

매스 매 티카

현재 가우스 필터에 사용되는 색상 수와 가우스 반경이 사용됩니다. 반경이 클수록 색상이 흐려지고 병합됩니다.

셀 수를 입력 할 수 없으므로 문제의 기본 요구 사항 중 하나를 충족하지 않습니다.

출력에는 각 색상의 셀 수와 총 셀 수가 포함됩니다.

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

최신 정보

quantImage2원하는 수의 셀을 입력으로 지정할 수 있습니다. 밀접한 일치를 찾을 때까지 더 큰 반지름을 가진 시나리오를 반복하여 최상의 가우시안 반지름을 결정합니다.

quantImage2 출력 (그림, 요청 된 셀, 사용 된 셀, 오류, 가우스 반경 사용).

그러나 매우 느립니다. 시간을 절약하기 위해 초기 반지름으로 시작할 수 있으며 기본값은 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}

출력에서 원하는 셀 수를 지정하는 예제입니다.

25 가지 색상의 90 개 셀을 요청하는 예입니다. 솔루션이 88 개의 셀, 2 % 오류를 반환합니다. 이 함수는 가우스 반경 55를 선택했습니다 (왜곡 왜곡).

원숭이 X


입력에 가우스 반지름이 포함되지만 셀 수는 포함되지 않은 예입니다.

25 색, 가우스 반경 5 픽셀

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

공


3 색, 반경 17 픽셀

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

웨이브 3 17


20 색, 반경 17 픽셀

색상 수는 늘 렸지만 초점은 맞지 않았습니다. 셀 수가 증가함에 유의하십시오.

웨이브 2


6 색, 반경 4 픽셀

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]

원숭이 1


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

원숭이 2


별이 빛나는 밤

6 개의 색상과 60 개의 셀만 있습니다. showColors소유권 주장에 사용 된 색상에 색상이 일치하지 않습니다 . (노란색은 5 가지 색상 중 표시되지 않지만 그림에 사용됩니다.)이를 알아낼 수 있는지 살펴 보겠습니다.

별이 빛나는 밤 1


이것은 절대적으로 화려하며 제한적인 매개 변수에 실제로 효과적입니다. 셀 수를 매개 변수로 바꿀 가능성이 있습니까? (나는 항상 셀 수에서 반지름에 대한 추정치를 찾아서 적용 한 다음 작은 셀을 병합하여 한계 아래로 떨어질 수 있다고 가정합니다.)
Martin Ender

showColors다양한 수의 색상과 반지름을 반복하고 원하는 수의 셀에 가장 가까운 조합을 선택하여 표를 만들 수 있습니다. 현재 가스를 가지고 있는지 확실하지 않습니다. 아마 나중에
DavidC

그래, 당신이 할 경우 알려주세요. (나는 또한 다른 이미지에 대한 더 많은 결과를보고 싶습니다. :))
Martin Ender

2
괜찮아. 규칙에 따라 연주 해 주셔서 감사합니다. ;)
Martin Ender

1
나는 구체를 좋아한다! 그들은 멋지고 둥글다
Sp3000

9

PIL이 포함 된 Python 2

이것은 여전히 ​​다소 진행중인 작업입니다. 또한 아래 코드는 스파게티의 끔찍한 혼란이므로 영감으로 사용해서는 안됩니다. :)

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

작동 원리

이 프로그램은 캔버스를 P영역 으로 나누고 각 영역은 구멍이없는 몇 개의 셀로 구성됩니다. 처음에 캔버스는 대략적인 정사각형으로 나뉘어지며 지역에 무작위로 할당됩니다. 그런 다음 반복 영역에서 이러한 영역이 "변형"됩니다. 여기서 지정된 픽셀은 다음과 같은 경우 해당 영역을 변경할 수 있습니다.

  1. 그 변화는 픽셀의 RGB 거리를 픽셀이 포함 된 영역의 평균 색상에서 감소시킵니다.
  2. 셀을 끊거나 병합하거나 구멍을 뚫지 않습니다.

후자의 조건은 로컬에서 시행 될 수 있으므로 프로세스는 셀룰러 오토 마톤과 비슷합니다. 이런 식으로 경로 찾기 등을 수행 할 필요가 없으므로 프로세스 속도가 크게 향상됩니다. 그러나, 세포가 분해 될 수 없기 때문에, 이들 중 일부는 다른 세포와 접하여 성장을 억제하는 "필라멘트"로서 길다. 이 문제를 해결하기 위해 "필라멘트 컷"이라고하는 프로세스가 있는데, N그 당시 셀 수가 적 으면 필라멘트 모양의 셀이 2 개로 분리되는 경우가 있습니다 . 크기가 1이면 셀도 사라질 수 있으며, 이로 인해 필라멘트가 잘릴 수 있습니다.

픽셀 전환 영역에 대한 인센티브가없는 픽셀이 없으면 프로세스가 종료 된 후 각 영역의 색상이 평균 색상으로 표시됩니다. 아래의 예, 특히 성운에서 볼 수 있듯이 출력에 일부 필라멘트가 남아있을 것입니다.

P = 30, N = 500

모나리자 비비 화려한 공 성운

나중에 더 많은 사진.

내 프로그램의 몇 가지 흥미로운 속성은 확률 적이므로 동일한 의사 난수 시드를 사용하지 않는 한 결과가 다른 실행마다 다를 수 있다는 것입니다. 그러나 무작위성이 필수는 아니지만, 파이썬이 일련의 좌표 또는 이와 유사한 것을 가로 지르는 특정 방식으로 인해 발생할 수있는 우발적 인 인공물을 피하고 싶었습니다. 이 프로그램은 모든 P색상과 거의 모든 N셀 을 사용하는 경향이 있으며 셀에는 의도적으로 구멍이 없습니다. 또한 변형 과정이 상당히 느립니다. 색깔의 공은 내 기계에서 생산하는데 거의 15 분이 걸렸습니다. 거꾸로, 당신은 켜GRAPHICAL_LOGGING옵션으로 변형 과정에 대한 멋진 일련의 그림을 얻을 수 있습니다. Mona Lisa를 GIF 애니메이션으로 만들었습니다 (파일 크기를 줄이기 위해 50 % 축소). 그녀의 얼굴과 머리카락을 자세히 보면 필라멘트 절단 과정이 실제로 작동하는 것을 볼 수 있습니다.

여기에 이미지 설명을 입력하십시오


와우,이 결과는 정말 아름답게 보입니다 (숫자로 그려진 것 같지는 않지만 여전히 아주 좋습니다 :)).
Martin Ender
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.