3

ruby-opencvをインストールして顔認識した

ruby-opencvは5年ぐらい前に停止したopencvというgemを、ser1zwさんという方がOpenCV2.x系に対応させ、Ruby2.0に対応させ、と着実にアップデートしているプロジェクト。

先日これをみて、そういえば良いプロジェクトなのに全く試してなかったと思い出してまずインストールした。
ruby-opencvの進捗の話(2014年2月版) – ser1zw's blog


試しに作ったものはここに置いてある
https://github.com/shokai/ruby-opencv-study

感想

まだ大した事試してないけど、感想としては、ドキュメントが無い。でもOpenCV本体のドキュメント見てそのままRubyの常識にそって書き直したらふつうに動くのでドキュメントなくてもいいかなと思った。

Rubyだとそのオブジェクトが持っているメソッド一覧はObject#methodsなどで見れるし、ruby-opencvのリポジトリをcloneしてきて git grep キーワード ext/ すればC++と対応してるRubyのメソッドはすぐ見つかる。

あとはそれぞれのC++ラッパーのオブジェクトにinspectメソッドがあると、見やすくて良くなるかと思った。例えばCvRectは4上下左右4座標が入っているんだけど、putsやpで4座標が標準出力できると便利だと思う。


Macにインストール

Mac (OSX 10.9.1)の場合、まずhomebrewでopencvを入れてからgem installしたら簡単にインストールできた。

% brew search opencv
% brew tap homebrew/science

% brew info opencv
opencv: stable 2.4.7.1

% brew install opencv
特にインストールオプション付けずにopencv 2.4.7.1が入った。

% gem install ruby-opencv
0.0.12が入った。


カメラでキャプチャして顔認識

最近追加されたEigenfacesやFisherfacesもあるけどとりあえず古典的なhaarlike cascadeでやってみる。

分類器は
/usr/local/share/OpenCV/haarcascades/haarcascade_frontalface_default.xml
をコピーしてきて使った。

ほぼサンプルまま。カメラでキャプチャして顔を認識する
capture_face_detect.rb
require 'opencv'

window = OpenCV::GUI::Window.new "face detect"
capture = OpenCV::CvCapture.open
detector = OpenCV::CvHaarClassifierCascade::load "./haarcascade_frontalface_default.xml"

loop do
image = capture.query
image = image.resize OpenCV::CvSize.new 640, 360
detector.detect_objects(image).each do |rect|
puts "detect!! : #{rect.top_left}, #{rect.top_right}, #{rect.bottom_left}, #{rect.bottom_right}"
image.rectangle! rect.top_left, rect.bottom_right, :color => OpenCV::CvColor::Red
end
window.show image
break if OpenCV::GUI::wait_key(100)
end

ウィンドウが開いて顔の部分に赤枠がでる


顔認識して画像ファイルを保存


Rubyなのでサーバーで使うような事を考えると、windowで画像を表示するのではなく顔認識した結果をファイルに書き出したほうがそれっぽいかなあとか考えてた

face_detect.rb
require 'opencv'

if ARGV.size < 2
STDERR.puts " % ruby #{$0} input.jpg output.jpg"
exit 1
end

input_filename = ARGV.shift
output_filename = ARGV.shift

image = OpenCV::IplImage::load input_filename
haar_xml_file = File.expand_path File.dirname(__FILE__), 'haarcascade_frontalface_default.xml'
detector = OpenCV::CvHaarClassifierCascade::load haar_xml_file

detector.detect_objects(image).each do |rect|
puts "detect!! : #{rect.top_left}, #{rect.top_right}, #{rect.bottom_left}, #{rect.bottom_right}"
image.rectangle! rect.top_left, rect.bottom_right, :color => OpenCV::CvColor::Red
end

image.save output_filename

% ruby face_detect.rb input.jpg output.jpg

結果
detect!! : <OpenCV::CvPoint:(125,9)>, <OpenCV::CvPoint:(472,9)>, <OpenCV::CvPoint:(125,356)>, <OpenCV::CvPoint:(472,356)>

シンプルに書けてよい。

2

scansnapで自炊した本をkindleで読めるように補正する(2)

