文档管理中心
最佳实践媒体相机相机分段式拍照性能优化

相机分段式拍照性能优化

概述

相机拍照性能依赖算法处理的速度,而处理效果依赖算法的复杂度,算法复杂度越高的情况下会导致处理时间越长。目前系统相机开发有两种相机拍照方案,分别是相机分段式拍照相机单段式拍照

  • 分段式拍照是系统相机开发的重要功能之一,即相机拍照可输出低质量图用作缩略图,提升用户感知拍照速度,同时使用高质量图保证最后的成图质量达到系统相机的水平,既满足了图像处理算法的需求,又不会阻塞前台的拍照速度,构筑相机性能竞争力,提升了用户的体验。
  • 单段式拍照是指在拍照过程中通过多帧融合以及多个底层算法处理之后返回一张高质量图片,这样导致Shot2See(用户点击拍照控件到在缩略图显示区域显示缩略图的过程)完成时延比较长。

分段式与单段式拍照的全质量图输出质量一致,但输出低质量图场景下单段式更优。如果开发者不需要获取全质量图并且也不考虑Shot2See的完成时延,建议使用单段式拍照,否则的话,建议使用分段式拍照。本篇文章主要以相机Shot2See场景为例,来展示分段式拍照Shot2See的完成时延要低于单段式拍照。

图1 分段式拍照流程示意图

效果展示

图2 单段式拍照效果图

图3 分段式拍照效果图

从上述效果图中可以看出,分段式拍照从用户点击拍照控件到在缩略图显示区域显示缩略图的耗时比单段式拍照要短。

性能对比分析方式

静态校验:在相机类应用中,如果使用单段式拍照,拍照过程中该场景下仅会返回一张图片,将图片用作Shot2See后的缩略图则会导致Shot2See完成时延比较长。

动态校验:开发者可以通过DevEco Studio中的Profiler工具去抓取Trace,获取到Trace之后,根据PhotoOutputNapi::Capture()和OnBufferAvailable()找到对应的Trace Marker,并通过两者之间的时间段来分析耗时,单段式拍照的时长为1900ms,而分段式拍照的时长为672.7ms。

图4 单段式拍照性能数据图

图5 分段式拍照耗时数据

性能对比分析表

展开

拍照实现方式

耗时(局限不同设备和场景,数据仅供参考)

单段式拍照

1900ms

分段式拍照

672.7ms

优化思路:在需要加快Shot2See完成时延的场景下,使用相机框架开发的分段式拍照方案,加快阶段一照片生成的速度。

场景示例

下面以应用中相机Shot2See(拍照之后自动跳转到照片编辑界面)为例,通过单段式拍照和分段式拍照的性能功耗对比,来展示两者的性能差异。

单段式拍照:

单段式拍照使用了on(type: 'photoAvailable', callback: AsyncCallback<Photo>): void接口注册了高质量图的监听,默认不使能分段式拍照。具体操作步骤如下所示:

1.相机媒体数据写入XComponent组件中,用来显示图像效果。具体代码如下所示:

收起
自动换行
深色代码主题
复制
  1. XComponent({
  2. type: XComponentType.SURFACE,
  3. controller: this.mXComponentController,
  4. imageAIOptions: this.options
  5. })
  6. .onLoad(async () => {
  7. Logger.info(TAG, 'onLoad is called');
  8. this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
  9. GlobalContext.get().setObject('cameraDeviceIndex', this.defaultCameraDeviceIndex);
  10. GlobalContext.get().setObject('xComponentSurfaceId', this.surfaceId);
  11. Logger.info(TAG, `onLoad surfaceId: ${this.surfaceId}`);
  12. await CameraService.initCamera(this.surfaceId, this.defaultCameraDeviceIndex);
  13. })
  14. .border({
  15. width: {
  16. top: Constants.X_COMPONENT_BORDER_WIDTH,
  17. bottom: Constants.X_COMPONENT_BORDER_WIDTH
  18. },
  19. color: Color.Black
  20. })
  21. .width('100%')
  22. .height(523)
  23. .margin({ top: 75, bottom: 72 })

2.initCamera函数完成一个相机生命周期初始化的过程。

(1) getCameraManager()获取CameraMananger相机管理器类。

(2) getSupportedCameras()getSupportedOutputCapability()方法获取支持的camera设备以及设备能力集。

(3) createPreviewOutput()createPhotoOutput()方法创建预览输出和拍照输出对象。

(4) CameraInput的open()方法打开相机输入。

