Nx.Tensorの真ん中を求める

[Tensorflow LiteとMoveNetモデルを用いた姿勢推定](https://www.tensorflow.org/lite/examples/pose_estimation/overview)の勉強をしていたときに学んだ内容をメモ。姿勢推定のコードについては[tflite_elixir](https://hex.pm/packages/tflite_elixir)に寄贈しました。

[https://github.com/cocoa-xu/tflite_elixir/blob/main/notebooks/pose_estimation_image_sequence.livemd](https://github.com/cocoa-xu/tflite_elixir/blob/main/notebooks/pose_estimation_image_sequence.livemd)

ここでは[Nx.Tensor](https://hexdocs.pm/nx/Nx.Tensor.html)の真ん中の値を求める技について考えます。

以下のもくもく会での成果です。

https://okazakirin-beam.connpass.com/

https://autoracex.connpass.com/

https://qiita.com/mnishiguchi/items/6a5a1b42ae4585dfb1a9

## 依存パッケージ

```elixir
Mix.install(
  [
    ## Nxをを使うため　
    {:nx, "~> 0.5.0"},
    ## Nxを高速化するため
    {:exla, "~> 0.5.0"},
    ## OpenCVを使うため　
    {:evision, "0.1.30"},
    ## Livebook上で図形を表示する場合に使用
    # {:kino, "~> 0.7.0"}
  ],
  config: [
    nx: [default_backend: EXLA.Backend]
  ]
)

# Evision作者が好んで用いるエイリアス
alias Evision, as: Cv
```

## いろんな真ん中

いろんな真ん中が考えられそうです。頭を整理します。

### 平均値（mean, average）

* 集合の要素の総和を集合の要素数で割ったもの

https://ja.wikipedia.org/wiki/平均

### 中央値（median）

* 順位が中央である値

https://ja.wikipedia.org/wiki/中央値

### 中点（midpoint）

* ある2点を両端とする線分上にあり、その両端から等しい距離にある点

https://ja.wikipedia.org/wiki/中点

### 幾何中心（geometric center）

* 図形に属する全ての点にわたってとった算術平均の位置にある点

https://ja.wikipedia.org/wiki/幾何中心

### 外心（circumcenter）

* 外接円の中心

https://ja.wikipedia.org/wiki/外接円

## Nx.mean/2 で平均値を求める

```elixir
points_nx = Nx.tensor([1, 1, 1, 2, 2, 2, 3, 3, 123, 456])
points_nx |> Nx.mean() |> Nx.to_number()
# 59.400001525878906
```

## Nx.median/2 で中央値を求める

```elixir
points_nx = Nx.tensor([1, 1, 1, 2, 2, 2, 3, 3, 123, 456])
points_nx |> Nx.median() |> Nx.to_number()
# 2.0
```

## Nx.mean/2 で中点を求める

```elixir
points_nx =
  Nx.tensor(
    [
      [50, 80],
      [220, 230]
    ],
    names: [:point, :value]
  )

[midpoint_x, midpoint_y] =
  points_nx
  |> Nx.mean(axes: [:point])
  |> Nx.to_list()

# [135.0, 155.0]
```

![download-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/1774e5a5-3f56-81ea-7ead-1e94a26faa84.png)

参考までにLivebookで可視化するためのコードを以下に示します。

```elixir
{img_height, img_width} = {300, 300}
default_color = {255, 255, 255}
midpoint_color = {0, 255, 0}

background_mat =
  Nx.tensor([0, 0, 0], type: :u8)
  |> Nx.broadcast({img_height, img_width, 3})
  |> Cv.Mat.from_nx_2d()

Enum.reduce(
  Nx.to_list(points_nx),
  background_mat,
  fn [x, y], acc_mat ->
    Cv.drawMarker(acc_mat, {trunc(x), trunc(y)}, default_color)
  end
)
|> Cv.drawMarker({trunc(midpoint_x), trunc(midpoint_y)}, midpoint_color, thickness: 2)
```




## Nx.mean/2 で幾何中心を求める

単純な重心であれば各座標の平均で求められるそうです。

https://jp.mathworks.com/matlabcentral/answers/324318-

```elixir
points_nx =
  Nx.tensor(
    [
      [50, 100],
      [120, 230],
      [220, 60]
    ],
    names: [:point, :value]
  )

[midpoint_x, midpoint_y] =
  points_nx
  |> Nx.mean(axes: [:point])
  |> Nx.to_list()

# [130.0, 130.0]
```

![download-2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/ba0e94b2-24e2-ce59-0344-65ab3cb9cfbe.png)

参考までにLivebookで可視化するためのコードを以下に示します。

```elixir
{img_height, img_width} = {300, 300}
default_color = {255, 255, 255}
midpoint_color = {0, 255, 0}

background_mat =
  Nx.tensor([0, 0, 0], type: :u8)
  |> Nx.broadcast({img_height, img_width, 3})
  |> Cv.Mat.from_nx_2d()

Enum.reduce(
  Nx.to_list(points_nx),
  background_mat,
  fn [x, y], acc_mat ->
    Cv.drawMarker(acc_mat, {trunc(x), trunc(y)}, default_color)
  end
)
|> Cv.drawMarker({trunc(midpoint_x), trunc(midpoint_y)}, midpoint_color, thickness: 2)
```


## Evision.minEnclosingCircle/1 で外心を求める

```elixir
points_nx =
  Nx.tensor(
    [
      [50.0, 100.0],
      [120.0, 230.0],
      [220.0, 60.0]
    ],
    names: [:point, :value]
  )

{{center_x, center_y}, radius} = Cv.minEnclosingCircle(points_nx)
# {{147.12850952148438, 131.5461883544922}, 102.12312316894531}
```

![download-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/837a80af-eb7b-8148-cb94-965d9ac89820.png)


参考までにLivebookで可視化するためのコードを以下に示します。

```elixir
{img_height, img_width} = {300, 300}
default_color = {255, 255, 255}
circle_color = {0, 255, 0}

background_mat =
  Nx.tensor([0, 0, 0], type: :u8)
  |> Nx.broadcast({img_height, img_width, 3})
  |> Cv.Mat.from_nx_2d()

Enum.reduce(
  Nx.to_list(points_nx),
  background_mat,
  fn [x, y], acc_mat ->
    Cv.drawMarker(acc_mat, {trunc(x), trunc(y)}, default_color)
  end
)
|> Cv.circle({trunc(center_x), trunc(center_y)}, ceil(radius), circle_color, thickness: 2)
|> Cv.drawMarker({trunc(center_x), trunc(center_y)}, circle_color, thickness: 2)
```

## お便りください

他にも便利な技や情報がありました、ぜひお便りください。

![20230409_113832.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/f0996638-1ef7-2a63-0169-3341917e32d2.jpeg)