lp6m’s blog

いろいろかきます

家のGPUマシンにVPSとポートフォワーディングを使用してリモートアクセス + リモート上のdockerコンテナにVSCodeでアタッチ

家のGPUマシンに外からアクセスしたい。家のGPUマシン上のdockerコンテナにアタッチしたい。
難しい設定はしたくない。備忘録。

環境

  • GPUマシン:ubuntu18.04
  • さくらVPS: ubuntu16.04 512MBの一番安いプランをレンタル中(VPSへのssh環境は既にセットアップ済み)
  • リモートマシン: ubuntu18.04

ポートフォワーディングの設定

とりあえず家のGPUマシンにssh接続できるようにする。参考リンクを参考にすればできる。

ネットワークの知識がホットミルクの膜くらい薄い僕が、リモートアクセスしたという話|森田 拓朗|note
SSHポートフォワード(トンネリング)を使って、遠隔地からLAN内のコンピュータにログインする - 2014-09-12 - ククログ
VPSを経由して安全に自宅サーバを公開する - Qiita

VPS側の設定

/etc/sshd_configに以下を追記。

GatewayPorts: yes

GPUマシン(リモート)側で以下を実行

vps_usernameはVPS上のubuntuユーザ名、vps_addressはVPSグローバルIP、192.168.xx.yyはGPUマシンのローカルIPを設定する。

ssh <vps_username>@<vps_address>-R 10022:192.168.xx.yy:22

ssh接続はしばらくするとタイムアウトするので適宜設定する。めんどくさかったらtopを実行しておけば切れない。
vpssshポートがデフォルトじゃない場合は適宜修正。

リモート側からアクセス

ssh <vps_username>@<vps_address> 
ssh <gpu_username>@localhost -p 10022

これでアクセスできる!

面倒くさいのでssh_configを設定しておく

~/.ssh/config に以下を追記。

Host           sakuravps
HostName       <vps_address>
User           <vps_username>

Host           tokyogpu
HostName       localhost
Port           10022
User           <gpu_username>
ProxyCommand   ssh -CW %h:%p sakuravps

これでssh tokyogpuでログインできるようになる。VSCodeでもssh接続でリモートアクセスできる。

dockerコンテナへアタッチ

VSCodeのremote developmentでdockerコンテナにアタッチしたいが、ポートフォワーディングしているせいか?うまくできなかった。
単純にsshしているリモートマシンで動作しているdockerコンテナへのアタッチは特に何の設定もせずにできるが、よくわからなかったので以下の方法を参考にした。
※ローカルマシン側にdocker環境をいれておかないとそもそもVSCodeのdocker remote developmentが使えないのでいれておく。

リモートサーバーの中のDockerにローカルから接続する - Eospedia

ssh -fNL localhost:23750:/var/run/docker.sock <gpu_username>@tokyogpu
export DOCKER_HOST=localhost:23750
code

とりあえずこれで直接アタッチできるようになった。
よくわからなかった部分をいつか理解したいが・・

Windows環境でOpenCV freetype2を使用する

環境

何か

