だるろぐ

明日できることは、今日しない。

UWP:SoftwareBitmap を縮小する

はてなブログにデカい写真を貼るときのフローがめんどくさい。どれぐらいめんどくさいかというと、ブログを書くペースが月1回に落ちるぐらいめんどくさい。

blog.daruyanagi.jp

――というわけで、年始は画像を縮小できるアプリを開発していた。要件は以下のとおり。

  • [共有]コマンドに対応(必須)
  • シンプル。ブログへのアップロードに使いそうな機能しか追加しない
    • 画像の縮小(できた)
    • 画像の回転(すぐできそうだけどやってない)
    • 顔認識して隠す(進捗半分)
    • 画像のクロップ(優先度低)

UWP アプリの開発は1年以上ぶりで、右も左もわからぬ。Microsoft Docs をさまよった結果、内部での画像データは SoftwareBitmap あたりで持つのがよさげだったが、当初は縮小の方法もいまいちわからかった。

Win2D を使う

そういうときは、やっぱり StackOverFlow だよね。親切にも SoftwareBitmap の拡張メソッドにしてくれていたので、そのまま使うことにした。ちなみに、これを利用するには NuGet で Win2D パッケージを別途インストールする必要がある。

// https://stackoverflow.com/questions/41251716/how-to-resize-a-softwarebitmap

public static SoftwareBitmap Resize(this SoftwareBitmap softwareBitmap, float newWidth, float newHeight)
{
    using (var resourceCreator = CanvasDevice.GetSharedDevice())
    using (var canvasBitmap = CanvasBitmap.CreateFromSoftwareBitmap(resourceCreator, softwareBitmap))
    using (var canvasRenderTarget = new CanvasRenderTarget(resourceCreator, newWidth, newHeight, canvasBitmap.Dpi))
    using (var drawingSession = canvasRenderTarget.CreateDrawingSession())
    using (var scaleEffect = new ScaleEffect())
    {
        scaleEffect.Source = canvasBitmap;
        scaleEffect.Scale = new System.Numerics.Vector2(newWidth / softwareBitmap.PixelWidth, newHeight / softwareBitmap.PixelHeight);
        drawingSession.DrawImage(scaleEffect);
        drawingSession.Flush();
        return SoftwareBitmap.CreateCopyFromBuffer(canvasRenderTarget.GetPixelBytes().AsBuffer(), BitmapPixelFormat.Bgra8, (int)newWidth, (int)newHeight, BitmapAlphaMode.Premultiplied);
    }
}

おおむね快適に動作するが、特定のサイズ(縦×横)の組み合わせで画像が乱れる問題が見つかったのが問題(割ったり、int でキャストしているところでなんかおかしいのかなぁ)。頑張って直してみようとしたが、自分には無理だった。

BitmapEncoder を用いる(WrietableBitmap 経由)

というわけで、基本に戻ることにした。BitmapEncoder を生成し、SoftwareBitmap を割り当てて、Transform してもらう。http://c5d5e5.asablo.jp/blog/2017/08/08/8642588 で提示されていたサンプルコードをベースに、SoftwareBitmap の拡張メソッドにしてみた。

// http://c5d5e5.asablo.jp/blog/2017/08/08/8642588

public static async Task<SoftwareBitmap> ResizeAsync(this SoftwareBitmap source, float newWidth, float newHeight)
{
    if (source == null) return null;

    using (var memory = new InMemoryRandomAccessStream())
    {
        // BitmapEncoder を用いメモリ上で source をリサイズ
        var id = BitmapEncoder.PngEncoderId;
        BitmapEncoder encoder = await BitmapEncoder.CreateAsync(id, memory);
        encoder.BitmapTransform.ScaledHeight = (uint)newHeight;
        encoder.BitmapTransform.ScaledWidth = (uint)newWidth;
        encoder.BitmapTransform.InterpolationMode = BitmapInterpolationMode.Fant;
        encoder.SetSoftwareBitmap(source);
        await encoder.FlushAsync();

        // リサイズしたメモリを WriteableBitmap に複写
        var writeableBitmap = new WriteableBitmap((int)newWidth, (int)newHeight);
        await writeableBitmap.SetSourceAsync(memory);

        // dest(XAML の Image コントロール互換)を作成し、WriteableBitmap から複写
        var dest = new SoftwareBitmap(BitmapPixelFormat.Bgra8, (int)newWidth, (int)newHeight, BitmapAlphaMode.Premultiplied);
        dest.CopyFromBuffer(writeableBitmap.PixelBuffer);

        return dest;
    }
}

Encoder から SoftwareBitmap を得るのに WrietableBitmap を経由しているのがあまりしっくりこないけど、こっちは問題なく動作した。

BitmapEncoder + BitmapDecorder

他人の力ばかり借りるのも何なので、自分でも考えてみたのはこちら。基本的にはさっきのやり方と変わらないけれど、WrietableBitmap ではなく、BitmapDecorder を利用している。

public static async Task<SoftwareBitmap> ResizeAsync2(this SoftwareBitmap source, float newWidth, float newHeight)
{
    if (source == null) return null;

    using (var memory = new InMemoryRandomAccessStream())
    {
        var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, memory);
        encoder.SetSoftwareBitmap(source);
        await encoder.FlushAsync();

        var decoder = await BitmapDecoder.CreateAsync(memory);

        var transform = new BitmapTransform()
        {
            ScaledHeight = (uint)newHeight,
            ScaledWidth = (uint)newWidth,
            InterpolationMode = BitmapInterpolationMode.Fant,
        };

        var dest = await decoder.GetSoftwareBitmapAsync(
            BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied, 
            transform, 
            ExifOrientationMode.IgnoreExifOrientation, ColorManagementMode.DoNotColorManage
        );

        return dest;
    }
}

これも問題なく動いたが、どうも WrietableBitmap バージョンと比べると動作が遅い。Encoder/Decorder の生成にかなりコストがかかるようだ。となると、そもそも Encoder/Decorder は毎回生成せず、使いまわしたほうが良いのかもしれない(拡張メソッドにするのもあまりよくない、もしくは拡張メソッドに Encoder を渡すようにする)。

とりあえず、これで基本的な使い方はわかったような気がするので、画像の回転などは簡単に作れそう(Transform するだけ)。作るうちにペン対応なんかもやりだして、なかなか完成しないけれど、開発中のアプリはなかなかよく動いており、「ブログ、また書こうかな」っていう気がわいてきた。