metric learningで人間並みのアニメキャラ識別性能を目指す!

はじめに


今回はmetric learningと呼ばれる技術を応用して、同じアニメキャラが写っている顔画像ほど類似度が大きくなるような特徴量空間を学習したいと思います。

この記事の内容は以前Qiitaで投稿した記事と同じプロジェクトをより発展させた(と思っている)ものです。

結論だけ知りたい人へ

人間のエラー率1.2%に対し、深層学習を使った今回の取り組みではエラー率11.3%と人間にはまだまだ及ばない結果になってしまいました。今後引き続きいろいろ取り組みを行って続編ではもう少し精度を上げられればと思います。

デモ

今回の取り組みで一番性能が良かったモデルを試せるデモを用意しました: Open In Colab

画像を2枚アップロードして、そこから抽出されたキャラクターの顔の間の類似度スコアを計算して表示するようなシンプルなデモになっております。

技術的な話


metric learning

Metric learningとは、似ている入力データ同士の距離が小さくなる(類似度が大きくなる)ような特徴量空間への変換を学習する技術です。

たとえばデータ  \boldsymbol{x} と、それぞれ似ている・似ていないデータ  \boldsymbol{x}^{+}, \boldsymbol{x}^{-} があったとき、何らかの距離指標  dist(\cdot, \cdot) において  dist(f(\boldsymbol{x}), f(\boldsymbol{x}^{+})) \lt dist(f(\boldsymbol{x}),f(\boldsymbol{x}^{-})) となるような変換  f(\cdot) を学習することが目的です。

既にわかりやすい解説記事が多くありますので、詳細な説明は例えば以下の記事に譲りたいと思います。

qiita.com

今回はこのmetric learningの技術を応用するに当たり、既に様々なmetric learningの損失関数がPyTorchで実装されているpytorch-metric-learningライブラリを利用させていただきました。

github.com

ハイパーパラメータ探索

これまでmetric learningの損失関数は様々なものが提案されており、性能が改善されていっている事が報告されていました。しかし以下の記事で紹介されている論文で示されているように、より公平な比較を行ったところ既存の損失関数はこれまで報告されていたほどには性能を改善していないのではないかという意見も出されています。

blog.seishin55.com

そこで今回の取り組みでは単に最新の損失関数を使うのではなく、Contrastive損失などの古典的な損失関数も含めて評価の対象とすることとしました。

前述した論文でも示されている通り、ハイパーパラメータのチューニングは手法間の公平な比較には重要です。 そこで、今回は勉強も兼ねてハイパーパラメータチューニングのライブラリであるoptunaを使ってみました。

github.com

実験


利用データ

モデルの学習には、個人的に収集した11万枚のアニメキャラ顔画像データを利用しました。

顔部分の切り出しには、以前Qiitaの方で紹介したEfficientDetを自作データセットで学習したモデルを利用しています。詳細は以下の記事やレポジトリをご参照ください。

qiita.com

github.com

これらの顔画像にはタグに基づくキャラクターのラベルが付けられています。 キャラクターの総数は3502で、1キャラクターあたりの平均顔画像数は31です。 各キャラクターは少なくとも3枚以上の顔画像を持ちますが、低頻出キャラクターの数が多く頻度が3,4のキャラクターがそれぞれ720, 508もいます。

一方テストデータには、2枚の顔画像しか持たない1399人のキャラクターの顔画像を利用します。

このテストデータにおいて同じキャラクターの顔画像ペア全てを正例とし、ほぼ同じ数の異なるキャラクターの顔画像ペアを負例としてランダムにサンプルしました。

テスト時には、上記画像ペアが同じキャラクターかどうかの分類を正しく行えるかどうかを評価します。

実験コード

以降で説明するハイパーパラメータチューニングに関する実験コードは以下のGithubレポジトリで公開しています。ドキュメントが適当なので使い方など分かりづらいかもしれませんが、興味がある方はご覧ください。

各クラスの画像がフォルダごとにまとめられている形式のデータセットであれば、設定ファイルをいじるだけでそのまま利用できるはずです。

github.com

結果


人間

まず人間である私がテストデータに対してアノテーションを行った結果、FPR(False Positive Rate)=1.22%, FNR(False Negative Rate)=22.0%という結果になりました。

FNRがやや大きく感じますが、同じキャラクターであっても絵柄が大きく異なったりすると同じキャラクターであると判断するのは案外難しいです。 またラベルノイズや顔検出の失敗も含まれるため、こんなものだろうという感覚です。

今回はこの人間の性能を目指し、同じFNRでどれだけFPRを人間と同じくらい小さくできるかを調べていきたいと思います。

ベースライン

深層学習モデルの性能を評価する前に、基準となるシンプルな方法での性能を調べてみたいと思います。

1つ目のベースラインはランダムに答えを出力するものです。

2つ目のベースラインは、LAB色空間における色ヒストグラムのコサイン類似度を用いてキャラクターが似ているかどうかを判断するものです。画像ごとのヒストグラムの計算方法の実装は以下のとおりです。nbinについては性能が最も良かったnbin=5に設定しました。

import cv2
import numpy as np

def color_histogram(path, nbin):
    img = cv2.imread(path)
    img = cv2.cvtColor(img, cv2.COLOR_BGR2LAB)
    hist = cv2.calcHist(img, [0,1,2], None, [nbin]*3, [0,256, 0,256,0,256])
    vec = hist.flatten()
    vec = vec / np.sum(vec)
    return vec

これらのベースラインの結果は以下のとおりになりました。

ヒストグラムを使ってもランダムより多少良い程度で、人間の識別能力には全く及ばないことがわかります。

手法 FPR
ランダム 77.9%
ヒストグラム 68.5%
人間 1.22%

損失関数の比較

続いて様々に提案されているmetric learningの損失を用いて学習した深層学習モデルの性能を比較してみます。

深層学習モデルにはResNet-18でクラス分類を行う全結合層を、500次元の特徴量ベクトルに変換する全結合層に置き換えたモデルを利用します。また、この全結合層の後にDropoutレイヤーを加え、特徴量ベクトルはL2ノルムが1となるように正規化されます。

この深層学習モデルを学習するにあたって、今回は以下の4種の損失関数を比較しました。

  • Contrastive
  • Triplet
  • ArcFace
  • ProxyNCA

それぞれの損失関数の性能をできるだけ正確に比較するため、以下のページに公開されている既存研究のハイパーパラメータ探索領域も参考にしながらハイパーパラメータ探索を行いました。

kevinmusgrave.github.io

損失関数のハイパーパラメータ探索範囲はそれぞれ以下のとおりとしました。

  • Contrastive
    • pos_margin: [0,2]
    • neg_margin: [0,2]
  • Triplet
    • margin: [0,2]
  • ArcFace
    • margin: [0,90]
    • scale: [0.01,100]
  • ProxyNCA
    • scale: [0,100]

またembeddingに対しL1,L2正則化を行うかどうかもチューニングし、行う場合は係数を10^{-6}から 10^{2} までの範囲で探索しました。 モデル(および一部の損失関数)の最適化にはRAdamを利用し、学習率は10^{-5}から10^{-2}の間で、weight decayの係数は10^{-10}から10^{-2}の間で探索しました。

探索に利用できる計算資源には限りがあるため、optunaによる最大試行回数は各損失関数あたり60回とし、エポック数も最大30として2エポック連続で性能(MAP@R)が改善しなかった場合は学習を打ち切るearly stoppingも行います。

またハイパーパラメータ探索中は、データのばらつきのよる評価のゆらぎを抑えるため、2つの異なるtrain-devセットの分割における性能を平均しました。 なおテスト時にはより多くのデータを使ってモデルが収束するまで学習するため、early stoppingの条件を緩くするなど探索時とはやや条件を変えて再学習しています。

それぞれの損失関数で最良の結果は以下のとおりとなりました。 先程までのベースラインと比べて、大きくFPRが改善していることがわかります。

一方で、人間の識別能力にはまだまだ及ばない結果となってしまいました。

損失関数 探索時のMAP@R (\uparrow ) 再学習時のMAP@R (\uparrow ) 再学習時のFPR (\downarrow ) 再学習時のEER (\downarrow )
Contrastive 0.260 0.308 12.2% 15.0%
Triplet 0.248 0.272 13.0% 16.0%
ArcFace 0.213 0.241 14.8% 17.7%
ProxyNCA 0.236 0.251 16.6% 18.9%
(参考)人間 N/A N/A 1.22% N/A

また損失関数に注目すると、興味深いことに最も古典的なContrastive損失が最も良い性能を示しました。 60回の試行回数で十分だったのかという疑問もあるため断定はできませんが、少なくとも今回の探索方針・データセット・タスクでは他の損失関数よりもContrastive損失が有用であったということのようです。 metric learningの損失に関しては、とりあえず新しい損失を使っておけば良いわけではないということが言えるのではないかと思います。

なお以後の実験も含め、設定ごとの探索結果の詳細はGoogle スプレッドシートにまとめてあります。興味のある方はご覧ください。

少し脇道にそれますが、得られたハイパーパラメータの探索結果を上記既存研究で得られていたものと比較してみます。

今回Contrastive損失に対しては

  • pos_margin: 0.681
  • neg_margin: 1.04

というハイパーパラメータが最良の結果を示しました。一方既存研究ではCUB200データセットに対するContrastive損失のハイパーパラメータ探索結果として以下のような結果が得られています。

