原文部落格:Doi技術團隊
連結位址:https://blog.doiduoyi.com/authors/1584446358138
初心:記錄優秀的Doi技術團隊學習經曆
本文連結:Android基于圖像語義分割實作人物背景更換
本教程是通過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個線程預測。
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都可以實作語義分割了。
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轉換為浮點數組,同時對圖像進行預處理,如通道順序的變換,有的模型還需要資料的标準化,但這裡沒有使用到。
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的為人物。
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中把模型複制到緩存目錄中,然後加載圖像語義分割模型。
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();
}
複制
建立幾個按鈕,來控制圖檔背景的更換。
// 擷取控件
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, "圖檔儲存失敗");
}
}
});
複制
首先需要選擇包含人物的圖檔,這時就需要對圖像進行預測,擷取語義分割結果,然後将圖像放大的跟原圖像一樣大小,并做這個臨時的畫布。
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();
}
複制
最後在這裡實作人物背景的更換,
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;
}
複制
實作的效果如下: