Dでの機能的画像処理

画像



最近、Dライブラリのグラフィックパッケージのリワークを完了しました。 std.algorithmおよびstd.rangeモジュールに触発されて、次の目標を達成しようとしました。





最初のバージョンから、画像処理パッケージのすべてのコンポーネントは、色の種類によってパラメーター化されました。 これは、グラフィックライブラリを実装する標準的な方法ではありません。ほとんどの場合、OOPインターフェイスを介して特定の種類の画像色を抽象化するか、すべての画像を単一ピクセル形式に変換します。 ただし、ほとんどの場合、これはメモリと時間の無駄です。通常、開発者は、画像データがユーザーによって入力されるアプリケーション(グラフエディターなど)を除き、画像がどの特定の形式で表示されるかを事前に知っています。 代わりに、私のライブラリは、すべての画像タイプを色のパラメータータイプを持つテンプレートとして宣言しています。



ライブラリでの作業の結果に非常に満足しているので、この投稿で興味深い点を共有したいと思います。










ライブラリはviewの定義で始まります

///  ,    width, height ///       ///    enum isView(T) = is(typeof(T.init.w) : size_t) && // width is(typeof(T.init.h) : size_t) && // height is(typeof(T.init[0, 0]) ); // color information
      
      







静的インターフェイスを宣言するこのメソッドは、std.range、たとえばisInputRangeで使用されるメソッドと同じです 。 OOPに意味が似ているインターフェイスを宣言する代わりに、Dの静的インターフェイスは、特定の機能の実装テスト( ダックタイピング )を使用して条件付きで決定されます 。 型が実装する操作がエラーなしでコンパイルされるか、特定の型を持つ場合、検証は成功します。 通常、これにはIsExpressionまたはtrait コンパイルが使用されます。



std.range.ElementTypeと同様に、ピクセルの色のビューで使用されるタイプを取得するテンプレート定義します。

 ///     view alias ViewColor(T) = typeof(T.init[0, 0]);
      
      







次に、ビューのいくつかの専門分野を定義します。

コード
 /// Views    ///    enum isWritableView(T) = isView!T && is(typeof(T.init[0, 0] = ViewColor!T.init)); ///  view   ///    . ///  views  "direct views" enum isDirectView(T) = isView!T && is(typeof(T.init.scanline(0)) : ViewColor!T[]);
      
      









繰り返しますが、これはisForwardRangeの定義に似ています。この特殊化に固有のいくつかの追加機能と同様に、型がすべての基本機能を実装していることを確認します。



ピクセルへの直接アクセスの実装は、 スキャンラインの直接ビューを介して決定できるため、これを実装するテンプレートミックスインを宣言します。

コード
 /// ,    view ///     direct view mixin template DirectView() { alias COLOR = typeof(scanline(0)[0]); ///   view[x, y] ref COLOR opIndex(int x, int y) { return scanline(y)[x]; } ///   view[x, y] = c COLOR opIndexAssign(COLOR value, int x, int y) { return scanline(y)[x] = value; } }
      
      









たとえば、メモリ内の画像を記述する画像テンプレートを定義します。

コード
 ///    ///      struct Image(COLOR) { int w, h; COLOR[] pixels; ///     y COLOR[] scanline(int y) { assert(y>=0 && y<h); return pixels[w*y..w*(y+1)]; } mixin DirectView; this(int w, int h) { size(w, h); } ///     void size(int w, int h) { this.w = w; this.h = h; if (pixels.length < w*h) pixels.length = w*h; } }
      
      









Dの配列は 、ポインターと長さを介し実装されます(また、拡張または接着された場合にのみメモリを要求します)。したがって、式ピクセル[w * y ... w *(y + 1)]は配列のコピーを作成しません。



単体テストは、コンパイル時に、 ImageisDirectViewインターフェイスの条件を実際に満たしていることを確認します。

 unittest { static assert(isDirectView!(Image!ubyte)); }
      
      












