ほりひログ

所属組織の製品 (Azure とか) に関連する内容が多めだけど、個人の見解であって、所属組織を代表する公式情報ではないです。

続 Azure Functions の python 関数で OpenCV を使う

振り返り

前の記事で「Azure Functions の python 関数で OpenCV を使いたい場合は、カスタム コンテナーを使いましょう」と結びましたが、カスタム コンテナーの利用には App Service プランが必要なため、お金の面で考えると、少しためらいが。

しかも従量課金プランの Azure Functions は、2019 年 8 月から Python が利用可能な Linux にも拡大されています。

azure.microsoft.com

従量課金プランで何とかしたい人もいるでしょう。

ということで

やはりカスタム コンテナーを使わず Python & OpenCV を使いたい!という人がいると信じて、あきらめ悪くチャレンジしました。

最初にお断り

下記の内容は、あくまで「試してみたらうまくいった」という程度なので、今後の Azure Functions の仕様変更などで動作しなくなる可能性は十分にありますので、ご注意ください。

とりあえず結論

一部コードに修正が必要だけども、できないことはない

そもそも何が問題なのか

OpenCVPython モジュールが、カスタム コンテナー以外で 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.0libglib-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 しても見つけてくれません。

なので、下記をそれぞれ試してみました。

  1. システムがロードできるパス LD_LIBRARY_PATH に追加する
  2. 動的ロードをする

結果

それぞれの結果です。

検証コードは、下記の 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 を追加しました。

f:id:horihiro:20191006080129p:plain

で、実行結果を見てみると。。。

f:id:horihiro:20191006091936p:plain

ダメです。ネイティブ ライブラリーを見つけてくれません。

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')

結果、こうなりました。

元画像 処理結果
f:id:horihiro:20191006094330p:plain f:id:horihiro:20191006094825p:plain

やったぜ!
正常に動作しているのではないでしょうか。

残る謎

なぜ LD_LIBRARY_PATH で指定したネイティブ ライブラリーをロードしてくれないのかは謎です。

(追記)
issue あげてみました。

(追記2)
サンプル 公開しました。