実例で学ぶPHP拡張モジュールの作り方

第3回WEBカメラから画像をキャプチャ

今回は外部ライブラリとリンクする方法、定数を定義する方法、引数を取り値を返す関数の作り方を紹介します。題材はWEBカメラの画像をキャプチャする拡張モジュールです。

WEBカメラのようなハードウェアにアクセスするのはPHPの標準機能では不可能なことで、まさに拡張モジュールの出番です。しかし、ハードウェアにアクセスするとなると敷居が高いと思われるかもしれませんが、ご心配なく。Intelが開発しているOpenCVというオープンソースの画像処理ライブラリを使えば、簡単にWEBカメラから画像をキャプチャできます。

OpenCVのインストール

まずは公式WikiのLinux向けインストールガイド(英文)を参考にOpenCVをインストールしてください。Mac OS Xの場合はconfigureのオプションで--without-gtk2 --with-carbon --with-quicktimeを指定することによってGTK2の代わりにMac OS X標準のウインドウシステムを、Video4Linux(カメラからのキャプチャ)とlibavcodec(動画ファイルからのキャプチャ)の代わりにQuickTimeを使うことができます。

specファイルを記述する

今回制作するcvcaptureモジュールのspecファイルの構成はリスト1のようになっています。

リスト1 specファイル概要(cvcapture-0.1.0.xml)
<?xml version="1.0" encoding="UTF-8"?>
<extension name="cvcapture" version="0.1.0">
  <summary>Capture frames from webcams.</summary>
  <!-- モジュール情報(説明は省略) -->
  <!-- 依存関係(リスト2-1、2-2) -->
  <!-- 定数定義(リスト3) -->
  <!-- 関数定義(リスト4) -->
</extension>

依存関係を定義する

依存関係の定義はリスト2-1にあるように、<deps>タグに記述します。OpenCVのように依存関係が複雑なライブラリを使いたい場合はpkg-configというツールを使うと依存関係が簡潔に記述できます[1]⁠。pkg-configを使わない場合はリスト2-2のように記述します。

リスト2-1 依存関係定義(cvcapture-0.1.0.xml)
<deps language="c" platform="all">
  <!-- pkg-configを使って“opencv”の設定を取得する -->
  <with name="opencv" mode="pkg-config">
    <!-- ヘッダ名(ヘッダのインクルードパスもpkg-configで設定される) -->
    <header name="cv.h"/>
    <header name="highgui.h"/>
  </with>
</deps>
リスト2-2 pkg-configを使わない依存関係定義
<deps language="c" platform="all">
  <!-- configureの引数--with-cvcaptureで指定されたパス(省略時は
       “/usr /usr/local”)から“include/opencv/cv.h”を探す -->
  <with testfile="include/opencv/cv.h">
    <!-- ヘッダ名(includeからの相対パス) -->
    <header name="opencv/cv.h"/>
    <header name="opencv/highgui.h"/>
    <!-- ライブラリ名(libなどの接頭辞や.soなどの拡張子は不要) -->
    <lib name="cv"/>
    <lib name="highgui"/>
  </with>
</deps>

定数を定義する

続いて、カメラの種類を指定するための定数を定義します。定数の情報は<constants>タグ内の<constant>タグに記述します。<constant>タグではtype属性で型、name属性で定数名、value属性で値を指定します。定数の説明は<description>タグや<summary>タグは使わず、<constant>タグの中に文字データとして記述します。説明はマニュアルに反映されるのですが、不要なら<constant ... />と書いてもかまいません。

リスト3 定数定義(cvcapture-0.1.0.xml)
<constants>
  <!-- CV_CAP_*はopencv/highgui.hで定義されているマクロ -->
  <constant type="int" name="CV_CAP_ANY" value="CV_CAP_ANY">autodetect</constant>
  <-- (省略) -->
  <constant type="int" name="CV_CAP_QT" value="CV_CAP_QT">QuickTime</constant>
