前面几篇文章介绍了Camera1,Camera2,CameraView和CameraX的使用,对各个API的使用,应该问题不大,不过在真正开发过程中,也会遇到各种不同的问题,本篇文章继续介绍相机开发过程中遇到的问题,主要是相机预览、拍照尺寸,方向,以及图像数据的处理。
尺寸
这里的尺寸,主要是预览尺寸、拍照尺寸和显示预览画面的View大小。
预览尺寸
如何获取预览尺寸?我们可以从cameraview的源码中获取到,分为了Camera1和Camera2。
Camera1
mCameraParameters = mCamera.getParameters();
// Supported preview sizes
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
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手机上支持的所有预览尺寸:
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
private Size chooseOptimalSize(SortedSet<Size> sizes) {
if (!mPreview.isReady()) { // Not yet laid out
return sizes.first(); // Return the smallest size
}
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) { // Iterate from small to large
if (desiredWidth <= size.getWidth() && desiredHeight <= size.getHeight()) {
return size;
}
result = size;
}
return result;
}
区分了横竖屏,然后得到尺寸中宽和高等于或者大于View的宽高的尺寸。
Camera2
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);
// Pick the smallest of those big enough
for (Size size : candidates) {
if (size.getWidth() >= surfaceLonger && size.getHeight() >= surfaceShorter) {
return size;
}
}
// If no size is big enough, pick the largest one.
return candidates.last();
}
先判断View宽高,区分其中较大值和较小值,然后再得到尺寸中宽和高大于或者等于View的较大值和较小值的尺寸。
拍照尺寸
代码也是从cameraview中截取出来的
Camera1
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
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手机支持的拍照尺寸:
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都是一样的逻辑,选取固定宽高比例中的最大尺寸,这样拍摄的图片最清晰。
Size largest = mPictureSizes.sizes(mAspectRatio).last();
方向
这里的设置方向有两种:图像预览方向和拍照方向。在这之前,需要先介绍几个概念:
- 屏幕坐标方向
- 设备自然方向
- 摄像头传感器方向
- 相机预览方向
屏幕坐标方向
在Android系统中,以屏幕左上角为坐标系统的原点(0,0)坐标,该坐标系是固定不变的,不会因为设备方向的变化而改变。
屏幕自然方向
每个设备都有一个自然方向,手机和平板自然方向不一样,如图所示,这里盗个图:
默认情况下,平板的自然方向是横屏,而手机的自然方向是竖屏方向。Android系统可以通过View的OrientationEventListener
监听设备方向,回调方法:
abstract public void onOrientationChanged(int orientation);
onOrientationChanged
返回0到359的角度,其中0表示自然方向。
摄像头传感器方向
手机相机的图像数据都是来自于摄像头硬件的图像传感器,这个传感器在被固定到手机上后有一个默认的取景方向,方向一般是和手机横屏方向一致,如上图所示。相机预览方向
将摄像头传感器捕获的图像,显示在屏幕上的方向,就是相机预览方向。默认情况下,和摄像头传感器方向一致,可以通过Camera API进行改变。
Camaer1可以使用setDisplayOrientation
设置预览方向,Camera2则可以通过TextureView来实现。
不同的摄像头位置,orientation
是不一样的,orientation就是摄像头传感器方向顺时针旋转到屏幕自然方向的角度。
后置
对横屏来说,屏幕的自然方向和相机的摄像头传感器方向一致的。对竖屏来说,看到的图像逆时针旋转了90度,因此预览方向需要顺时针旋转90度,才能与屏幕的自然方向保持一致。
前置
前置的orientation
270,收集到图像后(没有经过镜像处理),但是要显示到屏幕上,就要按照屏幕自然方向的坐标系来进行显示,需要顺时针旋转270度,才能和设备自然方向一致。预览的时候,做了镜像处理,所以只需要顺时针旋转90度,就能和设置自然方向一致。
那么Camera1和Camera2具体设置预览方向的代码,来自cameraview:
Camera1
private int calcDisplayOrientation(int screenOrientationDegrees) {
if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
return (360 - (mCameraInfo.orientation + screenOrientationDegrees) % 360) % 360; // compensate the mirror
} else { // back-facing
return (mCameraInfo.orientation - screenOrientationDegrees + 360) % 360;
}
}
代码中区分了前置和后置摄像头。
- 后置:
(mCameraInfo.orientation - screenOrientationDegrees + 360) % 360
,恢复到自然方向需要顺时针旋转,而屏幕逆时针旋转正好抵掉了摄像头的旋转,两者差值+360取模。 - 前置:
(mCameraInfo.orientation + screenOrientationDegrees) % 360
,屏幕竖直方向看到的是一个镜像,360-(mCameraInfo.orientation + screenOrientationDegrees) % 360
,顺时针旋转这个差值可以到自然方向,只不过这是个镜像,左右翻转了
Camera2 使用的TextureView的setTransform进行旋转,并有区分横竖屏。
/**
* Configures the transform matrix for TextureView based on {@link #mDisplayOrientation} and
* the surface size.
*/
void configureTransform() {
Matrix matrix = new Matrix();
if (mDisplayOrientation % 180 == 90) {
final int width = getWidth();
final int height = getHeight();
// Rotate the camera preview when the screen is landscape.
matrix.setPolyToPoly(
new float[]{
0.f, 0.f, // top left
width, 0.f, // top right
0.f, height, // bottom left
width, height, // bottom right
}, 0,
mDisplayOrientation == 90 ?
// Clockwise
new float[]{
0.f, height, // top left
0.f, 0.f, // top right
width, height, // bottom left
width, 0.f, // bottom right
} : // mDisplayOrientation == 270
// Counter-clockwise
new float[]{
width, 0.f, // top left
width, height, // top right
0.f, 0.f, // bottom left
0.f, height, // bottom right
}, 0,
4);
} else if (mDisplayOrientation == 180) {
matrix.postRotate(180, getWidth() / 2, getHeight() / 2);
}
mTextureView.setTransform(matrix);
}
拍照方向
设置预览方向并不会改变拍出照片的方向。
对于后置相机,相机采集到的图像和相机预览的图像是一样的,只需要旋转后置相机orientation度。
对于前置相机来说,相机预览的图像和相机采集到的图像是镜像关系。
采集的图像:顺时针旋转270度后,与屏幕自然方向一致。
预览的图像:顺时针旋转90度后,与屏幕自然方向一致。
最后盗用一张图来说明:
Camera1
使用mCameraParameters.setRotation()
设置拍照后图像方向:
mCameraParameters.setRotation(calcCameraRotation(displayOrientation));
......
/**
* Calculate camera rotation
*
* This calculation is applied to the output JPEG either via Exif Orientation tag
* or by actually transforming the bitmap. (Determined by vendor camera API implementation)
*
* Note: This is not the same calculation as the display orientation
*
* @param screenOrientationDegrees Screen orientation in degrees
* @return Number of degrees to rotate image in order for it to view correctly.
*/
private int calcCameraRotation(int screenOrientationDegrees) {
if (mCameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
return (mCameraInfo.orientation + screenOrientationDegrees) % 360;
} else { // back-facing
final int landscapeFlip = isLandscape(screenOrientationDegrees) ? 180 : 0;
return (mCameraInfo.orientation + screenOrientationDegrees + landscapeFlip) % 360;
}
}
相机采集到的图像,只需要旋转相机orientation度。
Camera2
根据CameraCharacteristics.SENSOR_ORIENTATION
,使用captureRequest
设置了JPEG图像的旋转方向。
// Calculate JPEG orientation.
@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 UO Y1 V0 Y2 U2 Y3 V2
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再进行交替存储。
最后盗用一个数据格式的总结:
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
,在源码中有段代码:
if (format == ImageFormat.NV21) {
throw new IllegalArgumentException(
"NV21 format is not supported");
}
在最新的ImageFormat.NV21
上有说明:
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)