酢ろぐ!

カレーが嫌いなスマートフォンアプリプログラマのブログ。

Windowsストアアプリで画像処理をおこなう

同じC#というプログラミング言語を使用しているのですが、WinForms、Windows Phone、Windowsストアアプリと異なるプラットフォーム毎に同じ処理を書いているので、アルゴリズムは一緒だけど実装方法が異なるというとても残念なことを繰り返しています。

Instagramのような格好良いエフェクトを実装するには、単一の画像処理のみで済むことはあまりありません。いくつもの処理を重ねて初めて格好良いエフェクトになります。例えば、トイカメラ風の味のあるエフェクトの場合、彩度とコントラストを強めにして、周辺部を暗く落としレンズの周辺光量不足を表現します。

何度も画像を処理させるのに、UIスレッドでなければ実行できないWriteableBitmapの生成をわざわざおこなうのはあまり賢い方法ではありません。ピクデルデータであるbyte配列とその幅と高さをパラメータに取るEffectメソッドをinterfaceで定義し、それぞれ専用のエフェクトクラスにて実装をおこなう形にしたいと思います。

ネガティブ(ネガポジ反転)処理を実装する

本記事では、Windowsストアアプリでの画像処理の例として、ネガティブ(ネガポジ反転)処理を実装します。

IEffect.csでのinterfaceの定義

さて最初にIEffect.csにてinterfaceの定義をおこないます。

// IEffect.cs

using System;

namespace Softbuild.Media.Effects
{
    public interface IEffect
    {
        byte[] Effect(int width, int height, byte[] source);
    }
}

IEffect.csの実装をおこなう専用のエフェクトクラスの実装

ネガティブ処理は、ネガポジ反転処理と呼ばれたりもします。

最近はデジタルカメラが主流となってしまったのでカメラフィルムのことはあまりご存知でないかもしれませんが、普通フィルムといえば色やコントラストが補正可能な下図のようなネガフィルムを指していました。ネガフィルムは撮影したモノの色調が反転した状態となっており、プリントの際には専用の印画紙を用いて更に色調を反転させていました。

撮影した写真を画像処理によって、ネガフィルムのように色調を反転させることを「ネガティブ処理」といいます。処理としては非常にシンプルで色調を反転させるには、255からピクセルの各RGB値を引くだけです。

// NegativeEffect.cs.cs

using System;

namespace Softbuild.Media.Effects
{
    /// <summary>
    /// ネガティブ処理をおこなうクラス
    /// </summary>
    public class NegativeEffect : IEffect
    {
        public byte[] Effect(int width, int height, byte[] source)
        {
            int pixelCount = width * height;
            var dest = new byte[source language=".Length"][/source];

            for (int i = 0; i < pixelCount; i++)
            {
                var index = i * 4;

                var b = 255 - source[index + 0];
                var g = 255 - source[index + 1];
                var r = 255 - source[index + 2];
                var a = source[index + 3];

                dest[index + 0] = (byte)Math.Min(255, Math.Max(0, b));
                dest[index + 1] = (byte)Math.Min(255, Math.Max(0, g));
                dest[index + 2] = (byte)Math.Min(255, Math.Max(0, r));
                dest[index + 3] = a;
            }

            return dest;
        }
    }
}

IRandomAccessStream型のストリームからWriteableBitmapオブジェクトへ変換する」にて紹介したWriteableBitmap用の拡張メソッド集である「WriteableBitmapExtensions」で、エフェクトを掛けた後の画像を取得できるようにしましょう。

WriteableBitmapオブジェクトからピクセルデータを取得し、ピクセルデータに対して各種専用のエフェクト処理を実行します。次に加工後のピクセルデータからWriteableBitmapオブジェクトを生成するには、WriteableBitmapExtensions.FromArrayメソッドを使用します。

// WriteableBitmapExtensions.cs

using System;
using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Softbuild.Media.Effects;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Xaml.Media.Imaging;

namespace Softbuild.Media
{
    static public class WriteableBitmapExtensions
    {
        〜〜 FromArrayメソッド等の記載 省略 〜〜
    
        /// <summary>
        /// パラメータ無しの画像処理をおこなう
        /// </summary>
        /// <param name="bitmap">元になるWriteableBitampオブジェクト</param>
        /// <param name="effecter">エフェクト処理オブジェクト</param>
        /// <returns>WriteableBitmapオブジェクト</returns>
        private static WriteableBitmap Effect(WriteableBitmap bmp, IEffect effecter)
        {
            // WriteableBitampのピクセルデータをバイト配列に変換する
            var srcPixels = bmp.PixelBuffer.ToArray();

            // パラメータ無しの画像処理をおこなう
            var dstPixels = effecter.Effect(bmp.PixelWidth, bmp.PixelHeight, srcPixels);

            // バイト配列からピクセルを作成する
            return WriteableBitmapExtensions.FromArray(bmp.PixelWidth, bmp.PixelHeight, dstPixels);
        }

IEffectを実装したエフェクトクラスを実装し、元画像のWriteableBitmapオブジェクトと実装したエフェクトクラスのオブジェクトをパラメータにするだけで、以下のように非常にシンプルにエフェクト処理を実行するメソッドを作成することができました。

