2016-03-30 10:07:07 Trent1985 阅读数 2886
  • 手绘漫画人体结构与比例入门基础教程

    课程详细介绍了插画常用的软件SAI、PS,数位板的功能和使用技巧,以及插画漫画的基础技巧,包括五官、人体比例的分配,头发、眼睛、衣服的细节刻画,造型原创等,详实的展示了插画漫画的创作思路和过程。

    3596 人正在学习 去看看 竹芝

手绘效果实现方案

本文介绍一种PS手绘效果的实现方案,PS步骤来自网络,本文介绍代码实现过程。


整体看来,虽然效果还是有很大差异,但是已经有了这种特效的风格了,毕竟PS效果图中关于头发的部分是人工手动涂抹实现的,这一点,程序是无法自动实现的,这一点还需要大家理解。

本人实现的算法流程如下:



大家可以对照一下看看。

这里贴出图像转手绘效果的PS实现教程,点击打开链接

2017-07-02 19:15:12 riba2534 阅读数 276615
  • 手绘漫画人体结构与比例入门基础教程

    课程详细介绍了插画常用的软件SAI、PS,数位板的功能和使用技巧,以及插画漫画的基础技巧,包括五官、人体比例的分配,头发、眼睛、衣服的细节刻画,造型原创等,详实的展示了插画漫画的创作思路和过程。

    3596 人正在学习 去看看 竹芝

首先,我们平时所用的彩色图像是RGB色,分别有三个图像通道:
red(0-255),green(0-255),blue(0-255)
1. 可以把一张图像理解为一个二维矩阵,在矩阵中每一个点都有一个属性,就是它的RGB色,以此来构成一个图像
2. 我们只需要改变对应像素点的对应值,就可以对一张图像进行操作

以下是两个图像变换的实例:

1.这是一个普通的把一张彩色图片通过对它每一个像素点的操作变成它的相反的颜色的

from PIL import Image
import numpy as np
a=np.array(Image.open('001.jpg'))
print(a.shape,a.dtype)
b=[255,255,255]-a
im=Image.fromarray(b.astype('uint8'))
im.save('002.jpg')

2. 这是图像中的一些黑白变换:

from PIL import Image
import numpy as np
a=np.array(Image.open('001.jpg').convert('L'))#.convert是变成黑白的

b=255-a#在对应的颜色通道减去他自己变成黑白底片的效果
im=Image.fromarray(b.astype('uint8'))
im.save('003.jpg')


c=(100/255)*a+150#区间变换,颜色比较淡的灰度的图片
im=Image.fromarray(c.astype('uint8'))
im.save('004.jpg')

d=255*(a/255)**2#像素平方,颜色比较深的图
im=Image.fromarray(d.astype('uint8'))
im.save('005.jpg')

3. 图像的手绘效果的变换:

先上代码:

from PIL import Image
import numpy as np
a=np.array(Image.open('测试.jpg').convert('L')).astype('float')

depth=10                        #(0-100)
grad=np.gradient(a)             #取图像灰度的梯度值
grad_x,grad_y=grad              #分别取横纵图像的梯度值
grad_x=grad_x*depth/100.
grad_y=grad_y*depth/100.
A=np.sqrt(grad_x**2+grad_y**2+1.)
uni_x=grad_x/A
uni_y=grad_y/A
uni_z=1./A

vec_el=np.pi/2.2                        #光源的俯视角度,弧度值
vec_az=np.pi/4                          #光源的方位角度,弧度值
dx=np.cos(vec_el)*np.cos(vec_az)        #光源对x轴的影响
dy=np.cos(vec_el)*np.sin(vec_az)        #光源对y轴的影响
dz=np.sin(vec_el)                       #光源对z轴的影响

b=255*(dx*uni_x+dy*uni_y+dz*uni_z)      #光源归一化
b=b.clip(0,255)

im=Image.fromarray(b.astype('uint8'))
im.save('手绘效果.jpg')    

代码解析:

根据灰度变化来模拟人类视觉的远近程度

  • 设计一个位于图像斜上方的虚拟光源
  • 光源相对于图像的俯视角为Elevation, 方位角为Azimuth
  • 建立光源对个点梯度值的影响函数
  • 运算出各点的新像素值


