#
#作者:韦访
#博客:https://blog.csdn.net/rookie_wei
#微信:1007895847
#添加微信的备注一下是CSDN的
#欢迎大家一起学习
#

1、概述

上一讲我们将SRGAN模型由HDF5转成了tflite,并且验证了我们的tflite模型是对的。这一讲,我们就来实现安卓的APP,我假设你们至少有点安卓APP开发基础的。

环境配置:

操作系统:Win10 64位

显卡:GTX 1080ti

Python:Python3.7

TensorFlow:2.3.0

安卓系统:Android 10

开发工具:Android Studio

2、效果图

APP界面如上图所示,由3个按键,一个进度条和两张图片组成。其中,TEST按键是对APP内置的三张图片随机进出SR处理,SELECT可以允许你从手机中选择图片进行SR处理,SAVE则是保存处理以后的图片到手机中,为了不弄乱你的手机相册,我把图片保存到新建的SuperResolution文件夹下,可以通过手机的文件管理器查看。

进度条则是显示SR处理的进度。左图是原图,右图是经过SR处理以后的高清图。

3、新建并配置Android APP

我这里使用的是Android Studio来开发,新建一个空白的APP。然后,直接运行,先确保APP能跑起来。

然后,配置APP以支持TensorFlow lite,打开app/build.gradle,在dependencies中添加如下代码,

implementation 'org.tensorflow:tensorflow-lite-support:0.0.0-nightly'
implementation 'org.tensorflow:tensorflow-lite-metadata:0.0.0-nightly'
implementation('org.tensorflow:tensorflow-lite:0.0.0-nightly') { changing = true }
implementation('org.tensorflow:tensorflow-lite-gpu:0.0.0-nightly') { changing = true }

然后点击右上角的Sync Now进行同步。

同步成功以后,我们的APP就支持TensorFlow lite了。

4、界面布局

接着,来设计界面的布局,比较简单,没什么太好说的,我就直接贴代码了,

<RelativeLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"><androidx.appcompat.widget.LinearLayoutCompatandroid:id="@+id/button_ll"android:layout_width="match_parent"android:layout_height="150px"android:orientation="horizontal"android:paddingTop="10dp"><Buttonandroid:id="@+id/test_button"android:layout_width="50dp"android:layout_height="wrap_content"android:layout_weight="1"android:layout_marginRight="10dp"android:layout_marginLeft="10dp"android:text="Test" /><Buttonandroid:id="@+id/select_button"android:layout_width="50dp"android:layout_height="wrap_content"android:layout_weight="1"android:layout_marginRight="10dp"android:layout_marginLeft="10dp"android:text="Select"/><Buttonandroid:id="@+id/save_button"android:layout_width="50dp"android:layout_height="wrap_content"android:layout_weight="1"android:layout_marginRight="10dp"android:layout_marginLeft="10dp"android:text="Save"/></androidx.appcompat.widget.LinearLayoutCompat><androidx.appcompat.widget.LinearLayoutCompatandroid:id="@+id/progress_bar_ll"android:layout_below="@+id/button_ll"android:layout_marginTop="10dp"android:layout_width="match_parent"android:layout_height="40dp"><ProgressBarandroid:id="@+id/sr_progress_bar"android:layout_marginLeft="20dp"android:layout_marginRight="20dp"android:layout_width="match_parent"style="?android:attr/progressBarStyleHorizontal"android:layout_gravity="center"android:max="100"android:progress="0"android:layout_height="30dp"/></androidx.appcompat.widget.LinearLayoutCompat><androidx.appcompat.widget.LinearLayoutCompatandroid:layout_below="@+id/progress_bar_ll"android:layout_marginTop="50dp"android:layout_width="match_parent"android:layout_height="match_parent"><LinearLayoutandroid:layout_weight="1"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:text="LR"android:textSize="20dp"android:gravity="center_horizontal"android:layout_width="match_parent"android:layout_height="wrap_content"></TextView><ImageViewandroid:id="@+id/imageview_src"android:layout_weight="1"android:layout_width="match_parent"android:layout_height="match_parent"/><ImageViewandroid:layout_weight="2"android:layout_width="match_parent"android:layout_height="match_parent"/></LinearLayout><LinearLayoutandroid:layout_weight="1"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="match_parent"><TextViewandroid:text="SR (x4)"android:textSize="20dp"android:gravity="center_horizontal"android:layout_width="match_parent"android:layout_height="wrap_content"></TextView><ImageViewandroid:id="@+id/imageview_dest"android:layout_weight="1"android:layout_width="match_parent"android:layout_height="match_parent"/><ImageViewandroid:layout_weight="2"android:layout_width="match_parent"android:layout_height="match_parent"/></LinearLayout></androidx.appcompat.widget.LinearLayoutCompat>
</RelativeLayout>

设计好布局以后,运行一下APP,

5、创建SRGanModel类

接下来,创建SRGANModel类,这个类主要实现模型的加载、为模型输入数据且获得模型推理的输出结果,最后将输出结果转成Bitmap格式。这其中主要涉及到3个变量,

private Interpreter tfLite;
private TensorImage inputImageBuffer;
private TensorBuffer outputProbabilityBuffer;

其中,Interpreter主要是用来加载模型和进行推理(前向)运算的,TensorImage则是用来给模型传输输入数据的,TensorFlowBuffer则是用来获得模型输出数据的。

5.1、导入模型

先来实现导入模型的函数,

/**** 导入模型* @param modelfile* @return*/
public boolean loadModel(String modelfile) {boolean ret = false;try {// 获取在assets中的模型MappedByteBuffer modelFile = loadModelFile(activity.getAssets(), modelfile);// 设置tflite运行条件,使用4线程和GPU进行加速Interpreter.Options options = new Interpreter.Options();options.setNumThreads(4);gpuDelegate = new GpuDelegate();options.addDelegate(gpuDelegate);// 实例化tflitetfLite = new Interpreter(modelFile, options);ret = true;} catch (IOException e) {e.printStackTrace();}return ret;
}

其中loadModelFile函数主要是用来读取在assets文件夹下的模型文件,代码如下,

/** Memory-map the model file in Assets. */
private MappedByteBuffer loadModelFile(AssetManager assets, String modelFilename)throws IOException {AssetFileDescriptor fileDescriptor = assets.openFd(modelFilename);FileInputStream inputStream = new FileInputStream(fileDescriptor.getFileDescriptor());FileChannel fileChannel = inputStream.getChannel();long startOffset = fileDescriptor.getStartOffset();long declaredLength = fileDescriptor.getDeclaredLength();return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength);
}

回到loadModel函数,Interpreter.Options主要是设置模型运行的硬件条件,比如,要用几个线程,是否用GPU等,它还有其他可以设置的,具体可以看官方教程:https://tensorflow.google.cn/lite/performance/best_practices

我这里设置4个线程并且使用GPU加速。如果你手机跑不起来,可以试着将GPU去掉,而只使用CPU。

5.2、设置模型输入和输出的shape和数据类型

接下来看看设置模型的输入,在上一讲中,我们需要根据输入图片宽高重新设置模型输入和输出的shape,这里我们同样需要进行这样的操作,代码如下,