</constants>

関数を定義する

最後に、カメラから画像をキャプチャする関数cv_camera_capture()と動画の最初のフレームをキャプチャする関数cv_file_capture()を定義します。cv_camera_capture()は第1引数でキャプチャした画像を保存するパス、第2引数でカメラを指定する数値(省略された場合は自動選択)を指定し、キャプチャに成功したかどうかを真偽値で返します。第3引数が指定された場合は、キャプチャした画像の幅と高さのペアを配列で代入します。cv_file_capture()も第2引数で読み込む動画ファイルのパスを指定する以外は同じです。

筆者はCodeGen_PECLを使うのは、ひな型の生成までにし、実際の処理はソースコードに直接記述するスタイルをお勧めするので、今回のspecファイルには関数の実装は書いていません。

値を返す関数のプロトタイプでは関数名の前にvoidではなく戻り値の型を書きます。また、引数の情報は型と名前のセットをカンマ区切りで指定します。このとき引数名の先頭に$をつけてはいけません。必須でない引数は角括弧で囲み、参照として受け取る引数は名前の前に&リスト4ではエスケープして&amp;)をつけます。引数の型を特定しない場合は⁠mixed⁠とします。

キャプチャする画像の大きさはカメラによって変わるので、両関数のテストケースでは<result mode="format">として期待する出力をフォーマット文字列にしています。また、cv_file_capture()のテストケースでは<skipif>タグでテスト用の動画ファイルが存在しない場合はテストを省略するようにしています。テストを省略する場合は⁠skip(半角スペース)⁠理由)⁠の書式で文字列を出力してテストケースを終了させます。

リスト4 関数定義
<function name="cv_camera_capture">
 <proto>bool cv_camera_capture(string filename[, int index[, mixed &amp;size]])</proto>
  <summary>Capture from camera.</summary>
  <description>(省略)</description>
   <test>
    <code><?data
if (cv_camera_capture("test_camera.jpg", CV_CAP_ANY, $size)) {
    print_r($size);
}
    ?></code>
    <result mode="format"><?data
Array
(
    [0] => %d
    [1] => %d
)
    ?></result>
  </test>
</function>

<function name="cv_file_capture">
  <proto>bool cv_file_capture(string dst_filename, string src_filename[, mixed &amp;size])</proto>
  <summary>Capture from video file.</summary>
  <description>(省略)</description>
  <test>
    <skipif><?data
if (!file_exists("sample.3g2")) {
    die("skip sample video file does not exist");
}
    ?></skipif>
    <code><?data
if (cv_file_capture("test_file.jpg", "sample.3g2", $size)) {
    print_r($size);
}
  ?></code>
  <result mode="format"><?data
Array
(
    [0] => %d
    [1] => %d
)
    ?></result>
  </test>
</function>

ソースコードを生成する

これでspecファイルが用意できたので、pecl-genコマンドでモジュールのソースコードを生成します。今回は--dirオプションで出力先のディレクトリ名も指定しています。

操作1 pecl-genを実行
$ pecl-gen --dir=cvcapture-0.1.0 cvcapture-0.1.0.xml
Creating 'cvcapture' extension in './cvcapture-0.1.0'

Your extension has been created in directory ./cvcapture.
See ./cvcapture-0.1.0/README and/or ./cvcapture-0.1.0/INSTALL for further instructions.

ソースコード解説

前回は生成されたソースコードの説明を一切しなかったので、関数の実装に入る前に、生成されたファイルを少し眺めてみましょう。

バージョンチェック

まずはconfig.m4です。phpizeコマンドでは、このファイルの内容を元にconfigureスクリプトを生成します。中を見ると、pkg-configを使ってCPPFLAGSやLDFLAGSを設定したりしていますが、そこではなくAC_MSG_CHECKINGから始まる箇所に注目してください。ここでPHPのバージョンをチェックしています。cvcaptureモジュールではPHP 4互換のAPIしか使っていないので、PHPのバージョンが4.0.0以降かどうかを調べるコードが生成されました。

