Круглые картины из натянутых ниток. Разоблачение

Недавно в новостях наткнулся на любопытную заметку «Москвичка создает невероятные картины из ниток с помощью алгоритма » (https://news.mail.ru/society/38107680/), на круглом ткацком станке нитки натягивались таким образом, чтобы получился рисунок:

И в России могут что-то создать — подумал я, не подозревая, что девушка-программист из Москвы привирает о авторстве своей программы.

Сомнения закрались когда Яндекс.директ реклама стала навязчиво предлагать мне такой же портрет с сайта артпаутина.ру

Круглые картины из ниток поставлены на потоковое производство, но цены кусаются.

Ого, ещё один «автор» круглых картин в России? — Странно.

Буквально 20 минут поиска и секрет открыт, картины рисует алгоритм программы, а наматывает ЧПУ станок, при этом исходный код программы для «рисования» свободно распространяется в сети интернет (ай-ай, артнитка, нехорошо присваивать чужие программы). Если дочитаете заметку до конца, программа вам в подарок 🙂

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

Первые работы приходилось наматывать вручную по подсказкам с компьютера

Основная идея состоит из трех этапов. Сначала выполняется небольшая предварительная обработка изображения, оно обрезается в квадрат, вписывается в круг и инвертируется в негатив. Затем следует обработка, реализованная на python, алгоритм определяет где следует разместить провести линию, чтобы получить лучшее представление изображения. Программа перебирает все возможные варианты линии из одной точки ко всем оставшимся точкам и находит область наибольшего затенения. Третий этап — генерирование кодов управления ЧПУ станком для автоматического натягивания нити по координатам.

Как создавать картины из ниток?

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

Исходник программы для рисования

Чтобы произвести обработку изображений на пиксельном уровне, мы используем openCV. Давайте начнем и сделаем некоторую начальную обработку изображения. Фрагмент ниже показывает, как можно загрузить, обрезать и изменить размер изображения.

import cv2
import numpy as np

# Invert grayscale image
def invertImage(image):
    return (255-image)

# Apply circular mask to image
def maskImage(image, radius):
    y, x = np.ogrid[-radius:radius + 1, -radius:radius + 1]
    mask = x**2 + y**2 > radius**2
    image[mask] = 0

    return image

# Load image
image = cv2.imread(imgPath)

# Crop image
height, width = image.shape[0:2]
minEdge= min(height, width)
topEdge = int((height - minEdge)/2)
leftEdge = int((width - minEdge)/2)
imgCropped = image[topEdge:topEdge+minEdge, leftEdge:leftEdge+minEdge]

# Convert to grayscale
imgGray = cv2.cvtColor(imgCropped, cv2.COLOR_BGR2GRAY)

# Resize image
imgSized = cv2.resize(imgGray, (2*imgRadius + 1, 2*imgRadius + 1)) 

# Invert image
imgInverted = invertImage(imgSized)

# Mask image
imgMasked = maskImage(imgInverted, imgRadius)

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

Алгоритм натягивания нитей

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

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

def pinCoords(radius, nPins=200, offset=0, x0=None, y0=None):
    alpha = np.linspace(0 + offset, 2*np.pi + offset, nPins + 1)

    if (x0 == None) or (y0 == None):
        x0 = radius + 1
        y0 = radius + 1

    coords = []
    for angle in alpha[0:-1]:
        x = int(x0 + radius*np.cos(angle))
        y = int(y0 + radius*np.sin(angle))

        coords.append((y, x))
    return coords

def linePixels(pin0, pin1):
    length = int(np.hypot(pin1[0] - pin0[0], pin1[1] - pin0[1]))

    x = np.linspace(pin0[1], pin1[1], length)
    y = np.linspace(pin0[0], pin1[0], length)

    return (x.astype(np.int)-1, y.astype(np.int)-1)

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

coords = pinCoords(imgRadius, nPins)
height, width = imgMasked.shape[0:1]

i = 0
lines = []
previousPins = []
oldPin = initPin
lineMask = np.zeros((height, width))

for line in range(nLines):
    i += 1
    bestLine = 0
    oldCoord = coords[oldPin]

     for index in range(1, nPins):
        pin = (oldPin + index) % nPins

        coord = coords[pin]
        xLine, yLine = linePixels(oldCoord, coord)

           lineSum = np.sum(imgMasked[yLine, xLine])

        if (lineSum > bestLine) and not(pin in previousPins):
            bestLine = lineSum
            bestPin = pin

    if len(previousPins) >= minLoop:
        previousPins.pop(1)
    previousPins.append(bestPin)

    lineMask = lineMask * 1
    cv2.line(lineMask, oldCoord, coords[bestPin], lineWeight, lineWidth)
    imgMasked = np.subtract(imgMasked, lineMask)

    lines.append((oldPin, bestPin))

     if bestPin == oldPin:
        break

    oldPin = bestPin

Результат работы алгоритма

Станок для автоматического изготовления картин.

На этом видеоролике видно работу ЧПУ станка, который натягивает нитку по генерации описанного выше алгоритма рисования, устройство станка очень простое, он собран из доступных комплектующих с AliExpress и может быть собран дома за день:

самодельный ЧПУ станок натягивает нить на ткацкий станок

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