0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

AI技術を使ってキャラの画像をラフ画に戻してみた。

Last updated at Posted at 2023-02-27

初めに

タイトルにもある通りです。
数回に分けてこういった画像を
2450112.jpg
sampling_img.png
こんな感じに変換する方法のざっくりとした解説を行っていきます。
Pythonの基本構文のお話やライブラリに関する突っ込んだ内容はここでは割愛して、別の機会に気が向けば記事にします。

もくじ

・初めに
・ソースコード全文
・解説
・二値化処理
・0パディング
・線画抽出
・あとがき

ソースコード全文

少しずつ解説していって最後に完成!って形にしてもいいのですが、面倒なので先に完成系を出してしまいます。

Python3 edge_samling.py
import cv2
import numpy as np

# 0パディングを行っている関数
def zero_padding(image):
    # 0パディングをするために元の画像より一回り大きい2次元配列を作成
    # 入力された画像が3次元だった場合
    if 3 == len(image.shape):
        padding_img = np.zeros([image.shape[0] + 2, image.shape[1] + 2, image.shape[2]], dtype=np.uint8)
    # それ以外だった場合
    else:
        padding_img = np.zeros([image.shape[0] + 2, image.shape[1] + 2], dtype=np.uint8)

    # 元画像の縦を展開
    for i, y in enumerate(image):
        i += 1
        # 元画像の横を展開
        for j, x in enumerate(y):
            j += 1
            # 0配列の中に画像を入力
            padding_img[i, j] = x

    return padding_img


# 二値化処理を行う関数
def binarization_image(image, threshold=127):
    # 画像をグレースケール化
    gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # 1pxごとの値が閾値よりも小さい・大きい場合それぞれ0・255に変更する
    gray_img[gray_img < threshold] = 0
    gray_img[gray_img >= threshold] = 255

    return gray_img


# 線画抽出を行う関数
def line_sampling(image, image_shape, kernel):
    # 線画抽出を行った後の画像の大きさを定義
    sampling_img = np.zeros([image_shape[0], image_shape[1]], dtype=np.uint8)

    # 縦のpx分回す
    for y in range(sampling_img.shape[0]):
        # 横のpx分回す
        for x in range(sampling_img.shape[1]):
            # カーネルと同じ大きさで画像を取得し、値を掛け合わせ、それらの値が入っている配列の各値を合算した結果を配列に格納
            sampling_img[y, x] = np.sum(image[y:y + 3, x:x + 3] * kernel)

    return sampling_img


# 二値化する際の閾値を定義
THRESHOLD = 128
# ラプラシアンフィルタを定義
LAPLACIAN_KERNEL = np.array([
    [1, 1, 1],
    [1, -8, 1],
    [1, 1, 1]
], dtype=np.uint8)

# 画像データを読み込み、ndarray化
img = cv2.imread(r"画像のパスに書き換えてください。")
# shapeは(縦, 横, チャンネル)の順番で格納されている
print(img.shape)

# 二値化処理
binarized_img = binarization_image(img, THRESHOLD)

# 0パディング
padding_img = zero_padding(binarized_img)

# 線画抽出
result_img = line_sampling(padding_img, img.shape, LAPLACIAN_KERNEL)

# 画像の色を反転
result_img[:, :] = 255 - result_img[:, :]

# 画像を保存
cv2.imwrite(r"保存先のパスに書き換えてください。", result_img)

# 線画の画像を出力
cv2.imshow('img', result_img)
cv2.waitKey(0)

これを動かすことでお好きな画像をラフ画にすることができます。
(パスの指定はお忘れなく)

解説

さて、変換の流れですが大まかに分けるとこんな感じ。

1.画像データの読み込み
2.二値化処理
3.0パディング
4.線画抽出
5.色調反転
6.画像出力

本記事では二値化処理0パディング線画抽出の三項目に焦点を当てて解説します。
(というよりほかに解説する項目がないだけ)

解説[二値化処理]

線画の抽出は二値化処理した画像データに対して畳み込みを行うことで、色の境目を浮き上がらせることで行っています。
そのため、まずは読みこんだ画像を二値化、つまりは白と黒の二色のみで構成された画像に変換して画像の輪郭をはっきりさせなくてはいけません。

Python3 binarization_image.py
    # 画像をグレースケール化
    gray_img = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

    # 1pxごとの値が閾値よりも小さい・大きい場合それぞれ0・255に変更する
    gray_img[gray_img < threshold] = 0
    gray_img[gray_img >= threshold] = 255

    return gray_img

