如何使用TaskGroup快速实现图片灰度化
======================

#### 问题现象

在处理像素比较大的图片时，放在单个线程中耗时也很长。如何使用TaskGroup实现分片处理图片？比如图片灰度化。

#### 背景知识

*   [TaskGroup](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-taskpool#taskgroup10)表示任务组，一次执行一组任务，适用于执行一组有关联的任务。如果所有任务正常执行，异步执行完毕后返回所有任务结果的数组，数组中元素的顺序与[addTask](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-taskpool#addtask10-1)的顺序相同；如果任意任务失败，则会抛出对应异常。如果任务组中存在多个任务失败的情况，则会抛出第一个失败任务的异常。任务组可以多次执行，但执行后不能新增任务。
*   灰度化是将彩色图像转换为灰度图像的处理过程，其核心原理是通过统一像素点的R、G、B分量值实现色彩空间转换。在该转换过程中，就是将彩色图像转换为每个像素点R、G、B分量值相等的灰度图像。

#### 解决方案

将图片的像素数据分片，通过TaskGroup一次执行一组图片灰度化的任务，实现并行处理，达到快速灰度化，具体步骤如下：

1.  读取图片像素数据，分割为四份，添加到TaskGroup中。等待返回数据，并且把数据合并，重新编码为图片，代码示例如下：
    
    pixelMapGrayScale(pixelMap: PixelMap) {
     _ // 读取pixelMap的像素数据。_
      let width = pixelMap.getImageInfoSync().size.width;
      let height = pixelMap.getImageInfoSync().size.height;
      let readBuffer: ArrayBuffer = new ArrayBuffer(width \* height \* 4);
      pixelMap.readPixelsToBufferSync(readBuffer);
      _// 分成四段并发调度。_
      let number: number = 4 \* Math.ceil(width \* height / 4);
      let buffer1: ArrayBuffer = readBuffer.slice(0, number);
      let buffer2: ArrayBuffer = readBuffer.slice(number, number \* 2);
      let buffer3: ArrayBuffer = readBuffer.slice(number \* 2, number \* 3);
      let buffer4: ArrayBuffer = readBuffer.slice(number \* 3);
    
      let group: taskpool.TaskGroup = new taskpool.TaskGroup();
      group.addTask(imageProcessing, buffer1);
      group.addTask(imageProcessing, buffer2);
      group.addTask(imageProcessing, buffer3);
      group.addTask(imageProcessing, buffer4);
    
      taskpool.execute(group, taskpool.Priority.HIGH).then((res: Array<Object>) => {
        _// 结果数组汇总处理，重新编码为图片。_
        console.info('execute res success', res.length);
        let byteBufferTemp: Uint8Array = new Uint8Array(new ArrayBuffer(width \* height \* 4));
        let offset: number = 0;
        for (let index = 0; index < res.length; index++) {
          const element = res\[index\] as Uint8Array;
          byteBufferTemp.set(element, offset);
          offset += element.length;
        }
        let color: ArrayBuffer = byteBufferTemp.buffer as ArrayBuffer;
        let opts: image.InitializationOptions =
          {
            editable: true,
            pixelFormat: pixelMap.getImageInfoSync().pixelFormat,
            size: { height: height, width: width }
          };
        this.pixelMapTemp = image.createPixelMapSync(color, opts);
        console.info('execute group success');
      });
    }
    
2.  开启图片处理线程，将像素做灰度化，代码示例如下：
    
    function imageProcessing(dataSlice: ArrayBuffer): Uint8Array {
     _ // 图像灰度化处理操作_
      let byteBuffer: Uint32Array = new Uint32Array(dataSlice);
      let byteBufferTemp: Uint8Array = new Uint8Array(new ArrayBuffer(byteBuffer.length \* 4));
      for (let index = 0; index < byteBuffer.length; index++) {
        _// 通过移位方式获取RGBA各通道的值。_
        let rgb = byteBuffer\[index\];
        let red = (rgb >> 0) & 0x000000ff;
        let green = (rgb >> 8) & 0x000000ff;
        let blue = (rgb >> 16) & 0x000000ff;
        let alpha = (rgb >> 24) & 0x000000ff;
     _   // 加权平均算法，获取灰度值。_
        let gray = (0.299 \* red + 0.587 \* green + 0.114 \* blue);
        byteBufferTemp\[index \* 4\] = gray;
        byteBufferTemp\[index \* 4 + 1\] = gray;
        byteBufferTemp\[index \* 4 + 2\] = gray;
        byteBufferTemp\[index \* 4 + 3\] = alpha;
      }
      return byteBufferTemp;
    }
    

完整示例参考如下：

import { taskpool } from '@kit.ArkTS';
import { image } from '@kit.ImageKit';
import { common } from '@kit.AbilityKit';

@Concurrent
function imageProcessing(dataSlice: ArrayBuffer): Uint8Array {
  _// 图像灰度化处理操作_
  let byteBuffer: Uint32Array = new Uint32Array(dataSlice);
  let byteBufferTemp: Uint8Array = new Uint8Array(new ArrayBuffer(byteBuffer.length \* 4));
  for (let index = 0; index < byteBuffer.length; index++) {
  _  // 通过移位方式获取RGBA各通道的值。_
    let rgb = byteBuffer\[index\];
    let red = (rgb >> 0) & 0x000000ff;
    let green = (rgb >> 8) & 0x000000ff;
    let blue = (rgb >> 16) & 0x000000ff;
    let alpha = (rgb >> 24) & 0x000000ff;
    _// 加权平均算法，获取灰度值。_
    let gray = (0.299 \* red + 0.587 \* green + 0.114 \* blue);
    byteBufferTemp\[index \* 4\] = gray;
    byteBufferTemp\[index \* 4 + 1\] = gray;
    byteBufferTemp\[index \* 4 + 2\] = gray;
    byteBufferTemp\[index \* 4 + 3\] = alpha;
  }
  return byteBufferTemp;
}

@Entry
@Component
struct Index {
  @State pixelMap: image.PixelMap | undefined = undefined;
  @State pixelMapTemp: image.PixelMap | undefined = undefined;

 _ // 通过rawfile目录下的文件名称创建pixelMap。_
  createPixelMapFromFile(url: string) {
    let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
    const fileData: Uint8Array = context.resourceManager.getRawFileContentSync(url);
  _  // 创建一个可供编辑的图片。_
    const buffer = fileData.buffer.slice(fileData.byteOffset, fileData.byteLength + fileData.byteOffset);
    let imageSource = image.createImageSource(buffer);
    const options: image.DecodingOptions = {
      editable: true,
      desiredPixelFormat: image.PixelMapFormat.RGBA\_8888,
    };
    let pixelMap: PixelMap = imageSource.createPixelMapSync(options);
   _ // 资源释放_
    imageSource.release();
    return pixelMap;
  }

  pixelMapGrayScale(pixelMap: PixelMap) {
  _  // 读取pixelMap的像素数据。_
    let width = pixelMap.getImageInfoSync().size.width;
    let height = pixelMap.getImageInfoSync().size.height;
    let readBuffer: ArrayBuffer = new ArrayBuffer(width \* height \* 4);
    pixelMap.readPixelsToBufferSync(readBuffer);
    _// 分成四段并发调度。_
    let number: number = 4 \* Math.ceil(width \* height / 4);
    let buffer1: ArrayBuffer = readBuffer.slice(0, number);
    let buffer2: ArrayBuffer = readBuffer.slice(number, number \* 2);
    let buffer3: ArrayBuffer = readBuffer.slice(number \* 2, number \* 3);
    let buffer4: ArrayBuffer = readBuffer.slice(number \* 3);

    let group: taskpool.TaskGroup = new taskpool.TaskGroup();
    group.addTask(imageProcessing, buffer1);
    group.addTask(imageProcessing, buffer2);
    group.addTask(imageProcessing, buffer3);
    group.addTask(imageProcessing, buffer4);

    taskpool.execute(group, taskpool.Priority.HIGH).then((res: Array<Object>) => {
     _ // 结果数组汇总处理，重新编码为图片。_
      console.info('execute res success', res.length);
      let byteBufferTemp: Uint8Array = new Uint8Array(new ArrayBuffer(width \* height \* 4));
      let offset: number = 0;
      for (let index = 0; index < res.length; index++) {
        const element = res\[index\] as Uint8Array;
        byteBufferTemp.set(element, offset);
        offset += element.length;
      }
      let color: ArrayBuffer = byteBufferTemp.buffer as ArrayBuffer;
      let opts: image.InitializationOptions =
        {
          editable: true,
          pixelFormat: pixelMap.getImageInfoSync().pixelFormat,
          size: { height: height, width: width }
        };
      this.pixelMapTemp = image.createPixelMapSync(color, opts);
      console.info('execute group success');
    });
  }

  aboutToAppear(): void {
   _ // 通过rawfile目录下的文件名称创建pixelMap，‘testicon.png’是测试图片，开发者需要在rawfile目录替换实际图片_
    this.pixelMap = this.createPixelMapFromFile('testicon.png');
  }

  build() {
    Row() {
      Column() {
        Text('原图')
        Image(this.pixelMap)
          .objectFit(ImageFit.Contain)
          .width('30%')
          .height(120)
          .margin({
            top: 3,
            bottom: 3,
            left: 5,
            right: 5
          })
        Text('灰度图')
        Image(this.pixelMapTemp)
          .objectFit(ImageFit.Contain)
          .width('30%')
          .height(120)
          .margin({
            top: 3,
            bottom: 3,
            left: 5,
            right: 5
          })
        Button('加权平均值')
          .backgroundColor('#0D9FFB')
          .margin({
            top: 10,
            bottom: 10,
            left: 5,
            right: 5
          })
          .onClick(() => {
            this.pixelMapGrayScale(this.pixelMap as image.PixelMap);
          })
      }
      .width('100%')
    }
    .height('100%')
  }
}

#### 常见FAQ

Q：在子线程进行蓝牙通信，需要一直发送，选用哪种线程类型，TaskGroup是否可以？

A：需要常驻的耗时任务建议使用[Worker](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-worker)线程。TaskGroup适用于执行一组有关联的耗时较短的任务。

Q：在调用addTask时一直报错：executed taskGroup cannot addTask。原因是什么？

A：在execute之后调用addTask就会产生这样的报错。TaskGroup是一次执行一组任务，适用于执行一组有关联的任务，不要在execute之后调用addTask。

Q：单个TaskGroup中的多个task任务是无序的吗？多个TaskGroup并发的时候是无序的？

A：单个TaskGroup中的多个task任务是无序的，但是返回的结果是按照addTask的顺序返回。多个TaskGroup并发的时候是无序。