Android数字图像处理之二值化
下面blabla一段废话心急的同志们可以跳过。
一幅图像包括目标物体,背景还有噪声,怎样从多值的数字图像中取出目标物体,最常用的方法就是设定某一阈值T,用T将图像的数据分成两大部分:大于T的像素群和小于T的像素群。这是研究灰度变换最特殊的方法,成为图像的二值化。二值化处理就是把图像f(x,y)分成目标物体和背景两个区域,然后求其阈值。二值化是数字图像处理中一项最基本的变换方法,通过非0取1、固定阈值、双固定阈值等不同的阈值化变换方法,使一幅灰度图变成了黑白二值图像,将所需的目标部分从复杂的图像背景中脱离出来,以利于以后的研究。
一、非0即1
思路:
- 传入需要处理的图像
- 遍历整个图像取出图像中每个点的像素值存到一个数组中(oldPix)
- 在循环中获取每个像素点的颜色值,并抽取每个像素中的r,g,b,a分量准备处理
- 利用灰度公式计算出每个点的灰度值(范围0-255),并做溢出处理
- 如果某点的灰度值为0则不做处理
- 如果某点的灰度值大于0,则将其灰度值置为255(非0即1)
- 将处理后的灰度值合成像素点的颜色值,存到一个新数组中(newPix)
- 创建一个高度、宽度和原图完全一样的新图
- 将存新数组中的颜色值赋给新图
- 将新图像返回
用于处理图像的函数如下
private Bitmap zeroAndOne(Bitmap bm) {
int width = bm.getWidth();//原图像宽度
int height = bm.getHeight();//原图像高度
int color;//用来存储某个像素点的颜色值
int r, g, b, a;//红,绿,蓝,透明度
//创建空白图像,宽度等于原图宽度,高度等于原图高度,用ARGB_8888渲染,这个不用了解,这样写就行了
Bitmap bmp = Bitmap.createBitmap(width, height
, Bitmap.Config.ARGB_8888);
int[] oldPx = new int[width * height];//用来存储原图每个像素点的颜色信息
int[] newPx = new int[width * height];//用来处理处理之后的每个像素点的颜色信息
/**
* 第一个参数oldPix[]:用来接收(存储)bm这个图像中像素点颜色信息的数组
* 第二个参数offset:oldPix[]数组中第一个接收颜色信息的下标值
* 第三个参数width:在行之间跳过像素的条目数,必须大于等于图像每行的像素数
* 第四个参数x:从图像bm中读取的第一个像素的横坐标
* 第五个参数y:从图像bm中读取的第一个像素的纵坐标
* 第六个参数width:每行需要读取的像素个数
* 第七个参数height:需要读取的行总数
*/
bm.getPixels(oldPx, 0, width, 0, 0, width, height);//获取原图中的像素信息
for (int i = 0; i < width * height; i++) {//循环处理图像中每个像素点的颜色值
color = oldPx[i];//取得某个点的像素值
r = Color.red(color);//取得此像素点的r(红色)分量
g = Color.green(color);//取得此像素点的g(绿色)分量
b = Color.blue(color);//取得此像素点的b(蓝色分量)
a = Color.alpha(color);//取得此像素点的a通道值
//此公式将r,g,b运算获得灰度值,经验公式不需要理解
int gray = (int)((float)r*0.3+(float)g*0.59+(float)b*0.11);
//下面前两个if用来做溢出处理,防止灰度公式得到到灰度超出范围(0-255)
if(gray > 255) {
gray = 255;
}
if(gray < 0) {
gray = 0;
}
if (gray != 0) {//如果某像素的灰度值不是0(黑色)就将其置为255(白色)
gray = 255;
}
newPx[i] = Color.argb(a,gray,gray,gray);//将处理后的透明度(没变),r,g,b分量重新合成颜色值并将其存储在数组中
}
/**
* 第一个参数newPix[]:需要赋给新图像的颜色数组//The colors to write the bitmap
* 第二个参数offset:newPix[]数组中第一个需要设置给图像颜色的下标值//The index of the first color to read from pixels[]
* 第三个参数width:在行之间跳过像素的条目数//The number of colors in pixels[] to skip between rows.
* Normally this value will be the same as the width of the bitmap,but it can be larger(or negative).
* 第四个参数x:从图像bm中读取的第一个像素的横坐标//The x coordinate of the first pixels to write to in the bitmap.
* 第五个参数y:从图像bm中读取的第一个像素的纵坐标//The y coordinate of the first pixels to write to in the bitmap.
* 第六个参数width:每行需要读取的像素个数The number of colors to copy from pixels[] per row.
* 第七个参数height:需要读取的行总数//The number of rows to write to the bitmap.
*/
bmp.setPixels(newPx, 0, width, 0, 0, width, height);//将处理后的像素信息赋给新图
return bmp;//返回处理后的图像
}
tips:表面上看每个像素点的alpha分量并没有用到,但是处理png图像时就能看到效果了。因为png图像的背景是透明的,我们在合成新像素值时利用了原像素点的alph值,也就是新图像的背景也是透明的。如果不用alpha分量,那么处理后的图像背景就不透明了(可能为纯黑纯白或其他颜色)。如果你不处理png图形则在合成颜色信息时可以用下面这个方法。
newPx[i] = Color.rgb(gray,gray,gray);
效果
非0即1法只有被处理图像中的纯黑部分(也就是r,g,b分量都为0)才会保留下来,其他部分都将变成白色。因此在使用范围上有一定的局限性。
二、固定阈值法
思路:
- 传入需要处理的图像
- 获取用户输入的阈值T
- 遍历整个图像取出图像中每个点的像素值存到一个数组中(oldPix)
- 在循环中获取每个像素点的颜色值,并抽取每个像素中的r,g,b,a分量准备处理
- 利用灰度公式计算出每个点的灰度值(范围0-255)
- 如果某点的灰度值小于给定阈值则将此点灰度值置为0(黑色)
- 如果某点的灰度值大于或等于给定阈值,则将该点灰度值置为255(白色)
- 将处理后的灰度值合成像素点的颜色值,存到一个新数组中(newPix)
- 创建一个高度、宽度和原图完全一样的新图
- 将存新数组中的颜色值赋给新图
- 将得到的新图传给ImageView
用于处理图像的函数如下:
private void singleThreshold(final Bitmap bm) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());//创建AlertDialog对话框
builder.setTitle("固定阈值法");//对话框标题
builder.setMessage("请输入阈值");//对话框内容
View view1 = LayoutInflater.from(getContext()).inflate(R.layout.threshold,null);//载入自定义布局
final EditText mSingleThresholdEt = view1.findViewById(R.id.digit_dialog);//自定义布局中的EditText,用于接收用户输入
builder.setView(view1);//将布局设置到对话框中
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {//对话框的确定按钮点击事件
@Override
public void onClick(DialogInterface dialogInterface, int i) {
int width = bm.getWidth();//原图像宽度
int height = bm.getHeight();//原图高度
int color;//用来存储某个像素点的颜色值
int r, g, b, a;//红,绿,蓝,透明度
int digit = 0;//用于存储用户在对话框中属于的阈值
//创建空白图像,宽度等于原图宽度,高度等于原图高度,用ARGB_8888渲染,这个不用了解,这样写就行了
Bitmap bmp = Bitmap.createBitmap(width, height
, Bitmap.Config.ARGB_8888);
int[] oldPx = new int[width * height];//用来存储原图每个像素点的颜色信息
int[] newPx = new int[width * height];//用来处理处理之后的每个像素点的颜色信息
/**
* 第一个参数oldPix[]:用来接收(存储)bm这个图像中像素点颜色信息的数组
* 第二个参数offset:oldPix[]数组中第一个接收颜色信息的下标值
* 第三个参数width:在行之间跳过像素的条目数,必须大于等于图像每行的像素数
* 第四个参数x:从图像bm中读取的第一个像素的横坐标
* 第五个参数y:从图像bm中读取的第一个像素的纵坐标
* 第六个参数width:每行需要读取的像素个数
* 第七个参数height:需要读取的行总数
*/
bm.getPixels(oldPx, 0, width, 0, 0, width, height);//获取原图中的像素信息
String str = mSingleThresholdEt.getText().toString();//获取用户输入的内容
if("".equals(str)) {//如果用户输入的内容为空
digit = 0;//将阈值置为0
} else {//否则
digit = Integer.valueOf(str);//将用户输入的阈值转换为整数
}
for (int j = 0; j < width * height; j++) {//循环处理图像中每个像素点的颜色值
color = oldPx[j];//取得某个点的像素值
r = Color.red(color);//取得此像素点的r(红色)分量
g = Color.green(color);//取得此像素点的g(绿色)分量
b = Color.blue(color);//取得此像素点的b(蓝色分量)
a = Color.alpha(color);//取得此像素点的a通道值
//此公式将r,g,b运算获得灰度值,经验公式不需要理解
int gray = (int)((float)r*0.3+(float)g*0.59+(float)b*0.11);
if(gray < digit) {//如果某点像素灰度小于给定阈值
gray = 0;//将该点像素的灰度值置为0(黑色)
} else {//如果某点像素灰度大于或等于给定阈值
gray = 255;//将该点像素的灰度值置为1(白色)
}
newPx[j] = Color.argb(a,gray,gray,gray);//将处理后的透明度(没变),r,g,b分量重新合成颜色值并将其存储在数组中
}
/**
* 第一个参数newPix[]:需要赋给新图像的颜色数组//The colors to write the bitmap
* 第二个参数offset:newPix[]数组中第一个需要设置给图像颜色的下标值//The index of the first color to read from pixels[]
* 第三个参数width:在行之间跳过像素的条目数//The number of colors in pixels[] to skip between rows.
* Normally this value will be the same as the width of the bitmap,but it can be larger(or negative).
* 第四个参数x:从图像bm中读取的第一个像素的横坐标//The x coordinate of the first pixels to write to in the bitmap.
* 第五个参数y:从图像bm中读取的第一个像素的纵坐标//The y coordinate of the first pixels to write to in the bitmap.
* 第六个参数width:每行需要读取的像素个数The number of colors to copy from pixels[] per row.
* 第七个参数height:需要读取的行总数//The number of rows to write to the bitmap.
*/
bmp.setPixels(newPx, 0, width, 0, 0, width, height);//将处理后的像素信息赋给新图
mImageView.setImageBitmap(bmp);//将处理后的新图赋给ImageView
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
下面是我们在接收用户输入时使用的对话框所引入的自定义布局threshold.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="阈值:"/>
<EditText
android:id="@+id/digit_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
效果
三、双固定阈值法
思路:
- 传入需要处理的图像
- 获取用户输入的阈值下限T1和阈值上限T2
- 遍历整个图像取出图像中每个点的像素值存到一个数组中(oldPix)
- 在循环中获取每个像素点的颜色值,并抽取每个像素中的r,g,b,a分量准备处理
- 利用灰度公式计算出每个点的灰度值(范围0-255)
- 如果某点的灰度值小于阈值下限T1或大于阈值上限T2,将其灰度值置为0
- 如果某点的灰度值大于等于阈值下限T1或小于等于阈值上限T2,将其灰度值置为255
- 将处理后的灰度值合成像素点的颜色值,存到一个新数组中(newPix)
- 创建一个高度、宽度和原图完全一样的新图
- 将存新数组中的颜色值赋给新图
- 将得到的新图传给ImageView
用于处理图像的函数如下
private void doubleThreshold(final Bitmap bm) {
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
builder.setTitle("双固定阈值法(0-255-0)");
builder.setMessage("请输入阈值");
View view1 = LayoutInflater.from(getContext()).inflate(R.layout.double_threshold_layout,null);
mLowThresholdEt = view1.findViewById(R.id.low_digit_dialog);
mHighThresholdEt = view1.findViewById(R.id.heigh_digit_dialog);
builder.setView(view1);
builder.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialogInterface, int i) {
int width = bm.getWidth();//原图像宽度
int height = bm.getHeight();//原图高度
int color;//用来存储某个像素点的颜色值
int r, g, b, a;//红,绿,蓝,透明度
//创建空白图像,宽度等于原图宽度,高度等于原图高度,用ARGB_8888渲染,这个不用了解,这样写就行了
Bitmap bmp = Bitmap.createBitmap(width, height
, Bitmap.Config.ARGB_8888);
int[] oldPx = new int[width * height];//用来存储原图每个像素点的颜色信息
int[] newPx = new int[width * height];//用来处理处理之后的每个像素点的颜色信息
/**
* 第一个参数oldPix[]:用来接收(存储)bm这个图像中像素点颜色信息的数组
* 第二个参数offset:oldPix[]数组中第一个接收颜色信息的下标值
* 第三个参数width:在行之间跳过像素的条目数,必须大于等于图像每行的像素数
* 第四个参数x:从图像bm中读取的第一个像素的横坐标
* 第五个参数y:从图像bm中读取的第一个像素的纵坐标
* 第六个参数width:每行需要读取的像素个数
* 第七个参数height:需要读取的行总数
*/
bm.getPixels(oldPx, 0, width, 0, 0, width, height);
String str1 = mLowThresholdEt.getText().toString();
String str2 = mHighThresholdEt.getText().toString();
if("".equals(str1)) {
lowDigit = 0;
} else {
lowDigit = Integer.valueOf(str1);//用户输入的阈值下限
}
if("".equals(str2)) {
heighDigit = 255;
} else {
heighDigit = Integer.valueOf(str2);//用户输入的阈值上限
}
for (int j = 0; j < width * height; j++) {//循环处理图像中每个像素点的颜色值
color = oldPx[j];//取得某个点的像素值
r = Color.red(color);//取得此像素点的r(红色)分量
g = Color.green(color);//取得此像素点的g(绿色)分量
b = Color.blue(color);//取得此像素点的b(蓝色分量)
a = Color.alpha(color);//取得此像素点的a通道值
//此公式将r,g,b运算获得灰度值,经验公式不需要理解
int gray = (int)((float)r*0.3+(float)g*0.59+(float)b*0.11);
if(gray < lowDigit || gray > heighDigit) {//如果某点像素的灰度值小于阈值下限或大于阈值上限
gray = 0;//将该点灰度值置为0(黑色)
} else {//如果某点像素的灰度值大于等于阈值下限或小于等于阈值上限
gray = 255;//将该点灰度值置为255(白色)
}
newPx[j] = Color.argb(a,gray,gray,gray);//将处理后的透明度(没变),r,g,b分量重新合成颜色值并将其存储在数组中
}
/**
* 第一个参数newPix[]:需要赋给新图像的颜色数组//The colors to write the bitmap
* 第二个参数offset:newPix[]数组中第一个需要设置给图像颜色的下标值//The index of the first color to read from pixels[]
* 第三个参数width:在行之间跳过像素的条目数//The number of colors in pixels[] to skip between rows.
* Normally this value will be the same as the width of the bitmap,but it can be larger(or negative).
* 第四个参数x:从图像bm中读取的第一个像素的横坐标//The x coordinate of the first pixels to write to in the bitmap.
* 第五个参数y:从图像bm中读取的第一个像素的纵坐标//The y coordinate of the first pixels to write to in the bitmap.
* 第六个参数width:每行需要读取的像素个数The number of colors to copy from pixels[] per row.
* 第七个参数height:需要读取的行总数//The number of rows to write to the bitmap.
*/
bmp.setPixels(newPx, 0, width, 0, 0, width, height);//将处理后的像素信息赋给新图
mImageView.setImageBitmap(bmp);//将处理后的新图赋给ImageView
}
});
AlertDialog dialog = builder.create();
dialog.show();
}
tips:此处我们用的模型为(0-255-0)即将小于下限或大于下限的灰度值置为0,将在下限和上限之间的灰度值置为255。读者可以仿照此例,做一个(255-0-255)的双阈值固定法
下面是我们在接收用户输入时使用的对话框所引入的自定义布局double_threshold_layout.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="下限值:"/>
<EditText
android:id="@+id/low_digit_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="上限值:"/>
<EditText
android:id="@+id/heigh_digit_dialog"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</LinearLayout>
效果
代码已上传到github,点击这里可以下载体验