HarmonyOS开发中的视频效果:滤镜、水印、裁剪与效果渲染管线全解析
HarmonyOS开发中的视频效果:滤镜、水印、裁剪与效果渲染管线全解析
核心要点:掌握HarmonyOS视频效果处理的核心技术,包括视频滤镜实现、色彩调整、视频水印叠加、视频裁剪,以及效果渲染管线的构建。
一、背景与动机
刷短视频的时候,你有没有注意过那些让人眼前一亮的效果?复古胶片滤镜让画面泛黄带颗粒感,动态水印在角落闪烁品牌logo,画面裁剪把16:9变成了9:16的竖屏……这些效果看似简单,背后却是一整套渲染管线在支撑。
视频效果处理,和图片效果处理有本质区别——图片是静态的,处理一次就完事;视频是动态的,每一帧都要处理,而且要保证实时性。30fps的视频意味着你只有33毫秒来处理一帧,包括滤镜计算、水印叠加、裁剪变换……时间非常紧张。
HarmonyOS提供了effectKit和Image Effect API来处理视觉效果,同时也可以通过Canvas和XComponent自定义渲染管线。今天我们就来把视频效果这件事从头到尾讲清楚。
二、核心原理
2.1 视频效果处理的本质
视频效果处理的本质是逐帧变换——对视频的每一帧图像应用某种数学变换:

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
管线设计的核心原则:
- 顺序敏感:先调色再加水印,和水印上再调色,结果完全不同
- 性能优先:把计算量大的效果放在后面,避免对已经处理过的数据做无用功
- 可插拔:每个效果节点应该独立,可以自由组合
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、使用归一化坐标保证一致性。
记住三个关键:
- 管线思维——效果是可组合的节点,不是硬编码的步骤
- 性能优先——颜色矩阵比卷积快100倍,能用矩阵就用矩阵
- 资源释放——PixelMap用完就释放,否则内存会爆炸
- 点赞
- 收藏
- 关注作者
评论(0)