振り返り
前の記事で「Azure Functions の python 関数で OpenCV を使いたい場合は、カスタム コンテナーを使いましょう」と結びましたが、カスタム コンテナーの利用には App Service プランが必要なため、お金の面で考えると、少しためらいが。
しかも従量課金プランの Azure Functions は、2019 年 8 月から Python が利用可能な Linux にも拡大されています。
従量課金プランで何とかしたい人もいるでしょう。
ということで
やはりカスタム コンテナーを使わず Python & OpenCV を使いたい!という人がいると信じて、あきらめ悪くチャレンジしました。
最初にお断り
下記の内容は、あくまで「試してみたらうまくいった」という程度なので、今後の Azure Functions の仕様変更などで動作しなくなる可能性は十分にありますので、ご注意ください。
とりあえず結論
一部コードに修正が必要だけども、できないことはない。
そもそも何が問題なのか
OpenCV の Python モジュールが、カスタム コンテナー以外で import できない理由は、Python のコード内の
import cv2
が実行された時にロードされる libgthread-2.0.so.0 をはじめとしたネイティブ ライブラリーが、従量課金プランで使用する 既定のコンテナー に入っていないことです。
当たり前ですが、入っていなければロードはできません。
azure-functions-core-tools では、--additional-packages
と --build-native-deps
を使えば、デプロイ パッケージ作成時に必要なネイティブ ライブラリーはインストールできます。
ですが、それは /usr/lib
は /usr/local/lib
などシステム ワイド (って使い方、あってます?) にインストールされてしまうので、生成されたデプロイ パッケージにこれらのネイティブ ライブラリーは含まれていません。
関数の実行には、必ず( libgthread-2.0.so.0
などが未インストールな) 既定のコンテナーが使用されるため、前回の記事の「azure-function-tools からバイナリーパッケージと一緒にデプロイ」のとおり、デプロイ パッケージ実行時は、ロード エラーが発生していました。
解決策
だったら、デプロイ パッケージに libgthread-2.0.so.0
と依存ライブラリーを入れてしまって、実行時のロードできるように配置したらいいじゃない。
やり方
やってみます。
1. ファイルの準備
まずはローカルに libgthread-2.0.so.0
とその依存ライブラリーを準備しますが、依存ライブラリーが多いと面倒なので、今回は opencv-python-headless
という Python ライブラリーを使用します。
これを使った場合の必要なネイティブ ライブラリーは、libgthread-2.0.so.0
と libglib-2.0.so.0
の二つです。
以下のスクリプトで、ネイティブ ライブラリーをローカルにコピーします。
#!/bin/sh CID=$(docker run -d --rm -it mcr.microsoft.com/azure-functions/python /bin/bash) docker exec ${CID} sh -c 'apt update && apt install libglib2.0-0 -y && cp -L $(ldconfig -p | grep -E "libg(thread|lib)-2.0.so.0" | sed -E "s/.* ([^ ]+$)/\1/") /tmp/' docker cp ${CID}:/tmp/ . docker kill ${CID}
# Azure Function Core Tools を真似して、docker コンテナー上でネイティブ ライブラリーをインストールして、それを持ってきています。
無事コピーできたようです。
$ ls -l ./tmp total 1112 -rwxrwxrwx 1 horihiro horihiro 1127520 Oct 6 07:05 libglib-2.0.so.0 -rwxrwxrwx 1 horihiro horihiro 6160 Oct 6 07:05 libgthread-2.0.so.0
2. Azure Functions のプロジェクトにコピー
先ほどコピーした tmp
フォルダーごと、開発しているプロジェクトに配置します。
こんな感じで。
$ tree . . ├── host.json ├── HttpTrigger │ ├── function.json │ ├── __init__.py │ └── sample.dat ├── local.settings.json ├── proxies.json ├── requirements.txt └── tmp ├── libglib-2.0.so.0 └── libgthread-2.0.so.0
3. ネイティブ ライブラリーをロードする設定
コピーしてきたネイティブ ライブラリー ./tmp/libg(thread|lib)-2.0.so.0
は、勝手に持ってきただけなので、このまま import cv2
しても見つけてくれません。
なので、下記をそれぞれ試してみました。
- システムがロードできるパス
LD_LIBRARY_PATH
に追加する - 動的ロードをする
結果
それぞれの結果です。
検証コードは、下記の HTTP トリガーの関数で、クエリー パラメータ image_url
で指定された URL にある画像をグレースケール画像に変換して返す関数です。
# Python 詳しくないんで、変なところがあってもご容赦を。
def main(req: func.HttpRequest) -> func.HttpResponse: logging.info('Python HTTP trigger function processed a request.') image_url = req.params.get('image_url') if not image_url: try: req_body = req.get_json() except ValueError: pass else: image_url = req_body.get('image_url') if image_url: local = re.sub(r'^.*/([^/]+)$', r'\1', image_url) rq.urlretrieve(image_url, '/tmp/' + local) im = cv2.imread('/tmp/' + local) gry = cv2.cvtColor(im, cv2.COLOR_BGR2GRAY) cv2.imwrite('/tmp/gray_' + local, gry) with open('/tmp/gray_' + local, 'rb') as f: mimetype = mimetypes.guess_type('/tmp/gray_' + local) return func.HttpResponse(f.read(), mimetype=mimetype[0]) else: return func.HttpResponse( "Please pass a image_url on the query string or in the request body", status_code=400 )
なので、少なくとも下記の関数が使えるかどうか、の検証になっているはずです。
imread
imwrite
cvtColor
a. システムがロードできるパスに追加する
Function App のアプリケーション設定から、LD_LIBRARY_PATH
に /home/site/wwwroot/tmp
を追加しました。
で、実行結果を見てみると。。。
ダメです。ネイティブ ライブラリーを見つけてくれません。
b. 動的ロードをする
次に動的ロードですが、これはつまり 「 import cv2
を諦める」となるので、やや本末転倒感が否めないですが、これくらいなら許してくれると信じて。
下記の Qiita の記事を参考に、import cv2
の部分を、強制的にネイティブ ライブラリーをロードし、動的に cv2
をインポートする Python コードに置き換えます。
qiita.com
# import cv2 import ctypes import importlib exlibpath = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + '/tmp/' ctypes.CDLL(exlibpath + 'libglib-2.0.so.0') ctypes.CDLL(exlibpath + 'libgthread-2.0.so.0') cv2 = importlib.import_module('cv2')
結果、こうなりました。
元画像 | 処理結果 |
---|---|
やったぜ!
正常に動作しているのではないでしょうか。
残る謎
なぜ LD_LIBRARY_PATH
で指定したネイティブ ライブラリーをロードしてくれないのかは謎です。
(追記)
issue あげてみました。
(追記2)
サンプル 公開しました。