(5) onCameraStatusChange()函数创建CameraManager注册回调。

(6) 最后调用sessionFlowFn()函数创建并开启Session。具体代码如下所示:

收起
自动换行
深色代码主题
复制
  1. /**
  2. * Initialize Camera Functions
  3. * @param surfaceId - Surface ID
  4. * @param cameraDeviceIndex - Camera Device Index
  5. * @returns No return value
  6. */
  7. async initCamera(surfaceId: string, cameraDeviceIndex: number): Promise<void> {
  8. Logger.debug(TAG, `initCamera cameraDeviceIndex: ${cameraDeviceIndex}`);
  9. this.photoMode = AppStorage.get('photoMode');
  10. if (!this.photoMode) {
  11. return;
  12. }
  13. try {
  14. await this.releaseCamera();
  15. // Get Camera Manager Instance
  16. this.cameraManager = this.getCameraManagerFn();
  17. if (this.cameraManager === undefined) {
  18. Logger.error(TAG, 'cameraManager is undefined');
  19. return;
  20. }
  21. // Gets the camera device object that supports the specified
  22. this.cameras = this.getSupportedCamerasFn(this.cameraManager);
  23. if (this.cameras.length < 1 || this.cameras.length < cameraDeviceIndex + 1) {
  24. return;
  25. }
  26. this.curCameraDevice = this.cameras[cameraDeviceIndex];
  27. let isSupported = this.isSupportedSceneMode(this.cameraManager, this.curCameraDevice);
  28. if (!isSupported) {
  29. Logger.error(TAG, 'The current scene mode is not supported.');
  30. return;
  31. }
  32. let cameraOutputCapability =
  33. this.cameraManager.getSupportedOutputCapability(this.curCameraDevice, this.curSceneMode);
  34. let previewProfile = this.getPreviewProfile(cameraOutputCapability);
  35. if (previewProfile === undefined) {
  36. Logger.error(TAG, 'The resolution of the current preview stream is not supported.');
  37. return;
  38. }
  39. this.previewProfileObj = previewProfile;
  40. // Creates the previewOutput output object
  41. this.previewOutput = this.createPreviewOutputFn(this.cameraManager, this.previewProfileObj, surfaceId);
  42. if (this.previewOutput === undefined) {
  43. Logger.error(TAG, 'Failed to create the preview stream.');
  44. return;
  45. }
  46. // Listening for preview events
  47. this.previewOutputCallBack(this.previewOutput);
  48. let photoProfile = this.getPhotoProfile(cameraOutputCapability);
  49. if (photoProfile === undefined) {
  50. Logger.error(TAG, 'The resolution of the current photo stream is not supported.');
  51. return;
  52. }
  53. this.photoProfileObj = photoProfile;
  54. // Creates a photoOutPut output object
  55. this.photoOutput = this.createPhotoOutputFn(this.cameraManager, this.photoProfileObj);
  56. if (this.photoOutput === undefined) {
  57. Logger.error(TAG, 'Failed to create the photo stream.');
  58. return;
  59. }
  60. // Creates a cameraInput output object
  61. this.cameraInput = this.createCameraInputFn(this.cameraManager, this.curCameraDevice);
  62. if (this.cameraInput === undefined) {
  63. Logger.error(TAG, 'Failed to create the camera input.');
  64. return;
  65. }
  66. // Turn on the camera
  67. let isOpenSuccess = await this.cameraInputOpenFn(this.cameraInput);
  68. if (!isOpenSuccess) {
  69. Logger.error(TAG, 'Failed to open the camera.');
  70. return;
  71. }
  72. // Camera status callback
  73. this.onCameraStatusChange(this.cameraManager);
  74. // Listens to CameraInput error events
  75. this.onCameraInputChange(this.cameraInput, this.curCameraDevice);
  76. // Session Process
  77. await this.sessionFlowFn(this.cameraManager, this.cameraInput, this.previewOutput, this.photoOutput);
  78. } catch (error) {
  79. let err = error as BusinessError;
  80. Logger.error(TAG, `initCamera fail: ${JSON.stringify(err)}`);
  81. }
  82. }

3.确定拍照输出流。通过cameraManager.createPhotoOutput()方法创建拍照输出流,参数为CameraOutputCapability类中的photoProfiles属性。