リスト5-1 PHPのバージョンチェック(config.m4)
  AC_MSG_CHECKING(PHP version)
  AC_TRY_COMPILE([#include <php_version.h>], [
#if PHP_VERSION_ID < 40000
#error  this extension requires at least PHP version 4.0.0
#endif
],
[AC_MSG_RESULT(ok)],
[AC_MSG_ERROR([need at least PHP 4.0.0])])

一見するとリスト5-1でちゃんとバージョンをチェックできているように思えますが、PHP_VERSION_IDはPHP 5.2以降でしか定義されていないマクロなので、PHP 4.4やPHP 5.1などでphpizeした場合、configureでエラーが出ます。本連載はPHP 5.2以降を対象にしていますが、それ以前のバージョンのPHPでも使えるようにしたいのなら、この箇所を丸ごと削除するか、リスト5-2のようにマクロPHP_MAJOR_VERSIONおよびPHP_MINOR_VERSIONを使ってバージョンを指定します。なお、これらのマクロはphp/main/php_version.hで定義されています。

リスト5-2 PHPのバージョンチェック(変更後)
  AC_MSG_CHECKING(PHP version)
  AC_TRY_COMPILE([#include <php_version.h>], [
#if PHP_MAJOR_VERSION < 4 || (PHP_MAJOR_VERSION == 4 && PHP_MINOR_VERSION < 4)
#error  this extension requires at least PHP version 4.4.0
#endif
],
[AC_MSG_RESULT(ok)],
[AC_MSG_ERROR([need at least PHP 4.4.0])])

引数情報

次に、php_cvcapure.hを見てください。ここで注目すべきは関数の引数情報を定義するマクロZEND_BEGIN_ARG_INFO_EXです。これはPHP 5から実装されたリフレクションおよびタイプヒンティングのためのマクロで、ZEND_BEGIN_ARG_INFO_EX()からZEND_END_ARG_INFO()までの間にZEND_ARG_INFO()で個々の引数に関する情報(参照として受け取るか否かとリフレクション用の名前)を記述します。PHP 4向けにはNULLとなっていますが、もしcvcaptureモジュールをPHP 4.xでも使いたい場合は、ここを3番目の引数を参照として受け取ることを示す変数third_arg_force_refにしてください。

リスト6 引数情報定義(php_cvcapture.h)
PHP_FUNCTION(cv_file_capture); /* 関数のプロトタイプ宣言 */
#if (PHP_MAJOR_VERSION >= 5)
ZEND_BEGIN_ARG_INFO_EX(cv_camera_capture_arg_info, ZEND_SEND_BY_VAL, ZEND_RETURN_VALUE, 1)
  ZEND_ARG_INFO(0, filename)
  ZEND_ARG_INFO(0, index)
  ZEND_ARG_INFO(1, size)
ZEND_END_ARG_INFO()
#else /* PHP 4.x */
#define cv_camera_capture_arg_info NULL /* third_arg_force_refに変更する */
#endif

これらの情報はcvcapture.cにあるモジュールが提供する関数のリストで使われます。

リスト7 関数リスト(cvcapture.c)
/* {{{ cvcapture_functions[] */
function_entry cvcapture_functions[] = {
  PHP_FE(cv_camera_capture   , cv_camera_capture_arg_info)
  PHP_FE(cv_file_capture     , cv_file_capture_arg_info)
  { NULL, NULL, NULL } /* 必ずヌル終端する */
};
/* }}} */

モジュール情報

モジュールに関する情報もメインのソースコードであるcvcapture.cに記述されています。特に重要なのがzend_function_entry構造体で、ここにモジュール情報のすべてが集約されています。

リスト8 モジュール情報(cvcapture.c)
/* {{{ cvcapture_module_entry
 */
