Task architecture

Read CameraX YUV Planes Within Verified Buffer Bounds

Validate byte addressing, plane origins, chroma coordinates and final-pixel bounds for the actual frame API and configured output.

Review status: Fact and editorial QA complete; owner review pending · 2026-10-08 · Fact IDs: ANDROID-CAMERAX-ANALYSIS-2026-01, COVE-OCR-01, ANDROID-YUV-PLANE-2026-10

Direct answer

ImageFormat.YUV_420_888 has existed since API 19. It is a flexible three-plane format, not a promise that one contiguous byte array contains an I420 or NV21 frame. Android specifies plane order as Y, U (Cb), V (Cr), guarantees a non-interleaved Y plane with pixelStride 1, and guarantees the U and V planes share rowStride and pixelStride. The chroma planes represent subsampled data. Each plane still has its own buffer and byte strides, so read samples from the actual plane metadata instead of deriving offsets from width alone. For a single plane, a candidate index is planeBase + row * rowStride + column * pixelStride. Row and column are sample coordinates; base, strides, and the resulting index are byte offsets. Use the expression only after planeBase and coordinate origin are established for the exact API and frame. Do not assume the returned ByteBuffer position is always zero, and do not treat its current position as a permanent plane origin after some code has consumed bytes. java.nio.Buffer defines position as the next relative-read cursor and limit as the first index that must not be read. A duplicate gives independent cursor state while sharing the same underlying bytes; it does not prove where image data begins. The framework Image.Plane documentation describes a direct ByteBuffer containing frame data and its row and pixel strides, but does not define a universal nonzero position as the plane base. AndroidX CameraX ImagePlane documentation added in 1.7.0-alpha03 says that interface sets position to plane data start and limit to plane data end. That version-specific AndroidX statement is not a general android.media.Image.Plane guarantee. Pin the actual output API and version, use only its documented origin rule, and otherwise keep the base unproven rather than manufacturing offsets. Bounds checks must target the last byte actually sampled, not an assumed allocation of rowStride multiplied by height. Android warns that raw plane buffers may be mapped only through the last pixel in the last row; trailing padding after that pixel is not guaranteed to exist. For each sample, validate the computed byte index against the verified base and current limit with overflow-safe arithmetic. Buffer.clear sets position to zero and limit to capacity, which can expose bytes outside the producer's valid window. Buffer.rewind sets position to zero and discards the mark but leaves limit unchanged; it can still make code read from a false origin. Use neither to repair an unproven base or range; use a duplicate and preserve the documented window. Use one image abstraction consistently. With a CameraX ImageProxy, read its format, dimensions, crop rectangle, and PlaneProxy array as one frame contract, and close the proxy when finished; do not close its wrapped android.media.Image yourself. Do not combine ImageProxy dimensions/crop with planes from the wrapped Image. Pin the exact CameraX API: YUV_420_888 and RGBA_8888 output options date from 1.1.0, NV21 from 1.5.0, and PRIVATE from 1.7.0-alpha03. NV21 output keeps the ImageProxy format value YUV_420_888 but formats the proxy data as NV21. Some devices may expose an underlying getImage() with different planes or dimensions, so keep proxy metadata and proxy planes together. PRIVATE was added in 1.7.0-alpha03, requires that version's output-capability check, is not CPU accessible, and returns an empty getPlanes() array; do not assume that option or capability exists in older dependencies or in Cove. Check the actual frame format before entering a CPU reader. Make arithmetic visible with synthetic plane fixtures. One fixture can use a 4 by 4 Y plane with base 0, rowStride 6, pixelStride 1, and limit 22: sample Y(3,3) is byte 21, and there are no mapped tail bytes after it. Give separate synthetic 2 by 2 U and V buffers base 0, rowStride 6, pixelStride 2, limit 9: sample (1,1) in each is byte 8. These independent arrays test arithmetic only; they do not prove real U/V buffers share or interleave storage. Also vary legal U/V stride pairs, add row-padding sentinels, and test truncated limits. Assert sample addresses and read ranges, not device-specific YUV-to-RGB appearance. Map crop-local coordinates in a defined order. With the same full-image origin, chroma sample coordinates are floor(fullX / 2), floor(fullY / 2). If the valid crop starts at cropLeft/cropTop, first add those offsets to the crop-local coordinate, then divide by two. For example, cropLeft 1 plus localX 1 gives fullX 2 and chromaX 1; flooring localX first would incorrectly give 0. This is coordinate accounting only, not a rotation or EXIF procedure. This is a CPU plane-addressing method, not a claim about Cove's current capture path or a recommendation to replace CameraX output settings. Keep rotation, transfer functions, RGB conversion, and image quality as separately specified stages. If the selected runtime instead requires RGB, document and test a real conversion contract after plane reads have passed their own fixtures. Report the exact API layer, CameraX version, requested output, observed ImageProxy format, dimensions, per-plane position/limit/strides, and test fixture revision; leave unsupported or undocumented origin behavior unknown.

