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

上一讲我们将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、界面布局

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

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.appcompat.widget.LinearLayoutCompat
        android:id="@+id/button_ll"
        android:layout_width="match_parent"
        android:layout_height="150px"
        android:orientation="horizontal"
        android:paddingTop="10dp"
        <Button
            android: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" />
        <Button
            android: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"/>
        <Button
            android: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.LinearLayoutCompat
        android:id="@+id/progress_bar_ll"
        android:layout_below="@+id/button_ll"
        android:layout_marginTop="10dp"
        android:layout_width="match_parent"
        android:layout_height="40dp">
        <ProgressBar
            android: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.LinearLayoutCompat
        android:layout_below="@+id/progress_bar_ll"
        android:layout_marginTop="50dp"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_weight="1"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:text="LR"
                android:textSize="20dp"
                android:gravity="center_horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
            </TextView>
            <ImageView
                android:id="@+id/imageview_src"
                android:layout_weight="1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
            <ImageView
                android:layout_weight="2"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
        </LinearLayout>
        <LinearLayout
            android:layout_weight="1"
            android:orientation="vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <TextView
                android:text="SR (x4)"
                android:textSize="20dp"
                android:gravity="center_horizontal"
                android:layout_width="match_parent"
                android:layout_height="wrap_content">
            </TextView>
            <ImageView
                android:id="@+id/imageview_dest"
                android:layout_weight="1"
                android:layout_width="match_parent"
                android:layout_height="match_parent"/>
            <ImageView
                android: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); // 实例化tflite tfLite = 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版中,我们需要指定输入数据的具体大小。 // 所以在这里,我们要根据输入图片的宽高来设置模型的输入的shape int[] 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倍,所以这里要乘以scale int[] 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[]转成Bitmap Bitmap 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() {
    @Override
    public 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[]转成Bitmap Bitmap 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[]转成Bitmap Bitmap 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() {
        @Override
        public 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() {
                        @Override
                        public void run() {
                            if (bitmap != null) {
                                imageViewSrc.setImageBitmap(bitmap);
                                imageViewDest.setImageBitmap(mergeBitmap);

设置SRGanModel的回调函数,

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

运行APP,运行结果如下,

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

12、从相册中选择照片

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

selectButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public 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 stub
    if(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() {
    @Override
    public 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

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

  • 浏览量 2017
  • 收藏 0
  • 0

所有评论(0)