zend_module_entry cvcapture_module_entry = {
  STANDARD_MODULE_HEADER, /* おまじない */
  "cvcapture", /* モジュール名 */
  cvcapture_functions, /* 関数リスト */
  PHP_MINIT(cvcapture),     /* Replace with NULL if there is nothing to do at php startup   */ 
  PHP_MSHUTDOWN(cvcapture), /* Replace with NULL if there is nothing to do at php shutdown  */
  PHP_RINIT(cvcapture),     /* Replace with NULL if there is nothing to do at request start */
  PHP_RSHUTDOWN(cvcapture), /* Replace with NULL if there is nothing to do at request end   */
  PHP_MINFO(cvcapture),
  "0.1.0", /* バージョン文字列 */
  STANDARD_MODULE_PROPERTIES /* おまじない */
};
/* }}} */

このうちPHP_{MINIT,MSHUTDOWN,RINIT,RSHUTDOWN,MINFO}は、それぞれモジュール初期化、モジュール終了、リクエスト初期化、リクエスト終了、モジュール情報表示の際に呼ばれるコールバック関数で、PHP_MINFO以外は必要なければNULLにしても構いません。cvcaptureモジュールではPHP_MINITに対応するPHP_MINFO_FUNCTIONで定数を定義しています。PHP_MINFOに対応するPHP_MINFO_FUNCTIONは値を返しませんが、それ以外のコールバック関数は初期化/終了に成功したらSUCCESS(マクロで、実体は0⁠⁠、失敗したらFAILURE(同-1)を返します。

リスト9 モジュール初期化関数(cvcapture.c)
/* {{{ PHP_MINIT_FUNCTION */
PHP_MINIT_FUNCTION(cvcapture)
{
  REGISTER_LONG_CONSTANT("CV_CAP_ANY", CV_CAP_ANY, CONST_PERSISTENT | CONST_CS);
  /* (省略) */
  REGISTER_LONG_CONSTANT("CV_CAP_QT", CV_CAP_QT, CONST_PERSISTENT | CONST_CS);

  /* add your stuff here */

  return SUCCESS;
}
/* }}} */

関数を実装する

ここからはcvcapture.cに関数を実装していきます。関数の実装が書かれていないspecファイルからソースコードを生成した場合、cv_camera_capture()はリスト10のようになっています。

リスト10 cv_camera_capture()関数(cvcapture.c)
/* {{{ proto bool cv_camera_capture(string filename[, int index[, mixed &size]])
  Capture from camera. */
PHP_FUNCTION(cv_camera_capture)
{
  /* 変数の宣言と初期化 */
  const char * filename = NULL;
  int filename_len = 0;
  long index = 0;
  zval * size = NULL;

  /* 引数をパース */
  if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "s|lz", &filename, &filename_len, &index, &size) == FAILURE) {
    return;
  }

  /* 実装されていない旨のエラーを出力 */
  php_error(E_WARNING, "cv_camera_capture: not yet implemented"); RETURN_FALSE;

  /* 戻り値をfalseにしてreturnする(C言語レベルの戻り値はvoidで、PHPレベルの
     戻り値であるzval *return_valueの型をboolに、値を0にしている) */
  RETURN_FALSE;
}
/* }}} cv_camera_capture */

変数の宣言を見ると、プロトタイプでstring(文字列)とした変数がconst char *型(文字列の先頭のアドレス)とint型(文字列の長さ)に、int(整数)とした変数がlong型に、mixed(未指定)とした変数がzval *型(汎用の変数コンテナ)に対応していることが分かります。引数をパースする関数zend_parse_parameters()の引数のうち"s|lz"というのが引数の型を指定するフォーマット文字列で、sは文字列の指示子、|はそれ以後の引数がオプションである事を示す修飾子、lは整数の指示子、zは型を指定しない変数の指示子です。

このコードに実装を書き加えてcv_camera_capture()を完成させます。

