LambdaでS3の.glbファイルのオフスクリーンレンダリング【Python,Docker,OSmesa】

# はじめに

LambdaでPyrenderを使ってオフスクリーンレンダリングをしたいのですが、レイヤを作るだけではOpenGLが使えないので使えません。OpenGLの機能をGPUの無いLambdaで実行するためにOSmesaというライブラリを使ってCPUでレンダリングします。自分用メモです。

# 前提

- AWSアカウントを持っている
- Dockerの実行環境

# やったこと

レイヤだけでは対応できないのでDockerイメージを作ってコンテナでデプロイします。Lambdaのベースイメージでは(自分がやった限りでは)うまくいかなかったので、Ubuntuのベースイメージを使います。フォルダ構成は以下の通りです。

```
.
│  app.py
│  Dockerfile
│  env.txt
|  entry.sh
└─ requirements.txt
```

Dockerfileは以下の通りです。

```Dockerfile:Dockerfile
FROM ubuntu:18.04

ARG FUNCTION_DIR="/function"
RUN mkdir -p ${FUNCTION_DIR}
WORKDIR ${FUNCTION_DIR}

RUN apt-get update
RUN apt-get install -y \
    python3.6 \
    python3-pip \
    python-opengl \
    libosmesa6 \
    autoconf \
    automake \
    libtool \
    libssl-dev \
    cmake \
    git \
    curl

ENV PYOPENGL_PLATFORM=osmesa

ADD https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie /usr/bin/aws-lambda-rie

COPY requirements.txt  .
RUN pip3 install -U pip
RUN pip3 install -r requirements.txt --target "${FUNCTION_DIR}"

COPY app.py ${FUNCTION_DIR}
COPY entry.sh /
RUN chmod 755 /usr/bin/aws-lambda-rie /entry.sh

ENTRYPOINT ["/entry.sh"]
CMD ["app.lambda_handler"]
```

ENVで環境変数``PYOPENGL_PLATFORM``に``osmesa``を指定します。``aws-lambda-rie``はローカルテスト用です。

他ファイルは以下のように記述します。

```python:app.py
import os
os.environ['PYOPENGL_PLATFORM'] = 'osmesa'
import base64
import requests
import io
import numpy as np
import cv2
import requests
import boto3
import json

import trimesh
from pyrender import PerspectiveCamera,\
                    PointLight,\
                    Primitive, Mesh, Scene,\
                    OffscreenRenderer

def lambda_handler(event, context):

    path = event['queryStringParameters']['path']

    width = int(1280/2)
    height = int(720/2)

    bucket_name = 'my-bucket'
    key = path
    s3_client = boto3.client('s3')
    sasurl = s3_client.generate_presigned_url(
        'get_object',
        Params={'Bucket': bucket_name,'Key': key},
        ExpiresIn=30)

    # sasで取得
    model = requests.get(sasurl).content
    model = io.BytesIO(model)
    try:
        model_gltf = trimesh.load(model,file_type='glb')
        model_trimesh = model_gltf.geometry[list(model_gltf.geometry.keys())[0]]
        model_mesh = Mesh.from_trimesh(model_trimesh)
        centroid = model_mesh.centroid
        extents = model_mesh.extents
        model_pose = np.eye(4)
        r = max(extents) # 最大の辺が大きさ1になるように正規化する
        model_pose = np.array([
            [1.0/r, 0.0,  0.0, -centroid[0]/r],
            [0.0, 1.0/r, 0.0, -centroid[1]/r],
            [0.0, 0.0,  1.0/r, -centroid[2]/r],
            [0.0, 0.0,  0.0, 1.0],
        ])
        cam = PerspectiveCamera(yfov=(np.pi / 3.0))
        cam_pose = np.array([
            [1.0,  0.0,  0.0,  0.0],
            [0.0,  1.0,  0.0,  0.0],
            [0.0,  0.0,  1.0,  1.5],
            [0.0,  0.0,  0.0,  1.0]
        ])
        light = PointLight(intensity=10)
        light_pose = np.array([
            [1.0, 0.0, 0.0, 0.0],
            [0.0, 1.0, 0.0, 0.0],
            [0.0, 0.0, 1.0, 1.5],
            [0.0, 0.0, 0.0, 1.0]
        ])
        scene = Scene()
        model_node = scene.add(model_mesh, pose=model_pose)

        cam_node = scene.add(cam, pose=cam_pose)
        light_node = scene.add(light, pose=light_pose)
        r = OffscreenRenderer(viewport_width=width, viewport_height=height)
        color, depth = r.render(scene)
        r.delete()

        color = cv2.cvtColor(color,cv2.COLOR_RGB2BGR)
        result, color = cv2.imencode('.png', color)
        thumbnail = color.tobytes()
        s3_client.put_object(Body=thumbnail,Bucket=bucket_name,Key=path.replace('.glb','.png'))
        results = 'complete'
    except Exception as e:
        print(e)
        results = 'failed'

    return {
        'statusCode': 200,
        'body': results
    }
```

```bash:entry.sh
#!/bin/sh
if [ -z "${AWS_LAMBDA_RUNTIME_API}" ]; then
    exec /usr/bin/aws-lambda-rie python3 -m awslambdaric $1
else
    exec python3 -m awslambdaric $1
fi
```

```:requirements.txt
boto3
pyopengl==3.1.5
pyrender
awslambdaric
trimesh
opencv-python
requests
```

上記のDockerfileをビルドしてECRにpush、Lambdaでコンテナイメージを設定します。なお、``env.txt``はローカルでのテスト用です。コンテナ実行時にcredentialを渡します。ECRへのpushやローカルでのテストのやり方などは自分の[この過去の記事](https://qiita.com/yosiiii/items/bb7c6793b2bdd2029b95)を参考にしてみてください。

# おわりに

OSmesaについての情報がもっと欲しいです。今時GPUがないPCがないのでしょうがないのかなと思いました。

間違い等ありましたらご指摘いただけると助かります。

# 参考

- [Lambda コンテナイメージの作成](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-create.html)
- [【AWS Lambda】独自のコンテナイメージを超シンプルに使用する方法](https://qiita.com/yohei7328/items/df7ebaa4544621e16c2b)
- [Lambda コンテナイメージをローカルでテストする](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/images-test.html)
- [Installation Guide — pyrender 0.1.45 documentation](https://pyrender.readthedocs.io/en/latest/install/index.html#installmesa)
- [AWS Lambda の新機能 – コンテナイメージのサポート](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/)