/**** 设置模型输入和输出的shape和类型* @param bitmap 要进行sr的图片*/private void setInputOutputDetails(Bitmap bitmap) {// 获取模型输入数据格式DataType imageDataType = tfLite.getInputTensor(0).dataType();
//        Log.e(TAG, "imageDataType:" + imageDataType.toString());// 创建TensorImage,用于存放图像数据inputImageBuffer = new TensorImage(imageDataType);inputImageBuffer.load(bitmap);// 因为模型的输入shape是任意宽高的图片,即{-1,-1,-1,3},但是在tflite java版中,我们需要指定输入数据的具体大小。// 所以在这里,我们要根据输入图片的宽高来设置模型的输入的shapeint[] inputShape = {1, bitmap.getHeight(), bitmap.getWidth(), 3};tfLite.resizeInput(tfLite.getInputTensor(0).index(), inputShape);
//        Log.e(TAG, "inputShape:" + bitmap.getByteCount());
//        for (int i : inputShape) {
//            Log.e(TAG, i + "");
//        }// 获取模型输出数据格式DataType probabilityDataType = tfLite.getOutputTensor(0).dataType();
//        Log.e(TAG, "probabilityDataType:" + probabilityDataType.toString());// 同样的,要设置模型的输出shape,因为我们用的模型的功能是在原图的基础上,放大scale倍,所以这里要乘以scaleint[] probabilityShape = {1, bitmap.getWidth() * scale, bitmap.getHeight() * scale, 3};//tfLite.getOutputTensor(0).shapeSignature();
//        Log.e(TAG, "probabilityShape:");
//        for (int i : probabilityShape) {
//            Log.e(TAG, i + "");
//        }// Creates the output tensor and its processor.outputProbabilityBuffer = TensorBuffer.createFixedSize(probabilityShape, probabilityDataType);}

5.3、实现推理函数

推理函数主要是将低分辨率的图片数据送入模型,并得到输出结果,然后将输出数据转为Bitmap格式,代码如下,

/**** 推理函数,将图片数据输送给模型并且得到输出结果,然后将输出结果转为Bitmap格式* @param bitmap* @return*/
public Bitmap inference(Bitmap bitmap) {// 根据原图的小块图片设置模型输入setInputOutputDetails(bitmap);// 执行模型的推理,得到小块图片sr后的高清图片tfLite.run(inputImageBuffer.getBuffer(), outputProbabilityBuffer.getBuffer());// 将高清图片数据从TensorBuffer转成float[],以转成安卓常用的Bitmap类型float[] results = outputProbabilityBuffer.getFloatArray();// 将图片从float[]转成BitmapBitmap b = floatArrayToBitmap(results, bitmap.getWidth() * scale, bitmap.getHeight() * scale);return b;
}

上面代码中,outputProbabilityBuffer得到输出结果,先将输出结果转成float数组,再通过floatArrayToBitmap函数将数组转成Bitmap,floatArrayToBitmap函数实现如下,

/**** 模型的输出结果是float型的数据,需要转成int型* @param data* @return*/private int floatToInt(float data) {int tmp = Math.round(data);if (tmp < 0){tmp = 0;}else if (tmp > 255) {tmp = 255;}
//        Log.e(TAG, tmp + " " + data);return tmp;}/**** 模型的输出得到的是一个float数据,这个数组就是sr后的高清图片信息,我们要将它转成Bitmap格式才好在安卓上使用* @param data 图片数据* @param width 图片宽度* @param height 图片高度* @return 返回图片的位图*/private Bitmap floatArrayToBitmap(float[] data, int width, int height) {int [] intdata = new int[width * height];// 因为我们用的Bitmap是ARGB的格式,而data是RGB的格式,所以要经过转换,A指的是透明度for (int i = 0; i < width * height; i++) {int R = floatToInt(data[3 * i]);int G = floatToInt(data[3 * i + 1]);int B = floatToInt(data[3 * i + 2]);intdata[i] = (0xff << 24) | (R << 16) | (G << 8) | (B << 0);//            Log.e(TAG, intdata[i]+"");}//得到位图Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);bitmap.setPixels(intdata, 0, width, 0, 0, width, height);return bitmap;}

6、实现MainActivity类

接着,在MainActivity类实现对界面的中的按钮设置监听事件,并调用SRGanModel类实现SR运算。

6.1、获取控件

先定义我们需要的控件,

private Button testButton;
private Button selectButton;
private Button saveButton;
private ImageView imageViewSrc;
private ImageView imageViewDest;

再在onCreate函数中获取控件,

testButton = findViewById(R.id.test_button);
selectButton = findViewById(R.id.select_button);
imageViewSrc = findViewById(R.id.imageview_src);
imageViewDest = findViewById(R.id.imageview_dest);
srProgressBar = findViewById(R.id.sr_progress_bar);
saveButton = findViewById(R.id.save_button);

为方便观察效果,再创建一个重置View的函数,

private void resetView() {Bitmap mBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);imageViewSrc.setImageBitmap(mBitmap);imageViewDest.setImageBitmap(mBitmap);srProgressBar.setProgress(0, true);
}

6.2、实例化SRGanModel类并导入模型

先定义,

private static final String SRGAN_MODEL_FILE = "gan_generator.tflite";
private Activity activity;
private SRGanModel srGanModel;

再在onCreate函数中给activity变量赋值,这是因为SRGanModel导入模型时需要访问assets文件夹的管理器AssetManager类需要这个变量,

activity = this;

接着实例化SRGanModel类并导入模型,

srGanModel = new SRGanModel(activity);
srGanModel.loadModel(SRGAN_MODEL_FILE);

6.3、调用SRGanModel类的推理函数并显示结果

接着,就是调用SRGanModel类的推理函数了,

private void srGanInference(Bitmap bitmap){Bitmap srBitmap = srGanModel.inference(bitmap);imageViewSrc.setImageBitmap(bitmap);imageViewDest.setImageBitmap(srBitmap);
}

6.4、为TEST按钮设置点击监听事件

接下来,为TEST按钮设置点击监听事件,当我们点击TEST按钮以后,随机从assets文件夹中选择一张图片进行SR处理,代码如下,

testButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {try {resetView();AssetManager assetManager = getAssets();InputStream inputStream = assetManager.open(testImages[random.nextInt(testImages.length)]);Bitmap bitmap = BitmapFactory.decodeStream(inputStream);srGanInference(bitmap);} catch (IOException e) {e.printStackTrace();}}
});

6.5、创建assets文件夹并将模型和示例图片拷贝到assets文件夹下

在main文件夹下创建assets文件夹,并将上一讲中的模型文件gan_generator.tflite和demo文件夹下的三张图片拷贝到assets文件夹下,

7、运行APP

接着运行APP,然后点击TEST按键,你会发现APP过几秒后会黑屏并闪退。这并不是代码有什么问题,而是因为SRGAN模型非常耗内存,而且安卓对每个APP都有内存大小限制的,超过系统设置的阈值以后,系统就会强制退出你的APP。那么怎么搞呢?由于模型不限制输入图片的大小,所以,如果输入图片小一点,是否就可以跑起来呢?我们用一个比示例图片还小得多的图片试试。将示例图片裁剪一个小片段并保持成图片,如下图所示,

我们就用这个small.png图片来试试,修改代码,将MainActivity.java中的

InputStream inputStream = assetManager.open(testImages[random.nextInt(testImages.length)]);

改成

InputStream inputStream = assetManager.open("small.png");

再运行APP并点击TEST按钮,得到结果如下,

可以看到,右边的图片比左边的图片清晰了很多,说明真的是因为输入图片太大的缘故。

8、将输入图片切分成多小块

我们先来看一下示例图片有多大,

靠,才124x118,那么小就让我们的APP崩溃了,是不是就说明我们的方案不可用了?直接甩锅说这是模型的问题不是我的问题了?办法总比困难多,既然,我们的small.png能运行,那么,一个解决方案就是将原图切分成很多个小块图片,然后把每一小块图片都送入模型中运行,得到每一小块的高清图以后,再将所有小块高清图拼接成一个大的高清图。现在我们来实现切分原图的函数,在SRGanModel类中实现,为了方便记录每一小块在原图中所处的位置,我们先定义SplitBitmap类来存放小块图片的行、列和位图信息,

/**** 这个类用来存放切分后的小块图片的信息*/
private class SplitBitmap{public int row; // 当前小块图片相对原图处于哪一行public int column; // 当前小块图片相对原图处于哪一列public Bitmap bitmap; // 当前小块图片的位图
}

然后实现切分原图的函数,

/**** 将原图切分成众多小块图片,根据原图的宽高和cropBitmapSize来决定分成多少小块* @param bitmap 待拆分的位图* @return 返回切割后的小块位图列表*/private ArrayList<SplitBitmap> splitBitmap(Bitmap bitmap) {// 获取原图的宽高int width = bitmap.getWidth();int height = bitmap.getHeight();// 原图宽高除以cropBitmapSize,得到应该将图片的宽和高分成几部分float splitFW = (float)width / cropBitmapSize;float splitFH = (float)height / cropBitmapSize;int splitW = (int)(splitFW);int splitH = (int)(splitFH);// 用来存放切割以后的小块图片的信息ArrayList<SplitBitmap> splitedBitmaps = new ArrayList<SplitBitmap>();Log.e(TAG, "width:" + width + " height:" + height);Log.e(TAG, "splitW:" + splitW + " splitH:" + splitH);Log.e(TAG, "splitFW:" + splitFW + " splitFH:" + splitFH);//对图片进行切割if (splitFW < 1.2 && splitFH < 1.2) {// 直接计算整张图SplitBitmap sb = new SplitBitmap();sb.row = 0;sb.column = 0;sb.bitmap = bitmap;splitedBitmaps.add(sb);} else if (splitFW < 1.2 && splitFH > 1) {// 仅在高度上拆分for (int i = 0; i < splitH; i++) {SplitBitmap sb = new SplitBitmap();sb.row = i;sb.column = 0;if (i == splitH - 1) {sb.bitmap = Bitmap.createBitmap(bitmap, 0, i * cropBitmapSize, cropBitmapSize, height - i * cropBitmapSize, null, false);}else {sb.bitmap = Bitmap.createBitmap(bitmap, 0, i * cropBitmapSize, cropBitmapSize, cropBitmapSize, null, false);}splitedBitmaps.add(sb);}} else if (splitFW > 1 && splitFH < 1.2) {// 仅在宽度上拆分for (int i = 0; i < splitW; i++) {SplitBitmap sb = new SplitBitmap();sb.row = 0;sb.column = i;if (i == splitW - 1) {sb.bitmap = Bitmap.createBitmap(bitmap, i * cropBitmapSize, 0, cropBitmapSize, width - i * cropBitmapSize, null, false);}else {sb.bitmap = Bitmap.createBitmap(bitmap, i * cropBitmapSize, 0, cropBitmapSize, cropBitmapSize, null, false);}splitedBitmaps.add(sb);}} else {// 在高度和宽度上都拆分for (int i = 0; i < splitH; i++) {for (int j = 0; j < splitW; j++) {int lastH = cropBitmapSize;int lastW = cropBitmapSize;// 最后一行的高度if (i == splitH - 1) {lastH = height - i * cropBitmapSize;
//                        Log.e(TAG, "lastH:" +lastH);}// 最后一列的宽度if (j == splitW - 1) {lastW = width - j * cropBitmapSize;
//                        Log.e(TAG, "lastW:" +lastW);}
//                    Log.e(TAG, "lastH:" + lastH + " lastW:" + lastW +
//                            " bitmapH:" + bitmap.getHeight() + " bitmapW:" + bitmap.getWidth() +
//                            " i * cropBitmapSize:" + i * cropBitmapSize + " j * cropBitmapSize:" + j * cropBitmapSize +
//                            " i:" + i + " j:" + j
//                    );SplitBitmap sb = new SplitBitmap();// 记录当前小块图片所处的行列sb.row = i;sb.column = j;// 获取当前小块的位图sb.bitmap = Bitmap.createBitmap(bitmap, j * cropBitmapSize, i * cropBitmapSize, lastW, lastH, null, false);splitedBitmaps.add(sb);}}}return splitedBitmaps;}

9、将小块图片合并成一张大图

既然有拆分图片的函数,那么自然要实现合并图片的函数,为了验证我们上面拆分和合并的函数是否对,我们再定义一只画笔,将每一小块用红框框出来,这样就比较显式的看到我们拆分和合并的结果,定义和初始化画笔的代码如下,

private final Paint boxPaint = new Paint();
/**** 初始化画笔,用来调试切分图片和合并图片的*/
private void initPaint() {boxPaint.setColor(Color.RED);boxPaint.setStyle(Paint.Style.STROKE);boxPaint.setStrokeWidth(2.0f);boxPaint.setStrokeCap(Paint.Cap.ROUND);boxPaint.setStrokeJoin(Paint.Join.ROUND);boxPaint.setStrokeMiter(100);
}

合并图片的代码如下,

/**** 合并小块位图列表为一个大的位图* @param splitedBitmaps 待合并的小块位图列表* @return 返回合并后的大的位图*/private Bitmap mergeBitmap(ArrayList<SplitBitmap> splitedBitmaps) {int mergeBitmapWidth = 0;int mergeBitmapHeight = 0;// 遍历位图列表,根据行和列的信息,计算出合并后的位图的宽高for (SplitBitmap sb : splitedBitmaps) {
//            Log.e(TAG, "sb.column:" + sb.column + " sb.row:" + sb.row + " sb.bitmap.getHeight():" + sb.bitmap.getHeight() + " sb.bitmap.getWidth():" + sb.bitmap.getWidth());if (sb.row == 0) {mergeBitmapWidth += sb.bitmap.getWidth();}if (sb.column == 0) {mergeBitmapHeight += sb.bitmap.getHeight();}}Log.e(TAG, "splitedBitmaps: " + splitedBitmaps.size() + " mergeBitmapWidth:" + mergeBitmapWidth + " mergeBitmapHeight:" + mergeBitmapHeight);// 根据宽高创建合并后的空位图Bitmap mBitmap = Bitmap.createBitmap(mergeBitmapWidth, mergeBitmapHeight, Bitmap.Config.ARGB_8888);// 创建画布,我们将在画布上拼接新的大位图Canvas canvas = new Canvas(mBitmap);// 计算位图列表的长度int splitedBitmapsSize = splitedBitmaps.size();//lastRowSB记录上一行的第一列的数据,主要用来判断当前行是否最后一行,因为最后一行之前的所有行的高度都是一致的SplitBitmap lastColumn0SB = null;for (int i = 0; i < splitedBitmapsSize; i++) {// 获取当前小块信息SplitBitmap sb = splitedBitmaps.get(i);// 根据当前小块所处的行列和宽高计算小块应处于大位图中的位置int left = sb.column * sb.bitmap.getWidth();int top = sb.row * sb.bitmap.getHeight();int right = left + sb.bitmap.getWidth();int bottom = top + sb.bitmap.getHeight();// 最后一列// 根据计算下一个小块位图的列数是否为0判断当前小块是否是最后一列if (i != 0 && i < splitedBitmapsSize - 1 && splitedBitmaps.get(i + 1).column == 0) {// 因为最后一列的宽度不确定,所以,要根据上一小块的宽高来计算当前小块在大位图中的起始位置SplitBitmap lastBitmap = splitedBitmaps.get(i - 1);left = sb.column * lastBitmap.bitmap.getWidth();top = sb.row * lastBitmap.bitmap.getHeight();right = left + sb.bitmap.getWidth();bottom = top + sb.bitmap.getHeight();}//最后一行// 根据对比上一行中的高度来计算当前行是否最后一行,因为最后一行前的所有行的高度都是一致的if (i != 0 && i < splitedBitmapsSize && lastColumn0SB != null && splitedBitmaps.get(i).bitmap.getHeight() != lastColumn0SB.bitmap.getHeight()) {
//                Log.e(TAG, "---------------");// 如果最后一行的高度和之前行的高度不一致,那么就要根据上一行中的高度来重新计算当前行的起始位置SplitBitmap lastColumnBitmap = lastColumn0SB;left = sb.column * lastColumnBitmap.bitmap.getWidth();top = sb.row * lastColumnBitmap.bitmap.getHeight();right = left + sb.bitmap.getWidth();bottom = top + sb.bitmap.getHeight();} else if (sb.column == 0) {// 记录上一行的第一个列的小块信息lastColumn0SB = sb;}// 这个是当前小块的信息Rect srcRect = new Rect(0, 0, sb.bitmap.getWidth(), sb.bitmap.getHeight());// 这个是当前小块应该在大图中的位置信息Rect destRect = new Rect(left, top, right, bottom);// 将当前小块画到大图中canvas.drawBitmap(sb.bitmap, srcRect, destRect, null);// 这个是为了调试而画的框canvas.drawRect(destRect, boxPaint);
//            Log.e(TAG,"I:" + i + " col:" + sb.column + " row:" + sb.row + " width:" + sb.bitmap.getWidth() + " height:" + sb.bitmap.getHeight());}return mBitmap;}

接着,我们就来验证我们的拆分和合并图片的代码,修改inference函数,代码如下,

 /**** 推理函数,将图片数据输送给模型并且得到输出结果,然后将输出结果转为Bitmap格式* @param bitmap* @return*/public Bitmap inference(Bitmap bitmap) {/
//        // 直接对图片进行SR运算,当输入图片比较大的时候,APP就会被系统强制退出了
//        // 根据原图的小块图片设置模型输入
//        setInputOutputDetails(bitmap);
//        // 执行模型的推理,得到小块图片sr后的高清图片
//        tfLite.run(inputImageBuffer.getBuffer(), outputProbabilityBuffer.getBuffer());
//        // 将高清图片数据从TensorBuffer转成float[],以转成安卓常用的Bitmap类型
//        float[] results = outputProbabilityBuffer.getFloatArray();
//        // 将图片从float[]转成Bitmap
//        Bitmap b = floatArrayToBitmap(results, bitmap.getWidth() * scale, bitmap.getHeight() * scale);/// 验证拆分和合并图片的代码ArrayList<SplitBitmap> splitedBitmaps = splitBitmap(bitmap);ArrayList<SplitBitmap> mergeBitmaps = new ArrayList<SplitBitmap>();for (SplitBitmap sb : splitedBitmaps) {SplitBitmap srsb = new SplitBitmap();srsb.column = sb.column;srsb.row = sb.row;srsb.bitmap = sb.bitmap;mergeBitmaps.add(srsb);}// 最后,将列表中的小块图片合并成一张大的图片并返回Bitmap mergeBitmap = mergeBitmap(mergeBitmaps);/return mergeBitmap;}

然后再将MainActivity.java中的代码

InputStream inputStream = assetManager.open(testImages[random.nextInt(testImages.length)]);

恢复回来。再运行APP,运行结果如下,

可以看到,我们的拆分和合并代码没问题。

10、将原图拆分成小块进行SR运算后再合并成高清图

有了上面的基础以后就好办了,先对小块图进行SR运算,再合并即可,代码如下,

/**** 推理函数,将图片数据输送给模型并且得到输出结果,然后将输出结果转为Bitmap格式* @param bitmap* @return*/public Bitmap inference(Bitmap bitmap) {/
//        // 直接对图片进行SR运算,当输入图片比较大的时候,APP就会被系统强制退出了
//        // 根据原图的小块图片设置模型输入
//        setInputOutputDetails(bitmap);
//        // 执行模型的推理,得到小块图片sr后的高清图片
//        tfLite.run(inputImageBuffer.getBuffer(), outputProbabilityBuffer.getBuffer());
//        // 将高清图片数据从TensorBuffer转成float[],以转成安卓常用的Bitmap类型
//        float[] results = outputProbabilityBuffer.getFloatArray();
//        // 将图片从float[]转成Bitmap
//        Bitmap b = floatArrayToBitmap(results, bitmap.getWidth() * scale, bitmap.getHeight() * scale);/
//        // 验证拆分和合并图片的代码
//        ArrayList<SplitBitmap> splitedBitmaps = splitBitmap(bitmap);
//        ArrayList<SplitBitmap> mergeBitmaps = new ArrayList<SplitBitmap>();
//        for (SplitBitmap sb : splitedBitmaps) {
//            SplitBitmap srsb = new SplitBitmap();
//            srsb.column = sb.column;
//            srsb.row = sb.row;
//            srsb.bitmap = sb.bitmap;
//            mergeBitmaps.add(srsb);
//        }
//        // 最后,将列表中的小块图片合并成一张大的图片并返回
//        Bitmap mergeBitmap = mergeBitmap(mergeBitmaps);/// 对所有原图的小块图片进行sr运算,并把得到的小块高清图存到sredBitmaps列表中ArrayList<SplitBitmap> splitedBitmaps = splitBitmap(bitmap);ArrayList<SplitBitmap> mergeBitmaps = new ArrayList<SplitBitmap>();for (SplitBitmap sb : splitedBitmaps) {// 根据原图的小块图片设置模型输入setInputOutputDetails(sb.bitmap);// 执行模型的推理,得到小块图片sr后的高清图片tfLite.run(inputImageBuffer.getBuffer(), outputProbabilityBuffer.getBuffer());// 将高清图片数据从TensorBuffer转成float[],以转成安卓常用的Bitmap类型float[] results = outputProbabilityBuffer.getFloatArray();// 将图片从float[]转成BitmapBitmap b = floatArrayToBitmap(results, sb.bitmap.getWidth() * scale, sb.bitmap.getHeight() * scale);SplitBitmap srsb = new SplitBitmap();srsb.column = sb.column;srsb.row = sb.row;srsb.bitmap = b;mergeBitmaps.add(srsb);}// 最后,将列表中的小块高清图片合并成一张大的高清图片并返回Bitmap mergeBitmap = mergeBitmap(mergeBitmaps);return mergeBitmap;}

运行APP,结果如下,

可以看到,右图已经是SR后的高清图了。

11、增加进度条功能

接下来,我们继续完善APP,因为SR的速度非常慢,所以增加一个进度条的功能,因为安卓APP中,操作UI只能是主线程,所以,我们为SRGanModel新增一个回调函数的接口,代码如下,

public void addSRProgressCallback(final SRProgressCallback callback) {this.callback = callback;
}public interface SRProgressCallback {public void callback(int progress);
}

接着,定义接口变量,

private SRProgressCallback callback;

再将inference函数修改成如下代码,

 /**** 推理函数,将图片数据输送给模型并且得到输出结果,然后将输出结果转为Bitmap格式* @param bitmap* @return*/public Bitmap inference(Bitmap bitmap) {/
//        // 直接对图片进行SR运算,当输入图片比较大的时候,APP就会被系统强制退出了
//        // 根据原图的小块图片设置模型输入
//        setInputOutputDetails(bitmap);
//        // 执行模型的推理,得到小块图片sr后的高清图片
//        tfLite.run(inputImageBuffer.getBuffer(), outputProbabilityBuffer.getBuffer());
//        // 将高清图片数据从TensorBuffer转成float[],以转成安卓常用的Bitmap类型
//        float[] results = outputProbabilityBuffer.getFloatArray();
//        // 将图片从float[]转成Bitmap
//        Bitmap b = floatArrayToBitmap(results, bitmap.getWidth() * scale, bitmap.getHeight() * scale);/
//        // 验证拆分和合并图片的代码
//        ArrayList<SplitBitmap> splitedBitmaps = splitBitmap(bitmap);
//        ArrayList<SplitBitmap> mergeBitmaps = new ArrayList<SplitBitmap>();
//        for (SplitBitmap sb : splitedBitmaps) {
//            SplitBitmap srsb = new SplitBitmap();
//            srsb.column = sb.column;
//            srsb.row = sb.row;
//            srsb.bitmap = sb.bitmap;
//            mergeBitmaps.add(srsb);
//        }
//        // 最后,将列表中的小块图片合并成一张大的图片并返回
//        Bitmap mergeBitmap = mergeBitmap(mergeBitmaps);/// 对所有原图的小块图片进行sr运算,并把得到的小块高清图存到sredBitmaps列表中ArrayList<SplitBitmap> splitedBitmaps = splitBitmap(bitmap);ArrayList<SplitBitmap> mergeBitmaps = new ArrayList<SplitBitmap>();float progress = 0;float total = splitedBitmaps.size() + 10; // 因为后面还有合并操作,所以分母设置的稍微大一点点int curIndex = 0;// 对所有原图的小块图片进行sr运算,并把得到的小块高清图存到sredBitmaps列表中for (SplitBitmap sb : splitedBitmaps) {callback.callback(Math.round(progress));// 根据原图的小块图片设置模型输入setInputOutputDetails(sb.bitmap);// 执行模型的推理,得到小块图片sr后的高清图片tfLite.run(inputImageBuffer.getBuffer(), outputProbabilityBuffer.getBuffer());// 将高清图片数据从TensorBuffer转成float[],以转成安卓常用的Bitmap类型float[] results = outputProbabilityBuffer.getFloatArray();// 将图片从float[]转成BitmapBitmap b = floatArrayToBitmap(results, sb.bitmap.getWidth() * scale, sb.bitmap.getHeight() * scale);SplitBitmap srsb = new SplitBitmap();srsb.column = sb.column;srsb.row = sb.row;srsb.bitmap = b;mergeBitmaps.add(srsb);progress = (curIndex++ / total) * 100;}// 最后,将列表中的小块高清图片合并成一张大的高清图片并返回Bitmap mergeBitmap = mergeBitmap(mergeBitmaps);callback.callback(100);return mergeBitmap;}

因为SR运算是耗时操作,所以最后我们开一个线程来操作,而不是用主线程来操作,所以修改MainActivity代码,新增变量,

private Handler handler;
private HandlerThread handlerThread;在onCreate函数中添加,
handlerThread = new HandlerThread("inference");
handlerThread.start();
handler = new Handler(handlerThread.getLooper());

新建函数,

private synchronized void runInBackground(final Runnable r) {if (handler != null) {handler.post(r);}
}

修改srGanInference函数,

private void srGanInference(Bitmap bitmap){runInBackground(new Runnable() {@Overridepublic void run() {mergeBitmap = srGanModel.inference(bitmap);Log.e(TAG, "imageView width:" + bitmap.getWidth() + " height:" + bitmap.getHeight() +" mergeBitmap width:" + mergeBitmap.getWidth() + " height:" + mergeBitmap.getHeight());// 显示图片runOnUiThread(new Runnable() {@Overridepublic void run() {if (bitmap != null) {imageViewSrc.setImageBitmap(bitmap);imageViewDest.setImageBitmap(mergeBitmap);}}});}});}

设置SRGanModel的回调函数,

srGanModel.addSRProgressCallback(new SRGanModel.SRProgressCallback() {@Overridepublic void callback(int progress) {srProgressBar.setProgress(progress, true);}
});

运行APP,运行结果如下,

可以看到,我们的进度条是在工作了的。

12、从相册中选择照片

继续完善,首先为SELECT按键新增点击监听事件,并打开相册,

selectButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {resetView();Intent intent= new Intent(Intent.ACTION_PICK,null);intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,"image/*");startActivityForResult(intent, 0x1);}
});

接着,实现选中图片后的操作,即对选中的图片进行SR运行,代码如下,

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {// TODO Auto-generated method stubif(data == null) {return;}try {Bitmap bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), data.getData());srGanInference(bitmap);}catch (Exception e){Log.d("MainActivity","[*]"+e);return;}super.onActivityResult(requestCode, resultCode, data);
}

运行APP,运行结果,

13、保存SR后的图片

最后,实现保存SR后的高清图的功能,先实现保存图片的函数,

private boolean saveBitmap(Bitmap bitmap) {boolean ret = false;final File rootDir = new File(SR_ROOT);if (!rootDir.exists()){if (!rootDir.mkdirs()) {Log.e(TAG, "Make dir failed");}}String filename = SR_ROOT + SystemClock.uptimeMillis() + ".png";try {final FileOutputStream out = new FileOutputStream(filename);bitmap.compress(Bitmap.CompressFormat.PNG, 99, out);out.flush();out.close();ret = true;} catch (final Exception e) {Log.e(TAG,  "Exception!");}return ret;
}

然后为SAVE按键设置点击监听事件,

saveButton.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if (mergeBitmap != null) {String text = "Save failed!";if (saveBitmap(mergeBitmap)){text = "Save success!";}Toast toast = Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT);toast.show();}}
});

接着,添加APP的读写权限,先创建以下两个函数,

private boolean hasPermission() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {return checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;} else {return true;}
}private void requestPermission() {if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {Toast.makeText(MainActivity.this,"Write external storage permission is required for this demo",Toast.LENGTH_LONG).show();}requestPermissions(new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);}
}