橋本商会 scansnapで自炊した本をkindleで読めるように補正するをOpenCV使ってC++で書き直したら60倍速くなった。635ページのオライリーの本が約1分半で処理できたし、画質も前のJRuby版より少し読みやすくなった気がする。


こういう事をする。

■やっている事
scansnapで自炊した本をそのままkindleで表示するには色々問題がある。

  • kindleの解像度は800×600だけど、画面内にページ位置等のUIが入るし画面を縦横回転させたり拡大縮小できるので、表示時にkindle上でリサイズされる。この時、線が細くて薄い部分が消滅してしまって、文字がかすれて読めなくなってしまう。
  • scansnapで取り込んだままのPDFファイルでは、コントラストが弱くてとても読みづらい。
  • ページの上下左右の余白が邪魔で、kindleに全画面表示した時に小さくなってしまう

kindle上でリサイズされる事を前提として、文字が消えてしまうような細い部分を太くしてやるしかない。
しょうがないので、コントラストを上げたり、拡大縮小されるのを前提として先にアンチエイリアス?的な処理をしたり、余白を切り取ったり、リサイズしたりするようにした。
上下左右を裁ち落とし→リサイズ→グレースケール化→2値化→黒の周りを#999999で太らせるという処理をしている。


上下左右裁ち落とし→グレースケール化→2値化→黒の周りを#000000で太らせる→リサイズ
という処理に変えた。JRuby+javax.imageioでピクセル単位に処理すると、解像度が大きい画像を処理するのが苦しかったので先にリサイズするようにしていた。
OpenCVは十分速いので、高解像度のまま処理して最後に縮小するように変更した。これでサイズが大きい本でも綺麗に変換できる。


■使う
OpenCV1.00以上と、boostのboost::system, boost::filesystem, boost::program_optionsのインストールが必要。少なくともMacとUbuntuでは動く。
その辺はgithub/shokai/scansnap_adjust_images_kindleに書いた。


gitリポジトリを持ってきてコンパイルする

git clone git://github.com/shokai/scansnap_adjust_images_kindle.git
cd scansnap_adjust_images_kindle/kindlize_images

# for Mac
make -f Makefile.macosx

./configureの作り方がよくわからないのでとりあえずMac用のMakefileだけ置いてある。
Linuxとかでも、opencv、boost::filesystem、boost::progoram_optionsをインストールしてあればコンパイルはできる。(この組み合わせは色々なOSでよく使ってる)


オライリーの判型の本をkindle用に補正する。上下左右の余白を削除して白黒になって文字が太くなる。イラストは見づらくなるかもしれない。
mkdir ~/tmp/mybook_kindle
./kindlize_images --help
./kindlize_images -i ~/tmp/mybook/ -o ~/tmp/mybook_kindle/ -t 190 -w 1200 -h 1600 --cleft 120 --cright 120 --ctop 150 --cbottom 150


はじめにPDFから連番のJPEG画像に書き出して、それからこのツールを使って変換して、最後に連番画像をPDFにまとめるという使い方を想定している。
PDFから連番画像を書き出す方法は、前の記事やgithubのREADME.mdに書いた。
連番画像を1つのPDFへ結合するのは、gitリポジトリの中に一緒にimages2pdf.appというautomatorアプリを入れて置いたのでそれを使うと良い。


■ソースコード
100行切った。boost::filesystemのおかげでディレクトリ内一括処理が書きやすかった。
JRuby使って書くよりもコードが35行短くなったけど、最初に試行錯誤するにはJRuby+javax.imageioでやる方が手軽だったな。

kindlize_images/kindlize_images.cpp at master from shokai's scansnap_adjust_images_kindle – GitHub
#include "cv.h"
#include "highgui.h"
#include <boost/program_options.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/fstream.hpp>
#include <iostream>
using namespace boost;
using namespace std;
namespace fs = boost::filesystem;

