文档管理中心
我的
您当前正在浏览新版开发者文档中心,目录分类和层级有所调整。点击左侧当前文档分类名称前的“☰”图标,可切换文档分类。 了解新版目录
指南媒体Camera Kit(相机服务)开发相机应用基础能力(ArkTS)相机预览花屏解决方案

相机预览花屏解决方案

概述

开发者在使用相机服务时,如果仅用于预览流展示,通常使用XComponent组件实现,如果需要获取每帧图像做二次处理(例如获取每帧图像完成二维码识别或人脸识别场景),可以通过ImageReceiver中imageArrival事件监听预览流每帧数据,解析图像内容。在解析图像内容时,如果未考虑stride,直接通过使用width*height读取图像内容去解析图像,会导致相机预览异常,从而出现相机预览花屏的现象。

当开发者获取预览流每帧图像buffer后,若发现图片内容出现花屏堆叠状,出现相机预览花屏现象,此时需要排查解析每帧图像,当预览流图像stride与width不一致时,需要对stride进行无效像素的去除处理。

实现原理

在计算机图形学和图像处理中,stride通常指的是在内存中存储多维数组(如图像或纹理)时,行与行之间的字节间隔,即每一行的起始地址与下一行的起始地址之间的距离,在本文中stride指的是图像的一行数据在内存中实际占用的字节数,出于内存对齐和提高读取效率的考虑,通常大于图像的宽度。

说明

stride在不同的平台底层上报的值不同,开发者需根据实际业务获取stride后做处理适配。在本文中通过预览流帧数据的返回值image.Component.rowStride获取stride。

如下图:在一个width为3,height为3,stride为4的图片上(例如定义了一个480*480分辨率的图像),实际分配内存并不是width*height即3*3(此处为定义的预览流分辨率的宽高比,即实际分配内存不是480*480),而是stride*height即4*3,这样实现了内存对齐,方便硬件处理。

图 1 需正确处理stride

如果开发者根据width和height数据去处理像素数据,即把0x00-0x09地址的数据当做像素去处理,就会出现解析了错误的像素数据的问题,并且使用了无效的像素0x03,0x07,会导致图片无法正常显示导致“相机花屏”现象。因此,要根据stride值处理预览数据流,去除无效的像素后送显,才能获取正确的预览流图像。

场景案例

以一种高频的用户使用场景为例,应用需要定义一个1080*1080分辨率的预览流图像,此时的stride在相关平台的返回值为1088,此时需要对stride进行处理,处理无效像素后解析出正确的像素数据,避免出现预览流花屏。

【反例】未处理stride:当开发者创建PixelMap解析buffer时,直接按照宽去读取每行数据,没有处理stride,此时若解析了无效像素数据并传给Image组件直接送显,可能会出现预览流花屏现象。

以下为部分示例代码:

  1. 应用通过image.ImageReceiver注册imageArrival图像回调方法,获取每帧图像数据实例image.Image,应用通过定义一个width为1080*height为1080分辨率的预览流直接创建pixelMap,此时获取到的stride的值为1088,解析buffer时若直接按照宽去读取每行数据(使用了无效像素数据)并存储到全局变量stridePixel中,传给Image送显,会导致预览流花屏。

    收起
    自动换行
    深色代码主题
    复制
    1. onImageArrival(receiver: image.ImageReceiver): void {
    2. receiver.on('imageArrival', () => {
    3. receiver.readNextImage((err: BusinessError, nextImage: image.Image) => {
    4. if (err || nextImage === undefined) {
    5. Logger.error(TAG, `requestPermissionsFromUser call Failed! error: ${err.code}`);
    6. return;
    7. }
    8. if (nextImage) {
    9. nextImage.getComponent(image.ComponentType.JPEG, async (_err, component: image.Component) => {
    10. let width: number = 1080;
    11. let height: number = 1080;
    12. let pixelMap: image.PixelMap | undefined = await image.createPixelMap(component.byteBuffer, {
    13. size: {
    14. height: height,
    15. width: width
    16. },
    17. srcPixelFormat: image.PixelMapFormat.NV21
    18. })
    19. AppStorage.setOrCreate('stridePixel', pixelMap);
    20. nextImage.release();
    21. })
    22. }
    23. });
    24. })
    25. }
  2. 在初始相机模块时,调用onImageArrival(),将未处理的width和height作为size,创建PixelMap,通过在Image中传入被@StorageLink修饰的变量stridePixel进行数据刷新,图片送显。

    收起
    自动换行
    深色代码主题
    复制
    1. @Component
    2. export struct PageThree {
    3. pathStack: NavPathStack = new NavPathStack();
    4. @State name: string = 'pageOne';
    5. @State isShowStridePixel: boolean = false;
    6. @StorageLink('stridePixel') @Watch('onStridePixel') stridePixel: image.PixelMap | undefined = undefined;
    7. @State imageWidth: number = 1080;
    8. @State imageHeight: number = 1080;
    9. @StorageLink('previewRotation') previewRotate: number = 0;
    10. onStridePixel(): void {
    11. this.isShowStridePixel = true;
    12. }
    13. aboutToAppear(): void {
    14. CameraService.initCamera(0, this.getUIContext());
    15. }
    16. aboutToDisappear(): void {
    17. CameraService.releaseCamera();
    18. }
    19. // ...
    20. build() {
    21. NavDestination() {
    22. // ...
    23. Column() {
    24. if (this.isShowStridePixel) {
    25. Image(this.stridePixel)
    26. .width(this.getUIContext().px2vp(this.imageWidth))
    27. .height(this.getUIContext().px2vp(this.imageHeight))
    28. .margin({ top: 150 })
    29. .rotate({
    30. z: 0.5,
    31. angle: this.previewRotate
    32. })
    33. }
    34. // ...
    35. }
    36. .justifyContent(FlexAlign.Center)
    37. .height('90%')
    38. .width('100%')
    39. }
    40. .backgroundColor(Color.White)
    41. .hideTitleBar(true)
    42. .onBackPressed(() => {
    43. this.pathStack.pop();
    44. return true;
    45. })
    46. .onReady((context: NavDestinationContext) => {
    47. this.pathStack = context.pathStack;
    48. })
    49. }
    50. }

