5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

evision で画像から顔を検出する

5
Last updated at Posted at 2023-02-12

はじめに

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 を使ってください

https://rancherdesktop.io/

セットアップ

必要なモジュールをインストールします

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())

lenna

正面の顔の検出

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)

lenna_frontal.png

確かに正しそうです

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)

eyes.png

ちゃんとそれぞれ位置は妥当ですね

横顔の検出

横顔のモデルを実行します

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) だと上下左右反転になります

https://hexdocs.pm/evision/0.1.28/Evision.html#flip/2

結果は以下のようになり、顔を検出できました

[{130, 177, 221, 221}]

バウンディングボックスを反転した画像に描き込みます

img
|> Evision.flip(1)
|> draw_rects.(profile_face_rect_list)

lenna_profile.png

位置も正しいですね

まとめ

物体検出では深層学習モデル(DNN)が主流になっているので、実務で使うことはあまりないと思いますが、色々なモデルがあって面白いですね

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?