TensorFlow入门教程(23)将图像超分辨率模型SRGAN移植到安卓APP(下)
##作者:韦访#博客:https://blog.csdn.net/rookie_wei#微信:1007895847#添加微信的备注一下是CSDN的#欢迎大家一起学习#1、概述上一讲我们将SRGAN模型由HDF5转成了tflite,并且验证了我们的tflite模型是对的。这一讲,我们就来实现安卓的APP,我假设你们至少有点安卓APP开发基础的。环境配置:操作系统:Win10 64位显卡:GTX 10
#
#作者:韦访
#博客: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、完整源码
更多推荐
所有评论(0)