AIはアニメキャラクターをどう見ているか?

こんにちは、hate_nana4です。

今回はAIの顔認識機能を検証して絵に役立てるか考えようという記事です。 技術的な内容としては拙いですが、絵を描く人目線で思ったことを書いていきます。 なのでどちらかというと絵を描く人向けの記事です。

主な使用ライブラリ
- OpenCV
- pytorch
- pytorch-gradcam

はじめに

記事の内容

  • 画像生成を使わないやり方でAIを絵に役立てることができるか
    画像生成が絵描きから賛否両論なら一回それなしで検証してみようということです
  • 現行の著作権法第30条の4における「著作物に表現された思想又は感情の享受を目的としない利用」にあたります
  • 筆者の主観が入っている部分がある

不明点や間違いなどありましたらご指摘いただけますと幸いです。

記事の目的

  • AIがアニメキャラクターを見ている視点を知る
  • 絵描きやAIに詳しい人の視点を知る
  • AIを絵やアニメにどう活かせるか皆さんの意見を聞きたい

もっと練習しろ、こうすれば良いのにといったコメント大歓迎です。

それでは内容に入っていきます。今回のテーマはこちら

AIはアニメキャラクターをどう見ているか?

私は趣味で絵を描いているのですが、キャラの顔を描くのが苦手です。
顔が似てない感じはあるんだけど違和感の原因が分からない…
これには2つ原因があると思っていて

「観察力が足りない」
「観察したものを正しく描く技術が足りない」

今回注目したのは「観察力が足りない」の方です。

もしAIが細かい違いも分かるなら絵の評価や添削に使えるのでは?
プロの先生が見るみたいに絵を添削してもらえるんじゃない?

この時点で模写しろデッサンしろと思った人、それは正しいです。
ですが技術で人の機能を代替しようとするのがAIなので今回はそういう話です。
 
検証に使わせていただいた作品は「リコリス・リコイル
2022年 A-1 Picturesさん制作のオリジナルアニメです。

検証方法

今回は短期シリーズのアニメを作るのにAIが役立つかというシナリオで進めます。 ここではキャラ似せという点に注目しAIからそのキャラクターを見分ける特徴が分かれば作画に統一感や“キャラクターらしさ”を出す助けになるのではという発想です。

実際のアニメ制作では作画監督という役職の方がこの役割を担っており、数十名のアニメーターに対し数名の作画監督が作画のブレや方向性をまとめています。

また、アニメは基本1話から順に作るので最初はキャラクターのデータが少ないと想定。序盤話数の時点でキャラクターを判別できるAIを実装しAIがどう見分けているか検証。対象キャラクターは主人公の錦木千束に絞り本人か本人でないかをAIに判別させます。

錦木千束(にしきぎちさと) 本作主人公の一人

引用 ©Spider Lily/アニプレックスABCアニメーションBS11

作品の選定理由

  • オリジナルアニメなのでアニメのみに絞ってデータを集めやすい
  • 1クール全13話で日本アニメの標準的な短期シリーズである
  • 放送当時から人気だったので知っている人が多いはず

検証の流れ

  1. 前例を調べる
  2. 手法を選ぶ
  3. 実装
  4. 検証

    1. 前例を調べる

「AI アニメ 画像 分類」などで検索するとアニメキャラクターを見分けてみた!という記事がヒットします。
今回主に参考にさせていただいたのはこちら。作画監督についても下のSHIROBAKOの記事で触れられています。

bohemia.hatenablog.com

bohemia.hatenablog.com

次に「AIがどう見ているか?」について、抽象的で検索しづらいのでAIに聞きます。
ChatGPTなどに「〇〇な技術ある?」と聞き得られた単語からさらに関連情報を調べます。

今回は2つの手法が見つかりました。
- CAM (Class Activation Mapping)
- Attentionマップ

2. 手法を選ぶ

ここでAI用語について少し解説です。 引用:機械学習とディープラーニング(深層学習)の違いとは?

AIという概念の中に機械学習、さらにその一種にディープラーニングが含まれるという形。
モデルは有名なものだとOpenAI社のGPTモデルとかGoogle社のGeminiモデルとか多数あります。

手法の話に戻ると
ディープラーニングでは機械がデータから自動的に特徴を抽出してルールを学びます。
その結果、顔認証なら人を見分ける能力を得ますがどんな基準で判断しているか人間には不明な部分が出てきます。

