Нарисуй изображение со змеей


28

Представьте себе непрерывный двухмерный путь, который может поворачивать только влево, вправо или идти прямо, не может пересекаться сам и должен заполнять прямоугольную сетку, например сетку пикселей на изображении. Мы назовем этот путь змеей .

Пример змеи

Этот увеличенный пример показывает путь змеи в сетке 10 × 4, который начинается красным и увеличивается на 2% на каждом шаге, пока не станет фиолетовым. (Черные линии только для того, чтобы подчеркнуть направление, в котором они движутся.)

Цель

Цель этого конкурса популярности - написать алгоритм, который пытается воссоздать данное изображение, используя одну змею, цвет которой постоянно меняется на небольшие величины.

Ваша программа должна принимать изображение истинного цвета любого размера, а также значение с плавающей запятой от 0 до 1 включительно, допуск .

Допуск определяет максимальную величину, которую цвет змеи может изменять на каждом шаге в пикселях. Мы определим расстояние между двумя цветами RGB как евклидово расстояние между двумя точками RGB, когда они расположены на цветном кубе RGB . Расстояние будет нормализовано, поэтому максимальное расстояние равно 1, а минимальное расстояние равно 0.

Псевдокод цветового расстояния: (Предполагается, что все входные значения являются целыми числами в диапазоне [0, 255]; выход нормализован.)

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

Если результат вызова этой функции для текущего цвета змеи и другого цвета больше заданного допуска, то змея может не смениться на этот другой цвет.

Если вы предпочитаете, вы можете использовать другую функцию цветового расстояния. Это должно быть что-то точное и хорошо задокументированное, например, перечисленное на http://en.wikipedia.org/wiki/Color_difference . Вы также должны нормализовать его, чтобы оно было в пределах [0, 1], т. Е. Максимально возможное расстояние должно быть 1, а минимальное должно быть 0. Сообщите нам в своем ответе, если вы используете другую метрику расстояния.

Тестовые изображения

Вы, конечно, должны опубликовать свои выходные изображения (и даже анимацию растущей змеи, если хотите). Я предлагаю опубликовать множество этих изображений, используя разные низкие допуски (возможно, около 0,005 до 0,03).

мандрил Мона Лиза Великая Волна Лена случайные цвета, градиенты разное (Большая Великая Волна)

Критерии победы

Как уже говорилось, это конкурс популярности. Ответ, получивший наибольшее количество голосов, победит. Ответы, которые обеспечивают наиболее точное и эстетически приятное изображение «змеиной траектории» входных изображений, должны быть поставлены на голосование.

Любой пользователь, который, как установлено, злонамеренно представляет изображения, которые не являются настоящими змеями, будет дисквалифицирован навсегда.

Заметки

  • Можно использовать только один путь змеи, и он должен полностью заполнить изображение, не касаясь одного и того же пикселя дважды.
  • Змея может начинаться и заканчиваться где угодно на изображении.
  • Змея может начинаться как любой цвет.
  • Змея должна оставаться в границах изображения. Границы не являются циклическими.
  • Змея не может двигаться по диагонали или более чем на один пиксель за раз.

14
Серьезно, как вам удалось опубликовать 14 действительно достойных заданий (одно из которых сейчас третье из лучших в истории) за 16 дней, не выполняя ни одного из них? Слава Богу, PPCG нужно больше таких людей, как ты! ;)
Мартин Эндер

@ MartinBüttner Не уверен. Они просто приходят ко мне естественным образом :) Честно говоря, один вопрос, который я написал,
Увлечения Кэлвина

Я не уверен , является ли мое решение застревает в бесконечном цикле, или это просто принимая действительно, очень долго. И это только изображение 80x80!
Ручка двери

1
О, мой ... это выглядит действительно весело.
cjfaure

1
@belisarius Я не думаю, что это должно быть именно оригинальное изображение, настолько близко, насколько это возможно.
25

