最后
**要想成为高级安卓工程师,必须掌握许多基础的知识。**在工作中,这些原理可以极大的帮助我们理解技术,在面试中,更是可以帮助我们应对大厂面试官的刁难。
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事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、相关文章:
=======
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。其中的关键类如下:
-
ClassifierActivity:app中物体识别的主页面,也是入口类
-
CameraActivity:ClassifierActivity的父类,包含了相机权限获取,初始化,图片转换等操作。
-
CameraConnectionFragment, LegacyCameraConnectionFragment:主页面中相机实时预览图片的区域,分为传统方式和当前方式两种。
-
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行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!