IplImage *adjust_image(IplImage *img, program_options::variables_map argmap){
int cleft = argmap["cleft"].as<int>();
int cright = argmap["cright"].as<int>();
int ctop = argmap["ctop"].as<int>();
int cbottom = argmap["cbottom"].as<int>();

const int w = img->width - cleft - cright;
const int h = img->height - ctop - cbottom;

cvSetImageROI(img, cvRect(cleft, ctop, w, h));

IplImage *img_gray = cvCreateImage(cvSize(w,h), IPL_DEPTH_8U, 1);
cvCvtColor(img, img_gray, CV_BGR2GRAY);

// 2値化
IplImage *img_bin = cvCreateImage(cvSize(w,h), IPL_DEPTH_8U, 1);
cvThreshold(img_gray, img_bin, argmap["threshold"].as<int>(), 255, CV_THRESH_BINARY);

// 文字を太らせる
cvErode(img_bin, img_bin, NULL, 1);

// リサイズ
int width = argmap["width"].as<int>();
int height = argmap["height"].as<int>();
IplImage *img_resized;
double scale = ((double)h)/w;
if(((double)height)/width < scale){ // 縦長
img_resized = cvCreateImage(cvSize((int)(height/scale), height), IPL_DEPTH_8U, 1);
}
else{ // 横長
img_resized = cvCreateImage(cvSize(width, (int)(scale*width)), IPL_DEPTH_8U, 1);
}
cvResize(img_bin, img_resized, CV_INTER_LINEAR);

cvReleaseImage(&img_bin);
cvReleaseImage(&img_gray);
return img_resized;
}

int main(int argc, char* argv[]) {
program_options::options_description opts("options");
opts.add_options()
("help", "ヘルプを表示")
("width,w", program_options::value<int>(), "output width")
("height,h", program_options::value<int>(), "output height")
("threshold,t", program_options::value<int>(), "binarize threshold")
("input,i", program_options::value<string>(), "input directory name")
("output,o", program_options::value<string>(), "output directory name")
("cleft", program_options::value<int>(), "crop left (pixel)")
("cright", program_options::value<int>(), "crop right (pixel)")
("ctop", program_options::value<int>(), "crop top (pixel)")
("cbottom", program_options::value<int>(), "crop bottom (pixel)");
program_options::variables_map argmap;
program_options::store(parse_command_line(argc, argv, opts), argmap);
program_options::notify(argmap);
if (argmap.count("help") || !argmap.count("input") || !argmap.count("output") ||
!argmap.count("threshold")) {
cerr << "[input, output] required" << endl;
cerr << opts << endl;
return 1;
}

string in_dir = argmap["input"].as<string>();
fs::path path = complete(fs::path(in_dir, fs::native));
fs::directory_iterator end;
for (fs::directory_iterator i(path); i!=end; i++){
string img_fullname = in_dir + i->leaf();
cout << img_fullname << endl;
IplImage *img, *img_result;
img = cvLoadImage(img_fullname.c_str());
if(!img){
cerr << "image file load error" << endl;
}
else{
img_result = adjust_image(img, argmap);
string out_filename = argmap["output"].as<string>() + "/" + i->leaf();
cvSaveImage(out_filename.c_str(), img_result);
cvReleaseImage(&img);
cvReleaseImage(&img_result);
}
}
}

0

ZeroMQでOpenCV cvOpticalFlowのデータを配信する

1VQ9がZeroMQで遊んでたので、俺も橋本商会 cvCalcOpticalFlowBMをZeroMQでpubしてみた。ZeroMQはなんか面倒な事を適当にやってくれるmessaging libraryで、色々な言語のバインディングが出ている。

ZeroMQのpubはセンサーのデータとかを垂れ流しにするのに都合がよさそう。
clientが何台いるかどうかを考えないで良いし、pub/subどちらが先に起動していても適当に接続処理をしてくれる。cookbookを見てるとmulticastやthread間通信にも使ってる。とりあえずセンサーデータ垂れ流しという用途に俺はよく使いそう。


ソースコードはgithubに置いた
他にも単純なカウントアップのpub/sub両方をC++/C/Rubyで書いた(6種)のと、twitterのstream APIをZMQ_PUBで中継するのを作ってみた(解説:zeromqインストール、twitter stream APIを中継 – 橋本詳解)。特にstream APIのHUB的存在は便利。

あと、mongrel2WebSocketやXMLSocketとZeroMQの接続をしてくれるようになるらしくて期待してる。



受信側
opticalflow_sub.rb

#!/usr/bin/env ruby
require 'rubygems'
require 'zmq'

ctx = ZMQ::Context.new
sock= ctx.socket(ZMQ::SUB)
sock.connect('tcp://127.0.0.1:5000')
sock.setsockopt(ZMQ::SUBSCRIBE, 'opticalflow')

