пятница, 20 июля 2012 г.

Распознаём QR-код в Android-приложении

Ранее я писал о том, как сгенерировать QR-код в Android-приложении. Для полноты картины не хватает ещё примера, как распознать QR-код. Простое решение для этого есть: вызвать с помощью Intent какое-нибудь из "посторонних" приложений, использующих ZXing. Эта библиотека предоставляет набор классов и ресурсов для отображения Activity c preview, самостоятельно распознаёт код и возвращает его в ваш метод onActivityResult.
Но как быть, если мы не хотим раскручивать чужие приложения? И встраивать к себе Activity и килограмм ресурсов из клиентской библиотеки ZXing не хочется... Выход есть. Мы можем самостоятельно реализовать всё что нам нужно, используя только ZXing core библиотеку. Давайте посмотрим, как это сделать.

Вначале нам нужно просто получить изображение с камеры. При чём делать это мы будем не по клику как в примере работы с камерой, а непрерывно. Для этого наше Activity должно интерфейс Camera.PreviewCallback и в методе onPreviewFrame мы сможем работать с preview-картинками в реальном времени. Чтобы распознавание кода не привело к тормозам в интерфейсе, каждую полученную картинку будем обрабатывать в отдельном потоке. Тот поток, который успешно распознал код, должен вызывать переход на другое Activity, где и будем отображать полученную из QR-кода строку. Если несколько потоков распознают картинку, отображать результат должен только первый. Для этого используем синхронизацию. Вот код нашего Activity:

import android.app.Activity;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.Size;
import android.os.Bundle;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup.LayoutParams;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import com.google.zxing.*;
import com.google.zxing.common.HybridBinarizer;
 
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
public class QKActivity extends Activity implements SurfaceHolder.Callback, Camera.PreviewCallback, Camera.AutoFocusCallback {
    private Camera camera;
    private SurfaceView preview;
    private ViewfinderView vfv;
    private Result rawResult;
    private String TAG = QKActivity.class.getSimpleName();
    private long currKey;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
 
        FrameLayout fl = new FrameLayout(this);
 
        preview = new SurfaceView(this);
        SurfaceHolder surfaceHolder = preview.getHolder();
        surfaceHolder.addCallback(this);
        fl.addView(preview, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        vfv = new ViewfinderView(this, null);
        fl.addView(vfv, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
        setContentView(fl);
    }
 
    @Override
    protected void onResume() {
        super.onResume();
        camera = Camera.open();
        vfv.setCamera(camera);
        currKey = System.currentTimeMillis();
    }
 
    @Override
    protected void onPause() {
        super.onPause();
        if (camera != null) {
            camera.setPreviewCallback(null);
            camera.stopPreview();
            camera.release();
            camera = null;
        }
    }
 
    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}
 
    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        try {
            camera.setPreviewDisplay(holder);
            camera.setPreviewCallback(this);
        } catch (IOException e) {
            e.printStackTrace();
        }
 
