Vẽ bằng số


42

Bạn đã cho một hình ảnh màu sắc trung thực. Nhiệm vụ của bạn là tạo ra một phiên bản của hình ảnh này, trông giống như nó được vẽ bằng cách sử dụng các số sơn (hoạt động của trẻ em, không phải là chữ tượng hình). Cùng với hình ảnh, bạn được cung cấp hai tham số: P , kích thước tối đa của bảng màu (tức là số lượng màu riêng biệt tối đa sẽ sử dụng) và N , số lượng ô tối đa sẽ sử dụng. Thuật toán của bạn không phải sử dụng tất cả các màu PN ô, nhưng nó không được sử dụng nhiều hơn thế. Hình ảnh đầu ra phải có cùng kích thước với đầu vào.

Một ô được định nghĩa là một vùng pixel liền kề mà tất cả đều có cùng màu. Điểm ảnh chỉ chạm vào một góc không được coi là tiếp giáp nhau. Các tế bào có thể có lỗ.

Nói tóm lại, bạn phải ước tính hình ảnh đầu vào chỉ với N vùng màu phẳng / màu đậm và P màu khác nhau.

Chỉ để trực quan hóa các tham số, đây là một ví dụ rất đơn giản (không có hình ảnh đầu vào cụ thể; thể hiện kỹ năng Paint điên rồ của tôi). Hình ảnh sau đây có P = 6N = 11 :

nhập mô tả hình ảnh ở đây

Dưới đây là một vài hình ảnh để kiểm tra thuật toán của bạn (chủ yếu là các nghi phạm thông thường của chúng tôi). Nhấp vào hình ảnh cho các phiên bản lớn hơn.

Sóng lớn San hô cầu vồng Đêm đầy sao con sông Gấu nâu Thác nước Mandrill Tinh vân cua Mỹ gothic nàng mô na Li Sa Hét lên

Vui lòng bao gồm một số ít kết quả cho các thông số khác nhau. Nếu bạn muốn hiển thị một số lượng lớn kết quả, bạn có thể tạo một bộ sưu tập trên imgur.com , để giữ cho kích thước của các câu trả lời hợp lý. Hoặc, đặt hình thu nhỏ trong bài đăng của bạn và làm cho chúng liên kết đến hình ảnh lớn hơn, như tôi đã làm ở trên. Ngoài ra, hãy thoải mái sử dụng các hình ảnh thử nghiệm khác, nếu bạn tìm thấy một cái gì đó tốt đẹp.

Tôi giả sử rằng các tham số khoảng N ≥ 500 , P ~ 30 sẽ tương tự như các mẫu sơn theo số thực.

Đây là một cuộc thi phổ biến, vì vậy câu trả lời có nhiều lượt bình chọn nhất sẽ thắng. Cử tri được khuyến khích đánh giá câu trả lời bằng cách

  • làm thế nào tốt những hình ảnh ban đầu là gần đúng.
  • thuật toán hoạt động tốt như thế nào trên các loại hình ảnh khác nhau (tranh có thể thường dễ hơn so với ảnh).
  • thuật toán hoạt động tốt như thế nào với các tham số rất hạn chế.
  • Làm thế nào hữu cơ / mịn các hình dạng tế bào trông.

Tôi sẽ sử dụng tập lệnh Mathicala sau đây, để xác thực kết quả:

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 đã đủ tốt để viết một trình xác minh trong Python 2 bằng PIL, mà bạn tìm thấy ở pastebin này .


2
Không phải là điều hiệu quả nhất, nhưng đây là trình xác minh Python 2 PIL .
Sp3000

Thật là một câu hỏi đáng yêu nhưng tôi đã hy vọng chúng ta cũng sẽ thấy phiên bản "vẽ theo số" thích hợp. Đó là với các số tại chỗ để tôi có thể sử dụng các câu trả lời :)

