🐡

StableDiffusion&ControlNet&SegmentAnythingを使って線画の自動下塗り&レイヤー分けツールを作った話

2023/06/24に公開

前置き

こんにちは、抹茶もなかです。

先日、一枚絵のレイヤー分けツールについて紹介した記事を書きました。
https://zenn.dev/aics/articles/833e6601130780

今回は、その時得た知見を応用して線画を自動的に下塗りし、ついでにレイヤー分けまでやってもらおうという試みです。

実際に作ったものはこちら
https://github.com/mattyamonaca/auto_undercoat

こんな感じで線画を入力すると下塗りをしてレイヤーで別れたPSDを出力してくれます。

なんで作ろうと思ったか

レイヤー分けツールを世に放った後、Twitterで作ったツールの反応をみていたところ、「線画を下塗りしてレイヤー分けする部分をAIにやって欲しい」という声を多く見かけた事からこのツールを作ることを考えました。
また、画像生成AIを一枚絵ポン出し以外の用途で実務に活用することはできないのか?という点をテーマとしてツールを作っているので、その目的にも合致していた、という点が大きいです。

自分でデジ絵を描いているときの肌感としても、下塗りとレイヤー分けは面倒な部分だと思うことも多く、線画作成や色塗りに比べて作業感が強いと感じる部分でもあり、自動化の需要は大きそうだなと思っていました。

実際、ツール公開後はレイヤー分けツール以上に好意的な意見を多くもらえたかなと思っています。
(その割にGitHubのスターは伸びないので欲しい評価を得ることの難しさは感じた)
https://twitter.com/GianMattya/status/1670226144241795074?s=20

下塗り&レイヤー分けってなんぞ

レイヤー分けについては先ほど紹介したAIを使わずに一枚絵をレイヤー分けできないか試みた話をご参照ください。
簡単に説明すると絵を領域と効果に分けて複数層の重なりとして構成する技術を指します。

下塗りについてですが、デジタルイラストにおける下塗りとは、影や光を描きこむ前にそのベースとなる色を塗る作業を指します。
また、後に作業がしやすいように塗った色毎にレイヤーを分けて管理することも多いです。

(昔自分が描いた下塗りの例)

着想

SD+CNによる線画の自動着色

本題に入ります。
では具体的にどのように線画から下塗りを作るか?についてですが、題名にもある通りStableDiffusion, ControlNet, SegmentAnythingの3つを使います。

StableDiffusionはOSSの画像生成AIであり、ControlNetはその画像生成AIの出力を成業する機構、SegmentAnythingは物体検出のAIとなります。
詳しい説明は割愛しますが、どれも有名なOSSであり、解説記事を上げている方も多いので目を通していただければと思います。
(SegmentAnythingについてはこちらもぜひ)
https://zenn.dev/mattyamonaca/articles/dcacb4f6dcd58f

実際に画像生成AIを使っている方はこの時点でピンとくると思われますが、今回はControlNetのLineart_animeとCannyを使います。

ControlNetは使用するモデルの種類によって、言語以外の情報を入力としてStableDiffusionの生成結果を成業することができます。
Lineart_animeとCannyは、どちらも線画を入力情報として、その線画に沿った画像を生成することができるモデルとなります。
また、ControlNetは複数のモデルを同時に使うことができ、Lineart_animeとCannyを同時併用すると、線画に忠実な画像を生成してくれることが知られています。

(詳しくはこちらの動画でも紹介されています)
https://www.youtube.com/watch?v=Z2n31Lhe6Vw&t=68s

ただし、このモデルにも課題があり、かなり線画に忠実な画像を出してくれるものの、あくまで画像生成であるため元の線画とは微妙に違った結果を出力する、線画と塗りが結合した一枚絵で出力される、という問題があります。

また、今回の目的は自動下塗り&レイヤー分けのため、影や光も塗った状態で出力されるSD+CNだけでは欲しい結果が得られません(※フラットLora等で下塗り風にする事は可能)

SegmentAnythingによる領域分け