        /// <summary>
        /// ネガポジ反転処理をしたWriteableBitampオブジェクトを返す
        /// </summary>
        /// <param name="bitmap">元になるWriteableBitampオブジェクト</param>
        /// <returns>WriteableBitampオブジェクト</returns>
        public static WriteableBitmap EffectNegative(this WriteableBitmap bmp)
        {
            return Effect(bmp, new NegativeEffect());
        }
    }
}

上記のコードを実行すると、下図のようにネガティブ変換処理した画像を得ることができます。左が元画像、右がネガティブ変換処理をした画像になります

f:id:ch3cooh393:20150202234018p:plain

グレースケール変換処理を実装する

グレースケール処理は、カラー画像をグレースケール画像へ変換します。グレースケール画像は白から黒にかけての256段階の濃淡で表現されており、白か黒しかない2値化された画像と比較すると表現力があります。

カラー画像をグレースケール画像へ変換するための計算方法はいくつかあります。各要素の最大値と最小値を足して2で割る中間値法や、テレビ放送規格であるNTSC係数による加重平均法が有名です。

加重平均法はピクセルのRGB値の各要素のうち、人間の目には緑が与える印象が強いため、他のR成分とB成分よりもG成分を強調してみましょう。

0.298912 * R + 0.586611 * G + 0.114478 * B

ここでは最もシンプルな単純平均法で輝度を求めます。1ピクセルのR成分、G成分、B成分の要素をすべて足し合わせて3で割り平均値を取ります。

// GrayscaleEffect.cs

using System;

namespace Softbuild.Media.Effects
{
    public class GrayscaleEffect : IEffect
    {
        public byte[] Effect(int width, int height, byte[] source)
        {
            int pixelCount = width * height;
            var dest = new byte[source language=".Length"][/source];

            for (int i = 0; i < pixelCount; i++)
            {
                var index = i * 4;

                // 単純平均法で輝度を求める
                var sum = source[index + 0] + source[index + 1] + source[index + 2];
                var y = (double)sum / 3;

                dest[index + 0] = (byte)Math.Min(255, Math.Max(0, y));
                dest[index + 1] = (byte)Math.Min(255, Math.Max(0, y));
                dest[index + 2] = (byte)Math.Min(255, Math.Max(0, y));
                dest[index + 3] = source[index + 3];
            }

            return dest;
        }
    }
}

上記のコードを実行すると、下図のようにグレースケール変換処理した画像を得ることができます。左が元画像、右がグレースケール変換処理をした画像になります

f:id:ch3cooh393:20150202234314p:plain

セピア調変換処理を実装する

セピア調変換は、カラー画像をセピア調画像へ変換します。セピア調画像は経年劣化したモノクロ写真に見られる色合いで、劣化に伴い明るい部分は黄色っぽくなり、暗い部分は赤褐色っぽくなります。

元々のセピアというのはイカ墨のことで、主にイカ墨を原料にした顔料を指します。昔の写真の代名詞で使われることもあります。日本工業規 格(JIS)にでも定義されており、RGBのそれぞれの値に直すと (R:107, G:74, B:43) です。

R成分を1として基準として考えると、ザックリと以下のような比率となります。

1 * R + 0.7 * G + 0.4 * B

グレースケール変換処理と同じ要領でセピア調変換の処理を実装してみましょう。

using System;

namespace Softbuild.Media.Effects
{
    public class SepiaEffect : IEffect
    {
        public byte[] Effect(int width, int height, byte[] source)
        {
            int pixelCount = width * height;
            var dest = new byte[source language=".Length"][/source];

            for (int i = 0; i < pixelCount; i++)
            {
                var index = i * 4;

                // 単純平均法で輝度を求める
                var sum = source[index + 0] + source[index + 1] + source[index + 2];
                var y = (double)sum / 3;

                // セピア調にする
                var b = y * 0.4;
                var g = y * 0.7;
                var r = y;
                var a = source[index + 3];

                dest[index + 0] = (byte)Math.Min(255, Math.Max(0, b));
                dest[index + 1] = (byte)Math.Min(255, Math.Max(0, g));
                dest[index + 2] = (byte)Math.Min(255, Math.Max(0, r));
                dest[index + 3] = a;
            }

            return dest;
        }
    }
}

