Техника изготовления картин из ниток (String Art) на OpenCV

портрет философа Иммануила Канта, выполненный из ниток
На региональном новостном сайте увидел новость: "Янтарному подарили портрет Канта из 5 километров ниток" и заинтересовался — как же подобные картины делаются.

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


, так и автоматически — при помощи самых разных станков:





Осталось разобраться — как же это делается программно.
По счастливому стечению обстоятельств на глаза попалась статья: Круглые картины из натянутых ниток. Разоблачение, которая и подсказала, что в основе лежит простой алгоритм, состоящий из следующих основных этапов:
1. предварительная обработка изображения: изображение переводится в градации серого, обрезается в квадрат, вписывается в круг и инвертируется в негатив.
2. процедура определения, где нужно провести линию: алгоритм в цикле перебирает все варианты проведения линии от одной точки ко всем остальны точкам, чтобы найти такой вариант, который даёт наиболее подходящее затенение.
3. генерация кодов управления станком ЧПУ: зная какие линии нужно провести для данного изображения — генерируются соответствующие команды для станка, который будет выполнять автоматическое натягивания нити по заданным координатам.

К сожалению, код который приводится в статье сразу не заработал, зато на гитхабе быстро нашёлся готовый вариант на Python и OpenCV: string-art/alog2.py

Всё реализовано довольно просто:

import cv2
import numpy as np

imgPath = './cat4.jpg' # My Cheshire Cat

# Parameters
imgRadius = 200     # Number of pixels that the image radius is resized to

initPin = 0         # Initial pin to start threading from
numPins = 200       # Number of pins on the circular loom
numLines = 500      # Maximal number of lines

minLoop = 3         # Disallow loops of less than minLoop lines
lineWidth = 3       # The number of pixels that represents the width of a thread
lineWeight = 15     # The weight a single thread has in terms of "darkness"

— подключаются нужные библиотеки и задаются соответствующие параметры, включая имя изображения, подлежащего преобразованию:
Чеширский кот

Задаются вспомогательные функции, которые реализуют предварительную обработку изображения — обрезки до круга и инвертирования:

# 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

# Compute coordinates of loom pins
def pinCoords(radius, numPins=200, offset=0, x0=None, y0=None):
    alpha = np.linspace(0 + offset, 2*np.pi + offset, numPins + 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((x, y))
    return coords

# Compute a line mask
def linePixels(pin0, pin1):
    length = int(np.hypot(pin1[0] - pin0[0], pin1[1] - pin0[1]))

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

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

А так же функции для расчёта кругового расположения штифтов, от которых будут натягиваться нитки.

Вся магия происходит здесь:

def main():
    # Load image
    image = cv2.imread(imgPath)

    print("[+] loaded " + imgPath + " for threading..")

    # 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]
    cv2.imwrite('./cropped.png', imgCropped)

    # Convert to grayscale
    imgGray = cv2.cvtColor(imgCropped, cv2.COLOR_BGR2GRAY)
    cv2.imwrite('./gray.png', imgGray)

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

    # Invert image
    imgInverted = invertImage(imgSized)
    cv2.imwrite('./inverted.png', imgInverted)

    # Mask image
    imgMasked = maskImage(imgInverted, imgRadius)
    cv2.imwrite('./masked.png', imgMasked)

    print("[+] image preprocessed for threading..")

    # Define pin coordinates
    coords = pinCoords(imgRadius, numPins)
    height, width = imgMasked.shape[0:2]

    # image result is rendered to
    imgResult = 255 * np.ones((height, width))

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

    imgResult = 255 * np.ones((height, width))

    # Loop over lines until stopping criteria is reached
    for line in range(numLines):
        i += 1
        bestLine = 0
        oldCoord = coords[oldPin]

        # Loop over possible lines
        for index in range(1, numPins):
            pin = (oldPin + index) % numPins

            coord = coords[pin]

            xLine, yLine = linePixels(oldCoord, coord)

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

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

        # Update previous pins
        if len(previousPins) >= minLoop:
            previousPins.pop(0)
        previousPins.append(bestPin)

        # Subtract new line from image
        lineMask = lineMask * 0
        cv2.line(lineMask, oldCoord, coords[bestPin], lineWeight, lineWidth)
        imgMasked = np.subtract(imgMasked, lineMask)

        # Save line to results
        lines.append((oldPin, bestPin))

        # plot results
        xLine, yLine = linePixels(coords[bestPin], coord)
        imgResult[yLine, xLine] = 0
        #cv2.imshow('image', imgResult)
        #cv2.waitKey(1)

        # Break if no lines possible
        if bestPin == oldPin:
            break

        # Prepare for next loop
        oldPin = bestPin

        # Print progress
        #sys.stdout.write("\b\b")
        #sys.stdout.write("\r")
        #sys.stdout.write("[+] Computing line " + str(line + 1) + " of " + str(numLines) + " total")
        #sys.stdout.flush()

    print("\n[+] Image threaded")

    # Wait for user and save before exit
    #cv2.waitKey(0)
    #cv2.destroyAllWindows()
    cv2.imwrite('./threaded.png', imgResult)

    #rgb_img = cv2.cvtColor(imgResult.astype('uint8'), cv2.COLOR_BGR2RGB)
    #plt.imshow(rgb_img)
    #plt.show()

main()

— картинка загружается, выполняется предварительная обработка, а далее в цикле

# Loop over lines until stopping criteria is reached
    for line in range(numLines):

перебираются возможные варианты расположения линий.
В качестве критерия оценки пригодности — используется оценка лучшего «затенения», то есть выбирается такая линия, которая покрывает наибольшее количество исходных тёмных областей исходного изображения:

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


Результаты предварительной обработки изображения:
— инвертированное изображение:
Инвертированный Чеширский кот

— на инвертированное изображение наложена круглая маска:
Круглая маска на картинку Чеширского кота

Итоговый результат работы программы:
Чеширский кот из ниток

Вот и всё! Оказалось, что немного Python и OpenCV способны творить и такие интересные штуки для автоматизированной генерации произведений искусства.

Ссылки
string-art/alog2.py
Круглые картины из натянутых ниток
Янтарному подарили портрет Канта из 5 километров ниток

По теме
Робот-художник
  • 0
  • 24 апреля 2020, 17:07
  • noonv

Комментарии (0)

RSS свернуть / развернуть

Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.