f:id:ronwall1701:20201226075636p:plain
https://kevinmusgrave.github.io/powerful-benchmarker/papers/mlrc_plots/cub_contrastive.html より引用

データセットやその他探索の条件などが異なるため一概に比較はできませんが、それぞれのデータセットで性能が最適なハイパーパラメータの領域は当たらずとも遠からず、といった印象を受けます。

新しいデータセットでmetric learningのハイパーパラメータを探索する際は、既存研究での探索の結果良さそうなハイパーパラメータを初期情報として与えることも有用なのかもしれません。

次元数・データ拡張(Data augmentation)の効果

これまでの実験では最終的なembeddingの次元を500に固定し、精度向上のためのデータ拡張は行っていませんでした。この節ではこれらの設定を変えることでより性能が向上できないかを調べます。

次元数

まず次元数を増やしたときの効果について調べます。

損失関数は前の節の結果を踏まえてContrastive損失を用いることとします。また次元数を増やすことによって最適なdropoutや距離に対するマージン閾値が変わってしまう可能性があるため、前の節と同様のハイパーパラメータ探索を行いました。

結果は以下の通りです。次元数を500から1000に増やしても、性能を大きく改善させることはできませんでした。そこで以降の実験では次元数は500のままとすることにします。

次元数 探索時のMAP@R (\uparrow ) 再学習時のMAP@R (\uparrow ) 再学習時のFPR (\downarrow ) 再学習時のEER (\downarrow )
500 0.260 0.308 12.2% 15.0%
1000 0.264 0.296 11.9% 16.1%

データ拡張(Data augmentation)

続いて、入力画像をランダムに編集してデータを水増しするデータ拡張を用いた場合の効果について調べていきたいと思います。

古典的なデータ拡張

まずは数年前から用いられているような、古典的なデータ拡張の手法を試してみたいと思います。利用するデータ拡張の種類は、以下の論文で有効性が示されていたFlip, Rotation, Translation (Shift), Erasingの4種類とします。

search.ieice.org

RotationとTranslation, Scaleについては、データ拡張の強度を以下のハイパーパラメータで調整します。

  • Rotation
    • 回転角: [0, 180]
  • Translation
    • 画像サイズに対する平行移動の大きさ: [0,0.3]
  • Scale
    • 拡大・縮小率: [1,1.3]

これらのハイパーパラメータおよび、Flip, Erasingを適用するかどうかを再びoptunaによりチューニングします。損失関数はContrastive損失を用い、次元数は500とします。そして探索空間を削減するため、損失関数の比較の際にチューニングしておいたハイパーパラメータは最適値で固定しました。

RandAugment

また、より発展的な手法として、データ拡張の手法として近年提案されたRandAugmentも試してみたいと思います。 この手法は、少ないハイパーパラメータの探索空間で対象の問題に良いデータ拡張を実現することを目指しています。 探索すべきハイパーパラメータは、適用するデータ拡張の手法数  N と、データ拡張の強度  M の2つのみです。

論文や解説記事、利用した実装を以下に紹介しておきますので、興味のある方はご覧ください。

論文:

https://arxiv.org/pdf/1909.13719.pdf

実装:

github.com

解説記事:

qiita.com

このケースでは探索空間が小さいため、以下のハイパーパラメータ設定に対してグリッドサーチを行いました。

  •  N: [1,2]
    • N=0の場合はデータ拡張なしに相当するため探索しませんでした。
  •  M: [2,6,10,14,18,22,26,30]

結果

結果は以下のとおりです。データ拡張を行うことによって性能をやや向上できました。

またRandAugmentがFlip, Rotation, Translation, Erasingの4種を組み合わせてチューニングした結果よりも良い性能を達成していることも確認できます。今回RandAugmentはグリッドサーチで18パターンの探索のみでこの性能を得ることができているため、非常に使い勝手も良い手法だと感じました。

データ拡張 探索時のMAP@R (\uparrow ) 再学習時のMAP@R (\uparrow ) 再学習時のFPR (\downarrow ) 再学習時のEER (\downarrow )
なし 0.260 0.308 12.2% 15.0%
古典的なデータ拡張 0.284 0.315 11.6% 15.2%
RandAugment 0.279 0.341 11.3% 14.3%

まとめと今後の課題

以上、しっかりハイパーパラメータチューニングを行った上でいろいろな損失関数やデータ拡張の方法を試してみましたが、まだまだエラー率は11%と、人間の識別能力(エラー率1.2%)には大きく水をあけられてしまっています。

今後はResNet-18よりもより大きなモデルの利用や、教師なし学習による事前学習の利用、そして訓練データ数を増やすなどの方策によってどの程度性能が改善できるかを調べて行けたらと思います。(やる気が続けばですが…)

論文紹介: Sketch Generation with Drawing Process Guided by Vector Flow and Grayscale