それでは、このモデルで何ができるでしょうか?



まず、実際にピクセルの配列を参照せずに、必要に応じて計算する画像を定義できます。

コード
 ///  view,    ///       template procedural(alias formula) { alias fun = binaryFun!(formula, "x", "y"); alias COLOR = typeof(fun(0, 0)); auto procedural(int w, int h) { struct Procedural { int w, h; auto ref COLOR opIndex(int x, int y) { return fun(x, y); } } return Procedural(w, h); } }
      
      









この同じ名前のテンプレートは、 std.functional.binaryFunを使用して、文字列( 混合されます)を述語またはデリゲート(ラムダ)に変換します。 この関数は戻り型autoを持ち、この関数内で宣言された構造体を返すため、 手続き Voldemort型の例です。



1つの色で塗りつぶされた手続き型画像の最も単純な例:

 ///  view,   ///     auto solid(COLOR)(COLOR c, int w, int h) { return procedural!((x, y) => c)(w, h); }
      
      







返されるビューのカラータイプがパラメータータイプcからどのように推測されるかに注意してください。そのため、完全修飾名がない場合でも、 solid(RGB(1、2、3)、10、10)はRGBピクセルからビューを返します。








このモデルで表現できるもう1つのことは、さまざまな方法で他のビューを変換するビューを作成することです。 一般的に使用されるコードの別のテンプレートミックスインを定義します。

コード
 /// ,   view   ///  view,    ///  mixin template Warp(V) { V src; auto ref ViewColor!V opIndex(int x, int y) { warp(x, y); return src[x, y]; } static if (isWritableView!V) ViewColor!V opIndexAssign(ViewColor!V value, int x, int y) { warp(x, y); return src[x, y] = value; } }
      
      









静的なif(isWritableView!V)行を見てみましょうこれは、 ビュー[x、y] = cステートメントが、基礎となるビューでサポートされている場合にのみ定義する必要があることを示しています。 基になるビューも変更できる場合にのみ、ラップされた合計ビューが変更可能になります。



この関数を使用して、別のビューの長方形部分を表すトリミングビューを定義できます。

コード
 ///  view    auto crop(V)(auto ref V src, int x0, int y0, int x1, int y1) if (isView!V) { assert( 0 <= x0 && 0 <= y0); assert(x0 < x1 && y0 < y1); assert(x1 <= src.w && y1 <= src.h); static struct Crop { mixin Warp!V; int x0, y0, x1, y1; @property int w() { return x1-x0; } @property int h() { return y1-y0; } void warp(ref int x, ref int y) { x += x0; y += y0; } static if (isDirectView!V) ViewColor!V[] scanline(int y) { return src.scanline(y0+y)[x0..x1]; } } static assert(isDirectView!V == isDirectView!Crop); return Crop(src, x0, y0, x1, y1); }
      
      









if(isView!V) テンプレート制約は、最初の引数がisViewインターフェイスの条件に一致することを確認します。



前と同じように、 トリミングisDirectViewを使用して、基になる画像がピクセルをサポートしている場合、ピクセルに直接アクセスします。 直接ピクセルアクセスは、一度に多数のピクセルを操作する場合に役立ちます。これにより、シーケンシャルアクセスと比較してパフォーマンスが向上します。 たとえば、あるイメージを別のイメージにコピーする場合、各ピクセルの値を個別に割り当てるよりも、スライスコピー(Dのタイプセーフなmemcpy置換)を使用する方がはるかに高速です。

コード
 ///   view  . /// Views    . void blitTo(SRC, DST)(auto ref SRC src, auto ref DST dst) if (isView!SRC && isWritableView!DST) { assert(src.w == dst.w && src.h == dst.h, "View size mismatch"); foreach (y; 0..src.h) { static if (isDirectView!SRC && isDirectView!DST) dst.scanline(y)[] = src.scanline(y)[]; else { foreach (x; 0..src.w) dst[x, y] = src[x, y]; } } }
      
      