loop do
puts sock.recv()
end


送信側。これを適当なパソンコにUSBカメラ刺して動かしておけば、別のマシンから動きが取れる!!
opticalflow_pub.cpp
// http://opencv.jp/sample/optical_flow.html
#include <cv.h>
#include <highgui.h>
#include <cxcore.h>
#include <ctype.h>
#include <stdio.h>
#include <iostream>
#include <boost/format.hpp>
#include <zmq.hpp>

using namespace std;
using namespace boost;

void detect_flow(IplImage *img, IplImage *img_p, IplImage *dst);
zmq::context_t ctx(1);
zmq::socket_t sock(ctx, ZMQ_PUB);

int main(int argc, char* argv[]) {
IplImage *img = NULL;
CvCapture *capture = NULL;
capture = cvCreateCameraCapture(0);
//capture = cvCaptureFromAVI("test.mov");
if(capture == NULL){
cerr << "capture device not found!!" << endl;
return -1;
}

sock.bind("tcp://127.0.0.1:5000");

CvSize size = cvSize(320, 240);
IplImage *img_resized = cvCreateImage(size, IPL_DEPTH_8U, 3);
IplImage *img_gray = cvCreateImage(size, IPL_DEPTH_8U, 1);
IplImage *img_gray_p = cvCreateImage(size, IPL_DEPTH_8U, 1);
IplImage *img_dst = cvCreateImage(size, IPL_DEPTH_8U, 3);

char winNameCapture[] = "Capture";
cvNamedWindow(winNameCapture, CV_WINDOW_AUTOSIZE);

while (1) {
img = cvQueryFrame(capture);
cvResize(img, img_resized);
cvCvtColor(img_resized, img_gray, CV_BGR2GRAY);
cvCopy(img_resized, img_dst);
detect_flow(img_gray, img_gray_p, img_dst);
cvShowImage(winNameCapture, img_dst);
cvCopy(img_gray, img_gray_p);
if (cvWaitKey(10) == 'q') break;
}

cvReleaseCapture(&capture);
cvDestroyWindow(winNameCapture);

return 0;
}

void detect_flow(IplImage *src_img1, IplImage *src_img2, IplImage *dst_img){
int i, j, dx, dy, rows, cols;
int block_size = 24;
int shift_size = 10;
CvMat *velx, *vely;
CvSize block = cvSize(block_size, block_size);
CvSize shift = cvSize(shift_size, shift_size);
CvSize max_range = cvSize(50, 50);

rows = int(ceil (double (src_img1->height) / block_size));
cols = int(ceil (double (src_img1->width) / block_size));
velx = cvCreateMat(rows, cols, CV_32FC1);
vely = cvCreateMat(rows, cols, CV_32FC1);
cvSetZero(velx);
cvSetZero(vely);

cvCalcOpticalFlowBM(src_img1, src_img2, block, shift, max_range, 0, velx, vely);
string result_str = string("");
for (i = 0; i < velx->width; i++) {
for (j = 0; j < vely->height; j++) {
dx = (int)cvGetReal2D(velx, j, i);
dy = (int)cvGetReal2D(vely, j, i);
cvLine(dst_img, cvPoint(i * block_size, j * block_size),
cvPoint(i * block_size + dx, j * block_size + dy), CV_RGB(255, 0, 0), 1, CV_AA, 0);
if(dx != 0 || dy != 0){
result_str += str(format("[%d,%d,%d,%d]") % (i*block_size) % (j*block_size) % dx % dy);
}
}
}
if(result_str.size() > 0){
result_str = str(format("opticalflow %s") % result_str);
cout << result_str << endl;
zmq::message_t msg(result_str.size()+1); // ZeroMQ
memcpy(msg.data(), result_str.c_str(), result_str.size()+1);
sock.send(msg);
}
}


g++ -O opticalflow_pub.cpp -o opticalflow_pub.bin -I/opt/local/include/opencv -lcv -lcvaux -lcxcore -lhighgui  -I/usr/local/include /usr/local/lib/libzmq.a


