前言:我是小松,今年大四,在android开发中持续耕耘,快来一起学习把
不知道大家有没有这种烦恼,手上有白底的证件照,但是学校偏偏要交红底的,万般无奈只能去照相馆再照,虽说可以进行PS,但是总归麻烦,现在可以用app一键解决啦
目录
效果如图
项目介绍
架构
java代码有以下类
layout布局仅有两个
由于项目比较复杂,接下来的将不再介绍基础代码,只介绍核心逻辑
布局
布局代码将不再介绍与功能无关的代码
activity_main
<Button
android:id="@+id/start_button"
android:layout_width="310dp"
android:layout_height="100dp"
android:onClick="startImageSegmentation"
android:layout_marginTop="30dp"
android:text="@string/start"
android:textSize="24sp"
/>
这里的开始按钮设置了一个***startImageSegmentation
函数
昨天升级了android3.6,有很多新功能,现在已经可以在同一个界面进行拖拽和代码编辑了,美滋滋哈哈哈
activity_still_cut
功能
MainActivity
MainActivity
主要是进行一些权限的设定和跳转,权限比如存储,选择照片等等
public void startImageSegmentation(View v) {
Intent intent = new Intent(MainActivity.this, StillCutPhotoActivity.class);
startActivity(intent);
}
Constant
这是一个定义修改颜色的模型,因为我们设置了四种换底颜色,如img_001是白色
public class Constant {
public static int[] IMAGES = {R.mipmap.img_001, R.mipmap.img_002,R.mipmap.img_003,R.mipmap.img_004};
public static final String VALUE_KEY = "index_value";
}
BitmapUtils
这是一个处理选出来的图片的工具类,在网上获取的代码,将选择的图片进行裁切等操作放在activity_still_cut
的imageView
中
public class BitmapUtils {
private static final String TAG = "BitmapUtils";
public static void recycleBitmap(Bitmap... bitmaps) {
for (Bitmap bitmap : bitmaps) {
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
bitmap = null;
}
}
}
private static String getImagePath(Activity activity, Uri uri) {
String[] projection = {MediaStore.Images.Media.DATA};
Cursor cursor = activity.managedQuery(uri, projection, null, null, null);
int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
return cursor.getString(columnIndex);
}
public static Bitmap loadFromPath(Activity activity, int id, int width, int height) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
InputStream is = activity.getResources().openRawResource(id);
int sampleSize = calculateInSampleSize(options, width, height);
options.inSampleSize = sampleSize;
options.inJustDecodeBounds = false;
return zoomImage(BitmapFactory.decodeStream(is), width, height);
}
public static Bitmap loadFromPath(Activity activity, Uri uri, int width, int height) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
String path = getImagePath(activity, uri);
BitmapFactory.decodeFile(path, options);
int sampleSize = calculateInSampleSize(options, width, height);
options.inSampleSize = sampleSize;
options.inJustDecodeBounds = false;
Bitmap bitmap = zoomImage(BitmapFactory.decodeFile(path, options), width, height);
return rotateBitmap(bitmap, getRotationAngle(path));
}
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
final int width = options.outWidth;
final int height = options.outHeight;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
// Calculate height and required height scale
final int heightRatio = Math.round((float) height / (float) reqHeight);
// Calculate width and required width scale
final int widthRatio = Math.round((float) width / (float) reqWidth);
// Take the larger of the values
inSampleSize = heightRatio > widthRatio ? heightRatio : widthRatio;
}
return inSampleSize;
}
// Scale pictures to screen width
private static Bitmap zoomImage(Bitmap imageBitmap, int targetWidth, int maxHeight) {
float scaleFactor =
Math.max(
(float) imageBitmap.getWidth() / (float) targetWidth,
(float) imageBitmap.getHeight() / (float) maxHeight);
Bitmap resizedBitmap =
Bitmap.createScaledBitmap(
imageBitmap,
(int) (imageBitmap.getWidth() / scaleFactor),
(int) (imageBitmap.getHeight() / scaleFactor),
true);
return resizedBitmap;
}
/** * Get the rotation angle of the photo * * @param path photo path * @return angle */
public static int getRotationAngle(String path) {
int rotation = 0;
try {
ExifInterface exifInterface = new ExifInterface(path);
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
rotation = 90;
break;
case ExifInterface.ORIENTATION_ROTATE_180:
rotation = 180;
break;
case ExifInterface.ORIENTATION_ROTATE_270:
rotation = 270;
break;
}
} catch (IOException e) {
SmartLog.e(TAG, "Failed to get rotation: " + e.getMessage());
}
return rotation;
}
public static Bitmap rotateBitmap(Bitmap bitmap, int angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
Bitmap result = null;
try {
result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
} catch (OutOfMemoryError e) {
SmartLog.e(TAG, "Failed to rotate bitmap: " + e.getMessage());
}
if (result == null) {
return bitmap;
}
return result;
}
}
ImageUtils
这是一个保存图片到本地的工具类,将换好底的图片保存到手机中,也是在网上获取的代码,
package com.example.changebackground.util;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import com.huawei.hms.mlsdk.common.internal.client.SmartLog;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class ImageUtils {
private static final String TAG = "ImageUtils";
private Context context;
public ImageUtils(Context context){
this.context = context;
}
// Save the picture to the system album and refresh it.
public void saveToAlbum(Bitmap bitmap){
File file = null;
String fileName = System.currentTimeMillis() +".jpg";
File root = new File(Environment.getExternalStorageDirectory().getAbsoluteFile(), this.context.getPackageName());
File dir = new File(root, "image");
if(dir.mkdirs() || dir.isDirectory()){
file = new File(dir, fileName);
}
FileOutputStream os = null;
try {
os = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
os.flush();
} catch (FileNotFoundException e) {
SmartLog.e(TAG, e.getMessage());
} catch (IOException e) {
SmartLog.e(TAG, e.getMessage());
}finally {
try {
if(os != null) {
os.close();
}
}catch (IOException e){
SmartLog.e(TAG, e.getMessage());
}
}
if(file == null){
return;
}
// Gallery refresh.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
String path = null;
try {
path = file.getCanonicalPath();
} catch (IOException e) {
SmartLog.e(TAG, e.getMessage());
}
MediaScannerConnection.scanFile(this.context, new String[]{path}, null,
new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
mediaScanIntent.setData(uri);
ImageUtils.this.context.sendBroadcast(mediaScanIntent);
}
});
} else {
String relationDir = file.getParent();
File file1 = new File(relationDir);
this.context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.fromFile(file1.getAbsoluteFile())));
}
}
}
核心类:StillCutActivity
接下来到了我们今天的核心步骤,如何识别出图片中的人脸人体,并进行背景替换呢?
是使用了华为的SDK,你需要先注册华为的账号,然后
将project处的build.gradle
修改
allprojects {
repositories {
jcenter()
google()
maven {url 'http://developer.huawei.com/repo/'}
}
}
在app处的build.gradle
添加依赖
implementation 'com.huawei.hms:ml-computer-vision-segmentation:1.0.3.300'
implementation 'com.huawei.hms:ml-computer-vision-image-segmentation-body-model:1.0.3.300'
然后在图像分割进行相关SDK接口的调用学习
首先创建视图
onCreate方法
@Override
public void onCreate(Bundle savedInstance) {
super.onCreate(savedInstance);
activityStillCutBinding = ActivityStillCutBinding.inflate(LayoutInflater.from(this));
setContentView(activityStillCutBinding.getRoot());
preview = activityStillCutBinding.previewPane;
activityStillCutBinding.back.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
finish();
}
});
isLandScape = (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE);
initAction(activityStillCutBinding);
}
这里的activityStillCutBinding是在新的android 3.6中推出的视图绑定功能,只需要在app处的build.gradle
中添加
android{
viewBinding {
enabled = true
}
}
即可开启视图绑定效果,activity_still_cut
和activity_main
将分别生成ActivityStillCutBing
和ActivityMainBinding
类,在Activity中可以直接调用,他们里面的控件,只要有id就可以以.id的形式调用,加载根布局的方法是activityStillCutBinding.getRoot()
InitActivon方法
private void initAction(ActivityStillCutBinding activityStillCutBinding) {
activityStillCutBinding.relativeChooseImg.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
selectLocalImage( REQUEST_CHOOSE_ORIGINPIC);
}
});
// Outline the edge.
activityStillCutBinding.relativateCut.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (imageUri == null) {
Toast.makeText( getApplicationContext(), R.string.please_select_picture, Toast.LENGTH_SHORT).show();
} else {
createImageTransactor();
Toast.makeText( getApplicationContext(), R.string.cut_success, Toast.LENGTH_SHORT).show();
}
}
});
// Save the processed picture.
activityStillCutBinding.relativateSave.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if ( processedImage == null) {
Toast.makeText( getApplicationContext(), R.string.no_pic_neededSave, Toast.LENGTH_SHORT).show();
try {
throw new Exception("null processed image");
} catch (Exception e) {
SmartLog.e(StillCutPhotoActivity.TAG, e.getMessage());
}
} else {
ImageUtils imageUtils = new ImageUtils( getApplicationContext());
imageUtils.saveToAlbum( processedImage);
Toast.makeText( getApplicationContext(), R.string.save_success, Toast.LENGTH_SHORT).show();
}
}
});
}
这里是为activity_still_cut
底部的三个按钮设置***,他们的id分别是relative_chooseImg
,relativate_cut
,relativate_save
,转成属性后,自动去掉下划线,下划线后第一个字母大写
selectLocalImage
方法
加载图片
private void selectLocalImage(int requestCode) {
Intent intent = new Intent(Intent.ACTION_PICK, null);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
startActivityForResult(intent, requestCode);
}
createImageTransactor方法
这个方法就是调用SDK中的API进行图像分割和底色替换,具体API含义可以在图像分割开发步骤中查询
private void createImageTransactor() {
MLImageSegmentationSetting setting = new MLImageSegmentationSetting.Factory()
.setAnalyzerType(MLImageSegmentationSetting.BODY_SEG)
.setExact(true)
.create();
analyzer = MLAnalyzerFactory.getInstance().getImageSegmentationAnalyzer(setting);
if (isChosen(originBitmap)) {
MLFrame mlFrame = new MLFrame.Creator().setBitmap(originBitmap).create();
Task<MLImageSegmentation> task = analyzer.asyncAnalyseFrame(mlFrame);
task.addOnSuccessListener(new OnSuccessListener<MLImageSegmentation>() {
@Override
public void onSuccess(MLImageSegmentation mlImageSegmentationResults) {
// 转换成功
if (mlImageSegmentationResults != null) {
foreground = mlImageSegmentationResults.getForeground();
preview.setImageBitmap( foreground);
processedImage = ((BitmapDrawable) ((ImageView) preview).getDrawable()).getBitmap();
changeBackground();
} else {
displayFailure();
}
}
}).addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(Exception e) {
// 转换失败
displayFailure();
return;
}
});
} else {
Toast.makeText(getApplicationContext(), R.string.please_select_picture, Toast.LENGTH_SHORT).show();
return;
}
}
上面只是分割了图像,接下来进行换底操作,
changeBackground
private void changeBackground() {
if (index< 0) {
Toast.makeText(getApplicationContext(), R.string.please_select_picture, Toast.LENGTH_SHORT).show();
} else {
int id = Constant.IMAGES[index];
loadOriginImage();
Pair<Integer, Integer> targetedSize = getTargetSize();
backgroundBitmap = BitmapUtils.loadFromPath(StillCutPhotoActivity.this, id, targetedSize.first, targetedSize.second);
}
if (isChosen(foreground) && isChosen(backgroundBitmap)) {
BitmapDrawable drawable = new BitmapDrawable(backgroundBitmap);
preview.setDrawingCacheEnabled(true);
preview.setBackground(drawable);
preview.setImageBitmap(foreground);
processedImage = Bitmap.createBitmap( preview.getDrawingCache());
preview.setDrawingCacheEnabled(false);
} else {
Toast.makeText(getApplicationContext(), R.string.please_select_picture, Toast.LENGTH_SHORT).show();
return;
}
}
代码中的preview
就是显示图片的ImageView
,当判定好人物前景和图像背景后,只需要将图像背景修改为我们指定的颜色即可
最后是保存,保存使用的是ImageUtil
类,只需要在保存的***按钮中调用这个类的相关函数即可
写在最后
新的android 3.6其实还有一些小问题,但是总体来说,相对于3.5升级了很多有用的功能,快去体验一下吧
更多实战android app可以关注公众号【小松与蘑菇】一起学习哦