Livebook から Amazon Rekognition の画像 AI を使って物体検出や顔検出、感情分析を行う

## はじめに

Livebook　から AWS のサービスを操作するシリーズです

今回は Amazon Rekognition を操作します

Amazon Rekognition は画像 AI サービスで、物体検出や顔識別などができます

https://aws.amazon.com/jp/rekognition/

今回は物体検出と顔検出、環状などの顔情報の分析を行います

実装したノートブックはこちら

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/aws/rekognition.livemd

## 事前作業

AWS のアカンウトと、 Rekognition の権限を持った IAM ユーザーと、その認証情報（ACCESS_KEY_ID と SECRET_ACCESS_KEY）が必要です

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/id_users_create.html#id_users_create_console

## 実行環境

Livebook 0.7.2 の Docker　イメージを元にしたコンテナで動かしました

https://hub.docker.com/layers/livebook/livebook/0.7.2/images/sha256-23b577ff5c956fa017bc148d41e219d3697b159813feeefbfc37a6853ccaf62e?context=explore

コンテナ定義はこちらを参照

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

## セットアップ

ex_aws_rekognition を中心に、必要なモジュールをインストールします

https://hexdocs.pm/ex_aws_rekognition/ExAws.Rekognition.html

また、データ分析のために Explorer 、画像処理のために Evision などもインストールします

```elixir
Mix.install([
  {:ex_aws, "~> 2.0"},
  {:ex_aws_rekognition, "~> 0.6"},
  {:poison, "~> 5.0"},
  {:hackney, "~> 1.18"},
  {:sweet_xml, "~> 0.7"},
  {:explorer, "~> 0.4"},
  {:evision, "~> 0.1"},
  {:download, "~> 0.0.4"},
  {:kino, "~> 0.8"}
])
```

エイリアス等の準備をします

```elixir
alias ExAws.Rekognition
alias Explorer.DataFrame
alias Explorer.Series
require Explorer.DataFrame
```

## 認証

入力エリアを用意し、そこに IAM ユーザーの認証情報を入力します

ACCESS_KEY_ID　と SECRET_ACCESS_KEY は秘密情報なので、値が見えないように `Kino.Input.password` を使います

https://qiita.com/RyoWakabayashi/items/debb1d048613ce505988

```elixir
access_key_id_input = Kino.Input.password("ACCESS_KEY_ID")
```

```elixir
secret_access_key_input = Kino.Input.password("SECRET_ACCESS_KEY")
```

リージョンもここで入力しておきましょう

```elixir
region_input = Kino.Input.text("REGION")
```

![スクリーンショット 2022-12-03 0.01.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9c877571-862a-22bd-528f-32d5cb6c28b5.png)

各認証情報を ExAws に渡すためにまとめておきます

秘密情報が実行結果に現れないよう、セルの最後には `"dummy"` を入れておきましょう

```elixir
auth_config = [
  access_key_id: Kino.Input.read(access_key_id_input),
  secret_access_key: Kino.Input.read(secret_access_key_input),
  region: Kino.Input.read(region_input)
]

"dummy"
```

## 物体検出

画像内にある物体を検出し、それが何であるか、画像内のどこにあるのかを取得します

いつもお世話になっている Lenna さんの画像を使います

```elixir
File.rm("Lenna_%28test_image%29.png")

lenna =
  Download.from("https://upload.wikimedia.org/wikipedia/en/7/7d/Lenna_%28test_image%29.png")
  |> elem(1)
```

いつものです

```elixir
lenna_mat = Evision.imread(lenna)
```

![スクリーンショット 2022-12-06 17.36.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/22da5297-aab1-9b24-a73d-30adf96d506c.png)

さあ、 `Rekognition.detect_labels` で Rekognition に物体検出させましょう

```elixir
rek_res =
  Evision.imencode(".png", lenna_mat)
  |> Rekognition.detect_labels()
  |> ExAws.request!(auth_config)
```

![スクリーンショット 2022-12-06 17.37.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/80d90555-5830-345d-b5e1-69c171d8b140.png)

色々返ってきてはいますが、これだと見にくいので整形しましょう

```elixir
detections_df =
  rek_res["Labels"]
  # 位置情報がないものは除外する
  |> Enum.filter(&(Enum.count(&1["Instances"]) > 0))
  |> Enum.flat_map(fn detection ->
    detection["Instances"]
    |> Enum.map(fn instance ->
      [
        label: detection["Name"],
        left: instance["BoundingBox"]["Left"],
        top: instance["BoundingBox"]["Top"],
        width: instance["BoundingBox"]["Width"],
        height: instance["BoundingBox"]["Height"],
        score: instance["Confidence"]
      ]
    end)
  end)
  |> DataFrame.new()

detections_df
|> Kino.DataTable.new()
```

![スクリーンショット 2022-12-06 17.38.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/d3c42431-4aa7-ab88-4056-c614abc0d636.png)

一覧にしてみると、どうも無駄に同じ位置で `Person` や `Woman` や `Adult` 、 `Female` が検出されています

確かに Lenna さんは `Person` で `Woman` で `Adult` で `Female` ですが、重なると見にくいので一つに絞ります

