2024年安卓最全tensorflow-android 官方demo源码分析(1),Android面试题及解析

最后

**要想成为高级安卓工程师,必须掌握许多基础的知识。**在工作中,这些原理可以极大的帮助我们理解技术,在面试中,更是可以帮助我们应对大厂面试官的刁难。


网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

2.2、jni

2.3、res

2.4、src

2.5、 build.gradle

3、app进行物体识别的流程

3.1、onCreate中请求相机权限并设置页面内容区的fragment

3.2、打开摄像头,并注册ConnectionCallback和OnImageAvailableListener

3.3、相机预览图片宽高确定后,回调onPreviewSizeChosen

3.3.1、分类器classifier的构造

3.3.2、预处理预览图片

3.4、相机预览图片available时,OnImageAvailableListener回调

4 总结


0、相关文章:

=======

tensorflow-android 官方demo源码分析

1、引言

====

目前深度学习模型已经应用到了各个领域,将TensorFlow训练模型部署到终端上也逐步变为了现实。特别是mobileNet等体积小,占用内存少的模型出现后,将深度学习应用到终端上逐渐变得火热起来。mobileNet针对于终端,将标准的卷积分解成了一个depthwise 卷积和一个1x1的标准卷积,大大降低了模型参数数量。同时支持输入channel和resolution的裁剪,也大大降低了模型体积。官方训练的MobileNet_v1_0.25_128_quant, 在输入channel裁剪为原先1/4, 图像尺寸变为128*128后,模型体积仅仅为4.1MB,但识别准确度仍然可以达到65.8%. 本文聚焦于TensorFlow模型在Android app中的如何应用,就不对mobileNet进行详细分析了。

官方在TensorFlow源码的tensorflow/examples/android/ 目录下提供了一个app demo,配置好环境后,就可以在Android studio中run起来并安装到手机中了。下面详细分析这个demo的源码。掌握了官方demo原理后,我们就能够一方面改造这个demo app,来实现其他功能,比如相册内容识别等。另一方面可以用自己训练好的模型来替换官方demo中的TensorFlow模型。

2、工程目录结构

========

重要的文件如下

2.1、assets


pb文件存放训练好的TensorFlow模型,txt文件为能够识别的物体的名字,也叫label。model和label成对出现。官方给出的inceptionV1模型能够识别1000种物体,基本能够满足我们的日常需求。添加自己的模型时,需要在assets目录中加入自己训练好的model和对应label文件。

2.2、jni


物体识别使用了摄像头等组件,需要调用到jni。我们不需要详细了解

2.3、res


资源文件,学过Android的小伙伴都知道

2.4、src


demo中包含了四个子项目,分别为物体识别Classifier, 物体检测Detector,语音识别Speech,图片个性化Stylize。四个demo只是在训练模型上有差别,与Android的结合大同小异。故本文重点分析物体识别Classifier。其中的关键类如下:

  1. ClassifierActivity:app中物体识别的主页面,也是入口类

  2. CameraActivity:ClassifierActivity的父类,包含了相机权限获取,初始化,图片转换等操作。

  3. CameraConnectionFragment, LegacyCameraConnectionFragment:主页面中相机实时预览图片的区域,分为传统方式和当前方式两种。

  4. TensorFlowImageClassifier:利用TensorFlow模型来预测物体的关键所在,包含识别器classifier的构造和图像识别两个主要方法。后面详细分析。

2.5、 build.gradle


编译项目的配置文件,工程环境配置时比较关键,本文重点讲解TensorFlow在Android上应用的原理,就不展开说了。

3、app进行物体识别的流程

==============

3.1、onCreate中请求相机权限并设置页面内容区的fragment


我们从ClassifierActivity的onCreate()看起,它继承于CameraActivity。主要作用为设置Activity的contentView,以及请求打开相机的权限。如下