そこでAIが重要だと判断している部分を人間にも説明するための技術、XAI(説明可能なAI)と呼ばれるものがあります。
これが「AIがどう見ているか?」を検証するためのポイントです。

CAM (Class Activation Mapping)

XAIの一種で、モデルが重要だと判断した箇所がヒートマップで赤く表示されます。
CAMはモデルに外付けできるオプションのようなもので初期は適用できるモデルに制限がありました。
今は研究が進み制限なく使えるものも出てきましたが性能が上がるほど処理が重くなります。
qiita.com

Attentionマップ

こちらはXAIとは出自が違い、Attention機構というモデル自体の特性を利用してます。
この機構は「Attention is All You Need」という論文があるほど汎用性が高くChatGPTにも使われてます。
こちらもモデルが物体を識別する時に注目する箇所を可視化できます。
www.softbanktech.co.jp
特にこの記事だとAttentionマップで物体の形をかなり正確に捉えているのがわかります。
www.guruguru.science

https://storage.guruguru.science/uploads/7a246f4b_de69_4ba7_bb10_f42cc61495fc/260epoch.png
Attentionマップもやってみたかったのですが実装となると分からない部分が多く、今回は比較的お手軽に試せるCAMの手法を使います。

3. 実装

ここからやっと実装です、検証は以下の流れで行います。

  • データの準備
  • データ拡張
  • 画像分類モデルとGrad-CAMのテスト
  • 転移学習
  • 転移学習済みモデルにGrad-CAMを適用

技術部分の説明が続くので結果だけ見たい人は4. 検証まで飛ばしてください。
使用コードは記事の最後に載せてます。

データの準備

まずは学習に使う画像データを集めます。
おそ松さんの記事で紹介されていたアニメのスクリーンショットから顔を切り抜く方法OpenCV+AnimeFace を使ってみました。
結果は失敗が3割以上。傾向としては顔が画面に近い、複数キャラがいる画像は失敗しやすく真横に近い顔はほぼ失敗でした。

切り抜き用に顔検出の分類器を作ることも考えましたが、今回はCAMの検証が本番なのと枚数は多くないので残りは手動でやりました。
後の手間を考えると分類器を作るほうがラクです。切り抜きサイズのズレが見つかると面倒なことになります。

約1000枚のデータが集まりました、前例を見ると数千枚はザラなのでこれは少ない方です。
今回は序盤話数のデータが少ない状態という想定なので00〜03話までをさらに選別し学習データとします。

学習データの選定基準はいくつか条件を考えて、以下のようにしています。

正例・負例とあるのは
千束のデータだけを正解として与えるとAIがその特徴しか覚えないため、千束以外のキャラを不正解のデータとして与えてます。
また遠目で解像度が低い、顔が怪しいものは不使用にしてます。

ディープラーニングでもデータを与えるのは人間なのでここのデータの質が結果に影響します。

データ拡張

ここでは少ないデータを水増しする処理を行います。
具体的には明度、彩度、コントラスト調整と左右反転などを使い1枚の画像を5パターンに増やします。
白飛びや黒つぶれがないよう注意。

最終的にもう少し選別して正例が165枚、負例はデータ拡張なしの千束以外のキャラで156枚。負例の割合が多いですが一旦これでいきます。

画像分類モデルとGrad-CAMのテスト

Grad-CAMはCAMの一種でCAMよりも多くのモデルに適用できます。
実装はこちらを参考に、Grad-CAM、Grad-CAM++を同時に使用できます。

qiita.com

VGG16という画像分類モデルを使い、画像を入力して判定する時の注目箇所をCAMでヒートマップ表示させます。
この時点ではまだ千束を見分ける仕様ではないので、モデルが最初の状態でどう出力するかのテストになります。

出力結果がこちら、上から元画像、Grad-CAM、Grad-CAM++の順

鼻や目・口に注目していること、Grad-CAM++は注目箇所が少し細かいことが分かります。
ここではただの人の顔として判定されている可能性が高いです。

転移学習

転移学習とは。
AI界隈では事前に大量のデータで学習させた高性能モデルを誰でも使えるオープンソースという文化があり、この高性能モデルを別のタスクに転用させる方法を転移学習と呼びます。

例えば絵の上手い人が新キャラを描く時に少ない資料からでも描けるのは本人の性能が高いから、というような感じで転移学習も少ないデータで高い性能を発揮できます。