接着在onCreate里添加下面代码,

// 申请读写权限
if (!hasPermission()) {requestPermission();
}

最后,在AndroidManifest.xml里添加,

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

并在<application>属性里添加

android:requestLegacyExternalStorage="true"

如下图所示,

运行APP,SR一张图片以后,点击SAVE按钮,然后去文件管理器里看看是否生成高清图,运行结果如下,

14、完整源码

https://mianbaoduo.com/o/bread/YZWVlZ5y

TensorFlow入门教程(23)将图像超分辨率模型SRGAN移植到安卓APP(下)相关推荐

  1. 谷歌colab“几键”运行图像超分辨率模型-ESRGAN,操作详解

    文章目录 1. 准备工作(前提) 2.Colab常用操作 3. git clone 从github 克隆代码 4. 下载数据集(DIV2K) 5. 解压数据 6. 查看代码,运行改名rename.py ...

  2. PULSE:一种基于隐式空间的图像超分辨率算法

    分享一篇 CVPR 2020 录用论文:PULSE: Self-Supervised Photo Upsampling via Latent Space Exploration of Generati ...

  3. 图像超分辨率综述学习之:Deep Learning for Image Super-resolution A Survey

    论文:https://arxiv.org/abs/1902.06068 1.基于监督学习的图像超分辨率模型框架 目前的SR还是侧重于有监督的学习,根据上采样的阶段不同划分为四种类型的模型框架: 1.1 ...

  4. 图像超分辨率 之 DRN 论文解读与感想

    图像超分辨率 之 DRN (Closed-loop Matters: Dual Regression Networks for Single Image Super-Resolution)论文解读与感 ...

  5. 从图像超分辨率快速入门pytorch

    文章目录 前言 正文 单一图像超分辨率(SISR) 训练4要素 网络模型 数据 训练 DataLoader 前言 最近又开始把pytorch拾起来,学习了github上一些项目之后,发现每个人都会用不 ...

  6. SRZoo--深度学习图像超分辨率工具

    点击上方"算法猿的成长",关注公众号,选择加"星标"或"置顶" 总第 145 篇文章,本文大约 2100 字,阅读大约需要 7 分钟 前言 ...

  7. 一文掌握图像超分辨率重建(算法原理、Pytorch实现)——含完整代码和数据

    目录 一.  图像超分辨率重建概述 1. 概念 2. 应用领域 3. 研究进展 3.1 传统超分辨率重建算法 3.2 基于深度学习的超分辨率重建算法 二.  SRResNet算法原理和Pytorch实 ...

  8. 【图像超分辨率】RS Image SR Based on Visual Saliency Analysis

    Remote-Sensing Image Superresolution Based on Visual Saliency Analysis and Unequal Reconstruction Ne ...

  9. GAN的应用-SRGAN图像超分辨率重构、U-net结构和字“姐”跳动学习心得

    GAN的应用 -- SRGAN图像超分辨率重构 项目地址:https://aistudio.baidu.com/aistudio/projectdetail/843989 文章来源:2017 IEEE ...