@Lembik Ban đầu tôi muốn bao gồm điều đó, nhưng tôi cảm thấy rằng nó bị phân tâm khỏi phần thú vị của câu hỏi. Mặc dù vậy, không quá khó để lấy đầu ra của một trong những bài nộp và chuyển đổi thành một mẫu.
Martin Ender

Đây là một bài viết hấp dẫn. Có ai đã đi thêm bước bổ sung các số màu như một Paint thực sự theo Số chưa?
B. Blair

Câu trả lời:


39

Python 2 với PIL ( Thư viện ảnh )

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)

Cập nhật thời gian! Bản cập nhật này có một thuật toán làm mịn đơn giản để làm cho hình ảnh trông mờ hơn. Nếu tôi cập nhật lại, tôi sẽ phải sửa lại một đoạn mã công bằng của mình, bởi vì nó đang trở nên lộn xộn & tôi hd 2 glf a fw thngs 2 mke t char lim.

Tôi cũng đã tạo ra màu sắc có nghĩa là trọng lượng dựa trên kích thước ô, làm mất một số chi tiết cho các tham số hạn chế hơn (ví dụ: trung tâm của tinh vân và cây chĩa ba của American Gothic) nhưng làm cho lựa chọn màu sắc tổng thể sắc nét và đẹp hơn. Điều thú vị là, nó mất toàn bộ nền cho các quả cầu raytraced với P = 5.

Tóm tắt thuật toán:

  1. Chuyển đổi các pixel thành không gian màu CIELAB: CIELAB xấp xỉ tầm nhìn của con người tốt hơn RGB. Ban đầu tôi đã sử dụng HSL (màu sắc, độ bão hòa, độ sáng) nhưng điều này có hai vấn đề - màu sắc của màu trắng / xám / đen không được xác định và màu sắc được đo theo độ bao quanh, làm cho k-có nghĩa là khó sử dụng.
  2. Chia hình ảnh thành các ô có màu giống như sử dụng chức năng lấp đầy: Chọn một pixel không nằm trong một ô và thực hiện lấp đầy bằng cách sử dụng dung sai đã chỉ định. Để đo khoảng cách giữa hai màu, tôi sử dụng chỉ tiêu Euclide tiêu chuẩn. Công thức phức tạp hơn có sẵn trên bài viết wiki này .
  3. Hợp nhất các ô nhỏ với các lân cận của chúng : Lấp đầy tạo ra rất nhiều ô 1 hoặc 2 pixel - hợp nhất các ô nhỏ hơn một kích thước đã chỉ định với ô lân cận có các pixel liền kề nhất. Điều này làm giảm đáng kể số lượng tế bào, cải thiện thời gian chạy cho các bước sau.
  4. Hợp nhất các vùng có màu tương tự nhau : Đi qua các ô theo thứ tự kích thước giảm dần. Nếu bất kỳ ô lân cận nào có màu trung bình cách nhau ít hơn một khoảng nhất định, hãy hợp nhất các ô. Tiếp tục đi qua các ô cho đến khi không thể hợp nhất được nữa.
  5. Hợp nhất cho đến khi chúng ta có ít hơn 1,5N ô (hợp nhất N) : Hợp nhất các ô lại với nhau, sử dụng cách tính điểm dựa trên kích thước ô và chênh lệch màu, cho đến khi chúng ta có tối đa 1,5N ô. Chúng tôi cho phép một chút chậm trễ vì chúng tôi sẽ hợp nhất lại sau.
  6. Hợp nhất cho đến khi chúng ta có ít hơn màu P, sử dụng phương tiện k (hợp nhất P) : Sử dụng thuật toán phân cụm k-nghĩa một số lần được chỉ định để tạo cụm màu của ô, tính trọng số dựa trên kích thước ô. Điểm số từng cụm dựa trên một biến thể của chỉ số Davies-Bouldin và chọn phân cụm tốt nhất để sử dụng.
  7. Làm mịn Gaussian gần đúng : Sử dụng một số hiệu ứng làm mờ tuyến tính để làm mờ Gaussian gần đúng ( chi tiết tại đây ). Sau đó, với mỗi pixel, chọn màu của chính nó và các vùng lân cận của nó trong hình ảnh được làm mờ gần nhất với màu của nó trong hình ảnh bị mờ. Phần này có thể được tối ưu hóa theo thời gian hơn nếu cần thiết vì tôi vẫn chưa thực hiện thuật toán tối ưu.
  8. Thực hiện một lần vượt lũ khác để xử lý các vùng mới : Điều này là cần thiết vì bước trước đó thực sự có thể tạo ra nhiều ô hơn .
  9. Thực hiện một quá trình hợp nhất tế bào nhỏ khác
  10. Thực hiện một lần hợp nhất N khác : Lần này chúng ta đi xuống các ô N thay vì 1,5N.

