ぼくのかんがえた（さいきょうの）ARMでビルドするOpenCVふぁーむ

# はじめに
これは、[OpenCV Advent Calendar 2016] (http://qiita.com/advent-calendar/2016/opencv) 24日目の記事です。関連記事は[目次](http://qiita.com/advent-calendar/2016/opencv)にまとめられています。

本当は「**浮動小数点の割り算の精度問題**」と題して、浮動小数点の精度や、プラットフォームごとの浮動小数点演算のSIMD化の副作用について語ろうと思っていたのですが、あまりにも小さい範囲の話なので、「きっと誰も喜ぶまい。それよりも**みんな大好きARM**のビルド方法を紹介した方が少しは役に立つ記事になるのではないか」と思い立って、方針変更しました。


という訳で今日はARMでOpenCVをビルド**しつづける**話

# OpenCV 公式のBuildbot
- http://pullrequest.opencv.org/ に行くと、OpenCVの公式のビルドの様子を眺めることができます。
![buildbot.png](https://qiita-image-store.s3.amazonaws.com/0/12162/b53ce72b-6f55-bbcc-371f-c987656d2699.png)
- ビルドされる状況を**日がな一日眺めていられる**気がします。
  - そして、投げられるPRも、テストFAILしてるものが案外多い、という感想です。
- 元来Intelと関係があったからなのか、ここで試されるビルドはほとんどがIntelマシン(x86)です。
- Linuxもx86_64だし、Macでのビルドもx86_64だし、iOSのビルドもクロスコンパイラでx86_64上で行われます。
- ARMのビルドも行われるのですが、毎回行われる訳ではなく、**OpenCVの公式開発者が必要なときに必要なPRに対して実行している**ようです。
- なお、行われるコンパイルはクロスコンパイルのARMv7、ARMv8、及び実機上でのTegraK1とTegraX1のようです。


# ARMでビルド
- さて、皆さん、**OpenCVをビルドできるARMのマシン、何台ぐらいお持ち**ですか？
- 今年はRaspberry Pi 3が発売され、安価に手に入るARMマシンが非常に増えたように思います。以下に代表例をいくつかピックアップしてみました。
 - Tegra K1
 - Tegra X1
 - ODROID-X2
 - ODROID-C2
 - PINE64
 - Raspberry Pi 3
 - HiKey
 - DragonBoard 410c
 - Helio X20
 - Orange Pi PC2
- 他にもBeagleBoardやCubieBoard、Banana Pi など枚挙に暇がありませんが、**筆者が持ってるものだけ列挙する**ことにします。
- さて、これだけ並べると案外環境がマチマチなもので、同じOpenCVのコードでも、環境の違いによりビルドできたりできなかったりします。
- 代表例はTegraK1とX1ですね。この2機種だけにはCUDA対応のGPUが搭載されており、必然的にビルドがCUDA付きで行われます。
- 他にもHiKeyに付属しているDebianではこのご時世にgcc 4.8です。
  - ```aarch64```のgcc 4.8はfp16対応が行われておらず、fp16関連の命令は受け付けてくれません。
  - なので、OpenCVのfp16関連の実装は残念ながらSW実装で行われることに（CPU自体はHW命令を持っているのに）
- 他にもOpenCVに付属しているlibpng は、ARM64bitでのビルドにちゃんと対応していないという問題があります。
  - なので、うっかりOSからlibpngのパッケージを削除すると、libpng のビルドが走り、結果リンクエラーに遭遇します。
- ここに書いたエラー一覧はこの一年で筆者が遭遇したARMのビルドエラーです。
- **誰か気づけよ！！！**って言いたくなる初歩的なビルドエラーばかりなのですが、前述の通りOpenCVのビルドマシンで**ARM用ビルドは毎回走る訳ではありません**
- ~~しかも本記事執筆時点では64bitARMはビルドできるものの、大量のテストがfailする状態ですので、なかなかに大変です。~~
  - 3.2.0-RCで確認したら、[PRの7526番](https://github.com/opencv/opencv/pull/7526)がマージされたおかげでほとんどのテストがPASSするようになっています


# ARMおじさんとして
- ここはARMおじさんとして自前の**ARMデバイスでちゃんとOpenCVがビルドできるか確認しないといけない**訳なんですが、これが意外と面倒。

1. 開発マシンからgithub にpushする
2. ARMマシンにログインして、github から1でpushしたbranchをpullする (cd opencv && git pull origin)
3. 念のためクリーンビルドをするので、一度ソースコードをサラの状態にする (rm -rf build)
4. ビルド用ディレクトリを作る (mkdir build)
5. cmake を走らせる (cd build && cmake .. ) **ただし、マシンによってオプションを変える必要がある**
6. 並列ビルドする(make -j 4 all) **ただし、コア数がマシンによって違うので注意**
7. OpenCVのビルドを待った後、テストを走らせる
8. 結果を目視、場合によってはパフォーマンステストの結果を集計する
9. **2-8をマシンの台数分繰り返す**

- 最後の9番がなかなかに鬼な感じのタスクですし、途中4.と5.も、毎回同じ作業な訳ではなく、**NEONを有効にしたりしなかったり**と、うっかり間違えることもあります
- もちろん、ビルドエラー、テストエラーが発生した場合は**1.からすべてやり直し**です。
- 修正箇所が小さかったらフルビルドしなくても良いですが、万全を期すためにはやはりフルビルドが必要不可欠です
- ARMおじさんとしてビルドをして修行しているわけですが、正直、毎日この苦行をやっていると**やってられるか！**と叫びたくなります


# やってられるか！
- 叫んでみた

# 解決策
- 何か良い解決策は無いかと思い立って色々調べてみたところ、[Jenkinsを使ってRaspebrry Pi2 上に、WindowsマシンからSSHログインしてリモート操作する記事](https://dev.iachieved.it/iachievedit/an-arm-build-farm-with-jenkins/)を見つけました
  - なんと、やはり同じことを考える人はいるものなんですね。
- 詳細はリンクした記事を読んでいただくとして、ざっくりまとめると以下の手順です

1. 母艦マシンにJenkinsをインストール
2. Jenkins にsshプラグインをインストール
3. ARMマシンにjava の実行環境をインストール
4. ARMマシンに鍵認証でログインできるアカウントをセットアップ
5. Jenkinsに秘密鍵とARMマシンのIPアドレスを登録
6. もろもろJenkinsでビルド手順をセットアップ (後述)
7. **3.-6.を台数分繰り返す**

- ここでも台数分繰り返す必要があります
- 皆さん、手持ちのARMデバイスでビルドを繰り返すのに、どれぐらい苦労してるんでしょうか？台数が多いと大変ですよね

# ビルドスクリプトについて
- 問題になるのがビルドスクリプトです。
- 前述したように、マシンによってNEONを有効にしたり、opencv_extraリポジトリの場所が違ったり、またビルドのコア数を変えなくてはいけません
- その対処のために、環境変数を利用しましょう
- Jenkinsではビルドマシン（スレーブ）ごとに環境変数を設定できます
- また、ビルド時のスクリプトに環境変数を含むこともできます

```
cd ~/openv-fork
rm -rf build
mkdir build
cd build
cmake ${CMAKE_BUILD_OPTION} ..
make ${PARALLEL_BUILD} all
```
- こんな具合に書いておくと、例えば32bit ARM でNEONを有効にしたい場合
- ```CMAKE_BUILD_OPTION```変数に ```-DENABLE_NEON=ON``` と書いておけばNEONを有効化してくれます
- また、```PARALLEL_BUILD```変数に各マシンのコア数を書いておけばビルド時にコア数を間違えて指定することもなくなります

# テストも実行する
- ビルドが終わったら、ジョブをカスケードして、テストを起動させるのが良いでしょう
- ただし、OpenCVのテストはコアを複数使ったりしますので、1つずつ走らせるように、各マシンの並列ビルド数(make時のコア数とは違う)を1にしておきます
- こうすることで毎回クリーンな状況でパフォーマンステストを実行できます

![build-trigger.png](https://qiita-image-store.s3.amazonaws.com/0/12162/ea4c6c14-3eae-804e-f6f6-262cf4d8bbd7.png)

- こんな感じで、実行するテストプログラムを変数としてジョブに渡し、テスト実行用に別のジョブを作成します

```
#!/bin/bash
cd ~/opencv-fork/build/bin
time ./${TARGET_PROGRAM} 
```
- このようにテストを実行するのに共用の部分だけをスクリプトに起こすことで、いろんなテストに使いまわすことができます。
- [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself)の原則ですね！

# 結果を待つ
- あとはgithubをポーリングするなり、連携するなりすれば、githubにブランチをpushしたら、各マシンで並列にビルド、テストが実行されます
- 一斉に走り出す姿はなかなかに壮観で、ビルドされる状況を**日がな一日眺めていられる**気がします。

![build-running.png](https://qiita-image-store.s3.amazonaws.com/0/12162/7c4aff94-e2ac-631e-e468-f5864498b6b6.png)

- (一部のマシンがオフラインなのはご愛嬌)

# 結果をDropboxとGitを使って集約する
- ビルドと実行はリモートのARMのターゲットマシンで実行されますが、実行時のログは母艦のWinマシンに残ります
- 実行時のログは ``Jenkins/${BUILD_TYPE}/builds/${TARGET_LOG}/log``` みたいな感じで取得できます
- 以下はあくまで一例ですが、シェルスクリプトで整形した後、 ```/D/work/buidLog``` 以下に```raspberry-pi-3-opencv_test_core.txt``` みたいなファイルを作り、その場でブランチ```raspberry-pi-3```にコミットします
 
```
/bin/tr -d '\r' < /D/Jenkins/${BUILD_TYPE}/builds/${TARGET_LOG}/log | /bin/sed -e 's/.*LCAAAAAAAAP9b85aBtbiIQTGjNKU4P08vOT.*//g' >  /D/work/Dropbox/buildLog/${TARGET_NODE}-${TARGET_PROGRAM}.txt
pushd /D/work/buildLog
/bin/git stash
/bin/git checkout ${TARGET_NODE}
/bin/tr -d '\r' < /D/Jenkins/${BUILD_TYPE}/builds/${TARGET_LOG}/log | /bin/sed -e 's/.*LCAAAAAAAAP9b85aBtbiIQTGjNKU4P08vOT.*//g' > ./${TARGET_NODE}-${TARGET_PROGRAM}.txt
/bin/git add ${TARGET_NODE}-${TARGET_PROGRAM}.txt
/bin/git commit -m "${TARGET_LOG}"
/bin/git push --set-upstream origin ${TARGET_NODE}:build/${TARGET_NODE}
popd
```
- 最後に、```git push``` していますが、このpush先のリポジトリをDropbox上に置いてあるため、ビルド/テストが終わると自動的にDropboxでログが取れるようになっています
- ポイントは、ファイル名には日付をつけず、同じテストのログは**同じ名前で保存します**。こうすることでビルドごとにコミットができて、ファイルの編集履歴としてビルドの結果を比較することができます
- 順当に履歴を回収していくと、こんな感じにマシンごとのビルドグラフを作れます
![build-history.png](https://qiita-image-store.s3.amazonaws.com/0/12162/2a9c5125-27d2-5224-dd8a-a96e1cfeb906.png)
- この時点でOpenCVの記事なのかgitの記事なのかDropboxの記事なのかもはや意味不明ですね


# ファームを作成した後
- 先程のテストの手順は

1. 開発マシンからgithub にpushする
2. ARMマシンにログインして、github から1でpushしたbranchをpullする (cd opencv && git push origin)
3. 念のためクリーンビルドをするので、一度ソースコードをサラの状態にする (rm -rf build)
4. ビルド用ディレクトリを作る (mkdir build)
5. cmake を走らせる (cd build && cmake .. ) **ただし、マシンによってオプションを変える必要がある**
6. 並列ビルドする(make -j 4 all) **ただし、コア数がマシンによって違うので注意**
7. OpenCVのビルドを待った後、テストを走らせる
8. 結果を目視、場合によってはパフォーマンステストの結果を集計する
9. **2-8をマシンの台数分繰り返す**

- となっておりました。2.-8. を台数分繰り返すのがなかなかに凶悪でした
- しかし、ファームを作ってしまえば、

1. 開発マシンからgithub にpushする

- だけでテストが完了します。**すごい！** ~~まるで一昔前のAppleのCMみたいですよね。~~
- おかげでARMおじさんは日々のストレスから解放されました

# 補足
- 以下の環境でテストしました
- Tegra K1
 - Cortex A15 x 4Core
 - Ubuntu 14.04 32bit
- Tegra X1
 - Cortex A57 x 4Core
 - Ubuntu 16.04 64bit
- ODROID-X2
 - Cortex A9 x 4Core
 - Ubuntu 14.04 32bit
- ODROID-C2
  - Cortex A53 x 4Core
  - Ubuntu 16.04 64bit
- PINE64
  - Cortex A53 x 4Core
  - Ubuntu 16.04 64bit
- Raspberry Pi 3
  - Cortex A53 x 4Core
  - Raspbian OS 32bit
- HiKey
  - Cortex A53 x 8Core
  - Debian 8.2 64bit
- DragonBoard 410c
  - Cortex A53 x 4Core
  - Debian 8.6 64bit
- Orange Pi PC2
  - Cortex A53 x 4Core
  - Debian 8.6 64bit
- MIPS CI20 (こいつはMIPS)
  - XBurst x 2Core
  - Debian 8.6 32bit

# さいごに
いよいよ明日でOpenCV Advent Calendar 2016もおしまいです