これで動いた座標とその方向 [x,y,dx,dy] が連続で送られてくる。
opticalflow [48,216,4,-29][72,216,0,-29][96,216,0,-29][264,216,-9,-29]
opticalflow [48,216,4,-29][96,216,0,-29][120,216,0,-29][264,216,-9,-29]
opticalflow [48,216,4,-29][96,168,0,10][96,192,-10,-20][96,216,0,-29][120,192,0,10][120,216,0,-29][144,216,0,-29][168,216,0,-29][192,48,-10,0][192,216,0,-29][216,216,0,-29][264,216,-9,-29]
opticalflow [96,168,0,10][96,192,-10,-10][96,216,0,-29][120,168,0,10][120,192,0,10][120,216,0,-29][144,216,0,-29][168,48,0,10][168,96,0,10][168,216,0,-29][192,72,0,40][192,96,0,-30][192,216,0,-29][264,216,-9,-29]
opticalflow [48,216,4,-29][96,168,0,10][96,216,0,-29][120,168,0,10][120,192,0,10][120,216,0,-29][144,216,0,-29][168,48,10,0][168,96,0,10][168,216,0,-29][192,96,0,-30][192,216,0,-29][264,216,-9,-29]

0

OpenCVで画像サイズを求めるgearman workerをdaemontoolsで管理する

OpenCVで画像のサイズを求めるgearman workerを作って、Rubyから呼ぶで作ったworkerをpreforkさせて、そいつらをdaemontoolsで管理できるようにした。あらかじめCPU個数+いくつかforkしておくと、CPUが複数あるマシンを生かせるし、解析前にlibcurlで画像を取得している時のI/O待ちが少なくなって良い。(この記事のworkerはlibcurl使ってない版だけど)
あと、返り値は自分で作ったjson_builder.hを使って返すようにした。

なにげに大量の画像の中からダウンロード失敗した破損画像を見つけるのに重宝している。

まずdaemontoolsをインストールしておく

gearmandもdaemontoolsで自動起動するようにしておく。


daemontoolsで管理できるようにする。
普通にforkしただけだと、daemontoolsでsvc -dしてプロセスを止めようとしてもforkした子プロセスの方が止まらない。

Perlの場合の良い例があった。
How to manage Gearman worker processes. – TokuLog 改メ tokuhirom’s blog
Parallel::Preforkを使っている。Parallel::Preforkのソースを読んでみたら、trap_signalsオプションで親プロセスがSIGTERMとSIGHUPをフックして、子プロセスにkillを送っていた。
よく考えたら普通のforkで親が子を殺すというやつだった。


Parallel::Preforkと同じ様にやる。
forkした後親が子のpidのリストを持っておいて、SIGTERM/SIGHUPをフックして、子を全部killする処理を追加した。

daemontoolsのrunスクリプトはこれ
#!/bin/sh
exec 2>&1
exec setuidgid sho /Users/sho/src/gearmand-study/imgsize/imgsizeWorker -s localhost -p 7003 --fork 5
起動すると5個にプロセスが増える。親はdaemontoolsのsuperviseが管理してくれる。
これでsvc -dとか-uとかすればまとめて起動終了するようになった。

imgsizeWorker.cpp
// 画像サイズを返すgearman worker
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <string>
#include <iostream>
#include <cv.h>
#include <highgui.h>
#include <boost/program_options.hpp>
#include <boost/regex.hpp>
#include <boost/format.hpp>
#include <boost/tuple/tuple.hpp>
#include <boost/tuple/tuple_io.hpp>
#include <boost/any.hpp>
#include <libgearman/gearman.h>
#include "json_builder.h"

using namespace boost;
using namespace std;

tuple<int, int> get_size(const string& fileName); // 画像のwidth,heightを返す
map<string,any> imgsize(const string& fileName); // gearman workerとしてclientに返すためのJSON Objectを作る
void *job_imgsize(gearman_job_st *job, void *cb_arg, size_t *result_size, gearman_return_t *ret_ptr);
void on_exit_signal(int sig);
vector<int> pids;

