画像の読み込みを別スレッドにしてみる(OpenCV-Python)

最近はtrioというライブラリを知りました。

[Python + Trio の非同期コーディングパターン]
(https://qiita.com/yura/items/689d065aba00fe14fbba)

をご覧ください。

----

画像処理・画像認識のプログラムを書くと、画像の処理・認識の時間に、次の画像を読み込んでおきたくなる。そこで、Pythonでのマルチスレッドのプログラムを書いてみることとした。

1. 逐次処理版を書く。
1. その言語でのマルチスレッドの枠組みを確認する。
1. どの処理とどの処理が独立であって、どうマルチスレッド化が可能か考える。
1. マルチスレッド版を実装する

# 逐次処理版を書く。
 後述のように逐次処理版を書いた。
この検出処理中に、次の画像を読み込んでおきたいと思う。
なお、動作のためには、人検出を行う入力画像を用意し、
    pat = r"*.png"
の部分を適宜書き換えてください。

逐次処理版

```py:detectLoop.py
# -*- coding: utf-8 -*-
# pylint: disable-msg=C0103
import glob
import cv2
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())

def detect(img):
    found = hog.detectMultiScale(img, winStride=(8, 8), padding=(32, 32), scale=1.05)
    return found

def draw_detections(img, rects, thickness=1):
    for x, y, w, h in rects:
        # the HOG detector returns slightly larger rectangles than the real objects.
        # so we slightly shrink the rectangles to get a nicer output.
        pad_w, pad_h = int(0.15*w), int(0.05*h)
        cv2.rectangle(img, (x+pad_w, y+pad_h), (x+w-pad_w, y+h-pad_h), (0, 255, 0), thickness)

if __name__ == '__main__':
    pat = r"*.png"
    names = glob.glob(pat)[:100]

    e1 = cv2.getTickCount()

    imgname = names[0]
    img = cv2.imread(imgname)

    for i in range(1, len(names), 1):
        found, scores = detect(img)
        imgnameNext = names[i]
        imgNext = cv2.imread(imgnameNext)
        if 0:
            print i, imgnameNext, imgNext.shape
            print found
        oldImg, imgname, img = img, imgnameNext, imgNext

        draw_detections(oldImg, found)
        cv2.imshow("detected", oldImg)
        cv2.waitKey(1)

    e2 = cv2.getTickCount()
    timeDiff = (e2 - e1)/ cv2.getTickFrequency()
    print timeDiff
```


# その言語でのマルチスレッドの枠組みを確認する
「threadingモジュールでスレッドを扱うには二つの方法がある．
threading.Threadのサブクラスを作る．
threading.Threadのインスタンスを作る．」
とのことだが、直接インスタンスを生成する方法を選んでみる。

まずは、関数から結果を返さない処理のマルチスレッドの例題を見てみる。

そうすると、次の部分が要点であることが見えてきた。

\# スレッドのインスタンスを作る。
t = threading.Thread(target=関数, args=関数の引数のタプル) 
t.start()
その他の処理
t.join()

この例だと関数の結果を戻すことができないのでスレッド間で結果を返すためにQueueを使ってスレッド間のデータの受け渡しをします。

```
def 関数名(queue, 引数):
    関数の処理
    queue.put(戻り値)

t = threading.Thread(target=関数名, args=(queue, 引数)) 
t.start()
# 処理を書く
t.join()
関数の戻り値 = queue.get()
```

# どうマルチスレッド化が可能か考える
　画像の呼び込みと人検出処理のどちらを別スレッドにするのかで2通りが考えられるが、
ここでは、人検出処理を別スレッドにするようにしてみた。
こう書くことで、次の画像の読み込みと、読み込み済みの画像の処理とを
並行して動作させることができるようだ。

# マルチスレッド版を実装する

マルチスレッド版

```py:detectLoop_thread.py
# -*- coding: utf-8 -*-
# pylint: disable-msg=C0103
import threading
import Queue
import glob
import cv2

global hog
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())

def detect2(queue, img):
    global hog
    found = hog.detectMultiScale(img, winStride=(8, 8), padding=(32, 32), scale=1.05)
    queue.put(found)

def draw_detections(img, rects, thickness=1):
    for x, y, w, h in rects:
        # the HOG detector returns slightly larger rectangles than the real objects.
        # so we slightly shrink the rectangles to get a nicer output.
        pad_w, pad_h = int(0.15*w), int(0.05*h)
        cv2.rectangle(img, (x+pad_w, y+pad_h), (x+w-pad_w, y+h-pad_h), (0, 255, 0), thickness)

if __name__ == '__main__':
    pat = r"*.png"
    names = glob.glob(pat)[:100]

    e1 = cv2.getTickCount()
    queue = Queue.Queue()
    imgname = names[0]
    img = cv2.imread(imgname)

    for i in range(1, len(names), 1):
        t = threading.Thread(target=detect2, args=(queue, img, ))
        t.start()
        imgnameNext = names[i]
        imgNext = cv2.imread(imgnameNext)
        t.join()
        found, scores = queue.get()
        if 0:
            print i, imgnameNext, imgNext.shape
            print found
        oldImg, imgname, img = img, imgNext, imgNext
        draw_detections(oldImg, found)
        cv2.imshow("detected", oldImg)
        cv2.waitKey(1)

    e2 = cv2.getTickCount()
    timeDiff = (e2 - e1)/ cv2.getTickFrequency()
    print timeDiff
```

動作結果は、100枚のpngファイルの読み込みにおいて0.8秒程度の処理時間の減少が見られる。（例題では人物検出処理自体にかかる時間を減らす必要があることがわかる。）

逐次処理版の秒数
29.5429292224
マルチスレッド版の秒数
28.7976980869


<追記>
#マルチスレッド版(別バージョン)

```py:detectLoop_thread2.py
# -*- coding: utf-8 -*-
# pylint: disable-msg=C0103
import threading
import Queue
import glob
import cv2

global hog
hog = cv2.HOGDescriptor()
hog.setSVMDetector(cv2.HOGDescriptor_getDefaultPeopleDetector())

def imread2(queue, imgnameNext):
    imgNext = cv2.imread(imgnameNext)
    queue.put(imgNext)

def draw_detections(img, rects, thickness=1):
    for x, y, w, h in rects:
        # the HOG detector returns slightly larger rectangles than the real objects.
        # so we slightly shrink the rectangles to get a nicer output.
        pad_w, pad_h = int(0.15*w), int(0.05*h)
        cv2.rectangle(img, (x+pad_w, y+pad_h), (x+w-pad_w, y+h-pad_h), (0, 255, 0), thickness)

if __name__ == '__main__':
    pat = r"*.png"
    names = glob.glob(pat)[:100]

    e1 = cv2.getTickCount()
    queue = Queue.Queue()
    imgname = names[0]
    img = cv2.imread(imgname)

    for i in range(1, len(names), 1):
        imgnameNext = names[i]
        t = threading.Thread(target=imread2, args=(queue, imgnameNext, ))
        t.start()
        found, scores = hog.detectMultiScale(img, winStride=(8, 8), padding=(32, 32), scale=1.05)
        t.join()
        imgNext = queue.get()
        
        if 0:
            print i, imgnameNext, imgNext.shape
            print found
        oldImg, imgname, img = img, imgNext, imgNext
        draw_detections(oldImg, found)
        cv2.imshow("detected", oldImg)
        cv2.waitKey(1)

    e2 = cv2.getTickCount()
    timeDiff = (e2 - e1)/ cv2.getTickFrequency()
    print timeDiff
```


追記：
　最近思ったこと。各スレッドの中で、画像の読み込みと検出とを行うという可能性があるのではないか。このやり方ができる場合、画像の読み込みと検出という計算時間がまったく異なるもので並列化するよりも、均等に作業量が振られたもので平行化ができるはず。

http://docs.python.jp/2/library/multiprocessing.html



# 参考にしたスクリプト例
16.2. threading — 高水準のスレッドインタフェース
http://docs.python.jp/2/library/threading.html

Python スレッドをやってみる（２）
http://bty.sakura.ne.jp/wp/archives/69

Pythonでマルチスレッド処理
http://qiita.com/konnyakmannan/items/2f0e3f00137db10f56a7



関連記事
画像の読み込みを別スレッドにしてみる(C++版)
http://qiita.com/nonbiri15/items/016bb38eb42a219e98e2


----
付記：
TRIO というライブラリがあって、それを使うとよいという話を聞いた。

https://trio.readthedocs.io/en/latest/#