前回、AWS SDK for C++を使ってS3にある画像をOpenCVで操作してみました。今度はHTTPのPOSTメソッドで送信された画像をOpenCVで操作するApacheモジュールを作ってみたくなったので試してみます。
これを作ります
アップロードされた画像の顔を検出して四角で囲んで表示します。
ApacheのモジュールをC++で書きたい
C++でApacheモジュールを書きたいんです。OpenCVの3.0でC言語関数形式のインターフェイスはメンテナンスが終了しているためC++を使うことが推奨されてますし。openCV will drop C API support soon
「ApacheのモジュールってCで書かれてるんじゃない?」って私も思っていましたが、どうやらC++で書けるみたいなんですね。こちらが非常に参考になりました。
試しに一丁モジュールを作って動かし、C++を使ってApacheのモジュールが書ける喜びを噛み締めましょう。
POSTされたデータをパースして画像をcv::Mat
にいれる
POSTメソッドで送信された画像をOpenCVで操作するApacheモジュールを作るためには、画像をcv::Mat
にいれてやることになります。
ここの部分が結構手こずったので、詳しく書いてみたいと思います。
libapreq2
を使う
調べたところlibapreq2
を使うのがPOSTされたデータを楽にパースする近道っぽかったです。なので、入れましょう。
yum install libapreq2-devel
こちらのmypost
モジュールを参考にC++で再実装を試してみたところ、見事にPOSTデータのパースに成功しました。
公式ドキュメントを読んでもみましたが、libapreq2
を使わないとなるとかなり骨が折れそうなので、私は素直に使うことにしました。
POSTされたデータをパースしたい
POSTされたテキストのパースまでは順調に進めたので、
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>imagesender</title>
</head>
<body>
<form action="/imagereceiver" method="post" enctype="multipart/form-data">
<input type="file" name="image" size="30" /><br />
<input type="submit" value="upload" />
</form>
</body>
</html>
こんなフォームを作って今度は画像をPOSTして動かしてみよう、と意気込んてみたはいいものの、ここにかなり手こずってしまいました。詳しく書いてみます。
まず考えたのは「POSTされた画像ってどこにあるんだろう?」ということです。これについてはリファレンスを読むことで辿りつけました。
Apache 2.X Filter Moduleを読み、apreq_handle_apache2()
を使うことでrequest_rec
とcommunicateができるapreqのハンドルであるapreq_handle_t
が作れることが分かりました。
そして、include/apreq_module.h File Referenceに書いてありますがapreq_body_get()
を使えばapreq_param_t
構造体のbody paramをapreq_handle_t
からフェッチできます。apreq_param_t
はメンバとしてupload
を持っています(apreq_param_t Struct Reference)。POSTされた画像はこの中に入っているみたいです。
どうやらapr_bucket_brigade
構造体であるupload
の中身をcv::Mat
に入れられれば良さそうだな、というところまではわかりました。
では、どうやって入れるのかという話ですが、私はこの辺からちんぷんかんぷんでした。いろいろ調べてみました。
apr_bucket_brigade
やapr_bucket
について
Apacheモジュールではデータの入出力は双方向循環リストを通して行います。
apr_bucket_brigade
は内部にapr_bucket
を持っており、apr_bucket
は前後のapr_bucket
へのポインタを持っているリング構造です。
apr_bucket_brigade
のnextは先頭のapr_bucket
を指しており、そのapr_bucket
のnextは次のapr_bucket
を指します。どんどん辿っていくと、先頭のapr_bucket
を指しているapr_bucket_brigade
に行きつきます。
逆も一緒で、apr_bucket_brigade
のprevは末尾のapr_bucket
を指しており、どんどん辿っていくと末尾のapr_bucket
を指しているapr_bucket_brigade
に行きつきます。apr_bucket_brigade
が番兵として機能します。
upload
をcv::Mat
に入れる
なので、apr_bucket_brigade
構造体であるupload
の中身をcv::Mat
に入れるコードはこんな感じになりました。
cv::Mat bb2Mat(request_rec *r, apr_bucket_brigade *upload) {
apr_bucket_brigade *bb = apr_brigade_create(r->pool, r->connection->bucket_alloc);
apreq_brigade_copy(bb, upload);
std::vector<char> vec;
for (apr_bucket *e = APR_BRIGADE_FIRST(bb); e != APR_BRIGADE_SENTINEL(bb); e = APR_BUCKET_NEXT(e)) {
const char *data;
apr_size_t len;
if (apr_bucket_read(e, &data, &len, APR_BLOCK_READ) != APR_SUCCESS) {
throw std::runtime_error("failed to read bucket");
}
const char *dup_data = apr_pstrmemdup(r->pool, data, len);
vec.insert(vec.end(), dup_data, dup_data + len);
apr_bucket_delete(e);
}
cv::Mat ret = cv::imdecode(cv::Mat(vec), CV_LOAD_IMAGE_COLOR);
if (ret.data == NULL) {
throw std::runtime_error("buffer is too short or contains invalid data");
}
return ret;
}
cv::Mat
に入れられたらあとはこっちのもんです。好きなようにしましょう。
cv::Mat
をapr_bucket_brigade
に入れる
この画像を表示してやるためには、今度はcv::Mat
をapr_bucket_brigade
に入れる必要があります。
やり方としては、
1.cv::Mat
をエンコードしてstd::string
にする
2.std::string
をapr_bucket_brigade
に入れる
こんな感じになります。image
がcv::Mat
の変数です。
// 1.cv::Matをエンコードしてstd::stringにする
std::vector<int> params{CV_IMWRITE_JPEG_QUALITY, 100};
std::vector<unsigned char> buf;
cv::imencode(".jpg", image, buf, params);
std::string data(buf.begin(), buf.end());
// 2.std::stringをapr_bucket_brigadeに入れる
apr_bucket *bucket = apr_bucket_pool_create(data.c_str(), data.length(), r->pool, r->connection->bucket_alloc);
apr_bucket_brigade *bucket_brigate = apr_brigade_create(r->pool, r->connection->bucket_alloc);
APR_BRIGADE_INSERT_TAIL(bucket_brigate, bucket);
余談
途中、見慣れない単語が出てきたので意味を調べたりしたところ、
- brigade:【陸海軍, 軍事】 旅団
- sentinel:歩哨, 哨兵
と、出てきました。ここにきてApacheという名前の戦闘ヘリがあることを思い出し、「なんて軍事的なんだ・・・」と思ったりもしましたが、"bucket brigade"で一つの言葉で意味はバケツリレー、そしてsentinelとは番兵のことでした。
Dockerで動かしてみる
目的のApacheモジュールを書いてみました。
さくっと動かせるように、Docker Hubで公開してみました。こちらです(ちなみにmod_imagereceiverって名前はいまいちだったなーと思います。でもいいや、と思っています)。
以下のコマンドで動かせます。
sudo docker run -dp 8080:80 d9magai/mod_imagereceiver
http://localhost:8080/form.html を開けばファイルアップロードのフォームが表示されるので、顔が映っている画像をアップロードしてみましょう。顔を検出して四角で囲んで表示されると思います。
おわりに
HTTPのPOSTメソッドで送信された画像をOpenCVで操作するApacheモジュールを書けました。またApacheのデータ構造もおぼろげながら理解できたので面白かったです。
そして前回と組み合わせることでPOSTされた画像とS3の画像をOpenCVでなんやかんやするApacheモジュールも書けそうです。