// 利用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步
-
预处理输入图片,读取像素点,并将RGB三通道数值归一化. 归一化后分布于 -117 ~ 138
-
将输入数据填充到TensorFlow中,并feed数据给模型
-
跑TensorFlow预测模型
-
将tensorflow预测模型输出节点的输出值拷贝出来
总结
开发是面向对象。我们找工作应该更多是面向面试。哪怕进大厂真的只是去宁螺丝,但你要进去得先学会面试的时候造飞机不是么?
作者13年java转Android开发,在小厂待过,也去过华为,OPPO等,去年四月份进了阿里一直到现在。等大厂待过也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。
这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
相信它会给大家带来很多收获:
资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!
要分为5步
-
预处理输入图片,读取像素点,并将RGB三通道数值归一化. 归一化后分布于 -117 ~ 138
-
将输入数据填充到TensorFlow中,并feed数据给模型
-
跑TensorFlow预测模型
-
将tensorflow预测模型输出节点的输出值拷贝出来
总结
开发是面向对象。我们找工作应该更多是面向面试。哪怕进大厂真的只是去宁螺丝,但你要进去得先学会面试的时候造飞机不是么?
作者13年java转Android开发,在小厂待过,也去过华为,OPPO等,去年四月份进了阿里一直到现在。等大厂待过也面试过很多人。深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。
这里附上上述的技术体系图相关的几十套腾讯、头条、阿里、美团等公司的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。
相信它会给大家带来很多收获:
[外链图片转存中…(img-J6yOJ2lC-1715876238454)]
[外链图片转存中…(img-7TcgynOE-1715876238458)]
资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图
当程序员容易,当一个优秀的程序员是需要不断学习的,从初级程序员到高级程序员,从初级架构师到资深架构师,或者走向管理,从技术经理到技术总监,每个阶段都需要掌握不同的能力。早早确定自己的职业方向,才能在工作和能力提升中甩开同龄人。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门,即可获取!