Desenhar uma imagem com uma cobra

28

Imagine um caminho bidimensional contínuo que só pode virar à esquerda, direita ou seguir em frente, não pode se cruzar e deve preencher uma grade retangular, como a grade de pixels em uma imagem. Vamos chamar esse tipo de caminho de cobra .

Exemplo de cobra

Este exemplo ampliado mostra um caminho de cobra em uma grade 10 × 4 que começa em vermelho e aumenta em matiz em cerca de 2% a cada passo até ficar roxo. (As linhas pretas servem apenas para enfatizar a direção que toma.)

Objetivo

O objetivo deste concurso de popularidade é escrever um algoritmo que tente recriar uma determinada imagem usando uma única cobra cuja cor muda continuamente em pequenas quantidades.

Seu programa deve ter uma imagem em cores reais de qualquer tamanho, bem como um valor de ponto flutuante entre 0 e 1, inclusive a tolerância .

A tolerância define a quantidade máxima que a cor da cobra pode mudar em cada etapa do tamanho de pixel. Definiremos a distância entre duas cores RGB como a distância euclidiana entre os dois pontos RGB quando organizados em um cubo de cores RGB . A distância será então normalizada, de modo que a distância máxima seja 1 e a distância mínima seja 0.

Pseudocódigo da distância da cor: (assume que todos os valores de entrada são números inteiros no intervalo [0, 255]; a saída é normalizada).

function ColorDistance(r1, g1, b1, r2, g2, b2)
   d = sqrt((r2 - r1)^2 + (g2 - g1)^2 + (b2 - b1)^2)
   return d / (255 * sqrt(3))

Se o resultado de chamar essa função na cor atual da cobra e outra cor for maior que a tolerância fornecida, a cobra não poderá mudar para essa outra cor.

Se preferir, você pode usar uma função de distância de cor diferente. Deve ser algo preciso e bem documentado, como os listados em http://en.wikipedia.org/wiki/Color_difference . Você também deve normalizá-lo para estar dentro [0, 1], ou seja, a distância máxima possível deve ser 1 e a mínima deve ser 0. Informe-nos em sua resposta se você usar uma métrica de distância diferente.

Imagens de teste

Obviamente, você deve postar suas imagens de saída (e até animações da cobra crescendo, se quiser). Sugiro postar uma variedade dessas imagens usando diferentes tolerâncias baixas (talvez em torno de 0,005 a 0,03).

Mandrill Monalisa A Grande Onda Lena cores aleatórias, gradientes diversos (Grande onda maior)

Critérios de vitória

Como afirmado, este é um concurso de popularidade. A resposta mais votada ganhará. As respostas que fornecem a representação "caminho da cobra" mais precisa e esteticamente agradável das imagens de entrada devem ser votadas para cima.

Qualquer usuário que esteja enviando imagens maliciosamente que não são cobras reais será desqualificado para sempre.

Notas

  • Apenas um caminho de cobra pode ser usado e ele deve preencher completamente a imagem sem tocar no mesmo pixel duas vezes.
  • A cobra pode começar e terminar em qualquer lugar da imagem.
  • A cobra pode começar como qualquer cor.
  • A cobra deve permanecer nos limites da imagem. Os limites não são cíclicos.
  • A cobra não pode se mover na diagonal ou mais de um pixel de cada vez.
Passatempos de Calvin
fonte
14
Sério, como você conseguiu postar 14 desafios realmente decentes (um dos quais agora é o terceiro melhor de todos os tempos) em 16 dias sem colocar um deles no sandbox? Muitos elogios, o PPCG precisa de mais pessoas como você! ;)
Martin Ender
@ MartinBüttner Não tenho certeza. Eles só vem naturalmente para mim :) Para ser justo a uma pergunta que eu tinha sandbox não foi recebido muito bem: meta.codegolf.stackexchange.com/a/1820/26997
de Calvino Hobbies
Eu não tenho certeza se a minha solução é preso em um loop infinito, ou é só tomar um realmente, realmente muito tempo. E é apenas uma imagem de 80x80!
Maçaneta
1
Oh meu ... isso parece realmente divertido.
Cjfaure
1
@belisarius Eu não acho que seja necessário ter exatamente a imagem original, o mais próximo possível de uma réplica.
Οurous

Respostas:

24

Python

Eu gero um caminho dinâmico para minimizar as mudanças de cor à medida que a cobra viaja. Aqui estão algumas imagens:

tolerância = 0,01

Mona Lisa tolerância 0,01 Tolerância de Mandrill 0,01