A preview looks correct while sampled camera pixels are striped

An Android vision prototype reads a CameraX analysis frame on the CPU. The preview appears normal, but a diagnostic sampling grid repeats chroma values and occasionally fails on the final row. The implementation currently assumes width-based contiguous offsets and calls rewind before reading.

Decision

Treat each plane as an independently bounded byte view, establish the exact buffer-origin contract, and compare stride-aware samples against synthetic fixtures before changing any color conversion or model code.

Expected outcome

The team can identify whether addressing, buffer bounds, output format, or a later conversion stage first diverges. A fixture pass remains arithmetic evidence, not proof of camera support, model acceptance, or color accuracy.

Evidence to collect

The claim becomes reviewable only when the following evidence is attached to the same artifact and test run:

  • Verified per-plane origin and byte strides
  • Padding and truncated-limit synthetic fixtures
  • Version-specific output and proxy identity

Task quality needs a versioned corpus and acceptance rules. Fluent sample output is not an accuracy result.

Implementation workflow

  1. Pin the frame API and output format. Record the CameraX version, configured setOutputImageFormat value, actual ImageProxy format and dimensions, and whether CPU planes are available. NV21 was added in 1.5.0; PRIVATE and its output-capability query were added in 1.7.0-alpha03. Use those options only when the pinned dependency exposes them and the device reports support. Keep ImageProxy metadata and PlaneProxy data paired; reject PRIVATE or unexpected formats before indexing.
  2. Snapshot plane metadata without changing the source cursor. For each plane record buffer position and limit at acquisition, rowStride, pixelStride, and expected plane dimensions. Use a duplicate for reads so relative operations do not mutate the source cursor. Prove which index marks plane origin for the exact documented API; if documentation or a controlled adapter cannot establish it, mark absolute addressing unavailable.
  3. Compute and guard one sample at a time. Treat row and column as sample counts; rowStride, pixelStride, base, and final index are byte offsets. Use checked arithmetic and validate the computed byte index against the chosen view's valid range, including the final sample in the last row. clear sets position to zero and limit to capacity; rewind sets position to zero but preserves limit and discards the mark. Neither establishes a plane origin, so preserve the documented window in a duplicate.
  4. Map luma and chroma coordinates separately. Respect Y, U, V plane order and each plane's own row and pixel stride. Under a shared full-image origin, map chroma as floor(fullX / 2), floor(fullY / 2). For a crop-local coordinate, add the verified crop origin first, then divide; test U/V strides as a matching pair without assuming NV21 order or shared storage.
  5. Exercise crop and failure boundaries. If the frame exposes a crop rectangle, preserve it as the valid region in full-resolution coordinates. For example cropLeft 1 plus localX 1 maps to fullX 2 and chromaX 1, not floor(localX / 2) = 0. Test out-of-range crop edges, truncated limits, negative or overflowing offsets, unexpected plane counts, and unsupported formats as explicit rejection cases.
  6. Compare synthetic fixtures before device observations. Create tiny planes with unique per-coordinate values, visible row-padding sentinels, planar and interleaved-style chroma strides, and an exact final-sample limit. Assert bytes read and coordinates. Only then capture real frame metadata on named devices; record observations without turning them into universal layout or color claims.

Keep each transition observable. A failure should identify the stage, artifact, runtime, and recovery action without logging private user content.