```elixir
detections_df =
  detections_df
  |> DataFrame.distinct(["left", "top", "width", "height"], keep_all: true)

detections_df
|> Kino.DataTable.new()
```

![スクリーンショット 2022-12-06 17.40.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/5c494aaf-eaf7-63ba-3975-64f1255d4006.png)

これで人物二人になりました

ではこの位置情報を画像にプロットしましょう

Rekognition から返ってくる Let や Top の値は画像全体の幅、高さを 1.0 としたときの比なので、元の画像サイズをかける必要があります

```elixir
{img_height, img_width, _} = Evision.Mat.shape(lenna_mat)
```

検出した位置情報毎に四角形とラベル文字を出力します

```elixir
detections_df
|> DataFrame.to_rows()
|> Enum.reduce(lenna_mat, fn detection, drawed ->
  label = detection["label"]
  left = img_width * detection["left"] |> trunc()
  top = img_height * detection["top"] |> trunc()
  right = left + img_width * detection["width"] |> trunc()
  bottom = top + img_height * detection["height"] |> trunc()

  drawed
  |> Evision.rectangle(
    {left, top},
    {right, bottom},
    {255, 0, 0},
    thickness: 4
  )
  |> Evision.putText(
    label,
    {left + 6, top + 26},
    Evision.Constant.cv_FONT_HERSHEY_SIMPLEX(),
    0.8,
    {0, 0, 255},
    thickness: 2
  )
end)
```

![スクリーンショット 2022-12-06 17.43.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a5010c2c-e6a7-8109-cbf0-ab237b8232e4.png)

さすが Rekognition です

手前の Lenna さんはもちろん、後ろの鏡に映った Lenna さんも検出できています

せっかくなので、一連の処理を関数化しましょう

```elixir
show_objects = fn input_mat ->
  Evision.imencode(".jpg", input_mat)
  |> Rekognition.detect_labels()
  |> ExAws.request!(auth_config)
  |> then(&(&1["Labels"]))
  |> Enum.filter(&(Enum.count(&1["Instances"]) > 0))
  |> Enum.flat_map(fn detection ->
    detection["Instances"]
    |> Enum.map(fn instance ->
      [
        label: detection["Name"],
        left: instance["BoundingBox"]["Left"],
        top: instance["BoundingBox"]["Top"],
        width: instance["BoundingBox"]["Width"],
        height: instance["BoundingBox"]["Height"],
        score: instance["Confidence"]
      ]
    end)
  end)
  |> DataFrame.new()
  |> DataFrame.distinct(["left", "top", "width", "height"], keep_all: true)
  |> DataFrame.to_rows()
  |> Enum.reduce(input_mat, fn detection, drawed ->
    {img_height, img_width, _} = Evision.Mat.shape(input_mat)

    label = detection["label"]
    left = img_width * detection["left"] |> trunc()
    top = img_height * detection["top"] |> trunc()
    right = left + img_width * detection["width"] |> trunc()
    bottom = top + img_height * detection["height"] |> trunc()

    drawed
    |> Evision.rectangle(
      {left, top},
      {right, bottom},
      {255, 0, 0},
      thickness: 4
    )
    |> Evision.putText(
      label,
      {left + 6, top + 26},
      Evision.Constant.cv_FONT_HERSHEY_SIMPLEX(),
      0.8,
      {0, 0, 255},
      thickness: 2
    )
  end)
end
```

この関数を使って、今度は例の犬画像から物体を検出します

```elixir
File.rm("dog.jpg")

dog =
  Download.from("https://raw.githubusercontent.com/pjreddie/darknet/master/data/dog.jpg")
  |> elem(1)
```

```elixir
dog_mat = Evision.imread(dog)
```

![スクリーンショット 2022-12-06 17.45.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/73e6c6e8-a12f-b121-d40c-d587ba73a611.png)

```elixir
show_objects.(dog_mat)
```

![スクリーンショット 2022-12-06 17.46.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/db2c37d7-52d6-a046-1502-d84983749d17.png)

木の枝に隠れているバイクまで見逃していません

## 顔検出

続いて顔検出です

`Rekognition.detect_faces` で顔検出ができます

`attributes: ["ALL"]` を指定すると、顔の位置情報だけでなく、感情などの情報も併せて取得できます

```elixir
rek_res =
  Evision.imencode(".png", lenna_mat)
  |> Rekognition.detect_faces(attributes: ["ALL"])
  |> ExAws.request!(auth_config)
```

![スクリーンショット 2022-12-06 17.48.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8911882a-99c4-8c99-71b4-9f9e1629bc8c.png)

Lenna さんの顔情報をとっておきます

```elixir
lenna_face =
  rek_res["FaceDetails"]
  |> Enum.at(0)
```

![スクリーンショット 2022-12-06 17.49.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/e5c28161-fac3-c720-2292-1acf7659fffc.png)

この顔情報からさまざまなことが分かります

### 顔の位置

物体検出と同じように顔の位置をプロットします

