tensorflow-android 官方demo源码分析

// 利用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);

final Canvas canvas = new Canvas(croppedBitmap);

canvas.drawBitmap(rgbFrameBitmap, frameToCropTransform, null);

// For examining the actual TF input.

if (SAVE_PREVIEW_BITMAP) {

ImageUtils.saveBitmap(croppedBitmap);

}

// 利用分类器classifier对图片进行预测分析,得到图片为每个分类的概率. 比较耗时,放在子线程中

runInBackground(

new Runnable() {

@Override

public void run() {

final long startTime = SystemClock.uptimeMillis();

// 1 classifier对图片进行识别,得到输入图片为每个分类的概率

final List<Classifier.Recognition> results = classifier.recognizeImage(croppedBitmap);

lastProcessingTimeMs = SystemClock.uptimeMillis() - startTime;

LOGGER.i(“Detect: %s”, results);

// 2 将得到的前三个最大概率的分类的名字及概率,反馈到app上。也就是results区域

cropCopyBitmap = Bitmap.createBitmap(croppedBitmap);

if (resultsView == null) {

resultsView = (ResultsView) findViewById(R.id.results);

}

resultsView.setResults(results);

// 3 请求重绘,并准备下一次的识别

requestRender();

readyForNextImage();

}

});

}

processImage()先做图片绘制方面的工作,将相机捕获的图片绘制出来。然后利用分类器classifier来识别图片,获取图片为每个分类的概率。最后将概率最大的前三个分类,展示在result区域上。这儿我们重点来看分类器是如何来识别图片的。也就是classifier.recognizeImage()

public List recognizeImage(final Bitmap bitmap) {

// 1 预处理输入图片,读取像素点,并将RGB三通道数值归一化. 归一化后分布于 -117 ~ 138

bitmap.getPixels(intValues, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight());

for (int i = 0; i < intValues.length; ++i) {

final int val = intValues[i];

floatValues[i * 3 + 0] = (((val >> 16) & 0xFF) - imageMean) / imageStd; // 归一化通道R

floatValues[i * 3 + 1] = (((val >> 8) & 0xFF) - imageMean) / imageStd; // 归一化通道G

floatValues[i * 3 + 2] = ((val & 0xFF) - imageMean) / imageStd; // 归一化通道B

}

Trace.endSection();

// 2 将输入数据填充到TensorFlow中,并feed数据给模型

// inputName为输入节点

// floatValues为输入tensor的数据源,

// dims构成了tensor的shape, [batch_size, height, width, in_channel], 此处为[1, inputSize, inputSize, 3]

Trace.beginSection(“feed”);

inferenceInterface.feed(inputName, floatValues, 1, inputSize, inputSize, 3);

Trace.endSection();

// 3 跑TensorFlow预测模型

// outputNames为输出节点名, 通过session来run tensor

Trace.beginSection(“run”);

inferenceInterface.run(outputNames, logStats);

Trace.endSection();

// 4 将tensorflow预测模型输出节点的输出值拷贝出来

// 找到输出节点outputName的tensor,并复制到outputs中。outputs为分类预测的结果,是一个一维向量,每个值对应labels中一个分类的概率。

Trace.beginSection(“fetch”);

inferenceInterface.fetch(outputName, outputs);

Trace.endSection();

// 5 得到概率最大的前三个分类,并组装为Recognition对象

PriorityQueue pq =

new PriorityQueue(

3,

new Comparator() {

@Override

public int compare(Recognition lhs, Recognition rhs) {

// Intentionally reversed to put high confidence at the head of the queue.

return Float.compare(rhs.getConfidence(), lhs.getConfidence());

}

});

for (int i = 0; i < outputs.length; ++i) {

if (outputs[i] > THRESHOLD) {

pq.add(

new Recognition(

“” + i, labels.size() > i ? labels.get(i) : “unknown”, outputs[i], null));

}

}

final ArrayList recognitions = new ArrayList();

int recognitionsSize = Math.min(pq.size(), MAX_RESULTS);

for (int i = 0; i < recognitionsSize; ++i) {

recognitions.add(pq.poll());

}

Trace.endSection(); // “recognizeImage”

return recognitions;

}

图片识别主要分为5步

  1. 预处理输入图片,读取像素点,并将RGB三通道数值归一化. 归一化后分布于 -117 ~ 138

  2. 将输入数据填充到TensorFlow中,并feed数据给模型

  3. 跑TensorFlow预测模型

  4. 将tensorflow预测模型输出节点的输出值拷贝出来

总结

开发是面向对象。我们找工作应该更多是面向面试。哪怕进大厂真的只是去宁螺丝,但你要进去得先学会面试的时候造飞机不是么?

作者13年java转Android开发,在小厂待过,也去过华为,OPPO等,去年四月份进了阿里一直到现在。等大厂待过也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

相信它会给大家带来很多收获:

960页全网最全Android开发笔记

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
要分为5步

  1. 预处理输入图片,读取像素点,并将RGB三通道数值归一化. 归一化后分布于 -117 ~ 138

  2. 将输入数据填充到TensorFlow中,并feed数据给模型

  3. 跑TensorFlow预测模型

  4. 将tensorflow预测模型输出节点的输出值拷贝出来

总结

开发是面向对象。我们找工作应该更多是面向面试。哪怕进大厂真的只是去宁螺丝,但你要进去得先学会面试的时候造飞机不是么?

作者13年java转Android开发,在小厂待过,也去过华为,OPPO等,去年四月份进了阿里一直到现在。等大厂待过也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。

这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

相信它会给大家带来很多收获:

[外链图片转存中…(img-jJXZjKNh-1715429866326)]

[外链图片转存中…(img-KqRx1U1O-1715429866330)]

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图

当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 15
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值