WindowsOpenCVを使うには、ビルド済みライブラリをダウンロードしてプロジェクトの設定を適切に行えば良い。
qiita.com
freetype2を使って日本語を画像に表示させたいが、ビルド済みのものにはfreetype2は含まれていない。(#include でno such file)
というわけで自前でビルドする。参考リンクの情報の寄せ集め。

Visual Studio 英語パックインストール

vcpkgに必要らしい。
kagasu.hatenablog.com

vcpkg インストール

Microsoftが作っているC++向けパッケージマネージャらしい。存在を知らなかった。
cmdかgit bashで行う

git clone https://github.com/microsoft/vcpkg
cd vcpkg
git checkout refs/tags/2020.11 #masterではharfbuzzのビルドに失敗した
bootstrap-vcpkg.bat
vcpkg install freetype
vcpkg install harfbuzz

うまくいけばvcpkg\installed以下にlibやdllができているはず。

cmakeインストール

Windows向けインストーラでインストール。最新版で問題ない。

OpenCVソースダウンロード

https://github.com/opencv/opencv/releases/tag/3.4.13
https://github.com/opencv/opencv_contrib/releases/tag/3.4.13

今回は3.4.13をダウンロード。 contrlibもダウンロードすること。
opencv_contrib/modules/freetype/CMakeLists.txtを下記リンクの通りに書き変える。   
How to use OpenCV FreeType module with Visual Studio · GitHub

CMake実行

今回はPythonで使用せずC++だけで使用するのでPythonサポートは全部切った(はず)
参考リンクではanacondaを使用しているようです:
GitHub - BabaGodPikin/Build-OpenCv-for-Python-with-Extra-Modules-Windows-10: This is a step by step guide to build OpenCV with Extra Modules for Python (Anaconda) for Windows without errors. Paticularly, I will use the freetype module in OpenCV-Contrib.

cd <your-opencv-workspace-path>
mkdir build
cd ..
cmake -G "Visual Studio 15 2017"
    -B <your-opencv-workspace-path>\opencv-3.4.13\build 
    -D BUILD_NEW_PYTHON_SUPPORT=OFF  
    -D BUILD_PYTHON_SUPPORT=OFF  
    -D BUILD_opencv_python3=no 
    -D OPENCV_SKIP_PYTHON_LOADER=ON  
    -D OPENCV_EXTRA_MODULES_PATH=<your-opencv-workspace-path>\opencv_contrib-3.4.13\modules 
    -D OPEN_CV_FORCE_PYTHON_LIBS=yes
      -DCMAKE_TOOLCHAIN_FILE=<your-vcpkg-path>\scripts\buildsystems\vcpkg.cmake

うまくいけばcmakeのログで

-- freetype2:   YES
-- harfbuzz:   YES

みたいなのが表示されているはず。

OpenCVビルド

/build/以下にOpenCV.slnが生成されている。ダブルクリックで起動。
ソリューションエクスプローラーの「CMakeLists->INSTALL」を右クリックしてビルド。VisualStudio上画面中央のDebugとReleaseを切り替えて2回ビルド。
Pythonのオプションを切ったからか、ノートPCでも15分もかからずビルドが終了した。

/build/install以下にヘッダやdllやlibが生成されているはず。
これらをqiita.comのように設定すれば、freetype2が使える。いい感じのところに配置してPATH環境変数に追加しておく。

utf8の罠

こちらのソースコードを使ってテストさせていただいたが、はじめ「あいうえお」が文字化けされて頭を抱えた。
f:id:lp6m:20210409154230p:plain
How to use OpenCV FreeType module with Visual Studio · GitHub
結果として、コンパイル時点で日本語が文字化けしているらしいので、/utf8オプションをつければよい。
/8 (ソースと実行可能文字セットをに設定します UTF-8 ) | Microsoft Docs

無事日本語をMatに描画できるようになりましたとさ。
f:id:lp6m:20210409154428p:plain


Ubuntu16.04/18.04 で nvidia-driverを使用すると画面がちらつく問題

PCを組んだ

新しくPCを組み直した。

M/B ASRock Steel Legend B550
CPU Ryzen 7 3700X
RAM CORSAIR DDR4-3200MHz 16GB*4
GPU ZOTAC GAMING GeForce RTX 2070 SUPER MINI
ケース NZXT H510 Elite
クーラー NZXT KRAKEN X53

電源やSSDなどはこれまでに使用していたものを流用した。

Ubuntu16.04/18.04のインストール

BIOS確認後、インストールメディアを作ってインストールを行うと、

  • Ubuntu16.04:nvidia-driverをインストールあと、画面がちらつく
  • Ubuntu18.04:インストール画面で既に画面がちらつく

という問題が発生した。
ちらつく、というのは画面に緑色の横線が入ったり、画面全体が点滅する。
BIOS画面やWindowsではそのような症状が起きないので、nvidia-driver側の問題だと思う。

調べてみると、以下のフォーラムにたどり着いた。
www.nvidia.com
この人はUbuntuではなくWindowsらしいけど、ケーブルの問題では?という内容が書かれていた。

購入したグラフィックカードには、HDMI*1, DisplayPort *3 の出力が存在する。DisplayPort-to-DisplayPortのケーブルを持っていなかったので、HDMIで繋いでいた。
DisplayPort-to-HDMIの変換コネクタを介してモニタにHDMI接続してもやはり同様の症状であった。

解決

結局、DisplayPort-to-DisplayPortのケーブルを購入した。DisplayPortで接続すると症状は消えた。

DNNDK + Ultra96でYOLOv3物体認識onFPGA (その1・FPGA向けデプロイまで)

はじめに

Deephiという中国の会社がXilinxに買収?されてXilinxからDNNDKとよばれるFPGAにDNNを簡単に実装するフレームワークがリリースされています。
f:id:lp6m:20190816163536j:plain:w500
今回はコレをやってみます。機械学習に関する詳しいことを全く理解していなくても適当にコマンドを叩くだけで物体認識onFPGAができてしまいそうです。
github.com

実行環境

  • Ubuntu16.04 LTS
  • GeForce GTX980
  • OpenCV 3.3.0
  • CUDA 9.0
  • CUDNN 7.0.5

UbuntuとCUDAとCUDNNのバージョンはDNNDKフレームワークがサポートするものが限られているので注意。
nvidiaのドライバのせいでログインループに陥ったりして環境構築をするのが大変だった。
自分の環境ではnvidiaドライバはnvidia-418がインストールされていて安定している。(nvidia-430やnvidia-387でログインループに陥った)

DNNDKのダウンロード・インストール

$tar -xvf xlnx_dnndk_v2.08_190201.tar.gz 
$cd xilinx_dnndk_v2.08/host_x86
$sudo ./install.sh Ultra96
Inspect system enviroment...
[system version]
No LSB modules are available.
Description: Ubuntu 16.04.6 LTS
16.04
[CUDA version]
9.0.176
[CUDNN version]
7.0.5
Begin to install DeePhi DNNDK tools on host ...
Complete installation successfully.

これでDNNモデルを量子化するdecentコマンドや量子化済みモデルをコンパイルするdnnc-dpu1.3.0コマンドが使用できるようになる。
このタイミングで対象ボードを指定するので、Ultra96向けに量子化コンパイルが行われるのだろう。

Tutorialのダウンロード・とりあえず実行

一旦、チュートリアル通りに実行してみます。

$git clone https://github.com/Xilinx/Edge-AI-Platform-Tutorials

チュートリアルのprerequisitesにも書いてあるようにYOLOの学習済みの重みをダウンロードする必要があります。

$cd Edge-AI-Platform-Tutorials/docs/Darknet-Caffe-Conversion/
$wget https://pjreddie.com/media/files/yolov3.weights
$mv yolov3.weights ./example_yolov3/0_model_darknet/
$bash -v tutorial.sh

これでいくつかの圧縮ファイルが解凍されて、必要なプログラムがmakeされます。
おそらく初めて実行すると環境が不十分でdarknetやcaffeのmakeがコケるので、適宜必要なものをインストールしてmakeが通るようにします。
何度も試行錯誤するときにmake cleanされるのはめんどくさいのでtutorial.shのmake cleanをコメントアウトして行いました。

UG1327ではcaffeのビルドに以下が必要と書かれています。参考程度に。
その他にdos2unixやprotobufとかもインストールする必要がありました。すべては覚えていません、スミマセン。

apt-get install -y --force-yes build-essential autoconf libtool libopenblasdev libgflags-dev libgoogle-glog-dev libopencv-dev protobuf-compiler libleveldbdev liblmdb-dev libhdf5-dev libsnappy-dev libboost-all-dev libssl-dev

※私の環境では 『.build_release/lib/libcaffe.so: `cvLoadImage' に対する定義されていない参照です』等でcaffeのビルドに失敗します。
これを回避するためcaffe-master/Makefileの421行目を書き換えました。

USE_PKG_CONFIG ?= 1 #0から変更

darknet(YOLO)で自前のデータを学習

今回は、とりあえずこの2つの障害物を認識してもらうことにします。認識精度などは置いといてとりあえず実行したかったので学習画像は各100枚程度にしました。
f:id:lp6m:20190816163536j:plain:w500

学習データの用意

f:id:lp6m:20190816165217p:plain:w400
こちらの記事を参考にさせていただいて、自前データの学習を行います。
チュートリアルをクローンしてきた時についてきたdarknet_originを使ってもいいのですが、今回はオリジナルのリポジトリからcloneしたほうで学習を行いました。
YOLOオリジナルデータの学習 - Take’s diary
Yolo v3を用いて自前のデータを学習させる + Yolo v3 &amp; opencv のインストール方法付き(Ubuntu 16.04, Opencv 3.3, Conda) - Qiita
1つめの記事にしたがって、yolov3-voc.cfgのclasses, filtersを3箇所書き換えました。(今回はclasses=2, filters=21)
2つめの記事にしたがって画像のアノテーションを行い、train.txt, test.txtを作成し、obj.data, obj.namesファイルを正しく記述します。

学習を実行

記事通りにデータを用意すれば、以下のコマンドで学習が始まります。

./darknet detector train cfg/obj.data cfg/yolov3-voc.cfg darknet53.conv.74 

GPUのメモリが足りずに以下のようなエラーで落ちることがあります。

CUDA Error: out of memory
darknet: ./src/cuda.c:36: check_error: Assertion `0' failed.

これはcfg/yolov3-voc.cfgの頭のsubdivisionsを16に変更することで回避しました。参考:darknetでYoLoV3マルチクラス学習 - ロボット、電子工作、IoT、AIなどの開発記録

2時間くらい放置して2000回程度回しました。

2101: 0.069695, 0.069695 avg, 0.001000 rate, 3.346575 seconds, 33616 images

avgの前の値が小さいほどいい感じに学習ができているということだそうです。今回は早く試したいのでとりあえずこの辺で学習は終了。
(重みは100回ごと?に保存されるようです)

学習済みモデルの確認

参考の記事通りに設定を書いていればbackupディレクトリ内に学習済みの重みが保存されているので、これを使って学習がうまくいったか確認します。

./darknet detector test cfg/obj.data cfg/yolov3-voc.cfg backup/yolov3-voc.backup test.jpg -thresh 0.1

これで先ほどの画像が結果として得られました。とりあえずはうまく行ってるようです。簡単にできてしまって凄い。

自前で学習した重みとネットワークをFPGA向けにデプロイ

基本的にはtutorial.shそのままでいいのですが、何故か自動化されて欲しいところが自動化されていなかったので簡単な追記を行いました。
修正したものはここで公開しています。
github.com
tutorial.shのワークフローを簡単に図にしたものを以下に示します。
f:id:lp6m:20190816174329p:plain:w500
オレンジ色の1.5_proto_patch.py2.5_proto_patch.pyは今回私が作成した簡単なコードです。
両方とも、その前のプロセスで出力されたDNNのモデル情報のテキストファイルを次のプロセスに渡すために、一部コメントアウトしたり情報を追加するものになっています。
ドキュメントでは自分で手作業で編集するようになっており、元々のtutorial.shでは、編集済みのファイルをコピーするようになっています。。
これだとモデル情報を変更した際に自動化できないので、雑なコードを記述しました。
自前で学習した重み・ネットワークをデプロイするために修正した箇所は以下の通りです。

0_convert.sh

学習に使用したモデルのcfgファイルと、学習済みの重みを手作業でコピーしておきます。
これらを読み込むように2つの引数を書き換えます。

python ../yolo_convert.py 0_model_darknet/yolov3-voc.cfg  0_model_darknet/myweight.weights 1_model_caffe/v3.prototxt 1_model_caffe/v3.caffemodel

0_test_darknet.sh

同様に自分のモデルと重み、検出クラスのファイルを使って検証を行うように書き換えます。

../darknet_origin/darknet detector valid  5_file_for_test/obj.data 0_model_darknet/yolov3-voc.cfg 0_model_darknet/myweight.weights -out yolov3_results_
cat results/yolov3_results_* >> 5_file_for_test/yolov3_darknet_result.txt	cat results/yolov3_results_* >> 5_file_for_test/yolov3_darknet_result.txt

学習時に使用したobj.namesを5_file_for_testディレクトリ内にコピーしておくことも忘れずに。

obj.data

obj.dataは学習時に使用したものから書き換えました。検証を行う対象の画像を1つにするためです。

classes = 2
valid = example_yolov3/5_file_for_test/image.txt
names = example_yolov3/5_file_for_test/obj.names

ここで検証対象の画像ファイルのパスが書かれているテキストファイルがimage.txtであると指定されています。
image.txtの中身はtest.jpgなので書き換える必要なし。test.jpgを自分が検証に使用したい画像に差し替えます。

1_test_caffe.sh

クラス数を自分が学習したクラス数に書き換えます。(今回は2)
これをしないと出力されるdetection.jpgに枠が無数に表示されておかしなことになった。

./../caffe-master/build/examples/yolo/yolov3_detect.bin 1_model_caffe/v3.prototxt \
                                                        1_model_caffe/v3.caffemodel \
                                                        5_file_for_test/image.txt \
                                                        -out_file 5_file_for_test/yolov3_caffe_result.txt \
                                                        -confidence_threshold 0.005 \
                                                        -classes 2 \
                                                        -anchorCnt 3

5_file_for_test/calib.txt

何をやっているのかは正直よくわかっていませんが、2_quantize.sh量子化を行う際にキャリブレーションという作業が行われるようです。
チュートリアルには

The 5_file_for_test/calib_data folder contains some images from the COCO dataset, to be used for the calibration process.

とかいてあるので、学習データのサブセットのファイル名を記述しておけばよいようです。
フォーマットはファイル名 1で、1には特に意味がないらしいです。
学習時に作成したtrain.txtのデータを整形してcalib.txtとしてあげればOKです。
指定したパスに学習画像もコピーしてあげる。

1.5_proto_patch.py, 2.5_proto_patch.py

0_convert.sh, 2_quantize.shで生成されたモデル情報は次プロセスまでに手作業で書き換える必要があります。
先述の通りなぜか元々は編集済みのものをコピーして書き換えるようになっていましたが、汎用性がないので適当にコードを書きました。
コードはここにあります。Edge-AI-Platform-Tutorials/docs/Darknet-Caffe-Conversion/example_yolov3 at master · lp6m/Edge-AI-Platform-Tutorials · GitHub

tutorial.sh

caffe-masterをビルドした後にcd ..抜けのミスがあるのでそれを追記。
1.5_proto_patch.pyと2.5_proto_patch.pyを途中で呼び出すように修正。
ここには#check the environment以降をそのまま貼り付けておきます。

#check the environment
python -c "import caffe; print caffe.__file__"
cd ..

############################################################################
# Section 3.0
############################################################################
cd example_yolov3/
rm results/*
rm 5_file_for_test/yolov3_*_result.txt

# step 0: Darknet to Caffe conversion
bash -v 0_convert.sh
# step 1: test Darknet and Caffe YOLOv3 models
bash -v 0_test_darknet.sh
bash -v 1_test_caffe.sh
# step 2: quantize YOLOv3 Caffe model
cp 1_model_caffe/v3.caffemodel  ./2_model_for_quantize/
cp 1_model_caffe/v3.prototxt 2_model_for_quantize/v3.prototxt
python 1.5_proto_patch.py 2_model_for_quantize/v3.prototxt 416 416 5_file_for_test/calib.txt 5_file_for_test/calib/
bash -v 2_quantize.sh
# step 3: compile ELF file
# cp 3_model_after_quantize/ref_deploy.prototxt 3_model_after_quantize/deploy.prototxt
python 2.5_proto_patch.py 3_model_after_quantize/deploy.prototxt
bash -v 3_compile.sh
# step 4: prepare the package for the ZCU102 board
cd ..
cp example_yolov3/4_model_elf/dpu_yolo.elf yolov3_deploy/model/
tar -cvf yolov3_deploy.tar ./yolov3_deploy
gzip -v  yolov3_deploy.tar

実行

ここまで修正すれば、自前のデータを読み込むようになったので、tutorial.shを実行してデプロイ!です。
感覚的には2の量子化は一瞬で、3のコンパイルが結構時間かかるっぽいです。
f:id:lp6m:20190817012757p:plain:w400
エラーが特にでていなければ終了。最後にデプロイしたデータをtar.gzに圧縮されるか聞かれます。
生成されたyolov3_deploy/model/dpu_yolo.elfが生成されたネットワークのようです。
yolov3_deploy内のテストデータ(coco_test.jpg, test.avi)は元々のデータなので手作業で差し替えました。

わりと1日ですんなりできてしまった。次回は実機で動作確認しようと思います。
ZC102じゃなくてUltra96だけどうまくいくかな?

Vivado HLSでRGB/HSV + HOG + SVMの高速物体検出をする2(完成)

前回(Vivado HLSでHOG+SVMの高速物体検出をする1(2つめのコンポーネントまで作成) - lp6m’s blog)の続き。
(相変わらず時間がすごくたってしまったけど)

何をつくったのか

ココにHLSプロジェクト・Vivadoプロジェクト・DeviceTreeなど全ておいています。とりあえずの使い方などはREADMEに書きました。
Python3+scikit-learnでLinear SVMの学習を行う。学習したパラメータを取り出して、C++に推論のコードを移植。その後推論コードをFPGA向けのアルゴリズムを使ってHLS IPを作成。
SWのみで実行した場合よりも270倍以上高速化された!!
github.com
このプロジェクトはHEART 2019 Design Contestのために開発しました。
コンテストで赤信号を検出して停止する様子:
www.youtube.com

仕様

作成したHLS IPの最終的な仕様は以下のようになった。

  • スライディングウインドウ法 * SVMで物体検出
  • 入力:320pix*240pix BGR画像(1画素32bit, 8bit不使用)((OpenCVではデフォルトがRGBではなくBGRの順。24bitでなく32bitにしているのは転送に使用するDMAが2のべき乗のデータ幅しか使えないため)
  • ウインドウサイズ:32pix*64pix
  • 出力:891個のウインドウ領域に対するSVMの出力
  • 特徴量:HOG(Histogram of Oriented Gradients)・BGR,HSV画素値

基本的なアルゴリズムは参照している論文の通り。これにBGR/HSV特徴量を追加した。
HOGについてはセルサイズ8pix, ブロックサイズ2*2,ヒストグラムのビン数9で論文と同様。
BGR/HSV画素特徴量に関しては以前のRFのとき(FPGAデザインコンテスト@FPT2018 開発記 - lp6m’s blog)同様、32pix*64pix画像を8*8に圧縮したものを使用する。

参照した論文との仕様の違い

  • HOGに加えてBGRHSV画素特徴量を使用している
  • 論文ではHOG特徴量のnormalizationにL1-sqrt正規化を使用しているが、sqrtの回路が大きく、また使用する効果がわからなかったのでL1正規化を使用した
  • 論文では16bit?の固定小数点を使用しているが精度がどれくらい落ちるかわからなかった(怖かった)ので、32bit(整数部10bit)の固定小数点を使用している
  • 論文では途中何度かシフト演算を入れているが入れていない

HLS IP全体像

作成したHLS IPには以下の図のような構成になっており、トップファンクションから呼ばれる関数が6つある(最後のmergeはトップファンクション内に書いている)。
画像の左部分がHOG, 右部分がBGRHSVの特徴量に対する処理となっている。左部分の4つの関数は論文を参考に実装したもの。
f:id:lp6m:20190624131858p:plain:w600
オレンジ色の矢印は外部BRAMからのSVMの重みを表す。

関数間のFIFO#pragma HLS DATAFLOWを使えばVivado HLSが適切にFIFOを設定してくれるが、関数のバイパスには対応しておらず、緑の矢印部分にはFIFOが挿入されない。
左部分のHOG部分と右部分のBGRHSV部分では、値がでてくるまでのレイテンシが異なるので、FIFOを挿入する必要がある。
このため、#pragma HLS STREAM variable = bgr_hsv_resultstream depth = 100 dim = 1で明示的にFIFOを挿入している。

前回の記事時点ではcompute_mag_and_bincell_histogram_generateを作成しただけだった。
HOG部分は基本的には論文そのまま実装しているので省略。何故か論文の図にはFIFOとかBRAMとだけ書いててどのようなアクセスをするのかが書いていなかったりして理解するのに少し時間がかかった。

HLS高位合成結果

合成結果は以下の通り。レイテンシがmin:160936, max:207736となっている。1画素に対して207736/320/240=2.7クロック程度かかる計算になる。ループのパイプライン化が完全にできていないところが何箇所かあるが、回路面積との兼ね合いでこのような結果になった。
回路面積を減らすために積算・除算のところに#pragma HLS allocation instances= limit=2を挿入している。
制約は8nsで合成結果は8.48snsになっているが、100MHzのクロックしか刺さないので問題ないとした。
f:id:lp6m:20190624154538p:plain:w800

Vivadoブロックデザイン

f:id:lp6m:20190624132449p:plain:w800
作成したHLS IPをDMA経由でZynq PSと接続する。
HLS IPのBRAMからの重み入力にはBlock Memory Generatorと接続する。Block Memory GeneratorとAXI BRAM Controllerを接続し、AXI BRAM ControllerをAXI Interconnect経由で接続することでBRAMの値をPSから読み書きできる。 参考:VIVADO HLS Training - BRAM interface #06 - YouTube
DMAをカスタマイズする部分としては以下の通り。

HLS IPの動作周波数は100MHzとした。
回路を合成した結果、タイミングエラーはなかった。

Ultra96へのOSインストール・Device Tree Overlay

Ultra96での実機動作のためのアレコレには、こちらのリンクを大変参考にさせていただきました。ありがとうございます。
qiita.com
proc-cpuinfo.fixstars.com
また、udmabufを使用したDMA制御のサンプルに、Interface1月号の「最強FPGAボードで人工知能カリカリ画像認識」を大変参考にさせていただきました。ありがとうございます。
shop.cqpub.co.jp

LinuxからFPGA上のIPコアを制御するための簡単な説明を書くと(嘘を書いていたら指摘して下さい)、FPGA IPの制御のためのレジスタはメモリマップされており、指定のアドレス(アドレスはAddress Editorで見れる)を制御することでFPGA IPを制御することができる。
一番単純なのはdevmemコマンドで物理アドレスを指定して直接レジスタを制御する方法で、C言語だとmmapして仮想アドレスを取得し、そのアドレスに値を書き込むことで実現できる。ただ物理アドレスを直接指定して制御するのは、OS等が使用しているメモリを破壊する可能性があり危険なので、デバイスドライバを記述するのがよいとされる。(udmabufはコレ!)xilinxのvideo系のデバイスドライバこの辺にあったりする(ドキュメントとか例が全然ない!)。


ただ自作のIP(今回はHLS IP)ごとにデバイスドライバを作成するのも面倒なので実験段階ではUIOという仕組みを使用することが多い。デバイスツリー*1generic-uioと記述し、使用するアドレスの範囲を記述する。これで比較的簡単・安全にIPを制御することができる。デバイスツリーによりLinux OSにはUIOデバイスとして/dev/uio に登録される。プログラムからは/dev/uioをopenしてからmmapして仮想アドレスを取得する。

これまではLinuxカーネルイメージの作成にPetaLinuxを使用していた。カーネルイメージを作成する際にデバイスツリーを使用していたので、回路を変更する度にカーネルイメージをビルドする必要があった。
今回からは最近流行り(?)のLinuxカーネルのDevice Tree Overlayという機能を使用することで、実機からFPGAのコンフィグレーション・デバイスツリーのオーバレイができるようになった!参考:FPGA+SoC+LinuxでDevice Tree Overlayを試してみた - Qiita


注意点としては、udmabufのためのDeviceTreeファイル(udmabuf0.dts, udmabuf1.dts)に記載するバッファサイズを余裕をもって大きく設定した際、合計が2^21(Vivadoで設定したDMAのWidth of Buffer Length Registerの値)を超えないようにすること。
当たり前ですが、ハマってしまったので。

IFレイヤー・アプリケーションの作成・性能評価

まずgithubに公開したapp/hog_svm_testについて説明する。
このアプリケーションは320pix*240pixのframe.pngから赤信号を検出し、検出結果をresult.pngに保存する。
result.pngはこんな感じになる。赤信号が正しく検出されていることが確認できる。
f:id:lp6m:20190624155504p:plain
SVMの重みはweights.jsonに保存されている。これを読み込んで、BRAMに書き込む。その後DMAで入力画像を転送・結果を転送している。
app/hog_svm_test/main.cppの51,52行目の

regs_write32(hls_regs, 0x01); //start
regs_write32(hls_regs, 0x80); //enable autorestart

はVivado HLSで合成時に自動生成されるドライバファイル、hls/hog_svm/solution1/impl/ip/drivers/hog_svm_v1_0/src/xhog.svm.c内の

void XHog_svm_Start(XHog_svm *InstancePtr) {
    u32 Data;

    Xil_AssertVoid(InstancePtr != NULL);
    Xil_AssertVoid(InstancePtr->IsReady == XIL_COMPONENT_IS_READY);

    Data = XHog_svm_ReadReg(InstancePtr->Control_bus_BaseAddress, XHOG_SVM_CONTROL_BUS_ADDR_AP_CTRL) & 0x80;
    XHog_svm_WriteReg(InstancePtr->Control_bus_BaseAddress, XHOG_SVM_CONTROL_BUS_ADDR_AP_CTRL, Data | 0x01);
}
void XHog_svm_EnableAutoRestart(XHog_svm *InstancePtr) {
    Xil_AssertVoid(InstancePtr != NULL);
    Xil_AssertVoid(InstancePtr->IsReady == XIL_COMPONENT_IS_READY);

    XHog_svm_WriteReg(InstancePtr->Control_bus_BaseAddress, XHOG_SVM_CONTROL_BUS_ADDR_AP_CTRL, 0x80);
}

この2つの関数を実行しているのと同じ。(先述のInterface1月号の記事を参考にさせていただきました)

app/realtime_webcam内にリアルタイム用のアプリケーションを公開している。USB Webカメラから取得した640pix*480pixの画像から赤信号をリアルタイムに検出する。
開発したHLS IPは検出できる赤信号のサイズが32pix*64pixなので、それより大きなサイズの赤信号を検出したい際は前処理として、SW側で画像を縮小する必要がある。
公開しているアプリケーションでは3種類のサイズの赤信号を検出しようとするので1フレームにつき3回の画像縮小処理・HLS IPの実行が必要になるが、30fps以上の性能を達成できる。
同じリアルタイム用のアプリケーションを全てSWのみで記述し、ホストPCで実行できるようにしたものがcpp/realtimetestに公開している。手元のノートPCで実行しても、1fps未満の性能しか出ない。
手元で実行するとこんな感じで1fps未満ですリアルタイムに検出される様子が見れます。
f:id:lp6m:20190624173759p:plain:w400
Ultra96でSWのみとHWアクセラレータありで320pix*240pix画像に対する処理時間を比較評価したところ、
SWのみ :6.22milisec / 160fps
HW使用:1700milisec / 0.58fps
結果、約275倍の高速化に成功した。やったー。
ちなみに、手元のノートPCで実行した場合、 260milisec / 3.84615fpsなので手元のPCよりも40倍以上は速い。

BRAMについて

BRAMについての工夫について説明する。
ウインドウサイズ32pix*64pixに対してHOG特徴量のセルサイズが8pix*8pix,ブロックサイズが2cell*2cellなので、ウインドウ内のブロックの個数は32/8-1 * 64\8-1、すなわち3*7となる。
このうち縦方向の3つのブロックについての積和演算を並列に行う。1つのブロック内には4つのセルが存在するので、全部で12個の積を並列に計算している。
並列に計算するには重みを保存しているBRAMへのアクセスは並列に行える必要がある。BGRHSVについても考えると、全部で3*4+4*4=28個の配列をHLSの入力に設定して、28個のBRAM GeneratorをVivado上で配置する必要があった。
さすがにこれは煩雑になると思い、ブロック内の4つの重みをまとめた32bit*4=128bitをBRAMの1wordにすることにした。128bitの値を32bitずつにスライスして、積算をするようにしている。

accum_fixed multiply_accum_hog(ap_uint<128> weight, ap_fixed_point ul, ap_fixed_point ur, ap_fixed_point bl, ap_fixed_point br){
	ap_fixed_point ul_weight = 0;
	ap_fixed_point ur_weight = 0;
	ap_fixed_point bl_weight = 0;
	ap_fixed_point br_weight = 0;
	ul_weight.range(31, 0) = weight.range(127, 96);
	ur_weight.range(31, 0) = weight.range(95, 64);
	bl_weight.range(31, 0) = weight.range(63, 32);
	br_weight.range(31, 0) = weight.range(31, 0);
	return (accum_fixed)ul_weight * (accum_fixed)ul + (accum_fixed)ur_weight * (accum_fixed)ur + (accum_fixed)bl_weight * (accum_fixed)bl + (accum_fixed)br_weight * (accum_fixed)br;
}

BRAMへの値の入力について

BRAMの値、すなわちSVMの重みはHLSコード内では32bit固定小数点として表されている。SWからBRAMの値をセットする際、通常のC++ではVivadoHLSが使用しているap_fixed型が存在しないためunsigned int型で、32bitの値を表現する。
これを実現するために、32bitの固定小数点を、ビット列をunsigned intのビット列としてみなした値に変換する必要がある。
このために、高位合成をしないただのユーティリティとしてのVivado HLSプロジェクトutil/ap_fixed_convertを作成した。
python3+scikit-learnで学習したパラメータをweights.hで、

ap_fixed<32, 10>  unscaled_weight[1140] = { -0.10424178, -0.030799653, 0.052530228..}

のように宣言しておき、(勿論これによりパラメータは与えた定数と同じ値ではなく、固定小数点の精度で表せる値として宣言される)
それを以下のコードでunsigned intとしてみなした値変換している。

unsigned int convFixedToUint(ap_fixed<32, 10> val){
	unsigned int res = 0;
	string str = val.to_string(2, false);
	str = str.substr(2, str.length() - 2);
	unsigned int tmp = 1;
	for(int i = str.length() - 1; i >= 0; i--){
		if(str[i] == '.') continue;

		if(str[i] == '1'){
			res += tmp;
		}
		tmp = tmp << 1;
	}
	return res;
}

感想・その他

  • 5月に1ヶ月くらいで実装した。開発が思うようにいかず、自動運転システムへの赤信号検出システムの組み込みはコンテスト本番の1時間前に完了した。チームメイトに迷惑をかけてしまった。
  • 再現・検証はできていないがap_fixed固定小数点間のビット精度が大きいものへのキャストが、C-simの結果とCo-simの結果で異なることがあった。(何かの勘違いで自分が悪かっただけなのかもしれない?)

もしこれが本当だとすると、C-simのときの処理系の解釈と、高位合成の際の処理系の解釈が異なることになる??キャストの方法については高位合成マニュアルUG902をそれなりに読んだが、それでもハマった際は全然解決しなかった。
結局ap_fixedのメソッドrange()を使用することでビット列をコピーすることで回避した。

  • 当初は入力画像は640pix*480pixだったが、回路面積が大きいからか?、HLS IP内でタイミングエラーが起きてしまったのでとりあえず画像サイズを320*240サイズに縮小してみた。ちゃんとした最適化は考えきれていません。
  • 実はSVMによる誤検知(全く赤信号でないものが赤信号と認識される)が多く、困っている。これは学習データに起因するものではなく、単に線形SVMの限界なのかもしれないと思っている。
  • 以前のコンテストの際の実装ではウインドウの数だけHLS IPを呼び出す必要があったが、今回は1フレームにつき数回(検出したいウインドウのサイズの種類の個数回)呼び出すだけで済むので、まずまず目標は達成された。
  • 焦ってぐちゃぐちゃに開発していたので、リポジトリを整理・公開するのにかなり時間がかかってしまった。
  • いつもTwitter等で助言を下さる方々:本当にありがとうございます・・・!

Vivado HLSでHOG+SVMの高速物体検出をする1(2つめのコンポーネントまで作成)

あらまし

前回(FPGAデザインコンテスト@FPT2018 開発記 - lp6m’s blog)からかなり時間が経ってしまった。
コンテストの際の信号検出の実装は、

  • 特徴量:RGB/HSV特徴量+HOG特徴量
  • 認識:ランダムフォレスト
  • FPGAの利用:1ウインドウ(32pix*64pix)のHOG特徴量の抽出

カメラ画像から物体を検出する際にウインドウを動かしていくスライディングウィンドウ法を使用していた。
FPGAに実装したIPは1ウインドウに対する特徴量の抽出しかできないため、1フレームの処理のために何度もHWを呼び出す必要があり、あまり高速化できているとは言えない状態だった。

次のコンテストに向けて、というか趣味的にももう少し性能を向上したい。
色々と「物体検出をFPGAで高速化」みたいなものを探した結果、今回はこの論文にたどり着いた。

https://www.researchgate.net/publication/324497117_Pure_FPGA_Implementation_of_an_HOG_Based_Real-Time_Pedestrian_Detection_System

ポイントは、

  • HOG+SVMによる人間認識
  • FPGAのみ(Pure)で、CPUやDRAMを使用しない
  • 1フレームの画像をパイプライン的に処理できる
  • スライディングウインドウのウインドウの動く間隔は、セルサイズと同じ(8pix)
  • 600*800のフレーム画像に対して162fpsで判定が可能

ということ。
論文は全部で16ページあって、結構詳しめに書いてくれている。
とりあえずこの論文を読んで、パクってそのまま実装していくことにする。
多分論文ではHLSは使用しておらず、RTLで書いている気がする。
将来的にはRGB/HSV特徴量も加えた判定ができるようにしたい。

論文の図をココに貼るのは気が引けるので、番号で参照することにする。
HOG+SVMによる高速検出は、大きく4つのコンポーネントからなっており、順に作っていく予定。

  1. FIgure3: Diagram of the Gradient Calculation sub-module in the proposed structure
  2. FIgure5: Diagram of the Cell Histogram Generation sub-module in the proposed structure
  3. Figure6: Diagram of the Block Histogram Normalization sub-module in the proposed structure
  4. Figure7: Diagram of the SVM Classification in the proposed stucture.

(以後1.〜4.の番号で参照する)

今回やったこと

1.と2.の2つのコンポーネントを作った。
1.はグレースケールのデータからmagnitudeとbin_indexを計算し、出力する
2.は8pix*8pixのセルごとに・ヒストグラムのbin_indexごとにmagnitudeを合算して、出力する。

今回のコミットのリンクを貼る。
github.com
合成のメイン:main.cppは以下。
ImageDetectionHW2/main.cpp at afe31ceca667bf795b03494b05265073333af3f3 · lp6m/ImageDetectionHW2 · GitHub

テストベンチmain_tb.cppは、
1,の処理はHW用の実装を動作させ、次に2.の処理をHW向けとSW向けそれぞれの実装を動作させて、出力の一致を確かめている。
(2.の入力のために両者で1.のHW用の実装を使っている理由は、SW用の実装とHWの実装の結果が違うため。ここ参照

論文と今回の実装の違い

  • 入力画像サイズが論文は600*800, 今回の実装は480*640
  • bin_indexの決め方や、gradientの計算方法は論文よりも軽い計算にしている(特に意味はない、前の実装を引き継いでいる)
  • ウインドウのサイズが論文では7*15, 今回の実装では未定

問題点1

テストベンチで出力を確認したが、問題なかった。

合成結果は以下の通り。(Detail->Instanceの上が2.で下が1.)
f:id:lp6m:20190425120134p:plain
1.はLatencyが307206≒480*640となっており、1cycleで1pixの処理ができている。
2.のInitiation Intervalが1を達成できず、2になってしまっている。
合成中のログは以下の通り。

INFO: [SCHED 204-61] Pipelining loop 'loop_y_loop_winx_loop_cell_index'.
WARNING: [SCHED 204-68] Unable to enforce a carried dependence constraint (II = 1, distance = 1, offset = 1)
   between 'store' operation (/home/lp6m/Xilinx/Vivado/2017.4/common/technology/autopilot/hls/hls_video_mem.h:765->hog_svm/src/main.cpp:123) of variable 'tmp.data.V', hog_svm/src/main.cpp:121 on array 'cellbuf[0].val[1]', hog_svm/src/main.cpp:100 and 'load' operation ('tmp.data.V', /home/lp6m/Xilinx/Vivado/2017.4/common/technology/autopilot/hls/hls_video_mem.h:729->hog_svm/src/main.cpp:122) on array 'cellbuf[0].val[1]', hog_svm/src/main.cpp:100.
INFO: [SCHED 204-61] Pipelining result : Target II = 1, Final II = 2, Depth = 6.

うーん、どうやら依存があるみたい。

問題点2

2.のコンポーネントでは、
8*480個の入力ごとに、「8個の入力ごとに9個の出力データが完成」になる。
ループ内でデータが完成したときに9個連続で同じポートから出力していると、出力のほうに律速してしまう。
とりあえず今は出力を9個つくって並列に出力できるようにしている。
ちなみに、ap_axisのdataメンバを配列にする、といったことはできなかった。(FPGAの部屋 Vivado HLSでのAXI4-Stream のテンプレートを作成する1参照)
他の解決策としては、

  • 9個のデータをビットを並べた1つのデータとして出す
  • 何らかのバッファをおいて後から出力する

が考えられる。


とりあえず3.4.を作ってから2.の問題点を考えるようにしよう。

FPGAデザインコンテスト@FPT2018 開発記

FPGAデザインコンテスト@FPT2018 開発記

はじめに

この記事はFPGA Advent Calendar 15日目の記事です。
先日FPT2018にて行われたFPGAデザインコンテストに参加しました。
FPGA搭載の自動運転ロボットが、決められたコースを走りながらいくつかの課題(障害物回避・信号検知・人間検知)をこなすといった内容のものでした。
FPGAボードに搭載されているハードマクロCPUの使用は認められています。外部との通信が完全に禁止されているのですべての判断・計算をロボットに搭載されたシステムで行う必要があり、効率的なシステムを構築する必要があります。
第8回 相磯秀夫杯 FPGAデザインコンテスト

大学院の同じ研究室の友人と2人で参加し、なんとか優勝することができました。
ここではその開発記・自分が担当した実装の内容について記録したいと思います。
友人とは完全に分業制で開発を行ったため、自動運転システムの進捗は全くわかりません。
まずは走行の様子の動画です。障害物の回避・信号の検知に成功しています。途中でコースアウトしてしまったのが残念・・

ZytleBot 本戦

時系列と、信号検出の詳細について紹介します。
だらだら書いてたらだいぶ長くなってしまいました・・ブログってもっと完結に書いたほうがいいのかな、
自分が作ったものはココにおいてます。全く整理していないので整理していきます・・
github.com

時系列

5月・6月

コンテストの存在を知る

7月

  • コンテストに参加したいと指導教員に伝えて、ZYBO + TurtleBot3を使用することを決めた

(研究室にTurtleBot3が存在したから選んだだけ)

  • ココの記事を参考にZYBOにUbuntu OSを搭載した

aster-ism.hatenablog.com

  • PetaLinuxのカーネル設定でWebカメラやTurtleBotに搭載されているOpenCRへの接続ができるようになった
  • ZYBOのUSB Host電源供給に苦しむ

( USBの端子が2つある8ポートハブの片方をZYBOに、もう片方をモバイルバッテリーに接続することで無理矢理電源を安定させている)
USB2.0ハブ 8ポートタイプ|株式会社バッファロー BUFFALO

8月

  • 那覇で行われたワークショップに参加:他のチームの進捗を見た
  • ZYBOにMIPI接続するPCam 5Cカメラの画像をUbuntuから取得するのに苦戦する→1ヶ月かけてなんとか成功

lp6m.hatenablog.com

 (デバイスドライバソースコードを読んだり、デバイスツリーについて勉強した)

  • 画像が取得できるようになり、自動運転SWの開発を完全にチームメイトに丸投げする

  大学の地下室のコース設置なども丸投げしてしまって申し訳なかった

 FPGA向けHalideコンパイラの開発に参加・多くのことを学んだ

9月

  • 慶応大学日吉キャンパスにて国内大会が開かれる。

  カメラ画像の取得以外は完全にSWで頑張っている状態で3位入賞。自分の貢献はほぼゼロ。
1位で優勝したチームが圧倒的強く、FPGAをうまく活用していることに憧れる、Twitterで交流する

10月

  • あと2ヶ月しかないという状況のなかFPTに向けて開発開始
  • PCamで取得した画像をFPGAで処理してからCPUに転送しようとしていろいろやってみる。
  • 結局1ヶ月かかって失敗(現在も未解決)

lp6m.hatenablog.com

10月末〜11月

あと1ヶ月で何ができるかを考えた
方針は、

  • むやみやたらに挑戦せず、頑張れば出来そうなラインをみつけて頑張る
  • 実装が公開されているものを探しまくってパクる・ひたすらググる
  • 必ず1ヶ月で完成させる、妥協するところは妥協する

信号検出ならなんとかなりそうということになり、信号の検出を目指すことに。
とりあえず3Dプリンタを購入してもらったので信号機を作った。


「ゼロから作るディープラーニング」を斜め読みして、「無理」となったので、ディープラーニング以外の機械学習アルゴリズムで、実装を完全に理解してフルスクラッチで実装できるアルゴリズムを探した。

以下の記事を参考にした。ありがとうございます。。
HOG特徴量とSVMを使った自動車の検出 - くーろんログ
画像からHOG特徴量の抽出 - Qiita
ランダムフォレストとSVMの使い分け - 静かなる名辞
ランダムフォレストのつくりかた(C++の実装例つき) - じじいのプログラミング

ZYBOにMIPI経由で接続されているPCamとは別にWebカメラを取り付けて、Webカメラから取得した画像から信号を検出する。
PCamは下向き、Webカメラは上向きに取り付けた。
f:id:lp6m:20181216185054j:plain:w500

次に実装が落ちているものを探した。以下のリポジトリにたどり着いた。
HOG+SVMによる人間検出 on Zedboard
GitHub - nikkatsa7/HOG_Zedboard: A real time Histogram of Oriented Gradients Implementation on FPGA
Real time HOG implementation on Zedboard - Xilinx XOHW18-222 - YouTube
 ・こちらはXilinxのコンペで優勝したらしい
 ・LinuxからAXI4/AXI-Lite経由でHLS IPを使う実装が公開されているので採用
 ・実はこのHOGのHLS実装が間違っていることに後々気づく
Python scikit-learnでRFによる車の検出
GitHub - t-lanigan/vehicle-detection-and-tracking: Detecting vehicles in a video stream using machine learning. Adds on to lane detection project.
 ・scikit-learnなんて使ったことないけどとりあえず実装が落ちていたので採用
この2つを参考に信号検出器を作成した。どうにか1ヶ月で完成させないといけなかったのでスケジュール管理したりしていた。
f:id:lp6m:20181216165203j:plain:w450

HOG特徴量とRFを使った信号検出器の作成

結局やったことは①と②の実装を組み合わせただけなんですが、、、まあ何をやったか書いていきます。

ランダムフォレスト・物体認識の仕組みの理解

まずは、②のソースコードやRFに関する記事を読んで、何をやっているのかを理解した。
(たぶん当たり前すぎて今更な内容なのでしょうが、恥ずかしながらそれすら知りませんでした。学部でこんな勉強したっけ・・?自分にとっては初めてだったので、まとめておきます。)

・学習
信号の画像(32x64)→特徴量の抽出(画素・ヒストグラムHOG) → RFの決定木を作成
f:id:lp6m:20181216173851p:plain:w700
・推論
カメラから取得した画像 → ウインドウをずらしながら画像を切り取り → 切り取った画像それぞれに対して → 特徴量の抽出(画素・ヒストグラムHOG) → RFによる認識
f:id:lp6m:20181216180447p:plain:w700
RFについての詳しい説明は省略しますが、いっぱい推論してくれる木が複数あって、
特徴量は1次元の配列に格納され、それぞれのif文の木をたどると(赤信号のサンプル数、赤信号以外のサンプル数)が得られます。

特徴量抽出・RF推論器のフルスクラッチ実装

②の実装において特徴量の抽出はscikit-learnとnumpyを使うことで数行で実現されている。
ライブラリのソースコードを読んでC++へ移植。scikit-learnのHOGC++でそのまま実装すると大変な行数になった。
https://gist.github.com/lp6m/349948c876bf1b80abe06bb9bfaed37a#file-hog-cpp

RF推論器のフルスクラッチ実装は簡単で、Scikit-learnで学習したモデルのを再帰的に辿ることで、if文の木を作成できる。というかほぼ答えみたいなのがあった
stackoverflow.com

フルスクラッチ実装ができたことで、Pythonとscikit-learnを用いて学習させ、学習モデルをC++コード化してC++から利用することができるようになった。

ちなみに、本来は抽出した特徴量に対する正規化を行ってから、RFに推論させるが、正規化処理はそれぞれの特徴量の配列要素に対して独立した計算なので、RFのif文の中のしきい値を逆正規化することで、推論時の正規化処理を省略している。

学習用画像作成ツール・学習済みモデルテスト用ツールの作成

・32x64の画像を学習用に大量に作成する必要があり、撮影した動画から生成した連番画像から信号の部分を手作業で切り抜く必要があった
・自分用にツールを作ったほうが早かったのでTkinterを使って作成
Python + Tkinterで連番画像ファイルを素早く切り抜くGUI画像トリミングツール - Qiita
・これを応用して、学習済みモデルを簡単に試すためのツールも後に作成した。(左下に赤信号である確率が表示される)
わりと外乱があっても精度がでていることがわかる。
f:id:lp6m:20181216174153g:plain:w500

ZYBO用にHOG計算の簡単実装・HLS実装

C++で実装したリアルタイム信号検出器をZYBOのCPUで動作させると1フレームにつき1.5secほどかかってしまった。主にHOG特徴量の抽出が非常に重たかった。(sqrtしたりatanしてるので当たり前)
そこで①の実装を参考にする。ユークリッド距離の計算・ヒストグラム正規化を簡略化したりatanのテーブルをLUTに持たせることで高速化していることを参考にする。
ただ、この実装をそのままパクって使うと、SWでの計算結果と実際にFPGAインプリメントした際のHWの計算結果が異なった。
理由はVivado HLSにおけるpragmaにあった。`#pragma HLS DEPENDENCE inter false`で配列インデックスへに対するループ依存がないことを高位合成ツールに伝えてパイプライン化を実現しているが、実際にはループ依存が存在する。
ただしこのpragmaを外すとレイテンシが大きいHWが生成されて頭を抱えた。

ふとインターンでやったラインバッファのことを思い出して、「HOG LineBuffer FPGA」で検索をかけたところ、以下の論文アーカイブ(?)に到達した。
(https://arxiv.org/ftp/arxiv/papers/1802/1802.02187.pdf)[A High-Performance HOG Extractor on FPGA]
各画素のgradientを計算する際に4近傍の画素の情報が必要になるが、ラインバッファで縦=3, 横=画像の幅=64の画素をもっておくことでループ内での画素読み込み(ブロックRAMからの読み込み)が1回で済む。
これを参考に、ラインバッファを使用したHOG特徴量抽出HLSコードを作成。レイテンシも短く、SWとHWで結果が一致するHLSコアが完成した!
■参考にしたリポジトリのHLSコード:ラインバッファなし
HOG_Zedboard/hog.cpp at master · nikkatsa7/HOG_Zedboard · GitHub
■実装したHLSコード:ラインバッファあり
ImageDetectionHW/main.cpp at master · lp6m/ImageDetectionHW · GitHub
比較すると、画素情報が格納されている`image_buffer`へのアクセス(=ブロックRAMへのアクセス)がループ内で1回で済んでいることがわかる。

↓これはなんかHLS実装してたときのメモ
f:id:lp6m:20181216181517p:plain:w500

FPGAへの実装・Linuxからの利用

作成したHLS IPコアは1枚の画像のHOG特徴量を計算してくれるコアなので、ウインドウが300個あるときは300回HLS IPコアを実行する必要がある。
Vivado上でHLS IPを4つならべて、4つの画像のHOG特徴量を同時に計算してくれるようにした。
f:id:lp6m:20181216182825p:plain:w500
LinuxからUIOを用いてHLS IPを利用するにあたっては①の実装を丸パクリさせていただいた。ありがとうございます。。。

結果的にSWで計算するよりも5,6倍高速になり、ZYBO上で12〜15fpsの信号検出を達成した。FPGAを活用したといえる(?)状態にはなった。

完成したものを友人に投げて、自動運転システムに組み込んでもらった。

おわりに

なんとか1ヶ月でFPGAを活用した信号検出プログラムを作成することができた。
画像認識や機械学習も初めてで、HLSツールもまともに使ったことがなかったのでなかなか頑張ったとは思っている。
開発中は、コースの整備やロボット本体の作成などに予想外に時間を取られた。
開発記には書いてないが、OpenCVを使ってカメラ画像を取得するとCPU使用率が非常に高くなったのでV4L2 APIをゴリゴリ叩いてカメラ画像を取得したりしている。
そういった絶妙なハマりポイントに陥りまくった。
手元のPCでは余裕で30fpsで信号を検出できるのにZYBOで実行したら1.5fpsとかになったりして、エッジデバイスの弱さを実感した。

通信プロトコルがAXI4/AXI4-LiteなのでCPUが通信を制御する必要があり、結局SW/HW間の通信がボトルネックになっているのが残念。
AXI-Streamプロトコルを使ってDMA転送することでもっとCPUの負荷を減らせるとは思う。DMAをLinuxから使えるようになりたい。
使えるようになったらブログにまとめたいと思う。

まだまだ課題もあるが、コンテストのおかげで色々と勉強することができた。
Twitter等で助言を頂いた方々、ありがとうございました。