我会将这个系列分为两个部分,分别讲解Android中图像的色彩变换及图像的形状变换两个方面的入门知识。
RGBA模型
RGBA是一种缩写,其含义分别是Red、Green、Blue、Aplha。即红、绿、蓝三原色以及透明度四个选项。我们平时所见到的各种颜色不过是这三原色通过不同的比例混合所得,而alpha则可以控制图片的透明度。
下面介绍一些图像处理中重要的三个概念:
- 色调/色相:物体传递的颜色
- 饱和度:颜色的纯度,从0%(灰)到100%(饱和)来进行描述
- 亮度/明度:颜色的相对明暗程度
Android中,系统提供类ColorMatrix来帮助调整图像的三个重要属性
色调
通过setRotate方法来设定整个图像的色调,第一个是需要设定的颜色,其中,0对应R,1对应G,2对应B。这里为了简单,给它设定一样的值hue。
ColorMatrix hueMatrix = new ColorMatrix();
hueMatrix.setRotate(0,hue); //R
hueMatrix.setRotate(1,hue); //G
hueMatrix.setRotate(2,hue); //B
饱和度
通过setSaturation方法来给图像设置饱和度,通过设置值,就可以控制图片的饱和度。
ColorMatrix saturationMatrix = new ColorMatrix();
saturationMatrix.setSaturation(saturation);
亮度
通过setScale方法来给图像设定亮度,四个参数分别是R亮度,G亮度,B亮度以及透明度亮度。
ColorMatrix lumMatrix = new ColorMatrix();
lumMatrix.setScale(lum,lum,lum,1);
通过ColorMatrix的方法调整图像效果
下面建立一个Android工程,来开始学习使用图像处理。
主界面
首先,建立一个主菜单,来选择对图片进行的修改。
activity_main.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:padding="16dp"
android:gravity="center"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="40dp"
android:textSize="40sp"
android:text="美图秀秀"/>
<Button
android:id="@+id/btn_primary_color"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="PRIMARY COLOR"/>
<Button
android:id="@+id/btn_color_matrix"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="COLOR MATRIX"/>
<Button
android:id="@+id/btn_3"
android:layout_width="match_parent"
android:layout_height="60dp"
android:text="等待实现"/>
</LinearLayout>
MainActivity.java
public class MainActivity extends AppCompatActivity {
private Button primaryButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
primaryButton = (Button)findViewById(R.id.btn_primary_color);
primaryButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
primaryColorEvent();
}
});
}
public void primaryColorEvent(){
Intent intent = new Intent(MainActivity.this,PrimaryColorActivity.class);
startActivity(intent);
}
}
三原色调整界面
可以大致分析一下,如果要调整一张图片,要经过这三个步骤:
- 获取一张图片
- 对图片进行一系列修改
- 返回一张图片
因此,不妨设计一个工具类,来进行图像相关的处理,这样可以提高代码的复用性。
public class ImageUtils {
public static Bitmap handleImageEffect(Bitmap bitmap,float hue,float saturation,float lum){
//由于不能直接在原图上修改,所以创建一个图片,设定宽度高度与原图相同。为32位ARGB图片
Bitmap currentBitmap = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888);
//创建一个和原图相同大小的画布
Canvas canvas = new Canvas(currentBitmap);
//创建笔刷并设置抗锯齿
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
//色相ColorMatrix
ColorMatrix hueMatrix = new ColorMatrix();
hueMatrix.setRotate(0,hue);
hueMatrix.setRotate(1,hue);
hueMatrix.setRotate(2,hue);
//饱和度ColorMatrix
ColorMatrix saturationMatrix = new ColorMatrix();
saturationMatrix.setSaturation(saturation);
//亮度ColorMatrix
ColorMatrix lumMatrix = new ColorMatrix();
lumMatrix.setScale(lum,lum,lum,1);
//将三种效果融合起来
ColorMatrix imageMatrix = new ColorMatrix();
imageMatrix.postConcat(hueMatrix);
imageMatrix.postConcat(saturationMatrix);
imageMatrix.postConcat(lumMatrix);
paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix));
canvas.drawBitmap(bitmap,0,0,paint);
return currentBitmap;
}
}
这里,用到了Canvas类,canvas类是一个画布类,后面进行的操作都会在画布上进行,而不是在原图上。建立了三个ColorMatrix,进行了不同方面的操作,并用postConcat方法将这些Matrix进行融合。通过setColorFilter方法来修改paint的相关属性,然后在canvas上将图片绘制出来。最后将修改后的图片返回出来。
回到我们的界面。设计是用三个Seekbar来分别控制色相、饱和度、亮度三个属性。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<ImageView
android:id="@+id/image_view"
android:layout_width="300dp"
android:layout_height="300dp"
android:scaleType="centerCrop"
android:layout_marginTop="24dp"
android:layout_marginBottom="24dp"
android:layout_centerHorizontal="true"/>
<SeekBar
android:id="@+id/seekbar_hue"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_below="@id/image_view"/>
<SeekBar
android:id="@+id/seekbar_saturation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_below="@id/seekbar_hue"/>
<SeekBar
android:id="@+id/seekbar_lum"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_below="@id/seekbar_saturation"/>
</RelativeLayout>
然后编辑PrimaryColorActivity。这里我们定义了一个最大值及中间值,可以让它从中间值开始变化,然后通过一系列方法来分别获取色相、饱和度、亮度的值。
public class PrimaryColorActivity extends AppCompatActivity implements SeekBar.OnSeekBarChangeListener{
private ImageView mImageView;
private SeekBar mSeekbarHue,mSeekbarSaturation,mSeekbarLum;
private final static int MAX_VALUE = 255; //最大值
private final static int MID_VALUE = 127; //中间值
private float mHue,mSaturation,mLum;
private Bitmap bitmap;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_primary_color);
//获取图片
bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test1);
mImageView = (ImageView)findViewById(R.id.image_view);
mImageView.setImageBitmap(bitmap);
mSeekbarHue = (SeekBar)findViewById(R.id.seekbar_hue);
mSeekbarSaturation = (SeekBar)findViewById(R.id.seekbar_saturation);
mSeekbarLum = (SeekBar)findViewById(R.id.seekbar_lum);
mSeekbarHue.setOnSeekBarChangeListener(this);
mSeekbarSaturation.setOnSeekBarChangeListener(this);
mSeekbarLum.setOnSeekBarChangeListener(this);
mSeekbarHue.setMax(MAX_VALUE);
mSeekbarSaturation.setMax(MAX_VALUE);
mSeekbarLum.setMax(MAX_VALUE);
mSeekbarHue.setProgress(MID_VALUE);
mSeekbarSaturation.setProgress(MID_VALUE);
mSeekbarLum.setProgress(MID_VALUE);
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
switch (seekBar.getId()){
case R.id.seekbar_hue:
//将0-255的值转换为色调值
mHue = (progress - MID_VALUE)*1.0F/MID_VALUE*180;
break;
case R.id.seekbar_saturation:
//将0-255值转换为饱和度值
mSaturation = progress*1.0F/MID_VALUE;
break;
case R.id.seekbar_lum:
//将0-255的值转换为亮度值
mLum = progress*1.0F/MID_VALUE;
break;
}
mImageView.setImageBitmap(ImageUtils.handleImageEffect(bitmap,
mHue,mSaturation,mLum));
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
}
运行后,拖动Seekbar,可以发现,我们成功地改变了图片的各项属性
原理分析——矩阵变换
我们之前用到了ColorMatrix类,众所周知,Matrix是矩阵的意思,所以这里实际上我们是通过操作矩阵来处理图像。Android中提供了如图的颜色矩阵,来帮助我们进行图像效果的处理, 图像中的每一个点都是一个矩阵分量,是由R、G、B、A和1组成:
我们用这个颜色矩阵乘上像素点对应的颜色矩阵分量,就能得到一个新的矩阵R1G1B1A1。如图:
这样,我们就把一个像素点通过颜色矩阵变换成了新的像素点(颜色调整后的效果)。
我们把如图的矩阵称为初始化矩阵,因为它乘上原来的像素点后,仍不变。
下面我们看一下这个矩阵,它在原来矩阵的基础上把两个地方的0变为了100,导致的后果就是原来的R G的值变为了它们加上100的值,也就是每个像素点的R G值都增加了100:
同样的,我们可以看一下这样的一个矩阵,它在原来的初始化矩阵的基础上,把矩阵G上的一个1变为了2,带入后可发现,G变为了原来的两倍,效果就是将整个图像的Green增加了两倍
我们可以发现,颜色矩阵的四行分别控制着像素点的R G B A四个属性,而颜色矩阵的第五列,我们称它为颜色偏移量,它不会直接改变某个颜色的系数,而是在原来的基础上调整整个颜色。
我们要改变一个颜色,不仅仅可以改变偏移量,还可以改变颜色的系数。
用矩阵变换来调整图像效果
我们基于原来的工程,增加一个Activity,并让MainActivity的第二个按钮跳转到此Activity。
首先是布局,我们制作如下的布局,准备在GridLayout中添加20个EditText来代表矩阵,用按钮来应用矩阵。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2"/>
<GridLayout
android:id="@+id/group"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:rowCount="4"
android:columnCount="5"
</GridLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/btn_change"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="改变"/>
<Button
android:id="@+id/btn_reset"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="重置"/>
</LinearLayout>
</LinearLayout>
<
然后,我们修改Activity的代码,动态将EditText添加到GridText,来映射我们的ColorMatrix。通过改变EditText来改变我们的ColorMatrix,使图片效果变化。
public class ColorMatrixActivity extends AppCompatActivity {
private ImageView mImageView;
private GridLayout mGroup;
private Bitmap bitmap;
private int mEtWidth,mEtHeight;
private EditText[] mEditTexts = new EditText[20];
private float[] mColorMatrix = new float[20]; //对应矩阵
private Button changeButton;
private Button resetButton;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_color_matrix);
bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.test1);
mImageView = (ImageView) findViewById(R.id.image_view);
mImageView.setImageBitmap(bitmap);
mGroup = (GridLayout) findViewById(R.id.group);
//动态创建EditText,填充GridLayout
//由于在onCreate中,mGroup还没有创建完成,无法获取宽高
//所以通过post方法,在控件绘制完毕后,执行Runnable的具体方法
mGroup.post(new Runnable() {
@Override
public void run() {
mEtWidth = mGroup.getWidth()/5;
mEtHeight = mGroup.getHeight()/4;
addEditText();
initMatrix();
}
});
changeButton = (Button)findViewById(R.id.btn_change);
changeButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
changeButtonEvent();
}
});
resetButton = (Button)findViewById(R.id.btn_reset);
resetButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
resetButtonEvent();
}
});
}
private void addEditText(){
for (int i=0;i<20;i++){
EditText editText = new EditText(ColorMatrixActivity.this);
mEditTexts[i] = editText;
mGroup.addView(editText,mEtWidth,mEtHeight);
}
}
private void initMatrix(){
for (int i=0;i<20;i++){
if (i%6 == 0){
//i为第0、6、12、18位时
mColorMatrix[i]=1;
mEditTexts[i].setText(String.valueOf(1));
}else{
mColorMatrix[i]=0;
mEditTexts[i].setText(String.valueOf(0));
}
}
}
private void getMatrix(){
for (int i=0;i<20;i++){
mColorMatrix[i] = Float.valueOf(mEditTexts[i].getText().toString());
}
}
private void setImageMatrix(){
Bitmap currentBitmap = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888);
ColorMatrix colorMatrix = new ColorMatrix();
colorMatrix.set(mColorMatrix);
Canvas canvas = new Canvas(currentBitmap);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix));
canvas.drawBitmap(bitmap,0,0,paint);
mImageView.setImageBitmap(currentBitmap);
}
public void changeButtonEvent(){
getMatrix();
setImageMatrix();
}
public void resetButtonEvent(){
initMatrix();
setImageMatrix();
}
}
可以看到,成功改变了图片效果,效果如图
学到这里,我们便可以解答之前为什么要用这样的参数来改变色相、饱和度和亮度了。以亮度为例,我们来看看setScale方法的源码。可以发现,ColorMatrix在内部也是使用这样一个颜色数组,同时将6的倍数位设置为相应的值,以此来改变亮度,说明我们如果在颜色矩阵中想要改变亮度,只需要将每个颜色的值同时提高即可。
/**
* Set this colormatrix to scale by the specified values.
*/
public void setScale(float rScale, float gScale, float bScale,float aScale) {
final float[] a = mArray;
for (int i = 19; i > 0; --i) {
a[i] = 0;
}
a[0] = rScale;
a[6] = gScale;
a[12] = bScale;
a[18] = aScale;
}
通过这些研究,我们可以这样总结:
图像处理,实际上就是研究不同颜色矩阵对图像的处理效果
比如我们在一些图像处理app中常见的怀旧效果,通过下图的设置方法即可得到
通过像素点进行图像处理
图像经过放大后,会呈现一个个点阵,每一个点实际上就是一个像素点。通过RGB的颜色配比,就可以显示出不同的颜色。
下面是一些对像素点处理形成图像特效的例子:
底片效果
对于ABC三个像素点,求B点的底片效果的算法如下。实际上就是对每个坐标点计算它的反色,即可得到
B.r = 255 - B.r;
B.g = 255 - B.g;
B.b = 255 - B.b;
老照片效果
求老照片效果对像素点的算法如下,其中pixR就是当前像素点的R值,以此类推。
newR = (int)(0.393 * pixR + 0.769 * pixG + 0.189 * pixB);
newG = (int)(0.349 * pixR + 0.686 * pixG + 0.168 * pixB);
newB = (int)(0.272 * pixR + 0.534 * pixG + 0.131 * pixB);
浮雕效果
对于ABC三个点,求B点的浮雕效果的算法如下。
B.r = C.r - B.r + 127;
B.g = C.g - B.g + 127;
B.b = C.b - B.b + 127;
下面,我们就通过对像素点的修改来改变图像的显示效果。
先新建一个新的Activity,并在MainActivity的第三个按钮中加上跳转到它的方法。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ImageView
android:id="@+id/image_view1"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<ImageView
android:id="@+id/image_view2"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<ImageView
android:id="@+id/image_view3"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
<ImageView
android:id="@+id/image_view4"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"/>
</LinearLayout>
</LinearLayout>
加入相应特效方法
我们在ImageUtils中加入几个新方法,分别做不同的处理
反色效果
代码如下,我们在里面新建了一个对应图片的像素的数组,然后通过getPixels方法获取所有像素。
getPixels的第二个参数是代表起点的偏移量,第三个参数是控制读取数组时的行距,一般使用width,后面两个参数代表第一次读取像素点的坐标,倒数第二个参数代表我们从bitmap中读取的宽度,最后一个是读取的高度。
然后我们分别对每个像素点通过Color类的red green blue alpha方法来获取r g b a四个值,并通过算法改变它的rgb值,并通过Color的argb方法转换为新的像素数组。需要注意的是,改变它的rgb值时,需要判断一下有没有超过0-255的限制,有的话则赋值为255或0。
Bitmap currentBitmap = Bitmap.createBitmap(width,height,
Bitmap.Config.ARGB_8888);
int[] oldPx = new int[width*height]; //存储像素点数组
int[] newPx = new int[width*height];
bitmap.getPixels(oldPx,0,width,0,0,width,height);
for (int i=0;i<width*height;i++){
color = oldPx[i];
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);
a = Color.alpha(color);
//通过算法计算新的rgb值
r = 255 - r;
g = 255 - g;
b = 255 - b;
if (r > 255) r=255;
else if (r < 0) r=0;
if (g > 255) g=255;
else if (g < 0) g=0;
if (b > 255) b=255;
else if (b < 0) b=0;
newPx[i] = Color.argb(a,r,g,b);
}
currentBitmap.setPixels(newPx,0,width,0,0,width,height);
return currentBitmap;
}
老照片效果
其他代码基本与之前的一样,只是算法稍作变动,并且不能在原来的rgb基础上改动:
public static Bitmap handleImageOldpicture(Bitmap bitmap){
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int color;
int r,g,b,a;
Bitmap currentBitmap = Bitmap.createBitmap(width,height,
Bitmap.Config.ARGB_8888);
int[] oldPx = new int[width*height]; //存储像素点数组
int[] newPx = new int[width*height];
bitmap.getPixels(oldPx,0,width,0,0,width,height);
for (int i=0;i<width*height;i++){
color = oldPx[i];
r = Color.red(color);
g = Color.green(color);
b = Color.blue(color);
a = Color.alpha(color);
int r1,g1,b1;
r1 = (int)(0.393 * r + 0.769 * g + 0.189 * b);
g1 = (int)(0.349 * r + 0.686 * g + 0.168 * b);
b1 = (int)(0.272 * r + 0.534 * g + 0.131 * b);
if (r1 > 255) r1=255;
else if (r1 < 0) r1=0;
if (g1 > 255) g1=255;
else if (g1 < 0) g1=0;
if (b1 > 255) b1=255;
else if (b1 < 0) b1=0;
newPx[i] = Color.argb(a,r1,g1,b1);
}
currentBitmap.setPixels(newPx,0,width,0,0,width,height);
return currentBitmap;
}
浮雕效果
与之前的差不多,唯一需要注意的是,我们需要用到前一个像素点的颜色,所以需要从1开始循环,然后通过相应算法,获取图片
public static Bitmap handleImagePixelsRelief(Bitmap bitmap){
int width = bitmap.getWidth();
int height = bitmap.getHeight();
int color,colorBefore;
int r,g,b,a;
int r1,g1,b1;
Bitmap currentBitmap = Bitmap.createBitmap(width,height,
Bitmap.Config.ARGB_8888);
int[] oldPx = new int[width*height]; //存储像素点数组
int[] newPx = new int[width*height];
bitmap.getPixels(oldPx,0,width,0,0,width,height);
for (int i=1;i<width*height;i++){
//取出前一个点的颜色
colorBefore = oldPx[i-1];
r = Color.red(colorBefore);
g = Color.green(colorBefore);
b = Color.blue(colorBefore);
a = Color.alpha(colorBefore);
color = oldPx[i];
r1 = Color.red(color);
g1 = Color.green(color);
b1 = Color.blue(color);
r = (r - r1 + 127);
g = (g - g1 + 127);
b = (b - b1 + 127);
if (r1 > 255) r1=255;
if (g1 > 255) g1=255;
if (b1 > 255) b1=255;
newPx[i] = Color.argb(a,r,g,b);
}
currentBitmap.setPixels(newPx,0,width,0,0,width,height);
return currentBitmap;
}
查看效果
我们在Activity中调用相应方法,查看效果。
public class PixelEffectActivity extends AppCompatActivity {
private ImageView imageView1,imageView2,imageView3,imageView4;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pixel_effect);
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test1);
imageView1 = (ImageView) findViewById(R.id.image_view1);
imageView2 = (ImageView) findViewById(R.id.image_view2);
imageView3 = (ImageView) findViewById(R.id.image_view3);
imageView4 = (ImageView) findViewById(R.id.image_view4);
imageView1.setImageBitmap(bitmap);
imageView2.setImageBitmap(ImageUtils.handleImageNegative(bitmap));
imageView3.setImageBitmap(ImageUtils.handleImageOldpicture(bitmap));
imageView4.setImageBitmap(ImageUtils.handleImagePixelsRelief(bitmap));
}
}
效果如图
本文在开源项目:https://github.com/Android-Alvin/Android-LearningNotes 中已收录,里面包含不同方向的自学编程路线、面试题集合/面经、及系列技术文章等,资源持续更新中...