前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android基于图像语义分割实现人物背景更换

Android基于图像语义分割实现人物背景更换

作者头像
夜雨飘零
修改2023-06-04 16:22:45
1K0
修改2023-06-04 16:22:45
举报
文章被收录于专栏:CSDN博客CSDN博客

本教程是通过PaddlePaddle的PaddleSeg实现的,该开源库的地址为:http://github.com/PaddlPaddle/PaddleSeg ,使用开源库提供的预训练模型实现人物的图像语义分割,最终部署到Android应用上。关于如何在Android应用上使用PaddlePaddle模型,可以参考笔者的这篇文章《基于Paddle Lite在Android手机上实现图像分类》

本教程开源代码地址:https://github.com/yeyupiaoling/ChangeHumanBackground

图像语义分割工具

首先编写一个可以在Android应用使用PaddlePaddle的图像语义分割模型的工具类,通过是这个PaddleLiteSegmentation这个java工具类实现模型的加载和图像的预测。

首先是加载模型,获得一个预测器,其中inputShape为图像的输入大小,NUM_THREADS为使用线程数来预测图像,最高可以支持4个线程预测。

代码语言:javascript
复制
    private PaddlePredictor paddlePredictor;
    private Tensor inputTensor;
    public static long[] inputShape = new long[]{1, 3, 513, 513};
    private static final int NUM_THREADS = 4;

    /**
     * @param modelPath model path
     */
    public PaddleLiteSegmentation(String modelPath) throws Exception {
        File file = new File(modelPath);
        if (!file.exists()) {
            throw new Exception("model file is not exists!");
        }
        try {
            MobileConfig config = new MobileConfig();
            config.setModelFromFile(modelPath);
            config.setThreads(NUM_THREADS);
            config.setPowerMode(PowerMode.LITE_POWER_HIGH);
            paddlePredictor = PaddlePredictor.createPaddlePredictor(config);

            inputTensor = paddlePredictor.getInput(0);
            inputTensor.resize(inputShape);
        } catch (Exception e) {
            e.printStackTrace();
            throw new Exception("load model fail!");
        }
    }

在预测开始之前,写两个重构方法,这个我们这个工具不管是图片路径还是图像的Bitmap都可以实现语义分割了。

代码语言:javascript
复制
    public long[] predictImage(String image_path) throws Exception {
        if (!new File(image_path).exists()) {
            throw new Exception("image file is not exists!");
        }
        FileInputStream fis = new FileInputStream(image_path);
        Bitmap bitmap = BitmapFactory.decodeStream(fis);
        long[] result = predictImage(bitmap);
        if (bitmap.isRecycled()) {
            bitmap.recycle();
        }
        return result;
    }

    public long[] predictImage(Bitmap bitmap) throws Exception {
        return predict(bitmap);
    }

现在还不能预测,还需要对图像进行预处理的方法,预测器输入的是一个浮点数组,而不是一个Bitmap对象,所以需要这样的一个工具方法,把图像Bitmap转换为浮点数组,同时对图像进行预处理,如通道顺序的变换,有的模型还需要数据的标准化,但这里没有使用到。

代码语言:javascript
复制
    private float[] getScaledMatrix(Bitmap bitmap) {
        int channels = (int) inputShape[1];
        int width = (int) inputShape[2];
        int height = (int) inputShape[3];
        float[] inputData = new float[channels * width * height];
        Bitmap rgbaImage = bitmap.copy(Bitmap.Config.ARGB_8888, true);
        Bitmap scaleImage = Bitmap.createScaledBitmap(rgbaImage, width, height, true);
        Log.d(TAG, scaleImage.getWidth() +  ", " + scaleImage.getHeight());

        if (channels == 3) {
            // RGB = {0, 1, 2}, BGR = {2, 1, 0}
            int[] channelIdx = new int[]{0, 1, 2};
            int[] channelStride = new int[]{width * height, width * height * 2};
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    int color = scaleImage.getPixel(x, y);
                    float[] rgb = new float[]{(float) red(color), (float) green(color), (float) blue(color)};
                    inputData[y * width + x] = rgb[channelIdx[0]];
                    inputData[y * width + x + channelStride[0]] = rgb[channelIdx[1]];
                    inputData[y * width + x + channelStride[1]] = rgb[channelIdx[2]];
                }
            }
        } else if (channels == 1) {
            for (int y = 0; y < height; y++) {
                for (int x = 0; x < width; x++) {
                    int color = scaleImage.getPixel(x, y);
                    float gray = (float) (red(color) + green(color) + blue(color));
                    inputData[y * width + x] = gray;
                }
            }
        } else {
            Log.e(TAG, "图片的通道数必须是1或者3");
        }
        return inputData;
    }

最后就可以执行预测了,预测的结果是一个数组,它代表了整个图像的语义分割的情况,0的为背景,1的为人物。

代码语言:javascript
复制
    private long[] predict(Bitmap bmp) throws Exception {
        float[] inputData = getScaledMatrix(bmp);
        inputTensor.setData(inputData);

        try {
            paddlePredictor.run();
        } catch (Exception e) {
            throw new Exception("predict image fail! log:" + e);
        }
        Tensor outputTensor = paddlePredictor.getOutput(0);
        long[] output = outputTensor.getLongData();
        long[] outputShape = outputTensor.shape();
        Log.d(TAG, "结果shape:"+ Arrays.toString(outputShape));
        return output;
    }

实现人物背景更换

MainActivity中,程序加载的时候就从assets中把模型复制到缓存目录中,然后加载图像语义分割模型。

