Android自动手绘,圆你儿时画家梦!

我的CSDN博客同步发布Android自动手绘,圆你儿时画家梦!

从小就喜欢到处乱画,家里一米以下墙上就没有一块干净的地方(那是老房子啦(⊙﹏⊙)b。好了,废话不多说,进入主题。今天主要跟大家分享一下如何将一张图片转成手绘效果,并模拟画家动态绘制。先把最终效果图亮出来,觉得好的请点个赞,您的点赞是对我的最大鼓励(O(∩_∩)O哈哈)。
效果图如下:

最终效果图

![GIF截图]](http://upload-images.jianshu.io/upload_images/2154124-487f5f5017b0f844.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
心动有木有!

原理

大概介绍一下实现原理。首先你得有一张图(废话~),接下来就是把这张图的轮廓提取出来,轮廓提取算法有很多,本人不是搞图像处理的,对图像处理感兴趣的童鞋可以查看相关资料。如果你有好的轮廓提取算法,也可以把源码中的算法替换掉,我们采用的轮廓提取算法是Sobel边缘检测。网上的实现有很多,我懒得去实现一遍,github有开源的实现,直接下载了:GraphicLib.本文不具体介绍轮廓提取算法。得到轮廓图以后,接下来要做的是,按照线条的走势,一个一个像素点绘制出来。注意,一定要按照线条的走势显示对应的像素点,如果用两个嵌套的for循环,动画会像网速不好时浏览器显示图片一样。难点就在于此,如何把像素点按照线条方向绘制。接下来我们一起研究。

代码实现

如何让绘制的点不会跳远太远,使之连贯起来?首先,对于一个刚绘制完成的点,接下来要绘制的点肯定要选择离它最近的点,这样肯定是最佳的下一个绘制点。因此,只要我们找到最近的点就可以,寻找最近的点,可以通过以圆的方式不断改变半径的大小进行探测。但是用圆的话需要各种三角函数运算,影响效率。我们可以换一种方法:根据当前的点可以轻松得到每一层以该点为中心的正方形,一层层遍历,直到找到需要的点就是我们要的点。遍历的方法很简单,就是比较对应的正方形的上下左右四条边上面的像素点。如下图所示:

寻找附近点

接下来看看如何用代码去实现寻找最近的点:

 //获取离指定点最近的一个未绘制过的点
    private Point getNearestPoint(Point p) {
        if (p == null) return null;
        //以点p为中心,向外扩大搜索范围,每次搜索的是与p点相距add的正方形
        for (int add = 1; add < mSrcBmWidth && add < mSrcBmHeight; add++) {
            //
            int beginX = (p.x - add) >= 0 ? (p.x - add) : 0;
            int endX = (p.x + add) < mSrcBmWidth ? (p.x + add) : mSrcBmWidth - 1;
            int beginY = (p.y - add) >= 0 ? (p.y - add) : 0;
            int endY = (p.y + add) < mSrcBmHeight ? (p.y + add) : mSrcBmHeight - 1;
            //搜索正方形的上下边
            for (int x = beginX; x <= endX; x++) {
                if (mArray[x][beginY]) {
                   //标记当前点已经访问过
                    mArray[x][beginY] = false;
                    return new Point(x, beginY);
                }
                if (mArray[x][endY]) {
                    //标记当前点已经访问过
                    mArray[x][endY] = false;
                    return new Point(x, endY);
                }
            }
            //搜索正方形的左右边
            for (int y = beginY + 1; y <= endY - 1; y++) {
                if (mArray[beginX][y]) {
                    //标记当前点已经访问过
                    mArray[beginX][beginY] = false;
                    return new Point(beginX, beginY);
                }
                if (mArray[endX][y]) {
                   //标记当前点已经访问过
                    mArray[endX][y] = false;
                    return new Point(endX, y);
                }
            }
        }

        return null;
    }

任何一个点,只要还存在没有绘制过的点,就一定能找得到与它最近的点,如果找不到,说明所有的点已经绘制完毕。为了防止查找到重复的点,需要把访问过的点做上记号(即设为false)。我们需要把整张图中每一个像素点位置作好记号,标记哪些点是需要绘制,哪些点是不需要绘制或者是已经绘制过。用一个boolean[][]型数组保存。还需要记录最后一次访问的点,以便继续下一次的绘制。根据最后一次访问的点继续寻找最近点,反复迭代,把所有的点绘制完成后,整张图就出来了。程序开始时,将最后一次访问的点初始化为左上角的点。


    private Point mLastPoint = new Point(0, 0);
//获取下一个需要绘制的点
    private Point getNextPoint() {
        mLastPoint = getNearestPoint(mLastPoint);
        return mLastPoint;
    }

接下来是将点绘制到Bitmap上,在将Bitmap绘制到SurfaceView的Canvas上。这里这么做的目的是,SurfaceView内部使用了双缓存,直接绘制到SurfaceView的Canvas可能会闪屏。

/**
     * //绘制
     * return :false 表示绘制完成,true表示还需要继续绘制
     */
    private boolean draw() {

        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.BLACK);
        //获取count个点后,一次性绘制到bitmap在把bitmap绘制到SurfaceView
        int count = 100;
        Point p = null;
        while (count-- > 0) {
            p = getNextPoint();
            if (p == null) {//如果p为空,说明所有的点已经绘制完成
                return false;
            }
            mTmpCanvas.drawPoint(p.x, p.y + offsetY, mPaint);
        }
        //将bitmap绘制到SurfaceView中
        Canvas canvas = mSurfaceHolder.lockCanvas();
        canvas.drawBitmap(mTmpBm, 0, 0, mPaint);
        if (p != null)
            canvas.drawBitmap(mPaintBm, p.x, p.y - mPaintBm.getHeight() + offsetY, mPaint);
        mSurfaceHolder.unlockCanvasAndPost(canvas);
        return true;
    }

