直接回答
YUV_420_888 是灵活的三平面格式,并不承诺一个连续字节数组就是 I420 或 NV21。Android 规定平面顺序为 Y、U(Cb)、V(Cr),Y 平面不交错且 pixelStride 保证为 1;U、V 平面的 rowStride 和 pixelStride 相同,色度数据采用子采样。每个 plane 仍有各自的缓冲区和字节步长,应按实际 plane 元数据取样,不能只用图像宽度推导地址。 对单个 plane,候选索引公式可以写成 planeBase + row * rowStride + column * pixelStride。row 和 column 是样本坐标计数;base、stride 和结果索引才是字节偏移。只有精确 API 已证明 planeBase 与坐标原点时,这个公式才可使用。不要默认返回的 ByteBuffer.position 永远是零;也不要把已经被代码读取过的当前位置当成永久 plane 原点。java.nio.Buffer 将 position 定义为下一次相对读取的游标,limit 定义为第一个不得读取的索引。duplicate 会创建游标状态独立、字节内容共享的缓冲视图,但它不会证明图像数据从哪里开始。 framework 的 android.media.Image.Plane 文档说明 getBuffer 返回包含帧数据的 direct ByteBuffer,并定义 row 与 pixel stride,但没有规定通用的非零 position 是 plane 基址。AndroidX CameraX 的 ImagePlane 文档从 1.7.0-alpha03 起写明该接口会把 position 设到 plane 数据起点、limit 设到数据终点。这是有版本范围的 AndroidX 约定,不是 framework Image.Plane 的通用保证。应固定实际输出 API 和版本,只依赖其已记录的原点规则;文档和受控适配器都无法证明时,就保留“原点未知”,不要编造偏移。 边界检查要覆盖最后实际采样的字节,而不是假定分配长度等于 rowStride 乘以 height。Android 警告,原始 plane 缓冲区可能只映射到最后一行的最后一个像素;末行后面的填充不一定存在。每个样本都要用无溢出的整数运算计算字节索引,再对照已验证基址和当前 limit 检查。Buffer.clear 会把 position 设为零、limit 设为 capacity;Buffer.rewind 只把 position 设为零并丢弃 mark,limit 保持不变。前者可能暴露生产者有效范围之外的字节,后者不会扩展 limit 但可能让代码从错误原点读取。不要把二者当作修正未证明基址或读取窗口的方法;使用 duplicate 并保留文档规定的窗口。 一次读取应始终使用同一个图像抽象。CameraX 输出选项依赖准确版本:API 文档列出 YUV_420_888、RGBA_8888 从 1.1.0 起,NV21 从 1.5.0 起,PRIVATE 从 1.7.0-alpha03 起;使用前固定实际库版本。请求 NV21 时,ImageProxy.getFormat 仍报告 YUV_420_888,但 proxy 数据按 NV21 排列;部分设备的 ImageProxy.getImage() 可能仍返回原始相机 Image,plane 数据或尺寸与 Proxy 不同。若读取 ImageProxy,就始终配对使用其格式、尺寸、cropRect 和 PlaneProxy,并关闭代理;不要混用底层 Image 的 plane。1.7.0-alpha03 的 PRIVATE 选项需通过该版本提供的设备能力检查,PRIVATE ImageProxy 不支持 CPU 访问且 getPlanes() 返回空数组;不能把 alpha API 或其能力假定为较早版本或 Cove 当前依赖。 用 4×4 合成亮度平面和 2×2 合成色度平面让算术可见:Y 的 base=0、rowStride=6、pixelStride=1、limit=22,则 Y(3,3) 的字节索引为 21,末行有效像素后没有填充;独立 U、V 缓冲各自 base=0、rowStride=6、pixelStride=2、limit=9,则样本 (1,1) 索引为 8。这些独立合成数组只验证地址算术,不证明真实设备的 U/V 缓冲共享或交错存储。再添加不同值与 padding 哨兵,并验证精确末样本 limit。只有当夹具契约明确 plane 起点时,才改 position/limit 测试;不要用某台设备的 YUV 转 RGB 外观代替索引测试。纯夹具不证明相机支持、模型接受或颜色准确度。 色度坐标要从同一全图原点计算。对全图坐标 fullX/fullY,子采样样本为 floor(fullX/2)、floor(fullY/2)。若坐标相对 cropRect,则先加已验证的 cropLeft/cropTop 再除以二。例如 cropLeft=1、localX=1,先得 fullX=2 再得 chromaX=1;先对 localX 除二会错误地得到 0。这只是坐标换算,不展开成旋转或 EXIF 教程。 本文是 CPU 平面寻址方法,不表示 Cove 当前采用这条相机输入路径,也不建议替换 CameraX 输出设置。旋转、裁剪几何、transfer function、RGB 转换和图像质量应作为不同阶段单独定义。如果运行时最终要求 RGB,应先让 plane 读值通过自己的夹具,再记录并验证后续转换契约。报告应写明 API 层、CameraX 版本、请求的输出、观察到的格式、尺寸、每个 plane 的 position/limit/stride 和夹具版本;不支持或未文档化的原点行为保持未知。
预览正常,但 CPU 采样图出现色度条纹
Android 视觉原型在 CPU 上读取 CameraX 分析帧。预览看起来正常,诊断采样网格却重复色度值,并偶尔在最后一行失败。当前实现假设偏移只由宽度决定,并在读取前调用 rewind。
决策
把每个 plane 视为独立且有边界的字节视图,先证明确切 API 的缓冲起点契约,再用合成夹具比较 stride-aware 样本;在此之前不改颜色转换或模型代码。
预期结果
团队可区分寻址、缓冲界限、输出格式与后续转换阶段的首次偏差。夹具通过只代表算术证据,不代表设备支持、模型接受或颜色准确率。
需要收集的证据
只有将以下证据与同一模型文件和测试运行关联起来,结论才可复核:
- ANDROID-YUV-PLANE-2026-10:官方事实卡记录 YUV_420_888 三平面、字节 stride 和缓冲边界;版本特定的原点合同不等于 Cove 当前实现。
- ANDROID-CAMERAX-ANALYSIS-2026-01:CameraX 分析的输出格式、旋转元数据与 ImageProxy 关闭边界;不证明具体设备的 plane 字节布局。
- COVE-OCR-01:Cove Photo 的可见文本输出属于模型/产品路径事实,不代表已经接入此 CPU plane 读取方法。
任务质量需要有版本记录的语料和验收规则;流畅的单条样例不等于准确度结果。
实施流程
- 固定帧 API 与输出格式. 记录 CameraX 版本、配置的 setOutputImageFormat、实际 ImageProxy 格式和尺寸,以及 plane 是否可由 CPU 访问。NV21 从 1.5.0 起提供,PRIVATE 从 1.7.0-alpha03 起提供;PRIVATE 要按该版本查询设备能力。成对使用 ImageProxy 元数据与 PlaneProxy 数据;PRIVATE 或意外格式应在索引前拒绝。
- 读取 plane 元数据但不改源游标. 每个 plane 记录取得缓冲区时的 position 和 limit、rowStride、pixelStride 及预期尺寸。用 duplicate 读取,让相对操作不改变源游标。根据准确 API 文档证明哪个索引是 plane 起点;文档或受控适配器无法证明时,将绝对寻址标为不可用。
- 逐样本计算并检查界限. row/column 是样本计数,rowStride、pixelStride、base 和最终索引才用字节计。使用溢出检查计算字节索引,并按当前视图范围校验最后一行的最后一个样本。clear 将 position 设为零并把 limit 扩至 capacity;rewind 只重置 position、丢弃 mark 且保留 limit。二者都不能证明 plane 原点;在 duplicate 中保留 API 规定的窗口。
- 分别映射亮度和色度坐标. 尊重 Y、U、V 平面顺序及各自 stride。同一全图原点下 chroma 坐标是 floor(fullX / 2)、floor(fullY / 2)。crop-local 坐标要先加已验证的 crop 起点再取整除;U/V stride 成对测试,不假设 NV21 顺序或共享存储。
- 覆盖 crop 和失败边界. 若帧提供 cropRect,将其保留为全分辨率有效区域。例如 cropLeft 1 加 localX 1 先得到 fullX 2,再得到 chromaX 1,不是先算 floor(localX / 2) 得到 0。把裁切边缘、limit 截断、负数或溢出偏移、plane 数异常及不支持格式作为明确拒绝用例。
- 先比较合成夹具,再观察设备. 创建每坐标唯一的微型平面,加入可见行填充哨兵、平面/交错式色度步长和精确到最后样本的 limit。断言字节和坐标,再在标明型号的设备上采集真实帧元数据;记录观察结果,不将其推广成通用布局或颜色结论。
让每个阶段的状态都可观察。失败信息应指出阶段、模型、运行时和恢复动作,同时避免记录用户私密内容。
验收标准
- 读取器先确认 YUV_420_888 和三平面结构,保留 Y/U/V 顺序,记录实际字节 stride,不从宽度推断打包存储。
- 每个绝对样本地址都有经 API 或适配器证明的 plane 基址,并以无溢出字节运算检查活动缓冲边界;row/column 仍是样本坐标,不依赖最后有效像素后的末行填充。
- duplicate 测试区分当前 position 与 plane 原点。clear 会把 limit 扩至 capacity;rewind 重置 position 但保留 limit。二者均不用于编造原点或丢弃生产者有效范围。
- CameraX 格式处理固定到库版本和实际 ImageProxy 输出,包含 NV21 proxy 字节与底层 Image 可能不同的情况。PRIVATE 或不可 CPU 访问帧由 CPU reader 拒绝;夹具成功不描述为硬件支持、模型接受或颜色准确度。
决策对照表
| 维度 | 选择或证据 | 解读方式 |
|---|---|---|
| 按宽度推导的紧凑偏移 | 假设每行是 width 字节,色度连续排列 | 写起来简单,但忽略行填充、pixelStride、独立 plane 边界和灵活 YUV 布局。 |
| 依元数据读取 plane | 使用已验证基址、字节 row/pixel stride 和缓冲 limit | 地址与界限假设可测试;本身仍不能证明 RGB 转换或模型输入支持。 |
| CameraX NV21 输出 | 使用 1.5.0 起提供的配置并从一致的 ImageProxy 读取 | Proxy 仍报告 YUV_420_888、数据却为 NV21;底层 Image 可能保留另一布局,不能混读。 |
| CameraX PRIVATE 输出 | 仅在固定版本与设备能力支持时用于非 CPU 路径 | PRIVATE 从 1.7.0-alpha03 起提供,不支持 CPU 读取且 getPlanes() 为空;不能外推到更早版本。 |
需要避免的失败模式
- 把 YUV_420_888 当作宽度决定的单块 I420 或 NV21 连续数组。
- 假设 framework Image.Plane 的 ByteBuffer.position 永远为零或必然是稳定 plane 原点。
- 按 rowStride 读取每一整行,包括没有映射保证的末行尾部填充。
还要避免静默回退、版本未固定、缺少取消处理,以及把不同配置的结果混在一起下结论。失败运行也应与成功记录一并保存。
最小复现记录
| 层级 | 记录内容 |
|---|---|
| 设备 | 厂商、型号、芯片、内存档位、操作系统版本 |
| 软件 | 应用版本和 Git 提交号 |
| 模型 | 系列、变体、版本、格式、文件长度、摘要、量化方式 |
| 运行时 | 名称、版本、请求后端及实际后端证据 |
| 工作负载 | 样例版本、输入摘要、提示词摘要、输出策略 |
| 结果 | 完成、失败、取消、回退及保护隐私的诊断信息 |
发布检查表
- ☐ 主要搜索问题已回答,且没有未经支持的数字。
- ☐ 所有模型文件和运行时版本均已固定。
- ☐ 代表性任务和失败处理策略均有说明。
- ☐ 生命周期、取消、清理和回退均已测试。
- ☐ 用户内容与网络边界均有说明。
- ☐ 结论只适用于被记录的配置。
- ☐ 新增测试结果时链接到原始方法或证据。
来源与相关证据
- Android ImageFormat API:YUV_420_888 平面顺序与布局
- Android Image.Plane API:buffer、rowStride 与 pixelStride
- AndroidX CameraX ImageAnalysis.Builder API:版本化输出格式
- AndroidX CameraX ImageAnalysis API:NV21 Proxy 与底层 Image 差异
- AndroidX CameraX ImageAnalysis.Analyzer API:PRIVATE CPU 访问限制
- Android Buffer API:position、limit 与读取操作
- AndroidX Camera ImagePlane API:特定版本的缓冲范围契约
中文部署与排错专题见。奇连 AI 端侧专题。
常见问题
能否用 width * height 把三个平面复制成一个 NV21 数组?
不能仅凭 YUV_420_888 元数据这样做。它可描述带各自行距和像素步长的平面或半平面色度;若消费者需要 NV21,应检查真实缓冲并另行定义重打包契约。
ByteBuffer.position 一定是 Android Image.Plane 的起点吗?
不要把它当成通用 framework 契约。position 是可变的相对读取游标;平台 Plane 文档没有像较新的 AndroidX ImagePlane alpha 接口那样明确承诺起点。固定准确 API 后再验证基址。
clear() 和 rewind() 对 ByteBuffer 的作用一样吗?
不一样。clear() 将 position 设为零、limit 设为 capacity;rewind() 将 position 设为零并保留 limit,同时丢弃 mark。二者都不能证明 plane 原点;用 duplicate 保留生产者有效窗口并独立移动游标。
每一行都可以读取 rowStride 个字节吗?
不可以。framework 文档说明末行后的 stride 可能未映射。只读取最后有效像素范围内的样本,不要访问假设存在的尾部填充。
如果夹具能读出预期 Y/U/V 值,是否证明相机或模型支持?
不能。它只证明该夹具布局下的索引算术。设备输出、CameraX 配置、颜色转换、运行时接受能力和任务质量都需要各自证据。