はのちゃ爆発

はのちゃが技術ネタとか日常のこととかを書いてます。

AWS Lambda (Serverless) with C# で画像処理しようとして試行錯誤した話

開発合宿の成果の一部。 後でQiitaにも書こうかなぁ。

Serverless とも銘打ってるけど実質 Lambda の話。

AWS Lambda で C# が使える!よし画像処理だ!

C#(.NET Framework) は System.Drawing 名前空間でグラフィックス機能を提供しています。 これを使うとピクセル単位で画像の操作ができるので、画像処理プログラムを簡単に記述できたりします。

なので、これを使って Lambda 上で画像処理できたらアツいな…と妄想し、 開発合宿でやることとしてこの「AWS Lambda with C# で画像処理してみた」をテーマとして掲げました。

だがしかし

現実はそう甘くなかった。

.NET Core だと System.Drawing が使えない問題

AWS Lambda 上の C#.NET Framework ではなく .NET Core 1.0.1 (C#) で動いています。 なので、 .NET Framework では使える機能が使えない、ということが割とあります。

今回使用しようとしていた System.Drawing もその一つ。 .NET Core だと利用できないため、画像処理をしたければ別のアプローチを考えなければいけません。

(リファレンスには載ってるんですが、こいつは何者なんでしょう…次期バージョンで使えるのかな

System.Drawing namespace)

代替案

Nuget で拡張機能を追加すると、それらは自動的にデプロイパッケージに含まれる、とのことなので、 System.Drawing の代わりになりそうな拡張を探して入れることでとりあえず代替できそうです。

今回見つけたのは "ImageSharp" というもの。

github.com

なんとなく画像の鮮明化とかしてくれそうな名前ですがそうではありません。

ImageSharp is a new cross-platform 2D graphics API designed to allow the processing of images without the use of System.Drawing.

System.Drawing に依存しない画像処理を提供してくれます。 今回の私のような .NET Core で画像処理したいユーザ向け。

"ImageProcessor" という名前のプロジェクトから派生したもの…っぽいですが定かではないです。

imageprocessor.org

使ってみる

代替案が見つかったので実際に使ってみましょう。

ImageSharp のインストール

NuGet で入れればいい…と思っていましたが、まだ正式リリース前のライブラリらしく、公式 NuGet ではインストールできません。

なので、 MyGet という別のサービスのリポジトリを手動でパッケージマネージャに登録してインストールする必要があります。

www.myget.org

登録するのにも微妙に詰まったのですが、登録方法は別記事でまとめたいと思っています。

ImageSharp で画像を読み込んでリサイズしてみ…あれ、使えない

ImageSharp を導入すると ImageSharp.Image クラスが使えるようになります。 画像サイズを指定して初期化したり、stream で初期化したり、 byte[] で初期化したりできます。 (この辺はgithubでコードを見るか、IntelliSenseで見たほうが早い気がします。)

使い方は簡単で、

using ImageSharp;
~~~
var image = new Image(何か適当なイメージソース)

インスタンスを作成できます。では実際に読み込んで…と行きたいところですが、実はこのままだと上手くいきません。 以下のようなエラーが出て処理に失敗します。

f:id:hano_tea:20170120003041p:plain

ImageSharp で画像を読み込むための別パッケージを追加する

公式リポジトリの Readme にちょこっと書いてあるのですが、

The ImageSharp library is made up of multiple packages, to make ImageSharp do anything useful you will want to make sure you include at least one format as a dependency otherwise you will not be able to save/load any images.

とあります。なので最低でも一つは何らかの画像フォーマット用のパッケージを追加でインクルードしておかないと動きません。

最初この記述に気付かずに謎エラーでしばらく悩んでいました… Installationに書いておいてくれ…

とりあえず JPEGPNG、お好みで BMP とか入れておくといいんじゃないでしょうか。 これらの各種画像フォーマット用追加ライブラリも、上記のMyGetから入れられます。

MyGet(を登録したNuGet)でインストールすると、これらのパッケージは自動的に ImageSharp で使われるようになります。

