***この記事は新歓ブログリレー2020 52日目の記事です***
こんにちは, 19電電のびすますです.
昨年度はふと2Q終わり辺りから系所属を頑張ろうなどと思ってたこともあり, 全然活動に参加できてなく認知もされていなかったと思うので, 本年度はもう少し活動に参加したいなーなど思ったりしています. 反省.
前書き
さて, 入学当初とも3Qのバ*オ*の*く*(前回の記事参照)からも変わらず, 私はC#(とVB.net)しか書けません. ですので, 元々C#かWindows Serverネタを書こうかなとは思っていたのですが, 正直書ける記事のネタがなかなか思い浮かばず, いっそ日記的に書き溜めていた転学院の話を投稿しようかどうか迷ったりもしてました. ただ, 思うところがあった(*1)ことと, せっかくなので技術ネタと好きなものを交えた話をしたいよなーと思い, 私がほぼ箱推ししていると言っても良いホロライブのVTuberさんで画像中の顔が誰であるのか判別するアプリケーションをML.netとOpenCvSharpで作る話をしてみることにしました. 機械学習とかホットなワードですけど全然触ってなかったので...
あと, 一つだけ言わせてもらいますとこの記事ではあまり理論的な話とかはしてません(文章量が無限大になってしまうので). ML.netでの画像識別の実装だけならこんな感じで頭使わなくてもできるよーって感じの話がメインだと思っていただければと。
概要
今回作成したアプリケーションの流れをまとめると, 次のような感じになっています:
A. 学習データを作成
- OpenCvSharpで画像からアニメ風の顔を検出するコードを作成
- 1で作成したプログラムにホロライブのVTuberさん32名の動画やTwitterの画像投稿などから適当に15枚程度選んで入力
- 2の出力結果をML.net Model Builderに学習データとして入力
B. 入力された画像から顔検出して判別するコードを作成(分類器の作成)
- A-1を応用して, 入力された画像の顔部分の座標を取得, ML.netで判別
- 画像の顔部分を枠で囲み, 判別結果とスコアを書き込んだものをアウトプット
一見機械学習と聞くと難しそうではありますが, ML.net Model Builderというかなり便利なツールを使えば殆ど画像の判定についてはコーディングする必要がありません(自動コード生成). それでは, 流れを追って簡単に解説していきます.
※改めて書かせていただきますが, 理論的な話や学習させる画像の枚数の少なさについては, 今回は無視することにします. 前者は無限に記事が長くなる∧私もよく理解していない部分が多数あるので下手なことを言って怒られたくないということ, 後者はPCスペックや32人もメンバーがいるのでなかなか画像を集めることが大変であるという問題があるためです.
A. 学習データの作成
顔抽出プログラム
FaceDetectAndOutput.csusing System.Collections.Generic;
using OpenCvSharp;
using System.IO;
const string ORIGIN_DIR = "./origin/";
const string DETECTED_DIR = "./detected/";
const string CC_PATH = "lbpcascade_animeface.xml";
public static void DetectAndOutput()
{
var cc = new CascadeClassifier(CC_PATH);
foreach (string fp in Directory.GetFiles(ORIGIN_DIR))
{
using (Mat mat = new Mat(fp))
{
List<Mat> l = new List<Mat>();
foreach (Rect r in cc.DetectMultiScale(mat, 1.11, 3))
{
var rr = new Rect(r.X, r.Y, r.Width, r.Height);
l.Add(mat.Clone(rr));
}
if (l.Count == 0)
{
//suffix「X」をつけてそのまま返す
mat.SaveImage(DETECTED_DIR + Path.GetFileNameWithoutExtension(fp) + "_X" + Path.GetExtension(fp));
}
else
{
for (int i = 0; i < l.Count; i++)
{
//suffix「i」をつけてトリミングしたものを返す
l[i].SaveImage(DETECTED_DIR + Path.GetFileNameWithoutExtension(fp) + "_" + i + Path.GetExtension(fp));
}
}
}
}
}
まずは顔検出して, 抽出を行うためのプログラムを上のように実装しました.
コードとして行っていることは, https://github.com/nagadomi/lbpcascade_animeface にあるLBP特徴ファイルlbpcascade_animeface.xml (MIT License) をOpenCvSharpから利用して, イラストから顔部分を抜き出し, 複数の顔が検出された場合は, 0番から順にsuffix(接尾字)をつけ, 顔が検出されなかった場合はsuffixとして「X」という文字をつけたファイル名でアウトプットするというものです.
ただし, オリジナルのデータをoriginディレクトリ, 切り取り後のデータをdetectedディレクトリ, bpcascade_animeface.xmlは実行ファイルと同ディレクトリに置くことにしました.
注意点としては, OpenCvはマルチバイト非対応なのでディレクトリやファイルの名称に日本語などが入っていると例外が発生することがあるということです.
よって, 文字を扱いうる関数(例えば読み込み, 出力, 文字挿入など)はOpenCv経由ではなく, C#の標準機能(Bitmap, Graphics)を使うと良いでしょう. なお, 今回の先述のコードでは私が入力ファイル名を調整すればいいだけですのでその事実を無視していますが, 実際の分類器(B)のコードでは配慮しています.
B. 分類器の作成
流石にブログ記事にコードのすべてを載せるのは厳しいのでGithubにコードは載せておきます.
今回は2020年4月現在のホロライブの(海外, ホロスターズなど除く)32のメンバーの画像を配信やTwitterなどから適当にかき集め, 直前で実装したDetectAndOutput()で顔を切り抜いたものを学習させます. 簡単のため, 各メンバーごとに15枚前後で実装しましたのでたまに間違うこともありますが, 試してみれば大体あっているかなーという印象です. もっときちんと学習データの作成をする必要はありますが, その手法等は調べればいくらでも出てきますので省略します.
もちろん, 他の企業や個人Vtuberでやってる方のデータも取り入れ, よりサンプルを増やせばわからないVTuberを調べる際に有用なアプリになると思いますが, 流石に負担がやばいし全部網羅しきれるわけ無いじゃんと思ったのでやめました. まあ, あくまで私のML.netの勉強用に軽く作ってみたという程度ですので, そこを含め細かいところ(特に学習データの雑さとか)は許していただきたいですね….
さて, 学習についてはML.Net Model Builderという, ウィザードに従ってただ画像を投げるだけで分類器のコードまで自動生成してくれるすぐれものがありますので, 今回はこちらを使うことにしました. 2020年4月現在では, ほぼ参考文献[1]の通りに行えばいいだけですが, 一応簡単に書いておきます.
ML.net Model Builderの使い方
- https://docs.microsoft.com/ja-jp/dotnet/machine-learning/how-to-guides/install-model-builder を参考にML.net Model Builderをインストールしておく.
- プロジェクトをいつもどおり作成して, ソリューションエクスプローラーで対象のプロジェクトを右クリックし, 「追加」→「Machine Learning」

