【FPGA】PYNQで画像処理を簡単に

# はじめに
PYNQは、Xilinx社製FPGA開発キットやアクセラレーションカード上に実装されたロジックをPythonから利用するためのフレームワークです。
**本記事では、Kria KV260とPYNQを利用したFPGAアクセラレーションの入門として、画像を反転させる単純なアプリの実装方法を概説します**。今回は画像反転という単純なロジックをFPGA上に実装しますが、**ロジックの変更によって様々な処理への応用が可能になると考えています**。
尚、使用するXilinx社製FPGA開発キットは、Kria KV260 AIスターターキットを用いました。
![Screenshot from 2022-10-29 10-46-41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2756450/691a91ad-ca07-db17-2901-2bc61926a4b0.png)
キャプチャの引用元：https://japan.xilinx.com/products/som/kria/kv260-vision-starter-kit.html

# 対象読者
- Pythonで書かれたプログラムをアクセラレーションしたい方
- Kria KV260などのXilinx社製FPGA開発キットやアクセラレーションカードの活用を検討している方

# 目的
PYNQの利用によって、PythonプログラムをFPGAで容易にアクセラレーションできると感じていただくこと

# 開発環境
- [Kria KV260](https://japan.xilinx.com/products/som/kria/kv260-vision-starter-kit.html)
- Vivado 2021.1
- Vitis HLS 2021.1
- Kria-PYNQ v1.0

# 参考文献
本記事を執筆するあたり、以下を参考にさせてもらいました。
ありがとうございます。

https://www.acri.c.titech.ac.jp/wordpress/archives/25

https://phys-higashi.com/826/

https://phys-higashi.com/114/

# 本文

## 0. 概要
画像反転プログラムのFPGA実装方法を以下の順で紹介します。

1. 画像反転プログラムの全体像を把握する。
2. Vitis HLSでFPGAロジックを実装する。
3. Vivadoでbitストリームを生成する。
4. Kria-PYNQを用いた画像反転プログラムを実行する。

## 1. 画像反転プログラムの全体像を把握する。
プログラムの全体像を確認し、アクセラレーションすべき部分を把握します。
以下は、画像反転のPythonプログラムです。
```main.py
from PIL import Image
import numpy as np

# path to image data
file_path ="../data/cat.jpg"
file_path1="../data/gray_cat.jpg"
file_path2="../data/reverse_gray_cat.jpg"

# info of raw data
im = np.array(Image.open(file_path))
print(type(im))
print(im.dtype)
print(im.shape)

# info of raw data converted in gray scale
im = np.array(Image.open(file_path).convert('L'))
print(type(im))
print(im.dtype)
print(im.shape)
Image.fromarray(im).save(file_path1)

h,w = im.shape
rim = np.zeros([h,w],dtype=im.dtype)
for i0 in range(h):
	for i1 in range(w):
		rim[i0][i1] = im[i0][w-1-i1]
	
Image.fromarray(rim).save(file_path2)

# show image data
im0 = Image.open(file_path)
im0.show()
im1 = Image.open(file_path1)
im1.show()
im2 = Image.open(file_path2)
im2.show()
```
プログラムを実行すると、3枚の画像が表示されます。右から1枚目は元画像（[Cifar10](https://www.cs.toronto.edu/~kriz/cifar.html)にある画像）、2枚目はグレースケールの画像、3枚目は2枚目を反転させたものです。
![cat.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2756450/0544b1ae-8077-72ec-caa2-b46438607aae.jpeg)![gray_cat.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2756450/7b700488-7b9f-8783-f2b6-c7dd43212824.jpeg)![reverse_gray_cat.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2756450/767ed8dd-2684-5f88-333e-8397693efd55.jpeg)

今回は、プログラムの中盤にある以下の画像反転部分のみをFPGAで処理させることを目指します。
```
for i0 in range(h):
	for i1 in range(w):
		rim[i0][i1] = im[i0][w-1-i1]
```
また、実装簡略化のために、**グレースケールの画像を反転させるFPGAロジックとします**。
つまり、上記の2枚目を入力に3枚目を出力するFPGAロジックを実装します。
**入出力の画像サイズは、uint8型、32x32の配列になります**。

## 2. Vitis HLSでFPGAロジックを実装する。
画像反転させるロジックは以下のようにコーディングしました。

```kernel.cpp
#include "kernel.h"

// LEN_IN is 32
const int c_size  = LEN_IN;

void img_flip(
		ap_uint<8> in[LEN_IN][LEN_IN], ap_uint<8> out[LEN_IN][LEN_IN]
)
{
#pragma HLS INTERFACE mode=s_axilite port=return bundle=ctrl
#pragma HLS INTERFACE mode=s_axilite port=in  bundle=ctrl
#pragma HLS INTERFACE mode=s_axilite port=out  bundle=ctrl
#pragma HLS INTERFACE mode=m_axi port=in  offset=slave bundle=gmem depth=c_size
#pragma HLS INTERFACE mode=m_axi port=out offset=slave bundle=gmem depth=c_size

	for(uint it0=0;it0<LEN_IN;it0++){
		#pragma HLS LOOP_TRIPCOUNT min = c_size max = c_size
		for(uint it1=0;it1<LEN_IN;it1++){
			#pragma HLS LOOP_TRIPCOUNT min = c_size max = c_size
			out[it0][it1] = in[it0][LEN_IN-1-it1];
		}
	}
}
```
ポイントは、Vitis HLSがコードをRTLに変換する際の指示をするために、**pragma**を利用することです。
[INTERFACE](https://docs.xilinx.com/r/ja-JP/ug1399-vitis-hls/%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%95%E3%82%A7%E3%82%A4%E3%82%B9%E3%81%AE%E3%83%A1%E3%83%A2%E3%83%AA-%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3)としては、s_axiliteとm_axiを利用しました。s_axiliteは制御用のインターフェースで、m_axiはデータのメモリ転送を実施するためのインターフェースです。[LOOP_TRIPCOUNT](https://docs.xilinx.com/r/ja-JP/ug1399-vitis-hls/pragma-HLS-loop_tripcount)は、ループの反復回数を指定するものです。

上記のコードをVitis HLSで高位合成します。詳細な手順はXilinx公式のマニュアルや[こちらのブログ](https://phys-higashi.com/826/)などを参考にして下さい。
尚、下図のように、**Solution Settingsにてm_axi_addr64のチェックを外すと、不要な制御信号が生成されなくなります**。
![Screenshot from 2022-10-24 00-32-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2756450/5f424f8f-0100-4743-0cd2-ad97bb504e4f.png)

## 3. Vivadoでbitストリームを生成する。
Vivado上でIP（Zynq UltraSCALE+ MPSoC, img_flip）を追加し、[Auto connect](https://www.xilinx.com/content/dam/xilinx/support/documents/sw_manuals_j/xilinx2021_2/ug940-vivado-tutorial-embedded-design.pdf#page=64)の機能を用いて配置配線を実施します。
先ほどVitis HLSで作成したIPは、img_flipという名前のIPです。以下は、配置配線後のDiagramです。
![Screenshot from 2022-10-26 23-27-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2756450/72b75e7e-0953-5cc6-8cd0-07e706e0df35.png)

オレンジでハイライトされている配線は、m_axiインターフェースにつながる配線です。
配線配線時の注意点は、**Zynq UltraSCALE+ MPSoCのIPを追加した段階では、S_AXI_HPC0_FPDの端子は用意されていないため、下図のようにユーザ側で端子を追加する必要があることです**。端子を追加した後に、Autconnectの機能を使えば、自動的に必要なIPなどの追加を含めて全体のロジックを組み上げてくれます。
配線接続完了後は、Create HDL wrapperなどを実施し、Vivado上でbitstreamを生成します。
Vivadoの使い方は、Xilinx公式のマニュアルや[こちらのブログ](https://phys-higashi.com/114/#toc10)を参考にしてください。

![Screenshot from 2022-10-26 23-25-21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2756450/cad74d61-1df4-6c62-a190-b628cf8fa36c.png)


## 4. Kria-PYNQを用いた画像反転プログラムを実行する。
**Kria KV260に[Kria-PYNQ](https://github.com/Xilinx/Kria-PYNQ)をインストールすると、Kria KV260上にjupyter notebookが立ち上がり、開発マシンからIPアドレスとポート番号(e.g. 192.168.10.114:9090)を指定してアクセスできるようになります**。これにより、開発マシンからKria KV260上で動くPythonプログラムをインタラクティブに開発できるようになります。
Vivadoでのbitstream生成が完了すると、design_1_wrapper.bitとdesign_1.hwhが生成されます。これらをKria KV260上のjupyter notebookから参照可能なフォルダに移動させ、ファイル名を同一のものに変えておきます。今回は、ファイル名をimg_flip.bitとimg_flip.hwhにしています。
以下の2枚のキャプチャは、PYNQを用いてFPGA上の画像反転ロジックを利用したプログラムのキャプチャです。プログラムの重要な部分のみを説明します。

![Screenshot from 2022-10-28 15-36-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2756450/a3075d65-5e6a-8e5a-9628-1a03651c7fdc.png)
- In [1] ライブラリのインポート、OverlayでbitstreamをFPGAにロードする。
- In [3] pynq allocateでFPGAからアクセス可能なCPU側で管理しているメモリ領域(入力画像用)を確保する。
- In [4] pynq allocateでFPGAからアクセス可能なCPU側で管理しているメモリ領域(出力画像用)を確保する。
- In [5] 確保したメモリ領域(入力画像用)の物理アドレスを確認する。
- In [6] 確保したメモリ領域(出力画像用)の物理アドレスを確認する。
***これらの物理アドレスをaxislite経由でFPGAに渡すことでFPGAからメモリを参照できるようになります。**
- In [7] グレースケールの画像を読み込む。
![Screenshot from 2022-10-28 15-36-35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2756450/50ee2adb-75b2-507c-924c-6bc6cb709260.png)
- In [9] 入力画像を表示する。
- In [11] 入力画像用に確保したメモリの物理アドレスを渡す先となる、制御インターフェースのアドレスを格納する。
- In [12] 出力画像用に確保したメモリの物理アドレスを渡す先となる、制御インターフェースのアドレスを格納する。
- In [13-14] 先ほどの制御インターフェースのアドレスに、入出力画像用に確保したメモリの物理アドレスを設定する。
- In [16] FPGAロジックを起動させる。
- In [20] 出力画像を表示する。

**Out[9]とOut[20]を比較すると、無事に画像が反転していると分かります**。成功です！
今回は簡単なロジックをFPGA上に実装しましたが、より複雑なロジックを実装することで、FPGAならではのアプリ開発も可能になると期待できます。

## 5. まとめ
Kria KV260とPYNQを利用した画像反転アプリの実装方法を概説しました。
今後は、より高度なロジックをFPGAに実装する予定です。

## 付録
読者がjupyter notebookに記載したコードを利用しやすくするために、以下のコマンドでpythonに変換したコードを示しておきます。
```
jupyter nbconvert --to python img_flip.ipynb 
```

```img_flip.py
#!/usr/bin/env python
# coding: utf-8

# In[1]:


from pynq.overlay import Overlay
from pynq import allocate
import numpy as np
from PIL import Image
base = Overlay("/home/root/jupyter_notebooks/bitstreams/img_flip.bit")


# In[2]:


krnl = base.img_flip_0
krnl.register_map


# In[3]:


src = allocate([32,32],np.uint8)


# In[4]:


dst = allocate([32,32],np.uint8)


# In[5]:


src.physical_address


# In[6]:


dst.physical_address


# In[7]:


file_path = "/home/root/jupyter_notebooks/data/gray_cat.jpg"
im = np.array(Image.open(file_path))
print(type(im))
print(im.dtype)
print(im.shape)


# In[8]:


for i in range(32):
    for j in range(32):
        src[i][j] = im[i][j]


# In[9]:


Image.open(file_path)


# In[10]:


src


# In[11]:


ADDR_IN = krnl.register_map.in_r.address


# In[12]:


ADDR_OUT = krnl.register_map.out_r.address


# In[13]:


krnl.write(ADDR_IN,src.physical_address)


# In[14]:


krnl.write(ADDR_OUT,dst.physical_address)


# In[15]:


krnl.read(0)


# In[16]:


krnl.write(0,1)


# In[17]:


krnl.read(0)


# In[18]:


dst


# In[19]:


file_path1 = "/home/root/jupyter_notebooks/data/flip.jpg"
Image.fromarray(dst).save(file_path1)


# In[20]:


Image.open(file_path1)

```