鴥彼晚风
发布于 2026-07-01 / 2 阅读
0
0

PyAV 视频平面 (Planes) 核心速查手册

1. 核心概念:什么是 Plane?

在 FFmpeg/PyAV 中,Plane(平面) 是帧数据的物理存储单元。一个视频帧(Frame)由一个或多个 Plane 组成,具体数量取决于像素格式:

像素格式

Plane 数量

布局说明

yuv420p

3

Y(全分辨率) + U(1/4) + V(1/4),分离存储

nv12

2

Y(全分辨率) + UV(交错,1/4),半平面

rgb24 / bgr24

1

R/G/B 交错存储于单一连续缓冲区

yuv444p

3

Y/U/V 均为全分辨率,分离存储

gray8

1

单通道灰度

💡 关键认知Plane 继承自 Buffer,它代表的是原始字节内存视图,而非结构化像素数组。直接操作 Plane 等同于操作 C 层面的裸指针。


2. Plane 作为 Buffer 的核心能力

由于 Plane 继承自 av.buffer.Buffer,它提供以下底层内存接口:

属性/方法

说明

视频开发用途

buffer_size

缓冲区总字节数

校验数据完整性、分配目标缓冲

line_size

每行字节数(含 padding)

⚠️ 不等于 width × bpp,必须用于行间跳转

readable / writable

内存读写权限

修改前检查;配合 frame.make_writable()

to_bytes()

导出为 Python bytes

调试/序列化;⚠️ 大数据量时避免使用

to_memoryview()

零拷贝内存视图

✅ 推荐:与 NumPy/PIL 交互的首选方式

update(data)

从 bytes/buffer 写入

填充自定义像素数据

⚠️ line_size ≠ 逻辑宽度

plane = frame.planes[0]  # Y plane of yuv420p

# ❌ 错误假设
row_bytes = frame.width  # 忽略了 stride/padding

# ✅ 正确做法
row_bytes = plane.line_size  # 实际内存行宽,通常 >= width

FFmpeg 为 SIMD 优化会对行做对齐(通常 32/64 字节),line_size 包含了这些 padding。逐行处理时必须以 line_size 为步长


3. 安全访问模式

只读分析(零拷贝)

frame = next(container.decode(video=0))
y_plane = frame.planes[0]

# ✅ 零拷贝获取内存视图,适合 NumPy 处理
import numpy as np
y_array = np.frombuffer(y_plane.to_memoryview(), dtype=np.uint8) \
            .reshape(frame.height, y_plane.line_size)[:, :frame.width]

写入修改(必须先确保可写)

frame = next(container.decode(video=0))

# ✅ 安全写入流程
frame.make_writable()          # 1. 确保独占可写副本
plane = frame.planes[0]        # 2. 获取平面
mv = plane.to_memoryview()     # 3. 获取内存视图
# 4. 通过 memoryview 或 update() 写入数据

❌ 危险操作清单

操作

风险

替代方案

直接修改未 make_writable() 的帧

共享内存污染 / Segfault

始终先调用 make_writable()

width 代替 line_size 做行偏移

读取到 padding 区域或越界

永远使用 plane.line_size

缓存 Plane 对象超出 Frame 生命周期

悬空指针 / Use-after-free

Plane 生命周期绑定 Frame,不要单独持有

对 packed 格式访问 planes[1]

IndexError

先检查 len(frame.planes) 或查像素格式文档

to_bytes() 处理 4K 帧

巨量内存拷贝,GC 压力

to_memoryview() 零拷贝


4. 常见像素格式的 Plane 遍历

YUV420P(最常用)

y, u, v = frame.planes[0], frame.planes[1], frame.planes[2]
# Y: height × line_size[0]
# U: (height//2) × line_size[1]
# V: (height//2) × line_size[2]

NV12(移动端/硬件编码常用)

y, uv = frame.planes[0], frame.planes[1]
# Y:  height × line_size[0]
# UV: (height//2) × line_size[1],U/V 交错排列

RGB24 / BGR24

rgb = frame.planes[0]  # 仅一个平面
# 大小: height × line_size[0],每像素 3 字节交错

5. 与外部库交互速查

目标库

推荐方式

注意事项

NumPy

np.frombuffer(plane.to_memoryview(), dtype=...)

注意 reshape 时用 line_size 再切片到 width

PIL/Pillow

Image.frombytes(fmt, (w,h), plane.to_bytes(), ...)

大帧性能差;优先转 NumPy 再转 PIL

OpenCV

先转 NumPy array,再 cv2.cvtColor

OpenCV 不直接接受 memoryview

TensorFlow/PyTorch

NumPy → tensor;或 torch.frombuffer

GPU 训练建议直接在 tensor 层面做格式转换

C/Cython

plane.ptr + plane.line_size

最底层,需自行管理生命周期


6. 开发排查备忘

症状

可能原因

诊断步骤

画面错位/条纹

用 width 代替 line_size 做行步进

打印 plane.line_size vs expected_width

Segfault / 花屏

修改了共享帧数据

确认 make_writable() 在修改前调用

IndexError on planes[i]

像素格式 plane 数量不符预期

打印 len(frame.planes)frame.format.name

内存暴涨

to_bytes() 在大帧上频繁调用

替换为 to_memoryview()

NumPy reshape 失败

buffer_size ≠ height × width

height × line_size reshape 后切片

写入无效

Plane 不可写

检查 plane.writable;确认 make_writable()

颜色偏差

UV plane 尺寸按全分辨率计算

YUV420 的 UV 宽高均为 Y 的一半

💡 核心记忆点

  • Plane = 原始内存视图,不是像素数组

  • 行步进永远用 line_size,永远不要用 width × bpp

  • 改数据前必 make_writable(),零开销保安全

  • 对外交互首选 to_memoryview(),避免 to_bytes() 拷贝

  • Plane 生命周期绑定 Frame,绝不单独缓存


评论