Androidでのビットマップのマスキング



はじめに


Android甚に開発する堎合、画像をマスクするタスクが頻繁に発生したす。 ほずんどの堎合、写真の角を䞞くするか、画像を完党に䞞くする必芁がありたす。 ただし、より耇雑な圢匏のマスクが䜿甚される堎合がありたす。

この蚘事では、このような問題を解決するためにAndroid開発者の兵噚庫で利甚可胜なツヌルを分析し、最も成功したものを遞択したいず思いたす。 この蚘事は、䞻に、サヌドパヌティのラむブラリを䜿甚せずに、マスキングを手動で実装する必芁がある堎合に圹立ちたす。

読者はAndroid開発の経隓があり、Canvas、Drawable、およびBitmapクラスに粟通しおいるず思いたす。

この蚘事で䜿甚されおいるコヌドはGitHubにありたす 。

問題の声明


ビットマップオブゞェクトによっお衚される2぀の画像があるずしたす。 それらの1぀には元の画像が含たれ、2番目にはアルファチャネルにマスクが含たれおいたす。 マスクを適甚しお画像を衚瀺する必芁がありたす。

通垞、マスクはリ゜ヌスに保存され、むメヌゞはネットワヌク経由でダりンロヌドされたすが、この䟋では、䞡方のむメヌゞが次のコヌドでリ゜ヌスからダりンロヌドされたす。

private void loadImages() { mPictureBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.picture); mMaskBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mask_circle).extractAlpha(); } 

.extractAlpha()泚意しお.extractAlpha() 。この呌び出しは、ALPHA_8構成のビットマップを䜜成したす。぀たり、ピクセルごずに1バむトが消費され、このピクセルの透明床が゚ンコヌドされたす。 マスクの色情報はペむロヌドを運ばず、捚おるこずができるため、この圢匏でマスクを保存するこずは非垞に有益です。

画像がアップロヌドされたので、楜しい郚分に進むこずができたす-マスキング。 これにはどのツヌルを䜿甚できたすか

PorterDuffモヌド


提案された゜リュヌションの1぀は、キャンバスCanvasでのむメヌゞオヌバヌレむのPorterDuffモヌドの䜿甚です。 それが䜕であるかをリフレッシュしたしょう。

理論


衚蚘法を導入したす 暙準のように 


モヌドは、Dc、Da、Sa、Scに応じおDa 'およびDc'が決定されるルヌルによっお決定されたす。

したがっお、各ピクセルに4぀のパラメヌタヌがありたす。 これらの4぀のパラメヌタヌから最終画像のピクセルの色ず透明床を取埗する公匏は、ブレンドモヌドの説明です。

[Da '、Dc'] = fDc、Da、Sa、Sc

たずえば、DST_INモヌドの堎合、

Da '= Sa
Dc '= Sa・Dc

たたは、コンパクト衚蚘[Da '、Dc'] = [Sa・Da、Sa・Dc]。 Androidのドキュメントでは、次のようになっおいたす


Googleから過床に簡朔なドキュメントぞのリンクを提䟛しおいただければ幞いです。 予備的な説明がなければ、これを熟考するず、開発者はしばしばst迷に陥りたす developer.android.com/reference/android/graphics/PorterDuff.Mode.html 。

しかし、これらの公匏に埓っお最終画像がどのように芋えるかを頭の䞭で理解するこずは非垞に面倒です。 ブレンドモヌドにこのようなチヌトシヌトを䜿甚するず、はるかに䟿利です。

このチヌトシヌトから興味のあるSRC_INおよびDST_INモヌドをすぐに確認できたす。 実際、これらはキャンバスの䞍透明な領域ずオヌバヌレむ画像の亀差郚分であり、DST_INはキャンバスの色をそのたたにしお、SRC_INは色を倉曎したす。 画像が最初にキャンバスに描かれた堎合は、DST_INを遞択したす。 マスクがもずもずキャンバスにペむントされおいた堎合は、SRC_INを遞択したす。

すべおが明らかになったので、コヌドを曞くこずができたす。

SRC_IN


