HarmonyOS开发中的视频效果:滤镜、水印、裁剪与效果渲染管线全解析

举报
Jack20 发表于 2026/06/20 21:14:06 2026/06/20
【摘要】 HarmonyOS开发中的视频效果:滤镜、水印、裁剪与效果渲染管线全解析核心要点:掌握HarmonyOS视频效果处理的核心技术,包括视频滤镜实现、色彩调整、视频水印叠加、视频裁剪,以及效果渲染管线的构建。 一、背景与动机刷短视频的时候,你有没有注意过那些让人眼前一亮的效果?复古胶片滤镜让画面泛黄带颗粒感,动态水印在角落闪烁品牌logo,画面裁剪把16:9变成了9:16的竖屏……这些效果看似...

HarmonyOS开发中的视频效果:滤镜、水印、裁剪与效果渲染管线全解析

核心要点:掌握HarmonyOS视频效果处理的核心技术,包括视频滤镜实现、色彩调整、视频水印叠加、视频裁剪,以及效果渲染管线的构建。


一、背景与动机

刷短视频的时候,你有没有注意过那些让人眼前一亮的效果?复古胶片滤镜让画面泛黄带颗粒感,动态水印在角落闪烁品牌logo,画面裁剪把16:9变成了9:16的竖屏……这些效果看似简单,背后却是一整套渲染管线在支撑。

视频效果处理,和图片效果处理有本质区别——图片是静态的,处理一次就完事;视频是动态的,每一帧都要处理,而且要保证实时性。30fps的视频意味着你只有33毫秒来处理一帧,包括滤镜计算、水印叠加、裁剪变换……时间非常紧张。

HarmonyOS提供了effectKit和Image Effect API来处理视觉效果,同时也可以通过Canvas和XComponent自定义渲染管线。今天我们就来把视频效果这件事从头到尾讲清楚。


二、核心原理

2.1 视频效果处理的本质

视频效果处理的本质是逐帧变换——对视频的每一帧图像应用某种数学变换:
图片.png

2.2 效果渲染管线

效果渲染管线(Render Pipeline)是视频效果处理的核心架构。它将多个效果串联起来,数据从一端进入,经过多个效果节点的处理,最终从另一端输出:

flowchart LR
    classDef primary fill:#4FC3F7,stroke:#0288D1,color:#000
    classDef warning fill:#FFB74D,stroke:#F57C00,color:#000
    classDef error fill:#EF5350,stroke:#C62828,color:#fff
    classDef info fill:#81C784,stroke:#388E3C,color:#000
    classDef purple fill:#CE93D8,stroke:#7B1FA2,color:#000

    A[输入帧]:::primary --> B[色彩调整]:::warning
    B --> C[滤镜效果]:::info
    C --> D[水印叠加]:::purple
    D --> E[裁剪变换]:::error
    E --> F[输出帧]:::primary

管线设计的核心原则

  1. 顺序敏感:先调色再加水印,和水印上再调色,结果完全不同
  2. 性能优先:把计算量大的效果放在后面,避免对已经处理过的数据做无用功
  3. 可插拔:每个效果节点应该独立,可以自由组合

2.3 滤镜的数学原理

常见的视频滤镜本质上是对像素值的数学变换:

滤镜类型 原理 公式(简化)
灰度 去除色彩信息 Gray = 0.299R + 0.587G + 0.114B
复古/暖色 增加红黄、减少蓝 R’ = R + 30, G’ = G + 15, B’ = B - 20
冷色 增加蓝、减少红 R’ = R - 20, G’ = G + 5, B’ = B + 30
反色 颜色取反 R’ = 255 - R, G’ = 255 - G, B’ = 255 - B
高对比度 拉大亮暗差距 V’ = (V - 128) × contrast + 128
模糊 周围像素加权平均 高斯模糊使用高斯核卷积

三、代码实战

3.1 基础实战:视频滤镜与色彩调整

使用effectKit对视频帧进行滤镜和色彩调整:

// VideoFilterDemo.ets
// 视频滤镜与色彩调整

import { image } from '@kit.ImageKit'
import { effectKit } from '@kit.EffectKit'

// 滤镜类型枚举
export enum FilterType {
  NONE = 'none',           // 无滤镜
  GRAYSCALE = 'grayscale', // 灰度
  SEPIA = 'sepia',         // 复古
  COOL = 'cool',           // 冷色
  WARM = 'warm',           // 暖色
  INVERT = 'invert',       // 反色
  VINTAGE = 'vintage',     // 老照片
  HIGH_CONTRAST = 'high_contrast' // 高对比度
}

// 色彩调整参数
export interface ColorAdjustParams {
  brightness: number   // 亮度 -100 ~ 100
  contrast: number     // 对比度 -100 ~ 100
  saturation: number   // 饱和度 -100 ~ 100
  hue: number          // 色调 -180 ~ 180
  temperature: number  // 色温 -100 ~ 100
}

@Entry
@Component
struct VideoFilterDemo {
  @State currentFilter: FilterType = FilterType.NONE
  @State colorParams: ColorAdjustParams = {
    brightness: 0,
    contrast: 0,
    saturation: 0,
    hue: 0,
    temperature: 0
  }
  @State previewPixelMap: PixelMap | null = null
  @State isProcessing: boolean = false