        Size previewSize = camera.getParameters().getPreviewSize();
        float aspect = (float) previewSize.width / previewSize.height;
        int previewSurfaceWidth = preview.getWidth();
        LayoutParams lp = preview.getLayoutParams();
        Camera.Parameters parameters = camera.getParameters();
        parameters.set("orientation", "landscape");
        camera.setParameters(parameters);
        lp.width = previewSurfaceWidth;
        lp.height = (int) (previewSurfaceWidth / aspect);
        preview.setLayoutParams(lp);
        try {
            camera.autoFocus(this);
        } catch (Exception e) {
            e.printStackTrace();
        }
        camera.startPreview();
    }
 
    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
    }
 
    @Override
    public void onPreviewFrame(final byte[] bytes, final Camera camera) {
        new Recognizer(currKey, bytes).start();
    }
 
    @Override
    public void onAutoFocus(boolean b, Camera cam) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (camera!=null && (Camera.Parameters.FOCUS_MODE_AUTO.equals(camera.getParameters().getFocusMode()) ||
                        Camera.Parameters.FOCUS_MODE_MACRO.equals(camera.getParameters().getFocusMode()))) {
                    camera.autoFocus(QKActivity.this);
                }
            }
        }).start();
    }
 
    public class Recognizer extends Thread {
 
        private long key;
        private byte[] bytes;
 
        public Recognizer(long key, byte[] bytes) {
            this.key = key;
            this.bytes = bytes;
        }
 
        @Override
        public void run() {
            try {
                Size previewSize = camera.getParameters().getPreviewSize();
                Rect rect = vfv.getFramingRectInPreview();
                LuminanceSource source = new PlanarYUVLuminanceSource(bytes, previewSize.width, previewSize.height, rect.left, rect.top,
                        rect.width(), rect.height(), false);
 
                Map<DecodeHintType,Object> hints = new HashMap<DecodeHintType, Object>();
                Vector<BarcodeFormat> decodeFormats = new Vector<BarcodeFormat>(1);
                decodeFormats.add(BarcodeFormat.QR_CODE);
                hints.put(DecodeHintType.POSSIBLE_FORMATS, decodeFormats);
                hints.put(DecodeHintType.NEED_RESULT_POINT_CALLBACK, new ResultPointCallback() {
                    @Override
                    public void foundPossibleResultPoint(ResultPoint resultPoint) {
                        vfv.addPossibleResultPoint(resultPoint);
                    }
                });
                MultiFormatReader mfr = new MultiFormatReader();
                mfr.setHints(hints);
                BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
                rawResult = mfr.decodeWithState(bitmap);
                if (rawResult!=null) {
                    Log.e(TAG, rawResult.getText()+" key="+key+" currKey="+currKey);
                    if (key==currKey) {
                        currKey = System.currentTimeMillis();
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                Intent i = new Intent(QKActivity.this, ResultActivity.class);
                                i.putExtra(ResultActivity.RESULT, rawResult.getText());
                                i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
                                startActivity(i);
                            }
                        });
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

Ещё одна "хитрость": мы выполняем автофокусировку камеры каждые 2 секунды, что позволяет улучшить "прицеливание".
Полный набор исходников в виде рабочего проекта доступен на github.

UPD (2.10.2013): Проект обновлён, если у вас были проблемы с распознаванием или с повторным распознаванием после отображения результата, проверьте. И, кстати, спасибо всем за замечания.

30 комментариев:

  1. Учусь, пишу:
    LuminanceSource source = new LuminanceSource(w,h);
    получаю ошибку "Cannot instantiate the type LuminanceSource".

    Что я делаю не так? Как-то не так подключил zxing?
    где рыть?

    ОтветитьУдалить
  2. А у меня получилось запустить но код не определяет просто запускается камера и все....

    ОтветитьУдалить
  3. было бы хорошо иметь готовые проекты того что вы делаете. т.е скачивание рабочего примера. Это поможет таким новичкам как я

    ОтветитьУдалить
  4. Спасибо за Ваши публикации, очень полезно и интересно. У меня такая же проблема с LuminanceSourceImpl компилятор пишет cannot be resolved to a type. Ответьте, пожалуйста, очень нужно.
    Спасибо.

    ОтветитьУдалить
  5. Этот комментарий был удален администратором блога.

    ОтветитьУдалить
  6. Загрузил, все работает. Автору спасибо.
    Господа, у кого не работает - возможно, Вы не подключили к проекту библиотеку Zxing?
    В Eclipse подключал вот так:
    Project -> Propertires -> Java Build Path -> Add external jars -> core.jar из zxing

    Вопрос: как перенастроить на распознование кодов EAN-13?

    ОтветитьУдалить
  7. А как перделать его впортретный и не поноэкранный?

    ОтветитьУдалить
  8. Уйти от полноэкранного варианта удалось,но вот к портретному виду никак.
    что нужно изменить в коде, чтоб в портретном виде работало?

    ОтветитьУдалить
  9. не работает, мучаюсь, приложение вылетает и закрывается(

    ОтветитьУдалить
  10. Приложение работает, только когда проверяю на своем устройстве (HTC ONE V) сканирование происходит только один раз, то есть если вернутся обратно в камеру, повторного сканирования не будет. И точки не на том месте где нужно.

    ОтветитьУдалить
  11. Запустилось норм но не распознает ни один qr код....

    ОтветитьУдалить
  12. После того как закомментил 3 строчки:
    // if (left + width > dataWidth || top + height > dataHeight) {
    // throw new IllegalArgumentException("Crop rectangle does not fit within image data.");
    // }
    приложение стало сканировать и распознавать. Но только один код. Потом надо перезапускать. Картина в точности как у Артема.

    ОтветитьУдалить
  13. Ошибка в автофокусе! Сначала нужно запустить превью, а затем вызывать автофокус:
    camera.startPreview();
    { try
    {
    camera.autoFocus(this);
    }
    catch (Exception e)
    {
    e.printStackTrace();
    }

    ОтветитьУдалить
    Ответы
    1. Спасибо, не мог понять почему автофокус не работает

      Удалить
  14. Почему на Adndoid 2.3 не работает, приложение сразу падает ((( А вот на Adndoid 4.1.2 работает хорошо

    ОтветитьУдалить
  15. Может знает кто-нибудь, как исправить?

    ОтветитьУдалить
    Ответы
    1. Попробуйте в методе onCreate добавить строку surfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

      Удалить
  16. Еще одна ошибка при работе с камерой под Android 2.1 getClientFromCookie: client appears to have died Никто не в курсе что это может означать?

    ОтветитьУдалить
  17. Samsung p3100 при распозновании нужно чтобы изображение попадало в правый нижний угол, только тогда нормально распознается. Желтые точки ресуются со смещением от реалной точки примерно -50px по X и Y.

    ОтветитьУдалить
  18. камера запускается и все...точки ставит не в тех местах...не распознает:( если кто-то решил проблему и все нормально работает, то пришлите плиз исходники, очень надо!

    ОтветитьУдалить
  19. только если в правом нижнем углу код- тогда распознает

    ОтветитьУдалить
  20. Этот комментарий был удален автором.

    ОтветитьУдалить
  21. Нормально работает в эмуляторе Genymotion. Вот только нужно добавить в манифест после этого:

    uses-permission android:name="android.permission.CAMERA"

    вот это:

    uses-feature android:name="android.hardware.camera"
    uses-feature android:name="android.hardware.camera.autofocus"

    Соответственно в скобочках) Тут удаляет(

    И мне кажется, что библиотека zbar быстрее работает. Правда там нет этих точек(в странных местах, которые в принципе можно закоментить тут) и осветления экрана и прямоугольной области по средине... =) Наверно можно вместе соединить.

    ОтветитьУдалить
  22. Не фокусируется камера. Точки желтые бегают но ничего не происходит

    ОтветитьУдалить
  23. не могу разобратся с библиоткой Camera. вызываю библиотеку а оно вычеркнута

    ОтветитьУдалить
  24. а если камера на устройстве без автофокусировки, будет ли работать программа или сразу вылетит??

    ОтветитьУдалить