POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

FeedlyRSSTwitterFacebook

本記事は、原著者の許諾のもとに翻訳・掲載しております。

少し前、私は Petros Vrellis が作り出した芸術作品に出会いました。それを見て、私の中のエンジニア魂が叫びました。「自動化だ!」下の画像で分かるように、Petrosは丸い織機を使って糸で肖像画を作ります。しかし、これにはかなりの根気が必要です。これから、私がざっとこのプロセスを自動化するステップをやってみます。しかし、もちろん、称賛されるべきは、この驚くべきアイディアを思い付くきっかけとなったアーティスト本人です。最終的に出来上がるのは、糸だけを使って画像をハーフトーンで表現したものです。

Petros Vrellis working on: A new way to knit
この主なアイデアは、3つのステップに分かれます。ステップ1で、様々な入力画像に対して、糸で作品を表現するためにちょっとした前処理を行います。ステップ2で、最善の画像表現とするために製作者が長い糸をどのように配置していくのか決める処理をPythonで行います。ステップ3では、私の場合、この重労働をこなすためにレーザーカッターをレーザーなしで動作させます。

画像の前処理

ピクセルレベルで画像処理をするのに 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)

画像が正しいサイズになれば、グレースケールに変換し、反転させます。この形式では、画像をもともと最も暗かった部分が最も高い値になるような配列として表現することが出来ます。最後に、画像に丸いマスクをかけます(丸の外側の値が全て0に設定されます)。




糸を通すアルゴリズム

元の画像を表現するために、糸をどのように通す必要があるかを決定するのが、アルゴリズムの中心的役割です。製作には、ピンが等間隔に並んだ丸い織機を使用することを想定しています。

上のクリップで見られるように、アルゴリズムはランダムなピンから始めます。画像を再現するのに最善のピンを探すためにあらゆる選択肢をチェックします。そして、最適な線を引いて、その線によって加わる”暗さ”を加算すると、最初に戻って同じことを繰り返します。下の関数は、織機のピンの座標と特定の線を表すマスクを生成します。

# Compute coordinates of loom pins
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((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)

上の関数を使って、線が最大数に達するか終了基準が満たされるかのいずれかまで、アルゴリズムが繰り返し画像に線を追加します。画像を特定の線積分で表すには、適応度関数が必要です。もともと暗かった領域の多くを線で覆うことによって、結果として高い適応度を実現します。

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

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

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

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

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

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

    # Prepare for next loop
    oldPin = bestPin

アルゴリズムがそれ以上引くべき線を見つけられなくなったら、ループから抜け出します。その結果をさらに処理することも可能です。以下は、アルゴリズムの結果の2つの例です。それぞれの画像の上にカーソルを合わせると、元の画像が見られます。

数値制御による糸通し

レーザーカッターに重要な作業を任せられるという手軽さにいつも魅力を感じていたので、自作を始めました。大きな投資は必要なく、実際にはレーザー管とレンズを購入しました。ということは、私の学生向けの小さな部屋の大部分がこの大きなデカルトのプロッタに占領されているということです。Petrosのプロジェクトに出会って、やっとこの使い道が決まりました。

結果の線の座標をGコードにパースすると、数値制御で糸を通すことが出来るようになります。糸の小さなガイドを作るのに、ペンを使いました。以下は、最初の約900本の線に相当する糸が通された状態です。この時点で、糸が切れたので、その日はそこで切り上げました。再度トライする時間ができたら、投稿を更新して最終結果の画像をアップします。


このコード全体は、 GitHub に投稿されていて、パラメータの調整の方法も書かれています。コメントやアドバイスは大歓迎なので、質問があれば、お気軽にご連絡ください。

監修者
監修者_古川陽介
古川陽介
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
複合機メーカー、ゲーム会社を経て、2016年に株式会社リクルートテクノロジーズ(現リクルート)入社。 現在はAPソリューショングループのマネジャーとしてアプリ基盤の改善や運用、各種開発支援ツールの開発、またテックリードとしてエンジニアチームの支援や育成までを担う。 2019年より株式会社ニジボックスを兼務し、室長としてエンジニア育成基盤の設計、技術指南も遂行。 Node.js 日本ユーザーグループの代表を務め、Node学園祭などを主宰。