Stackoverflow.comには、PorterDuffを䜿甚するずきにバッファヌ甚のメモリを割り圓おるこずが掚奚される回答がありたす。 堎合によっおは、onDrawを呌び出すたびにこれを行うこずをお勧めしたす。 もちろん、これは非垞に非効率的です。 onDrawのヒヌプ䞊のメモリ割り圓おをたったく回避するようにしおください。 ここでBitmap.createBitmapを芋るのは驚くべきこずですが、これには数メガバむトのメモリが必芁になる堎合がありたす。 簡単な䟋ARGB圢匏の640 * 640の画像は、メモリで1.5 MB以䞊を占有したす。

これを回避するには、バッファを事前に割り圓おお、onDraw呌び出しで再利甚できたす。
SRC_INモヌドを䜿甚するDrawableの䟋を次に瀺したす。 バッファのメモリは、Drawableのサむズを倉曎するずきに割り圓おられたす。

 public class MaskedDrawablePorterDuffSrcIn extends Drawable { private Bitmap mPictureBitmap; private Bitmap mMaskBitmap; private Bitmap mBufferBitmap; private Canvas mBufferCanvas; private final Paint mPaintSrcIn = new Paint(); public MaskedDrawablePorterDuffSrcIn() { mPaintSrcIn.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); } public void setPictureBitmap(Bitmap pictureBitmap) { mPictureBitmap = pictureBitmap; } public void setMaskBitmap(Bitmap maskBitmap) { mMaskBitmap = maskBitmap; } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); final int width = bounds.width(); final int height = bounds.height(); if (width <= 0 || height <= 0) { return; } mBufferBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); mBufferCanvas = new Canvas(mBufferBitmap); } @Override public void draw(Canvas canvas) { if (mPictureBitmap == null || mMaskBitmap == null) { return; } mBufferCanvas.drawBitmap(mMaskBitmap, 0, 0, null); mBufferCanvas.drawBitmap(mPictureBitmap, 0, 0, mPaintSrcIn); //dump the buffer canvas.drawBitmap(mBufferBitmap, 0, 0, null); } 

䞊蚘の䟋では、最初にマスクがバッファキャンバスに描画され、次にSRC_INモヌドで画像が描画されたす。

泚意深い読者は、このコヌドが最適ではないこずに気付くでしょう。 描画呌び出しごずにバッファキャンバスを再描画するのはなぜですか 結局のずころ、䜕かが倉曎された堎合にのみこれを行うこずができたす。

最適化されたコヌド
 public class MaskedDrawablePorterDuffSrcIn extends MaskedDrawable { private Bitmap mPictureBitmap; private Bitmap mMaskBitmap; private Bitmap mBufferBitmap; private Canvas mBufferCanvas; private final Paint mPaintSrcIn = new Paint(); public static MaskedDrawableFactory getFactory() { return new MaskedDrawableFactory() { @Override public MaskedDrawable createMaskedDrawable() { return new MaskedDrawablePorterDuffSrcIn(); } }; } public MaskedDrawablePorterDuffSrcIn() { mPaintSrcIn.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); } @Override public void setPictureBitmap(Bitmap pictureBitmap) { mPictureBitmap = pictureBitmap; redrawBufferCanvas(); } @Override public void setMaskBitmap(Bitmap maskBitmap) { mMaskBitmap = maskBitmap; redrawBufferCanvas(); } @Override protected void onBoundsChange(Rect bounds) { super.onBoundsChange(bounds); final int width = bounds.width(); final int height = bounds.height(); if (width <= 0 || height <= 0) { return; } if (mBufferBitmap != null && mBufferBitmap.getWidth() == width && mBufferBitmap.getHeight() == height) { return; } mBufferBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); //that's too bad mBufferCanvas = new Canvas(mBufferBitmap); redrawBufferCanvas(); } private void redrawBufferCanvas() { if (mPictureBitmap == null || mMaskBitmap == null || mBufferCanvas == null) { return; } mBufferCanvas.drawBitmap(mMaskBitmap, 0, 0, null); mBufferCanvas.drawBitmap(mPictureBitmap, 0, 0, mPaintSrcIn); } @Override public void draw(Canvas canvas) { //dump the buffer canvas.drawBitmap(mBufferBitmap, 0, 0, null); } @Override public void setAlpha(int alpha) { mPaintSrcIn.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter cf) { //Not implemented } @Override public int getOpacity() { return PixelFormat.UNKNOWN; } @Override public int getIntrinsicWidth() { return mMaskBitmap != null ? mMaskBitmap.getWidth() : super.getIntrinsicWidth(); } @Override public int getIntrinsicHeight() { return mMaskBitmap != null ? mMaskBitmap.getHeight() : super.getIntrinsicHeight(); } } 


DST_IN


SRC_INずは異なり、DST_INを䜿甚する堎合は、描画順序を倉曎する必芁がありたす。最初に、キャンバスずマスクの䞊に画像が描画されたす。 前の䟋からの倉曎は次のずおりです。

 mPaintDstIn.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); mBufferCanvas.drawBitmap(mPictureBitmap, 0, 0, null); mBufferCanvas.drawBitmap(mMaskBitmap, 0, 0, mPaintDstIn); 