代码语言:javascript
复制
String segmentationModelPath = getCacheDir().getAbsolutePath() + File.separator + "model.nb";
Utils.copyFileFromAsset(MainActivity.this, "model.nb", segmentationModelPath);
try {
    paddleLiteSegmentation = new PaddleLiteSegmentation(segmentationModelPath);
    Toast.makeText(MainActivity.this, "模型加载成功!", Toast.LENGTH_SHORT).show();
    Log.d(TAG, "模型加载成功!");
} catch (Exception e) {
    Toast.makeText(MainActivity.this, "模型加载失败!", Toast.LENGTH_SHORT).show();
    Log.d(TAG, "模型加载失败!");
    e.printStackTrace();
    finish();
}

创建几个按钮,来控制图片背景的更换。

代码语言:javascript
复制
// 获取控件
Button selectPicture = findViewById(R.id.select_picture);
Button selectBackground = findViewById(R.id.select_background);
Button savePicture = findViewById(R.id.save_picture);
imageView = findViewById(R.id.imageView);
selectPicture.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 打开相册
        Intent intent = new Intent(Intent.ACTION_PICK);
        intent.setType("image/*");
        startActivityForResult(intent, 0);
    }
});
selectBackground.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (resultPicture != null){
            // 打开相册
            Intent intent = new Intent(Intent.ACTION_PICK);
            intent.setType("image/*");
            startActivityForResult(intent, 1);
        }else {
            Toast.makeText(MainActivity.this, "先选择人物图片!", Toast.LENGTH_SHORT).show();
        }
    }
});
savePicture.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 保持图片
        String savePth = Utils.saveBitmap(mergeBitmap1);
        if (savePth != null) {
            Toast.makeText(MainActivity.this, "图片保存:" + savePth, Toast.LENGTH_SHORT).show();
            Log.d(TAG, "图片保存:" + savePth);
        } else {
            Toast.makeText(MainActivity.this, "图片保存失败", Toast.LENGTH_SHORT).show();
            Log.d(TAG, "图片保存失败");
        }
    }
});

首先需要选择包含人物的图片,这时就需要对图像进行预测,获取语义分割结果,然后将图像放大的跟原图像一样大小,并做这个临时的画布。

代码语言:javascript
复制
Uri image_uri = data.getData();
image_path = Utils.getPathFromURI(MainActivity.this, image_uri);
try {
    // 预测图像
    FileInputStream fis = new FileInputStream(image_path);
    Bitmap b = BitmapFactory.decodeStream(fis);
    long start = System.currentTimeMillis();
    long[] result = paddleLiteSegmentation.predictImage(image_path);
    long end = System.currentTimeMillis();

    // 创建一个任务为全黑色,背景完全透明的图片
    humanPicture = b.copy(Bitmap.Config.ARGB_8888, true);
    final int[] colors_map = {0x00000000, 0xFF000000};
    int[] objectColor = new int[result.length];

    for (int i = 0; i < result.length; i++) {
        objectColor[i] = colors_map[(int) result[i]];
    }
    Bitmap.Config config = humanPicture.getConfig();
    Bitmap outputImage = Bitmap.createBitmap(objectColor, (int) PaddleLiteSegmentation.inputShape[2], (int) PaddleLiteSegmentation.inputShape[3], config);
    resultPicture = Bitmap.createScaledBitmap(outputImage, humanPicture.getWidth(), humanPicture.getHeight(), true);

    imageView.setImageBitmap(b);
    Log.d(TAG, "预测时间:" + (end - start) + "ms");
} catch (Exception e) {
    e.printStackTrace();
}

最后在这里实现人物背景的更换,

代码语言:javascript
复制
Uri image_uri = data.getData();
image_path = Utils.getPathFromURI(MainActivity.this, image_uri);
try {
    FileInputStream fis = new FileInputStream(image_path);
    changeBackgroundPicture = BitmapFactory.decodeStream(fis);
    mergeBitmap1 = draw();
    imageView.setImageBitmap(mergeBitmap1);
} catch (Exception e) {
    e.printStackTrace();
}

// 实现换背景
public Bitmap draw() {
    // 创建一个对应人物位置透明其他正常的背景图
    Bitmap bgBitmap = Bitmap.createScaledBitmap(changeBackgroundPicture, resultPicture.getWidth(), resultPicture.getHeight(), true);
    for (int y = 0; y < resultPicture.getHeight(); y++) {
        for (int x = 0; x < resultPicture.getWidth(); x++) {
            int color = resultPicture.getPixel(x, y);
            int a = Color.alpha(color);
            if (a == 255) {
                bgBitmap.setPixel(x, y, Color.TRANSPARENT);
            }
        }
    }

    // 添加画布,保证透明
    Bitmap bgBitmap2 = Bitmap.createBitmap(bgBitmap.getWidth(), bgBitmap.getHeight(), Bitmap.Config.ARGB_8888);
    Canvas canvas1 = new Canvas(bgBitmap2);
    canvas1.drawBitmap(bgBitmap, 0, 0, null);

    return mergeBitmap(humanPicture, bgBitmap2);
}

// 合并两张图片
public static Bitmap mergeBitmap(Bitmap backBitmap, Bitmap frontBitmap) {
    Bitmap bitmap = backBitmap.copy(Bitmap.Config.ARGB_8888, true);
    Canvas canvas = new Canvas(bitmap);
    Rect baseRect = new Rect(0, 0, backBitmap.getWidth(), backBitmap.getHeight());
    Rect frontRect = new Rect(0, 0, frontBitmap.getWidth(), frontBitmap.getHeight());
    canvas.drawBitmap(frontBitmap, frontRect, baseRect, null);
    return bitmap;
}

实现的效果如下:

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-08-29 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
作者已关闭评论
0 条评论
热度
最新
推荐阅读
目录
  • 图像语义分割工具
  • 实现人物背景更换
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档