Thời gian xử lý cho mỗi hình ảnh phụ thuộc nhiều vào kích thước và độ phức tạp của nó, với thời gian từ 20 giây đến 7 phút cho các hình ảnh thử nghiệm.

Vì thuật toán sử dụng ngẫu nhiên hóa (ví dụ: hợp nhất, phương tiện k), bạn có thể nhận được các kết quả khác nhau trên các lần chạy khác nhau. Dưới đây là so sánh hai lần chạy cho hình ảnh con gấu, với N = 50 và P = 10:

ĐỤ M


Lưu ý: Tất cả các hình ảnh dưới đây là liên kết. Hầu hết những hình ảnh này là trực tiếp từ lần chạy đầu tiên, nhưng nếu tôi không thích kết quả đầu ra, tôi đã cho phép mình có tới ba lần thử để công bằng.

N = 50, P = 10

L M một r k Cười mở miệng o w viết sai rồi g o tôi

N = 500, P = 30

đụ . . . : ( một một một một một một

Nhưng tôi khá lười biếng khi vẽ bằng màu sắc, vì vậy chỉ để cho vui ...

N = 20, P = 5

một một một một một một một một một một một một

Ngoài ra, thật thú vị khi xem điều gì sẽ xảy ra khi bạn cố gắng nén 1 triệu màu vào N = 500, P = 30:

một

Đây là hướng dẫn từng bước của thuật toán cho hình ảnh dưới nước với N = 500 và P = 30, ở dạng GIF hoạt hình:

một


Tôi cũng đã tạo một bộ sưu tập cho các phiên bản trước của thuật toán ở đây . Đây là một số mục yêu thích của tôi từ phiên bản trước (từ khi tinh vân có nhiều ngôi sao hơn và con gấu trông có vẻ xa hơn):

một một


Nếu bất cứ ai cũng có Ngoại lệ khi chương trình cố gắng giải nén pixel, có vẻ như im = im.convert("RGB")nó là cần thiết cho một số bức ảnh. Tôi sẽ đưa nó vào sau khi tôi cấu trúc lại mã một chút.
Sp3000

15

Python 2 với PIL

Cũng là một giải pháp Python và có lẽ rất nhiều công việc đang tiến hành:

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)

Thuật toán tuân theo cách tiếp cận khác với SP3000, bắt đầu bằng màu trước:

  • Tìm một bảng màu của các màu P bằng cách phân cụm k-nghĩa và vẽ hình ảnh trong bảng màu giảm này.

  • Áp dụng một bộ lọc trung bình nhẹ để loại bỏ một số tiếng ồn.

  • Tạo một danh sách tất cả các ô đơn sắc và sắp xếp nó theo kích thước.

  • Hợp nhất các tế bào nhỏ nhất với người láng giềng lớn nhất của mình cho đến khi có chỉ N tế bào còn lại.

Có khá nhiều chỗ để cải thiện, cả về tốc độ và chất lượng của kết quả. Đặc biệt là bước hợp nhất tế bào có thể mất đến nhiều phút và cho kết quả tối ưu.


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


