Livebook で Elixir の Evision 、 Image の画像と Python の OpenCV 、 Pillow の画像を相互変換する

## はじめに

Livebook で Python が実行できるようになりました（試験導入ですが）

それだけでなく、 Elixir で定義した変数を Python で使ったり、 Python で定義した変数を Elixir で使ったり、外部ファイルや通信を介したりすることなく、シームレスにデータのやり取りをすることが可能です

前回の記事では数値や文字列、マップ、リストなどの基本的な変数について、 Elixir と Python 間で連携しました

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

今回の記事はその発展形として、画像データを Elixir 、 Python 間で連携します

Elixir は Evision 、 Image で画像を扱います

https://github.com/cocoa-xu/evision

https://github.com/elixir-image/image

Python は OpenCV 、 Pillow で画像を扱います

https://github.com/opencv/opencv-python

https://github.com/python-pillow/Pillow

Evision 、 Image 、 OpenCV 、 Pillow の画像データを相互変換することで、Elixir で分散処理した先のノードで Python が画像処理するなど、様々な応用が考えられます

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

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/pythonx/pythonx_image_processing.livemd

## セットアップ

Livebook のセットアップセルをクリックすると、左下に "+ Python" ボタンが表示されます

![スクリーンショット 2025-05-29 9.52.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/69e9ecb2-dbb0-471a-b111-a5852af54213.png)

 "+ Python" ボタンをクリックすると、セットアップセルに Python セル用の設定が自動的に追加されます

 ![スクリーンショット 2025-05-29 9.56.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8f86e1f3-7444-4384-87de-4eecfdbb80d3.png)

上部、 Elixir 側のセットアップセルに以下のコードを入力します

```elixir
Mix.install([
  {:pythonx, "~> 0.4.2"},
  {:kino_pythonx, "~> 0.1.0"},
  {:evision, "~> 0.2.13"},
  {:image, "~> 0.59.3"}
])
```

下部、 Python 側のセットアップせるに以下のコードを入力します

```toml
[project]
name = "project"
version = "0.0.0"
requires-python = "==3.13.*"
dependencies = [
  "opencv-python == 4.11.*",
  "pillow == 11.2.*"
]
```

これにより、 Elixir 、 Python でそれぞれ利用する画像処理パッケージがインストールされました

## OpenCV to Evision

まずは OpenCV の画像データを Evision の画像データに変換しましょう

Python セルで Python のパッケージを読み込みます

```python
# Python
import io

import cv2
import numpy as np

from PIL import Image
```

Python セルで Numpy を使い、 200 x 300 の青い画像を生成します

```python
# Python
img = np.zeros((200, 300, 3), np.uint8)
img[:,:,0] = 255

img
```

実行結果

```python
array([[[255,   0,   0],
        [255,   0,   0],
        [255,   0,   0],
        ...,
        [255,   0,   0],
        [255,   0,   0],
        [255,   0,   0]]], shape=(200, 300, 3), dtype=uint8)
```

このままだとただの Numpy 配列としてしか表示されないので、 Pillow の画像に変換します

```python
# Python
Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
```

すると、 Jupyter と同じように画像が表示されます

![スクリーンショット 2025-06-01 1.44.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9d8102bd-912b-46ac-a443-427f8ca64b17.png)

では、 Numpy 配列を Elixir に持っていきましょう

そのままでは Elixir で扱えないため、先に配列に変換します

```python
# Python
l_img = img.tolist()
```

配列をデコードして Nx のテンソル、 Evision の行列に順次変換することで、画像を表示できます

```elixir
# Elixir
l_img
|> Pythonx.decode()
|> Nx.tensor(type: :u8)
|> Evision.Mat.from_nx_2d()
```

実行結果

![スクリーンショット 2025-06-01 1.52.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/20a5ed7a-c284-4f32-9b43-2ef48a6a22dd.png)

しかし、この方法では 200 x 300 x 3 の配列をループしてデコードしている関係上、処理に 10 秒ほどかかってしまいます

もっと高速に Python から Elixir にデータを持っていくため、バイト配列を利用します

```python
# Python
b_img = img.tobytes()
b_img
```

実行結果

```python
b'\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff\x00\x00\xff...'
```

バイト配列をデコードし、バイナリから Evision 行列に変換します

```elixir
# Elixir
ex_img =
  b_img
  |> Pythonx.decode()
  |> Evision.Mat.from_binary(:u8, 200, 300, 3)
```

実行結果

![スクリーンショット 2025-06-01 1.52.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/20a5ed7a-c284-4f32-9b43-2ef48a6a22dd.png)

この方法では処理に数ミリ秒しかかかりません

## Evision to OpenCV

今度は Evision の画像（行列）を OpenCV の画像（Numpy 配列）に変換します

```elixir
# Elixir
sky_img =
  [255, 255, 0]
  |> Nx.tensor(type: :u8)
  |> Nx.broadcast({200, 300, 3})
  |> Evision.Mat.from_nx_2d()
```