紹介論文

最近以下の論文がArxivに投稿されました[1]。(AAAI 2021にも採択されているようです。) 画像を基に鉛筆画を生成する技術ということですが、個人的に興味がある分野なので読んでみました。またこの記事中の図は特記しない限り原論文[1]から引用しています。

arxiv.org

公開実装

著者らにより実装が公開されています。

github.com

ただ手元の画像ですぐに試すには少し不便だったので、Google colaboratoryで簡単なノートブックを作成しました。

手元の画像でCPUインスタンスで実行すると完了までに数時間かかったので、場合によっては小さい画像で試したほうが良いかもしれません。

colab.research.google.com

論文内容

目的と新規性

この論文で取り扱うタスクの目的は、入力画像を鉛筆画に変換することです。

3DモデルとNon-Photorealistic Renderingを用いるアプローチなど、このタスクに対して従来から様々な手法が提案されています。 しかしこれらの従来手法は最終的な鉛筆画のみを出力し、鉛筆画を描いていくプロセスを出力することはできませんでした。

この研究の新規性は最終的な出力のみでなく、以下の図のように一本一本線を引いていくプロセスも出力できることです。

f:id:ronwall1701:20201222182537p:plain

手法

提案手法は鉛筆画とその描画のプロセスを出力するための問題を以下の2つに分解して解いています。

  • 鉛筆の線のシミュレーション (stroke simulation)
  • どのような鉛筆の線を引くかの決定 (stroke drawing)

Stroke simulation

まず最初のstroke simulationでは、実際の鉛筆画の線に近い線を再現することが目的になります。 この目的を達成するため、この研究では実際の鉛筆画で線がどのように引かれているかを分析しています。

線を水平にする

実際の鉛筆画はある一部分のみに注目すると、平行な線によって構成されていることがわかります。 そこで、処理の最初の段階でこのような平行な線が並んでいる部分を切り出し、線が水平になるように回転させます。

線の明度分布を得る

次に、線に垂直な方向(以下の図(a)におけるyの矢印)に並ぶ各ピクセルの明度がどのように変化するかを調べます。 以下の図の例では、この明度の変化の例が(b)に示されています。 この図をみるとわかるように、それぞれの鉛筆の線は明度の変化のグラフ上でV字型のパターンとして現れます。

この特徴を利用すると、グラフ上の明度の極大値の位置で区切る((b)における赤の点線)ことにより、個々の線ごとの「V字型」、すなわち鉛筆の線を垂直に切ったときの明度変化のパターンを取り出すことができます。 さらにこの処理を図(a)のピクセル列(x座標が同じピクセル)それぞれに対して実行することで、それぞれの線毎に複数の「V字型」パターンを得ることができます。

この論文では、この「V字型」に関して以下の仮定をしています

  • 同じ線に注目したとき、線の中心(最も明度が低いピクセル)からの距離が同じピクセルの明度はi.i.d.にサンプルされる
    • この分布をガウス分布と仮定し、平均と分散を計算する
  • 上記平均と分散は、線の中心からの距離 d、中心明度 G 、線の幅  W により以下のようにモデル化される
    •  mean(d)=G+(255-G)\times \frac{2d}{W-1}
    •  variance(d)=(255-G)\times cos \frac{\pi d}{W-1}

f:id:ronwall1701:20201222182826p:plain

Stroke drawing

以上のstroke simulationの節で説明した処理によって、鉛筆画の線を統計的にモデル化することができました。

よって画像を鉛筆画に変換するためには、どのような濃さ、幅、長さの線を、どこに、どの方向に向かって描くかを決めればよいです。

方向の決定

線を描く方向は、入力画像の勾配情報を用いて決定します。

より具体的には、Edge Tangent Flow (ETF) [2]と呼ばれる手法を利用したそうです。勾配に対して垂直な方向が線を引く方向になります。

最後にピクセルごとに得られた方向を予め決められた粒度に量子化し、それぞれの領域の中では平行な線が引かれるような領域に分割してやります。

線を引く

線を引く処理は、上の処理で作成された平行な線が引かれる領域ごとに行われます。

まず最初に各ピクセルの明度を量子化します。この量子化された明度が線の中心の濃さになります。

そしてこの濃さに基づいて、描画対象の領域に予め与えられた線の間の幅(+少しのノイズ)だけ離れた平行線を引いていきます。

  • なお、ここでstroke simulationで導入したモデルにより、線の幅と中心の濃さが決まれば線の幅方向の明度の分布を決めるていると思われます。

複数領域の線の合成

以上の処理で領域ごとに平行線を引いていくことができました。

しかしこれを単に合成すると以下の図の左側のように、領域の境界部分でうまく線がつながらなかったりする現象が起きてしまいます。