Caminhos de cores cíclicas para as imagens acima (azul para vermelho, ficando mais verde à medida que se repete):

Caminho de cobra Mona Lisa em cores cíclicas Caminho de cobra Mandrill em cores cíclicas

O caminho é gerado iniciando com algum caminho inicial e adicionando loops 2x2 até a imagem ser preenchida. A vantagem desse método é que os loops podem ser adicionados em qualquer lugar do caminho, para que você não possa se pintar em um canto e tenha mais liberdade para criar o caminho que deseja. Acompanho os possíveis loops adjacentes ao caminho atual e os armazeno em uma pilha, ponderada pela mudança de cor ao longo do loop. Então, saio do loop com o mínimo de mudança de cor, adiciono-o ao caminho e repito até que a imagem seja preenchida.

Na verdade, eu acompanho os loops sozinhos ('DetourBlock' no código) e depois reconstruo o caminho; isso foi um erro, pois existem alguns casos especiais para largura / altura ímpar e passei várias horas depurando o método de reconstrução. Ah bem.

A métrica de geração de caminho precisa ser ajustada e eu também tenho uma idéia para uma melhor coloração, mas achei que seria o primeiro, pois funciona muito bem. Exceto por este, que parece melhor em alguns dos caminhos fixos:

Miscelânea 0,01 Tolerância

Aqui está o código Python, com desculpas pelos meus hábitos de codificação atrozes:

# 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()

E mais algumas imagens com uma tolerância muito baixa de 0,001 :

Tolerância 0.001 da Great Wave Mona Lisa 0,001 tolerância Lena 0,001 tolerância

E também o ótimo caminho das ondas, porque é elegante:

insira a descrição da imagem aqui

EDITAR

A geração do caminho parece melhor ao minimizar a distância das cores entre as cores médias dos blocos adjacentes, em vez de minimizar a soma das distâncias das cores entre os pixels adjacentes. Além disso, verifica-se que você pode calcular a média das cores de quaisquer dois caminhos de cobra compatíveis com tolerância e terminar com outro caminho de cobra compatível com tolerância. Então, eu atravesso o caminho nos dois sentidos e os medio, o que suaviza muitos artefatos. Zombie Lena e Scary Hands Mona parecem muito melhores. Versões finais:

Tolerância 0,01 :

Mona final 0,01 Final Lena 0.01

Grande onda final 0,01

Tolerância 0.001 :

Final Mona Final Lena

Grande onda final

adipy
fonte
4
Melhor ainda! Eu amo a aparência da Great Wave!
Hobbies de Calvin
Eu gosto do que a resposta a este desafio foi feito em python heh
Albert Renshaw
17

Java

Meu programa gera um caminho de cobra para uma determinada largura e altura, usando um algoritmo semelhante ao que gera a curva de Hilbert.

insira a descrição da imagem aqui

(joguinho: na foto acima, a cobra começa no canto superior esquerdo. Você consegue encontrar onde ele termina? Boa sorte :)

Aqui estão os resultados para vários valores de tolerância:

Tolerância = 0,01

tolerância = 0,01

Tolerância = 0,05

tolerância = 0,05

Tolerância = 0,1

tolerância = 0,01

Tolerância = 0,01

Onda

Com blocos de pixel 4x4 e o caminho visível

insira a descrição da imagem aqui

Computando o caminho da cobra

Um caminho de cobra é armazenado em uma matriz inteira de dupla dimensão. A cobra sempre entra na grade pelo canto superior esquerdo. Existem 4 operações básicas que meu programa pode executar em um determinado caminho de cobra:

  • crie um novo caminho de cobra para uma grade de largura 1 ou altura 1. O caminho é apenas uma linha simples que vai da esquerda para a direita ou para cima e para baixo, dependendo do caso.

  • aumente a altura da grade, adicionando no topo um caminho de cobra da esquerda para a direita e espelhando a grade (a cobra deve sempre entrar na grade pelo canto superior esquerdo)

  • aumente a largura da grade, adicionando à esquerda um caminho de cobra de cima para baixo e invertendo a grade (a cobra deve sempre entrar na grade pelo canto superior esquerdo)

  • dobrar a dimensão da grade usando o algoritmo "estilo Hilbert" (veja a descrição abaixo)

Usando uma série dessas operações atômicas, o programa é capaz de gerar um caminho de cobra de qualquer tamanho.

O código abaixo calcula (em ordem inversa) quais operações serão necessárias para obter uma determinada largura e altura. Uma vez calculadas, as ações são executadas uma a uma até obtermos um caminho de cobra do tamanho esperado.

enum Action { ADD_LINE_TOP, ADD_LINE_LEFT, DOUBLE_SIZE, CREATE};

public static int [][] build(int width, int height) {
    List<Action> actions = new ArrayList<Action>();
    while (height>1 && width>1) {
        if (height % 2 == 1) {
            height--;
            actions.add(Action.ADD_LINE_TOP);
        }
        if (width % 2 == 1) {
            width--;                
            actions.add(Action.ADD_LINE_LEFT);
        }
        if (height%2 == 0 && width%2 == 0) {
            actions.add(Action.DOUBLE_SIZE);
            height /= 2;
            width /= 2;
        }
    }
    actions.add(Action.CREATE);
    Collections.reverse(actions);
    int [][] tab = null;
    for (Action action : actions) {
        // do the stuff
    }

Dobrar o tamanho do caminho da cobra:

O algoritmo que dobra o tamanho funciona da seguinte maneira:

Considere este nó que está vinculado a RIGHT e BOTTOM. Eu quero dobrar de tamanho.

 +-
 |

Existem duas maneiras de dobrar de tamanho e manter as mesmas saídas (direita e inferior):

 +-+- 
 |
 +-+
   |

ou

+-+
| |
+ +-
|

Para determinar qual escolher, eu preciso manipular para cada direção do nó um valor "shift", indicando se a porta de saída é deslocada para a esquerda / direita ou para cima / para baixo. Sigo o caminho como a cobra faria e atualizo o valor do turno ao longo do caminho. O valor do turno determina exclusivamente qual bloco expandido eu preciso usar para a próxima etapa.

Arnaud
fonte
3
+1 para a curva de Hilbert. Parece bastante natural com este, mas se você pudesse postar seu código, seria bom.
Izlin
@izlin Há um monte de código - Vou tentar postar algumas partes
Arnaud
1
@SuperChafouin Se tiver menos de 30 mil caracteres, poste tudo. O SE adicionará uma barra de rolagem automaticamente.
Martin Ender
Vai refazer um pouco meu código que é rápido e sujo e postá-lo :-)
Arnaud
3
Eu desisto, onde isso acaba ?!
TMH
10

Python

Aqui está um algoritmo muito simples para começar. Ele começa no canto superior esquerdo da imagem e espirala no sentido horário para dentro, tornando a cor o mais próximo possível da cor do próximo pixel, mantendo-se dentro da tolerância.

import Image

def colorDist(c1, c2): #not normalized
    return (sum((c2[i] - c1[i])**2 for i in range(3)))**0.5

def closestColor(current, goal, tolerance):
    tolerance *= 255 * 3**0.5
    d = colorDist(current, goal)
    if d > tolerance: #return closest color in range
        #due to float rounding this may be slightly outside of tolerance range
        return tuple(int(current[i] + tolerance * (goal[i] - current[i]) / d) for i in range(3))
    else:
        return goal

imgName = 'lena.png'
tolerance = 0.03

print 'Starting %s at %.03f tolerance.' % (imgName, tolerance)

img = Image.open(imgName).convert('RGB')

imgData = img.load()
out = Image.new('RGB', img.size)
outData = out.load()

x = y = 0
c = imgData[x, y]
traversed = []
state = 'right'

updateStep = 1000

while len(traversed) < img.size[0] * img.size[1]:
    if len(traversed) > updateStep and len(traversed) % updateStep == 0:
        print '%.02f%% complete' % (100 * len(traversed) / float(img.size[0] * img.size[1]))
    outData[x, y] = c
    traversed.append((x, y))
    oldX, oldY = x, y
    oldState = state
    if state == 'right':
        if x + 1 >= img.size[0] or (x + 1, y) in traversed:
            state = 'down'
            y += 1
        else:
            x += 1
    elif state == 'down':
        if y + 1 >= img.size[1] or (x, y + 1) in traversed:
            state = 'left'
            x -= 1
        else:
            y += 1
    elif state == 'left':
        if x - 1 < 0 or (x - 1, y) in traversed:
            state = 'up'
            y -= 1
        else:
            x -= 1
    elif state == 'up':
        if y - 1 < 0 or (x, y - 1) in traversed:
            state = 'right'
            x += 1
        else:
             y -= 1
    c = closestColor(c, imgData[x, y], tolerance)

out.save('%.03f%s' % (tolerance, imgName))
print '100% complete'

Demora um ou dois minutos para executar as imagens maiores, embora eu tenha certeza de que a lógica em espiral poderia ser amplamente otimizada.

Resultados

Eles são interessantes, mas não lindos. Surpreendentemente, uma tolerância acima de 0,1 produz resultados de aparência bastante precisos.

A Grande Onda com tolerância de 0,03:

A Grande Onda com tolerância de 0,03

Mona Lisa com tolerância de 0,02:

Mona Lisa com tolerância 0,02

Lena com tolerância de 0,03, 0,01, 0,005 e 0,003:

Lena com tolerância de 0,03 Lena com tolerância de 0,01 Lena com tolerância 0,005 [Lena com tolerância 0,003

Coisas diversas com tolerância de 0,1, 0,07, 0,04 e 0,01:

Material diverso com tolerância de 0,1 Coisas diversas com tolerância de 0,07 Coisas diversas com tolerância de 0,04 Material diverso com tolerância de 0,01

Passatempos de Calvin
fonte
13
Parece legítimo escrever um programa de cobra com Python.
Arnaud
10

Cobra

@number float
use System.Drawing
class Program
    var source as Bitmap?
    var data as List<of uint8[]> = List<of uint8[]>()
    var canvas as List<of uint8[]> = List<of uint8[]>()
    var moves as int[] = @[0,1]
    var direction as bool = true
    var position as int[] = int[](0)
    var tolerance as float = 0f
    var color as uint8[] = uint8[](4)
    var rotated as bool = false
    var progress as int = 0
    def main
        args = CobraCore.commandLineArgs
        if args.count <> 3, throw Exception()
        .tolerance = float.parse(args[1])
        if .tolerance < 0 or .tolerance > 1, throw Exception()
        .source = Bitmap(args[2])
        .data = .toData(.source to !)
        .canvas = List<of uint8[]>()
        average = float[](4)
        for i in .data
            .canvas.add(uint8[](4))
            for n in 4, average[n] += i[n]/.source.height
        for n in 4, .color[n] = (average[n]/.source.width).round to uint8
        if .source.width % 2
            if .source.height % 2
                .position = @[0, .source.height-1]
                .update
                while .position[1] > 0, .up
                .right
            else
                .position = @[.source.width-1, .source.height-1]
                .update
                while .position[1] > 0, .up
                while .position[0] > 0, .left
                .down
        else
            if .source.height % 2
                .position = @[0,0]
                .update
            else
                .position = @[.source.width-1,0]
                .update
                while .position[0] > 0, .left
                .down
        .right
        .down
        while true
            if (1-.source.height%2)<.position[1]<.source.height-1
                if .moves[1]%2==0
                    if .direction, .down
                    else, .up
                else
                    if .moves[0]==2, .right
                    else, .left
            else
                .right
                if .progress == .data.count, break
                .right
                .right
                if .direction
                    .direction = false
                    .up
                else
                    .direction = true
                    .down
        image = .toBitmap(.canvas, .source.width, .source.height)
        if .rotated, image.rotateFlip(RotateFlipType.Rotate270FlipNone)
        image.save(args[2].split('.')[0]+'_snake.png')

    def right
        .position[0] += 1
        .moves = @[.moves[1], 0]
        .update

    def left
        .position[0] -= 1
        .moves = @[.moves[1], 2]
        .update

    def down
        .position[1] += 1
        .moves = @[.moves[1], 1]
        .update

    def up
        .position[1] -= 1
        .moves = @[.moves[1], 3]
        .update

    def update
        .progress += 1
        index = .position[0]+.position[1]*(.source.width)
        .canvas[index] = .closest(.color,.data[index])
        .color = .canvas[index]

    def closest(color1 as uint8[], color2 as uint8[]) as uint8[]
        d = .difference(color1, color2)
        if d > .tolerance
            output = uint8[](4)
            for i in 4, output[i] = (color1[i] + .tolerance * (color2[i] - _
            color1[i]) / d)to uint8
            return output
        else, return color2

    def difference(color1 as uint8[], color2 as uint8[]) as float
        d = ((color2[0]-color1[0])*(color2[0]-color1[0])+(color2[1]- _
        color1[1])*(color2[1]-color1[1])+(color2[2]-color1[2])*(color2[2]- _
        color1[2])+0f).sqrt
        return d / (255 * 3f.sqrt)

    def toData(image as Bitmap) as List<of uint8[]>
        rectangle = Rectangle(0, 0, image.width, image.height)
        data = image.lockBits(rectangle, System.Drawing.Imaging.ImageLockMode.ReadOnly, _
        image.pixelFormat) to !
        ptr = data.scan0
        bytes = uint8[](data.stride*image.height)
        System.Runtime.InteropServices.Marshal.copy(ptr, bytes, 0, _
        data.stride*image.height)
        pfs = Image.getPixelFormatSize(data.pixelFormat)//8
        pixels = List<of uint8[]>()
        for y in image.height, for x in image.width
            position = (y * data.stride) + (x * pfs)
            red, green, blue, alpha = bytes[position+2], bytes[position+1], _
            bytes[position], if(pfs==4, bytes[position+3], 255u8)
            pixels.add(@[red, green, blue, alpha])
        image.unlockBits(data)
        return pixels

    def toBitmap(pixels as List<of uint8[]>, width as int, height as int) as Bitmap
        image = Bitmap(width, height, Imaging.PixelFormat.Format32bppArgb)
        rectangle = Rectangle(0, 0, image.width, image.height)
        data = image.lockBits(rectangle, System.Drawing.Imaging.ImageLockMode.ReadWrite, _
        image.pixelFormat) to !
        ptr = data.scan0
        bytes = uint8[](data.stride*image.height)
        pfs = System.Drawing.Image.getPixelFormatSize(image.pixelFormat)//8
        System.Runtime.InteropServices.Marshal.copy(ptr, bytes, 0, _
        data.stride*image.height)
        count = -1
        for y in image.height, for x in image.width 
            pos = (y*data.stride)+(x*pfs)
            bytes[pos+2], bytes[pos+1], bytes[pos], bytes[pos+3] = pixels[count+=1]
        System.Runtime.InteropServices.Marshal.copy(bytes, 0, ptr, _
        data.stride*image.height)
        image.unlockBits(data)
        return image

Preenche a imagem com uma cobra como:

#--#
   |
#--#
|
#--#
   |

Isso permite um ajuste de cor muito mais rápido do que apenas linhas em direções alternadas, mas não se torna tão irregular quanto uma versão em três larguras.

Mesmo com tolerâncias muito baixas, as bordas de uma imagem ainda são visíveis (embora com a perda de detalhes em resoluções menores).

0,01

insira a descrição da imagem aqui

0,1

insira a descrição da imagem aqui

0,01

insira a descrição da imagem aqui

0,01

insira a descrição da imagem aqui

0,1

insira a descrição da imagem aqui

0,03

insira a descrição da imagem aqui

0,005

insira a descrição da imagem aqui

Furioso
fonte
1

C #

O Snake começa no pixel superior esquerdo com a cor branca e alterna da esquerda para a direita e depois da direita para a esquerda na imagem.

using System;
using System.Drawing;

namespace snake
{
    class Snake
    {
        static void MakeSnake(Image original, double tolerance)
        {
            Color snakeColor = Color.FromArgb(255, 255, 255);//start white
            Bitmap bmp = (Bitmap)original;
            int w = bmp.Width;
            int h = bmp.Height;
            Bitmap snake = new Bitmap(w, h);

            //even rows snake run left to right else run right to left
            for (int y = 0; y < h; y++)
            {
                if (y % 2 == 0)
                {
                    for (int x = 0; x < w; x++)//L to R
                    {
                        Color pix = bmp.GetPixel(x, y);
                        double diff = Snake.RGB_Distance(snakeColor, pix);
                        if (diff < tolerance)
                        {
                            snakeColor = pix;
                        }
                        //else keep current color
                        snake.SetPixel(x, y, snakeColor);
                    }
                }
                else
                {
                    for (int x = w - 1; x >= 0; x--)//R to L
                    {
                        Color pix = bmp.GetPixel(x, y);
                        double diff = Snake.RGB_Distance(snakeColor, pix);
                        if (diff < tolerance)
                        {
                            snakeColor = pix;
                        }
                        //else keep current color
                        snake.SetPixel(x, y, snakeColor);
                    }
                }
            }

            snake.Save("snake.png");
        }

        static double RGB_Distance(Color current, Color next)
        {
            int dr = current.R - next.R;
            int db = current.B - next.B;
            int dg = current.G - next.G;
            double d = Math.Pow(dr, 2) + Math.Pow(db, 2) + Math.Pow(dg, 2);
            d = Math.Sqrt(d) / (255 * Math.Sqrt(3));
            return d;
        }

        static void Main(string[] args)
        {
            try
            {
                string file = "input.png";
                Image img = Image.FromFile(file);
                double tolerance = 0.03F;
                Snake.MakeSnake(img, tolerance);
                Console.WriteLine("Complete");
            }
            catch(Exception e)
            {
                Console.WriteLine(e.Message);
            }

        }
    }
}

Tolerância da imagem resultante = 0,1

insira a descrição da imagem aqui

bacchusbeale
fonte