int main(int argc, char* argv[]) {
  program_options::options_description opts("options");
  opts.add_options()
    ("help,h", "helpを表示")
    ("server,s", program_options::value<string>(), "gearmanサーバーのアドレス")
    ("port,p", program_options::value<int>(), "gearmanサーバーのport番号")
    ("fork", program_options::value<int>(), "preforkする数")
    ("test,t", program_options::value<string>(), "gearman worker単体テスト用query");
  program_options::variables_map argmap;
  program_options::store(parse_command_line(argc, argv, opts), argmap);
  program_options::notify(argmap);

  if(!argmap.count("help")){
    if(argmap.count("test")){
      cout << "---test---" << endl;
      string gearman_param = argmap["test"].as<string>();
      cout << json_builder::toJson(imgsize(gearman_param)) << endl; // 単体でworkerとしてのテスト
      return 0;
    }else if(argmap.count("server") && argmap.count("port")){
      if(argmap.count("fork")){
int i, pid;
for(i = 1; i < argmap["fork"].as<int>(); i++){
  pid = fork();
  if(pid == 0){ // 子プロセス
    pids.clear();
    break;
  }
  else{ // 親プロセス
    pids.push_back(pid);
    cout << str(format("fork:%d - parent:%d child:%d") % 
i %
getpid() %
pid) << endl;
  }
}
      }
      if(pids.size() > 0){ // 親プロセスの終了シグナルをフックする
signal(SIGTERM, on_exit_signal);
signal(SIGHUP, on_exit_signal);
      }
      gearman_worker_st worker;
      gearman_worker_create(&worker);
      string g_server = argmap["server"].as<string>();
      int g_port = argmap["port"].as<int>();

      struct hostent *g_host = gethostbyname((char*)g_server.c_str());
      string g_server_addr = str(format("%d.%d.%d.%d") %
 (uint)(uchar)g_host->h_addr[0] %
 (uint)(uchar)g_host->h_addr[1] %
 (uint)(uchar)g_host->h_addr[2] %
 (uint)(uchar)g_host->h_addr[3]);

      gearman_worker_add_server(&worker, g_server_addr.c_str(), g_port);
      gearman_worker_add_function(&worker, "img_size", 0, job_imgsize, NULL);
      cout << str(format("---start worker (%s:%d)---") %
  g_server_addr % g_port) << endl;
      while(true) gearman_worker_work(&worker); // workerとして待機
      return 0;
    }
  }
  cerr << "server,portが必要です" << endl;
  cerr << opts << endl;
  return 1;
  
}

// opencvで画像サイズを取得
tuple<int, int> get_size(const string& fileName){
  IplImage *img = cvLoadImage(fileName.c_str());
  if(!img){
    return make_tuple(-1, -1);
  }
  else{
    int width = img->width;
    int height = img->height;
    cvReleaseImage(&img);
    return make_tuple(width, height);
  }
}

// 画像サイズを取得してgearman serverに返すJSON Objectを作る
map<string,any> imgsize(const string& fileName){
  map<string,any> result_m;
  int width, height;
  tie(width, height) = get_size(fileName);
  if(width > 0 && height > 0){
    result_m["width"] = width;
    result_m["height"] = height;
  }
  else{
    result_m["error"] = string("image load error");
  }
  return result_m;
}

// gearman worker job
void *job_imgsize(gearman_job_st *job, void *cb_arg, size_t *result_size, gearman_return_t *ret_ptr){
  string fileName = (char*)gearman_job_workload(job);
  cout << fileName << endl;
  string result_str = json_builder::toJson(imgsize(fileName));
  cout << " => " << result_str << endl;
  char *result = (char*)strdup(result_str.c_str());
  *result_size = result_str.size();
  *ret_ptr = GEARMAN_SUCCESS;
  return result;
}

void on_exit_signal(int sig){
  for(int i = 0; i < pids.size(); i++){
    cout << str(format("kill (pid:%d)") % pids[i]) << endl;
    if(kill(pids[i], SIGKILL) < 0){
      cerr << str(format("kill failed (pid:%d)") % pids[i]) << endl;
    }
  }
  exit(0);
}

0

OpenCVで画像のサイズを求めるgearman workerを作って、Rubyから呼ぶ

ファイル名を渡すと画像サイズを返すgearman workerを作った。

{"width":1600, "height":1200}
という風にJSON風に値を返す。


以前画像のだいたいの色を求めるgearman workerを作ってたんだけど、C++で書いたworker側を単独で動作テストする事ができなくてどうしようか悩んだ。テスト用のclientと同時に作らなければならなくて、どちらにバグがあるのか切り分けるのが面倒だった。

