内容简介:上一篇文章主要是掌握ZXing解码整体的步骤,关于细节方面的代码就一笔带过了,本篇文章将会深入细节,更详细的讲解有关相机配置方面的知识。直接看代码,找到调用相机初始化配置的代码,上篇文章已经分析了在上篇文章分析到这句代码
上一篇文章主要是掌握ZXing解码整体的步骤,关于细节方面的代码就一笔带过了,本篇文章将会深入细节,更详细的讲解有关相机配置方面的知识。
ZXing的相机初始配置
直接看代码,找到调用相机初始化配置的代码,上篇文章已经分析了在 CaptureActivity
中怎么调到 initCamera
方法的,这里再次看下这个方法的代码,如下
private void initCamera(SurfaceHolder surfaceHolder) { if (surfaceHolder == null) { throw new IllegalStateException("No SurfaceHolder provided"); } //相机已经打开 if (cameraManager.isOpen()) { Log.w(TAG, "initCamera() while already open -- late SurfaceView callback?"); return; } try { //打开相机并初始化硬件参数 cameraManager.openDriver(surfaceHolder); // 实例化一个handler并开始预览. if (handler == null) { handler = new CaptureActivityHandler(this, decodeFormats, decodeHints, characterSet, cameraManager); } decodeOrStoreSavedBitmap(null, null); } catch (IOException ioe) { Log.w(TAG, ioe); displayFrameworkBugMessageAndExit(); } catch (RuntimeException e) { // Barcode Scanner has seen crashes in the wild of this variety: // java.?lang.?RuntimeException: Fail to connect to camera service Log.w(TAG, "Unexpected error initializing camera", e); displayFrameworkBugMessageAndExit(); } } 复制代码
上篇文章分析到这句代码 cameraManager.openDriver(surfaceHolder);
就直接说了这句代码的作用,并没有进入 openDriver
方法详细的看代码,这里看下 openDriver
中的代码,如下
public synchronized void openDriver(SurfaceHolder holder) throws IOException { OpenCamera theCamera = camera; if (theCamera == null) { //更具requestedCameraId打开对应的摄像头 theCamera = OpenCameraInterface.open(requestedCameraId); if (theCamera == null) { throw new IOException("Camera.open() failed to return object from driver"); } camera = theCamera; } //是否已经初始化,没有初始化则进行初始化 if (!initialized) { initialized = true; configManager.initFromCameraParameters(theCamera);//分析一 if (requestedFramingRectWidth > 0 && requestedFramingRectHeight > 0) { setManualFramingRect(requestedFramingRectWidth, requestedFramingRectHeight); requestedFramingRectWidth = 0; requestedFramingRectHeight = 0; } } Camera cameraObject = theCamera.getCamera(); Camera.Parameters parameters = cameraObject.getParameters(); String parametersFlattened = parameters == null ? null : parameters.flatten(); // Save these, temporarily try { configManager.setDesiredCameraParameters(theCamera, false); } catch (RuntimeException re) { // Driver failed Log.w(TAG, "Camera rejected parameters. Setting only minimal safe-mode parameters"); Log.i(TAG, "Resetting to saved camera params: " + parametersFlattened); // Reset: if (parametersFlattened != null) { parameters = cameraObject.getParameters(); parameters.unflatten(parametersFlattened); try { cameraObject.setParameters(parameters); configManager.setDesiredCameraParameters(theCamera, true); } catch (RuntimeException re2) { // Well, darn. Give up Log.w(TAG, "Camera rejected even safe-mode parameters! No configuration"); } } } cameraObject.setPreviewDisplay(holder); } 复制代码
这里重点看下“分析一” initFromCameraParameters
方法中的代码,如下
void initFromCameraParameters(OpenCamera camera) { Camera.Parameters parameters = camera.getCamera().getParameters(); WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); //获取WindowManager默认的Display Display display = manager.getDefaultDisplay(); //屏幕的旋转角度 int displayRotation = display.getRotation(); int cwRotationFromNaturalToDisplay; switch (displayRotation) { case Surface.ROTATION_0: cwRotationFromNaturalToDisplay = 0; break; case Surface.ROTATION_90: cwRotationFromNaturalToDisplay = 90; break; case Surface.ROTATION_180: cwRotationFromNaturalToDisplay = 180; break; case Surface.ROTATION_270: cwRotationFromNaturalToDisplay = 270; break; default: // Have seen this return incorrect values like -90 if (displayRotation % 90 == 0) { cwRotationFromNaturalToDisplay = (360 + displayRotation) % 360; } else { throw new IllegalArgumentException("Bad rotation: " + displayRotation); } } Log.i(TAG, "Display at: " + cwRotationFromNaturalToDisplay); int cwRotationFromNaturalToCamera = camera.getOrientation(); Log.i(TAG, "Camera at: " + cwRotationFromNaturalToCamera); // Still not 100% sure about this. But acts like we need to flip this: if (camera.getFacing() == CameraFacing.FRONT) { cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360; Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera); } /* SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); String overrideRotationString; if (camera.getFacing() == CameraFacing.FRONT) { overrideRotationString = prefs.getString(PreferencesActivity.KEY_FORCE_CAMERA_ORIENTATION_FRONT, null); } else { overrideRotationString = prefs.getString(PreferencesActivity.KEY_FORCE_CAMERA_ORIENTATION, null); } if (overrideRotationString != null && !"-".equals(overrideRotationString)) { Log.i(TAG, "Overriding camera manually to " + overrideRotationString); cwRotationFromNaturalToCamera = Integer.parseInt(overrideRotationString); } */ cwRotationFromDisplayToCamera = (360 + cwRotationFromNaturalToCamera - cwRotationFromNaturalToDisplay) % 360; Log.i(TAG, "Final display orientation: " + cwRotationFromDisplayToCamera); if (camera.getFacing() == CameraFacing.FRONT) { Log.i(TAG, "Compensating rotation for front camera"); cwNeededRotation = (360 - cwRotationFromDisplayToCamera) % 360; } else { cwNeededRotation = cwRotationFromDisplayToCamera; } Log.i(TAG, "Clockwise rotation from display to camera: " + cwNeededRotation); Point theScreenResolution = new Point(); display.getSize(theScreenResolution); screenResolution = theScreenResolution; Log.i(TAG, "Screen resolution in current orientation: " + screenResolution); cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution); Log.i(TAG, "Camera resolution: " + cameraResolution); bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution); Log.i(TAG, "Best available preview size: " + bestPreviewSize); boolean isScreenPortrait = screenResolution.x < screenResolution.y; boolean isPreviewSizePortrait = bestPreviewSize.x > bestPreviewSize.y; if (isScreenPortrait == isPreviewSizePortrait) { previewSizeOnScreen = bestPreviewSize; } else { previewSizeOnScreen = new Point(bestPreviewSize.y, bestPreviewSize.x); } Log.i(TAG, "Preview size on screen: " + previewSizeOnScreen); } 复制代码
虽然这个方法代码有点多,但是因为这个方法是用来相机初始配置的,所以,要详细的分析一下,首先看下这部分的代码
Camera.Parameters parameters = camera.getCamera().getParameters(); WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); //获取WindowManager默认的Display Display display = manager.getDefaultDisplay(); //屏幕的旋转角度 int displayRotation = display.getRotation(); int cwRotationFromNaturalToDisplay; switch (displayRotation) { case Surface.ROTATION_0: cwRotationFromNaturalToDisplay = 0; break; case Surface.ROTATION_90: cwRotationFromNaturalToDisplay = 90; break; case Surface.ROTATION_180: cwRotationFromNaturalToDisplay = 180; break; case Surface.ROTATION_270: cwRotationFromNaturalToDisplay = 270; break; default: // Have seen this return incorrect values like -90 if (displayRotation % 90 == 0) { cwRotationFromNaturalToDisplay = (360 + displayRotation) % 360; } else { throw new IllegalArgumentException("Bad rotation: " + displayRotation); } } Log.i(TAG, "Display at: " + cwRotationFromNaturalToDisplay); int cwRotationFromNaturalToCamera = camera.getOrientation(); Log.i(TAG, "Camera at: " + cwRotationFromNaturalToCamera); // Still not 100% sure about this. But acts like we need to flip this: if (camera.getFacing() == CameraFacing.FRONT) { cwRotationFromNaturalToCamera = (360 - cwRotationFromNaturalToCamera) % 360; Log.i(TAG, "Front camera overriden to: " + cwRotationFromNaturalToCamera); } cwRotationFromDisplayToCamera = (360 + cwRotationFromNaturalToCamera - cwRotationFromNaturalToDisplay) % 360; Log.i(TAG, "Final display orientation: " + cwRotationFromDisplayToCamera); if (camera.getFacing() == CameraFacing.FRONT) { Log.i(TAG, "Compensating rotation for front camera"); cwNeededRotation = (360 - cwRotationFromDisplayToCamera) % 360; } else { cwNeededRotation = cwRotationFromDisplayToCamera; } 复制代码
相信没有相机开发经验的同学,看到这段代码会一脸懵逼,没关系,我们一步步来,在理解这段代码前,需要我们掌握下面的一些概念。
- 屏幕坐标: 在Android系统中,屏幕的左上角是坐标系统的原点(0,0)坐标。原点向右延伸是X轴正方向,原点向下延伸是Y轴正方向。
- 自然方向: 每个设备都有一个自然方向,手机和平板的自然方向不同。手机的自然方向是portrait(竖屏),平板的自然方向是landscape(横屏)。
- 图像传感器(Image Sensor)方向: 手机相机的图像数据都是来自于摄像头硬件的图像传感器,这个传感器在被固定到手机上后有一个默认的取景方向,这个方向如下图所示,坐标原点位于手机横放时的左上角:
- 相机图像的预览方向: Android 系统提供一个 API 来手动设置 Camera 的预览方向,叫 setDisplayOrientation。默认情况下这个值是0,与图像 Sensor 方向一致,所以对于横屏应用来说就不需要更改这个 Camera 预览方向。 但是,如果你的应用是竖屏应用,就必须通过这个 API 将 Camera 的预览方向旋转 90 度,让摄像头预览方向与手机屏幕方向保持一致,这样才会得到正确的预览画面。
- 相机采集照片的方向: 这个与相机的预览方向无关,相机采集照片的方向与Image Sensor 方向一致,如果竖屏拍照后直接保存,这时候保存的照片会是横屏的。
强烈建议大家先看下这篇文章 Android: Camera相机开发详解(上) —— 知识储备 ,相信看过之后,你就会理解上面的代码了,其实,上面代码的作用就是 设置相机采集图片的预览方向,就是无论手机是横屏还是竖屏,你看到的图像都是与手机方向一致的。
设置相机预览图像的最佳比例
文章前部分,已经分析了ZXing设置预览方向的代码,但是只设置预览方向还是不够的,还要根据屏幕的宽高比来找到相机采集图片最合适的预览尺寸,否则就会出现相机预览图拉伸变形的问题。 继续看 initFromCameraParameters
方法中的代码,如下
Point theScreenResolution = new Point(); display.getSize(theScreenResolution); screenResolution = theScreenResolution; Log.i(TAG, "Screen resolution in current orientation: " + screenResolution); cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution); Log.i(TAG, "Camera resolution: " + cameraResolution); bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution); Log.i(TAG, "Best available preview size: " + bestPreviewSize); boolean isScreenPortrait = screenResolution.x < screenResolution.y; boolean isPreviewSizePortrait = bestPreviewSize.x > bestPreviewSize.y; if (isScreenPortrait == isPreviewSizePortrait) { previewSizeOnScreen = bestPreviewSize; } else { previewSizeOnScreen = new Point(bestPreviewSize.y, bestPreviewSize.x); } 复制代码
上面代码中的 screenResolution
变量是屏幕分辨率,从这个变量中可以分别获取屏幕宽高的像素值。我们来重点看下这两句代码
cameraResolution = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution); bestPreviewSize = CameraConfigurationUtils.findBestPreviewSizeValue(parameters, screenResolution); 复制代码
上面的一句代码是获取相机的最佳分辨率,下面的一句代码是获取获取相机的最佳预览尺寸。现在来看下是怎么获取最佳尺寸的, findBestPreviewSizeValue
方法的代码如下
public static Point findBestPreviewSizeValue(Camera.Parameters parameters, Point screenResolution) { //获取相机支持的尺寸,手机不同会有不同的值 List<Camera.Size> rawSupportedSizes = parameters.getSupportedPreviewSizes(); if (rawSupportedSizes == null) { Log.w(TAG, "Device returned no supported preview sizes; using default"); Camera.Size defaultSize = parameters.getPreviewSize(); if (defaultSize == null) { throw new IllegalStateException("Parameters contained no preview size!"); } return new Point(defaultSize.width, defaultSize.height); } if (Log.isLoggable(TAG, Log.INFO)) { StringBuilder previewSizesString = new StringBuilder(); for (Camera.Size size : rawSupportedSizes) { previewSizesString.append(size.width).append('x').append(size.height).append(' '); } Log.i(TAG, "Supported preview sizes: " + previewSizesString); } //这句代码是获取屏幕宽高的比例 double screenAspectRatio = screenResolution.x / (double) screenResolution.y; // Find a suitable size, with max resolution int maxResolution = 0; Camera.Size maxResPreviewSize = null; //for循环的作用是找到相机合适的尺寸和最大的分辨率,这里 //合适的尺寸指的是和屏幕宽高比相同的尺寸。 for (Camera.Size size : rawSupportedSizes) { int realWidth = size.width; int realHeight = size.height; int resolution = realWidth * realHeight; if (resolution < MIN_PREVIEW_PIXELS) { continue; } boolean isCandidatePortrait = realWidth < realHeight; int maybeFlippedWidth = isCandidatePortrait ? realHeight : realWidth; int maybeFlippedHeight = isCandidatePortrait ? realWidth : realHeight; double aspectRatio = maybeFlippedWidth / (double) maybeFlippedHeight; double distortion = Math.abs(aspectRatio - screenAspectRatio); if (distortion > MAX_ASPECT_DISTORTION) { continue; } //这句代码是找到与屏幕宽高比一致的尺寸,否则就用相机默认的尺寸 if (maybeFlippedWidth == screenResolution.x && maybeFlippedHeight == screenResolution.y) { Point exactPoint = new Point(realWidth, realHeight); Log.i(TAG, "Found preview size exactly matching screen size: " + exactPoint); return exactPoint; } // Resolution is suitable; record the one with max resolution if (resolution > maxResolution) { maxResolution = resolution; maxResPreviewSize = size; } } // If no exact match, use largest preview size. This was not a great idea on older devices because // of the additional computation needed. We're likely to get here on newer Android 4+ devices, where // the CPU is much more powerful. if (maxResPreviewSize != null) { Point largestSize = new Point(maxResPreviewSize.width, maxResPreviewSize.height); Log.i(TAG, "Using largest suitable preview size: " + largestSize); return largestSize; } // If there is nothing at all suitable, return current preview size Camera.Size defaultPreview = parameters.getPreviewSize(); if (defaultPreview == null) { throw new IllegalStateException("Parameters contained no preview size!"); } Point defaultSize = new Point(defaultPreview.width, defaultPreview.height); Log.i(TAG, "No suitable preview sizes, using default: " + defaultSize); return defaultSize; } 复制代码
从上面代码中的注释可以看到这里存在一些小问题, 上面代码的逻辑是有与屏幕像素比例相同的相机尺寸才返回,否则就用相机默认的尺寸,相机默认的尺寸可能与屏幕的尺寸比有较大的差距,这样就会出现预览图像变形的问题。
这里可以将代码优化为,返回最接近屏幕宽高比的相机尺寸。这里的优化将会在后面的文章中进行详细的讲解。
上面的代码是将一些变量的值设置好,最终,配置相机的参数在 CameraConfigurationManager
类中的 setDesiredCameraParameters
中,这里就不详细分析了。
旋转采集图片的方向
这里没有处理采集的照片,采集到的照片数据还是横屏的,如下
这个图片是我竖屏时扫描的,但是获取相机采集的数据确是横屏的,所以,需要进行一些处理。 首先,需要在相机捕获图像数据成功的回调方法onPreviewFrame
中改变代码,更改后的代码如下
@Override public void onPreviewFrame(byte[] data, Camera camera) { Point cameraResolution = configManager.getCameraResolution(); Handler thePreviewHandler = previewHandler; if (cameraResolution != null && thePreviewHandler != null) { Point screenResolution = configManager.getScreenResolution(); Message message; if (screenResolution.x < screenResolution.y){ // 手机为竖屏时 message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.y, cameraResolution.x, data); } else { // 手机为横屏时 message = thePreviewHandler.obtainMessage(previewMessage, cameraResolution.x, cameraResolution.y, data); } message.sendToTarget(); previewHandler = null; } else { Log.d(TAG, "Got preview callback, but no handler or resolution available"); } 复制代码
解释:手机竖屏时,相机传感器采集的数据为横屏的数据,为了与竖屏相对应,需要将相机采集的图片宽高互换,这里只是互换了宽高,但是采集的数据宽高并没有转换,因此还需要将数据的宽高转换。
代码如下
//将原始图像传感器的数据转换为竖屏 if (width < height) { // portrait byte[] rotatedData = new byte[data.length]; for (int x = 0; x < width; x++) { for (int y = 0; y < height; y++) rotatedData[y * width + width - x - 1] = data[y + x * height]; } data = rotatedData; } 复制代码
将上面的代码,加入到 DecodeHandler
类中的 decode
方法开头即可。
虽然,这时已经将相机采集的横屏数据转化为竖屏的了,但是,工作还没有完成,还需要设置获取二维码的区域,设置的方法是CameraManager类中的getFramingRectInPrevie方法。这里我就补贴具体的代码了,大家根据前文的内容和自己的思考来修改里面的代码。
结束语
文章主要分析了相机配置的代码,选择拍摄图像的最佳尺寸及处理相机采集到的数据,重点是要理解相机的数据采集与图像预览的设置。本篇修改的代码在 这里 。
本文已由公众号“AndroidShared”首发
以上所述就是小编给大家介绍的《ZXing源码解析三:相机的配置与数据的处理》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- ReactNative源码解析-初识源码
- Spring源码系列:BeanDefinition源码解析
- Spring源码分析:AOP源码解析(下篇)
- Spring源码分析:AOP源码解析(上篇)
- 注册中心 Eureka 源码解析 —— EndPoint 与 解析器
- 新一代Json解析库Moshi源码解析
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。