上記のコードを実行すると、下図のようにセピア調処理した画像を得ることができます。左が元画像、右がセピア調処理をした画像になります。

f:id:ch3cooh393:20150202234906p:plain

彩度を調整する

彩度というのは鮮やかさのことです。

彩度(Saturation)は、基本的に0.0~1.0で表現されます。HSV色空間における彩度は、全体の明るさに対する純色の割合を示します。彩度が0の時は、色相(Hue)は定義されませんので、色は無色もしくは灰色になります。

using System;

namespace Softbuild.Media.Effects
{
    /// <summary>
    /// 彩度調整処理をおこなうクラス
    /// </summary>
    public class SaturationEffect : IEffect
    {
        /// <summary>
        /// 調整する彩度の倍率
        /// </summary>
        private double Saturation { get; set; }

        /// <summary>
        /// SaturationEffect クラスの新しいインスタンスを初期化します。
        /// </summary>
        /// <param name="saturation">彩度を表現する(0.0~1.0 標準:0.5)</param>
        public SaturationEffect(double saturation)
        {
            Saturation = saturation * 2;
        }

        /// <summary>
        /// 彩度調整処理をおこなう
        /// </summary>
        /// <param name="width">ビットマップの幅</param>
        /// <param name="height">ビットマップの高さ</param>
        /// <param name="source">処理前のピクセルデータ</param>
        /// <returns>処理後のピクセルデータ</returns>
        public byte[] Effect(int width, int height, byte[] source)
        {
            int pixelCount = width * height;
            var dest = new byte[source language=".Length"][/source];

            for (int i = 0; i < pixelCount; i++)
            {
                var index = i * 4;

                // 処理前のピクセルの各ARGB要素を取得する
                double b = source[index + 0];
                double g = source[index + 1];
                double r = source[index + 2];
                double a = source[index + 3];

                // 単純平均法で輝度を求める
                double y = (b + g + r) / 3;

                // 輝度をベースに彩度を求める
                b = y + Saturation * (b - y);
                g = y + Saturation * (g - y);
                r = y + Saturation * (r - y);

                // 処理後のバッファへピクセル情報を保存する
                dest[index + 0] = (byte)Math.Min(255, Math.Max(0, b));
                dest[index + 1] = (byte)Math.Min(255, Math.Max(0, g));
                dest[index + 2] = (byte)Math.Min(255, Math.Max(0, r));
                dest[index + 3] = source[index + 3];
            }

            return dest;
        }
    }
}

上記のコードを実行すると、下図のように彩度調整処理した画像を得ることができます。左が元画像、右が彩度調整処理をした画像になります

f:id:ch3cooh393:20150202235418p:plain

コントラストを調整する

コントラスト調整では明暗の強弱をつけます。コントラストはもっとも明るい輝度ともっとも暗い輝度との差のことで、上げすぎると明暗がはっきりする代わりに色の差がなくなり、下げすぎると明暗がなくなり中間色になります。

以下のサンプルコードでは、高速化のためにlut(look up table)を作っています。

// ConstrastEffect.cs

using System;

namespace Softbuild.Media.Effects
{
    public class ContrastEffect : IEffect
    {
        private byte[] ContrastTable { get; set; }
        private double Contrast { get; set; }

        public ContrastEffect(double contrast)
        {
            Contrast = contrast * 2;

            // コントラストの変換テーブルを作成する
            ContrastTable = new byte[256];
            for (int i = 0; i < 256; i++)
            {
                double value = ((double)i - 0.5) * Contrast + 0.5;
                ContrastTable[i] = (byte)Math.Min(255, Math.Max(0, value));
            }
        }

コンストラクタ引数のcontrastが大きくなればなるほど差は大きくなります。実際の処理の中では、lutを元に入力画像から出力画像への変更をおこなっています。

        public byte[] Effect(int width, int height, byte[] source)
        {
            int pixelCount = width * height;
            var dest = new byte[source.Length];

            for (int i = 0; i < pixelCount; i++)
            {
                var index = i * 4;

                var b = source[index + 0];
                var g = source[index + 1];
                var r = source[index + 2];
                var a = source[index + 3];

                // 変換テーブル
                b = ContrastTable[b];
                g = ContrastTable[g];
                r = ContrastTable[r];

                dest[index + 0] = b;
                dest[index + 1] = g;
                dest[index + 2] = r;
                dest[index + 3] = a;
            }

            return dest;
        }
    }
}

上記のコードを実行すると、下図のようにコントラスト調整処理した画像を得ることができます。左が元画像、右がコントラスト調整処理をした画像になります

f:id:ch3cooh393:20150202235136p:plain

応用編

関連記事

WindowsランタイムAPI(Windows Runtime API, WinRT API)を使ってアプリ開発する際に逆引きとしてお使いください。