先ほどのVGG16のテストも大量のデータで学習済みのモデルを使っています。 この後に千束を見分ける仕様にしたモデルは区別するため転移学習後のモデルと呼びます。

学習にはGPUを使いたいのでGoogle Colaboを使います。
無料でGPUが使用できるサービスですが無料プランだと使えるメモリや接続時間に制限があります。Colabo上ではプログラミングも行えるのでモデルの挙動を確認しながら学習させます。

こちらも参考にしてます。

www.think-self.com

学習結果はこちら。今回は学習データの85%を学習、15%を検証に分けてます。
青が学習データ、オレンジが検証データで10エポック(10周)も回すと正解率95%を超えました。
もう少し学習すれば詰めれそうですが、データが少ない状態では過学習にならないようここで止めます。
過学習とは同じテストを何度も解いて見かけの成績は良くなるが他のテストで良い点が取れない状態です。

次に学習後にテストデータ100枚を判定させるとほぼ正解でした。
下の2枚は誤判定が出たのでこれらは最後の検証でチェックします。

転移学習済みモデルにGrad-CAMを適用

転移学習後のモデルにGrad-CAM、Grad-CAM++を適用します。
最初はGoogle Colaboでやろうとしたのですがローカルの方が快適なので、転移学習後のモデルをローカルでも使えるように調整してます。
Grad-CAMはそこまで重くないのでCPUでも数秒で結果が出力されます。

転移学習前と同じ画像で比較したのがこちら。注目ポイントが若干変わってるようです。
この後は他の画像も使って検証を行います。

画像上のClass0は正例、千束であるという意味です。

4. 検証

いよいよ最後の検証です。ここでは学習データに使わなかったデータを全話数から使います。
あらためて学習データの選定基準を確認してから結果を見ていきましょう。

正例:千束である

千束:正例 転移学習前は顔の中心だった注目ポイントが髪にも移っているようです。
特に前髪の先や顔の下あたりの髪の先を見ている感じです。 くるみ:正例 どの画像でも正例として誤判定されてます。
注目ポイントは千束のパターンと近く、そのうえで間違われてるようです。
ここで学習データの中身を確認するとくるみの画像は1枚も入っていないことが分かりました。
仮説としては、負例に入ってなかったので負例と学習しなかった、髪色が千束と近く間違えた、などが考えられます。

負例:千束でない

画像上のClass1は負例を意味します。ちなみに負例の学習データは半分以上たきなでした。

たきな:負例 千束と違って頭の上部や顔の周りに注目している感じです。
楠木さん:負例 先ほど正例と判定された画像でも今回は負例でした。
楠木さんの場合はあご周辺に注目しているようです。他の画像でも負例になったので、先ほどは偶然正解率から漏れて正例と判定された可能性があります。 他の大人組も一通りやってみましたが負例と判定。キャラによって注目ポイントが異なります。

横顔、変顔

横顔:正例 OpenCV + Animefaceでは切り抜かれなかった横顔もちゃんと判定されてます。
正面と特徴が違うはずですが、横顔に近い時は髪の下の形に注目してるようです。
3,4枚目は髪の下の形が変わってるパターンですが、この時は顔や前髪に注目してるようです。 変顔:正例 変顔でも問題なく正解、これも顔のパーツより髪に注目している感じです。
3枚目は負例と判定されました。注目ポイントが襟に集まっており通常とパターンが違います。
しかし似たようなリアル調の千束の顔がこれしかなく絵柄のせいなのか偶然か判断できません。
比較として4枚目のような険しい顔でもやってみましたが正例と判定されました。

髪型違い、幼少期

千束の髪型違い:正例 これは前髪の先ではなく鼻周辺に注目してるようです。
たきなの髪型違い:負例 注目ポイントが顔の下など移ってるようです。
髪型違いは学習データに入れてませんがキャラは正しく判定、注目ポイントは髪型で変化。 千束(幼少期):正例
3,4枚目は負例。4枚目は目や目にかかる影に注目している?ようです。
また、3枚目のような逆光は他にも負例になるケースがあったので次で紹介します。

顔を隠すもの:血のり、逆光、ゴーグル、銃など

血のりアリ&逆光気味:正例 他の血のりアリのカットも正例でした。
逆光:負例 1枚目と2枚目を比べると顔全体が暗い場合は注目できてないようです。
ゴーグルあり:正例 他のゴーグルありのカットも正例でした。
銃が顔の前にある:正例 これも髪に注目してるようです。