基本上绘制算完成了,附上完整的代码:


package com.hc.myoutline;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;

/**
 * Package com.hc.myoutline
 * Created by HuaChao on 2016/5/27.
 */
public class DrawOutlineView extends SurfaceView implements SurfaceHolder.Callback {

    private SurfaceHolder mSurfaceHolder;
    private Bitmap mTmpBm;
    private Canvas mTmpCanvas;
    private int mWidth;
    private int mHeight;
    private Paint mPaint;
    private int mSrcBmWidth;
    private int mSrcBmHeight;
    private boolean[][] mArray;
    private int offsetY = 100;

    private Bitmap mPaintBm;
    private Point mLastPoint = new Point(0, 0);

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

    public DrawOutlineView(Context context, AttributeSet attrs) {
        super(context, attrs);

        init();
    }

    private void init() {
        mSurfaceHolder = getHolder();
        mSurfaceHolder.addCallback(this);
        mPaint = new Paint();
        mPaint.setColor(Color.BLACK);
    }

    //设置画笔图片
    public void setPaintBm(Bitmap paintBm) {
        mPaintBm = paintBm;
    }

    //获取离指定点最近的一个未绘制过的点
    private Point getNearestPoint(Point p) {
        if (p == null) return null;
        //以点p为中心,向外扩大搜索范围,每次搜索的是与p点相距add的正方形
        for (int add = 1; add < mSrcBmWidth && add < mSrcBmHeight; add++) {
            //
            int beginX = (p.x - add) >= 0 ? (p.x - add) : 0;
            int endX = (p.x + add) < mSrcBmWidth ? (p.x + add) : mSrcBmWidth - 1;
            int beginY = (p.y - add) >= 0 ? (p.y - add) : 0;
            int endY = (p.y + add) < mSrcBmHeight ? (p.y + add) : mSrcBmHeight - 1;
            //搜索正方形的上下边
            for (int x = beginX; x <= endX; x++) {
                if (mArray[x][beginY]) {
                    mArray[x][beginY] = false;
                    return new Point(x, beginY);
                }
                if (mArray[x][endY]) {
                    mArray[x][endY] = false;
                    return new Point(x, endY);
                }
            }
            //搜索正方形的左右边
            for (int y = beginY + 1; y <= endY - 1; y++) {
                if (mArray[beginX][y]) {
                    mArray[beginX][beginY] = false;
                    return new Point(beginX, beginY);
                }
                if (mArray[endX][y]) {

                    mArray[endX][y] = false;
                    return new Point(endX, y);
                }
            }
        }

        return null;
    }

    //获取下一个需要绘制的点
    private Point getNextPoint() {
        mLastPoint = getNearestPoint(mLastPoint);
        return mLastPoint;
    }


    /**
     * //绘制
     * return :false 表示绘制完成,true表示还需要继续绘制
     */
    private boolean draw() {

        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.BLACK);
        //获取count个点后,一次性绘制到bitmap在把bitmap绘制到SurfaceView
        int count = 100;
        Point p = null;
        while (count-- > 0) {
            p = getNextPoint();
            if (p == null) {//如果p为空,说明所有的点已经绘制完成
                return false;
            }
            mTmpCanvas.drawPoint(p.x, p.y + offsetY, mPaint);
        }
        //将bitmap绘制到SurfaceView中
        Canvas canvas = mSurfaceHolder.lockCanvas();
        canvas.drawBitmap(mTmpBm, 0, 0, mPaint);
        if (p != null)
            canvas.drawBitmap(mPaintBm, p.x, p.y - mPaintBm.getHeight() + offsetY, mPaint);
        mSurfaceHolder.unlockCanvasAndPost(canvas);
        return true;
    }
    //重画
    public void reDraw(boolean[][] array) {
        if (isDrawing) return;
        mTmpBm = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
        mTmpCanvas = new Canvas(mTmpBm);
        mPaint.setColor(Color.WHITE);
        mPaint.setStyle(Paint.Style.FILL);
        mTmpCanvas.drawRect(0, 0, mWidth, mHeight, mPaint);
        mLastPoint = new Point(0, 0);
        beginDraw(array);
    }

    private boolean isDrawing = false;

    public void beginDraw(boolean[][] array) {
        if (isDrawing) return;
        this.mArray = array;
        mSrcBmWidth = array.length;
        mSrcBmHeight = array[0].length;

        new Thread() {
            @Override
            public void run() {
                while (true) {
                    isDrawing = true;
                    boolean rs = draw();
                    if (!rs) break;
                    try {
                        sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                isDrawing = false;
            }
        }.start();
    }


    @Override
    public void surfaceCreated(SurfaceHolder holder) {

    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        this.mWidth = width;
        this.mHeight = height;
        mTmpBm = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        mTmpCanvas = new Canvas(mTmpBm);
        mPaint.setColor(Color.WHITE);
        mPaint.setStyle(Paint.Style.FILL);
        mTmpCanvas.drawRect(0, 0, mWidth, mHeight, mPaint);
        Canvas canvas = holder.lockCanvas();
        canvas.drawBitmap(mTmpBm, 0, 0, mPaint);
        holder.unlockCanvasAndPost(canvas);

        mPaint.setStyle(Paint.Style.STROKE);
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {

    }
}

接下来是在MainActivity里面传入参数过来,附上MainActivity的代码:

package com.hc.myoutline;

import android.graphics.Bitmap;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.MotionEvent;

public class MainActivity extends AppCompatActivity {
    private DrawOutlineView drawOutlineView;
    private Bitmap sobelBm;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);
        //将Bitmap压缩处理,防止OOM
        Bitmap bm = CommenUtils.getRatioBitmap(this, R.drawable.test, 100, 100);
        //返回的是处理过的Bitmap
        sobelBm = SobelUtils.Sobel(bm);
        drawOutlineView = (DrawOutlineView) findViewById(R.id.outline);

        Bitmap paintBm = CommenUtils.getRatioBitmap(this, R.drawable.paint, 10, 20);
        drawOutlineView.setPaintBm(paintBm);

    }
    //根据Bitmap信息,获取每个位置的像素点是否需要绘制
    //使用boolean数组而不是int[][]主要是考虑到内存的消耗
    private boolean[][] getArray(Bitmap bitmap) {
        boolean[][] b = new boolean[bitmap.getWidth()][bitmap.getHeight()];

        for (int i = 0; i < bitmap.getWidth(); i++) {
            for (int j = 0; j < bitmap.getHeight(); j++) {
                if (bitmap.getPixel(i, j) != Color.WHITE)
                    b[i][j] = true;
                else
                    b[i][j] = false;
            }
        }
        return b;
    }

    boolean first = true;
    
   //点击时开始绘制
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if (first) {
            first = false;
            drawOutlineView.beginDraw(getArray(sobelBm));
        } else
            drawOutlineView.reDraw(getArray(sobelBm));
        return true;
    }
}

关于轮廓提取的具体实现代码这里不粘出,可以去GitHub查看:GraphicLib 或者是下载我的源代码查看。所有内容已经结束,赶紧下载源码运行一下你的照片去秀一下你的逼格吧!

最后的最后:请注意,源码中,轮廓的提取是运行在主线程中,如果图片比较复杂,可能会导致ANR,建议另开线程处理。别问为什么我不去改,一个字:懒!另外,接下来一篇文章中,我将介绍提高运算速度相关内容,提升轮廓提取速度,敬请期待~

源码地址:Android自动手绘,圆你儿时画家梦!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,378评论 25 707
  • 在Android系统中,有一种特殊的视图,称为SurfaceView,它拥有独立的绘图表面,即它不与其宿主窗口共享...
    一个不掉头发的开发阅读 11,274评论 12 74
  • 本文章转载于搜狗测试 前文回顾 上周liren从面试官的角度跟大家分享了如何进行简历的筛选。 但作为面试者,如何提...
    夜境阅读 371评论 0 0
  • 今天下午放学回家,我妈妈给我买了一台学习机_米蛋,以为可以吃呢,这名字很好笑。它是粉红色的,有一只耳朵可以随时提...
    A叶瑞妹阅读 353评论 1 4
  • 笔者暑假面试了6家互联网创业公司,大多数以失败告终,促使笔者下决心在求职之前准备一番,下面就跟各位分享一下一个-1...
    Loren阅读 386评论 0 11