Android 不规则封闭区域填充 手指秒变油漆桶
转载请标明出处:
http://blog.csdn.net/lmj623565791/article/details/45954255;
本文出自:【张鸿洋的博客】
一、概述
在上一篇的叙述中,我们通过图层的方式完成了图片颜色的填充(详情请戳:Android 不规则图像填充 小玩着色游戏),不过在着色游戏中更多的还是基于边界的图像的填充。本篇博客将详细描述。
图像的填充有2种经典算法。
- 一种是种子填充法。种子填充法理论上能够填充任意区域和图形,但是这种算法存在大量的反复入栈和大规模的递归,降低了填充效率。
- 另一种是扫描线填充法。
注意:实际上图像填充的算法还是很多的,有兴趣可以去Google学术上去搜一搜。
ok,下面先看看今天的效果图:
ok,可以看到这样的颜色填充比上一篇的基于层的在素材的准备上要easy 很多~~~
二、原理分析
首先我们简述下原理,我们在点击的时候拿到点击点的”颜色”,然后按照我们选择的算法
进行填色即可。
算法1:种子填充法,四联通/八联通
详细介绍,可以参考多边形区域填充算法--递归种子填充算法
算法简介:假设要将某个区域填充成红色。
从用户点击点的像素开始,上下左右(八联通还有左上,左下,右上,右下)去判断颜色,如果四个方向上的颜色与当前点击点的像素一致,则改变颜色至目标色。然后继续上述这个过程。
ok,可以看到这是一个递归的过程,1个点到4个,4个到16个不断的去延伸。如果按照这种算法,你会写出类似这样的代码:
/**
* @param pixels 像素数组
* @param w 宽度
* @param h 高度
* @param pixel 当前点的颜色
* @param newColor 填充色
* @param i 横坐标
* @param j 纵坐标
*/
private void fillColor01(int[] pixels, int w, int h, int pixel, int newColor, int i, int j)
{
int index = j * w + i;
if (pixels[index] != pixel || i >= w || i < 0 || j < 0 || j >= h)
return;
pixels[index] = newColor;
//上
fillColor01(pixels, w, h, pixel, newColor, i, j - 1);
//右
fillColor01(pixels, w, h, pixel, newColor, i + 1, j);
//下
fillColor01(pixels, w, h, pixel, newColor, i, j + 1);
//左
fillColor01(pixels, w, h, pixel, newColor, i - 1, j);
}
代码很简单,但是如果你去运行,会发生StackOverflowException
异常,这个异常主要是因为大量的递归造成的。虽然简单,但是在移动设备上使用该方法不行。
于是,我就想,这个方法不是递归深度过多么,那么我可以使用一个Stack去存像素点,减少递归的深度和次数,于是我把代码改成如下的方式:
/**
* @param pixels 像素数组
* @param w 宽度
* @param h 高度
* @param pixel 当前点的颜色
* @param newColor 填充色
* @param i 横坐标
* @param j 纵坐标
*/
private void fillColor(int[] pixels, int w, int h, int pixel, int newColor, int i, int j)
{
mStacks.push(new Point(i, j));
while (!mStacks.isEmpty())
{
Point seed = mStacks.pop();
Log.e("TAG", "seed = " + seed.x + " , seed = " + seed.y);
int index = seed.y * w + seed.x;
pixels[index] = newColor;
if (seed.y > 0)
{
int top = index - w;
if (pixels[top] == pixel)
{
mStacks.push(new Point(seed.x, seed.y - 1));
}
}
if (seed.y < h - 1)
{
int bottom = index + w;
if (pixels[bottom] == pixel)
{
mStacks.push(new Point(seed.x, seed.y + 1));
}
}
if (seed.x > 0)
{
int left = index - 1;
if (pixels[left] == pixel)
{
mStacks.push(new Point(seed.x - 1, seed.y));
}
}
if (seed.x < w - 1)
{
int right = index + 1;
if (pixels[right] == pixel)
{
mStacks.push(new Point(seed.x + 1, seed.y));
}
}
}
}
方法的思想也比较简单,将当前像素点入栈,然后出栈着色,接下来分别判断四个方向的,如果符合条件也进行入栈(只要栈不为空持续运行)。ok,这个方法我也尝试跑了下,恩,这次不会报错了,但是速度特别的慢~~~~慢得我是不可接受的。(有兴趣可以尝试,记得如果ANR,点击等待)。
这样来看,第一种算法,我们是不考虑了,没有办法使用,主要原因是假设对于矩形同色区域,都是需要填充的,而算法一依然是各种入栈。于是考虑第二种算法
扫描线填充法
详细可参考扫描线种子填充算法的解析和扫描线种子填充算法。
算法思想[4]:
- 初始化一个空的栈用于存放种子点,将种子点(x, y)入栈;
- 判断栈是否为空,如果栈为空则结束算法,否则取出栈顶元素作为当前扫描线的种子点(x, y),y是当前的扫描线;
- 从种子点(x, y)出发,沿当前扫描线向左、右两个方向填充,直到边界。分别标记区段的左、右端点坐标为xLeft和xRight;
- 分别检查与当前扫描线相邻的y - 1和y + 1两条扫描线在区间[xLeft, xRight]中的像素,从xRight开始向xLeft方向搜索,假设扫描的区间为AAABAAC(A为种子点颜色),那么将B和C前面的A作为种子点压入栈中,然后返回第(2)步;
上述参考自参考文献[4],做了些修改,文章[4]中描述算法,测试有一点问题,所以做了修改.
可以看到该算法,基本上是一行一行着色的,这样的话在大块需要着色区域的效率比算法一要高很多。
ok,关于算法的步骤大家目前觉得模糊,一会可以参照我们的代码。选定了算法以后,接下来就开始编码了。
三、编码实现
我们代码中引入了一个边界颜色,如果设置的话,着色的边界参考为该边界颜色,否则会只要与种子颜色不一致为边界。
(一)构造方法与测量
public class ColourImageView extends ImageView
{
private Bitmap mBitmap;
/**
* 边界的颜色
*/
private int mBorderColor = -1;
private boolean hasBorderColor = false;
private Stack<Point> mStacks = new Stack<Point>();
public ColourImageView(Context context, AttributeSet attrs)
{
super(context, attrs);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ColourImageView);
mBorderColor = ta.getColor(R.styleable.ColourImageView_border_color, -1);
hasBorderColor = (mBorderColor != -1);
L.e("hasBorderColor = " + hasBorderColor + " , mBorderColor = " + mBorderColor);
ta.recycle();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int viewWidth = getMeasuredWidth();
int viewHeight = getMeasuredHeight();
//以宽度为标准,等比例缩放view的高度
setMeasuredDimension(viewWidth,
getDrawable().getIntrinsicHeight() * viewWidth / getDrawable().getIntrinsicWidth());
L.e("view's width = " + getMeasuredWidth() + " , view's height = " + getMeasuredHeight());
//根据drawable,去得到一个和view一样大小的bitmap
BitmapDrawable drawable = (BitmapDrawable) getDrawable();
Bitmap bm = drawable.getBitmap();
mBitmap = Bitmap.createScaledBitmap(bm, getMeasuredWidth(), getMeasuredHeight(), false);
}
可以看到我们选择的是继承ImageView,这样只需要将图片设为src即可。
构造方法中获取我们的自定义边界颜色,当然可以不设置~~
重写测量的目的是为了获取一个和View一样大小的Bitmap便于我们操作。
接下来就是点击啦~
(二)onTouchEvent
@Override
public boolean onTouchEvent(MotionEvent event)
{
final int x = (int) event.getX();
final int y = (int) event.getY();
if (event.getAction() == MotionEvent.ACTION_DOWN)
{
//填色
fillColorToSameArea(x, y);
}
return super.onTouchEvent(event);
}
/**
* 根据x,y获得改点颜色,进行填充
*
* @param x
* @param y
*/
private void fillColorToSameArea(int x, int y)
{
Bitmap bm = mBitmap;
int pixel = bm.getPixel(x, y);
if (pixel == Color.TRANSPARENT || (hasBorderColor && mBorderColor == pixel))
{
return;
}
int newColor = randomColor();
int w = bm.getWidth();
int h = bm.getHeight();
//拿到该bitmap的颜色数组
int[] pixels = new int[w * h];
bm.getPixels(pixels, 0, w, 0, 0, w, h);
//填色
fillColor(pixels, w, h, pixel, newColor, x, y);
//重新设置bitmap
bm.setPixels(pixels, 0, w, 0, 0, w, h);
setImageDrawable(new BitmapDrawable(bm));
}
可以看到,我们在onTouchEvent中获取(x,y),然后拿到改点坐标:
- 获得点击点颜色,获得整个bitmap的像素数组
- 改变这个数组中的颜色
- 然后重新设置给bitmap,重新设置给ImageView
重点就是通过fillColor
去改变数组中的颜色
/**
* @param pixels 像素数组
* @param w 宽度
* @param h 高度
* @param pixel 当前点的颜色
* @param newColor 填充色
* @param i 横坐标
* @param j 纵坐标
*/
private void fillColor(int[] pixels, int w, int h, int pixel, int newColor, int i, int j)
{
//步骤1:将种子点(x, y)入栈;
mStacks.push(new Point(i, j));
//步骤2:判断栈是否为空,
// 如果栈为空则结束算法,否则取出栈顶元素作为当前扫描线的种子点(x, y),
// y是当前的扫描线;
while (!mStacks.isEmpty())
{
/**
* 步骤3:从种子点(x, y)出发,沿当前扫描线向左、右两个方向填充,
* 直到边界。分别标记区段的左、右端点坐标为xLeft和xRight;
*/
Point seed = mStacks.pop();
//L.e("seed = " + seed.x + " , seed = " + seed.y);
int count = fillLineLeft(pixels, pixel, w, h, newColor, seed.x, seed.y);
int left = seed.x - count + 1;
count = fillLineRight(pixels, pixel, w, h, newColor, seed.x + 1, seed.y);
int right = seed.x + count;
/**
* 步骤4:
* 分别检查与当前扫描线相邻的y - 1和y + 1两条扫描线在区间[xLeft, xRight]中的像素,
* 从xRight开始向xLeft方向搜索,假设扫描的区间为AAABAAC(A为种子点颜色),
* 那么将B和C前面的A作为种子点压入栈中,然后返回第(2)步;
*/
//从y-1找种子
if (seed.y - 1 >= 0)
findSeedInNewLine(pixels, pixel, w, h, seed.y - 1, left, right);
//从y+1找种子
if (seed.y + 1 < h)
findSeedInNewLine(pixels, pixel, w, h, seed.y + 1, left, right);
}
}
可以看到我已经很清楚的将该算法的四个步骤标识到该方法中。好了,最后就是一些依赖的细节上的方法:
/**
* 在新行找种子节点
*
* @param pixels
* @param pixel
* @param w
* @param h
* @param i
* @param left
* @param right
*/
private void findSeedInNewLine(int[] pixels, int pixel, int w, int h, int i, int left, int right)
{
/**
* 获得该行的开始索引
*/
int begin = i * w + left;
/**
* 获得该行的结束索引
*/
int end = i * w + right;
boolean hasSeed = false;
int rx = -1, ry = -1;
ry = i;
/**
* 从end到begin,找到种子节点入栈(AAABAAAB,则B前的A为种子节点)
*/
while (end >= begin)
{
if (pixels[end] == pixel)
{
if (!hasSeed)
{
rx = end % w;
mStacks.push(new Point(rx, ry));
hasSeed = true;
}
} else
{
hasSeed = false;
}
end--;
}
}
/**
* 往右填色,返回填充的个数
*
* @return
*/
private int fillLineRight(int[] pixels, int pixel, int w, int h, int newColor, int x, int y)
{
int count = 0;
while (x < w)
{
//拿到索引
int index = y * w + x;
if (needFillPixel(pixels, pixel, index))
{
pixels[index] = newColor;
count++;
x++;
} else
{
break;
}
}
return count;
}
/**
* 往左填色,返回填色的数量值
*
* @return
*/
private int fillLineLeft(int[] pixels, int pixel, int w, int h, int newColor, int x, int y)
{
int count = 0;
while (x >= 0)
{
//计算出索引
int index = y * w + x;
if (needFillPixel(pixels, pixel, index))
{
pixels[index] = newColor;
count++;
x--;
} else
{
break;
}
}
return count;
}
private boolean needFillPixel(int[] pixels, int pixel, int index)
{
if (hasBorderColor)
{
return pixels[index] != mBorderColor;
} else
{
return pixels[index] == pixel;
}
}
/**
* 返回一个随机颜色
*
* @return
*/
private int randomColor()
{
Random random = new Random();
int color = Color.argb(255, random.nextInt(256), random.nextInt(256), random.nextInt(256));
return color;
}
ok,到此,代码就介绍完毕了~~~
最后贴下布局文件~~
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:zhy="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<com.zhy.colour_app_01.ColourImageView
zhy:border_color="#FF000000"
android:src="@drawable/image_007"
android:background="#33ff0000"
android:layout_width="match_parent"
android:layout_centerInParent="true"
android:layout_height="match_parent"/>
</RelativeLayout>
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ColourImageView">
<attr name="border_color" format="color|reference"></attr>
</declare-styleable>
</resources>
参考链接
- http://www.docin.com/p-811949206.html (扫描线种子填充算法的解析)
- http://blog.csdn.net/jia20003/article/details/8908464 (图像处理之泛洪填充算法(Flood Fill Algorithm))
- http://blog.csdn.net/orbit/article/details/7323090 (递归种子填充算法)
- http://blog.csdn.net/orbit/article/details/7343236(扫描线种子填充算法,貌似有点小问题)
ok~
源码点击下载,欢迎star or fork ~~~
群号:264950424,欢迎入群
微信公众号:hongyangAndroid
(欢迎关注,第一时间推送博文信息)
版权声明:本文为博主原创文章,未经博主允许不得转载。
Android 不规则封闭区域填充 手指秒变油漆桶的更多相关文章
- Android不规则点击区域详解
Android不规则点击区域详解 摘要 今天要和大家分享的是Android不规则点击区域,准确说是在视觉上不规则的图像点击响应区域分发. 其实这个问题比较简单,对于很多人来说根本不值得做为一篇博文写出 ...
- 微软大礼包 | 集合在线学习资源,助你秒变AI达人
编者按:人工智能的浪潮正如火如荼地袭来,未来人工智能将大有所为,人们的生活轨迹也正在技术不断向前推进的过程中逐渐改变.人工智能不是科研人员或开发人员的专属,微软希望能够将人工智能带给每个人,从开发者到 ...
- Python文字转换语音,让你的文字会「说话」,抠脚大汉秒变撒娇萌妹
作者 | pk 哥 来源公众号 | Python知识圈(ID:PythonCircle) APP 也有文字转换为语音的功能,虽然听起来很别扭,但是基本能解决长辈们看不清文字或者眼睛疲劳,通过文字转换为 ...
- 国庆出游神器:魔幻黑科技换天造物,让vlog秒变科幻大片!
摘要:国庆旅游景点人太多,拍出来的照片全是人人人.车车车,该怎么办?不妨试试这个黑科技,让你的出游vlog秒变科幻大片. 本文分享自华为云社区<国庆出游神器,魔幻黑科技换天造物,让vlog秒变科 ...
- HMS Core Discovery第17期直播预告|音随我动,秒变音色造型师
[导读] 随着音视频内容品类的不断丰富及音乐创作门槛不断降低,大量用户正热切的参与到全民创作的大潮中.我们应该怎么去拥抱移动端影音潜力市场?音频编辑又可以有什么新玩法? 本期直播<音随我动,秒变 ...
- HMS Core Discovery第17期回顾|音随我动,秒变音色造型师
HMS Core Discovery第17期直播<音随我动,秒变音色造型师>,已于8月25日圆满结束,本期直播我们邀请了HMS Core音频编辑服务的产品经理.技术专家以及创新娱乐类应用& ...
- Android不规则瀑布流照片墙的实现+LruCache算法
watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvZnJhbmNpc3NoaQ==/font/5a6L5L2T/fontsize/400/fill/I0JBQk ...
- 【转】安装Intel HAXM为Android 模拟器加速,30秒内启动完成
http://www.cnblogs.com/Li-Cheng/p/4351966.html http://www.cnblogs.com/csulennon/p/4178404.html https ...
- 安装Intel HAXM为Android 模拟器加速,30秒内启动完成
要求 必备知识 windows 7 基本操作. 运行环境 windows 7(64位); Android Studio 1.1.0;JDK 1.7.0_75(64位);android-sdk_r24 ...
随机推荐
- DB Query Analyzer 6.02 is released, 71 articles concerned have been published
DB Query Analyzer is presented by Master Genfeng, Ma from Chinese Mainland. It has English version n ...
- HTML中的javascript交互
在Android开发中,越来越多的商业项目使用了Android原生控件与WebView进行混合开发,当然不仅仅就是显示一个WebView那么简单,有时候还需要本地Java代码与HTML中的javasc ...
- [ Java学习基础 ] Java的对象容器 -- 集合
当你有很多书时,你会考虑买一个书柜,将你的书分门别类摆放进入.使用了书柜不仅仅使房间变得整洁,也便于以后使用书时方便查找.在计算机中管理对象亦是如此,当获得多个对象后,也需要一个容器将它们管理起来,这 ...
- 使用 focus() 和 blur()
<html> <head> <style type="text/css"> a:active {color:green} </style& ...
- Visual Studio 2013创建自定义多项目模版
首先附上效果图: 可以看到输入解决方案名称后,自动创建了我事先写好的架构,并且项目名及Server层名称都变了,并且依然保持了引用关系. 下面讲具体步骤: 第一步:建立解决方案,并将需要的代码全部写好 ...
- Day8 接口与归一化设计
接口:在程序的使用中,我不能把程序的主体直接提供给使用者,一般是提供一个接口. 为什么要使用接口: 1,接口提取了一群共同的函数,可以把接口当做一个函数的集合. 2,让子类去实现接口中的函数. 归一化 ...
- sublime中安装sublimecodeintel插件
本文是基于在Windows上对sublime进行相关配置. 1.安装sublime,在官网http://www.sublimetext.com/3. 如果是在Linux系统上安装只需要输入命令直接安装 ...
- Install OpenCV 3.0 and Python 2.7+ on Ubuntu
为了防止原文消失或者被墙,转载留个底,最好还是去看原贴,因为随着版本变化,原贴是有人维护升级的 http://www.pyimagesearch.com/2015/06/22/install-Open ...
- Floyd 算法求多源最短路径
Floyd算法: Floyd算法用来找出每对顶点之间的最短距离,它对图的要求是,既可以是无向图也可以是有向图,边权可以为负,但是不能存在负环(可根据最小环的正负来判定). 基本算法: Floyd算法基 ...
- Scala编程入门---数组操作之Array.ArrayBuffer以及遍历数组
在Scala中,Array代表的含义与Java类似,也是长度不可改变的数组.此外,由于Scala与java都是运行在JVM中,双方可以互相调用,因此Scala数组底层实际上是java数组.列如字符串数 ...