前言:我是小松,今年大四,在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_cutimageView


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_cutactivity_main将分别生成ActivityStillCutBingActivityMainBinding类,在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可以关注公众号【小松与蘑菇】一起学习哦