この問題を解消するため、後処理として境界部分の線を線の幅だけ伸ばしてやる処理を行います。この後処理を行った例が以下の図の右側です。 ただしこの図からもわかるとおり、この後処理によりぼやけていまう箇所がでてきてしまいます。 そこで、エッジを強調するため、edge mapを最後に乗算する処理も行います。

f:id:ronwall1701:20201223185443p:plain

線の描画順の決定

これまでの処理で鉛筆画を構成する線のリストが得られたため、後はこれらの線をどのような順番で描いていくかを決めればよいです。

実際の画家が鉛筆画を描くときにはくっきりとした物体の輪郭に最初に線を引く、という観察に基づき、この論文では以下のように線の明度 Gと線に対応する領域 D内のピクセルにおける勾配の大きさ T_iに基づくスコアを計算して大きい順に線を引いていきます。

 S=(255-G) \times \sum_{i\in D}T_i

実際に使ってみた

最初の方で紹介したcolabノートブックを使って、実際に写真を鉛筆画に変換してみました。以下の写真は論文から引用したものではなく私が用意したものになります。

入力画像はこちらのうな重の画像です。

f:id:ronwall1701:20201223194223j:plain

そして最終的な出力はこちらのようになりました。

f:id:ronwall1701:20201223194452j:plain

またこの手法の特徴として、以下のように鉛筆画生成の途中結果も生成することができます。

f:id:ronwall1701:20201223194903j:plain

うな重のお盆に筋状の模様が入っているせいか、そこに引きずられてスプーンやおさらの輪郭より前にお盆のディテールを描いてしまっているところが目に付きますね。 Semantic segmentationとかの出力を使って上手くこの辺を改善できないかが気になります。

感想

論文を見かけたときにはもう少し機械学習要素が強い手法かと思っていましたが、実際に読んでみるとかなり地道な分析や処理を積み上げているようで少し意外でした。

写真から手書き風の背景を作成するなど、創作向けの応用で使えないかな〜といろいろ考えています。

最後までお読み頂きありがとうございました。なにか間違えているところなどあればコメントなどでご連絡ください!それでは!

参考文献

[1] Tong, Zhengyan, et al. "Sketch Generation with Drawing Process Guided by Vector Flow and Grayscale." arXiv preprint arXiv:2012.09004 (2020).

[2] Kang, Henry, Seungyong Lee, and Charles K. Chui. "Coherent line drawing." Proceedings of the 5th international symposium on Non-photorealistic animation and rendering. 2007.

学習済みspeaker embeddingの性能比較をしてみた

はじめに

皆さんは声優の声を分類したり比較したりしてみたいと思ったことはありませんか? 関連する技術として、話者認証(speaker verification)などの分野で、人の声が似ているかどうかを判断するためのベクトル表現としてspeaker embeddingを計算するモデルがいろいろと研究されているようです。

しかし一からモデルを学習するのはデータも計算リソースも時間も必要なので大変です。 そこで、今回は事前学習済みモデルを公開してくださっている研究を探して、実際に日本語のデータで声優の声を区別する性能を比較してみることにしました。

実験コードは公開しています。この記事に含まれていない事前学習済みモデルなどをご存じの場合はコメントなどで教えていただければ幸いです!

学習済みspeaker embeddingモデルの比較実験

比較方法

比較実験には公開されている事前学習済みモデルをそのまま利用しました。特にfine-tuningなどは行っていません。

データ

テストデータには、100人の声優が日本語テキストを読み上げた音声データを収録しているJVS (Japanese versatile speech)コーパスを利用しました。

評価指標

得られたspeaker embeddingを用いて与えられた音声ペアが同じ声優の音声かどうかを分類し、性能を比較しました。

具体的には、以下のようなpositive, negative音声ペアを各クラス10000個ずつランダムに作成し、ペアごとの類似度を分類に用いるスコアとして利用しました。(当然各比較実験で利用されるペアは手法が違っても同一としました。)

  • positive
    • 話者が同じ2つの音声のペア
  • negative
    • 話者が異なる2つの音声のペア

以下の結果ではこのスコアの評価指標としてEERを示しています。

話者認証の研究では複数のenroll voiceを利用した認証の性能をEERで示しているようですが、今回は実装の簡便さのために上記のような方法を取っています。

実装コード

評価の際に利用したGoogle colaboratoryのコードを公開しています。

JVSコーパスのデータセットのzipファイルをGoogle driveresearch/jvs_ver1.zipとして配置してある前提です。(Google driveのマウントが必要です。)

結果

評価の結果は以下のようになりました。

juanmc2005/SpeakerEmbeddingLossComparisonにおいて公開されている事前学習済みのモデルが最も性能が良い結果になっています。

