Оказалось, что подобная техника называется 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 способны творить и такие интересные штуки для автоматизированной генерации произведений искусства.
Комментарии (0)
RSS свернуть / развернутьТолько зарегистрированные и авторизованные пользователи могут оставлять комментарии.