Lần đầu tiên tôi đã thử sử dụng cách tiếp cận tương tự (đã cố gắng thực hiện nó trong Javascript trên một canvs) nhưng cuối cùng đã bỏ cuộc vì nó diễn ra quá lâu, vì vậy thật tuyệt khi thấy nó trông như thế nào, hoạt động tốt!
flawr

Công việc rất tốt. Tôi yêu con gấu với 20 tế bào.
DavidC

15

Toán học

Hiện tại, điều này lấy số lượng màu và bán kính Gaussian được sử dụng trong bộ lọc Gaussian. Bán kính càng lớn, độ mờ và hợp nhất của màu sắc càng lớn.

Vì nó không cho phép nhập số lượng ô, nên nó không đáp ứng một trong những yêu cầu cơ bản của thử thách.

Đầu ra bao gồm số lượng ô cho mỗi màu và tổng số ô.

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

Cập nhật

quantImage2cho phép chỉ định số lượng ô mong muốn làm đầu vào. Nó xác định Bán kính Gaussian tốt nhất bằng cách lặp qua các kịch bản có bán kính lớn hơn cho đến khi tìm thấy kết quả khớp gần.

quantImage2 đầu ra (hình ảnh, ô được yêu cầu, ô đã sử dụng, lỗi, gaussian Radius được sử dụng).

Đó là, tuy nhiên, rất chậm. Để tiết kiệm thời gian, bạn có thể bắt đầu với bán kính ban đầu, giá trị mặc định là 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}

Ví dụ mà chúng tôi chỉ định số lượng ô mong muốn trong đầu ra.

Ví dụ yêu cầu 90 ô với 25 màu. Giải pháp trả về 88 ô, lỗi 2%. Hàm đã chọn bán kính Gaussian là 55. (Rất nhiều biến dạng).

Ape X


Các ví dụ mà đầu vào bao gồm bán kính Gaussian, nhưng không phải là số lượng ô.

25 Màu sắc, bán kính Gaussian 5 pixel

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

những quả bóng


Ba màu, bán kính 17 pixel

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

sóng 3 17


Hai mươi màu, bán kính 17 pixel

Chúng tôi tăng số lượng màu sắc nhưng không tập trung. Lưu ý sự gia tăng số lượng tế bào.

sóng 2


Sáu màu, bán kính 4 pixel

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

sóng3


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

vượn 1


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

vượn 2


Đêm đầy sao

Chỉ với 6 màu và 60 ô. Có một sự không phù hợp màu sắc trong các màu sắc mà showColorsnó tuyên bố sử dụng. (Màu vàng không xuất hiện trong số 5 màu nhưng nó được sử dụng trong bản vẽ.) Tôi sẽ xem liệu tôi có thể tìm ra điều này không.

đêm đầy sao 1


Điều này là hoàn toàn tuyệt đẹp, và hoạt động thực sự tốt cho các tham số hạn chế. Bất kỳ cơ hội để biến số lượng tế bào thành một tham số? (Tôi cho rằng bạn luôn có thể tìm thấy một số ước tính cho bán kính từ số lượng ô, áp dụng điều đó và sau đó hợp nhất các ô nhỏ cho đến khi bạn ở dưới giới hạn.)
Martin Ender

Có thể tạo Bảng showColors, lặp qua một loạt các màu và bán kính và chọn kết hợp gần nhất với số lượng ô mong muốn. Không chắc chắn nếu tôi có khí để làm điều đó vào lúc này. Có lẽ sau này.
DavidC

Chắc chắn, cho tôi biết nếu bạn làm. (Tôi cũng muốn xem thêm một số kết quả cho các hình ảnh khác. :))
Martin Ender

2
Tốt rồi. Cảm ơn đã chơi theo luật. ;)
Martin Ender

1
Tôi thích những quả cầu! Chúng rất đẹp và tròn
Sp3000