実装 EER Google colaboratory 動作例 Note 
Jungjee/RawNet [Jung+20] 23.5% サンプル 固定長への切り取りをランダムに15回行い平均。
CorentinJ/Real-Time-Voice-Cloning [Jia+18] 20.6% サンプル
philipperemy/deep-speaker [Li+17] 18.0% サンプル
juanmc2005/SpeakerEmbeddingLossComparison [Coria+20] 16.8% サンプル 論文によると、SincNet [Ravanelli+18] による特徴量抽出とx-vector [Snyder+18]をend-to-endにadditive angular margin lossで学習しています。

おわりに

今回は事前学習済みモデルが公開されているspeaker embeddingモデルを比較してみました。 今後はこれらのモデルを利用していろいろと遊んでいければと思っています。

参考文献

  • [Coria+20]
    • Coria, Juan M., et al. "A Comparison of Metric Learning Loss Functions for End-To-End Speaker Verification." arXiv preprint arXiv:2003.14021 (2020).
  • [Jia+18]
    • Jia, Ye, et al. "Transfer learning from speaker verification to multispeaker text-to-speech synthesis." Advances in neural information processing systems 31 (2018): 4480-4490.
  • [Jung+20]
    • Jung, Jee-weon, et al. "Improved RawNet with Feature Map Scaling for Text-independent Speaker Verification using Raw Waveforms." Proc. Interspeech 2020 (2020): 1496-1500.
  • [Li+17]
    • Li, Chao, et al. "Deep Speaker: an End-to-End Neural Speaker Embedding System." arXiv (2017): arXiv-1705.
  • [Ravanelli+18]
    • Mirco Ravanelli and Yoshua Bengio. Speaker Recognition from Raw Waveform with SincNet. 2018 IEEE Spoken Language Technology Workshop (SLT), pages 1021–1028,
  • .
  • [Snyder+18]
    • David Snyder, Daniel Garcia-Romero, Gregory Sell, Daniel Povey, and Sanjeev Khudanpur. X-Vectors: Robust DNN Embeddings for Speaker Recognition. 2018 IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP), pages 5329–5333, 2018.

クロスシミュレーションされた布の頂点を動的に制御する方法

記事のまとめ

  • クロスシミュレーションがついているメッシュの頂点を制御する方法を紹介
  • 制御したいときだけ有効にできるような方法
  • HookモディファイアとVertex Weight Mixモディファイアを利用
  • 面倒な初期設定を行うためのスクリプトも紹介

やりたいこと

オブジェクトに被さった布をつまみ上げたり、お姫様キャラのモデルに挨拶をさせるときにスカートを摘まみ上げたり、「布をつまみあげる」操作をしたいケースって結構あると思います。

ただ基本的には常に頂点を制御したいわけではなく、布をつまみ上げている時だけ制御して、他のフレームでは普通にクロスシミュレーションに従って欲しいわけです。

やりたいことの実現例

要は、こちらの動画のようなことをやりたいわけです。

f:id:ronwall1701:20201218151709g:plain

こちらの例では、クロスシミュレーションされている布上のある頂点(片方のEmptyが追従している点)を、一時的に制御用のEmptyに追従させ持ち上げています。それ以外の時点では、頂点は他の部分と同じようにクロスシミュレーションに従っています。

どうやるのか

クロスシミュレーションで一部の頂点の位置を固定するには、ピン止めする頂点グループ(以降pinグループと呼ぶ)をあらかじめ設定しておけばよいです。

しかし今回は

  1. 制御対象の頂点を動的に変更したい
  2. (例えば今回のEmptyのように)制御対象の頂点を別オブジェクトに追従させたい

という2つの要求があるので、単にピン止めする頂点を静的に設定しておくだけではだめです。

今回は、この要求をそれぞれVertex Weight Mixモディファイアと、Hookモデイファイアで解決しました。

Vertex Weight Mixモディファイア

このモディファイアは、オブジェクトの各頂点がある頂点グループAに対して持つ重みを、別の頂点グループBの重みを用いて変更する機能をもっています。

今回のケースでは、常に固定したい頂点グループ(pinグループ)の重みに、一時的に制御したい頂点グループ(hookグループ)の重みを足し合わせるような処理を行えば良さそうです。そうすることによって、クロスシミュレーションで固定される頂点グループを動的に変更することができます。

そのような処理を行うためのパラメータは、例えば以下のようになります。ここで、pinは常に固定する頂点を含む頂点グループで、hook1が動的に制御を切り替える頂点を含む頂点グループです。(どちらも同じクロスオブジェクトの頂点グループです。)

f:id:ronwall1701:20201218151841p:plain

Hookモディファイア

このモディファイアは、オブジェクトの一部の頂点の位置を別のオブジェクトに追従させるような機能をもっています。