protected void onCreate(final Bundle savedInstanceState) {

// 设置window layout,以及设置contentView

LOGGER.d("onCreate " + this);

super.onCreate(null);

getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

setContentView(R.layout.activity_camera);

// 有相机权限,则进行设置相机实时图片预览区域的Fragment,否则,请求权限,让用户确定

if (hasPermission()) {

setFragment();

} else {

requestPermission();

}

}

相机权限请求requestPermission,通过发送android.permission.CAMERA 权限请求即可,做过Android的小伙伴都知道,不详细分析了。下面看setFragment()方法

protected void setFragment() {

// 获取相机,通过CameraService选择正确的摄像头。本app中不使用前置摄像头

String cameraId = chooseCamera();

// 构建相机的Fragment.注册Camera.PreviewCallback,android.hardware.Camera的callback

Fragment fragment;

if (useCamera2API) {

// 摄像头支持高级的图像处理功能时,构造CameraConnectionFragment实例。后面详细分析

CameraConnectionFragment camera2Fragment =

CameraConnectionFragment.newInstance(

new CameraConnectionFragment.ConnectionCallback() {

@Override

// 选择了预览图片的大小时的回调

public void onPreviewSizeChosen(final Size size, final int rotation) {

previewHeight = size.getHeight();

previewWidth = size.getWidth();

CameraActivity.this.onPreviewSizeChosen(size, rotation);

}

},

this,

getLayoutId(),

getDesiredPreviewFrameSize());

camera2Fragment.setCamera(cameraId);

fragment = camera2Fragment;

} else {

// 摄像头只支持部分功能时,fallback到传统的API

fragment =

new LegacyCameraConnectionFragment(this, getLayoutId(), getDesiredPreviewFrameSize());

}

// fragment填充到container位置处

getFragmentManager()

.beginTransaction()

.replace(R.id.container, fragment)

.commit();

}

下面来看CameraConnectionFragment,构造fragment时我们传入了两个比较重要的回调,一个是cameraConnectionCallback,它在打开摄像头时回调,一个是imageListener,它在摄像头拍摄到图片时回调。我们后面会详细分析。先来看fragment的生命周期中的几个重要方法。onCreateView() onViewCreated()基本没做太多事情,onResume()中有个关键动作,它调用了openCamera()方法来打开摄像头。我们来详细分析。

public void onResume() {

super.onResume();

startBackgroundThread();

if (textureView.isAvailable()) {

// 屏幕没有处于关闭状态时,打开摄像头。textureView是fragment中展示摄像头实时捕获的图片的区域。

openCamera(textureView.getWidth(), textureView.getHeight());

} else {

textureView.setSurfaceTextureListener(surfaceTextureListener);

}

}

3.2、打开摄像头,并注册ConnectionCallback和OnImageAvailableListener


下面来看openCamera()方法。

private void openCamera(final int width, final int height) {

// 设置camera捕获图片的一些输出参数,图片预览大小previewSize,摄像头方向sensorOrientation等。最重要的是回调我们之前传入到fragment中的cameraConnectionCallback的onPreviewSizeChosen()方法。

setUpCameraOutputs();

// 设置手机旋转后的适配,这儿不用关心

configureTransform(width, height);

// 利用CameraManager这个Android底层类,打开摄像头。这儿也不是我们关注的重点

final Activity activity = getActivity();

final CameraManager manager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE);

try {

if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {

throw new RuntimeException(“Time out waiting to lock camera opening.”);

}

manager.openCamera(cameraId, stateCallback, backgroundHandler);

} catch (final CameraAccessException e) {

LOGGER.e(e, “Exception!”);

} catch (final InterruptedException e) {

throw new RuntimeException(“Interrupted while trying to lock camera opening.”, e);

}

}

上面setUpCameraOutputs()比较重要,它设置了camera捕获图片的一些参数。如图片预览大小previewSize,摄像头方向sensorOrientation等。最重要的是回调我们之前传入到fragment中的cameraConnectionCallback的onPreviewSizeChosen()方法。我们来看之前CameraActivity中传入的cameraConnectionCallback