  // 滤镜列表
  private filters: { type: FilterType; name: string; icon: string }[] = [
    { type: FilterType.NONE, name: '原图', icon: '🔲' },
    { type: FilterType.GRAYSCALE, name: '灰度', icon: '⬛' },
    { type: FilterType.SEPIA, name: '复古', icon: '📜' },
    { type: FilterType.COOL, name: '冷色', icon: '❄️' },
    { type: FilterType.WARM, name: '暖色', icon: '☀️' },
    { type: FilterType.INVERT, name: '反色', icon: '🔄' },
    { type: FilterType.VINTAGE, name: '老照片', icon: '📷' },
    { type: FilterType.HIGH_CONTRAST, name: '高对比', icon: '⚡' }
  ]

  // 应用滤镜效果
  async applyFilter(pixelMap: PixelMap, filterType: FilterType): Promise<PixelMap> {
    this.isProcessing = true
    
    try {
      // 创建滤镜效果
      const filter = effectKit.createEffect(pixelMap)
      
      switch (filterType) {
        case FilterType.GRAYSCALE:
          // 灰度滤镜
          filter.grayscale()
          break
          
        case FilterType.SEPIA:
          // 复古滤镜 - 使用棕褐色调
          filter.setColorMatrix([
            0.393, 0.769, 0.189, 0, 0,
            0.349, 0.686, 0.168, 0, 0,
            0.272, 0.534, 0.131, 0, 0,
            0, 0, 0, 1, 0
          ])
          break
          
        case FilterType.COOL:
          // 冷色滤镜 - 增加蓝色
          filter.setColorMatrix([
            0.9, 0, 0, 0, 0,
            0, 0.9, 0, 0, 0,
            0, 0, 1.2, 0, 20,
            0, 0, 0, 1, 0
          ])
          break
          
        case FilterType.WARM:
          // 暖色滤镜 - 增加红黄色
          filter.setColorMatrix([
            1.2, 0, 0, 0, 20,
            0, 1.1, 0, 0, 10,
            0, 0, 0.9, 0, 0,
            0, 0, 0, 1, 0
          ])
          break
          
        case FilterType.INVERT:
          // 反色滤镜
          filter.invert()
          break
          
        case FilterType.VINTAGE:
          // 老照片效果 - 降低饱和度 + 泛黄 + 轻微模糊
          filter.setColorMatrix([
            0.6, 0.3, 0.1, 0, 20,
            0.2, 0.5, 0.1, 0, 10,
            0.1, 0.2, 0.4, 0, 0,
            0, 0, 0, 1, 0
          ])
          break
          
        case FilterType.HIGH_CONTRAST:
          // 高对比度
          filter.brightness(10)
          filter.contrast(30)
          break
          
        case FilterType.NONE:
        default:
          // 无滤镜,不做处理
          break
      }
      
      // 获取处理后的PixelMap
      const result = await filter.getEffectPixelMap()
      this.isProcessing = false
      return result
      
    } catch (err) {
      this.isProcessing = false
      console.error(`[Filter] 滤镜处理失败: ${err}`)
      return pixelMap
    }
  }

  // 应用色彩调整
  async applyColorAdjust(pixelMap: PixelMap, params: ColorAdjustParams): Promise<PixelMap> {
    try {
      const filter = effectKit.createEffect(pixelMap)
      
      // 亮度调整
      if (params.brightness !== 0) {
        filter.brightness(params.brightness)
      }
      
      // 对比度调整
      if (params.contrast !== 0) {
        filter.contrast(params.contrast)
      }
      
      // 饱和度调整
      if (params.saturation !== 0) {
        filter.saturate(params.saturation)
      }
      
      // 色调旋转
      if (params.hue !== 0) {
        filter.hueRotate(params.hue)
      }
      
      return await filter.getEffectPixelMap()
    } catch (err) {
      console.error(`[ColorAdjust] 色彩调整失败: ${err}`)
      return pixelMap
    }
  }