9

Python 2 với PIL

Đây vẫn là một công việc đang tiến triển. Ngoài ra, mã dưới đây là một mớ hỗn độn khủng khiếp của spaghetti, và không nên được sử dụng như một nguồn cảm hứng. :)

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

Làm thế nào nó hoạt động

Chương trình chia khung vẽ thành Pcác vùng, mỗi vùng bao gồm một số ô không có lỗ. Ban đầu, khung vẽ chỉ được chia thành các ô vuông gần đúng, được gán ngẫu nhiên cho các vùng. Sau đó, các vùng này bị "biến dạng" trong một quy trình lặp, trong đó một pixel đã cho có thể thay đổi vùng của nó nếu

  1. thay đổi sẽ làm giảm khoảng cách RGB của pixel so với màu trung bình của vùng chứa nó và
  2. nó không phá vỡ hoặc hợp nhất các tế bào hoặc giới thiệu các lỗ hổng trong chúng.

Điều kiện thứ hai có thể được thi hành cục bộ, vì vậy quá trình này hơi giống với máy tự động di động. Bằng cách này, chúng tôi không phải thực hiện bất kỳ tìm đường nào hoặc như vậy, giúp tăng tốc quá trình lên rất nhiều. Tuy nhiên, vì các tế bào không thể bị phá vỡ, một số trong số chúng kết thúc như những "sợi tơ" dài bao quanh các tế bào khác và kìm hãm sự phát triển của chúng. Để khắc phục điều này, có một quá trình gọi là "cắt dây tóc", đôi khi phá vỡ một tế bào hình dây tóc thành hai, nếu có ít hơn Ncác tế bào tại thời điểm đó. Các tế bào cũng có thể biến mất nếu kích thước của chúng là 1, và điều này nhường chỗ cho các sợi tơ bị cắt.

Quá trình kết thúc khi không có pixel nào có động lực để chuyển đổi các vùng và sau đó, mỗi vùng được tô màu đơn giản bằng màu trung bình của nó. Thông thường sẽ có một số sợi còn lại trong đầu ra, như có thể thấy trong các ví dụ dưới đây, đặc biệt là trong tinh vân.

P = 30, N = 500

nàng mô na Li Sa Khỉ đầu chó Quả bóng đầy màu sắc Tinh vân

Thêm hình ảnh sau.

Một số tính chất thú vị của chương trình của tôi là nó có xác suất, do đó kết quả có thể khác nhau giữa các lần chạy khác nhau, trừ khi bạn sử dụng cùng một hạt giống giả ngẫu nhiên. Tuy nhiên, tính ngẫu nhiên là không cần thiết, tôi chỉ muốn tránh bất kỳ tạo tác ngẫu nhiên nào có thể xuất phát từ cách cụ thể Python đi ngang qua một bộ tọa độ hoặc một cái gì đó tương tự. Chương trình có xu hướng sử dụng tất cả các Pmàu và hầu hết tất cả các Nô, và các ô không bao giờ chứa lỗ theo thiết kế. Ngoài ra, quá trình biến dạng là khá chậm. Những quả bóng màu mất gần 15 phút để sản xuất trên máy của tôi. Ở phía trên, nó bậtGRAPHICAL_LOGGINGtùy chọn, bạn sẽ nhận được một loạt hình ảnh tuyệt vời của quá trình biến dạng. Tôi đã biến những cái Mona Lisa thành hoạt hình GIF (thu nhỏ 50% để giảm kích thước tệp). Nếu bạn nhìn kỹ vào khuôn mặt và mái tóc của cô ấy, bạn có thể thấy quá trình cắt dây tóc đang hoạt động.

nhập mô tả hình ảnh ở đây


Ồ, những kết quả này trông rất đẹp (mặc dù không hoàn toàn giống như được vẽ bằng số, nhưng vẫn rất đẹp :)).
Martin Ender
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.