最新文章

  1. 创建maven项目但是总是报错
  2. 利用jdom生成XML文件
  3. python flask gunicorn nginx 部署
  4. Python 【快手】短视频的自动上传与发布实例演示,同时支持抖音、哔哩哔哩、小红书、微视、西瓜视频、微信视频号等平台的视频自动化同步发布
  5. 牛客题霸 NC16 判断二叉树是否对称
  6. 转) javascript 中的escape 与C#互相转化
  7. 案例 员工信息维护系统 c# 1613925570
  8. 开放Nginx在文件夹列表功能
  9. 黑客是否可以攻击被拔掉网线的电脑?
  10. 数据库 一致性读当前读
  11. 设计模式原则之五:里氏置换原则
  12. 关于“svn: Can‘t connect to host ‘*.*.*.*‘: 由于连接方在一段时间后没有正确答复或连接”的解决方法
  13. MySQL 入门(二)—— MySQL理论基础
  14. 常见电子元器件检测经验
  15. android so库放在哪,Android开发中so文件放置的位置
  16. Android视频直播推流的实现
  17. ShadowGun 的学习笔记 - GodRays
  18. 最新Gxlcms有声小说系统/小说听书系统源码
  19. java replaceLast
  20. C# 蓝牙编程(InTheHand.Net.Personal.dll-32feet),教程地址

热门文章

  1. openEuler进化记:一颗探索宇宙的生态之星
  2. 据说蟑螂会吃同伴的尸体
  3. PTA 7-3 春夏秋冬
  4. 机器学习算法_明确解释:4种机器学习算法
  5. JVM 为什么推荐将-Xms -Xmx值设置为相同
  6. docker安装kali
  7. pyquery基本使用
  8. 圣诞节就要到了——快用python安排几棵圣诞树吧~
  9. 奇迹暖暖文件传至服务器失败,奇迹暖暖无法安装或登录游戏的解决方法
  10. 1.8 莱布尼兹的天才发明