Ответы:


24

питон

Я генерирую динамический путь, чтобы минимизировать изменения цвета при перемещении змеи. Вот несколько изображений:

допуск = 0,01

Мона Лиза 0,01 толерантности Мандрил 0,01 допуск

Циклические цветовые контуры для вышеуказанных изображений (от синего к красному, становясь зеленее при повторении)

Змеиная тропинка Моны Лизы в циклических цветах Mandrill Snake Path в циклических цветах

Путь генерируется, начиная с некоторого начального пути, затем добавляя к нему циклы 2х2, пока изображение не будет заполнено. Преимущество этого метода заключается в том, что петли можно добавлять в любом месте пути, поэтому вы не можете нарисовать себя в углу и иметь больше свободы для построения желаемого пути. Я отслеживаю возможные петли, смежные с текущим путем, и сохраняю их в куче, взвешенной по изменению цвета вдоль петли. Затем я выскакиваю из цикла с наименьшим изменением цвета и добавляю его к пути, и повторяю, пока изображение не будет заполнено.

Я фактически отслеживаю только циклы ('DetourBlock' в коде), а затем восстанавливаю путь; это было ошибкой, так как есть некоторые особые случаи для нечетной ширины / высоты, и я потратил несколько часов на отладку метода реконструкции. Ну что ж.

Метрика генерации пути нуждается в настройке, и у меня также есть идея для лучшей окраски, но я подумал, что сначала это получу, так как она работает довольно хорошо. За исключением этого, который кажется лучше в некоторых фиксированных путях:

Разное Материал 0.01 Допуск

Вот код 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 :

Великая волна 0,001 допуск Мона Лиза 0,001 толерантность Лена 0,001 толерантность

А также путь великой волны, потому что он аккуратный:

введите описание изображения здесь

РЕДАКТИРОВАТЬ

Генерирование пути выглядит лучше при минимизации цветового расстояния между средними цветами соседних блоков, а не при минимизации суммы цветовых расстояний между соседними пикселями. Кроме того, оказывается, что вы можете усреднить цвета любых двух совместимых с допусками путей змей и в итоге получить еще один допуск-совместимый путь змей. Таким образом, я пересекаю путь в обоих направлениях и усредняю ​​их, что сглаживает множество артефактов. Зомби Лена и Страшные Руки Моны выглядят намного лучше. Окончательные версии:

Допуск 0,01 :

Финал Мона 0,01 Финал Лена 0.01

Финал Великой Волны 0.01

Допуск 0,001 :

Финал мона Финал лена

Последняя Великая Волна


4
Лучший еще! Мне нравится, как выглядит Великая волна!
Увлечения Кэлвина

Мне нравится, что ответ на этот вызов был сделан в Python хе
Альберт Реншоу

17

Джава

Моя программа генерирует путь змеи для заданной ширины и высоты, используя алгоритм, аналогичный тому, который генерирует кривую Гильберта.

введите описание изображения здесь

(маленькая игра: на картинке выше змея начинается в верхнем левом углу. Можете ли вы найти, где он заканчивается? Удачи :)

Вот результаты для различных значений допуска:

Допуск = 0,01

Допуск = 0,01

Допуск = 0,05

Допуск = 0,05

Допуск = 0,1

Допуск = 0,01

Допуск = 0,01

Волна

С блоками 4x4 пикселей и видимым путем

введите описание изображения здесь

Вычисление пути змеи

Путь змеи хранится в массиве целых чисел двойного измерения. Змея всегда входит в сетку в верхнем левом углу. Моя программа может выполнить 4 основных действия по заданному пути змеи:

  • создайте новый путь змеи для сетки шириной 1 или высотой 1. Путь - это простая линия, которая идет слева направо или вверх вниз, в зависимости от случая.

  • увеличьте высоту сетки, добавив сверху путь змеи слева направо, а затем отразив сетку (змея всегда должна входить в сетку в верхнем левом углу)

  • увеличить ширину сетки, добавив слева путь змеи сверху вниз, затем перевернув сетку (змея всегда должна входить в сетку в верхнем левом углу)

  • удвоить размерность сетки, используя алгоритм «гильбертова стиля» (см. описание ниже)

