High-Speed Barcode and QR Code Detection on Android Using Camera2 API

Imagine you’re using barcode technology to scan parcels on a fast-moving logistics conveyor belt. A common challenge is recognizing barcodes from blurred images caused by motion. While advanced algorithms can mitigate this issue, enhancing image quality is a more effective first step. One straightforward solution is to adjust the camera’s shutter speed, also known as exposure time. By increasing the shutter speed, you can minimize motion blur. In this article, I’ll demonstrate how to use Android Camera2 APIs to control shutter speed and build a simple Android barcode reader that can effectively decode barcodes from fast-moving objects.

Prerequisites

Tuning Shutter Speed with Camera2 API for Barcode Detection

Building a basic Android Camera2 app doesn’t require starting from scratch. You can download or fork the android-Camera2Basic sample code provided by Google.

Intializing Dynamsoft Barcode Reader

In Camera2BasicFragment.java, initialize the Dynamsoft Barcode Reader SDK in the onViewCreated method:

try {
    BarcodeReader.initLicense(
            "LICENSE-KEY",
            new DBRLicenseVerificationListener() {
                @Override
                public void DBRLicenseVerificationCallback(boolean isSuccessful, Exception e) {
                }
            });
    mBarcodeReader = new BarcodeReader();
}
catch (Exception e) {
    throw new RuntimeException(e);
}

Setting Up Shutter Speed Selection

To allow users to select the shutter speed, create an AlertDialog with a list view:

Activity activity = getActivity();
if (null != activity) {
    AlertDialog.Builder builder = new AlertDialog.Builder(activity);
    builder.setTitle("Shutter Speed");

    final ArrayAdapter<String> arrayAdapter = new ArrayAdapter<>(activity, android.R.layout.select\_dialog\_singlechoice);
    arrayAdapter.add("1/1000 s");
    arrayAdapter.add("1/500 s");
    arrayAdapter.add("1/250 s");
    arrayAdapter.add("1/125 s");
    arrayAdapter.add("1/100 s");

    builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            dialog.dismiss();
        }
    });

    builder.setAdapter(arrayAdapter, new DialogInterface.OnClickListener() {
        @Override
        public void onClick(DialogInterface dialog, int which) {
            mShutterSpeed = SHUTTER\_SPEED.get(which);
            startPreview();
        }
    });
    builder.show();
}

Configuring ImageReader

To capture images for barcode detection, get a list of compatible sizes for the YUV_420_888 image format and instantiate ImageReader with the chosen size:

Size outputPreviewSize = new Size(MAX_PREVIEW_WIDTH, MAX_PREVIEW_HEIGHT);
Size[] sizeList =  map.getOutputSizes(ImageFormat.YUV_420_888);
for (Size size : sizeList) {
    if(size.getWidth() * size.getHeight() > 1000000)
        continue;
    else{
        outputPreviewSize = size;
        break;
    }
}

mImageReader = ImageReader.newInstance(outputPreviewSize.getWidth(), outputPreviewSize.getHeight(),
        ImageFormat.YUV_420_888, 4);

Decoding Barcodes in ImageReader Callback

In the onImageAvailable() callback, retrieve the image buffer and use the decodeBuffer() function for barcode detection:

public void onImageAvailable(ImageReader reader) {
    Image image = reader.acquireLatestImage();
    if (image == null) return;

    ByteBuffer buffer = image.getPlanes()[0].getBuffer();
    byte[] bytes = new byte[buffer.remaining()];
    buffer.get(bytes);
    int nRowStride = image.getPlanes()[0].getRowStride();
    int nPixelStride = image.getPlanes()[0].getPixelStride();
    image.close();
    try {
        TextResult[] results = mBarcodeReader.decodeBuffer(bytes, mImageReader.getWidth(), mImageReader.getHeight(), nRowStride * nPixelStride, EnumImagePixelFormat.IPF_NV21);
        String output = "";
        if (results != null && results.length > 0) {
            for (TextResult result: results) {
                String format = result.barcodeFormatString;
                String text = result.barcodeText;

                output += "Format: " + format + ", Text: " + text;
            }
        }
        else {
            output = "";
        }

        Message msg = new Message();
        msg.what = MSG_UPDATE_TEXTVIEW;
        msg.obj = output;
        mMainHandler.sendMessage(msg);

    } catch (Exception e) {
        e.printStackTrace();
    }
}

Updating the UI on the Main Thread

Since the listener is triggered on a background thread, update the UI using the main thread handler:

mMainHandler = new Handler(){
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_UPDATE_TEXTVIEW:
                String result = (String)msg.obj;
                mTextView.setText((String)msg.obj);

                if (!result.equals("")) {
                    mToneGenerator.startTone(ToneGenerator.TONE_PROP_BEEP2);
                }

        }
    }
};

Adding ImageReader Surface to CaptureRequest

Include the ImageReader surface in the CaptureRequest.Builder:

mPreviewRequestBuilder.addTarget(mImageReader.getSurface());

Controlling Shutter Speed and Starting Preview

When the camera capture session is ready, start the preview and control the shutter speed by disabling auto-exposure:

private void startPreview() {
    try {
        // Auto focus should be continuous for camera preview.
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE,
                CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE);

        // Adjust the shutter speed
        mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF);
        mPreviewRequestBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, mShutterSpeed);

        // Finally, we start displaying the camera preview.
        mPreviewRequest = mPreviewRequestBuilder.build();
        mCaptureSession.setRepeatingRequest(mPreviewRequest,null,null);
    } catch (CameraAccessException e) {
        e.printStackTrace();
    }
}

Running the Barcode Reader

Now, build and run your simple Android barcode reader. If barcodes aren’t being detected from fast-moving objects, try adjusting the shutter speed:

Android barcode reader with Camera2 API

Note: Due to hardware limitations, some budget phones may produce lower-quality preview images when the shutter speed is set to 1/500 seconds or faster.

Source Code

https://github.com/yushulx/android-camera-barcode-mrz-document-scanner/tree/main/examples/9.x/shutter_speed