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 P
cá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
- 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à
- 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 N
cá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
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 P
mà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_LOGGING
tù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.