ではどうするか?という事で思いついたのが、画像生成AIを着色に利用するのではなく、生成結果をセマンティックセグメンテーションの補足情報として利用するという方法です。
SegmentAnythingは入力された画像を意味のある単位でできるだけ細かく分割してくれるセグメンテーションモデルですが、さすがに色がついていない白黒線画をそのまま入力するとセグメントの精度がかなり悪いです。
また、入力された線画は色が塗られていないため、領域に対してどの色で下塗りをすべきか?という判断を行う情報がありません。
この部分を、画像生成AIを使ってサポートしてやることはできないか?というのが今回の肝です。

(白黒線画のみで領域分けするとこんな感じ)

処理手順

  1. (背景が透明、もしくは白色の)線画を入力し、SD+CNで自動着色
  2. 生成結果を入力とし、SegmentAnythingによるセグメンテーションを実施、領域ごとの座標を保存
  3. 領域毎に自動着色で使われた色の最頻値を取得し、その領域に属するピクセルを最頻色で塗りつぶす
  4. 入力された線画(白背景の場合は白色を透明化した後)を[3]で取得した画像に重ねる
  5. 再度SegmentAnythingをかけ、[2-4]をもう一度繰り返す(画像生成による微妙なズレの修正)
  6. [5]でえられた画像を分けた領域毎にレイヤー化し最後に入力画像を線画レイヤーとして加えPSD化

実際の処理結果を画像で再現するとこんな形になります。

  • 入力画像

  • 生成画像

  • 領域分け

  • 出力結果

コードはこんな感じ(詳細はGithubにて)

    def undercoat(self, input_image, pos_prompt, neg_prompt, bg_type):
        image = pil2cv(input_image)
        image = cv2.cvtColor(image, cv2.COLOR_BGRA2RGBA)
        if bg_type == "alpha":
            line_img = pil2cv(input_image)
            line_img = cv2.cvtColor(line_img, cv2.COLOR_BGRA2RGBA)
            index = np.where(image[:, :, 3] == 0)
            image[index] = [255, 255, 255, 255]
            input_image = cv2pil(image)
        else:
            line_img = get_line_img(image)

        pipe = get_cn_pipeleine()
        detectors = get_cn_detector(input_image)
            
        gen_image = generate(pipe, detectors, pos_prompt, neg_prompt)

        masks = segment(model_dir, pil2cv(gen_image))
        output, layer_list = get_flat_img(gen_image, masks)
        layer_list.append(line_img)

        layers = []

        for layer in layer_list:
            layers.append(cv2.resize(layer, (line_img.shape[1], line_img.shape[0])))

        output = cv2.resize(output, (line_img.shape[1], line_img.shape[0]))

        filename = save_psd(
            line_img,
            [layers],
            ["base"],
            [BlendMode.normal],
            output_dir,
        )

        output = cv2pil(output)
        line_img = cv2pil(line_img)

        output = Image.alpha_composite(output, line_img)
        output = pil2cv(output)

        return output, layers, filename

Tips

工夫した点

  • フラットな塗りになるようにデフォルトのポジティブプロンプトを埋め込んである
    SegmentAnythingの弱点として、影や光の効果が強いとそこを分けてしまうという点があります。
    今回はなるべく影や光の影響を受けたくないので、テキストプロンプトを一部固定することで毎回フラットな(光や影の効果が薄い)画像を生成し、その画像に対してセグメンテーションを実施するといった形になっています。

おわりに

下塗り&レイヤー分けツールの構想についてここまで説明してきました。
当初思い描いていたものはある程度形にできたかなと思いますが、やはり画像生成を途中処理に用いているため塗りのはみだしや塗り残し等、まだまだ課題が残っています。
このあたりの精度向上に関しても案はあるので、引き続き精度向上に取り組んで行こうと考えています。

最後に宣伝ですが、私のGithubアカウントではほかにもイラストや画像を取り扱ったツールを作って公開しています。

この領域に興味のある方はぜひフォローいただけますと幸いです。
またスターを投げていただけるとやる気が出ます。

そのほか、Twitterでも最新の取り組み状況をつぶやいていますので、目を通してもらえたら嬉しいです。
https://twitter.com/GianMattya

それでは、長らくお付き合いいただきありがとうございました。

AIものづくり研究会

Discussion