Техника изготовления картин из ниток (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 километров ниток

По теме
Робот-художник


Добавить комментарий

Arduino

Что такое Arduino?
Зачем мне Arduino?
Начало работы с Arduino
Для начинающих ардуинщиков
Радиодетали (точка входа для начинающих ардуинщиков)
Первые шаги с Arduino

Разделы

  1. Преимуществ нет, за исключением читабельности: тип bool обычно имеет размер 1 байт, как и uint8_t. Думаю, компилятор в обоих случаях…

  2. Добрый день! Я недавно начал изучать программирование под STM32 и ваши уроки просто бесценны! Хотел узнать зачем использовать переменную типа…

3D-печать AI Android Arduino Bluetooth CraftDuino DIY IDE iRobot Kinect LEGO OpenCV Open Source Python Raspberry Pi RoboCraft ROS swarm ИК автоматизация андроид балансировать бионика версия видео военный датчик дрон интерфейс камера кибервесна манипулятор машинное обучение наше нейронная сеть подводный пылесос работа распознавание робот робототехника светодиод сервомашинка собака управление ходить шаг за шагом шаговый двигатель шилд юмор

OpenCV
Робототехника
Будущее за бионическими роботами?
Нейронная сеть - введение