2. 「Image Classification」をクリックする.

3. 学習させるデータを格納したフォルダを選択すし, 「Train」をクリックする. 選択したフォルダのサブディレクトリが分類される名称となる.(下の画像のように, assetsフォルダに分類名としたいものをサブディレクトリとしてその中に学習させる画像を入れておけば良い.)


4. そのまま「Start Training」をクリックし, 暫く待つ.

5. 学習が終わると, 「Evaluate」ボタンが出てくるため, これをクリックする.

6. そのまま「Code」をクリックする. なお, Try Your Modelで判別がうまくいくのか試すことも可能.

7. 「Add Projects」をクリックすると, ソリューションにプロジェクトが2つ追加される. これで準備は完了.


8. コードの使い方の説明がされる画面に切り替わる. このようにウィザードに従っていくだけで, たった数行で画像の判別が行えるコードが書けることになる.

※例えば,
ModelOutput modelOutput = ConsumeModel.Predict(new ModelInput()
{
ImageSource = IMAGE_PATH
});
としたときの, modelOutput.Predictionには, 最もスコアの高かったものが入っていることになる.
注意: そのままビルドしても, 作成したPC以外では動かない可能性が極めて高いことに注意.
自動生成されたコードのうち, 変数"modelPath", "TRAIN_DATA_FILEPATH", "MODEL_FILEPATH", "DATA_FILEPATH"を実行ディレクトリ下のパスなどにして, どの環境でも動くよう適切に書き換えておかなくてはなりません.
成果物について
作成したアプリケーションは, 調べたい画像の入っているディレクトリを指定してあげればサブディレクトリを除くそのディレクトリ内の"jpg", "jpeg", "png"形式のファイルをすべて調べ上げ, 顔検出し, 情報を付け加えたものをresultsフォルダに出力するという仕様にしています.
また, 元々は(マルチバイト非対応である)OpenCvSharpのみで(キャラクタ判定以外の)画像処理を実装しようとしていたので, 判定される名称は日本語ではなく名前を子音orあ行のみとって読む表記法を使っています. まあ.Net標準機能での描画に切り替えたので日本語になっていてもいいのですが, 学習のし直しが必要になって流石に面倒なのでやめました.
今度はややスコアにはばらつきあるものの全員判定できてるな…
— びすます (@b1smut) April 19, 2020
log: https://t.co/yfdh3aRYeO pic.twitter.com/k5Cd9Zxt1A
一応, 需要があるのかどうかわかりませんがGithubにコードを上げておきました. ただ, そのままビルドしても"assets"フォルダも"MLModel.zip"も抜いていますので動きません. もしこのアプリを実行してみたいという方がいれば(いなさそうだけど), すぐには厳しいですが個別に渡しますのでTwitterかなんかで聞いてみてください.
Githubのリンクは, 以下になります.
https://github.com/4zuq/HLLIVMLApp
参考文献
[1] https://qiita.com/tfukumori/items/e11e0485fa21b84727a2
[2] http://ultraist.hatenablog.com/entry/20110718/1310965532
どうでもいい独り言
(*1)いわゆる高得点のくる科目の情報や表に中々出ないような話を書いても, 個人によって教員や評価形式の合う合わないがあります. そういう類の情報をあてにしすぎて失敗をしてしまった人は私を含めていたりするものですし, こういう場(特にサークルブログでもありますし)で不特定多数に読まれうる記事として詳細に書いてしまうのは, 情報戦というこれまでの性質を破壊しかねないという面もありますので, ちょっと気がひけるなあという気がしました. Twitterや個人ブログで独立して感想を述べてみるくらいなら全然いいのでしょうが.
あとは, 下手に本音を言って炎上したり攻撃されたら怖いなあというところもあったりします(これも私個人ならどうでもいいですが, ここでやったら迷惑かかりますし.) そんなわけで, (私は上級生にコンタクトを取ったりはせず自分で情報を集めていたのですが,) そういう類の情報に興味がある方は, やはり情報を持ってそうな個人に直接聞いてみるのが一番かなと思います. (Twitterで教員名をa行と子音表記の形にして調べればたいてい評判が出てきたりもしますが)
(*2)
実はGithubに上げるときにコマンドミスして全コードが消えるなどの悲劇がありました. Debug, Releaseフォルダはなぜか残ってたのでdotPeekで逆コンパイルしてコードを大体復元できたのでこの記事を投稿できたのですが, なぜこんな変なデータの消え方をしたのか謎でしかない. 怖い.