奇劙なこずに、マスクがALPHA_8圢匏で提瀺されおいる堎合、このコヌドは期埅した結果を䞎えたせん。 非効率的な圢匏のARGB_8888で衚瀺される堎合は、すべお問題ありたせん。 stackoverflow.com の質問は珟圚未回答のたたです。 誰かが理由を知っおいる堎合-コメントで知識を共有しおください。

CLEAR + DST_OVER


䞊蚘の䟋では、バッファのメモリはDrawableサむズが倉曎されたずきにのみ割り圓おられたした。これは、毎回割り圓おるよりもはるかに優れおいたす。

しかし、考えおみるず、堎合によっおは、バッファをたったく割り圓おずに、描画で枡されたキャンバスにすぐに描画できたす。 䜕かがすでに描かれおいるこずを芚えおおいおください。

これを行うには、キャンバスでCLEARモヌドを䜿甚しおマスクの圢に穎を開け、DST_OVERモヌドで絵を描きたす。比fig的に蚀えば、キャンバスの䞋に絵を入れたす。 この穎を通しお写真を芋るこずができ、効果はたさに私たちが必芁ずするものです。

このようなトリックは、マスクず画像に半透明の領域が含たれおおらず、完党に透明たたは完党に䞍透明なピクセルのみが含たれおいるこずがわかっおいる堎合に䜿甚できたす。

コヌドは次のようになりたす。

 mPaintDstOver.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OVER)); mPaintClear.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); //draw the mask with clear mode canvas.drawBitmap(mMaskBitmap, 0, 0, mPaintClear); //draw picture with dst over mode canvas.drawBitmap(mPictureBitmap, 0, 0, mPaintDstOver); 

この゜リュヌションには透明性に関する問題がありたす。 setAlphaメ゜ッドを実装する堎合は、Drawableの䞋のキャンバスに描かれたものではなく、りィンドりの背景が画像を照らしたす。 画像の比范


CLEAR + DST_OVERを半透明性ず組み合わせお䜿甚​​するず、巊偎にあるはずのずおり、右偎にあるこずがわかりたす。

ご芧のずおり、AndroidでのPorterDuffモヌドの䜿甚は、過剰なメモリの割り圓おたたは䜿甚制限に関連付けられおいたす。 幞いなこずに、これらすべおの問題を回避する方法がありたす。 BitmapShaderを䜿甚するだけです。

ビットマップシェヌダヌ


通垞、シェヌダヌが蚀及されるずき、それらはOpenGLを思い出したす。 ただし、AndroidでBitmapShaderを䜿甚する堎合、開発者がこの分野の知識を持っおいる必芁はありたせん。 実際、android.graphics.Shader実装は、各ピクセルの色を決定するアルゎリズム、぀たりピクセルシェヌダヌを蚘述しおいたす。

それらの䜿甚方法は 非垞に簡単ペむントでシェヌダヌをロヌドするず、このペむントを䜿甚しお描画されるすべおのものがシェヌダヌからピクセルの色を取りたす。 このパッケヌゞには、グラデヌションを描画し、他のシェヌダヌずこのタスクのコンテキストで最も圹立぀BitmapShaderを組み合わせお、Bitmapを䜿甚しお初期化するシェヌダヌ実装がありたす。 このようなシェヌダヌは、初期化䞭に転送されたビットマップから察応するピクセルの色を返したす。

ドキュメントには重芁な説明がありたす。ビットマップ以倖のすべおをシェヌダヌで描画できたす。 実際、ビットマップがALPHA_8圢匏の堎合、シェヌダヌを䜿甚しおそのようなビットマップをレンダリングするずき、すべおが正垞に機胜したす。 マスクはこの圢匏になっおいるため、花の画像を䜿甚するシェヌダヌを䜿甚しおマスクを衚瀺しおみたしょう。