  build() {
    Column() {
      // 预览区域
      if (this.previewPixelMap) {
        Image(this.previewPixelMap)
          .width('100%')
          .height(280)
          .objectFit(ImageFit.Contain)
      } else {
        Column() {
          Text('视频帧预览区')
            .fontSize(16)
            .fontColor('#666666')
        }
        .width('100%')
        .height(280)
        .justifyContent(FlexAlign.Center)
        .backgroundColor('#2a2a3e')
      }

      // 处理中指示器
      if (this.isProcessing) {
        LoadingProgress()
          .width(24)
          .height(24)
          .color('#4FC3F7')
          .margin({ top: 8 })
      }

      // 滤镜选择
      Text('滤镜效果')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#ffffff')
        .margin({ top: 16, bottom: 8 })

      Scroll() {
        Row() {
          ForEach(this.filters, (item: { type: FilterType; name: string; icon: string }) => {
            Column() {
              Text(item.icon)
                .fontSize(28)
              Text(item.name)
                .fontSize(12)
                .fontColor(this.currentFilter === item.type ? '#4FC3F7' : '#aaaaaa')
                .margin({ top: 4 })
            }
            .width(60)
            .height(70)
            .justifyContent(FlexAlign.Center)
            .borderRadius(8)
            .backgroundColor(this.currentFilter === item.type ? 'rgba(79,195,247,0.2)' : '#2a2a3e')
            .border({
              width: this.currentFilter === item.type ? 2 : 0,
              color: '#4FC3F7'
            })
            .onClick(() => {
              this.currentFilter = item.type
              // 实际项目中这里触发帧处理
            })
          })
        }
      }
      .scrollable(ScrollDirection.Horizontal)
      .width('100%')
      .padding({ left: 16, right: 16 })

      // 色彩调整
      Text('色彩调整')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#ffffff')
        .margin({ top: 16, bottom: 8 })

      Column() {
        // 亮度
        Row() {
          Text('亮度')
            .fontSize(12)
            .fontColor('#aaaaaa')
            .width(50)
          Slider({ value: this.colorParams.brightness, min: -100, max: 100 })
            .width('60%')
            .onChange((value: number) => {
              this.colorParams.brightness = value
            })
          Text(`${this.colorParams.brightness}`)
            .fontSize(12)
            .fontColor('#ffffff')
            .width(40)
        }
        .width('100%')

        // 对比度
        Row() {
          Text('对比度')
            .fontSize(12)
            .fontColor('#aaaaaa')
            .width(50)
          Slider({ value: this.colorParams.contrast, min: -100, max: 100 })
            .width('60%')
            .onChange((value: number) => {
              this.colorParams.contrast = value
            })
          Text(`${this.colorParams.contrast}`)
            .fontSize(12)
            .fontColor('#ffffff')
            .width(40)
        }
        .width('100%')

        // 饱和度
        Row() {
          Text('饱和度')
            .fontSize(12)
            .fontColor('#aaaaaa')
            .width(50)
          Slider({ value: this.colorParams.saturation, min: -100, max: 100 })
            .width('60%')
            .onChange((value: number) => {
              this.colorParams.saturation = value
            })
          Text(`${this.colorParams.saturation}`)
            .fontSize(12)
            .fontColor('#ffffff')
            .width(40)
        }
        .width('100%')

        // 色调
        Row() {
          Text('色调')
            .fontSize(12)
            .fontColor('#aaaaaa')
            .width(50)
          Slider({ value: this.colorParams.hue, min: -180, max: 180 })
            .width('60%')
            .onChange((value: number) => {
              this.colorParams.hue = value
            })
          Text(`${this.colorParams.hue}°`)
            .fontSize(12)
            .fontColor('#ffffff')
            .width(40)
        }
        .width('100%')
      }
      .padding({ left: 16, right: 16 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a2e')
  }
}

3.2 进阶实战:视频水印叠加

视频水印是视频内容保护的重要手段,支持文字水印和图片水印:

// VideoWatermarkDemo.ets
// 视频水印叠加

import { image } from '@kit.ImageKit'

// 水印位置枚举
export enum WatermarkPosition {
  TOP_LEFT = 'top_left',
  TOP_RIGHT = 'top_right',
  BOTTOM_LEFT = 'bottom_left',
  BOTTOM_RIGHT = 'bottom_right',
  CENTER = 'center'
}

// 文字水印配置
export interface TextWatermarkConfig {
  text: string              // 水印文字
  fontSize: number          // 字号
  fontColor: string         // 字体颜色
  position: WatermarkPosition // 位置
  opacity: number           // 透明度 0~1
  offsetX: number           // X偏移
  offsetY: number           // Y偏移
  rotation: number          // 旋转角度
  shadow: boolean           // 是否有阴影
}

// 图片水印配置
export interface ImageWatermarkConfig {
  imagePixelMap: PixelMap   // 水印图片
  position: WatermarkPosition
  opacity: number
  offsetX: number
  offsetY: number
  scale: number             // 缩放比例
  rotation: number
}

// 水印管理器
export class WatermarkManager {
  // 计算水印在画面中的实际坐标
  static calcWatermarkPosition(
    frameWidth: number,
    frameHeight: number,
    watermarkWidth: number,
    watermarkHeight: number,
    position: WatermarkPosition,
    offsetX: number,
    offsetY: number
  ): { x: number; y: number } {
    const padding = 20 // 边距
    
    switch (position) {
      case WatermarkPosition.TOP_LEFT:
        return { x: padding + offsetX, y: padding + offsetY }
      case WatermarkPosition.TOP_RIGHT:
        return { x: frameWidth - watermarkWidth - padding + offsetX, y: padding + offsetY }
      case WatermarkPosition.BOTTOM_LEFT:
        return { x: padding + offsetX, y: frameHeight - watermarkHeight - padding + offsetY }
      case WatermarkPosition.BOTTOM_RIGHT:
        return { x: frameWidth - watermarkWidth - padding + offsetX, y: frameHeight - watermarkHeight - padding + offsetY }
      case WatermarkPosition.CENTER:
        return {
          x: (frameWidth - watermarkWidth) / 2 + offsetX,
          y: (frameHeight - watermarkHeight) / 2 + offsetY
        }
    }
  }

  // 在Canvas上绘制文字水印
  static drawTextWatermark(
    canvas: CanvasRenderingContext2D,
    config: TextWatermarkConfig,
    frameWidth: number,
    frameHeight: number
  ) {
    const pos = this.calcWatermarkPosition(
      frameWidth, frameHeight,
      config.text.length * config.fontSize * 0.6, // 估算文字宽度
      config.fontSize,
      config.position,
      config.offsetX,
      config.offsetY
    )

    canvas.save()
    
    // 设置透明度
    canvas.globalAlpha = config.opacity
    
    // 设置旋转
    if (config.rotation !== 0) {
      canvas.translate(pos.x, pos.y)
      canvas.rotate(config.rotation * Math.PI / 180)
      canvas.translate(-pos.x, -pos.y)
    }

    // 绘制阴影
    if (config.shadow) {
      canvas.shadowColor = 'rgba(0, 0, 0, 0.5)'
      canvas.shadowBlur = 4
      canvas.shadowOffsetX = 2
      canvas.shadowOffsetY = 2
    }

    // 绘制文字
    canvas.font = `${config.fontSize}px sans-serif`
    canvas.fillStyle = config.fontColor
    canvas.fillText(config.text, pos.x, pos.y + config.fontSize)
    
    canvas.restore()
  }

  // 在Canvas上绘制图片水印
  static drawImageWatermark(
    canvas: CanvasRenderingContext2D,
    config: ImageWatermarkConfig,
    frameWidth: number,
    frameHeight: number
  ) {
    const scaledWidth = config.imagePixelMap.getImageInfoSync().size.width * config.scale
    const scaledHeight = config.imagePixelMap.getImageInfoSync().size.height * config.scale

    const pos = this.calcWatermarkPosition(
      frameWidth, frameHeight,
      scaledWidth, scaledHeight,
      config.position,
      config.offsetX,
      config.offsetY
    )

    canvas.save()
    
    canvas.globalAlpha = config.opacity
    
    if (config.rotation !== 0) {
      canvas.translate(pos.x + scaledWidth / 2, pos.y + scaledHeight / 2)
      canvas.rotate(config.rotation * Math.PI / 180)
      canvas.translate(-(pos.x + scaledWidth / 2), -(pos.y + scaledHeight / 2))
    }

    canvas.drawImage(config.imagePixelMap, pos.x, pos.y, scaledWidth, scaledHeight)
    
    canvas.restore()
  }

  // 生成平铺水印(全画面重复水印,常用于版权保护)
  static drawTiledTextWatermark(
    canvas: CanvasRenderingContext2D,
    text: string,
    fontSize: number,
    fontColor: string,
    opacity: number,
    frameWidth: number,
    frameHeight: number,
    gapX: number = 200,
    gapY: number = 100,
    rotation: number = -30
  ) {
    canvas.save()
    canvas.globalAlpha = opacity
    canvas.font = `${fontSize}px sans-serif`
    canvas.fillStyle = fontColor

    // 旋转整个画布
    canvas.translate(frameWidth / 2, frameHeight / 2)
    canvas.rotate(rotation * Math.PI / 180)
    canvas.translate(-frameWidth / 2, -frameHeight / 2)

    // 平铺绘制
    const textWidth = text.length * fontSize * 0.6
    for (let y = -frameHeight; y < frameHeight * 2; y += gapY) {
      for (let x = -frameWidth; x < frameWidth * 2; x += gapX + textWidth) {
        canvas.fillText(text, x, y)
      }
    }

    canvas.restore()
  }
}

// ====== 水印配置UI ======

@Entry
@Component
struct VideoWatermarkDemo {
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  
  @State textWatermark: TextWatermarkConfig = {
    text: 'HarmonyOS',
    fontSize: 24,
    fontColor: '#ffffff',
    position: WatermarkPosition.BOTTOM_RIGHT,
    opacity: 0.7,
    offsetX: 0,
    offsetY: 0,
    rotation: 0,
    shadow: true
  }
  @State useTiledWatermark: boolean = false
  @State tiledText: string = '版权所有'
  @State frameWidth: number = 360
  @State frameHeight: number = 240

  build() {
    Column() {
      // Canvas预览区域
      Canvas(this.canvasContext)
        .width('100%')
        .height(240)
        .backgroundColor('#2a2a3e')
        .onReady(() => {
          this.renderWatermark()
        })

      // 水印文字配置
      Text('文字水印配置')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .fontColor('#ffffff')
        .margin({ top: 16, bottom: 8 })

      // 水印文字
      Row() {
        Text('文字:')
          .fontSize(14)
          .fontColor('#aaaaaa')
          .width(50)
        TextInput({ text: this.textWatermark.text })
          .width('60%')
          .height(36)
          .onChange((value: string) => {
            this.textWatermark.text = value
            this.renderWatermark()
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
      .margin({ bottom: 8 })

      // 透明度
      Row() {
        Text('透明度:')
          .fontSize(14)
          .fontColor('#aaaaaa')
          .width(50)
        Slider({ value: this.textWatermark.opacity * 100, min: 10, max: 100 })
          .width('55%')
          .onChange((value: number) => {
            this.textWatermark.opacity = value / 100
            this.renderWatermark()
          })
        Text(`${Math.round(this.textWatermark.opacity * 100)}%`)
          .fontSize(14)
          .fontColor('#ffffff')
          .width(40)
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
      .margin({ bottom: 8 })

      // 位置选择
      Row() {
        Text('位置:')
          .fontSize(14)
          .fontColor('#aaaaaa')
          .width(50)
        
        ForEach([
          { pos: WatermarkPosition.TOP_LEFT, label: '↖' },
          { pos: WatermarkPosition.TOP_RIGHT, label: '↗' },
          { pos: WatermarkPosition.BOTTOM_LEFT, label: '↙' },
          { pos: WatermarkPosition.BOTTOM_RIGHT, label: '↘' },
          { pos: WatermarkPosition.CENTER, label: '⊕' }
        ], (item: { pos: WatermarkPosition; label: string }) => {
          Button(item.label)
            .fontSize(16)
            .width(40)
            .height(40)
            .backgroundColor(this.textWatermark.position === item.pos ? '#4FC3F7' : '#333333')
            .onClick(() => {
              this.textWatermark.position = item.pos
              this.renderWatermark()
            })
        })
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
      .margin({ bottom: 8 })

      // 旋转角度
      Row() {
        Text('旋转:')
          .fontSize(14)
          .fontColor('#aaaaaa')
          .width(50)
        Slider({ value: this.textWatermark.rotation, min: -90, max: 90 })
          .width('55%')
          .onChange((value: number) => {
            this.textWatermark.rotation = value
            this.renderWatermark()
          })
        Text(`${this.textWatermark.rotation}°`)
          .fontSize(14)
          .fontColor('#ffffff')
          .width(40)
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
      .margin({ bottom: 8 })

      // 平铺水印开关
      Row() {
        Text('平铺水印:')
          .fontSize(14)
          .fontColor('#aaaaaa')
          .width(70)
        Toggle({ type: ToggleType.Switch, isOn: this.useTiledWatermark })
          .onChange((isOn: boolean) => {
            this.useTiledWatermark = isOn
            this.renderWatermark()
          })
      }
      .width('100%')
      .padding({ left: 16, right: 16 })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a2e')
  }

  // 渲染水印到Canvas
  private renderWatermark() {
    if (!this.canvasContext) return
    
    // 清空画布
    this.canvasContext.clearRect(0, 0, this.frameWidth, this.frameHeight)
    
    // 绘制背景(模拟视频帧)
    this.canvasContext.fillStyle = '#3a3a4e'
    this.canvasContext.fillRect(0, 0, this.frameWidth, this.frameHeight)
    
    // 绘制模拟视频内容
    this.canvasContext.fillStyle = '#5a5a7e'
    this.canvasContext.fillRect(20, 20, this.frameWidth - 40, this.frameHeight - 40)
    this.canvasContext.font = '16px sans-serif'
    this.canvasContext.fillStyle = '#888888'
    this.canvasContext.fillText('视频帧内容区域', this.frameWidth / 2 - 60, this.frameHeight / 2)
    
    if (this.useTiledWatermark) {
      // 平铺水印
      WatermarkManager.drawTiledTextWatermark(
        this.canvasContext,
        this.tiledText,
        16,
        'rgba(255,255,255,0.3)',
        0.3,
        this.frameWidth,
        this.frameHeight
      )
    } else {
      // 单个文字水印
      WatermarkManager.drawTextWatermark(
        this.canvasContext,
        this.textWatermark,
        this.frameWidth,
        this.frameHeight
      )
    }
  }
}

3.3 高级实战:效果渲染管线与视频裁剪

将多个效果串联成管线,并实现视频裁剪功能:

// EffectPipelineDemo.ets
// 效果渲染管线与视频裁剪

import { image } from '@kit.ImageKit'
import { effectKit } from '@kit.EffectKit'

// 效果节点基类
export interface EffectNode {
  type: string
  enabled: boolean
  process(pixelMap: PixelMap): Promise<PixelMap>
}

// 滤镜效果节点
export class FilterEffectNode implements EffectNode {
  type: string = 'filter'
  enabled: boolean = true
  filterType: string = 'none'
  colorMatrix: number[] = []

  constructor(filterType: string, colorMatrix?: number[]) {
    this.filterType = filterType
    if (colorMatrix) {
      this.colorMatrix = colorMatrix
    }
  }

  async process(pixelMap: PixelMap): Promise<PixelMap> {
    if (!this.enabled || this.filterType === 'none') return pixelMap
    
    const filter = effectKit.createEffect(pixelMap)
    
    if (this.colorMatrix.length > 0) {
      filter.setColorMatrix(this.colorMatrix)
    }
    
    switch (this.filterType) {
      case 'grayscale':
        filter.grayscale()
        break
      case 'invert':
        filter.invert()
        break
      case 'blur':
        filter.blur(5)
        break
    }
    
    return await filter.getEffectPixelMap()
  }
}

// 色彩调整效果节点
export class ColorAdjustEffectNode implements EffectNode {
  type: string = 'colorAdjust'
  enabled: boolean = true
  brightness: number = 0
  contrast: number = 0
  saturation: number = 0

  async process(pixelMap: PixelMap): Promise<PixelMap> {
    if (!this.enabled) return pixelMap
    
    const filter = effectKit.createEffect(pixelMap)
    
    if (this.brightness !== 0) filter.brightness(this.brightness)
    if (this.contrast !== 0) filter.contrast(this.contrast)
    if (this.saturation !== 0) filter.saturate(this.saturation)
    
    return await filter.getEffectPixelMap()
  }
}

// 裁剪效果节点
export class CropEffectNode implements EffectNode {
  type: string = 'crop'
  enabled: boolean = true
  cropX: number = 0
  cropY: number = 0
  cropWidth: number = 0
  cropHeight: number = 0

  async process(pixelMap: PixelMap): Promise<PixelMap> {
    if (!this.enabled) return pixelMap
    
    const info = pixelMap.getImageInfoSync()
    const x = Math.max(0, Math.min(this.cropX, info.size.width - 1))
    const y = Math.max(0, Math.min(this.cropY, info.size.height - 1))
    const width = Math.min(this.cropWidth, info.size.width - x)
    const height = Math.min(this.cropHeight, info.size.height - y)
    
    if (width <= 0 || height <= 0) return pixelMap
    
    // 使用PixelMap的cropRegion进行裁剪
    const region: image.Region = {
      x: x, y: y,
      w: width, h: height
    }
    
    // 创建裁剪后的PixelMap
    const croppedPixelMap = await pixelMap.crop(region)
    return croppedPixelMap
  }
}

// 效果渲染管线
export class EffectPipeline {
  private nodes: EffectNode[] = []
  
  // 添加效果节点
  addNode(node: EffectNode): EffectPipeline {
    this.nodes.push(node)
    return this
  }
  
  // 移除指定类型的效果节点
  removeNode(type: string): EffectPipeline {
    this.nodes = this.nodes.filter(n => n.type !== type)
    return this
  }
  
  // 在指定位置插入效果节点
  insertNode(index: number, node: EffectNode): EffectPipeline {
    this.nodes.splice(index, 0, node)
    return this
  }
  
  // 获取所有节点
  getNodes(): EffectNode[] {
    return [...this.nodes]
  }
  
  // 执行管线处理
  async process(pixelMap: PixelMap): Promise<PixelMap> {
    let result: PixelMap = pixelMap
    
    for (const node of this.nodes) {
      if (node.enabled) {
        try {
          result = await node.process(result)
        } catch (err) {
          console.error(`[Pipeline] 节点 ${node.type} 处理失败: ${err}`)
          // 出错时跳过该节点,继续处理后续节点
        }
      }
    }
    
    return result
  }
  
  // 清空管线
  clear(): EffectPipeline {
    this.nodes = []
    return this
  }
}

// ====== 渲染管线UI ======

@Entry
@Component
struct EffectPipelineDemo {
  private pipeline: EffectPipeline = new EffectPipeline()
  private settings: RenderingContextSettings = new RenderingContextSettings(true)
  private canvasContext: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings)
  
  @State pipelineNodes: { type: string; name: string; enabled: boolean }[] = []
  @State cropX: number = 0
  @State cropY: number = 0
  @State cropWidth: number = 320
  @State cropHeight: number = 180
  @State brightness: number = 0
  @State contrast: number = 0
  @State saturation: number = 0
  @State selectedFilter: string = 'none'
  @State isProcessing: boolean = false
  @State processTimeMs: number = 0

  // 可用滤镜
  private filterOptions: { key: string; name: string; matrix?: number[] }[] = [
    { key: 'none', name: '无' },
    { key: 'grayscale', name: '灰度' },
    { key: 'invert', name: '反色' },
    { key: 'blur', name: '模糊' },
    {
      key: 'sepia', name: '复古',
      matrix: [
        0.393, 0.769, 0.189, 0, 0,
        0.349, 0.686, 0.168, 0, 0,
        0.272, 0.534, 0.131, 0, 0,
        0, 0, 0, 1, 0
      ]
    },
    {
      key: 'cool', name: '冷色',
      matrix: [
        0.9, 0, 0, 0, 0,
        0, 0.9, 0, 0, 0,
        0, 0, 1.2, 0, 20,
        0, 0, 0, 1, 0
      ]
    }
  ]

  // 重建管线
  private rebuildPipeline() {
    this.pipeline.clear()
    this.pipelineNodes = []

    // 第一步:色彩调整
    if (this.brightness !== 0 || this.contrast !== 0 || this.saturation !== 0) {
      const colorNode = new ColorAdjustEffectNode()
      colorNode.brightness = this.brightness
      colorNode.contrast = this.contrast
      colorNode.saturation = this.saturation
      this.pipeline.addNode(colorNode)
      this.pipelineNodes.push({ type: 'colorAdjust', name: '色彩调整', enabled: true })
    }

    // 第二步:滤镜
    if (this.selectedFilter !== 'none') {
      const filterOption = this.filterOptions.find(f => f.key === this.selectedFilter)
      const filterNode = new FilterEffectNode(
        this.selectedFilter,
        filterOption?.matrix
      )
      this.pipeline.addNode(filterNode)
      this.pipelineNodes.push({ type: 'filter', name: `滤镜:${filterOption?.name}`, enabled: true })
    }

    // 第三步:裁剪
    if (this.cropWidth > 0 && this.cropHeight > 0) {
      const cropNode = new CropEffectNode()
      cropNode.cropX = this.cropX
      cropNode.cropY = this.cropY
      cropNode.cropWidth = this.cropWidth
      cropNode.cropHeight = this.cropHeight
      this.pipeline.addNode(cropNode)
      this.pipelineNodes.push({ type: 'crop', name: '裁剪', enabled: true })
    }
  }

  build() {
    Scroll() {
      Column() {
        // 标题
        Text('效果渲染管线')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffffff')
          .margin({ bottom: 16 })

        // Canvas预览
        Canvas(this.canvasContext)
          .width('100%')
          .height(200)
          .backgroundColor('#2a2a3e')
          .onReady(() => {
            this.renderPreview()
          })

        // 管线节点可视化
        Text('管线流程')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#4FC3F7')
          .margin({ top: 16, bottom: 8 })

        if (this.pipelineNodes.length === 0) {
          Text('暂无效果节点,请添加效果')
            .fontSize(14)
            .fontColor('#666666')
        } else {
          Scroll() {
            Row() {
              Text('输入帧')
                .fontSize(12)
                .fontColor('#81C784')
                .padding(6)
                .borderRadius(4)
                .backgroundColor('rgba(129,199,132,0.2)')
              
              ForEach(this.pipelineNodes, (node: { type: string; name: string; enabled: boolean }) => {
                Row() {
                  Text('→')
                    .fontSize(16)
                    .fontColor('#666666')
                    .margin({ left: 4, right: 4 })
                  
                  Text(node.name)
                    .fontSize(12)
                    .fontColor('#4FC3F7')
                    .padding(6)
                    .borderRadius(4)
                    .backgroundColor('rgba(79,195,247,0.2)')
                }
              })
              
              Text('→')
                .fontSize(16)
                .fontColor('#666666')
                .margin({ left: 4, right: 4 })
              
              Text('输出帧')
                .fontSize(12)
                .fontColor('#CE93D8')
                .padding(6)
                .borderRadius(4)
                .backgroundColor('rgba(206,147,216,0.2)')
            }
          }
          .scrollable(ScrollDirection.Horizontal)
          .width('100%')
        }

        // 色彩调整
        Text('色彩调整')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffffff')
          .margin({ top: 16, bottom: 8 })

        Row() {
          Text('亮度')
            .fontSize(12)
            .fontColor('#aaaaaa')
            .width(40)
          Slider({ value: this.brightness, min: -50, max: 50 })
            .width('55%')
            .onChange((value: number) => {
              this.brightness = value
              this.rebuildPipeline()
            })
          Text(`${this.brightness}`)
            .fontSize(12)
            .fontColor('#ffffff')
            .width(30)
        }
        .width('100%')
        .padding({ left: 16, right: 16 })

        Row() {
          Text('对比度')
            .fontSize(12)
            .fontColor('#aaaaaa')
            .width(40)
          Slider({ value: this.contrast, min: -50, max: 50 })
            .width('55%')
            .onChange((value: number) => {
              this.contrast = value
              this.rebuildPipeline()
            })
          Text(`${this.contrast}`)
            .fontSize(12)
            .fontColor('#ffffff')
            .width(30)
        }
        .width('100%')
        .padding({ left: 16, right: 16 })

        Row() {
          Text('饱和度')
            .fontSize(12)
            .fontColor('#aaaaaa')
            .width(40)
          Slider({ value: this.saturation, min: -50, max: 50 })
            .width('55%')
            .onChange((value: number) => {
              this.saturation = value
              this.rebuildPipeline()
            })
          Text(`${this.saturation}`)
            .fontSize(12)
            .fontColor('#ffffff')
            .width(30)
        }
        .width('100%')
        .padding({ left: 16, right: 16 })

        // 滤镜选择
        Text('滤镜效果')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffffff')
          .margin({ top: 16, bottom: 8 })

        Row() {
          ForEach(this.filterOptions, (option: { key: string; name: string }) => {
            Button(option.name)
              .fontSize(12)
              .height(32)
              .backgroundColor(this.selectedFilter === option.key ? '#4FC3F7' : '#333333')
              .onClick(() => {
                this.selectedFilter = option.key
                this.rebuildPipeline()
              })
          })
        }
        .width('100%')
        .justifyContent(FlexAlign.SpaceEvenly)
        .padding({ left: 16, right: 16 })

        // 裁剪配置
        Text('视频裁剪')
          .fontSize(16)
          .fontWeight(FontWeight.Bold)
          .fontColor('#ffffff')
          .margin({ top: 16, bottom: 8 })

        Row() {
          Text('宽')
            .fontSize(12)
            .fontColor('#aaaaaa')
            .width(20)
          Slider({ value: this.cropWidth, min: 100, max: 720 })
            .width('55%')
            .onChange((value: number) => {
              this.cropWidth = value
              this.rebuildPipeline()
            })
          Text(`${this.cropWidth}`)
            .fontSize(12)
            .fontColor('#ffffff')
            .width(40)
        }
        .width('100%')
        .padding({ left: 16, right: 16 })

        Row() {
          Text('高')
            .fontSize(12)
            .fontColor('#aaaaaa')
            .width(20)
          Slider({ value: this.cropHeight, min: 100, max: 480 })
            .width('55%')
            .onChange((value: number) => {
              this.cropHeight = value
              this.rebuildPipeline()
            })
          Text(`${this.cropHeight}`)
            .fontSize(12)
            .fontColor('#ffffff')
            .width(40)
        }
        .width('100%')
        .padding({ left: 16, right: 16 })

        // 处理耗时
        if (this.processTimeMs > 0) {
          Text(`处理耗时: ${this.processTimeMs}ms`)
            .fontSize(14)
            .fontColor(this.processTimeMs > 33 ? '#EF5350' : '#81C784')
            .margin({ top: 8 })
        }
      }
      .padding(16)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#1a1a2e')
  }

  // 渲染预览
  private renderPreview() {
    if (!this.canvasContext) return
    this.canvasContext.clearRect(0, 0, 400, 200)
    this.canvasContext.fillStyle = '#3a3a4e'
    this.canvasContext.fillRect(0, 0, 400, 200)
    
    // 绘制裁剪区域指示
    this.canvasContext.strokeStyle = '#4FC3F7'
    this.canvasContext.lineWidth = 2
    this.canvasContext.setLineDash([5, 5])
    this.canvasContext.strokeRect(this.cropX, this.cropY, this.cropWidth, this.cropHeight)
    this.canvasContext.setLineDash([])
  }
}

四、踩坑与注意事项

4.1 实时滤镜的性能瓶颈

:对视频每一帧都调用effectKit处理,在低端设备上可能导致严重卡顿。

  • 降低处理分辨率:先缩小到处理分辨率,处理完再放大
  • 使用颜色矩阵代替复杂滤镜:颜色矩阵计算量远小于模糊等卷积操作
  • 跳帧处理:不是每帧都处理,每隔1~2帧处理一次

4.2 PixelMap的生命周期管理

:视频帧处理过程中频繁创建PixelMap,不及时释放导致内存暴涨。

// 处理完一帧后立即释放
async processFrame(pixelMap: PixelMap): Promise<PixelMap> {
  const result = await this.pipeline.process(pixelMap)
  // 释放原始帧(如果不再需要)
  pixelMap.release()
  return result
}

4.3 裁剪坐标的归一化问题

:不同分辨率的视频,裁剪区域用绝对像素值会导致在不同设备上裁剪位置不一致。

:使用归一化坐标(0~1),处理时再转换为实际像素值:

// 归一化裁剪参数
interface NormalizedCrop {
  x: number  // 0~1
  y: number  // 0~1
  width: number  // 0~1
  height: number  // 0~1
}

// 转换为实际像素
function toPixelCrop(crop: NormalizedCrop, frameWidth: number, frameHeight: number): image.Region {
  return {
    x: Math.round(crop.x * frameWidth),
    y: Math.round(crop.y * frameHeight),
    w: Math.round(crop.width * frameWidth),
    h: Math.round(crop.height * frameHeight)
  }
}

4.4 水印的防去除处理

:简单的水印容易被裁剪或覆盖去除。

  • 使用平铺水印覆盖整个画面,增加去除难度
  • 在画面中心区域也放置水印
  • 使用半透明水印,兼顾美观和防护
  • 水印位置随机微调,防止自动化去除

4.5 Canvas绘制与PixelMap转换

:Canvas绘制水印后,需要将Canvas内容转回PixelMap才能继续处理,这个转换过程有性能开销。

:如果只需要在播放时显示水印,直接在Canvas上绘制即可,不需要转回PixelMap。只有在需要保存或编码输出时才需要转换。


五、HarmonyOS 6适配

5.1 API变更

变更项 HarmonyOS 5 HarmonyOS 6
Image Effect 基础滤镜 新增AI滤镜(人像美颜、背景虚化)
GPU渲染 部分支持 全面支持GPU加速渲染
实时滤镜 CPU处理为主 新增Compute Shader加速
HDR效果 不支持 新增HDR色调映射滤镜
视频裁剪 PixelMap裁剪 新增基于解码器的裁剪(零拷贝)

5.2 迁移指南

// HarmonyOS 6 新增的AI滤镜
import { imageEffect } from '@kit.EffectKit'

// 人像美颜滤镜(HarmonyOS 6新增)
async function applyBeautyFilter(pixelMap: PixelMap): Promise<PixelMap> {
  const effect = imageEffect.createImageEffect(pixelMap)
  // AI美颜:磨皮、美白、瘦脸
  // effect.addFilter(imageEffect.FilterType.BEAUTY, {
  //   smoothLevel: 50,    // 磨皮等级
  //   whitenLevel: 30,    // 美白等级
  //   thinFaceLevel: 20   // 瘦脸等级
  // })
  return await effect.getEffectPixelMap()
}

// 背景虚化滤镜(HarmonyOS 6新增)
async function applyBokehEffect(pixelMap: PixelMap): Promise<PixelMap> {
  const effect = imageEffect.createImageEffect(pixelMap)
  // AI背景虚化
  // effect.addFilter(imageEffect.FilterType.BOKEH, {
  //   blurLevel: 80,      // 虚化程度
  //   focalPoint: { x: 0.5, y: 0.5 } // 对焦点
  // })
  return await effect.getEffectPixelMap()
}

5.3 性能提升

HarmonyOS 6对效果处理做了以下优化:

  • GPU加速:滤镜处理从CPU迁移到GPU,速度提升5~10倍
  • 零拷贝裁剪:基于解码器的裁剪,无需PixelMap拷贝
  • AI滤镜:人像美颜等AI效果使用NPU加速,实时性更好

六、总结

mindmap
  root((视频效果))
    滤镜效果
      颜色矩阵变换
      灰度/复古/冷暖色
      模糊/锐化
      AI美颜(HarmonyOS 6)
    色彩调整
      亮度/对比度/饱和度
      色调旋转
      色温调整
    视频水印
      文字水印
      图片水印
      平铺水印
      位置/透明度/旋转
    视频裁剪
      PixelMap裁剪
      归一化坐标
      零拷贝裁剪(HarmonyOS 6)
    渲染管线
      效果节点串联
      顺序敏感
      可插拔设计
      异常隔离
    关键要点
      实时处理性能
      PixelMap及时释放
      归一化坐标
      水印防去除

一句话总结:视频效果处理的核心是渲染管线——将滤镜、色彩调整、水印、裁剪等效果封装为独立节点,按顺序串联执行。性能优化的关键是利用颜色矩阵代替复杂计算、及时释放PixelMap、使用归一化坐标保证一致性。

记住三个关键

  1. 管线思维——效果是可组合的节点,不是硬编码的步骤
  2. 性能优先——颜色矩阵比卷积快100倍,能用矩阵就用矩阵
  3. 资源释放——PixelMap用完就释放,否则内存会爆炸
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。