图像形状的浮雕边缘显示 android 中的深度

Emboss edges of a image shape showing the depth in android

我想显示 3D 浮雕外观,如下图所示。我使用了 EmbossMaskFilter 但无法显示效果(请参见下面的代码)。有没有不同的方法来做到这一点?或者我如何为此使用 EmbossMaskFilter。

需要输出

我的输出

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);
}

我认为您需要更大的模糊半径和更小的环境光和镜面高光值。

我通过创建 UI 来调整参数来解决这个问题。

这是我使用的代码,你可以试试看。我使用了一个简单的蓝色矩形,但你应该能够轻松插入任何你想绘制的图像。

MainActivity.java:

import android.graphics.EmbossMaskFilter;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.SeekBar;
import android.widget.TextView;

public class MainActivity extends ActionBarActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final DrawView drawView = (DrawView) findViewById(R.id.drawView);

        final TextView ambientLightTextView = (TextView) findViewById(R.id.ambientLightTextView);

        SeekBar ambientLightSeekBar = (SeekBar) findViewById(R.id.ambientLightSeekBar);
        ambientLightSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                float value = (float) progress / 1000F;
                ambientLightTextView.setText(Float.toString(value));
                drawView.setAmbientLight(value);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {}
        });

        final TextView specularHighlightTextView = (TextView) findViewById(R.id.specularHighlightTextView);

        SeekBar specularHighlightSeekBar = (SeekBar) findViewById(R.id.specularHighlightSeekBar);
        specularHighlightSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                float value = (float) progress / 100f;
                specularHighlightTextView.setText(Float.toString(value));
                drawView.setSpecularHighlight(value);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {}
        });

        final TextView blurRadiusTextView = (TextView) findViewById(R.id.blurRadiusTextView);

        SeekBar blurRadiusSeekBar = (SeekBar) findViewById(R.id.blurRadiusSeekBar);
        blurRadiusSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                float value = (float) progress / 5F;
                blurRadiusTextView.setText(Float.toString(value));
                drawView.setBlurRadius(value);
            }

            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {}
        });
    }

}

DrawView.java:

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.EmbossMaskFilter;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;

public class DrawView extends View {

    private Rect rect = new Rect();

    private Paint paint;

    private EmbossMaskFilter filter;

    private float[] mLightSource = new float[] {2, 5, 5};

    private float mAmbientLight = 0.5F;

    private float mSpecularHighlight = 8F;

    private float mBlurRadius = 15f;

    public DrawView(Context context) {
        super(context);
        init();
    }

    public DrawView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public DrawView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {

        setLayerType(LAYER_TYPE_SOFTWARE, null);

        filter = new EmbossMaskFilter(mLightSource, mAmbientLight, mSpecularHighlight, mBlurRadius);

        paint = new Paint();
        paint.setColor(0xFF0000FF);
        paint.setStyle(Paint.Style.FILL);
        paint.setAntiAlias(true);
        paint.setDither(true);
        paint.setMaskFilter(filter);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        rect.left = getWidth() / 3;
        rect.right = rect.left * 2;
        rect.top = getHeight() / 3;
        rect.bottom = rect.top * 2;

        canvas.drawRect(rect, paint);
    }

    public void setAmbientLight(float value) {
        if (value > 1.0F) value = 1.0F;
        if (value < 0) value = 0;
        mAmbientLight = value;
        filter = new EmbossMaskFilter(mLightSource, mAmbientLight, mSpecularHighlight, mBlurRadius);
        paint.setMaskFilter(filter);
        invalidate();
    }

    public void setSpecularHighlight(float value) {
        mSpecularHighlight = value;
        filter = new EmbossMaskFilter(mLightSource, mAmbientLight, mSpecularHighlight, mBlurRadius);
        paint.setMaskFilter(filter);
        invalidate();
    }

    public void setBlurRadius(float value) {
        mBlurRadius = value;
        filter = new EmbossMaskFilter(mLightSource, mAmbientLight, mSpecularHighlight, mBlurRadius);
        paint.setMaskFilter(filter);
        invalidate();
    }
}

activity_main.xml:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <!-- make sure you match this up with the actual package name in your code -->
    <com.example.embossmaskfilter.DrawView
        android:id="@+id/drawView"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layerType="software" />

    <TextView
        android:id="@+id/textView2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_below="@id/drawView"
        android:layout_centerVertical="true"
        android:text="Ambient Light"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <TextView
        android:id="@+id/ambientLightTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignTop="@id/textView2"
        android:text="0.5"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <SeekBar
        android:id="@+id/ambientLightSeekBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/textView2"
        android:max="1000" />

    <TextView
        android:id="@+id/textView3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_below="@id/ambientLightSeekBar"
        android:layout_centerVertical="true"
        android:text="Specular Highlight"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <TextView
        android:id="@+id/specularHighlightTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignTop="@id/textView3"
        android:text="0.5"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <SeekBar
        android:id="@+id/specularHighlightSeekBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/textView3"
        android:max="1000" />

    <TextView
        android:id="@+id/textView4"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentStart="true"
        android:layout_below="@id/specularHighlightSeekBar"
        android:layout_centerVertical="true"
        android:text="Blur Radius"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <TextView
        android:id="@+id/blurRadiusTextView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignTop="@id/textView4"
        android:text="0.5"
        android:textAppearance="?android:attr/textAppearanceSmall" />

    <SeekBar
        android:id="@+id/blurRadiusSeekBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentStart="true"
        android:layout_below="@+id/textView4"
        android:max="1000" />

</RelativeLayout>

如果您不怕涉足一些低级图像处理,您可以使用 convolution matrices 来实现浮雕效果。您可以使用图像的轮廓(即 alpha 通道)作为过滤器的输入。例如,如果您使用这个 5x5 矩阵:

| -2  0  0  0  0 |
|  0 -1  0  0  0 |
|  0  0  n  0  0 | * (1/n)
|  0  0  0  1  0 |
|  0  0  0  0  2 |

并将其应用到这样的图像(代表一个 alpha 通道):

你会得到这样的效果:

所有计算值都减少了 127,以确保它们在 0-255 的范围内。我在这个特定示例中使用了 n = 10。您可以通过使用不同大小的矩阵(不难推断)来操纵半径,通过调整 n 的值(值越大,效果越微妙)来操纵深度。

给定原始 alpha 通道和计算出的蒙版,您可以确定要应用于原始图像的各个像素的偏移量,从而创建浮雕效果。

希望对您有所帮助。

如果您只想进行位图处理(而不是 3D 或矢量),您最好的选择可能是:

  1. 从您的拼图中生成模板掩码,
  2. 使用Difference of Gaussians对其进行处理(本例中我使用了大小为12和2像素的内核),然后对结果进行归一化和反转,
  3. 使用蒙版 (1.) 作为模板通道将“2”的输出 Alpha 混合到原始图像中。

更新:代码来了。我试图重用您的变量名,以便更容易理解。该代码尽可能使用 Renderscript 内在函数,以使事情变得更快、更有趣。

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);

让我们稍作休息,看看到目前为止我们有什么。观察左侧的整个图像是不透明的(即包括拼图外的 space),我们现在必须切出形状并适当地消除其边缘的锯齿。不幸的是,使用原始形状是行不通的,因为它太大并且切掉太多,导致边缘附近出现令人不快的伪影(右图)。

因此我们绘制另一条路径,这次使用较窄的笔触...

    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);

...以获得更好看的结果。让我们用它来遮盖背景图像。

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

你好!现在只是 Renderscript 设置代码。

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));
}

最后 onDraw():

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

TODO:检查其他描边斜接是否会产生更舒适的角。