2018-11-24 21:27:46 qq_41616984 阅读数 99
  • 手绘漫画人体结构与比例入门基础教程

    课程详细介绍了插画常用的软件SAI、PS,数位板的功能和使用技巧,以及插画漫画的基础技巧,包括五官、人体比例的分配,头发、眼睛、衣服的细节刻画,造型原创等,详实的展示了插画漫画的创作思路和过程。

    3596 人正在学习 去看看 竹芝

手绘与码绘的比较—模拟风吹树动

一、内容介绍
本文主要介绍用手绘和码绘两种方式模仿一种自然现象–风吹树动的过程,以及在实现过程对这两种方式的比较和思考。之所以选择风吹树动,是因为每次速写画一些场景时(尤其是室外场景)都难免要画树,而我自己在画树时,经常不知道怎么下手,再加上这学期上了图像处理,互动媒体的课,因此想要用代码来实现这种效果。

二、作品展示
手绘
在这里插入图片描述

手绘作品图1-1
在这里插入图片描述

手绘作品图1-2

在这里插入图片描述
动态效果

三、手绘与码绘的比较
下面将从以下几个角度来分析比较两种方式:技法,工具,理念,创作体验,呈现效果,载体,局限性,应用。

A.技法:
让我们从手绘的整个过程开始,绘画可分为选择工具,构思画面,动手,后期加工这几个步骤,首先,我们可以根据自己构思的画面选择合适的笔和纸(如钢笔,水笔或者铅笔等),画面构思需要考虑整个画面的大小比例物体的结构线阴影透视物体遮挡前后关系不重要细节的取舍等等,我在手绘树时,会构思如何对树干的走向,结构线,树叶的遮挡进行处理,后期加工处理有很多种方式,我们可以借用印刷技术,将我们的草图扫描到电脑里面,进行一些加工,然后打印在像衣服,杯子等物品上。

然后我们来谈谈码绘,也可以把它分成选择工具,构思,实现,后期加工这几步,码绘的工具当然是编程语言和平台,C++,Java等都可以实现,还有一种比较灵活方便的是Shader Labguage,目前三大主流Shader Labguage是基于OpenGL的GLSL语言,基于DirectX的HLSL语言,以及NVIDIA公司的Cg语言,这些语言相应的平台也有很多,如Processing,Visual Studio,Unity,openframeworks等,我是在processing上实现的,processing的语法和Java很类似,然后我想着重讲一下码绘树的构思和实现环节, 这个环节可分成两个部分:树的生成树在风力作用下运动,我用基于L-x系统的算法生成树,然后使用perlin噪声产生随机风场,根据每一个节块受到的力使树干旋转相应角度,这个过程中我发现这种方法生成树与手绘有很多共同之处,首先,两种方式生成的树都是由一个个小的单元不断组合形成,这里我想用一组图来解释:手绘的树由一些小的结构线不断重复所构成,不同种类的树的结构线不一样,如灌木和棕榈的结构线要硬直些,而乔木的要圆润些(图2-1),让我们再来看码绘的,图2-2b的大树是以图2-2a的树干为基本单元不断迭代生成的,仔细观察可以看到这棵大树的每个部分都有相同的结构,让我们再比较图2-3b和图2-2b中的树,明显看到组成这两棵树的基本单元树干也是不同的(实现代码在第四部分)。
在这里插入图片描述
图2-1
图2-2a在这里插入图片描述
左图为2-2a 右图为2-2b
图2-3a在这里插入图片描述
左图为2-3a 右图为2-3b
大多数时候用手绘实现动态效果大多利用一些视错觉,现在有一种有趣的玩法是讲一些经典绘画作品串在一起形成一些有趣的动图,具体可以点击经典名画动图,而码绘实现可以用一些随机函数如perlin噪声和循环实现,至于算法的实现过程以及生成原理请参考《基于 L- 系统和 Perlin噪声函数的风吹树动模拟》