收起
自动换行
深色代码主题
复制
  1. /**
  2. * Creates a photoOutPut output object
  3. */
  4. createPhotoOutputFn(cameraManager: camera.CameraManager,
  5. photoProfileObj: camera.Profile): camera.PhotoOutput | undefined {
  6. let photoOutput: camera.PhotoOutput;
  7. try {
  8. photoOutput = cameraManager.createPhotoOutput(photoProfileObj);
  9. Logger.info(TAG, `createPhotoOutputFn success: ${photoOutput}`);
  10. return photoOutput;
  11. } catch (error) {
  12. let err = error as BusinessError;
  13. Logger.error(TAG, `createPhotoOutputFn failed: ${JSON.stringify(err)}`);
  14. return undefined;
  15. }
  16. }

4.触发拍照。通过photoOutput类的capture()方法,执行拍照任务。该方法有两个参数,分别为拍照设置参数的setting以及回调函数,setting中可以设置照片的质量和旋转角度。具体代码如下所示:

收起
自动换行
深色代码主题
复制
  1. /**
  2. * Trigger a photo taking based on the specified parameters
  3. */
  4. async takePicture(): Promise<void> {
  5. Logger.info(TAG, 'takePicture start');
  6. let cameraDeviceIndex = GlobalContext.get().getT<number>('cameraDeviceIndex');
  7. let photoSettings: camera.PhotoCaptureSetting = {
  8. quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
  9. mirror: cameraDeviceIndex ? true : false
  10. };
  11. try {
  12. await this.photoOutput?.capture(photoSettings);
  13. Logger.info(TAG, 'takePicture end');
  14. } catch (error) {
  15. let err = error as BusinessError;
  16. Logger.warn('testTag', `capture failed, code=${err.code}, message=${err.message}`);
  17. }
  18. }

5.设置拍照photoAvailable()的回调来获取Photo对象,点击拍照按钮,触发此回调函数,调用getComponent()方法根据图像的组件类型从图像中获取组件缓存ArrayBuffer,使用createImageSource()方法来创建图片源实例,最后通过createPixelMap()获取PixelMap对象。注意:如果已经注册了photoAssetAvailable()回调,并且在Session开始之后又注册了photoAvailable()回调,会导致流被重启。不建议开发者同时注册photoAvailable()和photoAssetAvailable()。

收起
自动换行
深色代码主题
复制
  1. photoOutput.on('photoAvailable', (err: BusinessError, photo: camera.Photo) => {
  2. Logger.info(TAG, 'photoAvailable begin');
  3. if (err) {
  4. Logger.error(TAG, `photoAvailable err:${err.code}`);
  5. return;
  6. }
  7. let imageObj: image.Image = photo.main;
  8. imageObj.getComponent(image.ComponentType.JPEG, (err: BusinessError, component: image.Component) => {
  9. Logger.info(TAG, `getComponent start`);
  10. if (err) {
  11. Logger.error(TAG, `getComponent err:${err.code}`);
  12. return;
  13. }
  14. let buffer: ArrayBuffer = component.byteBuffer;
  15. let imageSource: image.ImageSource = image.createImageSource(buffer);
  16. imageSource.createPixelMap((err: BusinessError, pixelMap: image.PixelMap) => {
  17. if (err) {
  18. Logger.error(TAG, `createPixelMap err:${err.code}`);
  19. return;
  20. }
  21. this.handlePhotoAssetCb(pixelMap);
  22. });
  23. });
  24. })

以上代码中执行handleImageInfo()函数来对PixelMap进行全局存储并跳转到预览页面。具体代码如下所示:

收起
自动换行
深色代码主题
复制
  1. handleSavePicture = (photoAsset: photoAccessHelper.PhotoAsset | image.PixelMap): void => {
  2. Logger.info(TAG, 'handleSavePicture');
  3. this.setImageInfo(photoAsset);
  4. AppStorage.set<boolean>('isOpenEditPage', true);
  5. Logger.info(TAG, 'setImageInfo end');
  6. }
  7. setImageInfo(photoAsset: photoAccessHelper.PhotoAsset | image.PixelMap): void {
  8. Logger.info(TAG, 'setImageInfo');
  9. GlobalContext.get().setObject('photoAsset', photoAsset);
  10. }

6.进入到预览界面,通过GlobalContext.get().getT<image.PixelMap>('imageInfo')方法获取PixelMap信息,并通过Image组件进行渲染显示。

分段式拍照:

分段式拍照是应用下发拍照任务后,系统将分多阶段上报不同质量的图片。在一阶段,系统快速上报低质量图,应用通过on(type: 'photoAssetAvailable', callback: AsyncCallback<photoAccessHelper.PhotoAsset>): void接口会收到一个PhotoAsset对象,通过该对象可调用媒体库接口,读取图片或落盘图片。在二阶段,分段式子服务会根据系统压力以及定制化场景进行调度,将后处理好的原图回传给媒体库,替换低质量图。具体操作步骤如下所示:

由于分段式拍照和单段式拍照步骤1-步骤4相同,就不再进行赘述。

5.设置拍照photoAssetAvailable()的回调来获取photoAsset,点击拍照按钮,触发此回调函数,然后执行handlePhotoAssetCb()函数来完成photoAsset全局的存储并跳转到预览页面。

收起
自动换行
深色代码主题
复制
  1. photoOutput.on('photoAssetAvailable', (err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset) => {
  2. Logger.info(TAG, 'photoAssetAvailable begin');
  3. if (err) {
  4. Logger.error(TAG, `photoAssetAvailable err:${err.code}`);
  5. return;
  6. }
  7. this.handlePhotoAssetCb(photoAsset);
  8. });

6.进入预览界面通过GlobalContext.get().getT<image.PixelMap>('imageInfo')方法获取PhotoAsset信息,执行requestImage函数中的photoAccessHelper.MediaAssetManager.requestImageData()方法根据不同的策略模式,请求图片资源数据,这里的请求策略为均衡模式BALANCE_MODE, 最后分段式子服务会根据系统压力以及定制化场景进行调度,将后处理好的原图回传给媒体库来替换低质量图。具体代码如下所示:

收起
自动换行
深色代码主题
复制
  1. photoBufferCallback: (arrayBuffer: ArrayBuffer) => void = (arrayBuffer: ArrayBuffer) => {
  2. Logger.info(TAG, 'photoBufferCallback is called');
  3. let imageSource = image.createImageSource(arrayBuffer);
  4. imageSource.createPixelMap((err: BusinessError, data: image.PixelMap) => {
  5. if (err) {
  6. Logger.info(TAG, `createPixelMap err:${err.code}`);
  7. return;
  8. }
  9. Logger.info(TAG, 'createPixelMap is called');
  10. this.curPixelMap = data;
  11. });
  12. };
  13. requestImage(requestImageParams: RequestImageParams): void {
  14. class MediaDataHandler implements photoAccessHelper.MediaAssetDataHandler<ArrayBuffer> {
  15. onDataPrepared(data: ArrayBuffer, map: Map<string, string>): void {
  16. Logger.info(TAG, 'onDataPrepared map' + JSON.stringify(map));
  17. requestImageParams.callback(data);
  18. Logger.info(TAG, 'onDataPrepared end');
  19. }
  20. };
  21. let requestOptions: photoAccessHelper.RequestOptions = {
  22. deliveryMode: photoAccessHelper.DeliveryMode.BALANCE_MODE,
  23. };
  24. const handler = new MediaDataHandler();
  25. photoAccessHelper.MediaAssetManager.requestImageData(requestImageParams.context, requestImageParams.photoAsset,
  26. requestOptions, handler)
  27. .then(() => {
  28. Logger.info(TAG, 'requestImageData success');
  29. })
  30. .catch((err: BusinessError) => {
  31. Logger.error(TAG, `requestImageData failed, code=${err.code}, message=${err.message}`)
  32. })
  33. }
  34. aboutToAppear() {
  35. Logger.info(TAG, 'aboutToAppear begin');
  36. if (this.photoMode === Constants.SUBSECTION_MODE) {
  37. let curPhotoAsset = GlobalContext.get().getT<photoAccessHelper.PhotoAsset>('photoAsset');
  38. this.photoUri = curPhotoAsset.uri;
  39. let requestImageParams: RequestImageParams = {
  40. context: this.getUIContext().getHostContext(),
  41. photoAsset: curPhotoAsset,
  42. callback: this.photoBufferCallback
  43. };
  44. this.requestImage(requestImageParams);
  45. Logger.info(TAG, `aboutToAppear photoUri: ${this.photoUri}`);
  46. } else if (this.photoMode === Constants.SINGLE_STAGE_MODE) {
  47. this.curPixelMap = GlobalContext.get().getT<image.PixelMap>('photoAsset');
  48. }
  49. }

7.将步骤6获取的PixelMap对象数据通过Image组件进行渲染显示。

总结

通过分段式拍照,确保低质量图可接受的基础上,加快了Shot2See的完成时延,同时第二段保证了高质量照片不损失图片效果,达到与系统相机一致的拍照质量。

在 最佳实践 中进行搜索
请输入您想要搜索的关键词