画像をリサイズしてみる

画像が上記のパッケージのインストールでできるようになったので、実際に画像処理をしてみたい…のですが、 画像処理を行うためのライブラリはまた別に入れる必要があります。

ImageSharp.Processing というのがその画像処理用のパッケージなので、これもインストールします。

ここまでやってようやく画像処理っぽいことができます。

ImageSharp で画像を読み込んで作成した Image のインスタンスに画像処理用メソッドが追加されています。

リサイズを行う場合は、

resizedImage = image.Resize(x, y)

だけで実現できます。他にもいろいろな処理が実装されていますが、とりあえず今回はリサイズで。

リサイズした画像はそのままでは見えませんので、

resizedImage.SaveAsJpeg(ファイルストリーム的なもの)

で書きだします。Lambdaだったら /tmp 以下の適当な場所に書き出せばいいかと思います。

ちなみに、ここで使っている SaveAs... メソッドは、他の画像フォーマット用パッケージを入れていれば それに対応した形式での出力が使える(はず)なので、お好きな形式をご利用ください。

SaveAsHoge で画像を出力したら、無事リサイズされた画像が見れるようになります。

Lambda的にはこの後、出力した画像をさらにS3に上げたりとかします。

まとめ

まとめると、以下のようなコードで S3からの画像読み込み-> ImageSharpでの画像処理 -> S3へのJPG書き出し が実現できます。

using System.Collections.Generic;
using System.Net;
using System.IO;
using System.Threading.Tasks;
using System.Diagnostics;

using Amazon.S3;
using Amazon.S3.Model;
using Amazon.Lambda.Core;
using Amazon.Lambda.APIGatewayEvents;

using ImageSharp;


// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializerAttribute(typeof(Amazon.Lambda.Serialization.Json.JsonSerializer))]

namespace LambdaImageConverter
{
    public class Functions
    {
        private AmazonS3Client s3Client;

        /// <summary>
        /// Default constructor that Lambda will invoke.
        /// </summary>
        public Functions()
        {
            s3Client = new AmazonS3Client();
        }


        /// <summary>
        /// A Lambda function to respond to HTTP Get methods from API Gateway
        /// </summary>
        /// <param name="request"></param>
        /// <returns>The list of blogs</returns>
        public async Task<APIGatewayProxyResponse> Get(APIGatewayProxyRequest request, ILambdaContext context)
        {
            context.Logger.LogLine("Get Request\n");

            Image testImage = await LoadImage(context);
            var resizedImage = testImage.Resize(256, 256);

            resizedImage.SaveAsJpeg(new FileStream("/tmp/lena_resized.jpg", FileMode.Create));

            var putReq = new PutObjectRequest
            {
                BucketName = "<Bucket名>",
                Key = "lena_resized.jpg",
                FilePath = "/tmp/lena_resized.jpg"
            };
            
            await s3Client.PutObjectAsync(putReq);

            var response = new APIGatewayProxyResponse
            {
                StatusCode = (int)HttpStatusCode.OK,
                Body = "Hello AWS Serverless",
                Headers = new Dictionary<string, string> { { "Content-Type", "text/plain" } }
            };

            return response;
        }

        private async Task<Image> LoadImage(ILambdaContext context)
        {
            var obj = await s3Client.GetObjectAsync("<Bucket名>", "lena.jpg");
            Image img = new Image(obj.ResponseStream);
            return img;
        }

        private void Logging(string str, ILambdaContext context)
        {
            context.Logger.LogLine(str);
        }
    }
}

以上、AWS Lambda with C# でも使える画像処理方法の紹介でした。

本当はS3に新しい画像を投げ込んだらそれをトリガーとして、投げ込まれた画像に何らかの処理を施し、 HTTPレスポンスとして変換後の画像を返したり、任意の画像URLから画像を取得してそれに対して画像処理する、 みたいなシステムにしたかったのですが、合宿中にそこまではたどり着かず…

上記の最終目標的なところは、今後の課題としてやっていきたいと思います。 Azure Functions だともっと簡単に出来るかもしれないっぽいし