2024年tensorflow-android 官方demo源码分析,大厂面试经验分享稿

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

下图是我进阶学习所积累的历年腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节

整理不易,望各位看官老爷点个关注转发,谢谢!祝大家都能得到自己心仪工作。

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

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

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

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

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);

}

// 输出矩阵是否需要转置。如果旋转为90度和270度时需要。转置后,宽高互换。

final boolean transpose = (Math.abs(applyRotation) + 90) % 180 == 0;

final int inWidth = transpose ? srcHeight : srcWidth;

final int inHeight = transpose ? srcWidth : srcHeight;

// 如果src尺寸和dest尺寸不同,则需要做裁剪

if (inWidth != dstWidth || inHeight != dstHeight) {

final float scaleFactorX = dstWidth / (float) inWidth;

final float scaleFactorY = dstHeight / (float) inHeight;

if (maintainAspectRatio) {

// 保持宽高比例不变,不会有形变,但可能会被剪切。此时宽高scale的因子相同

final float scaleFactor = Math.max(scaleFactorX, scaleFactorY);

matrix.postScale(scaleFactor, scaleFactor);

} else {

// 不用保持宽高不变,直接匹配为dest的尺寸。可能会发生形变

matrix.postScale(scaleFactorX, scaleFactorY);

}

}

if (applyRotation != 0) {

// 平移变换

matrix.postTranslate(dstWidth / 2.0f, dstHeight / 2.0f);

}

return matrix;

}

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


当相机预览图片准备好时,Android系统的cameraDevice会回调之前注册的OnImageAvailableListener。下面来看OnImageAvailableListener都做了哪些事情。

public void onImageAvailable(final ImageReader reader) {

// onPreviewSizeChosen被回调后,设置了previewWidth和previewHeight,才处理预览图片

if (previewWidth == 0 || previewHeight == 0) {

return;

}

// 构造图片输出矩阵

if (rgbBytes == null) {

rgbBytes = new int[previewWidth * previewHeight];

}

try {

// 获取图片

final Image image = reader.acquireLatestImage();

if (image == null) {

return;

}

// 正在处理图片时,则直接返回

if (isProcessingFrame) {

image.close();

return;

}

// yuv转换为rgb格式

isProcessingFrame = true;

Trace.beginSection(“imageAvailable”);

final Plane[] planes = image.getPlanes();

fillBytes(planes, yuvBytes);

yRowStride = planes[0].getRowStride();

final int uvRowStride = planes[1].getRowStride();

final int uvPixelStride = planes[1].getPixelStride();

imageConverter =

new Runnable() {

@Override

public void run() {

ImageUtils.convertYUV420ToARGB8888(

yuvBytes[0],

yuvBytes[1],

yuvBytes[2],

previewWidth,

previewHeight,

yRowStride,

uvRowStride,

uvPixelStride,

rgbBytes);

}

};

postInferenceCallback =

new Runnable() {

@Override

public void run() {

image.close();

isProcessingFrame = false;

}

};

// 这儿是关键,利用训练模型来预测图片,后面详细分析

processImage();

} catch (final Exception e) {

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

Trace.endSection();

return;

}

Trace.endSection();

}

onImageAvailable()先做一些预校验,如previewWidth是否被设置,当前是否正在处理图片等。然后将相机捕获的yuv格式图像转为rgb格式。最后,也是最重要的一步,调用processImage,利用TensorFlow模型来处理图片。下面我们详细分析processImage

protected void processImage() {

// 图片的绘制等,不是模型预测的重点,不分析了

rgbFrameBitmap.setPixels(getRgbBytes(), 0, previewWidth, 0, 0, previewWidth, previewHeight);

关于面试的充分准备

一些基础知识和理论肯定是要背的,要理解的背,用自己的语言总结一下背下来。

虽然 Android 没有前几年火热了,已经过去了会四大组件就能找到高薪职位的时代了。这只能说明 Android 中级以下的岗位饱和了,现在高级工程师还是比较缺少的,我能明显感觉到国庆后多了很多高级职位,所以努力让自己成为高级工程师才是最重要的。

好了,希望对大家有所帮助。

接下来是整理的一些Android学习资料,有兴趣的朋友们可以关注下我免费领取方式

①Android开发核心知识点笔记

②对标“阿里 P7” 40W+年薪企业资深架构师成长学习路线图

③面试精品集锦汇总

④全套体系化高级架构视频

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!

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

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

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

以努力让自己成为高级工程师才是最重要的。

好了,希望对大家有所帮助。

接下来是整理的一些Android学习资料,有兴趣的朋友们可以关注下我免费领取方式

①Android开发核心知识点笔记

②对标“阿里 P7” 40W+年薪企业资深架构师成长学习路线图

[外链图片转存中…(img-9UVNkfS9-1715692536806)]

③面试精品集锦汇总

[外链图片转存中…(img-aOHxGWQf-1715692536807)]

④全套体系化高级架构视频

**Android精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水!

[外链图片转存中…(img-lz65Ha3K-1715692536808)]

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值