そこで、workerの起動時に
./imgsizeWorker --test "/path/to/imagefile.jpg"
という風にgearman clientから来る引数と同じ形式で渡すと、単体のプログラムとしても動作チェックできるようにした。


こうすると普通にworkerとして起動する。
./imgsizeWorker -s localhost -p 7003
これはboost::program_optionsでやると便利だった。
今後はこのコードを雛形にすればC++とOpenCVで高速に画像解析して、gearmand経由でスクリプト言語から呼び出しまくれる。


■使ったライブラリ
boostライブラリを中心にいろいろ使った。

opencv1.0は画像サイズを取得する為だけに使った。GUIを使って無いのにcvLoadImage()のためだけにhighgui.hを読み込んでいる。

boost::tupleとboost::tieを使うと返り値を複数返す関数が作れるので、画像の情報を返すのに便利。そういえばRubyでは普通に使ってたけどC#3.5やAS3.0ではタプル無かったな。欲しい。ASはArrayに何でも入れやすいからいいか。
boost::tupleで多値を受け取る – 橋本詳解
boost::tieでtupleを展開 – 橋本詳解

boost::formatでprintf風にstd::stringをフォーマット。
boost::program_optionsで引数をparseする。
boost::program_optionsでコマンドライン引数を読む – 橋本詳解

rubyはxing-gearman-rubyを使った。
橋本商会 Cでgearman workerを書いてRubyのclientから呼び出す


■プログラム
libgearmanのgearman_worker_add_serverはIPアドレスを渡さないとならないので、一応ホスト名を解決するようにした。

imgsizeWorker.cpp
// 画像サイズを返すgearman worker
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string>
#include <iostream>
#include <cv.h>
#include <highgui.h>
#include <boost/program_options.hpp>
#include <boost/regex.hpp>
#include <boost/format.hpp>
#include <boost/tuple/tuple.hpp>
#include <boost/tuple/tuple_io.hpp>
#include <libgearman/gearman.h>

using namespace boost;
using namespace std;

tuple<int, int> get_size(string fileName); // 画像のwidth,heightを返す
string imgsize(string fileName); // gearman workerとしてclientに返すstringに整形する
void *job_imgsize(gearman_job_st *job, void *cb_arg, size_t *result_size, gearman_return_t *ret_ptr);

int main(int argc, char* argv[]) {
  program_options::options_description opts("options");
  opts.add_options()
    ("help,h", "helpを表示")
    ("server,s", program_options::value<string>(), "gearmanサーバーのアドレス")
    ("port,p", program_options::value<int>(), "gearmanサーバーのport番号")
    ("test,t", program_options::value<string>(), "gearman worker単体テスト用query");
  program_options::variables_map argmap;
  program_options::store(parse_command_line(argc, argv, opts), argmap);
  program_options::notify(argmap);

  if(!argmap.count("help")){
    if(argmap.count("test")){
      cout << "---test---" << endl;
      string gearman_param = argmap["test"].as<string>();
      imgsize(gearman_param); // 単体でworkerとしてのテスト
      return 0;
    }else if(argmap.count("server") && argmap.count("port")){
      gearman_worker_st worker;
      gearman_worker_create(&worker);
      string g_server = argmap["server"].as<string>();
      int g_port = argmap["port"].as<int>();

      struct hostent *g_host = gethostbyname((char*)g_server.c_str());
      string g_server_addr = str(format("%d.%d.%d.%d") %
 (uint)(uchar)g_host->h_addr[0] %
 (uint)(uchar)g_host->h_addr[1] %
 (uint)(uchar)g_host->h_addr[2] %
 (uint)(uchar)g_host->h_addr[3]);

      gearman_worker_add_server(&worker, g_server_addr.c_str(), g_port);
      gearman_worker_add_function(&worker, "img_size", 0, job_imgsize, NULL);
      cout << str(format("---start worker (%s:%d)---") %
  g_server_addr % g_port) << endl;
      while(true) gearman_worker_work(&worker); // workerとして待機
      return 0;
    }
  }
  cerr << "server,portが必要です" << endl;
  cerr << opts << endl;
  return 1;
  
}