Используя серию этих атомарных операций, программа может генерировать путь змеи любого заданного размера.

Приведенный ниже код вычисляет (в обратном порядке), какие операции потребуются для получения заданной ширины и высоты. После вычисления действия выполняются одно за другим, пока мы не получим путь змеи ожидаемого размера.

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
    }

Удвоение размера пути змеи:

Алгоритм, который удваивает размер, работает следующим образом:

Рассмотрим этот узел, который связан с ПРАВО и ВНИЗ. Я хочу удвоить его размер.

 +-
 |

Есть 2 способа удвоить его размер и сохранить одинаковые выходы (справа и снизу):

 +-+- 
 |
 +-+
   |

или

+-+
| |
+ +-
|

Чтобы определить, какой из них выбрать, мне нужно обработать для каждого направления узла значение «смещения», указывающее, сдвинута ли выходная дверь влево / вправо или вверх / вниз. Я следую пути, как змея, и обновляю значение сдвига вдоль пути. Значение смещения однозначно определяет, какой расширенный блок мне нужно использовать для следующего шага.


3
+1 для кривой Гильберта. Это выглядит довольно естественно с этим, но если бы вы могли опубликовать свой код, это было бы хорошо.
Излин

@izlin Существует много кода - я постараюсь опубликовать некоторые части
Арно

1
@SuperChafouin Если в нем менее 30 тысяч символов, пожалуйста, напишите все. SE автоматически добавит полосу прокрутки.
Мартин Эндер

Немного переработаю мой код, который будет быстрым и грязным, и опубликую его :-)
Арно,

3
Я сдаюсь, где это заканчивается ?!
TMH

10

питон

Вот очень простой алгоритм для начала работы. Он начинается в верхнем левом углу изображения и вращается по часовой стрелке внутрь, делая цвет максимально приближенным к цвету следующего пикселя, оставаясь в пределах допуска.

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'

Запуск больших изображений занимает минуту или две, хотя я уверен, что спиральная логика может быть значительно оптимизирована.

Полученные результаты

Они интересные, но не великолепные. Удивительно, что допуск выше 0,1 дает довольно точные результаты.

Великая волна с допуском 0,03:

Великая волна на 0,03 толерантности

Мона Лиза с допуском 0,02:

Мона Лиза с допуском 0,02

Лена при допуске 0,03, затем 0,01, затем 0,005, затем 0,003:

Лена на 0,03 толерантности Лена с допуском 0,01 Лена с допуском 0,005 [Лена с допуском 0,003

Разное с допуском 0,1, затем 0,07, затем 0,04, затем 0,01:

Разное с допуском 0,1 Разное с допуском 0,07 Разное с допуском 0,04 Разное с допуском 0,01


13
Кажется законным писать змеиную программу на Python.
Арно

10

кобра

@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

Заполняет изображение змеей, как:

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

Это обеспечивает гораздо более быструю настройку цвета, чем просто линии в чередующихся направлениях, но не становится таким блочным, как в версии с шириной 3.

Даже при очень низких допусках края изображения все еще видны (хотя с потерей деталей в меньших разрешениях).

0,01

введите описание изображения здесь

0,1

введите описание изображения здесь

0,01

введите описание изображения здесь

0,01

введите описание изображения здесь

0,1

введите описание изображения здесь

0.03

введите описание изображения здесь

0,005

введите описание изображения здесь


1

C #

Змея начинается с верхнего левого пикселя с белым цветом и чередуется слева направо, а затем справа налево вниз по изображению.

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

        }
    }
}

Результирующая устойчивость изображения = 0,1

введите описание изображения здесь

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.