蟒蛇
我生成一条动态路径以最大程度地减少蛇行进时的颜色变化。以下是一些图片:
公差= 0.01
以上图像的循环颜色路径(蓝色到红色,重复时变绿色):
该路径是通过从一些初始路径开始,然后在其上添加2x2循环直到图像被填充而生成的。这种方法的优点是可以将循环添加到路径上的任何位置,因此您无法将自己画在角落,而拥有更大的自由来构建所需的路径。我跟踪与当前路径相邻的可能循环,并将它们存储在堆中,并根据循环中颜色的变化进行加权。然后,我弹出颜色变化最少的循环并将其添加到路径中,然后重复进行直到图像填满为止。
我实际上只跟踪循环(代码中的“ DetourBlock”),然后重建路径;这是一个错误,因为在某些特殊情况下,宽度/高度为奇数,我花了几个小时调试重构方法。那好吧。
路径生成量度需要调整,我也有一个更好的着色的想法,但是我认为我会首先使用它,因为它工作得很好。除了这一点,在某些固定路径中似乎更好:
这是Python代码,为我的糟糕编码习惯道歉:
# snakedraw.py
# Image library: Pillow
# Would like to animate with matplotlib... (dependencies dateutil, six)
import heapq
from math import pow, sqrt, log
from PIL import Image
tolerance = 0.001
imageList = [ "lena.png", "MonaLisa.png", "Mandrill.png", "smallGreatWave.png", "largeGreatWave.png", "random.png"]
# A useful container to sort objects associated with a floating point value
class SortContainer:
def __init__(self, value, obj):
self.fvalue = float(value)
self.obj = obj
def __float__(self):
return float(self.fvalue)
def __lt__(self, other):
return self.fvalue < float(other)
def __eq__(self, other):
return self.fvalue == float(other)
def __gt__(self, other):
return self.fvalue > float(other)
# Directional constants and rotation functions
offsets = [ (1,0), (0,1), (-1,0), (0,-1) ] # RULD, in CCW order
R, U, L, D = 0, 1, 2, 3
def d90ccw(i):
return (i+1) % 4
def d180(i):
return (i+2) % 4
def d90cw(i):
return (i+3) % 4
def direction(dx, dy):
return offsets.index((dx,dy))
# Standard color metric: Euclidean distance in the RGB cube. Distance between opposite corners normalized to 1.
pixelMax = 255
cChannels = 3
def colorMetric(p):
return sqrt(sum([ pow(p[i],2) for i in range(cChannels)])/cChannels)/pixelMax
def colorDistance(p1,p2):
return colorMetric( [ p1[i]-p2[i] for i in range(cChannels) ] )
# Contains the structure of the path
class DetourBlock:
def __init__(self, parent, x, y):
assert(x%2==0 and y%2==0)
self.x = x
self.y = y
self.parent = None
self.neighbors = [None, None, None, None]
def getdir(A, B):
dx = (B.x - A.x)//2
dy = (B.y - A.y)//2
return direction(dx, dy)
class ImageTracer:
def __init__(self, imgName):
self.imgName = imgName
img = Image.open(imgName)
img = img.convert(mode="RGB") # needed for BW images
self.srcImg = [ [ [ float(c) for c in img.getpixel( (x,y) ) ] for y in range(img.size[1]) ] for x in range(img.size[0])]
self.srcX = img.size[0]
self.srcY = img.size[1]
# Set up infrastructure
self.DetourGrid = [ [ DetourBlock(None, 2*x, 2*y) \
for y in range((self.srcY+1)//2)] \
for x in range((self.srcX+1)//2)]
self.dgX = len(self.DetourGrid)
self.dgY = len(self.DetourGrid[0])
self.DetourOptions = list() # heap!
self.DetourStart = None
self.initPath()
def initPath(self):
print("Initializing")
if not self.srcX%2 and not self.srcY%2:
self.AssignToPath(None, self.DetourGrid[0][0])
self.DetourStart = self.DetourGrid[0][0]
lastDB = None
if self.srcX%2: # right edge initial path
self.DetourStart = self.DetourGrid[-1][0]
for i in range(self.dgY):
nextDB = self.DetourGrid[-1][i]
self.AssignToPath(lastDB, nextDB)
lastDB = nextDB
if self.srcY%2: # bottom edge initial path
if not self.srcX%2:
self.DetourStart = self.DetourGrid[-1][-1]
for i in reversed(range(self.dgX-(self.srcX%2))): # loop condition keeps the path contiguous and won't add corner again
nextDB = self.DetourGrid[i][-1]
self.AssignToPath(lastDB, nextDB)
lastDB = nextDB
# When DetourBlock A has an exposed side that can potentially detour into DetourBlock B,
# this is used to calculate a heuristic weight. Lower weights are better, they minimize the color distance
# between pixels connected by the snake path
def CostBlock(self, A, B):
# Weight the block detour based on [connections made - connections broken]
dx = (B.x - A.x)//2
dy = (B.y - A.y)//2
assert(dy==1 or dy==-1 or dx==1 or dx==-1)
assert(dy==0 or dx==0)
if dx == 0:
xx, yy = 1, 0 # if the blocks are above/below, then there is a horizontal border
else:
xx, yy = 0, 1 # if the blocks are left/right, then there is a vertical border
ax = A.x + (dx+1)//2
ay = A.y + (dy+1)//2
bx = B.x + (1-dx)//2
by = B.y + (1-dy)//2
fmtImg = self.srcImg
''' Does not work well compared to the method below
return ( colorDistance(fmtImg[ax][ay], fmtImg[bx][by]) + # Path connects A and B pixels
colorDistance(fmtImg[ax+xx][ay+yy], fmtImg[bx+xx][by+yy]) # Path loops back from B to A eventually through another pixel
- colorDistance(fmtImg[ax][ay], fmtImg[ax+xx][ay+yy]) # Two pixels of A are no longer connected if we detour
- colorDistance(fmtImg[bx][by], fmtImg[bx+xx][by+yy]) ) # Two pixels of B can't be connected if we make this detour
'''
return ( colorDistance(fmtImg[ax][ay], fmtImg[bx][by]) + # Path connects A and B pixels
colorDistance(fmtImg[ax+xx][ay+yy], fmtImg[bx+xx][by+yy])) # Path loops back from B to A eventually through another pixel
# Adds a detour to the path (really via child link), and adds the newly adjacent blocks to the potential detour list
def AssignToPath(self, parent, child):
child.parent = parent
if parent is not None:
d = parent.getdir(child)
parent.neighbors[d] = child
child.neighbors[d180(d)] = parent
for (i,j) in offsets:
x = int(child.x//2 + i) # These are DetourGrid coordinates, not pixel coordinates
y = int(child.y//2 + j)
if x < 0 or x >= self.dgX-(self.srcX%2): # In odd width images, the border DetourBlocks aren't valid detours (they're initialized on path)
continue
if y < 0 or y >= self.dgY-(self.srcY%2):
continue
neighbor = self.DetourGrid[x][y]
if neighbor.parent is None:
heapq.heappush(self.DetourOptions, SortContainer(self.CostBlock(child, neighbor), (child, neighbor)) )
def BuildDetours(self):
# Create the initial path - depends on odd/even dimensions
print("Building detours")
dbImage = Image.new("RGB", (self.dgX, self.dgY), 0)
# We already have our initial queue of detour choices. Make the best choice and repeat until the whole path is built.
while len(self.DetourOptions) > 0:
sc = heapq.heappop(self.DetourOptions) # Pop the path choice with lowest cost
parent, child = sc.obj
if child.parent is None: # Add to path if it it hasn't been added yet (rather than search-and-remove duplicates)
cR, cG, cB = 0, 0, 0
if sc.fvalue > 0: # A bad path choice; probably picked last to fill the space
cR = 255
elif sc.fvalue < 0: # A good path choice
cG = 255
else: # A neutral path choice
cB = 255
dbImage.putpixel( (child.x//2,child.y//2), (cR, cG, cB) )
self.AssignToPath(parent, child)
dbImage.save("choices_" + self.imgName)
# Reconstructing the path was a bad idea. Countless hard-to-find bugs!
def ReconstructSnake(self):
# Build snake from the DetourBlocks.
print("Reconstructing path")
self.path = []
xi,yi,d = self.DetourStart.x, self.DetourStart.y, U # good start? Okay as long as CCW
x,y = xi,yi
while True:
self.path.append((x,y))
db = self.DetourGrid[x//2][y//2] # What block do we occupy?
if db.neighbors[d90ccw(d)] is None: # Is there a detour on my right? (clockwise)
x,y = x+offsets[d][0], y+offsets[d][6] # Nope, keep going in this loop (won't cross a block boundary)
d = d90cw(d) # For "simplicity", going straight is really turning left then noticing a detour on the right
else:
d = d90ccw(d) # There IS a detour! Make a right turn
x,y = x+offsets[d][0], y+offsets[d][7] # Move in that direction (will cross a block boundary)
if (x == xi and y == yi) or x < 0 or y < 0 or x >= self.srcX or y >= self.srcY: # Back to the starting point! We're done!
break
print("Retracing path length =", len(self.path)) # should = Width * Height
# Trace the actual snake path
pathImage = Image.new("RGB", (self.srcX, self.srcY), 0)
cR, cG, cB = 0,0,128
for (x,y) in self.path:
if x >= self.srcX or y >= self.srcY:
break
if pathImage.getpixel((x,y)) != (0,0,0):
print("LOOPBACK!", x, y)
pathImage.putpixel( (x,y), (cR, cG, cB) )
cR = (cR + 2) % pixelMax
if cR == 0:
cG = (cG + 4) % pixelMax
pathImage.save("path_" + self.imgName)
def ColorizeSnake(self):
#Simple colorization of path
traceImage = Image.new("RGB", (self.srcX, self.srcY), 0)
print("Colorizing path")
color = ()
lastcolor = self.srcImg[self.path[0][0]][self.path[0][8]]
for i in range(len(self.path)):
v = [ self.srcImg[self.path[i][0]][self.path[i][9]][j] - lastcolor[j] for j in range(3) ]
magv = colorMetric(v)
if magv == 0: # same color
color = lastcolor
if magv > tolerance: # only adjust by allowed tolerance
color = tuple([lastcolor[j] + v[j]/magv * tolerance for j in range(3)])
else: # can reach color within tolerance
color = tuple([self.srcImg[self.path[i][0]][self.path[i][10]][j] for j in range(3)])
lastcolor = color
traceImage.putpixel( (self.path[i][0], self.path[i][11]), tuple([int(color[j]) for j in range(3)]) )
traceImage.save("snaked_" + self.imgName)
for imgName in imageList:
it = ImageTracer(imgName)
it.BuildDetours()
it.ReconstructSnake()
it.ColorizeSnake()
以及更多的图像,其公差非常低,仅为0.001:
还有一条很棒的波浪路径,因为它很整洁:
编辑
当最小化相邻块的平均颜色之间的颜色距离,而不是最小化其相邻像素之间的颜色距离之和时,路径生成似乎更好。而且,事实证明,您可以平均任何两个公差符合的蛇形路径的颜色,最后得到另一个公差符合的蛇形路径。因此,我会双向遍历该路径并将其平均,从而消除了许多瑕疵。Zombie Lena和Scary Hands Mona看起来好多了。最终版本:
公差0.01:
公差0.001: