Imagine a continuous 2-dimensional path that can only turn left, right, or go straight, cannot intersect itself, and must fill a rectangular grid such as the grid of pixels in an image. We'll call this kind of path a snake.

Snake example

This enlarged example shows a snake path in a 10×4 grid that starts out red and increases in hue by about 2% at every step until it is purple. (The black lines are only to emphasize the direction it takes.)

Goal

The goal in this popularity contest is to write an algorithm that attempts to recreate a given image using a single snake whose color continuously changes by small amounts.

Your program must take in a true-color image of any size as well as a floating point value between 0 and 1 inclusive, the tolerance.

Tolerance defines the maximum amount the color of the snake is allowed to change in each pixel sized step. We'll define the distance between two RGB colors as the Euclidean distance between the two RGB points when arranged on an RGB color cube. The distance will then be normalized so the maximum distance is 1 and the minimum distance is 0.

Color distance pseudocode: (Assumes all input values are integers in the range [0, 255]; output is normalized.)

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

If the result of calling this function on the snake's current color and another color is greater than the given tolerance then the snake may not change into that other color.

If you prefer, you may use a different color distance function. It must be something accurate and well documented such as those listed at http://en.wikipedia.org/wiki/Color_difference. You also must normalize it to be in within [0, 1], i.e. the maximum possible distance must be 1 and the minimum must be 0. Tell us in your answer if you use a different distance metric.

Test Images

You should of course post your output images (and even animations of the snake growing if you want). I suggest posting a variety of these images using different low tolerances (maybe around 0.005 to 0.03).

Mandrill Mona Lisa The Great Wave Lena random colors, gradients misc (Larger Great Wave)

Win Criteria

As stated, this is a popularity contest. The highest voted answer will win. Answers that provide the most accurate and aesthetically pleasing "snake path" depiction of the input images should be voted up.

Any user who is found to be maliciously submitting images that are not actual snakes will be disqualified forever.

Notes

  • Only one snake path may be used and it must completely fill the image without touching the same pixel twice.
  • The snake may start and end anywhere in the image.
  • The snake may start as any color.
  • The snake must stay in the bounds of the image. The bounds are not cyclic.
  • The snake cannot move diagonally or more than one pixel at a time.
  • 14
    Seriously, how did you manage to post 14 really decent challenges (one of which is now the third best ever) in 16 days without ever sandboxing one of them? Big kudos, PPCG needs more people like you! ;) – Martin Ender Jul 23 '14 at 8:06
  • @MartinBüttner Not sure. They just come naturally to me :) To be fair the one question I did sandbox was not received too well: meta.codegolf.stackexchange.com/a/1820/26997 – Calvin's Hobbies Jul 23 '14 at 8:14
  • I'm not sure whether my solution is stuck in an infinite loop, or it's just taking a really, really long time. And it's only an 80x80 image! – Doorknob Jul 23 '14 at 12:11
  • 1
    Oh my...this looks REALLY fun. – cjfaure Jul 23 '14 at 19:39
  • 1
    @belisarius I don't think it's required to be exactly the original image, just as close a replica as possible. – Οurous Jul 24 '14 at 6:00
up vote 24 down vote accepted

Python

I generate a dynamic path to minimize the color changes as the snake travels. Here are some images:

tolerance = 0.01

Mona Lisa 0.01 tolerance Mandrill 0.01 tolerance

Cyclic color paths for the above images (blue to red, getting greener as it repeats):

Mona Lisa Snake Path in Cyclic Colors Mandrill Snake Path in Cyclic Colors

The path is generated by starting with some initial path, then adding 2x2 loops onto it until the image is filled. The advantage of this method is that the loops can be added anywhere on the path, so you can't paint yourself into a corner and have more freedom to build the path you want. I keep track of the possible loops adjacent to the current path and store them in a heap, weighted by the color change along the loop. I then pop off the loop with the least color change and add it to the path, and repeat until the image is filled.

I actually track the loops alone ('DetourBlock' in the code), then reconstruct the path; this was a mistake as there are some special cases for odd width/height and I spent several hours debugging the reconstruction method. Oh well.

The path generation metric needs tuning and I also have an idea for better colorization, but I thought I'd get this out first since it works quite well. Except for this one, which seems better in some of the fixed paths:

Misc Stuff 0.01 Tolerance

Here's the Python code, with apologies for my atrocious coding habits:

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

And some more images at a very low tolerance of 0.001:

Great Wave 0.001 tolerance Mona Lisa 0.001 tolerance Lena 0.001 tolerance

And also the great wave path because it's neat:

enter image description here

EDIT

The path generation seems better when minimizing the color distance between the average colors of adjacent blocks, rather than minimizing the sum of the color distances between their adjacent pixels. Also, it turns out you can average the colors of any two tolerance-compliant snake paths and end up with another tolerance-compliant snake path. So I traverse the path both ways and average them, which smooths out a lot of the artifacts. Zombie Lena and Scary Hands Mona look much better. Final versions:

Tolerance 0.01:

Final Mona 0.01 Final Lena 0.01

Final Great Wave 0.01

Tolerance 0.001:

Final Mona Final Lena

Final Great Wave

  • 4
    Best one yet! I love how the Great Wave looks! – Calvin's Hobbies Aug 9 '14 at 4:26
  • I like the the answer to this challenge was made in python heh – Albert Renshaw Feb 14 '17 at 8:11

Java

My program generate a snake path for a given width & height, using an algorithm similar to the one that generates the Hilbert curve.

enter image description here

(little game: in the above picture, the snake starts at the top left corner. Can you find where he ends? Good luck :)

Here are the results for various tolerance values :

Tolerance = 0.01

tolerance=0.01

Tolerance = 0.05

tolerance=0.05

Tolerance = 0.1

tolerance=0.01

Tolerance = 0.01

Wave

With 4x4 pixel blocks & the path visible

enter image description here

Computing the snake path

A snake path is stored in a double dimension integer array. The snake always enters the grid by the upper left corner. There are 4 basic operation my program can do on a given snake path:

  • create a new snake path for a grid of width 1 or height 1. The path is just a simple line that goes left to right or up to down, depending on the case.

  • increase the grid height, by adding at the top a snake path from left to right, then by mirroring the grid (the snake must always enter the grid by the upper left corner)

  • increate the grid width, by adding at the left a snake path from top to bottom, then by flipping the grid (the snake must always enter the grid by the upper left corner)

  • double the dimension of the grid using an "Hilbert style" algorithm (see description below)

Using a series of these atomic operations, the program is able to generate a snake path of any given size.

The code below computes (in reverse order) which operations will be needed to obtain a given width & height. Once computed, the actions are executed one by one until we got a snake path of the expected size.

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
    }

Doubling the snake path size:

The algorithm that double the size works as follow:

Consider this node which is linked to RIGHT and BOTTOM. I want to double its size.

 +-
 |

There are 2 ways to double its size and keep the same exits (right and bottom):

 +-+- 
 |
 +-+
   |

or

+-+
| |
+ +-
|

To determine which one to choose, I need to handle for each node direction a "shift" value, indicating if the exit door is shifted left/right or up/down. I follow the path as the snake would do, and update the shift value along the path. The shift value determines uniquely which expanded block I need to use for the next step.

  • 3
    +1 for the Hilbert curve. It looks pretty natural with this one but if you could post your code it would be nice. – izlin Jul 24 '14 at 8:17
  • @izlin There is a lot of code - I'll try to post some parts – Super Chafouin Jul 24 '14 at 8:24
  • 1
    @SuperChafouin If it's less than 30k characters, please post all of it. SE will add a scrollbar automatically. – Martin Ender Jul 24 '14 at 8:28
  • Will rework a little bit my code that is quick and dirty and post it :-) – Super Chafouin Jul 24 '14 at 8:37
  • 3
    I give up, where does it end?! – TMH Jul 25 '14 at 13:32

Python

Here's a very simple algorithm to get things started. It starts at the top left of the image and spirals clockwise inwards, making the color as close as possible to the next pixel's color while staying within the tolerance.

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'

It takes a minute or two to run the larger images, though I'm sure the spiraling logic could be vastly optimized.

Results

They're interesting but not gorgeous. Amazingly a tolerance above 0.1 produces quite accurate looking results.

The Great Wave at 0.03 tolerance:

The Great Wave at 0.03 tolerance

Mona Lisa at 0.02 tolerance:

Mona Lisa at 0.02 tolerance

Lena at 0.03 tolerance, then 0.01, then 0.005, then 0.003:

Lena at 0.03 tolerance Lena at 0.01 tolerance Lena at 0.005 tolerance [Lena at 0.003 tolerance

Misc stuff at 0.1 tolerance, then 0.07, then 0.04, then 0.01:

Misc stuff at 0.1 tolerance Misc stuff at 0.07 tolerance Misc stuff at 0.04 tolerance Misc stuff at 0.01 tolerance

  • 13
    Seems legitimate to write a snake program with Python. – Super Chafouin Jul 24 '14 at 9:59

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

Fills the image with a snake like:

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

This allows for much faster color adjustment than just lines in alternating directions, but doesn't become as blocky as a 3-wide version does.

Even at very low tolerances, the edges of an image are still visible (although at the loss of detail in smaller resolutions).

0.01

enter image description here

0.1

enter image description here

0.01

enter image description here

0.01

enter image description here

0.1

enter image description here

0.03

enter image description here

0.005

enter image description here

C#

Snake starts at top left pixel with colour white and alternates left to right and then right to left down the image.

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);
            }

        }
    }
}

Result image tolerance = 0.1

enter image description here

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.