B.理念
借用百度词条对理念的解释:一是“看法、思想。思维活动的结果”,二是“理论,观念(希腊文idea)。通常指思想。有时亦指表象或客观事物在人脑里留下的概括的形象。” 。从这个角度来看,人们对手绘的看法有:创作者表达自我情感的一个载体,闲暇时会进行的爱好,观察世界的方式,赚钱的工具等等。对身边学艺术的同学以及一些设计大师观察,发现手绘对他们来说是一个帮助自己抓住灵感,理清思路,表达自己想法且能够和他人沟通的一种非常好的工具。
而在现实生活中,我们对码绘的想法也大致相同,除了不会用代码画画来抓住灵感,因为码绘的起点的是会编程,然后还要在写完代码调试成功后确定灵感还在。我个人对两者的看法是,手绘是一个有生活气息,有趣味的过程,而码绘相对要理智,酷炫些。

C.创作体验
在创作体验上手绘和码绘的差别还是挺大的,一方面,我们完成一幅作品依靠我们的各种感觉器官以及手对笔的掌控能力,而码绘就主要靠脑力以及耐心了;另一方面,我们在任何环境下都可以进行绘画(只要有笔和纸),我们会喜欢一边听歌一边画,而在码绘时,我们希望自己是在一个安静的环境里敲着代码同时自己也是清醒的状态。

D.呈现效果
手绘与码绘的作品请看上面第二部分的作品展示。手绘作品与码绘作品呈现出来的效果是很不一样的,手绘作品会让人觉得很有趣,即使作品中有很多不精细或者走形的地方也会让人觉得很有趣,可爱,相较之下,码绘会给人一种距离感,让人感觉很冰冷,但也会有一些高级感。

E.局限性及应用
手绘作品在表现空间立体感,制造动态效果是无法与码绘相比的,结合手绘作品的呈现效果,手绘适合一些用于产品设计,产品包装,海报等一些需要人情味的设计中,而码绘可以通过精细的计算设计制作出逼真的动态以及立体效果,因此适合在地质模拟,电影特效上这种对仿真度要求高的工作中使用。

四、主要代码

//生成构建树的字符串
class Grammar
{
  string rules[];
  int ruleNum=0;
  int times;
  chArr result;
  char axioms;
  Grammar(int t, string R[], char a)
  {
    this.ruleNum=R.length;
    this.times=t;
    result=new chArr();
    rules=new string[R.length];
    for (int k=0; k<R.length; k++)
    {
      //rules[k]=new string();
      rules[k]=R[k];
    }
    axioms=a;
    this.Iteration();
  }

