ほぼノーコードで顔認証を実装する

## はじめに

Elixir の Livebook を使うと、ほぼノーコードで顔認証が実装できます

![スクリーンショット 2023-07-10 11.43.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/b34edf9d-e968-1603-27bb-794ad90bb65e.png)

書くのも依存モジュールのインストールだけなので、ロジックは不要です

## 実行環境

- Elixir: 1.14.2
- OTP: 24
- Livebook: 0.9.3

こちらの @nako_sleep_9h さんの記事に Livebook のはじめ方を書いているので、是非使ってみてください

https://qiita.com/nako_sleep_9h/items/585786648d2b7560d010

:::note warn
evision をコードからビルドする関係で、実行環境によっては動かない可能性があります

私の使っているコンテナであれば確実に動きます

https://github.com/RyoWakabayashi/elixir-learning

また、 localhost で実行している場合（https 通信になっていない場合）、ブラウザによってカメラが起動しないことがあります

その場合は [ngrok](https://ngrok.com/) 等を使い、 https 通信で Livebook にアクセスしてください
:::

## セットアップ

Livebook を開いたら、右上 "+ New notebook" ボタンをクリックし、新しいノートブックを開きます

![スクリーンショット 2023-07-06 13.29.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d58beefa-622b-8bd1-9910-a2ba3d02a960.png)

"Notebook dependencies and setup" と書かれている枠（セル）の中をクリックすると、セルの中が黒くなります

![setup_cell.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ec17f418-edcb-aac9-5dda-95bcf946aa3d.gif)

そこに以下のコードを入力します

```elixir
Mix.install([
  {:kino, "~> 0.9"},
  {:evision, github: "cocoa-xu/evision", branch: "main"}
],
system_env: [
  {"EVISION_PREFER_PRECOMPILED", "false"}
])
```

書くコードはこれだけです

セルの上に出ている "Setup" ボタンをクリックし、セットアップを実行します

モジュールのインストールが開始され、しばらくすると ":ok" と表示されます

:::note info
2023/7/10 現在

evision にいくつかの PR を出し、GitHub 上の main ブランチでは解消されていますが、未リリース状態です

https://github.com/cocoa-xu/evision/pull/204
https://github.com/cocoa-xu/evision/pull/205
https://github.com/cocoa-xu/evision/pull/207
https://github.com/cocoa-xu/evision/pull/208

そのため、この記事ではコードからビルドしています（なので、インストールにすごく時間がかかります）
リリースされた場合は Hex からのインストールに変更します
:::

## スマートセルの追加

"Section" の文字と、その下のセルの間、左右中央あたりにマウスカーソルを持っていくと、 "+ Code" "+ Block" "+ Smart" の３つのボタンが表示されます

"+ Smart" のボタンをクリックします

出てくるドロップダウンの中から "Evision: OpenCV Model Zoo" をクリックします

![smart_cell.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/36539a57-c2a0-659a-ca05-00a68ab9fd36.gif)

表示されたフォーム内左上の TASK をクリックすると、選択肢が表示されます

![スクリーンショット 2023-07-05 21.06.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ff882ad6-8bc7-03d8-bc30-1d448661e1cf.png)

選択肢から "Face recognition" をクリックします

以下のようにフォームが変化するので、左上の "Evaluate" をクリックします

![スクリーンショット 2023-07-06 11.40.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/c09f3140-4d3c-96a4-5e46-7b11f95d6fda.png)

少しすると、画像アップロード用のエリアが２つ表示されます

![スクリーンショット 2023-07-05 21.07.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6b0cf00c-1806-eedc-3000-ac3adda9424f.png)

これで顔認証の準備ができました

上で選んだ画像に映る顔と、下で選んだ画像に映る顔を比べ、同じ顔かどうかを判定してくれます

## 顔認証の実行

上画像には認証したい個人の顔写真をアップロードします

"Upload" ボタンから画像ファイルを選択、もしくは画像ファイルをドラッグ＆ドロップでアップロード可能です

![スクリーンショット 2023-07-06 11.45.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/55f07229-96e0-f66a-46f9-f71479b16884.png)

下画像には、上画像の個人と同じ人物かどうか判定したい写真を指定します

今回はカメラから写真を撮影するので、 "Open camera" をクリックします

ブラウザから以下のように許可を求めるメッセージが表示されたら「許可する」などをクリックし、カメラの使用を許可します

![スクリーンショット 2023-07-05 21.09.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/24ad80d1-efa8-8a02-97a5-66d63198ed29.png)

どのカメラを使うか選択肢が出るので、使いたいカメラ（基本的に「System Default」）をクリックします

![スクリーンショット 2023-07-05 21.10.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/de74f51d-ded5-c404-fe6c-21826c327ec7.png)

カメラ映像が表示されたら、任意のタイミングで "Take photo" をクリックします

カメラ映像が消えて少し待つと、クリックした瞬間の静止画が表示されます

この状態で "Run" ボタンをクリックすると、顔認証が実行されます

![recognition.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/57e4c0da-b598-6d53-7943-aed3f2f83fee.gif)

上画像と同じ人物だった場合、以下のような結果が表示されます

```
Result: true, cosine_score: 0.6452961420286556
```

`Result: true` なので、同一人物と判定しています

上画像を別の顔(AI に生成させた適当な人)に変えてみます

![スクリーンショット 2023-07-10 11.53.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/fce2e521-a830-d746-d095-d82bcd78cb41.png)

`Result: false` で別人として判定できました

## タネ明かし

スマートセルをコードに変換すると、以下のようになります

```elixir
recognizer =
  Evision.Zoo.FaceRecognition.SFace.init(:default_model,
    backend: Evision.Constant.cv_DNN_BACKEND_OPENCV(),
    target: Evision.Constant.cv_DNN_TARGET_CPU(),
    distance_type: :cosine_similarity,
    cosine_threshold: 0.5,
    l2_norm_threshold: 1.128
  )

detector =
  Evision.Zoo.FaceDetection.YuNet.init(:default_model,
    backend: Evision.Constant.cv_DNN_BACKEND_OPENCV(),
    target: Evision.Constant.cv_DNN_TARGET_CPU(),
    nms_threshold: 0.3,
    conf_threshold: 0.9,
    top_k: 5
  )

original_input = Kino.Input.image("Original")
comparison_input = Kino.Input.image("Comparison")

form =
  Kino.Control.form([original: original_input, comparison: comparison_input],
    submit: "Run"
  )

frame = Kino.Frame.new()

form
|> Kino.Control.stream()
|> Stream.filter(&(&1.data.original != nil or &1.data.comparison != nil))
|> Kino.listen(fn %{data: %{original: original_image, comparison: comparison_image}} ->
  Kino.Frame.render(frame, Kino.Markdown.new("Running..."))

  original_image =
    Evision.Mat.from_binary(
      original_image.data,
      {:u, 8},
      original_image.height,
      original_image.width,
      3
    )

  comparison_image =
    Evision.Mat.from_binary(
      comparison_image.data,
      {:u, 8},
      comparison_image.height,
      comparison_image.width,
      3
    )

  original_results = Evision.Zoo.FaceDetection.YuNet.infer(detector, original_image)
  comparison_results = Evision.Zoo.FaceDetection.YuNet.infer(detector, comparison_image)

  case {original_results, comparison_results} do
    {%Evision.Mat{}, %Evision.Mat{}} ->
      original_bbox = Evision.Mat.to_nx(original_results, Nx.BinaryBackend)[0][0..-2//1]

      comparison_bbox =
        Evision.Mat.to_nx(comparison_results, Nx.BinaryBackend)[0][0..-2//1]

      original_blob =
        Evision.FaceRecognizerSF.alignCrop(recognizer, original_image, original_bbox)

      original_feature =
        Evision.FaceRecognizerSF.feature(recognizer, original_blob)
        |> Evision.Mat.to_nx()
        |> Evision.Mat.from_nx()

      comparison_blob =
        Evision.FaceRecognizerSF.alignCrop(recognizer, comparison_image, comparison_bbox)

      comparison_feature =
        Evision.FaceRecognizerSF.feature(recognizer, comparison_blob)
        |> Evision.Mat.to_nx()
        |> Evision.Mat.from_nx()

      %{matched: matched, retval: val, measure: measure} =
        Evision.Zoo.FaceRecognition.SFace.match_feature(
          recognizer,
          original_feature,
          comparison_feature
        )

      original_image =
        Evision.cvtColor(original_image, Evision.Constant.cv_COLOR_RGB2BGR())

      comparison_image =
        Evision.cvtColor(comparison_image, Evision.Constant.cv_COLOR_RGB2BGR())

      vis_original =
        Evision.Zoo.FaceDetection.YuNet.visualize(original_image, original_results[0])

      vis_comparison =
        Evision.Zoo.FaceDetection.YuNet.visualize(comparison_image, comparison_results[0])

      vis = [
        Kino.Image.new(Evision.imencode(".png", vis_original), :png),
        Kino.Image.new(Evision.imencode(".png", vis_comparison), :png)
      ]

      Kino.Frame.render(frame, Kino.Layout.grid(vis, columns: 2))

      Kino.Frame.append(
        frame,
        Kino.Markdown.new("Result: #{matched}, #{measure}: #{val}")
      )

    {{:error, _}, %Evision.Mat{}} ->
      Kino.Frame.render(
        frame,
        Kino.Markdown.new("Cannot detect any face in the original image")
      )

    {%Evision.Mat{}, _} ->
      Kino.Frame.render(
        frame,
        Kino.Markdown.new("Cannot detect any face in the comparison image")
      )

    {_, _} ->
      Kino.Frame.render(
        frame,
        Kino.Markdown.new("Cannot detect any face in both original and comparison images")
      )
  end
end)

Kino.Layout.grid([form, frame], boxed: true, gap: 16)
```

Evision が [YuNet](https://github.com/ShiqiYu/libfacedetection) で顔を検出し、 [SFace](https://github.com/zhongyy/SFace) で顔の特徴量を取得しています

２つの顔のコサイン類似度を求めることで、同一人物かどうかの判定を行なっています

## まとめ

使っている技術自体は OpenCV なのでそんなに新しくはないのですが、 SmartCell にすることでノーコード化され、誰でもすぐに顔認証が実装できるようになりました