強い逆光などで顔一面が暗くなると判別が難しくなるようです。くるみの誤判定の件とあわせて考えると、髪の色というか輝度に注目して判断してるのかもしれません。試しに髪がない場合では…

正例と負例どちらも出ました
左が正例ですが、注目ポイントの肌の色で誤判定しているようです。
このおじさんは千束だったかもしれません。

検証結果まとめ

  • 千束の正面顔の場合は前髪や髪の下、髪色に注目して判定してそう
  • 千束の横顔の場合は髪の下の形に注目して判定してそう
  • くるみのように千束と髪色が近いキャラは負例として学習させないと誤判定の可能性あり
  • 髪型や年齢の変化、変顔、顔に血のりやゴーグルなどがあってもほぼ正しく判定できる
  • 同じキャラでも髪型の違いで注目ポイントが異なる
  • 強い逆光などで顔全体が暗くなると判定が怪しくなる

    おわりに

ここまで読んでいたただきありがとうございます。

調べてるうちにこのアプローチ意味が薄いんじゃないかと思ったのですが、やってみるとキャラによって有意な差が出たのは面白かったです。

ただ結果として、今回の検証範囲では絵の評価や添削に使うのは難しそうです
・精度面 細かい違いまでは分からない、絵を描くのに知りたいのは目の微妙なニュアンスとか
・機能面 評価、添削する機能はまた別、または人間が読み取って解釈する必要がある。
今回使ったVGG16は画像分類モデルなので機能としては分類しかできません。もっと多機能で高性能なモデルで検証すれば他のタスクにも使えるかもしれませんが…
今後の課題とします。論文やGithub読んで実装できるようになるのが次の目標です。

ちなみに顔が似ない問題については、結局模写をやってます。
そもそも自分で良し悪しのポイントが分からないとAI調整できないよねということで。こういうのを感覚じゃなく技術で補助したいのですが、絵の場合は人間性能も大事なようです。

また、このような記事でAIの使い方に少しでも興味を持っていただけたなら幸いです。途中使った顔の切り抜きなどはかなり便利なので、AIは単純なタスクの自動化にも使えます。

最後に、この記事を書いたのは目的は絵を描く人やAIに詳しい人の視点を知ることです。上級者から見ればどうして分からないの?と思うことでも、調べて分からなければ聞くしかない。

この記事に対する感想、ご指摘、絵に役立てるAIの使い方のアイデア、論文読んで実装ってどうやってるのかなどありましたら是非コメントでお願いします。


使用コード

  • 転移学習に使用したColaboのページ