今回のケースでは、一時的に制御したい頂点を含む頂点グループ(hookグループ)を、(先ほどのEmptyのような)制御用のオブジェクトに追従させれば良さそうです。そうすることによって、制御用オブジェクトの位置変化を制御対象の頂点に反映させ、クロスシミュレーションに用いることができます。

パラメータ設定は、今回は以下のようにしました。

f:id:ronwall1701:20201218151845p:plain

パラメータの動的変更

以上の2つのモディファイアのパラメータを(同時に)変更することによって、対象となる頂点を動的に変更しながら位置を制御することができます。より具体的には、以下のパラメータを設定します:

  • Vertex Weight MixモディファイアのGlobal Influenceパラメータ
  • 0.0でオフ、1.0でオン
  • HookモデイファイアのStrengthパラメータ
  • 0.0でオフ、1.0でオン

ただし、このパラメータの変更速度が速すぎるとシミュレーションの結果が破綻するケースが確認されたので、これらのパラメータはある程度の余裕をもたせてゆっくり変更すると良いかもしれません。

モディファイア設定コード

布の複数の場所を別々につまみたい場合、以上の設定を場所ごとにすべて設定しなければなりません。それでは面倒なので、今回は以上のモディファイアの設定を自動で行うスクリプトを用意しました。

使い方ですが、動的に制御したい頂点を編集モードで選択した状態でScriptingウィンドウなどから実行してください。この時スクリプトの最初の方にあるNAME変数とPIN_GROUPはそれぞれ適切に変更してから実行してください。(以下の説明も参照)

  • NAME
  • 動的に制御される頂点の頂点グループ、制御・追従用のEmptyの名前に利用される。
  • _Tracer.{NAME}, _Handler.{NAME}がそれぞれ追従用、制御用のEmpty
  • 追従用のEmptyは制御対象の頂点に常に追従する
  • PIN_GROUP
  • 常に固定される頂点を含む頂点グループの名前
  • この頂点グループがクロスシミュレーションのpin止め頂点グループとして登録されていると仮定

実際に制御を開始する際には、追従用のEmptyの位置を利用するなどして、制御対象の頂点の位置と制御用のEmptyの位置があまり離れないようにした方が、破綻などが起こりづらいかもしれません。

# NOTE: 編集モードで、つかみたい頂点を選択している状態で実行すること

from functools import reduce
import bpy

NAME = "hook1"
PIN_GROUP = "pin"

# 選択頂点の座標を読み込む
mode = bpy.context.active_object.mode
bpy.ops.object.mode_set(mode='OBJECT')
selectedVerts = [v for v in bpy.context.active_object.data.vertices if v.select]
selectedVertsId = [v.index for v in bpy.context.active_object.data.vertices if v.select]
target_obj = bpy.context.active_object

# 掴む対象の頂点グループを作成
hook_group = target_obj.vertex_groups.new(name=NAME)
hook_group.add(selectedVertsId, 1.0, "ADD")

# 選択頂点のグローバル座標を計算
local2world = target_obj.matrix_world
pos = reduce(sum, [local2world @ v.co for v in selectedVerts]) / len(selectedVerts)
print(pos)

# 頂点位置追従用のEmptyと、位置設定用のEmptyを作成する
bpy.ops.object.add(type="EMPTY", location=pos)
objs = bpy.context.selected_objects
assert len(objs)==1
tracer_empty = objs[0]
tracer_empty.name = f"_Tracer.{NAME}"

bpy.ops.object.add(type="EMPTY", location=pos)
objs = bpy.context.selected_objects
assert len(objs)==1
handle_empty = objs[0]
handle_empty.name = f"_Handle.{NAME}"

# 対象のオブジェクトのトップにモディファイアを設定する関数
def add_modifier(type, obj):
initial_names = set([m.name for m in obj.modifiers])
bpy.context.view_layer.objects.active = obj
bpy.ops.object.modifier_add(type=type)
after_names = set([m.name for m in obj.modifiers])
modifier_name = list(after_names - initial_names)[0]
while obj.modifiers[0].name != modifier_name:
bpy.ops.object.modifier_move_up(modifier=modifier_name)

return obj.modifiers[modifier_name]

# Hookモデイファイアの設定
modifier = add_modifier("HOOK", target_obj)
modifier.falloff_type = "NONE"
modifier.object = handle_empty
modifier.strength = 1.0
modifier.vertex_group = NAME

# Vertex Weight Mixモデイファイアの設定
modifier = add_modifier("VERTEX_WEIGHT_MIX", target_obj)
modifier.vertex_group_a = PIN_GROUP
modifier.vertex_group_b = NAME
modifier.mix_set = "B"
modifier.mix_mode = "ADD"

# 
constraint = tracer_empty.constraints.new("COPY_TRANSFORMS")
constraint.target = target_obj
constraint.subtarget = NAME

さいごに

