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]は配列のコピヌを䜜成したせん。

単䜓テストは、コンパむル時に、 ImageがisDirectViewむンタヌフェむスの条件を実際に満たしおいるこずを確認したす。
 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がないため、 yExprがxを䜿甚しない堎合にのみ正垞にむンストヌルできたす。 匏__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; } } 


parallelがstd.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に感謝したす。

Source: https://habr.com/ru/post/J218429/


All Articles