colab.research.google.com

  • VGG16 + Grad-CAM

      import torch
      from torchvision import transforms, datasets
      import torchvision.transforms as transforms
      import torchvision.models as models
    
      from PIL import Image, ImageDraw, ImageFont
      import cv2
      import os
      from pathlib import Path
    
      from gradcam.utils import visualize_cam
      from gradcam import GradCAM, GradCAMpp
    
      # device = torch.device("cuda:0" if torch.cuda.is_available()  else "cpu")
      # 今回はCPUを使用
    
      # 入力画像の前処理
      def prepare_data_from_folder(device, folder_path):
          images = []  # 変換後の画像を格納するリスト
          transform_pipeline = transforms.Compose([
              transforms.Resize((224, 224)),
              transforms.ToTensor()
          ])
    
      # フォルダから入力画像を読み込み前処理を適用 jpg, png に対応 
          for img_path in Path(folder_path).glob('*'):
              if img_path.suffix.lower() in ['.jpg', '.png']:  # 拡張子でフィルタリング
                  input_img = Image.open(img_path).convert("RGB")  # RGBAからRGBへ変換
                  torch_img = transform_pipeline(input_img).to(device)
                  images.append(torch_img)
    
          return images
    
      # 転移学習の重みの文字列の不備を修正、通常時ここは不要
      def adjust_state_dict(loaded_state_dict):
          new_state_dict = {}
          for key, value in loaded_state_dict.items():
              # "model."プレフィックスを削除して新しいキー名を作成
              new_key = key.replace("model.", "")
              new_state_dict[new_key] = value
          return new_state_dict
    
      # モデルの定義 出力層を2クラス分類に変更、転移学習済みの重みをロード
      def create_model(device):
          model = models.vgg16(pretrained=False)  # 事前学習済みの重みはここではロードしない
          num_features = model.classifier[6].in_features
          model.classifier[6] = torch.nn.Linear(num_features, 2)
    
          # 転移学習済みの重みをロード 重みのパスは任意で変更してください
          loaded_state_dict = torch.load('学習済みの重みのパス', map_location=device)
          # state_dictのキー名を調整
          adjusted_state_dict = adjust_state_dict(loaded_state_dict)
          # 調整後のstate_dictをモデルにロード
          model.load_state_dict(adjusted_state_dict)
    
          model = model.to(device)
          return model
    
      # Grad-CAM ヒートマップと判定クラスを画像に重ねて出力
      def create_heatmap(input_model, image):
          """
          ヒートマップを作成する
          ヒートマップ単体と入力画像にヒートマップを重ねた画像を返す
          """
          # 推論状態にする
          input_model.eval()
    
          # モデルや構築によって抽出する層を変更する
          target_layer = input_model.features[28]
    
          # Grad-CAM
          gradcam = GradCAM(input_model, target_layer)
          gradcam_pp = GradCAMpp(input_model, target_layer)
    
          normed_img = transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])(image)[None]
    
          # 推論結果確認
          output = input_model(normed_img)
          id = torch.argmax(output[0])
    
          mask, _ = gradcam(normed_img)
          heatmap, result = visualize_cam(mask, image)
    
          mask_pp, _ = gradcam_pp(normed_img)
          heatmap_pp, result_pp = visualize_cam(mask_pp, image)
    
          return heatmap, heatmap_pp, result, result_pp, id
    
      # PyTorchのテンソル形式の画像をPILイメージに変換し、指定されたファイルパスに保存する役割
      def save_img(image, file_path, prediction_id):
          """
          画像を保存する。クラスを画像に追加する
          """
          img = transforms.ToPILImage()(image)
          draw = ImageDraw.Draw(img)
          # テキストの内容を予測結果に基づいて設定
          text = f"Predicted Class: {prediction_id}"
          draw.text((10, 10), text, fill="white")
          img.save(file_path)
    
    
          img.save(file_path)
    
      # 入力画像フォルダと出力画像フォルダを指定 パスは任意で変更してください
      if __name__ == '__main__':
          device = torch.device("cpu")
          input_folder_path = '入力画像フォルダのパス'
          output_folder_path = '出力画像フォルダのパス'
    
          # 出力フォルダが存在しない場合は作成
          os.makedirs(output_folder_path, exist_ok=True)
    
          model = create_model(device)
          images = prepare_data_from_folder(device, input_folder_path)
    
          # Grad-CAM, Grad-CAM++のヒートマップと元画像に重ねた4パターンの画像を保存
          for i, img in enumerate(images):
              heatmap, heatmap_pp, result, result_pp, prediction_id = create_heatmap(model, img)
              # 推論結果のIDを渡して画像保存
              save_img(heatmap, os.path.join(output_folder_path, f'heatmap_{i}.jpg'), prediction_id)
              save_img(heatmap_pp, os.path.join(output_folder_path, f'heatmap_pp_{i}.jpg'), prediction_id)
              save_img(result, os.path.join(output_folder_path, f'result_{i}.jpg'), prediction_id)
              save_img(result_pp, os.path.join(output_folder_path, f'result_pp_{i}.jpg'), prediction_id)
    

引用・参考

ディープラーニングでおそ松さんの六つ子は見分けられるのか? 〜準備編〜

人工知能でアニメの作画監督の仕事を助けよう 〜準備編〜

CAMの発展と実装(Grad-CAM, Score-CAM, Group-CAM)

Attention Is All You Need

深層学習入門:画像分類(5)Attention 機構

Dinoで学習した時のattention map の図示

pytorch-gradcamで簡単にGrad-CAMを行う

【転移学習】学習済みVGG16 による転移学習を行う方法【PyTorch】

自己紹介

「hate_nana4」と書いて「ハテナナシ」と読みます。

映画・漫画・アニメ・ゲーム好きのオタクです。 趣味で絵を描いたりしてますがあまり上手くはないです。

最近はAIが凄いのでSF好きとしてはテンション上がってたのですが クリエイター側からの批判も多くオタクとしては微妙な気分です。 絵を描くのも凄い技術が出るのもどっちも楽しい側からすると 上手いこと付き合っていけないものかと思ってます。

このブログでは皆さんに聞きたい日々の疑問を書いていこうと思います。 よろしくお願いいたします。