前面几篇文章介绍了Camera1,Camera2,CameraView和CameraX的使用,对各个API的使用,应该问题不大,不过在真正开发过程中,也会遇到各种不同的问题,本篇文章继续介绍相机开发过程中遇到的问题,主要是相机预览、拍照尺寸,方向,以及图像数据的处理。
尺寸 这里的尺寸,主要是预览尺寸、拍照尺寸和显示预览画面的View大小。
预览尺寸 如何获取预览尺寸?我们可以从cameraview 的源码中获取到,分为了Camera1和Camera2。
Camera1 1 2 3 4 5 6 7 8 mCameraParameters = mCamera.getParameters(); mPreviewSizes.clear(); for (Camera.Size size : mCameraParameters.getSupportedPreviewSizes()) { Log.d("DEBUG" , "###### SupportedPreviewSizes: width=" + size.width + ", height=" + size.height); mPreviewSizes.add(new Size (size.width, size.height)); }
Camera2 1 2 3 4 5 6 7 8 mPreviewSizes.clear(); for (android.util.Size size : map.getOutputSizes(mPreview.getOutputClass())) { int width = size.getWidth(); int height = size.getHeight(); if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) { mPreviewSizes.add(new Size (width, height)); } }
不同的厂商和系统所支持的预览尺寸是不一样,下面是红米Note 5A手机上支持的所有预览尺寸:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 SupportedPreviewSizes: width=1280 , height=720 SupportedPreviewSizes: width=960 , height=720 SupportedPreviewSizes: width=864 , height=480 SupportedPreviewSizes: width=800 , height=480 SupportedPreviewSizes: width=768 , height=432 SupportedPreviewSizes: width=720 , height=480 SupportedPreviewSizes: width=640 , height=640 SupportedPreviewSizes: width=640 , height=480 SupportedPreviewSizes: width=480 , height=640 SupportedPreviewSizes: width=640 , height=360 SupportedPreviewSizes: width=576 , height=432 SupportedPreviewSizes: width=480 , height=360 SupportedPreviewSizes: width=480 , height=320 SupportedPreviewSizes: width=384 , height=288 SupportedPreviewSizes: width=352 , height=288 SupportedPreviewSizes: width=320 , height=240 SupportedPreviewSizes: width=240 , height=320 SupportedPreviewSizes: width=240 , height=160 SupportedPreviewSizes: width=176 , height=144 SupportedPreviewSizes: width=144 , height=176 SupportedPreviewSizes: width=160 , height=120
这里尺寸的比例一般都是4:3、16:9,其他比例是在此基础上裁剪出来的
选取预览尺寸 在相同宽高比下,选择最接近View的宽高,避免过大的预览尺寸, 造成性能损耗, 引起预览卡顿。 在cameraview 源码中,默认定义的宽高比AspectRatio DEFAULT_ASPECT_RATIO = AspectRatio.of(4, 3)
Camera1 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private Size chooseOptimalSize (SortedSet<Size> sizes) { if (!mPreview.isReady()) { return sizes.first(); } int desiredWidth; int desiredHeight; final int surfaceWidth = mPreview.getWidth(); final int surfaceHeight = mPreview.getHeight(); if (isLandscape(mDisplayOrientation)) { desiredWidth = surfaceHeight; desiredHeight = surfaceWidth; } else { desiredWidth = surfaceWidth; desiredHeight = surfaceHeight; } Size result = null ; for (Size size : sizes) { if (desiredWidth <= size.getWidth() && desiredHeight <= size.getHeight()) { return size; } result = size; } return result; }
区分了横竖屏,然后得到尺寸中宽和高等于或者大于View的宽高的尺寸。
Camera2 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private Size chooseOptimalSize () { int surfaceLonger, surfaceShorter; final int surfaceWidth = mPreview.getWidth(); final int surfaceHeight = mPreview.getHeight(); if (surfaceWidth < surfaceHeight) { surfaceLonger = surfaceHeight; surfaceShorter = surfaceWidth; } else { surfaceLonger = surfaceWidth; surfaceShorter = surfaceHeight; } SortedSet<Size> candidates = mPreviewSizes.sizes(mAspectRatio); for (Size size : candidates) { if (size.getWidth() >= surfaceLonger && size.getHeight() >= surfaceShorter) { return size; } } return candidates.last(); }
先判断View宽高,区分其中较大值和较小值,然后再得到尺寸中宽和高大于或者等于View的较大值和较小值的尺寸。
拍照尺寸 代码也是从cameraview 中截取出来的
Camera1 1 2 3 4 5 6 mPictureSizes.clear(); for (Camera.Size size : mCameraParameters.getSupportedPictureSizes()) { Log.d("DEBUG" , "###### SupportedPictureSizes: width=" + size.width + ", height=" + size.height); mPictureSizes.add(new Size (size.width, size.height)); }
Camera2 1 2 3 4 5 protected void collectPictureSizes (SizeMap sizes, StreamConfigurationMap map) { for (android.util.Size size : map.getOutputSizes(ImageFormat.JPEG)) { mPictureSizes.add(new Size (size.getWidth(), size.getHeight())); } }
在红米Note 5A手机支持的拍照尺寸:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 SupportedPictureSizes: width=4160 , height=3120 SupportedPictureSizes: width=4160 , height=2340 SupportedPictureSizes: width=4096 , height=3072 SupportedPictureSizes: width=4096 , height=2304 SupportedPictureSizes: width=4000 , height=3000 SupportedPictureSizes: width=3840 , height=2160 SupportedPictureSizes: width=3264 , height=2448 SupportedPictureSizes: width=3200 , height=2400 SupportedPictureSizes: width=2976 , height=2976 SupportedPictureSizes: width=2592 , height=1944 SupportedPictureSizes: width=2592 , height=1458 SupportedPictureSizes: width=2688 , height=1512 SupportedPictureSizes: width=2304 , height=1728 SupportedPictureSizes: width=2048 , height=1536 SupportedPictureSizes: width=2336 , height=1314 SupportedPictureSizes: width=1920 , height=1080 SupportedPictureSizes: width=1600 , height=1200 SupportedPictureSizes: width=1440 , height=1080 SupportedPictureSizes: width=1280 , height=960 SupportedPictureSizes: width=1280 , height=768 SupportedPictureSizes: width=1280 , height=720 SupportedPictureSizes: width=1200 , height=1200 SupportedPictureSizes: width=1024 , height=768 SupportedPictureSizes: width=800 , height=600 SupportedPictureSizes: width=864 , height=480 SupportedPictureSizes: width=800 , height=480 SupportedPictureSizes: width=720 , height=480 SupportedPictureSizes: width=640 , height=480 SupportedPictureSizes: width=640 , height=360 SupportedPictureSizes: width=480 , height=640 SupportedPictureSizes: width=480 , height=360 SupportedPictureSizes: width=480 , height=320 SupportedPictureSizes: width=352 , height=288 SupportedPictureSizes: width=320 , height=240 SupportedPictureSizes: width=240 , height=320
这里尺寸的比例一般也是4:3、16:9
选取拍照尺寸 Camaer1和Camera2都是一样的逻辑,选取固定宽高比例中的最大尺寸,这样拍摄的图片最清晰。
1 Size largest = mPictureSizes.sizes(mAspectRatio).last();
方向 这里的设置方向有两种:图像预览方向和拍照方向。在这之前,需要先介绍几个概念:
屏幕坐标方向
设备自然方向
摄像头传感器方向
相机预览方向
屏幕坐标方向 在Android系统中,以屏幕左上角为坐标系统的原点(0,0)坐标,该坐标系是固定不变的,不会因为设备方向的变化而改变。
屏幕自然方向 每个设备都有一个自然方向,手机和平板自然方向不一样,如图所示,这里盗个图: 默认情况下,平板的自然方向是横屏,而手机的自然方向是竖屏方向。Android系统可以通过View的OrientationEventListener
监听设备方向,回调方法:
1 abstract public void onOrientationChanged (int orientation) ;
onOrientationChanged
返回0到359的角度,其中0表示自然方向。
摄像头传感器方向 手机相机的图像数据都是来自于摄像头硬件的图像传感器,这个传感器在被固定到手机上后有一个默认的取景方向,方向一般是和手机横屏方向一致,如上图所示。
相机预览方向 将摄像头传感器捕获的图像,显示在屏幕上的方向,就是相机预览方向。默认情况下,和摄像头传感器方向一致,可以通过Camera API进行改变。 Camaer1可以使用setDisplayOrientation
设置预览方向,Camera2则可以通过TextureView来实现。 不同的摄像头位置,orientation
是不一样的,orientation就是摄像头传感器方向顺时针旋转到屏幕自然方向的角度。
后置 后置的orientation
90 对横屏来说,屏幕的自然方向和相机的摄像头传感器方向一致的。 对竖屏来说,看到的图像逆时针旋转了90度,因此预览方向需要顺时针旋转90度,才能与屏幕的自然方向保持一致。
前置 前置的orientation
270,收集到图像后(没有经过镜像处理),但是要显示到屏幕上,就要按照屏幕自然方向的坐标系来进行显示,需要顺时针旋转270度,才能和设备自然方向一致。预览的时候,做了镜像处理,所以只需要顺时针旋转90度,就能和设置自然方向一致。 那么Camera1和Camera2具体设置预览方向的代码,来自cameraview :
Camera1
1 2 3 4 5 6 7 private int calcDisplayOrientation (int screenOrientationDegrees) { if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { return (360 - (mCameraInfo.orientation + screenOrientationDegrees) % 360 ) % 360 ; } else { return (mCameraInfo.orientation - screenOrientationDegrees + 360 ) % 360 ; } }
代码中区分了前置和后置摄像头。
后置:(mCameraInfo.orientation - screenOrientationDegrees + 360) % 360
,恢复到自然方向需要顺时针旋转,而屏幕逆时针旋转正好抵掉了摄像头的旋转,两者差值+360取模。
前置:(mCameraInfo.orientation + screenOrientationDegrees) % 360
,屏幕竖直方向看到的是一个镜像,360-(mCameraInfo.orientation + screenOrientationDegrees) % 360
,顺时针旋转这个差值可以到自然方向,只不过这是个镜像,左右翻转了
Camera2 使用的TextureView的setTransform进行旋转,并有区分横竖屏。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 void configureTransform () { Matrix matrix = new Matrix (); if (mDisplayOrientation % 180 == 90 ) { final int width = getWidth(); final int height = getHeight(); matrix.setPolyToPoly( new float []{ 0.f , 0.f , width, 0.f , 0.f , height, width, height, }, 0 , mDisplayOrientation == 90 ? new float []{ 0.f , height, 0.f , 0.f , width, height, width, 0.f , } : new float []{ width, 0.f , width, height, 0.f , 0.f , 0.f , height, }, 0 , 4 ); } else if (mDisplayOrientation == 180 ) { matrix.postRotate(180 , getWidth() / 2 , getHeight() / 2 ); } mTextureView.setTransform(matrix); }
拍照方向 设置预览方向并不会改变拍出照片的方向。 对于后置相机,相机采集到的图像和相机预览的图像是一样的,只需要旋转后置相机orientation度。 对于前置相机来说,相机预览的图像和相机采集到的图像是镜像关系。 采集的图像:顺时针旋转270度后,与屏幕自然方向一致。 预览的图像:顺时针旋转90度后,与屏幕自然方向一致。 最后盗用一张图来说明:
Camera1 使用mCameraParameters.setRotation()
设置拍照后图像方向:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 mCameraParameters.setRotation(calcCameraRotation(displayOrientation)); ...... private int calcCameraRotation (int screenOrientationDegrees) { if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) { return (mCameraInfo.orientation + screenOrientationDegrees) % 360 ; } else { final int landscapeFlip = isLandscape(screenOrientationDegrees) ? 180 : 0 ; return (mCameraInfo.orientation + screenOrientationDegrees + landscapeFlip) % 360 ; } }
相机采集到的图像,只需要旋转相机orientation度。
Camera2 根据CameraCharacteristics.SENSOR_ORIENTATION
,使用captureRequest
设置了JPEG图像的旋转方向。
1 2 3 4 5 6 7 8 @SuppressWarnings("ConstantConditions") int sensorOrientation = mCameraCharacteristics.get( CameraCharacteristics.SENSOR_ORIENTATION); captureRequestBuilder.set(CaptureRequest.JPEG_ORIENTATION, (sensorOrientation + mDisplayOrientation * (mFacing == Constants.FACING_FRONT ? 1 : -1 ) + 360 ) % 360 );
图像数据 Android Camera默认返回的数据格式是NV21。Camera1通过mParameters.setPreviewFormat()
设置,Camera2通过ImageReader.newInstance()
设置。 ImageFormat枚举了很多种图片格式,其中ImageFormat.NV21和ImageFormat.YV12是官方推荐的格式,NV21、YV12格式都属于 YUV 格式,也可以表示为YCbCr,Cb、Cr的含义等同于U、V。
YUV YUV是一种颜色编码方法,和它类似的还有RGB颜色编码方法,主要应用于电视系统和模拟视频领域。其中YUV代表三个分量,Y 代表明亮度,U 和 V 表示的是色度,色度又定义了颜色的两个方面:色调和饱和度。将Y与UV分离,没有UV信息一样可以显示完整的图像,但是只能显示灰度图。
YUV采样格式 YUV 图像的主流采样方式有如下三种:
YUV 4:4:4 采样:每一个Y对应一组UV分量
YUV 4:2:2 采样:每两个Y共用一组UV分量
YUV 4:2:0 采样:每四个Y共用一组UV分量
盗个图说明比较清晰,黑点表示采样该像素点的Y分量,空心圆圈表示采用该像素点的UV分量
YUV存储格式 有两种存储格式,planar和packed。
planar:先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V
packed:每个像素点的Y,U,V是连续交替存储
YUV格式信息可以参考:YUV pixel formats 根据采样方式和存储格式的不同,形成了多种YUV格式,常见的YUV格式:
采样/格式
YUV422
YUVY 格式
UYVY 格式
YUV422P 格式
YUV420
YUV420P (YV12、YU12格式)
YUV420P (NV12、NV21格式)
YUVY格式 YUVY格式属于packed存储格式,相邻的两个Y共用其相邻的两个U、V
Y0、Y1共用 U0、V0 Y2、Y3共用 U2、V2
UYVY格式 UYVY格式也属于packed存储格式,与YUYV格式不同的是UV的排列顺序不一样而已
YUV422P格式 YUV422P格式属于planar存储格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V
YV12、YU12格式 YU12和YV12格式都属于YUV420P格式,YUV420P是planar存储格式。先存储所有Y,然后在存储U、V。 YU12和YV12的区别在于YU12是先Y再U后V,而YV12是先Y再V后U。
NV12、NV21格式 NV12、NV21格式YUV420SP格式,YUV420SP也是planar存储格式。先存储所有Y,然后按照UV或者VU的交替顺序进行存储。 NV12格式先存储Y,然后UV再进行交替存储。 NV21格式则是先存储Y,然后VU再进行交替存储。 最后盗用一个数据格式的总结:
1 2 3 4 YV21: YYYYYYYY UU VV => YUV420P YV12: YYYYYYYY VV UU => YUV420P NV12: YYYYYYYY UV UV => YUV420SP NV21: YYYYYYYY VU VU => YUV420SP
Android Camera 默认数据格式是 NV21,Camera1直接设置mParameters.setPreviewFormat(ImageFormat.NV21)
,然后拍照回调中的 raw data 数据返回就是 NV21的。 Camera2通过ImageReader.newInstance()
设置,但是不能直接设置格式ImageFormat.NV21
,在源码中有段代码:
1 2 3 4 if (format == ImageFormat.NV21) { throw new IllegalArgumentException ( "NV21 format is not supported" ); }
在最新的ImageFormat.NV21
上有说明:
1 2 3 4 YCrCb format used for images, which uses the NV21 encoding format. This is the default format for android.hardware.Camera preview images, when not otherwise set with android.hardware.Camera.Parameters.setPreviewFormat(int ). For the android.hardware.camera2 API, the YUV_420_888 format is recommended for YUV output instead.
Camera2建议使用YUV_420_888
来替代,所以要得到NV21的数据需要进行数据转化,具体可以参考Image类浅析(结合YUV_420_888)
参考