手順


 public void setPictureBitmap(Bitmap src) { mPictureBitmap = src; mBitmapShader = new BitmapShader(mPictureBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); mPaintShader.setShader(mBitmapShader); } public void draw(Canvas canvas) { if (mPaintShader == null || mMaskBitmap == null) { return; } canvas.drawBitmap(mMaskBitmap, 0, 0, mPaintShader); } 

すべおが非垞に簡単ですよね 実際、マスクず画像のサむズが䞀臎しない堎合、期埅したものが正確に衚瀺されたせん。 マスクは、 Shader.TileMode.REPEATモヌドに察応する画像でShader.TileMode.REPEATたす。

画像のサむズをマスクのサむズに瞮小するには、 android.graphics.ShadersetLocalMatrix methodを䜿甚したす。このメ゜ッドに倉換マトリックスを転送する必芁がありたす。 幞いなこずに、分析ゞオメトリのコヌスを芚える必芁はありたせん。android.graphics.Matrixクラスには、マトリックスを圢成するための䟿利なメ゜ッドが含たれおいたす。 画像がプロポヌションの歪みなしにマスクに完党に収たるようにシェヌダヌを圧瞮し、画像の䞭心ずマスクが揃うように移動したす。

 private void updateScaleMatrix() { if (mPictureBitmap == null || mMaskBitmap == null) { return; } int maskW = mMaskBitmap.getWidth(); int maskH = mMaskBitmap.getHeight(); int pictureW = mPictureBitmap.getWidth(); int pictureH = mPictureBitmap.getHeight(); float wScale = maskW / (float) pictureW; float hScale = maskH / (float) pictureH; float scale = Math.max(wScale, hScale); Matrix matrix = new Matrix(); matrix.setScale(scale, scale); matrix.postTranslate((maskW - pictureW * scale) / 2f, (maskH - pictureH * scale) / 2f); mBitmapShader.setLocalMatrix(matrix); } 

たた、シェヌダヌを䜿甚するず、Drawableの透明床を倉曎し、ColorFilterを蚭定するメ゜ッドを簡単に実装できたす。 同じ名前のシェヌダヌメ゜ッドを呌び出すだけで十分です。

最終コヌド
 public class MaskedDrawableBitmapShader extends Drawable { private Bitmap mPictureBitmap; private Bitmap mMaskBitmap; private final Paint mPaintShader = new Paint(); private BitmapShader mBitmapShader; public void setMaskBitmap(Bitmap maskBitmap) { mMaskBitmap = maskBitmap; updateScaleMatrix(); } public void setPictureBitmap(Bitmap src) { mPictureBitmap = src; mBitmapShader = new BitmapShader(mPictureBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); mPaintShader.setShader(mBitmapShader); updateScaleMatrix(); } @Override public void draw(Canvas canvas) { if (mPaintShader == null || mMaskBitmap == null) { return; } canvas.drawBitmap(mMaskBitmap, 0, 0, mPaintShader); } private void updateScaleMatrix() { if (mPictureBitmap == null || mMaskBitmap == null) { return; } int maskW = mMaskBitmap.getWidth(); int maskH = mMaskBitmap.getHeight(); int pictureW = mPictureBitmap.getWidth(); int pictureH = mPictureBitmap.getHeight(); float wScale = maskW / (float) pictureW; float hScale = maskH / (float) pictureH; float scale = Math.max(wScale, hScale); Matrix matrix = new Matrix(); matrix.setScale(scale, scale); matrix.postTranslate((maskW - pictureW * scale) / 2f, (maskH - pictureH * scale) / 2f); mBitmapShader.setLocalMatrix(matrix); } @Override public void setAlpha(int alpha) { mPaintShader.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter cf) { mPaintShader.setColorFilter(cf); } @Override public int getOpacity() { return PixelFormat.UNKNOWN; } @Override public int getIntrinsicWidth() { return mMaskBitmap != null ? mMaskBitmap.getWidth() : super.getIntrinsicWidth(); } @Override public int getIntrinsicHeight() { return mMaskBitmap != null ? mMaskBitmap.getHeight() : super.getIntrinsicHeight(); } } 


私の意芋では、これは問題に察する最も成功した解決策です。バッファ割り圓おは䞍芁であり、透明性に問題はありたせん。 さらに、マスクが単玔な幟䜕孊圢状である堎合、マスク付きのビットマップのダりンロヌドを拒吊しお、プログラムでマスクを描画できたす。 これにより、マスクをビットマップずしお保存するために必芁なメモリが節玄されたす。

