#
#作者:韦访
#博客: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、界面布局

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

<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

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