Acceptance criteria

  • The reader verifies YUV_420_888 and three planes, preserves the documented Y/U/V order, and records actual per-plane byte strides instead of inferring packed storage from width.
  • Every absolute sample address has an API- or adapter-verified plane base and is checked against the active buffer bounds with overflow-safe byte arithmetic; row and column remain sample coordinates, and no code relies on last-row padding beyond the final valid pixel.
  • Duplicate-buffer tests distinguish cursor position from plane origin. clear is known to expand limit to capacity; rewind resets position but preserves limit. Neither is used to invent an origin or discard the producer's valid range.
  • CameraX format handling is pinned to a library version and the actual ImageProxy output, including NV21-specific proxy data versus a potentially different wrapped Image. PRIVATE/non-CPU-accessible frames are rejected by CPU readers; its 1.7.0-alpha03 option and device-capability check are not assumed available in earlier versions or in an unpinned dependency. Fixture success is not described as hardware support, model acceptance, or color accuracy.

Decision table

DimensionChoice or evidenceHow to interpret it
Width-derived packed offsetAssume each plane row is width bytes and chroma is contiguousFast to write but ignores row padding, pixel stride, independent plane bounds, and flexible YUV layout.
Metadata-driven plane readUse verified base plus byte row/pixel strides and per-buffer limitMakes address and bounds assumptions testable; still says nothing by itself about RGB conversion or model input support.
CameraX NV21 outputUse the 1.5.0+ option and read a consistent ImageProxyThe proxy still reports YUV_420_888 but its bytes are NV21; the wrapped Image may differ on some devices.
CameraX PRIVATE outputUse only when the pinned 1.7.0-alpha03+ API and device capability allow a non-CPU pathIt is not CPU accessible and getPlanes() is empty; do not invoke the option through an older dependency.

Failure patterns to prevent

  • Assuming packed NV21 or I420 layout
  • Inventing plane origins with rewind
  • Reading unmapped final-row padding

Also prevent silent fallback, unpinned artifacts, missing cancellation, and conclusions that combine unlike configurations. Store unsuccessful runs alongside successful ones.

Minimum reproducibility record

LayerRecord
DeviceManufacturer, model, chipset, RAM class, operating-system build
SoftwareApplication version and git commit
ModelFamily, variant, revision, format, file length, hash, quantization
RuntimeName, revision, requested backend, observed backend evidence
WorkloadFixture revision, input hash, prompt hash, output policy
OutcomeCompleted, failed, cancelled, fallback, and privacy-safe diagnostics

Release checklist

  • ☐ The primary query is answered without an unsupported number.
  • ☐ Every artifact and runtime is pinned.
  • ☐ The representative task and failure policy are explicit.
  • ☐ Lifecycle, cancellation, cleanup, and fallback are tested.
  • ☐ User-content and network boundaries are documented.
  • ☐ Result wording applies only to the recorded configuration.
  • ☐ The page links to raw method or evidence when results are added.

Sources and related evidence

Chinese deployment and troubleshooting content is organized in the .奇连 AI 端侧专题.

Frequently asked questions

Can I copy all three planes into one NV21 array by using width times height?

Not from YUV_420_888 metadata alone. It may describe planar or semiplanar chroma with per-plane row and pixel strides; inspect the actual buffers and define a separate repacking contract if a consumer requires NV21.

Does ByteBuffer.position always identify the start of an Android Image.Plane?

Do not assume that as a universal framework contract. Buffer position is a mutable relative-read cursor; the platform Plane documentation does not give it the same explicit origin promise as the newer AndroidX ImagePlane alpha interface. Pin and verify the exact API before using a base offset.

Do clear() and rewind() change a ByteBuffer the same way?

No. clear() sets position to zero and limit to capacity; rewind() sets position to zero and preserves limit while discarding the mark. Neither proves a plane origin. Keep the producer's valid window and use a duplicate for independent cursor state.

May I read rowStride bytes for every row?

No. The framework documents that the stride after the last row may not be mapped. Bounds-check each sample through the last valid pixel, and do not read hypothetical trailing padding.

If my fixture returns the expected Y, U, and V values, does that prove camera or model support?

No. It proves the tested arithmetic for the fixture layout. Device output, configured CameraX format, color conversion, runtime acceptance, and task quality need separate evidence.