この部分ですね。やってることは至極簡単です。
まずはOpenCVからcvtColorを呼ぶことでモノクロの画像にしてしまいます。
諸君、我々は失敗した。
gray.png

次にしきい値をもとに灰色の部分を白か黒かに分けていきます。

    # 1pxごとの値が閾値よりも小さい・大きい場合それぞれ0・255に変更する
    gray_img[gray_img < threshold] = 0
    gray_img[gray_img >= threshold] = 255

これも特に難しいことはせず、if分岐して白(255)か黒(0)に置き換えていくだけです。
binary_img.png
この時点ですでにそれっぽくなっていますね。
これで画像の輪郭をはっきりさせることができました。

解説[0パディング]

# 0パディングを行っている関数
def zero_padding(image):
    # 0パディングをするために元の画像より一回り大きい2次元配列を作成
    # 入力された画像が3次元だった場合
    if 3 == len(image.shape):
        padding_img = np.zeros([image.shape[0] + 2, image.shape[1] + 2, image.shape[2]], dtype=np.uint8)
    # それ以外だった場合
    else:
        padding_img = np.zeros([image.shape[0] + 2, image.shape[1] + 2], dtype=np.uint8)

    # 元画像の縦を展開
    for i, y in enumerate(image):
        i += 1
        # 元画像の横を展開
        for j, x in enumerate(y):
            j += 1
            # 0配列の中に画像を入力
            padding_img[i, j] = x

    return padding_img

次に0パディングです。ざっくり言ってしまうと画像の外周部分に余白を追加することでこの後の畳み込みを行う際に、座標[0,0]のピクセルから畳み込むことができるようにしています。
というのも、畳み込み処理では1pxごとに情報を処理していくのですがその際の計算に最低でも処理するpxの周囲8pxの情報が必要なのです。
そのため0パディングを行わないと画像外周に近い部分が正常に処理されないという悲しい事態に・・・。
元の画像に0パディング処理を行うとこんな感じ。
padding.png

解説[線画抽出]

さて、いよいよ本題です。
こちらが実際に畳み込みを行っている部分。

# 線画抽出を行う関数
def line_sampling(image, image_shape, kernel):
    # 線画抽出を行った後の画像の大きさを定義
    sampling_img = np.zeros([image_shape[0], image_shape[1]], dtype=np.uint8)

    # 縦のpx分回す
    for y in range(sampling_img.shape[0]):
        # 横のpx分回す
        for x in range(sampling_img.shape[1]):
            # カーネルと同じ大きさで画像を取得し、値を掛け合わせ、それらの値が入っている配列の各値を合算した結果を配列に格納
            sampling_img[y, x] = np.sum(image[y:y + 3, x:x + 3] * kernel)

    return sampling_img

それに加えて重要な個所がこちら。

# ラプラシアンフィルタを定義
LAPLACIAN_KERNEL = np.array([
    [1, 1, 1],
    [1, -8, 1],
    [1, 1, 1]
], dtype=np.uint8)

先ほど触れた周囲8pxを指定している箇所になります。
今回は画像の輪郭を浮き上がらせるだけなのでこの形になっています。
画像処理の目的に応じて別の形で作成したり違う形のものを複数使用したり、といったこともあるのですがそれはまたの機会に・・・。

詳しく知りたい方はラプラシアンフィルタで検索してみてください。
https://www.google.com/search?q=%E3%83%A9%E3%83%97%E3%83%A9%E3%82%B7%E3%82%A2%E3%83%B3%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF&oq=&aqs=chrome.1.35i39i362l8.515625991j0j15&sourceid=chrome&ie=UTF-8

この配列を画像データの1pxごとに重ね合わせ、それぞれのpxで掛けた値を合計し元の配列に格納していくことで近くに異なる色があるpxの配列にはより大きな値が、周囲に同じ色しか存在しないpxには小さな値が格納されていくはずです。
この工程が畳み込みConvolutionと呼称されているのです。
最終的に全pxに対して畳み込みを行うとこんな感じになります。
black.png
これであとは画像の色調を反転させてから出力すれば変換完了です。

あとがき

おつかれさまでした。
今回はさらっと触れただけでしたが、この技術はAIを使った画像処理の基礎中の基礎だったりします。
気になる方はコンボリューションニューラルネットワーク(CNN)等の単語で検索してみると楽しいかもしれません。
あ、初投稿なので読みにくい箇所等あると思いますがご了承ください。
次の記事も遠くないうちに書きたい・・・。

記事作成:Y&Q

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?