pats pats - 5 months ago 48
Android Question

Emboss edges of a image shape showing the depth in android

I want to show 3D embossed look and feel as shown in following image. I used EmbossMaskFilter but cannot get it to show the effect (see code below). Is there a different way to do this? or how can I use the EmbossMaskFilter for this.

Required output

enter image description here

My output

enter image description here

Path path2 = new Path();
public Paint fillPaint = null;
// called in constructor
public void createPath()
{
//path 2 Big one
araay = new Point[]{new Point(144,320),new Point(109,200), new Point(171,308),new Point(178,240),new Point(171,172),new Point(109,282),new Point(144,160)};
AddBeziers(path2, araay, 320, 144);
AddLine(path2, 216, 144 );
AddLine(path2, 216, 216 );
AddLine(path2, 144, 320);

MaskFilter mEmboss = new EmbossMaskFilter(new float[] { 1, 1, 1 }, 0.4f, 6, 3.5f);
fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
fillPaint.setColor(Color.WHITE);
fillPaint.setFlags(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
fillPaint.setAntiAlias(true);
fillPaint.setDither(true);
fillPaint.setStrokeJoin(Paint.Join.ROUND);
fillPaint.setStrokeCap(Paint.Cap.ROUND);
fillPaint.setStyle(Paint.Style.FILL);
paint.setMaskFilter(mEmboss);
}

// add lines to the path
protected Path AddLine(Path path, int endX, int endY) {
//path.moveTo(startX, startY);

path.lineTo(endX, endY);
return path;
}

// add curves to the path
protected Path AddBeziers(Path path, Point[] points, int lastX, int lastY) {

if (points[0].X != lastX && points[0].Y != lastY)
path.moveTo(points[0].X, points[0].Y);

int index = 1;

path.cubicTo(points[index].X, points[index].Y, points[index + 1].X,
points[index + 1].Y, points[index + 2].X, points[index + 2].Y);
index = index + 3;
path.cubicTo(points[index].X, points[index].Y, points[index + 1].X,
points[index + 1].Y, points[index + 2].X, points[index + 2].Y);

return path;
}

//draw on canvas
@Override
public void onDraw(Canvas canvas) {

canvas.drawPath(path2, fillPaint);
super.onDraw(canvas);
}

Answer

If you only want to do bitmap processing (as opposed to 3D or vectors), your best bet probably is to:

  1. Generate a stencil mask from your puzzle piece,
  2. Use Difference of Gaussians to process it (I used kernels of size 12 and 2 pixels in this example), then normalize and invert the result,
  3. Alpha-blend the output of "2" into original image using the mask (1.) as the stencil channel.

MaskDifference of GaussiansResult

UPDATE: here comes the code. I tried to reuse your variable names so that it's easier to understand. The code uses Renderscript intrinsics whenever possible in order to make things faster and more interesting.

private Paint fillPaint = null;
private Path path2;
private Bitmap mBitmapIn;
private Bitmap mBitmapPuzzle;
private RenderScript mRS;
private Allocation mInAllocation;
private Allocation mPuzzleAllocation;
private Allocation mCutterAllocation;

private Allocation mOutAllocation;
private Allocation mOutAllocation2;
private Allocation mAllocationHist;
private ScriptIntrinsicBlur mScriptBlur;
private ScriptIntrinsicBlend mScriptBlend;
private ScriptIntrinsicHistogram mScriptHistogram;
private ScriptIntrinsicLUT mScriptLUT;
private Context ctx;
private int bw = 780;
private int bh = 780;

private void init()
{
    mBitmapIn = loadBitmap(R.drawable.cat7); // background image
    mBitmapPuzzle = Bitmap.createBitmap(bw, bh, Bitmap.Config.ARGB_8888);  // this will hold the puzzle
    Canvas c = new Canvas(mBitmapPuzzle);

    path2 = new Path();
    createPath(5);  // create the path with stroke width of 5 pixels
    c.drawPath(path2, fillPaint);  // draw it on canvas

    createScript();  // get renderscripts and Allocations ready

    // Apply gaussian blur of radius 25 to our drawing
    mScriptBlur.setRadius(25);
    mScriptBlur.setInput(mPuzzleAllocation);
    mScriptBlur.forEach(mOutAllocation);

    // Now apply the blur of radius 1
    mScriptBlur.setRadius(1);
    mScriptBlur.setInput(mPuzzleAllocation);
    mScriptBlur.forEach(mOutAllocation2);

    // Subtract one blur result from another
    mScriptBlend.forEachSubtract(mOutAllocation, mOutAllocation2);

    // We now want to normalize the result (e.g. make it use full 0-255 range).
    // To do that, we will first compute the histogram of our image
    mScriptHistogram.setOutput(mAllocationHist);
    mScriptHistogram.forEach(mOutAllocation2);

    // copy the histogram to Java array...
    int []hist = new int[256 * 4];
    mAllocationHist.copyTo(hist);

    // ...and walk it from the end looking for the first non empty bin
    int i;
    for(i = 255; i > 1; i--)
        if((hist[i * 4] | hist[i * 4 + 1] | hist[i * 4 + 2]) != 0)
            break;

    // Now setup the LUTs that will map the image to the new, wider range.
    // We also use the opportunity to inverse the image ("255 -").
    for(int x = 0; x <= i; x++)
    {
        int val = 255 - x * 255 / i;

        mScriptLUT.setAlpha(x, 255);  // note we always make it fully opaque
        mScriptLUT.setRed(x, val);
        mScriptLUT.setGreen(x, val);
        mScriptLUT.setBlue(x, val);
    }


    // the mapping itself.
    mScriptLUT.forEach(mOutAllocation2, mOutAllocation);

Let's make a short break and see what we have so far. Observe that the entire image on the left is opaque (i.e. including the space outside the puzzle), and we now have to cut out the shape and antialias its edge properly. Unfortunately, using original shape won't work, as it is too large and cuts out too much, leading to unpleasant artifacts near the edge (figure on the right).

enter image description here enter image description here

We therefore draw another path, this time using a narrower stroke...

    Bitmap mBitmapCutter = Bitmap.createBitmap(bw, bh, Bitmap.Config.ARGB_8888);
    c = new Canvas(mBitmapCutter);
    path2 = new Path();
    createPath(1);  // stroke width 1
    c.drawPath(path2, fillPaint);
    mCutterAllocation = Allocation.createFromBitmap(mRS, mBitmapCutter);

    // cookie cutter now
    mScriptBlend.forEachDstIn(mCutterAllocation, mOutAllocation);

...for a much better looking result. Let's use it to mask out a background image. enter image description here

    mScriptBlend.forEachMultiply(mOutAllocation, mInAllocation);
    mInAllocation.copyTo(mBitmapPuzzle);
}

enter image description here

Hello there! Now just the Renderscript setup code.

private void createScript() {
    mRS = RenderScript.create(ctx);

    mPuzzleAllocation = Allocation.createFromBitmap(mRS, mBitmapPuzzle);

    // three following allocations could actually use createSized(),
    // but the code would be longer.
    mInAllocation = Allocation.createFromBitmap(mRS, mBitmapIn);
    mOutAllocation = Allocation.createFromBitmap(mRS, mBitmapPuzzle);
    mOutAllocation2 = Allocation.createFromBitmap(mRS, mBitmapPuzzle);

    mAllocationHist = Allocation.createSized(mRS, Element.I32_3(mRS), 256);

    mScriptBlur = ScriptIntrinsicBlur.create(mRS, Element.U8_4(mRS));
    mScriptBlend = ScriptIntrinsicBlend.create(mRS, Element.U8_4(mRS));
    mScriptHistogram = ScriptIntrinsicHistogram.create(mRS, Element.U8_4(mRS));
    mScriptLUT = ScriptIntrinsicLUT.create(mRS, Element.U8_4(mRS));
}

And finally onDraw():

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawBitmap(mBitmapPuzzle, 0, 0, fillPaint);
    super.onDraw(canvas);
}

TODO: check if other stroke miters would give more pleasant corners.