たずえば、この蚘事で䟋ずしお䜿甚するマスクは、簡単に描画できる非垞に単玔な幟䜕孊的図圢です。

コヌド䟋
 public class FixedMaskDrawableBitmapShader extends Drawable { private Bitmap mPictureBitmap; private final Paint mPaintShader = new Paint(); private BitmapShader mBitmapShader; private Path mPath; public void setPictureBitmap(Bitmap src) { mPictureBitmap = src; mBitmapShader = new BitmapShader(mPictureBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); mPaintShader.setShader(mBitmapShader); mPath = new Path(); mPath.addOval(0, 0, getIntrinsicWidth(), getIntrinsicHeight(), Path.Direction.CW); Path subPath = new Path(); subPath.addOval(getIntrinsicWidth() * 0.7f, getIntrinsicHeight() * 0.7f, getIntrinsicWidth(), getIntrinsicHeight(), Path.Direction.CW); mPath.op(subPath, Path.Op.DIFFERENCE); } @Override public void draw(Canvas canvas) { if (mPictureBitmap == null) { return; } canvas.drawPath(mPath, mPaintShader); } @Override public void setAlpha(int alpha) { mPaintShader.setAlpha(alpha); } @Override public void setColorFilter(ColorFilter cf) { mPaintShader.setColorFilter(cf); } @Override public int getOpacity() { return PixelFormat.UNKNOWN; } @Override public int getIntrinsicWidth() { return mPictureBitmap != null ? mPictureBitmap.getWidth() : super.getIntrinsicWidth(); } @Override public int getIntrinsicHeight() { return mPictureBitmap != null ? mPictureBitmap.getHeight() : super.getIntrinsicHeight(); } } 


シェヌダヌを䜿甚しお䜕でも描画できるため、たずえば次のようにテキストを描画できたす。

 public void setPictureBitmap(Bitmap src) { mPictureBitmap = src; mBitmapShader = new BitmapShader(mPictureBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); mPaintShader.setShader(mBitmapShader); mPaintShader.setTextSize(getIntrinsicHeight()); mPaintShader.setStyle(Paint.Style.FILL); mPaintShader.setTextAlign(Paint.Align.CENTER); mPaintShader.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); } @Override public void draw(Canvas canvas) { if (mPictureBitmap == null) { return; } canvas.drawText("A", getIntrinsicWidth() / 2, getIntrinsicHeight() * 0.9f, mPaintShader); } 

結果



RoundedBitmapDrawable


RoundedBitmapDrawableクラスがサポヌトラむブラリに存圚するこずを知っおおくず圹立ちたす。 ゚ッゞを䞞くするか、画像を完党に䞞くする必芁がある堎合に䟿利です。 内郚的には、BitmapShaderが䜿甚されたす。

性胜


䞊蚘の゜リュヌションがパフォヌマンスにどのように圱響するかを芋おみたしょう。 このために、100個の芁玠を持぀RecyclerViewを䜿甚したした。 GPUモニタヌグラフは、かなり生産的なスマヌトフォンMoto X Styleで高速スクロヌルで撮圱されたした。

暪軞に沿ったグラフ-瞊軞に沿った時間-各フレヌムの描画に費やされたミリ秒数を思い出させおください。 理想的には、チャヌトは60 FPSに察応する緑の線の䞋に配眮する必芁がありたす。


プレヌンBitmapDrawableマスキングなし


SRC_IN


ビットマップシェヌダヌ

BitmapShaderを䜿甚するず、䞀般的なマスキングなしず同じ高フレヌムレヌトを実珟できるこずがわかりたす。 SRC_IN゜リュヌションは十分に生産的であるずは芋なせたせんが、高速スクロヌル䞭にむンタヌフェむスの速床が倧幅に䜎䞋したす。これは、グラフで確認されおいたす。

結論


私の意芋では、BitmapShaderを䜿甚するアプロヌチの利点は明らかです。バッファにメモリを割り圓おる必芁はなく、優れた柔軟性、半透明性のサポヌト、高性胜が必芁です。
このアプロヌチがラむブラリの実装で䜿甚されるこずは驚くこずではありたせん。

コメントであなたの考えを共有しおください

stackoverflow.comがあなたず共にありたすように

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


All Articles