cropと同じ考え方を使用して、最近傍アルゴリズムに従って別のビューをタイル表示またはスケーリングするビューを実装できます(より複雑なスケーリングアルゴリズムは、命令型スタイルでより適切に実装されます)。 コードはcropに非常に似ているため、ここには含めません。



cropがソースを通常の引数として使用する場合でも、この関数などの使用目的は、元のビューのメソッドであるかのようになります: someView.nearestNeighbor(100、100).tile(1000、1000).crop(50、50、950、 950) 。 この可能性は、 「Uniform Function Call Syntax」 (または単にUFCS)と呼ばれる言語の機能によります。これにより、 fun(a、b ...)の代わりにa.fun(b ...)を記述できます。 この機能の主な利点は、 チェーンを整理する機能( a.fun1()。Fun2()。 Fun3(fun2(fun1(a)) )の代わりにFun3( )であり、これはPhobosおよびこのパッケージで最大限に使用されます。








ビューのサイズが変わらない単純な変換の場合、各ピクセル座標へのユーザー指定の式の適用を簡素化する補助関数を定義できます。

コード
 ///  view    ///    template warp(string xExpr, string yExpr) { auto warp(V)(auto ref V src) if (isView!V) { static struct Warped { mixin Warp!V; @property int w() { return src.w; } @property int h() { return src.h; } void warp(ref int x, ref int y) { auto nx = mixin(xExpr); auto ny = mixin(yExpr); x = nx; y = ny; } private void testWarpY()() { int y; y = mixin(yExpr); } ///  x     y   ///  x,      scanlines. static if (xExpr == "x" && __traits(compiles, testWarpY()) && isDirectView!V) ViewColor!V[] scanline(int y) { return src.scanline(mixin(yExpr)); } } return Warped(src); } }
      
      









warpは渡された式を検証するためにトリッキーな方法を使用します。 ただし、 testWarpY関数テンプレート引数としてゼロのテンプレートとして宣言されます。 これにより、コンパイラーは、使用されるまでこの関数の本体のセマンティック分析を行わなくなります。 また、スコープにxがないため、 yExprxを使用しない場合にのみ正常にインストールできます。 式__traits(コンパイル、testWrapY())はそれをチェックするだけです。 これにより、安全に実行できると確信している場合にのみ、直接ビュースキャンラインを定義できます。 例:

コード
 ///  view    x alias hflip = warp!(q{wx-1}, q{y}); ///  view    y alias vflip = warp!(q{x}, q{hy-1}); ///  view    x  y alias flip = warp!(q{wx-1}, q{hy-1});
      
      









q {...}構文は、文字列定数を定義する便利な方法です。 このエントリは通常Dコードに使用され、その後Dコードがどこかで混合されます。 式は混合場所のすべての文字にアクセスできます。この場合、これはWrapped構造のワープ関数とtestWarpYメソッドです。



vflipスキャンラインメソッドの宣言に必要な最初の2つの条件を満たしているため、 someViewがoneの場合、 someView.vflip()は直接ビューになります。 そして、これはvflip広告の明示的な検証なしで達成されました。



使用される抽象化は動的な多態性に依存しないため、コンパイラはすべての変換レイヤーの呼び出しを自由に組み込むことができます。 画像を2回反転しても操作は生成されず、実際にはi [ 5、5 ]およびi.hflip()。Hflip()[5、5]は同じマシンコードを生成します。 より優れたバックエンドを備えたDコンパイラーは、さらに積極的な最適化を実行できます:たとえば、X軸とY軸を反転するflipXY関数、およびsrc.flipXY ()としてrotateCW(画像を反時計回りに90度回転)を定義する場合、その後、最適化中に4つの正常なrotateCW呼び出しがカットアウトされます。








ピクセル自体の操作に移りましょう。 std.algorithmの主な関数はmapで 、別の範囲に式を遅延的に適用する範囲を返します。 カラーマップでは、このアイデアを色に使用しています。

コード
 ///  view,    ///     view. template colorMap(alias pred) { alias fun = unaryFun!(pred, false, "c"); auto colorMap(V)(auto ref V src) if (isView!V) { alias OLDCOLOR = ViewColor!V; alias NEWCOLOR = typeof(fun(OLDCOLOR.init)); struct Map { V src; @property int w() { return src.w; } @property int h() { return src.h; } auto ref NEWCOLOR opIndex(int x, int y) { return fun(src[x, y]); } } return Map(src); } }
      
      









colorMapを使用して、画像の色を反転する関数を定義するのは簡単です:

 alias invert = colorMap!q{~c};
      
      







colorMapでは、ソースと結果の色タイプを一致させる必要はありません。 これにより、色変換に使用できます: read( "image.bmp")。ParseBMP!RGB()。ColorMap!(C => BGRX(cb、cg、cr))は、RGBビットマップをBGRXビューとして返します。






画像処理は、多くの場合、並列化に適しています。 std.parallelismは、並列画像処理のタスクを簡単にするのに役立ちます。

コード
 ///  view    ///  fun    /// . ///   , ///    , ///  vjoin  vjoiner. template parallel(alias fun) { auto parallel(V)(auto ref V src, size_t chunkSize = 0) if (isView!V) { auto processSegment(R)(R rows) { auto y0 = rows[0]; auto y1 = y0 + rows.length; auto segment = src.crop(0, y0, src.w, y1); return fun(segment); } import std.range : iota, chunks; import std.parallelism : taskPool, parallel; if (!chunkSize) chunkSize = taskPool.defaultWorkUnitSize(src.h); auto range = src.h.iota.chunks(chunkSize); alias Result = typeof(processSegment(range.front)); auto result = new Result[range.length]; foreach (n; range.length.iota.parallel(1)) result[n] = processSegment(range[n]); return result; } }
      
      







parallelstd.parallelismに存在する関数と名前を共有している場合でも、異なるシグネチャを持ち、異なるタイプで機能するため、競合はありません。



同時に、 image.process()image.parallel!(Segment => segment.process())。Vjoin()に置き換えることで、操作を複数のスレッドに分割できます。








実際の例:










テンプレートアプローチは、パフォーマンスの大幅な向上を約束します。 簡単なベンチマークとして、このプログラムはディレクトリ内のすべての画像のスケールを25%削減します。

コード
 import std.file; import std.path; import std.stdio; import ae.utils.graphics.color; import ae.utils.graphics.gamma; import ae.utils.graphics.image; void main() { alias BGR16 = Color!(ushort, "b", "g", "r"); auto gamma = GammaRamp!(ushort, ubyte)(2.2); foreach (de; dirEntries("input", "*.bmp", SpanMode.shallow)) { static Image!BGR scratch1; static Image!BGR16 scratch2, scratch3; de .read .parseBMP!BGR(scratch1) .parallel!(segment => segment .pix2lum(gamma) .copy(scratch2) .downscale!4(scratch3) .lum2pix(gamma) .copy )(4) .vjoin .toBMP .toFile(buildPath("output-d", de.baseName)); } }
      
      









同等のImageMagickコマンドを使用した結果を比較しました。

 convert \ input/*.bmp \ -depth 16 \ -gamma 0.454545 \ -filter box \ -resize 25% \ -gamma 2.2 \ -depth 8 \ output-im/%02d.bmp
      
      







バージョンDは4〜5倍高速に実行されます。 もちろん、これは不公平な比較です。両方が16ビット色深度、ガンマ補正、マルチスレッドを使用し、同じアーキテクチャ向けに最適化されている場合でも、Dプログラムにはこのタスク用に特別に最適化されたコードが含まれています。 さまざまなJITテクニックを考慮しないと、パッケージを汎用画像処理ライブラリと比較できません。








グラフィックパッケージはGitHubで入手できます 。 この記事に貢献してくれたDavid Ellsworthに感謝します。



All Articles