new CameraConnectionFragment.ConnectionCallback() {

@Override

// 预览图片的宽高确定后回调

public void onPreviewSizeChosen(final Size size, final int rotation) {

// 获取相机捕获的图片的宽高,以及相机旋转方向。

previewHeight = size.getHeight();

previewWidth = size.getWidth();

// 相机捕获的图片的大小确定后,需要对捕获图片做裁剪等预操作。这将回调到ClassifierActivity中。我们后面重点分析。

CameraActivity.this.onPreviewSizeChosen(size, rotation);

}

}

我们这就分析清楚了打开摄像头前cameraConnectionCallback的回调流程了,还记得我们传入了另外一个listener吧,也就是onImageAvailableListener, 它在摄像头被打开后,捕获的图片available时由系统回调到。摄像头打开后,会create一个新的预览session,其中就会设置OnImageAvailableListener到CameraDevice中。这个过程我们不做详细分析了。

3.3、相机预览图片宽高确定后,回调onPreviewSizeChosen


上面分析到onPreviewSizeChosen会调用到ClassifierActivity中。它主要做了两件事,构造分类器classifier,它是模型分类预测的一个比较关键的类。另外就是预处理输入图片,如裁剪到和模型训练所使用的图片相同的尺寸。

// 图片预览展现出来时回调。主要是构造分类器classifier,和裁剪输入图片为224*224

@Override

public void onPreviewSizeChosen(final Size size, final int rotation) {

final float textSizePx = TypedValue.applyDimension(

TypedValue.COMPLEX_UNIT_DIP, TEXT_SIZE_DIP, getResources().getDisplayMetrics());

borderedText = new BorderedText(textSizePx);

borderedText.setTypeface(Typeface.MONOSPACE);

// 构造分类器,利用了TensorFlow训练出来的Model,也就是.pb文件。这是后面做物体分类识别的关键

classifier =

TensorFlowImageClassifier.create(

getAssets(),

MODEL_FILE,

LABEL_FILE,

INPUT_SIZE,

IMAGE_MEAN,

IMAGE_STD,

INPUT_NAME,

OUTPUT_NAME);

previewWidth = size.getWidth();

previewHeight = size.getHeight();

sensorOrientation = rotation - getScreenOrientation();

LOGGER.i(“Camera orientation relative to screen canvas: %d”, sensorOrientation);

LOGGER.i(“Initializing at size %dx%d”, previewWidth, previewHeight);

rgbFrameBitmap = Bitmap.createBitmap(previewWidth, previewHeight, Config.ARGB_8888);

croppedBitmap = Bitmap.createBitmap(INPUT_SIZE, INPUT_SIZE, Config.ARGB_8888);

// 将照相机获取的原始图片,转换为224*224的图片,用来作为模型预测的输入。

frameToCropTransform = ImageUtils.getTransformationMatrix(

previewWidth, previewHeight,

INPUT_SIZE, INPUT_SIZE,

sensorOrientation, MAINTAIN_ASPECT);

cropToFrameTransform = new Matrix();

frameToCropTransform.invert(cropToFrameTransform);

addCallback(

new DrawCallback() {

@Override

public void drawCallback(final Canvas canvas) {

renderDebug(canvas);

}

});

}

3.3.1、分类器classifier的构造

classifier分类器是模型预测图片分类中比较重要的类,其中一些概念和深度学习以及TensorFlow紧密相关。代码如下

// 构造物体识别分类器