実行結果

![スクリーンショット 2025-06-01 1.57.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/730036d1-e5b3-43f5-a606-ce71115ad222.png)

先ほどと同じように、バイト配列として連携します

```elixir
# Elixir
py_b_img = Evision.Mat.to_binary(sky_img)
```

実行結果

```elixir
<<255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255,
  255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0, 255, 255, 0,
  255, 255, 0, 255, 255, ...>>
```

```python
# Python
py_img = np.frombuffer(py_b_img, dtype=np.uint8)
py_img = np.reshape(py_img, [200, 300, 3])
py_img
```

実行結果

```python
array([[[255, 255,   0],
        [255, 255,   0],
        [255, 255,   0],
        ...,
        [255, 255,   0],
        [255, 255,   0],
        [255, 255,   0]]], shape=(200, 300, 3), dtype=uint8)
```

変換した結果を画像として表示します

```python
# Python
Image.fromarray(cv2.cvtColor(py_img, cv2.COLOR_BGR2RGB))
```

実行結果

![スクリーンショット 2025-06-01 2.01.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/31552cfd-8c2d-4282-a805-971c4a93130c.png)

## Pillow to Evision

次は Pillow の画像を Evision の画像に変換します

```python
# Python
pil_img = Image.new("RGB", (300, 200), (0, 255, 0))
pil_img
```

実行結果

![スクリーンショット 2025-06-01 2.02.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/2fe957c9-7378-453a-888e-259b913f79ea.png)

PNG 形式のバイナリに変換します

```python
# Python
pil_b_img = io.BytesIO()
pil_img.save(pil_b_img, format='PNG')
pil_b_img = pil_b_img.getvalue()
pil_b_img
```

実行結果

```python
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01,\x00\x00\x00\xc8\x08\x02\x00\x00\x00\xdd\xbdK\x02\x00\x00\x02BIDATx\x9c\xed\xd31\x01\x00 ...'
```

バイナリを `Pythonx.decode` でデコードした後、 `Evision.imdecode` で画像データとして読み込みます

```elixir
# Elixir
ex_pil_img =
  pil_b_img
  |> Pythonx.decode()
  |> Evision.imdecode(Evision.Constant.cv_IMREAD_COLOR())
```

実行結果

![スクリーンショット 2025-06-01 2.05.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/46475cc2-7b53-4d58-aaa5-30b15df5a1db.png)

## Evision to Pillow

逆方向も同様にバイナリを介して連携します

```elixir
# Elixir
png_img = Evision.imencode(".png", ex_pil_img)
```

実行結果

```elixir
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01,\x00\x00\x00\xc8\x08\x02\x00\x00\x00\xdd\xbdK\x02\x00\x00\x02BIDATx\x9c\xed\xd31\x01\x00 ...'
```

```python
# Python
Image.open(io.BytesIO(png_img))
```

実行結果

![スクリーンショット 2025-06-01 2.02.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/2fe957c9-7378-453a-888e-259b913f79ea.png)

## Pillow to Image

Elixir の Image パッケージの画像データにも変換してみましょう

```python
# Python
pil_yellow_img = Image.new("RGB", (300, 200), (255, 255, 0))
pil_yellow_img
```

実行結果

![スクリーンショット 2025-06-01 2.08.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/efc5808b-969d-4979-9d2b-309925705fee.png)

今までと同じようにバイナリに変換します

```python
# Python
pil_yb_img = io.BytesIO()
pil_yellow_img.save(pil_yb_img, format='PNG')
pil_yb_img = pil_yb_img.getvalue()
pil_yb_img
```

実行結果

```python
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x01,\x00\x00\x00\xc8\x08\x02\x00\x00\x00\xdd\xbdK\x02\x00\x00\x02BIDATx\x9c\xed\xd31\x01\x00 ...' 
```

バイナリからデコードし、 Image に変換します

```elixir
# Elixir
ex_yellow_img =
  pil_yb_img
  |> Pythonx.decode()
  |> Image.from_binary!()
```

実行結果

![スクリーンショット 2025-06-01 2.11.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/a92ceb44-b54b-490a-927d-3c62fc0df26b.png)

## Image to Pillow

逆も同様です

```elixir
# Elixir
ex_yb_img = Image.write!(ex_yellow_img, :memory, suffix: ".png")
```

![スクリーンショット 2025-06-01 2.13.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/78e51e2b-3f67-4e1a-9f8a-02f4e76bd3a3.png)

```python
# Python
Image.open(io.BytesIO(ex_yb_img))
```

実行結果

![スクリーンショット 2025-06-01 2.14.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/cdcb68dd-9ce4-44a6-93f3-be31872884b9.png)

## まとめ

バイナリを経由することで、膨大な行列である画像データの連携も高速に行うことができました

Elixir の強みと Python の強みを組み合わせることで、様々な画像処理ができそうです