2017年10月2日月曜日

std::threadでバッファリングして連番画像再生

メインスレッドの裏で連番画像をロード(バッファリング)しておきながら OpenCVのウィンドウ上に連番画像を動画として再生するサンプルコード. C++11のstd::threadを使ってなるべく単純なコードになるように必要最小限に.C++11とスレッドは難しい...

サンプルコード:
https://github.com/r168xr169/AsyncLoader

プログラムの重用なところの解説

まずはmain関数.バッファはvector配列bufでn_buffer個のcv::Matを格納するようにしている. フレーム番号i_currentを用いて配列の添字をi_current % n_bufferとしているので範囲外にならずに最初に戻るようになっている.

int main(void)
{
    //load関数をスレッドとして起動
    thread th = thread(load);

//(中略)

    //動画表示のループ(waitKeyを呼ばないとimshowがうまく動作しない)
    while(waitKey(1) != 27)
    {

//(中略)

        //開始からの時刻をフレーム番号に
        i_current = ms / (1000.0 / fps);

        //最後のフレームまでいくと終了
        if (n_frames <= i_current) break;

//(中略)

        //バッファ中の画像フレームを再生
        imshow("window", buf[i_current % n_buffer]);
    }

    return 0;
}

次に,スレッドとして呼び出されるload関数.

void load(void)
{
    while (is_alive)
    {
        //最後にロードしたもの(i_last)が
        //再生中のフレーム(i_current)からバッファサイズ(n_bufffer)分先にあれば
        //これ以上バッファが足らないのでロードを待機する
        if (i_current + (n_buffer - 1) <= i_last)
        {
            is_ready = true;
        }
        else {

//(中略)
            //最後のロードしたものの次のフレームのファイル名を作成
            int i_last_pp = ++i_last;
            std::ostringstream oss;
            oss << "jpg/img" << setfill('0') << setw(3) << i_last_pp << ".jpg";

//(中略)
            //画像をロード
            buf[i_last_pp % n_buffer] = imread(oos.str());
        }

        //適当にsleepを入れてこのスレッドだけでCPU負荷が一杯になるのを防ぐ
        this_thread::sleep_for(milliseconds(1));
    }
}

細かい部分の解説

準備部分

DLLのロードやウィンドウの生成の負荷が高いので,初期フレーム付近で描画遅れたり, フレームレートが低下したりする問題があるので, 完全に準備できてから再生開始するためにこのようにしている. is_readyが準備できた合図になる.

while (!is_ready) { 
    this_thread::sleep_for(milliseconds(1)); 
}

フレームレートの制御

制御というより開始時刻から現在のフレーム番号を計算しているだけ. C++11のstd::chronoを使ってみた.使い方が正しいか自信がない.

auto diff = system_clock::now() - start;
long long ms = duration_cast<milliseconds>(diff).count();
i_current = ms / (1000.0 / fps);

デバッグ用のcoutにはmutexを使う

このlock()とunlock()無しでcoutすると,メインスレッドとload関数のスレッドでcout出力された文字列が 混ざり合ってしまう.mutexを使ってロックすると1つのスレッドがcoutし終わるまで, 他のスレッドの動作は停止するので出力が混ざることは

mtx.lock();
cout << this_thread::get_id() << "; i_current = " << i_current << endl;
mtx.unlock();

再生が速過ぎる場合の対処

ここは再生が速過ぎる場合の対処だけども,むしろ起動時にロードが遅れて再生に抜かれることが多いのでこのようにしている. 初期状態ではi_last=-2,i_current=-1としている.

if (i_last < i_current) {
    i_last = i_current;
    is_ready = false;
}

ファイル名のインクリメント方法

sstreamを使うとこういう風にできるけど直観的でないしこんなの誰も覚えてない.

std::ostringstream oss;
oss << "jpg/img" << setfill('0') << setw(3) << i_last_pp << ".jpg";
C言語のstdio.hならすごくシンプルだけど256の部分は文字数をよく考えないといけない.
char fname[256];
sprintf(fname, "jpg/img%03d.jpg", i_last_pp);
boost::formatならこんな風にシンプルで美しくできる.スピードは遅いらしい.
string sequence = "jpg/img%03d.jpg";
string fname = (boost::format(sequence) % i_last_pp ).str();

参考資料

変更部分が全て排他制御されている場合でもatomicは必要か?(質問者も回答者もレベルが高い)
https://teratail.com/questions/54740