public static Classifier create(

AssetManager assetManager,

String modelFilename,

String labelFilename,

int inputSize,

int imageMean,

float imageStd,

String inputName,

String outputName) {

// 1 构造TensorFlowImageClassifier分类器,inputName和outputName分别为模型输入节点和输出节点的名字

TensorFlowImageClassifier c = new TensorFlowImageClassifier();

c.inputName = inputName;

c.outputName = outputName;

// 2 读取label文件内容,将内容设置到出classifier的labels数组中

String actualFilename = labelFilename.split(“file:///android_asset/”)[1];

Log.i(TAG, "Reading labels from: " + actualFilename);

BufferedReader br = null;

try {

// 读取label文件流,label文件表征了可以识别出来的物体分类。我们预测的物体名称就是其中之一。

br = new BufferedReader(new InputStreamReader(assetManager.open(actualFilename)));

// 将label存储到TensorFlowImageClassifier的labels数组中

String line;

while ((line = br.readLine()) != null) {

c.labels.add(line);

}

br.close();

} catch (IOException e) {

throw new RuntimeException(“Problem reading label file!” , e);

}

// 3 读取model文件名,并设置到classifier的interface变量中。

c.inferenceInterface = new TensorFlowInferenceInterface(assetManager, modelFilename);

// 4 利用输出节点名称,获取输出节点的shape,也就是最终分类的数目。

// 输出的shape为二维矩阵[N, NUM_CLASSES], N为batch size,也就是一批训练的图片个数。NUM_CLASSES为分类个数

final Operation operation = c.inferenceInterface.graphOperation(outputName);

final int numClasses = (int) operation.output(0).shape().size(1);

Log.i(TAG, "Read " + c.labels.size() + " labels, output layer size is " + numClasses);

// 5. 设置分类器的其他变量

c.inputSize = inputSize; // 物体分类预测时输入图片的尺寸。也就是相机原始图片裁剪后的图片。默认为224*224

c.imageMean = imageMean; // 像素点RGB通道的平均值,默认为117。用来将0~255的数值做归一化的

c.imageStd = imageStd; // 像素点RGB通道的归一化比例,默认为1

// 6. 分配Buffer给输出变量

c.outputNames = new String[] {outputName}; // 输出节点名字

c.intValues = new int[inputSize * inputSize];

c.floatValues = new float[inputSize * inputSize * 3]; // RGB三通道

c.outputs = new float[numClasses]; // 预测完的结果,也就是图片对应到每个分类的概率。我们取概率最大的前三个显示在app中

return c;

}

3.3.2、预处理预览图片

// 预处理预览图片,裁剪,旋转等操作。

// srcWidth, srcHeight为预览图片宽高。dstWidth dstHeight为训练模型时使用的图片的宽高

// applyRotation 旋转角度,必须是90的倍数,

// maintainAspectRatio 如果为true,旋转时缩放x而保证y不变

public static Matrix getTransformationMatrix(

final int srcWidth,

final int srcHeight,

final int dstWidth,

final int dstHeight,

final int applyRotation,

final boolean maintainAspectRatio) {

// 定义预处理后的图片像素矩阵

final Matrix matrix = new Matrix();

// 处理旋转

if (applyRotation != 0) {

// 旋转只能处理90度的倍数

if (applyRotation % 90 != 0) {

LOGGER.w(“Rotation of %d % 90 != 0”, applyRotation);

}

// translate平移,保持圆心不变

matrix.postTranslate(-srcWidth / 2.0f, -srcHeight / 2.0f);

// rotate旋转

matrix.postRotate(applyRotation);

最后

有任何问题,欢迎广大网友一起来交流,分享高阶Android学习视频资料和面试资料包~

偷偷说一句:群里高手如云,欢迎大家加群和大佬们一起交流讨论啊!

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

atio) {

// 定义预处理后的图片像素矩阵

final Matrix matrix = new Matrix();

// 处理旋转

if (applyRotation != 0) {

// 旋转只能处理90度的倍数

if (applyRotation % 90 != 0) {

LOGGER.w(“Rotation of %d % 90 != 0”, applyRotation);

}

// translate平移,保持圆心不变

matrix.postTranslate(-srcWidth / 2.0f, -srcHeight / 2.0f);

// rotate旋转

matrix.postRotate(applyRotation);

最后

有任何问题,欢迎广大网友一起来交流,分享高阶Android学习视频资料和面试资料包~

偷偷说一句:群里高手如云,欢迎大家加群和大佬们一起交流讨论啊!

[外链图片转存中…(img-UXnh4NZq-1715750133648)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化学习资料的朋友,可以戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值