  void Iteration()
  {


    result.addCharacters(this.axioms);
    for (int i=0; i<this.times; i++)
    {
      chArr tmpstr=new chArr();
      for (int j=0; j<result.size; j++)
      {
        tmpstr.addCharacters(search(result.get(j)));
      }
      result .set(tmpstr);
    }
  }
  chArr getResult()
  {

    return result;
  }
  chArr search(char target)
  {
    //string generations;
    chArr tmp=new chArr();
    tmp.addCharacters(target);
    //generations=new string(target);

    for (int i=0; i<rules.length; i++)
    {
      if (rules[i].first==target)
      {
        int index;
        index=(int)(random(1)*rules[i].size);
        return rules[i].str.get(index);
      }
    }
    return tmp;
  }
}
//构建树的节点
void getVertices()
  {

    chArr result=this.grammar.getResult();
    int i=0;
    int count =0;
    float ang=90;
    IntList outbrake=new IntList();
    FloatList angleSign=new FloatList();
    FloatList radiusSign=new FloatList();
    FloatList lengthSign=new FloatList();
    float radius=treeRadius, len=treeLen;
    State curS;
    ArrayList<State> node1=new ArrayList<State>();
    curS=firstState;
    for (int loop=0; loop<result.size; loop++)
    {
      char ch=result.get(loop);
      switch(ch) {
        //move forward
      case 'F':
        {
          PVector p=new PVector();
          State cur=new State();

          if (i==0)
          {
            p.set(400, 150);
            cur.setStartPos(p);
            cur.setAngle(90);
            cur.Length=treeLen;
            cur.radius=treeRadius;
          } else
          {
            cur.Length=len*lengthFactor;
            cur.radius=radius*radiusFactor;
            radius=cur.radius;
            len=cur.Length;
          }
          cur.setAngle(ang);
          curS=cur;
          stack.push(cur);
          break;
        }
      case 'f':
        {
          PVector p1=new PVector();
          State cur=new State();

          if (i==0)
          {
            p1.set(400, 150);
            cur.setStartPos(p1);
            cur.setAngle(90);
            cur.Length=treeLen;
            cur.radius=treeRadius;
          } else
          {
            cur.Length=len*lengthFactor;
            cur.radius=radius*radiusFactor;
            radius=cur.radius;
            len=cur.Length;
          }
          cur.setAngle(ang);
          curS=cur;
          stack.push(cur);
          break;
        }

      case '+':
        ang=ang-(this.increment.y);
        break;
      case '-':
        ang=ang+(this.increment.y);
        break;
      case '[':
        {
          angleSign.append(curS.angle);
          lengthSign.append(curS.Length);
          radiusSign.append(curS.radius);
          outbrake.append(0);
          node1.add(curS);
          count++;
          break;
        }
      case ']':
        {
          radius=radiusSign.get(count-1);
          len=lengthSign.get(count-1);
          ang=angleSign.get(count-1);
          angleSign.remove(angleSign.size()-1);
          lengthSign.remove(lengthSign.size()-1);
          radiusSign.remove(radiusSign.size()-1);
          outbrake.append(1);
          curS.level=1;
          curS=node1.get(count-1);
          node1.remove(count-1);
          count--;

          break;
        }
      default:
        break;
      }
      i++;
    }
  }

五.参考资料
1.了解基于L-系统算法
[1]: 基于L-系统和 Perlin 噪声函数的风吹树动模拟-文档
[2]:基于L-系统和 Perlin 噪声函数的风吹树动模拟–原论文

2.了解perlin噪声
[1]: Perlin在线讲座

3.代码画画
[1]:用代码画画——搞艺术的学编程有啥用?
[2]:开始第一幅“码绘”——以编程作画的基本方法
3.processing学习
[1]: processing学习文档
[2]: processing三维用法

2016-05-31 12:29:40 whuhan2013 阅读数 2178
  • 手绘漫画人体结构与比例入门基础教程

    课程详细介绍了插画常用的软件SAI、PS,数位板的功能和使用技巧,以及插画漫画的基础技巧,包括五官、人体比例的分配,头发、眼睛、衣服的细节刻画,造型原创等,详实的展示了插画漫画的创作思路和过程。

    3596 人正在学习 去看看 竹芝

效果图

原理

大概介绍一下实现原理。首先你得有一张图(废话~),接下来就是把这张图的轮廓提取出来,轮廓提取算法有很多,本人不是搞图像处理的,对图像处理感兴趣的童鞋可以查看相关资料。如果你有好的轮廓提取算法,也可以把源码中的算法替换掉,我们采用的轮廓提取算法是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自动手绘,圆你儿时画家梦

参考链接

Android自动手绘,圆你儿时画家梦! - huachao1001的专栏 - 博客频道 - CSDN.NET

2019-07-14 14:07:20 Hill_L 阅读数 66
  • 手绘漫画人体结构与比例入门基础教程

    课程详细介绍了插画常用的软件SAI、PS,数位板的功能和使用技巧,以及插画漫画的基础技巧,包括五官、人体比例的分配,头发、眼睛、衣服的细节刻画,造型原创等,详实的展示了插画漫画的创作思路和过程。

    3596 人正在学习 去看看 竹芝

对图像的处理,结合了数据库numpy和图像库的PIL的一些简单应用。
图像的处理,整个过程:
1)导入图片;
2)将图片转换成矩阵或者数组格的格式。(原因在于图片以RGB格式保存在数组中)
3)通过数值运算改变RGB的值;(梯度
4)将调整后的数组值,重新生成一个图像文件;
5) 将图像文件保存即可。

没有更多推荐了,返回首页