- エヌビディアの手島でございます
- この記事はOpenCVの画像処理をGPU(CUDA)で高速化する - Qiita(以降「元記事」)を読んで、最後に書かれているリクエストを検証したものです。
OpenCVでの処理(リサイズなど)を、簡単にマルチコア化する方法をどなたかご存知でしたら教えて頂けないでしょうか? TBBを使ってスレッドを作る方法は色々見つかったのですが、単にcv::resize()をマルチコア動作で高速化させたいです。
- Python バインディングが遅い
- OpenCV の
resize
はずっと昔からマルチコア対応している
- 元記事では、CUDAで高速化することは可能だったが、どうにもCPU版が1コアでしか動作していないように見える、という話でした。
- 手元にJetson Nanoがあり、試してみました。
- CUDA 10.0
- GCC 7.5.0
- Kernel 4.9.140-tegra
- Python 2.7.17
- OpenCV 4.4.0-dev (a10d289997)
- 検証用コードがありましたので、そちらを流用してCPU版の
resize
を実行してみました。- ファイルのパスや、
imshow
を取り除いたりしています。
- ファイルのパスや、
import sys
import time
import cv2
### VALUES
NUM_REPEAT = 10000
### Read source image
img_src = cv2.imread("lena.jpg")
### Run with CPU
time_start = time.time()
for i in range (NUM_REPEAT):
img_dst = cv2.resize(img_src, (300, 300))
time_end = time.time()
print ("CPU = {0}".format((time_end - time_start) * 1000 / NUM_REPEAT) + "[msec]")
- 実行結果 4 スレッド
$ time python opencv_resize.py
CPU = 2.94346778393[msec]
real 0m29.859s
user 0m29.320s
sys 0m0.080s
- 実行結果 1 スレッド
$ time OPENCV_FOR_THREADS_NUM=1 python opencv_resize.py
CPU = 2.93191969395[msec]
real 0m29.764s
user 0m29.260s
sys 0m0.068s
- 確かにこの結果だけ見ると、1回の
resize
の所要時間は、4スレッドでも1スレッドでも大差ありません。 - 念の為、
resize
関数内にgetNumThreads()
関数を忍び込ませてみましたが、ちゃんと環境変数OPENCV_FOR_THREADS_NUM
が反映されていて、通常は4スレッド、OPENCV_FOR_THREADS_NUM=1
指定時は1スレッドで並列(実行)されていました。
- OpenCV のテストプログラムには、パフォーマンスを簡単に調べられる
perf
プログラムが付属しています。(ソースからビルドする必要あり) resize
はimgproc
モジュールなので、opencv_perf_imgproc
を実行してみればC++版での速度がわかります。- 実行結果 4 スレッド
$ ./opencv_perf_imgproc --perf_min_samples=100 --gtest_filter=MatInfo_SizePair_resizeUpLinearNonExact.resizeUpLinearNonExact/5
Time compensation is 0
TEST: Skip tests with tags: 'mem_6gb', 'verylong'
CTEST_FULL_OUTPUT
OpenCV version: 4.4.0-dev
OpenCV VCS version: 4.4.0-216-ga10d289997
Build type: Release
Compiler: /usr/bin/c++ (ver 7.5.0)
Parallel framework: pthreads (nthreads=4)
CPU features: NEON FP16
OpenCL is disabled
Note: Google Test filter = MatInfo_SizePair_resizeUpLinearNonExact.resizeUpLinearNonExact/5
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MatInfo_SizePair_resizeUpLinearNonExact
[ RUN ] MatInfo_SizePair_resizeUpLinearNonExact.resizeUpLinearNonExact/5, where GetParam() = (8UC3, (640x480, 1280x720))
[ PERFSTAT ] (samples=100 mean=5.38 median=5.38 min=5.33 stddev=0.04 (0.8%))
[ OK ] MatInfo_SizePair_resizeUpLinearNonExact.resizeUpLinearNonExact/5 (5409 ms)
[----------] 1 test from MatInfo_SizePair_resizeUpLinearNonExact (5409 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (5410 ms total)
[ PASSED ] 1 test.
real 0m5.935s
user 0m19.468s
sys 0m1.968s
- 実行結果 1 スレッド
$ OPENCV_FOR_THREADS_NUM=1 ./opencv_perf_imgproc --perf_min_samples=100 --gtest_filter=MatInfo_SizePair_resizeUpLinearNonExact.resizeUpLinearNonExact/5
Time compensation is 0
TEST: Skip tests with tags: 'mem_6gb', 'verylong'
CTEST_FULL_OUTPUT
OpenCV version: 4.4.0-dev
OpenCV VCS version: 4.4.0-216-ga10d289997
Build type: Release
Compiler: /usr/bin/c++ (ver 7.5.0)
Parallel framework: pthreads (nthreads=1)
CPU features: NEON FP16
OpenCL is disabled
Note: Google Test filter = MatInfo_SizePair_resizeUpLinearNonExact.resizeUpLinearNonExact/5
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from MatInfo_SizePair_resizeUpLinearNonExact
[ RUN ] MatInfo_SizePair_resizeUpLinearNonExact.resizeUpLinearNonExact/5, where GetParam() = (8UC3, (640x480, 1280x720))
.
[ PERFSTAT ] (samples=100 mean=17.74 median=17.74 min=17.67 stddev=0.03 (0.2%))
[ OK ] MatInfo_SizePair_resizeUpLinearNonExact.resizeUpLinearNonExact/5 (17751 ms)
[----------] 1 test from MatInfo_SizePair_resizeUpLinearNonExact (17751 ms total)
[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (17751 ms total)
[ PASSED ] 1 test.
real 0m18.357s
user 0m18.152s
sys 0m0.088s
- 解説
- オプションの
--gtest_filter=MatInfo_SizePair_resizeUpLinearNonExact.resizeUpLinearNonExact/5
は、数あるパフォーマンスチェック条件のうち、resize
関数のINTER_LINEAR
を使うやつをピックアップしてあります。 - また、ビルド時には
WITH_CAROTENE=OFF
してありますので、純粋にOpenCVのネイティブな(CUDAでもOpenCLでもCAROTENEでもIPPでも無い)実装が採用されています。 - 結果は、
median
のところを見ると一番効果がわかります。- 4コア時には 5.38 ms
- 1コア時には 17.74 ms
- この結果だけ見ると、4コアで実行することで、約3.31倍高速化されています。
- 少なくともC++版はマルチコアの恩恵を受けていることがわかります。
- オプションの
- こうなると、不思議なのは、Python版はシングルコア対応、C++版はマルチコア対応、という風に読めてしまいます。
- しかし、Python版というのは、結果裏でC++版を呼び出しているに過ぎず、あくまでPython バインディング(ラッパー)です
- また、内部的に4スレッドで並列しているのも確認できます。
- 実はポイントは検証コードのここでした。
img_dst = cv2.resize(img_src, (300, 300)) <=====
- このコードはもともとのLenaの画像(256x256)を(300x300)に拡大するコードです。
resize
関数は、出力サイズで計算量が決まりますので、試しに、この画像を4K(3840x2160)に拡大してみます。
img_dst = cv2.resize(img_src, (3840, 2160)) <=====
- 再度Python版を実行してみます。(なお、画像サイズ変更に伴い、処理時間があまりにも長くなったので、
NUM_REPEAT
を10000
から100
に変更しました) - 実行結果 4 スレッド
$ time python opencv_resize.py
CPU = 26.5402293205[msec]
real 0m2.928s
user 0m10.444s
sys 0m0.328s
- 実行結果 1 スレッド
$ time OPENCV_FOR_THREADS_NUM=1 python opencv_resize.py
CPU = 94.3131709099[msec]
real 0m9.706s
user 0m9.548s
sys 0m0.136s
- 今度は、実行結果が26.54 ms VS 94.31 ms と約3.554倍高速化されました
- コマンドの冒頭につけている
time
コマンドはbash
内蔵のコマンドで、「実際の経過時間」及び「利用されたCPU時間」がそれぞれreal
とuser
に表示されます。
単位は全て秒(s) | real | user |
---|---|---|
Python版(元コード,1スレッド) | 29.764 | 29.260 |
Python版(元コード,4スレッド) | 29.859 | 29.320 |
C++版(1スレッド) | 18.357 | 18.152 |
C++版(4スレッド) | 5.935 | 19.468 |
Python版(サイズ4K,1スレッド) | 9.706 | 9.548 |
Python版(サイズ4K,4スレッド) | 2.928 | 10.444 |
- C++版
はをtime
コマンドをつけ忘れたので、再度計測してのちほど更新します。time
付きで再計測しました。 - 試行回数を変えたり画像サイズを変えたりしているので、各行ごとの比較は意味が無いのですが、
real
とuser
の値が変わるかどうかに着目してみてください。 - 画像サイズを大きくしたPython版では
real
の数値に対してuser
の数値が1スレッド時はほぼ同等、4スレッド時は約3倍の差が生じています。 - C++版でも、
real
とuser
の数値は画像サイズを大きくしたPython版と同じ傾向が見えます。 - これは実時間と比較して3倍以上のCPU時間を利用した、という意味でマルチコア実装が正しく行われていることを意味します。
resize
関数は出力画像のサイズによって処理時間が変わるのですが、どうやらPython版では1回の関数callに時間がかかるため、どうしてもオーバーヘッドが生じる模様です。- そして、(256x256) -> (300x300)の拡大だとマルチコアで高速化される分よりオーバーヘッド分が大きく、マルチコア処理していないように見える、ということのようです。
- 問: OpenCVでの処理(リサイズなど)を、簡単にマルチコア化する方法をどなたかご存知でしたら教えて頂けないでしょうか?
- 答:
resize
既にはマルチコア対応されている。 - 補足: 実際は
parallel_for
クラスで並列化され、この仕組自体はたしか2.4ぐらいの昔から実装されています。(この行だけうろ覚え)
- 本来ならQiitaのコメント機能を使うのが筋なのですが、諸々思うところがあって、こちらに書いています。
Gistには「イイね」機能が無いので、「イイね」する代わりに、GistにもStarがあるのを失念してました。Gistのstarか、QiitaのコメントにLGTMをクリックしていただくか、twitterで拡散して頂ければ、と思います。