// opencvで画像サイズを取得
tuple<int, int> get_size(string fileName){
  IplImage *img = cvLoadImage(fileName.c_str());
  if(!img){
    return make_tuple(-1, -1);
  }
  else{
    return make_tuple(img->width, img->height);
    cvReleaseImage(&img);
  }
}

// 画像サイズを取得してgearman serverに返すstringに整形する
string imgsize(string fileName){
  string result_str = "";
  int width, height;
  tie(width, height) = get_size(fileName);
  if(width > 0 && height > 0){
    result_str += str(format("{\"width\":%d, \"height\":%d}")
    % width % height);
  }
  else{
    result_str = "error : image load error";
  }
  cout << fileName << " => " << result_str << endl;
  return result_str;
}

// gearman worker job
void *job_imgsize(gearman_job_st *job, void *cb_arg, size_t *result_size, gearman_return_t *ret_ptr){
  string fileName = (char*)gearman_job_workload(job);
  string result_str = imgsize(fileName);
  
  char *result = (char*)strdup(result_str.c_str());
  *result_size = result_str.size();
  *ret_ptr = GEARMAN_SUCCESS;
  return result;
}


Makefile
# Mac用Makefile
SRC = imgsizeWorker.cpp
DST = imgsizeWorker

prefix=/opt/local
INCPATH=$(prefix)/include
LIBPATH=$(prefix)/lib

CV_LIBS= -lcv -lcvaux -lcxcore -lhighgui
BOOST_LIBS= $(LIBPATH)/libboost_program_options-mt.a

GEAR_INCPATH=/usr/local/include
GEAR_LIBPATH=/usr/local/lib
GEAR_LIBS=$(GEAR_LIBPATH)/libgearman.a

all:
g++ -O $(SRC) -o $(DST) -I$(INCPATH)/opencv -L. -L$(LIBPATH) $(CV_LIBS) -I$(INCPATH)/boost $(BOOST_LIBS) -I$(GEAR_INCPATH)/libgearman -L. -L$(GEAR_LIBPATH) $(GEAR_LIBS)


client側。workerからの返り値の先頭にerrorが書いていなかったらJSONとしてparseする
testclient.rb
#!/usr/bin/env ruby
# -*- coding: utf-8 -*-
require 'rubygems'
require 'gearman'
require 'json'

if ARGV.size < 1
  puts '画像へのパスが必要'
  puts 'ruby testclient.rb ~/path/to/images/*.png'
  exit 1
end

c = Gearman::Client.new(['localhost:7003'])
taskset = Gearman::TaskSet.new(c)

ARGV.sort{|a,b| a.split(/\//).last.to_i <=> b.split(/\//).last.to_i}.each{|name|
  puts "add task #{name}"
  task = Gearman::Task.new("img_size", name+"\0")
  task.on_complete{|result|
    if !(result =~ /^error/)
      print "return: #{name} => "
      p JSON.parse(result) 
    else
      puts "return: #{name} => #{result}"
    end

  }
  taskset.add_task(task)
}
taskset.wait(100) # wait 100(sec)


&をつけていくつかworkerを起動する
./imgsizeWorker -s localhost -p 7003&
./imgsizeWorker -s localhost -p 7003&


clientからtask登録。フォルダ内のjpgファイルを全部登録する
ruby testclient.rb ~/Pictures/selected/*.jpg
サイズが返ってくる
add task /Users/sho/Pictures/selected/a66dab3a.jpg
add task /Users/sho/Pictures/selected/3ed6f38e.jpg
add task /Users/sho/Pictures/selected/77ab53f0.jpg
add task /Users/sho/Pictures/selected/889bd644.jpg
add task /Users/sho/Pictures/selected/73177294.jpg
return: /Users/sho/Pictures/selected/a66dab3a.jpg => {"height"=>1200, "width"=>1600}
return: /Users/sho/Pictures/selected/3ed6f38e.jpg => {"height"=>1200, "width"=>1600}
return: /Users/sho/Pictures/selected/77ab53f0.jpg => {"height"=>1200, "width"=>1600}
return: /Users/sho/Pictures/selected/889bd644.jpg => {"height"=>800, "width"=>1280}
return: /Users/sho/Pictures/selected/73177294.jpg => {"height"=>1200, "width"=>1600}