【正例一】开发者使用width,height,stride三个值,处理相机预览流数据,处理stride方法一如下。

分两种情况:

  1. 当stride和width相等时,按宽读取buffer不影响结果。

  2. 当stride和width不等时,将相机返回的预览流数据即component.byteBuffer的数据去除stride,拷贝得到新的dstArr数据进行数据处理,将处理后的dstArr数组buffer,通过width和height直接创建pixelMap, 并存储到全局变量stridePixel中,传给Image送显。

    收起
    自动换行
    深色代码主题
    复制
    1. onImageArrival(receiver: image.ImageReceiver): void {
    2. // ...
    3. if (nextImage) {
    4. nextImage.getComponent(image.ComponentType.JPEG,
    5. async (err, component: image.Component) => {
    6. let width: number = 1080;
    7. let height: number = 1080;
    8. let stride: number = component.rowStride;
    9. Logger.info(TAG, `receiver getComponent width:${width} height:${height} stride:${stride}`);
    10. if (stride === width) {
    11. let pixelMap: image.PixelMap | undefined = await image.createPixelMap(component.byteBuffer, {
    12. size: { height: height, width: width },
    13. srcPixelFormat: image.PixelMapFormat.NV21,
    14. })
    15. AppStorage.setOrCreate('stridePixel', pixelMap);
    16. } else {
    17. const dstBufferSize: number = width * height *
    18. 1.5;
    19. const dstArr: Uint8Array = new Uint8Array(dstBufferSize);
    20. for (let j = 0; j < height * 1.5; j++) {
    21. const srcBuf: Uint8Array = new Uint8Array(component.byteBuffer, j * stride,
    22. width);
    23. dstArr.set(srcBuf, j * width);
    24. }
    25. let pixelMap: image.PixelMap | undefined = await image.createPixelMap(dstArr.buffer, {
    26. size: { height: height, width: width },
    27. srcPixelFormat: image.PixelMapFormat.NV21,
    28. })
    29. AppStorage.setOrCreate('stridePixel', pixelMap);
    30. }
    31. nextImage.release();
    32. })
    33. }
    34. });
    35. })
    36. }

【正例二】开发者使用width,height,stride三个值,处理相机预览流数据,处理stride方法二如下。

分两种情况:

  1. 当stride和width相等时,与正例一情况一致,此处不再赘述。

  2. 当stride和width不等时,如果应用想使用byteBuffer预览流数据创建pixelMap直接显示,可以根据stride*height字节的大小先创建pixelMap,然后调用PixelMap的cropSync()方法裁剪掉多余的像素,从而正确处理stride,解决预览流花屏问题。

    收起
    自动换行
    深色代码主题
    复制
    1. onImageArrival(receiver: image.ImageReceiver): void {
    2. receiver.on('imageArrival', () => {
    3. receiver.readNextImage((err: BusinessError, nextImage: image.Image) => {
    4. // ...
    5. if (nextImage) {
    6. nextImage.getComponent(image.ComponentType.JPEG, async (_err, component: image.Component) => {
    7. let width: number = 1080;
    8. let height: number = 1080;
    9. let stride: number = component.rowStride;
    10. Logger.info(TAG, `receiver getComponent width:${width} height:${height} stride:${stride}`);
    11. if (stride === width) {
    12. let pixelMap: image.PixelMap | undefined = await image.createPixelMap(component.byteBuffer, {
    13. size: { height: height, width: width },
    14. srcPixelFormat: image.PixelMapFormat.NV21,
    15. })
    16. AppStorage.setOrCreate('stridePixel', pixelMap);
    17. } else {
    18. let pixelMap: image.PixelMap | undefined = await image.createPixelMap(component.byteBuffer, {
    19. size: { height: height, width: stride },
    20. srcPixelFormat: 8,
    21. })
    22. try {
    23. pixelMap.cropSync({
    24. size: { width: width, height: height },
    25. x: 0,
    26. y: 0
    27. })
    28. let pixelBefore: PixelMap | undefined = AppStorage.get('stridePixel');
    29. await pixelBefore?.release();
    30. AppStorage.setOrCreate('stridePixel', pixelMap);
    31. } catch (error) {
    32. let err: BusinessError = error as BusinessError;
    33. hilog.warn(0x000, 'testTag', `setColorMode failed, code=${err.code}, message=${err.message}`);
    34. }
    35. }
    36. nextImage.release();
    37. })
    38. }
    39. });
    40. })
    41. }

效果对比

表 1

展开
(反例)未处理 stride (正例)处理 stride 的方案一 (正例)处理 stride 的方案二

常见问题

如何获取相机预览流帧数据

通过ImageReceiver中imageArrival事件监听获取底层返回的图像数据,详细请参见双路预览(ArkTS)。

如何获取预览流图像的stride的值

可以通过预览流帧数据的返回值image.Component.rowStride获取stride。