物理シミュレーションのキャッシュが残っていたり、制御が急すぎると破綻したりするので結構扱いが難しいです。とはいえいろいろ調べても他に良さそうな方法も出てこず…。何か別のアプローチなどご存じの方はお教えください。

スカートメッシュにボーンを自動でセットアップするスクリプト

はじめに

Vtuberが流行ってだいぶ経ってしまいましたが、最近今さらながらBlenderでの3Dキャラクターモデル作りにはまっています。せっかくスカートをモデリングしてもカチコチの剛体だと流石にアレなので、Blenderの物理シミュレーションを使ってみたいですよね。

今回は、その作業を一部自動化するときに使ったスクリプトについて書いていきたいと思います。

なお、こちらの記事は以前別の場所で公開していたものを移植してきたものです。

目的

クロスシミュレーションが適用されているメッシュがあったとき、そのメッシュの変形に追従するようなボーンを自動で設定する。

補足

そもそもなんでクロスシミュレーションに追従するボーンが必要なんじゃ、という話ですが、例えば:

  • クロスシミュレーションの結果をボーンのアニメーションに変換してBlender以外のソフトに出力する。
  • 同じクロスシミュレーションの結果を複数のオブジェクト(二重のスカートとか)に反映させる。

というような使い道があると思います。(私のケースでは後者でした。)

参考にしたサイト

https://dskjal.com/blender/skirt-setup-script.html

実際にやろうとしていることはこちらで紹介されているスクリプトと同様のタスクです。 ただこちらはBlender 2.7時点で開発されていたものらしく、Blender 2.8での使い方がよくわかりませんでした。

https://blender.stackexchange.com/questions/41235/how-to-make-rig-that-reacts-to-gravity-or-seem-to-behave-physically-correct

そこで今回は勉強もかねて、こちらのサイトで紹介されているプロセスを自動化する方針で実装しました。

コード

https://github.com/kosuke1701/add-skirt-bones

使い方

1. クロスシミュレーションを適用するメッシュを作成する。

今回のスクリプトでは以下のような円柱状の四角ポリゴンからなるメッシュを想定しています。(以後このメッシュをクロスメッシュと呼びます。) このメッシュには既にクロスシミュレーションの設定がされているものとします。

f:id:ronwall1701:20201218150802p:plain

2. パラメータを設定する

skirt_bones.pyのパラメータを直接編集します。(パネルとかでもっとかっこよくできるとは思いますが、現状はこれで…)

パラメータは8~12行目のもので、それぞれ以下のような意味があります。

パラメータ名 役割
N_STEP_HORI クロスメッシュの列(上下方向のエッジ)のうち、実際にボーンを設定する間隔
N_STEP_VERT クロスメッシュの列(水平方向のエッジループ)のうち、実際にボーンを設定する間隔
UPPER_DIR クロスメッシュの上方向。(クロスメッシュは、各列のうち最も上にある頂点が固定されるものと仮定しています。)
COL_NAME 大量のEmptyオブジェクトが生成されるため、それらをまとめておくCollection名
HEAD_NAME ボーンやEmpty、アーマチュアの名前に利用される文字列

最初の2つのパラメータは、クロスメッシュの全エッジにボーンを設定するとボーン数が多くなりすぎてしまうような場合に、間を間引きする目的で設定しています。

また3つ目のUPPER_DIRをデフォルトのZ方向でなくX,Y方向とすることで、スカートだけでなく袖などにも同じスクリプトを利用することができます。

3. スクリプトの実行

クロスメッシュをObject Modeで選択している状態でskirt_bones.pyを実行します。

(Text EditorビューのTextメニューからスクリプトを開き、同じくTextメニューのRun Scriptから実行できます)

実行すると、以下のようにクロスメッシュのエッジに沿ってボーンが作成されます。

f:id:ronwall1701:20201218150844p:plain

実際にクロスシミュレーションを実行すると、以下のようにクロスメッシュの動きにボーンが追従することを確認することができます。

f:id:ronwall1701:20201218150915g:plain

4. 設定されたボーンを使ってモデルを動かす

実際に作成されたボーンを使ってスカートモデルを動かすあたりの作業については、こちらのサイトが参考になると思います。

https://aobayu.hatenablog.com/entry/2019/03/20/111212

おわりに

BlenderPythonスクリプトを動かすのは初めての経験だったため、勝手がわからずコーディングにはかなり苦労しました。(その結果がスクリプトの汚さにも反映されています…)

ただこの処理を自動化したおかげで、スカートに対し行った処理を左右の袖に対しても簡単に行うことができ、かなり楽をすることができました。

やり方を覚えてしまえばPythonBlenderにおけるかなり強力なツールだと思うので、今後も使いどころを見つけて使っていきたいと思います。 最後まで読んでいただき、どうもありがとうございました。