Skip to content

Instantly share code, notes, and snippets.

@tomoaki0705
Last active Apr 6, 2021
Embed
What would you like to do?
「OpenCVの画像処理をGPU(CUDA)で高速化する」を読んでresizeをマルチコア対応するお話。

はじめに

OpenCVでの処理(リサイズなど)を、簡単にマルチコア化する方法をどなたかご存知でしたら教えて頂けないでしょうか? TBBを使ってスレッドを作る方法は色々見つかったのですが、単にcv::resize()をマルチコア動作で高速化させたいです。

TL;DR

  • 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スレッドで並列(実行)されていました。

C++での再現

  • OpenCV のテストプログラムには、パフォーマンスを簡単に調べられるperfプログラムが付属しています。(ソースからビルドする必要あり)
  • resizeimgprocモジュールなので、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 版のresizeはmulti core 非対応なのか?

  • こうなると、不思議なのは、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_REPEAT10000から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時間」がそれぞれrealuserに表示されます。
単位は全て秒(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付きで再計測しました。
  • 試行回数を変えたり画像サイズを変えたりしているので、各行ごとの比較は意味が無いのですが、realuserの値が変わるかどうかに着目してみてください。
  • 画像サイズを大きくしたPython版ではrealの数値に対してuserの数値が1スレッド時はほぼ同等、4スレッド時は約3倍の差が生じています。
  • C++版でも、realuserの数値は画像サイズを大きくしたPython版と同じ傾向が見えます。
  • これは実時間と比較して3倍以上のCPU時間を利用した、という意味でマルチコア実装が正しく行われていることを意味します。
  • resize関数は出力画像のサイズによって処理時間が変わるのですが、どうやらPython版では1回の関数callに時間がかかるため、どうしてもオーバーヘッドが生じる模様です。
  • そして、(256x256) -> (300x300)の拡大だとマルチコアで高速化される分よりオーバーヘッド分が大きく、マルチコア処理していないように見える、ということのようです。

結論

  • 問: OpenCVでの処理(リサイズなど)を、簡単にマルチコア化する方法をどなたかご存知でしたら教えて頂けないでしょうか?
  • 答: resize既にはマルチコア対応されている。
  • 補足: 実際はparallel_forクラスで並列化され、この仕組自体はたしか2.4ぐらいの昔から実装されています。(この行だけうろ覚え)

その他

  • 本来ならQiitaのコメント機能を使うのが筋なのですが、諸々思うところがあって、こちらに書いています。
  • Gistには「イイね」機能が無いので、「イイね」する代わりに、 GistにもStarがあるのを失念してました。Gistのstarか、QiitaのコメントにLGTMをクリックしていただくか、twitterで拡散して頂ければ、と思います。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment