はじめに
Elixir の evision (OpenCV) を使って、画像から顔を検出します
@the_haigo さんがすでにやっていたので、もう少し詳しくやります
いつものように Livebook を使います
実装したノートブックはこちら
実行環境
- Elixir: 1.14.2 OTP 24
- Livebook: 0.8.1
以下のリポジトリーの Docker コンテナ上で起動しています
Docker が使える環境であれば簡単に実行できます
https://docs.docker.com/engine/install/
Docker Desktop を無償利用できない場合は Rancher Desktop を使ってください
セットアップ
必要なモジュールをインストールします
Mix.install([
{:req, "~> 0.3"},
{:evision, "~> 0.1"},
{:kino, "~> 0.8"}
])
Req はモデルファイルや画像を Web からダウンロードするのに使っています
モデルのダウンロード
OpenCV の GitHub リポジトリーから Haar カスケード分類器のモデルファイルをダウンロードしてきます
prefix = "https://github.com/opencv/opencv/raw/master/data/haarcascades"
frontal_face_model_path = "frontal_face_model.xml"
"#{prefix}/haarcascade_frontalface_default.xml"
|> Req.get!(connect_options: [timeout: 300_000], output: frontal_face_model_path)
eye_model_path = "eye_model.xml"
"#{prefix}/haarcascade_eye.xml"
|> Req.get!(connect_options: [timeout: 300_000], output: eye_model_path)
profile_face_model_path = "profile_face_model.xml"
"#{prefix}/haarcascade_profileface.xml"
|> Req.get!(connect_options: [timeout: 300_000], output: profile_face_model_path)
- frontal_face_model.xml: 正面の顔を検出するモデル
- eye_model.xml: 目を検出するモデル
- profile_face_model.xml: 横顔を検出するモデル
他にも色々なモデルが公開されています
Haar カスケードの原理については以下のサイトが参考になります
ざっくり仕組みを解説すると、以下のような流れで物体を検出しています
- 一つのモデルは複数の分類器をカスケード(連結)したものになっている
- 一つの分類器は以下の処理を行う
- 画像から一部の領域(四角形)を切り取る
- 切り取った領域が対象の物体なのかどうか分類する
- 領域の位置や大きさを変化させながら画像全体の領域を分類していく
- 「対象の物体である」と分類された領域は、次の分類器に送られる
- 最初はざっくりとした分類器で、進むにつれ厳密な分類器になる
- 全ての分類器で「対象の物体である」と分類された領域を「画像内で対象の物体が存在する領域」とする
モデルファイルの読み込み
各モデルファイルを読み込み、カスケード分類器を用意します
カスケード分類器の読み込みは Evision.CascadeClassifier.cascadeClassifier で行います
frontal_face_model = Evision.CascadeClassifier.cascadeClassifier(frontal_face_model_path)
eye_model = Evision.CascadeClassifier.cascadeClassifier(eye_model_path)
profile_face_model = Evision.CascadeClassifier.cascadeClassifier(profile_face_model_path)
画像のダウンロード
いつものレナさんをダウンロードして画像として読み込みます
img =
"https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png"
|> Req.get!()
|> then(& &1.body)
|> Evision.imdecode(Evision.Constant.cv_IMREAD_COLOR())
正面の顔の検出
detectMultiScale
カスケード分類器による物体検出は Evision.CascadeClassifier.detectMultiScale で実行します
face_rect_list = Evision.CascadeClassifier.detectMultiScale(frontal_face_model, img)
実行結果は以下のように、位置情報(バウンディングボックス)の配列になっています
[{217, 201, 173, 173}]
一つのバウンディングボックスはタプルになっていて、左から以下の値を表します
- 左上X座標
- 左上Y座標
- 幅
- 高さ
検出結果が正しいかどうか、バウンディングボックスを画像上に描画します
四角形の配列を画像に描画する関数を用意します
draw_rects = fn img, rect_list ->
Enum.reduce(rect_list, img, fn rect, drawed_mat ->
{left, top, width, height} = rect
Evision.rectangle(
drawed_mat,
{left, top},
{left + width, top + height},
{255, 0, 0},
thickness: 4
)
end)
end
関数を実行します
draw_rects.(img, face_rect_list)
確かに正しそうです
detectMultiScale2
物体検出には detectMultiScale2 という関数も用意されています
{face_rect_list, number_of_detections} =
Evision.CascadeClassifier.detectMultiScale2(frontal_face_model, img)
実行結果は以下のようになります
{[{217, 201, 173, 173}], 'D'}
実行結果は {<バウンディングボックスの配列>, <各バウンディングボックスの近傍に検出された領域数>} となっており、 detectMultiScale よりも返り値が増えています
'D' と言われても何のことが分かりませんが、これは領域数の配列が文字リストだと判断されているせいです
以下のようにして値を取り出します
hd(number_of_detections)
すると値が 68 と返ってきます
つまり顔として検出できた領域には、最初のざっくり分類器の時点で68個の顔領域候補がありました、ということを表しています
が、その情報は特に何の意味もないので使いません
detectMultiScale3
更に detectMultiScale3 という関数も用意されています
optional ですが、引数に outputRejectLevels: true を指定しないとエラーになります
こちらは3つの値が返ってきます
{face_rect_list, reject_levels, level_weights} =
Evision.CascadeClassifier.detectMultiScale3(frontal_face_model, img, outputRejectLevels: true)
実行結果は以下のようになります
{[{217, 201, 173, 173}], [25], [8.02876805968117]}
{<バウンディングボックスの配列>, <何番目の分類器で排除されたかの配列>, <バウンディングボックスの確信度の配列>} という形です
2番目の値に意味はありませんが(最終的に排除されなかった領域なので)、最後の値は確信度なので利用価値がありそうです
目の検出
目の検出モデルを実行してみましょう
eye_rect_list = Evision.CascadeClassifier.detectMultiScale(eye_model, img)
実行結果は以下のとおりで、両目を検出できています
[{242, 241, 51, 51}, {308, 245, 43, 43}]
検出位置を可視化します
draw_rects.(img, eye_rect_list)
ちゃんとそれぞれ位置は妥当ですね
横顔の検出
横顔のモデルを実行します
profile_face_rect_list = Evision.CascadeClassifier.detectMultiScale(profile_face_model, img)
実行結果は [] となり、顔は検出できませんでした
このモデルは顔の左側しか学習していないため、主に顔の右側を見せているこの写真では検出できないのです
向きを変えてみましょう
profile_face_rect_list =
img
|> Evision.flip(1)
|> then(&Evision.CascadeClassifier.detectMultiScale(profile_face_model, &1))
Evision.flip(img, 1) で画像を左右反転しています
Evision.flip(img, 0) だと上下反転、 Evision.flip(img, -1) だと上下左右反転になります
結果は以下のようになり、顔を検出できました
[{130, 177, 221, 221}]
バウンディングボックスを反転した画像に描き込みます
img
|> Evision.flip(1)
|> draw_rects.(profile_face_rect_list)
位置も正しいですね
まとめ
物体検出では深層学習モデル(DNN)が主流になっているので、実務で使うことはあまりないと思いますが、色々なモデルがあって面白いですね



