• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# 相机分段式拍照性能提升实践
2
3## 概述
4
5相机拍照性能依赖算法处理的速度,而处理效果依赖算法的复杂度,算法复杂度越高的情况下会导致处理时间就越长。目前系统相机开发有两种相机拍照方案,分别是[相机分段式拍照](../media/camera/camera-deferred-capture-case.md)和相机单段式拍照:
6
7- 分段式拍照是系统相机开发的重要功能之一,即相机拍照可输出低质量图用作缩略图,提升用户感知拍照速度,同时使用高质量图保证最后的成图质量达到系统相机的水平,构筑相机性能竞争力。这样可以优化系统的拍照响应时延,从而提升用户的体验。
8- 单段式拍照是在拍照过程中通过多帧融合以及多个底层算法仅会返回一张高质量图片,这样导致Shot2See(Shot2See指的是从用户点击拍照控件到在缩略图显示区域显示缩略图)完成时延比较长。
9
10分段式拍照和单段式拍照返回的图片在全质量图的情况下图片质量是一致的,但是在低质量的情况下单段式拍照的图片质量要优于分段式拍照。如果开发者考虑Shot2See的完成时延以及获取全质量图,建议使用分段式拍照,否则的话,建议使用单段式拍照。
11本篇文章主要以相机Shot2See场景为例,来展示分段式拍照Shot2See的完成时延要低于单段式拍照。
12
13**分段式拍照流程示意图**
14
15![camera-subsection-mode-image](figures/camera-subsection-mode-image.png)
16
17## 效果展示
18
19如下效果图所示,单段式拍照从点击拍照控件到在缩略图显示区域显示缩略图的耗时比分段式拍照的时间长。
20
21| 单段式拍照效果图 | 分段式拍照效果图|
22|-------------------------------------------------------------------------------|---------------------------------------------------------------------------|
23| ![camera-single-stage-mode-video](figures/camera-single-stage-mode-video.gif) | ![camera-subsection-mode-video](figures/camera-subsection-mode-video.gif) |
24
25## 性能对比分析方式
26
27代码静态校验:在相机类应用中,如果使用单段式拍照,拍照过程中该场景下仅会返回一张图片,将图片用作Shot2See后的缩略图则会导致Shot2See完成时延比较长。
28
29动态校验:开发者可以通过DevEco Studio中的Profiler工具去抓取Trace,获取到Trace之后,根据PhotoOutputNapi::Capture和OnBufferAvailable找到对应的Trace Marker,通过两者之间的时间段来分析耗时,通过Trace可以查看,单段式拍照的时长超过1s,而分段式拍照的时长为743.1ms。
30
31单段式拍照性能数据如下图所示:
32
33![](../performance/figures/camera-single-stage-mode-performance.png)
34
35分段式拍照耗时数据如下图所示:
36
37![](../performance/figures/camera-subsection-mode-performance.png)
38
39性能对比分析表:
40
41| **拍照实现方式** | **耗时(局限不同设备和场景,数据仅供参考)**  |
42|------------| ------- |
43| 单段式拍照      | 2.1s |
44| 分段式拍照      | 741.3ms |
45
46优化思路:在需要加快Shot2See完成时延的场景下,使用相机框架开发的分段式拍照方案,加快第一段照片生成的速度。
47
48## 场景示例
49
50下面以应用中相机Shot2See场景为例,通过单段式拍照和分段式拍照的性能功耗对比,来展示两者的性能差异。
51
52### 单段式拍照
53
54单段式拍照使用了`on(type:'photoAvailable',callback:AsyncCallback<Photo>):void`接口注册了全质量图的监听,默认不使能分段式拍照。具体操作步骤如下所示:
55
561. 相机媒体数据写入[XComponent组件](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)中,用来显示图像效果。具体代码如下所示:
57
58   ```typescript
59   XComponent({
60       id: 'componentId',
61       type: XComponentType.SURFACE,
62       controller: this.mXComponentController
63   })
64    .onLoad(async () => {
65      Logger.info(TAG, 'onLoad is called');
66      this.surfaceId = this.mXComponentController.getXComponentSurfaceId();
67      GlobalContext.get().setObject('cameraDeviceIndex', this.defaultCameraDeviceIndex);
68      GlobalContext.get().setObject('xComponentSurfaceId', this.surfaceId);
69      let surfaceRect: SurfaceRect = {
70          surfaceWidth: Constants.X_COMPONENT_SURFACE_HEIGHT, surfaceHeight: Constants.X_COMPONENT_SURFACE_WIDTH
71      };
72      this.mXComponentController.setXComponentSurfaceRect(surfaceRect);
73      Logger.info(TAG, `onLoad surfaceId: ${this.surfaceId}`);
74      await CameraService.initCamera(this.surfaceId, this.defaultCameraDeviceIndex);
75    })
76   ```
77
782. initCamera函数完成一个相机生命周期初始化的过程。
79
80- 首先通过[getCameraManager](../reference/apis-camera-kit/arkts-apis-camera-f.md#cameragetcameramanager)来获取CameraMananger相机管理器类。
81- 调用[getSupportedCameras](../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#getsupportedcameras)和[getSupportedOutputCapability](../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#getsupportedoutputcapability11)方法来获取支持的camera设备以及设备能力集。
82- 调用[createPreviewOutput](../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#createpreviewoutput)和[createPhotoOutput](../reference/apis-camera-kit/arkts-apis-camera-CameraManager.md#createphotooutput11)方法来创建预览输出和拍照输出对象。
83- 使用CameraInput的open方法来打开相机输入,通过onCameraStatusChange函数来创建CameraManager注册回调。
84- 最后调用sessionFlowFn函数创建并开启Session。具体代码如下所示:
85
86   ```typescript
87   async initCamera(surfaceId: string, cameraDeviceIndex: number): Promise<void> {
88     Logger.info(TAG, `initCamera cameraDeviceIndex: ${cameraDeviceIndex}`);
89     this.photoMode = AppStorage.get('photoMode')
90     if (!this.photoMode) {
91       return;
92     }
93     try {
94       await this.releaseCamera();
95       // 获取相机管理器实例
96       this.cameraManager = this.getCameraManagerFn();
97       if (this.cameraManager === undefined) {
98         Logger.error(TAG, 'cameraManager is undefined');
99         return;
100       }
101       // 获取支持指定的相机设备对象
102       this.cameras = this.getSupportedCamerasFn(this.cameraManager);
103       if (this.cameras.length < 1 || this.cameras.length < cameraDeviceIndex + 1) {
104         return;
105       }
106       this.curCameraDevice = this.cameras[cameraDeviceIndex];
107       let isSupported = this.isSupportedSceneMode(this.cameraManager, this.curCameraDevice);
108       if (!isSupported) {
109         Logger.error(TAG, 'The current scene mode is not supported.');
110         return;
111       }
112       let cameraOutputCapability =
113         this.cameraManager.getSupportedOutputCapability(this.curCameraDevice, this.curSceneMode);
114       let previewProfile = this.getPreviewProfile(cameraOutputCapability);
115       if (previewProfile === undefined) {
116         Logger.error(TAG, 'The resolution of the current preview stream is not supported.');
117         return;
118       }
119       this.previewProfileObj = previewProfile;
120       // 创建previewOutput输出对象
121       this.previewOutput = this.createPreviewOutputFn(this.cameraManager, this.previewProfileObj, surfaceId);
122       if (this.previewOutput === undefined) {
123         Logger.error(TAG, 'Failed to create the preview stream.');
124         return;
125       }
126       // 监听预览事件
127       this.previewOutputCallBack(this.previewOutput);
128       let photoProfile = this.getPhotoProfile(cameraOutputCapability);
129       if (photoProfile === undefined) {
130         Logger.error(TAG, 'The resolution of the current photo stream is not supported.');
131         return;
132       }
133       this.photoProfileObj = photoProfile;
134       // 创建photoOutPut输出对象
135       this.photoOutput = this.createPhotoOutputFn(this.cameraManager, this.photoProfileObj);
136       if (this.photoOutput === undefined) {
137         Logger.error(TAG, 'Failed to create the photo stream.');
138         return;
139       }
140       // 创建cameraInput输出对象
141       this.cameraInput = this.createCameraInputFn(this.cameraManager, this.curCameraDevice);
142       if (this.cameraInput === undefined) {
143         Logger.error(TAG, 'Failed to create the camera input.');
144         return;
145       }
146       // 打开相机
147       let isOpenSuccess = await this.cameraInputOpenFn(this.cameraInput);
148       if (!isOpenSuccess) {
149         Logger.error(TAG, 'Failed to open the camera.');
150         return;
151       }
152       // 镜头状态回调
153       this.onCameraStatusChange(this.cameraManager);
154       // 监听CameraInput的错误事件
155       this.onCameraInputChange(this.cameraInput, this.curCameraDevice);
156       // 会话流程
157       await this.sessionFlowFn(this.cameraManager, this.cameraInput, this.previewOutput, this.photoOutput);
158     } catch (error) {
159       let err = error as BusinessError;
160       Logger.error(TAG, `initCamera fail: ${err.code}`);
161     }
162   }
163   ```
164
1653. 确定拍照输出流。通过cameraManager.createPhotoOutput方法创建拍照输出流,参数为[CameraOutputCapability](../reference/apis-camera-kit/arkts-apis-camera-i.md#cameraoutputcapability)类中的photoProfiles属性。
166
167   ```typescript
168   createPhotoOutputFn(cameraManager: camera.CameraManager,
169     photoProfileObj: camera.Profile): camera.PhotoOutput | undefined {
170     let photoOutput: camera.PhotoOutput | undefined = undefined;
171     try {
172       photoOutput = cameraManager.createPhotoOutput(photoProfileObj);
173       Logger.info(TAG, `createPhotoOutputFn success: ${photoOutput}`);
174     } catch (error) {
175       let err = error as BusinessError;
176       Logger.error(TAG, `createPhotoOutputFn failed: ${err.code}`);
177     }
178     return photoOutput;
179   }
180   ```
181
1824. 触发拍照。通过photoOutput类的[capture](../reference/apis-camera-kit/arkts-apis-camera-PhotoOutput.md#capture)方法,执行拍照任务。该方法有两个参数,第一个参数为拍照设置参数的setting,setting中可以设置照片的质量和旋转角度,第二参数为回调函数。具体代码如下所示:
183
184   ```typescript
185   async takePicture(): Promise<void> {
186     Logger.info(TAG, 'takePicture start');
187     let cameraDeviceIndex = GlobalContext.get().getT<number>('cameraDeviceIndex');
188     let photoSettings: camera.PhotoCaptureSetting = {
189       quality: camera.QualityLevel.QUALITY_LEVEL_HIGH,
190       mirror: cameraDeviceIndex ? true : false
191     };
192     await this.photoOutput?.capture(photoSettings);
193     Logger.info(TAG, 'takePicture end');
194   }
195   ```
196
1975. 设置拍照[photoAvailable](../reference/apis-camera-kit/arkts-apis-camera-PhotoOutput.md#onphotoavailable11)的回调来获取Photo对象,点击拍照按钮,触发此回调函数,调用getComponent方法根据图像的组件类型从图像中获取组件缓存ArrayBuffer,使用createImageSource方法来创建图片源实例,最后通过createPixelMap获取PixelMap对象。注意:如果已经注册了photoAssetAvailable回调,并且在Session开始之后又注册了photoAvailable回调,会导致流被重启。不建议开发者同时注册photoAvailable和photoAssetAvailable。
198
199   ```typescript
200   photoOutput.on('photoAvailable', (err: BusinessError, photo: camera.Photo) => {
201     Logger.info(TAG, 'photoAvailable begin');
202     if (photo === undefined) {
203       Logger.error(TAG, 'photo is undefined');
204       return;
205     }
206     let imageObj: image.Image = photo.main;
207     imageObj.getComponent(image.ComponentType.JPEG, (err: BusinessError, component: image.Component) => {
208       Logger.info(TAG, `getComponent start`);
209       if (component === undefined) {
210         Logger.error(TAG, 'getComponent failed');
211         return;
212       }
213       let buffer: ArrayBuffer = component.byteBuffer;
214       let imageSource: image.ImageSource = image.createImageSource(buffer);
215       imageSource.createPixelMap((err: BusinessError, pixelMap: image.PixelMap) => {
216         if (!pixelMap) {
217           return;
218         }
219         this.handleImageInfo(pixelMap);
220       })
221     })
222   })
223   ```
224
225   以上代码中执行handleImageInfo函数来对PixelMap进行全局存储并跳转到预览页面。具体代码如下所示:
226
227   ```typescript
228   handleSavePicture = (imageInfo: photoAccessHelper.PhotoAsset | image.PixelMap): void => {
229     Logger.info(TAG, 'handleSavePicture');
230     this.setImageInfo(imageInfo);
231     AppStorage.set<boolean>('isOpenEditPage', true);
232     Logger.info(TAG, 'setImageInfo end');
233   }
234
235   setImageInfo(imageInfo: photoAccessHelper.PhotoAsset | image.PixelMap): void {
236     Logger.info(TAG, 'setImageInfo');
237     GlobalContext.get().setObject('imageInfo', imageInfo);
238   }
239   ```
240
2416. 进入到预览界面,通过GlobalContext.get().getT<image.PixelMap>('imageInfo')方法获取PixelMap信息,并通过Image组件进行渲染显示。
242
243   ```typescript
244   this.curPixelMap = GlobalContext.get().getT<image.PixelMap>('imageInfo');
245
246   Image(this.curPixelMap)
247     .objectFit(ImageFit.Contain)
248     .width(Constants.FULL_PERCENT)
249     .height(Constants.EIGHTY_PERCENT)
250   ```
251
252### 分段式拍照
253
254分段式拍照是应用下发拍照任务后,系统将分多阶段上报不同质量的图片。在第一阶段,系统快速上报低质量图,应用通过`on(type:'photoAssetAvailable',callback:AsyncCallback<PhotoAsset>):void`接口会收到一个PhotoAsset对象,通过该对象可调用媒体库接口,读取图片或落盘图片。在第二阶段,分段式子服务会根据系统压力以及定制化场景进行调度,将后处理好的原图回传给媒体库,替换低质量图。具体操作步骤如下所示:
255
256由于分段式拍照和单段式拍照[步骤1](#场景示例)-步骤4相同,就不再进行赘述。
257
2585. 设置拍照[photoAssetAvailable](../reference/apis-camera-kit/arkts-apis-camera-PhotoOutput.md#onphotoassetavailable12)的回调来获取photoAsset,点击拍照按钮,触发此回调函数,然后执行handlePhotoAssetCb函数来完成photoAsset全局的存储并跳转到预览页面。注意:如果已经注册了photoAssetAvailable回调,并且在Session开始之后又注册了photoAvailable回调,会导致流被重启。不建议开发者同时注册photoAvailable和photoAssetAvailable。
259
260   ```typescript
261   photoOutput.on('photoAssetAvailable', (err: BusinessError, photoAsset: photoAccessHelper.PhotoAsset) => {
262     Logger.info(TAG, 'photoAssetAvailable begin');
263     if (photoAsset === undefined) {
264       Logger.error(TAG, 'photoAsset is undefined');
265       return;
266     }
267     this.handlePhotoAssetCb(photoAsset);
268   });
269   ```
270
271   以上代码中执行handleImageInfo函数来对photoAsset进行全局存储并跳转到预览页面。具体代码如下所示:
272
273   ```typescript
274   handleSavePicture = (imageInfo: photoAccessHelper.PhotoAsset | image.PixelMap): void => {
275     Logger.info(TAG, 'handleSavePicture');
276     this.setImageInfo(imageInfo);
277     AppStorage.set<boolean>('isOpenEditPage', true);
278     Logger.info(TAG, 'setImageInfo end');
279   }
280
281   setImageInfo(imageInfo: photoAccessHelper.PhotoAsset | image.PixelMap): void {
282     Logger.info(TAG, 'setImageInfo');
283     GlobalContext.get().setObject('imageInfo', imageInfo);
284   }
285   ```
286
2876. 进入预览界面通过GlobalContext.get().getT<image.PixelMap>('imageInfo')方法获取PhotoAsset信息,执行requestImage函数中的photoAccessHelper.MediaAssetManager.requestImageData方法根据不同的策略模式,请求图片资源数据,这里的请求策略为均衡模式BALANCE_MODE,
288最后分段式子服务会根据系统压力以及定制化场景进行调度,将后处理好的原图回传给媒体库来替换低质量图。具体代码如下所示:
289
290   ```typescript
291   photoBufferCallback: (arrayBuffer: ArrayBuffer) => void = (arrayBuffer: ArrayBuffer) => {
292     Logger.info(TAG, 'photoBufferCallback is called');
293     let imageSource = image.createImageSource(arrayBuffer);
294     imageSource.createPixelMap((err: BusinessError, data: image.PixelMap) => {
295       Logger.info(TAG, 'createPixelMap is called');
296       this.curPixelMap = data;
297     });
298   };
299
300   requestImage(requestImageParams: RequestImageParams): void {
301     try {
302       class MediaDataHandler implements photoAccessHelper.MediaAssetDataHandler<ArrayBuffer> {
303         onDataPrepared(data: ArrayBuffer, map: Map<string, string>): void {
304           Logger.info(TAG, 'onDataPrepared begin');
305           Logger.info(TAG, `onDataPrepared quality: ${map['quality']}`);
306           requestImageParams.callback(data);
307           Logger.info(TAG, 'onDataPrepared end');
308         }
309       };
310       let requestOptions: photoAccessHelper.RequestOptions = {
311         deliveryMode: photoAccessHelper.DeliveryMode.BALANCE_MODE,
312       };
313       const handler = new MediaDataHandler();
314       photoAccessHelper.MediaAssetManager.requestImageData(requestImageParams.context, requestImageParams.photoAsset,
315         requestOptions, handler);
316     } catch (error) {
317       Logger.error(TAG, `Failed in requestImage, error code: ${error.code}`);
318     }
319   }
320
321   aboutToAppear() {
322     Logger.info(TAG, 'aboutToAppear begin');
323     if (this.photoMode === Constants.SUBSECTION_MODE) {
324       let curPhotoAsset = GlobalContext.get().getT<photoAccessHelper.PhotoAsset>('imageInfo');
325       this.photoUri = curPhotoAsset.uri;
326       let requestImageParams: RequestImageParams = {
327         context: this.getUIContext().getHostContext(),
328         photoAsset: curPhotoAsset,
329         callback: this.photoBufferCallback
330       };
331       this.requestImage(requestImageParams);
332       Logger.info(TAG, `aboutToAppear photoUri: ${this.photoUri}`);
333     } else if (this.photoMode === Constants.SINGLE_STAGE_MODE) {
334       this.curPixelMap = GlobalContext.get().getT<image.PixelMap>('imageInfo');
335     }
336   }
337   ```
338
3397. 将步骤6获取的PixelMap对象数据通过Image组件进行渲染显示。
340
341   ```typescript
342   Image(this.curPixelMap)
343     .objectFit(ImageFit.Contain)
344     .width(Constants.FULL_PERCENT)
345     .height(Constants.EIGHTY_PERCENT)
346   ```
347
348## 总结
349
350通过分段式拍照,确保低质量图可接受的基础上,加快了Shot2See的完成时延,同时第二段保证了高质量照片不损失图片效果,达到与系统相机一致的拍照质量。
351
352## 完整示例
353
354[相机分段式拍照源码](https://gitee.com/harmonyos-cases/cases/tree/master/test/performance/camera_shot2see)