Topics
Recent articles

Android & Mobile

CameraX ZXing YUV rowStride pixelStride crop QR scanner

Learn how row stride, pixel stride, crop bounds, and rotation interact when feeding CameraX YUV frames into ZXing, and ensure proper proxy lifecycle management.

Table of Contents5 sections
A fluffy orange cat rests beside a laptop showing a code editor, creating a cozy work-from-home setting.
A fluffy orange cat rests beside a laptop showing a code editor, creating a cozy work-from-home setting.

When you integrate CameraX with the ZXing decoding library for real-time barcode scanning, a naive byte-array copy often fails because it assumes that image planes are tightly packed. Hardware camera buffers frequently include padding bytes at the end of each row. Ignoring these alignment offsets corrupts the luminance matrix and causes decoding failures or misaligned bounding boxes.

Quick solution: Read each row explicitly according to its row stride instead of performing a single bulk copy, and always wrap the resulting byte array in a custom luminance source before calling the decoder. For broader implementation context on real-time computer vision pipelines, you can also review how to configure Camerax Ml Kit Real Time Image safely.

public class PlanarYUVLuminanceSource extends LuminanceSource {
    private final byte[] yuvData;
    private final int dataWidth;
    private final int dataHeight;
    private final int left;
    private final int top;

    public PlanarYUVLuminanceSource(byte[] yuvData, int dataWidth, int dataHeight, int left, int top, int width, int height) {
        super(width, height);
        this.yuvData = yuvData;
        this.dataWidth = dataWidth;
        this.dataHeight = dataHeight;
        this.left = left;
        this.top = top;
    }

    @Override
    public byte[] getRow(int y, byte[] row) {
        if (y < 0 || y >= getHeight()) {
            throw new IllegalArgumentException("Requested row is outside the image: " + y);
        }
        int width = getWidth();
        if (row == null || row.length < width) {
            row = new byte[width];
        }
        int offset = (y + top) * dataWidth + left;
        System.arraycopy(yuvData, offset, row, 0, width);
        return row;
    }

    @Override
    public byte[] getMatrix() {
        int width = getWidth();
        int height = getHeight();
        byte[] resultat = new byte[width * height];
        int offset = top * dataWidth + left;
        if (width == dataWidth) {
            System.arraycopy(yuvData, offset, resultat, 0, width * height);
            return resultat;
        }
        for (int y = 0; y < height; y++) {
            int sourceOffset = (y + top) * dataWidth + left;
            System.arraycopy(yuvData, sourceOffset, resultat, y * width, width);
        }
        return resultat;
    }
}

How row stride and crop bounds affect luminance extraction

According to the official Android documentation on CameraX image analysis, frames produced by the camera subsystem are delivered as an ImageProxy containing multiple planes. Each plane abstracts a specific color channel. For YUV_420_888 format, the Y plane holds the luminance data. Developers must inspect the row stride because hardware constraints often require rows to align with specific byte boundaries, such as 32 or 64 bytes.

When you query the Android Image.Plane stride contract, you will find that getRowStride() frequently exceeds the visible image width. If your application copies the entire plane buffer into a flat array without skipping these padding bytes, every subsequent row will shift horizontally. This misalignment shears the image data and renders standard barcode patterns unrecognizable to decoding algorithms.

Furthermore, crop bounds interact directly with these stride offsets. When you apply a bounding rectangle to isolate a region of interest in the viewfinder, your code must calculate offsets using the stride width rather than the visible width. Neglecting the stride when calculating starting indices means your crop window points to arbitrary memory locations outside your intended target.

When is rotation applied to decoding coordinates?

Image rotation introduces another layer of complexity during coordinate mapping. Camera sensors capture frames in a fixed hardware orientation, whereas the UI display rotates dynamically based on device rotation sensors. When the analysis executor receives an ImageProxy, the rotation degrees indicate how much the frame must be rotated to match the current UI orientation.

Decoding libraries such as ZXing expect upright matrices. If you rotate the image buffer directly in software before extraction, you incur heavy CPU penalties. The more efficient approach is to let the luminance source handle coordinate transformations on demand or to rotate the bounding box coordinates post-decode. Developers must ensure that rotation degrees are factored into any coordinate scaling calculations when mapping detected barcode points back to the preview view.

Who closes the proxy on every failure path?

Resource leaks are a primary source of camera freezes and application crashes during continuous image analysis. An ImageProxy holds a reference to an underlying native buffer that must be closed explicitly to allow the camera pipeline to produce the next frame. If your analysis code throws an exception during luminance extraction or barcode scanning, the proxy can remain unreleased.

To prevent camera stalls, wrap your processing logic in a try-finally block. The analysis callback must always invoke imageProxy.close() in the finally block regardless of whether decoding succeeded, threw an exception, or found no barcodes. Never close the underlying Media.Image directly outside of the proxy wrapper lifecycle contract provided by CameraX.

@Override
public void analyze(@NonNull ImageProxy imageProxy) {
    try {
        ByteBuffer buffer = imageProxy.getPlanes()[0].getBuffer();
        byte[] data = new byte[buffer.remaining()];
        buffer.get(data);
        
        PlanarYUVLuminanceSource source = new PlanarYUVLuminanceSource(
            data,
            imageProxy.getPlanes()[0].getRowStride(),
            imageProxy.getHeight(),
            0,
            0,
            imageProxy.getWidth(),
            imageProxy.getHeight()
        );
        
        // Pass source to ZXing decoder implementation
        // Reference: https://github.com/zxing/zxing/blob/master/core/src/main/java/com/google/zxing/PlanarYUVLuminanceSource.java
        processDecoding(source);
        
    } catch (Exception e) {
        handleDecodingFailure(e);
    } finally {
        imageProxy.close();
    }
}

Verification and execution checklist

Continue Exploring

You Might Also Like

View all articles
Android elapsedRealtime and Monotonic Deadlines
4 min read

Android elapsedRealtime and Monotonic Deadlines

Learn how to use Android elapsedRealtime and Kotlin cancellation to reject late coroutine responses during startup health checks, preventing stale data from overwriting UI state.

Fix Firebase Variant CI Configuration
3 min read

Fix Firebase Variant CI Configuration

Learn how the Google Services Gradle plugin matches build variants to configuration files, where variant-specific JSON files live, and how to verify that your CI pipeline exercises the intended Firebase project.