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 16 17## 效果展示 18 19如下效果图所示,单段式拍照从点击拍照控件到在缩略图显示区域显示缩略图的耗时比分段式拍照的时间长。 20 21| 单段式拍照效果图 | 分段式拍照效果图| 22|-------------------------------------------------------------------------------|---------------------------------------------------------------------------| 23|  |  | 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 34 35分段式拍照耗时数据如下图所示: 36 37 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)