```elixir
{img_height, img_width, _} = Evision.Mat.shape(lenna_mat)

box = lenna_face["BoundingBox"]

left = img_width * box["Left"] |> trunc()
top = img_height * box["Top"] |> trunc()
right = left + img_width * box["Width"] |> trunc()
bottom = top + img_height * box["Height"] |> trunc()

lenna_mat
|> Evision.rectangle(
  {left, top},
  {right, bottom},
  {255, 0, 0},
  thickness: 4
)
```

![スクリーンショット 2022-12-06 17.50.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/1b9e2f75-7b73-3265-269d-711790a62f48.png)

### 顔のパーツ

目や鼻の位置も取得できます

```elixir
{img_height, img_width, _} = Evision.Mat.shape(lenna_mat)

lenna_face["Landmarks"]
|> Enum.reduce(lenna_mat, fn detection, drawed ->

  x = img_width * detection["X"] |> trunc()
  y = img_height * detection["Y"] |> trunc()

  drawed
  |> Evision.circle(
    {x, y},
    4,
    {255, 0, 0},
    thickness: -1
  )
end)
```

![スクリーンショット 2022-12-06 17.51.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/96557d91-a18a-7bc7-d054-aefcab1161b5.png)

目尻、眉尻、顎先、鼻先など、かなり細かく取得できますね

### 顔の向き

顔の向きまで分かります

```elixir
lenna_face["Pose"]
|> then(&%{
  "どれくらい首を傾げているか" => Float.round(&1["Roll"]),
  "どれくらい見上げたり見下ろしたりしているか" => Float.round(&1["Pitch"]),
  "どれくらい左右に向いているか" => Float.round(&1["Yaw"]),
})
```

![スクリーンショット 2022-12-06 17.52.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/49b10b9f-4e01-a1a7-04be-4d8fc532d6ef.png)

値の単位は「度」で -180 〜 180 の範囲です

これを使えば「正面を見ているか」「上を見上げているか」といったことが判定できます

### 写りの良さ

顔がちゃんと写っているか判定できます

明るさを見ると、暗くて見えづらい顔を判別できます

```elixir
lenna_face["Quality"]["Brightness"]
|> Float.round()
|> then(&"明るさ #{&1}%")
```

![スクリーンショット 2022-12-06 17.54.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/816df6b5-c4ba-869c-d089-7334ce5ecb3d.png)

鮮明さで顔にフォーカスが合っているかを判定できます

```elixir
lenna_face["Quality"]["Sharpness"]
|> Float.round()
|> then(&"鮮明さ #{&1}%")
```

![スクリーンショット 2022-12-06 17.55.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/6471520e-0c82-7ef8-8737-3ac8dc1afdba.png)

### 感情

8種類の感情について判定できます

```elixir
lenna_face["Emotions"]
|> Enum.map(fn emotion ->
  %{
    "confidence" => Float.round(emotion["Confidence"]),
    "emotion" => emotion["Type"]
  }
end)
|> DataFrame.new()
|> DataFrame.select(["emotion", "confidence"])
|> Kino.DataTable.new()
```

![スクリーンショット 2022-12-06 17.56.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a80686e3-830a-bdb1-2c87-1e0896b4de80.png)

Lenna さんは CARM 86% なので、ほぼ感情がない（表れていない）状態です

### 年齢

年齢までバレます

```elixir
lenna_face["AgeRange"]
|> then(&"#{&1["Low"]}歳 〜 #{&1["High"]}歳")
```

![スクリーンショット 2022-12-06 17.58.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/df4cb03a-0527-d542-aaf6-bf214a945d38.png)

### 性別

性別も分かります

```elixir
lenna_face["Gender"]
|> then(fn attr ->
  "#{Float.round(attr["Confidence"])}% " <>
    case attr["Value"] do
      "Female" ->
        "女性"
      "Male" ->
        "男性"
    end
end)
```

![スクリーンショット 2022-12-06 17.59.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8de1c216-a74d-166b-5486-3950506d38be.png)

### その他の情報

他にも色々判定してくれます

そうであるか、そうでないかを日本語に置き換えるための関数を用意します

```elixir
to_jp = fn bool ->
  case bool do
    true ->
      "る"
    false ->
      "ない"
  end
end
```

判定する項目の一覧を用意します

```elixir
attributes =
  [
    {"Smile", "微笑んで"},
    {"EyesOpen", "目を開けて"},
    {"MouthOpen", "口を開けて"},
    {"Eyeglasses", "メガネをかけて"},
    {"Sunglasses", "サングラスをかけて"},
    {"Mustache", "髭が生えて"},
    {"Beard", "ハゲて"},
  ]
```

それぞれについて判定結果を表示します

```elixir
attributes
|> Enum.map(fn {en, jp} ->
  lenna_face[en]
  |> then(fn attr ->
    "#{Float.round(attr["Confidence"])}% #{jp}い#{to_jp.(attr["Value"])}"
  end)
end)
```

![スクリーンショット 2022-12-06 18.01.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/99f8ccbe-c834-7851-4e58-f83fdfebae7f.png)

## 　まとめ

画像 AI サービスの Rekognition は Livebook と非常に相性がいいですね

色々な写真や動画を分析できそうです