変数宣言の追加

最初に、関数内部で使う変数の宣言がないので、リスト11を既にある変数宣言の後に追加します[2]⁠。また、⁠long index⁠の初期値を0からCV_CAP_ANYにしておきます(CV_CAP_ANYの実体も0ですが、念のため⁠⁠。

リスト11 追加の変数宣言

char *fullpath;     /* 保存先のフルパス */
CvCapture *capture; /* キャプチャ構造体 */
IplImage *image;    /* イメージ構造体 */

セーフモードとopen_basedirのチェック

引数のパースの後に、まず画像の保存先のパスをチェックをします。チェックの内容は、文字列の途中に'\0'が含まれていないかと、フルパスの取得、open_basedirおよびセーフモードのチェックです[3]⁠。

リスト12 パスのチェック
if (strlen(filename) != filename_len ||
    (fullpath = expand_filepath(filename, NULL TSRMLS_CC)) == NULL)
{
  php_error_docref(NULL TSRMLS_CC, E_WARNING, "Wrong filename given");
  RETURN_FALSE;
}

if (php_check_open_basedir(fullpath TSRMLS_CC) ||
    (PG(safe_mode) && !php_checkuid(fullpath, NULL, CHECKUID_CHECK_FILE_AND_DIR)))
{
  efree(fullpath);
  RETURN_FALSE;
}

PHPの変数から来る文字列はバイナリデータも含めて必ずヌル終端されている(長さより最低1バイト余分に確保され、末尾に'\0'が付加されている)はずなので、strlen()などの引数にすることができます。もしヌル終端されていない文字列があったとしたら、それはそのデータを生成した拡張モジュールの問題です。

フルパスを取得する関数expand_path()の戻り値はPHPのメモリ管理用関数emalloc()で確保された領域のアドレスなので、free()ではなくefree()で開放します。また、リスト12ではエラーの出力にリスト10にあるphp_error()ではなく、より詳細なエラーメッセージを出力する関数php_error_docref()を使っています。引数のE_WARNINGはエラーコードで、より重大なエラーの場合はE_ERRORを、軽微なエラーの場合はE_NOTICEを指定します。PHP拡張モジュールでは主にこれら3つのエラーを使います[4]⁠。なお、php_error()、php_error_docref()ともに、前回紹介したphp_printf()と同じフォーマットが使えます。

php_check_open_basedir()およびphp_checkuid()は問題があった場合にエラーを出力してくれるので、別途エラーを出力する必要はありません。

画像のキャプチャ

ここがcv_camera_capture()の肝となる部分です。が、ここではOpenCVの関数を使っている以外に特筆すべきところはありません。OpenCVの詳細な使い方については本連載の趣旨から外れるので割愛させていただきます。

リスト13 キャプチャ処理
/* キャプチャ構造体を作成 */
capture = cvCreateCameraCapture((int)index);
if (capture == NULL) {
  php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot create camera capture");
  efree(fullpath);
  RETURN_FALSE;
}

/* フレームを取得 */
image = cvQueryFrame(capture);
if (image == NULL) {
  php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot retrieve image");
  cvReleaseCapture(&capture);
  efree(fullpath);
  RETURN_FALSE;
}

/* 画像を保存 */
if (!cvSaveImage(filename, image)) {
  php_error_docref(NULL TSRMLS_CC, E_WARNING, "Cannot save image to '%s'", filename);
  cvReleaseCapture(&capture);
  efree(fullpath);
  RETURN_FALSE;
}

仕上げ

最後に、画像サイズの代入と変数の開放です。画像サイズを代入するのは第3引数が指定された(sizeがNULLでない)場合だけです。配列操作の解説は次回以降にしたいと思います。

リスト14 画像サイズの代入と変数の開放
if (size != NULL) {
  CvSize imgsize = cvGetSize(image); /* 画像サイズを取得 */
  zval_dtor(size);  /* 変数の中身を開放してから */
  array_init(size); /* 配列として初期化し直して */
  add_next_index_long(size, (long)imgsize.width);   /* 幅を追加 */
  add_next_index_long(size, (long)imgsize.height);  /* 高さを追加 */
}

cvReleaseCapture(&capture); /* imageも同時に開放される */
efree(fullpath);
RETURN_TRUE; /* 戻り値をtrueにしてreturn */

これでcv_camera_capture()の実装は終わりました。cv_file_capture()の実装はcv_camera_capture()と共通の部分がほとんどなので省略します。

インストール

実装が終わったら、いよいよcvcaptureモジュールをビルドします。pkg-configを使っているので、configureのオプションでOpenCVのインストール先を指定する必要はありません。cv_file_capture()のテストをしたい場合は、適当な3GPP2形式の動画ファイルをsample.3g2という名前で作業ディレクトリにコピーしてください。

操作2 ビルド&テスト
$ phpize
Configuring for:
PHP Api Version:         20041225
Zend Module Api No:      20060613
Zend Extension Api No:   220060519
$ ./configure --enable-cvcapture
(省略)
$ make
(省略)
Build complete.
Don't forget to run 'make test'.
$ make test
(省略)
=====================================================================
Running selected tests.
PASS cv_camera_capture() function [tests/cv_camera_capture.phpt] 
PASS cv_file_capture() function [tests/cv_file_capture.phpt] 
=====================================================================
Number of tests :    2                 2
Tests skipped   :    0 (  0.0%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :    0 (  0.0%) (  0.0%)
Tests passed    :    2 (100.0%) (100.0%)
---------------------------------------------------------------------
Time taken      :    2 seconds
=====================================================================

テストが通ることが確認できたら、あとはインストールするだけです。

操作3 インストール
$ sudo make install
Installing shared extensions:     /usr/local/lib/php/extensions/no-debug-non-zts-20060613/

実際に使ってみる

無事にインストールできたところで、実際に使ってみましょう。リスト15は1秒置きに10回カメラの画像をキャプチャするスクリプトです。出力する画像ファイル形式は拡張子から自動で決定されます。

OpenCVがサポートしていない拡張子が指定された場合や、カメラに接続できなかった場合などは「OpenCV ERROR:(理由)...」というエラーメッセージが表示され、プロセスが強制終了するので注意してください。この挙動は、あと数回に分けてcvcaptureモジュールを改良していくシリーズの最後で、OpenCVの例外をPHPの例外に変換することで対処します。

リスト15 サンプルスクリプト(cvcapture-sample.php)
<?php
extension_loaded('cvcapture') || dl('cvcapture.so');

for ($i = 1; $i <= 10; $i++) {
    cv_camera_capture(sprintf('capture%02d.jpg', $i));
    sleep(1);
}

図1はサンプルスクリプトの実行結果です。ちゃんと撮影できていますね。じっとしてくれない犬をMacBook内蔵のiSightカメラで撮影するために10回リピートしたという噂も聞かれますが、リピートしたのは次回へのフリでもあります。:-)

図1 キャプチャした画像
capture09

おわりに

今回は依存するライブラリの指定方法、定数の定義、戻り値や引数の指定、エラー出力、パスのチェックなど、多岐にわたる内容を紹介しました。詰め込みすぎのきらいは否めませんが、いずれも重要な項目です。

ところで、今回作成した関数cv_camera_capture()およびcv_file_caputure()は1回だけ使うなら便利なのですが、リスト15のように連続でキャプチャする場合は、呼び出される度にカメラに接続またはファイルを開くので処理の効率がよくありません。特に動画では最初のフレームしか取得できないという制限もあります。そこで、次回はcvcaptureモジュールにリソースのサポートを追加し、リソースを使って連続でキャプチャする関数を実装します。

サンプルファイルのダウンロード

付録は後日掲載

おすすめ記事

記事・ニュース一覧