diff --git a/fast-barcode-scanner-demo/src/main/java/dk/schaumburgit/fastbarcodescannerdemo/MainActivity.java b/fast-barcode-scanner-demo/src/main/java/dk/schaumburgit/fastbarcodescannerdemo/MainActivity.java index fcb1ccc..f0b4bd1 100644 --- a/fast-barcode-scanner-demo/src/main/java/dk/schaumburgit/fastbarcodescannerdemo/MainActivity.java +++ b/fast-barcode-scanner-demo/src/main/java/dk/schaumburgit/fastbarcodescannerdemo/MainActivity.java @@ -89,14 +89,13 @@ private void startScan() { mScanner = new FastBarcodeScanner(this, (TextureView)null); //mScanner = new FastBarcodeScanner(this, mSurfaceView); //mScanner.setScanningStateListener(this); - mScanner.setBarcodeListener(this); } Button startButton = (Button)findViewById(R.id.start); Button stopButton = (Button)findViewById(R.id.button3); startButton.setEnabled(false); - mScanner.StartScan(); + mScanner.StartScan(this, null); stopButton.setEnabled(true); } @@ -149,6 +148,11 @@ public void run() { } } + @Override + public void onError(Exception error) { + + } + private static final int REQUEST_CAMERA_PERMISSION = 1; private static final String FRAGMENT_DIALOG = "dialog"; diff --git a/fast-barcode-scanner/src/main/java/dk/schaumburgit/fastbarcodescanner/FastBarcodeScanner.java b/fast-barcode-scanner/src/main/java/dk/schaumburgit/fastbarcodescanner/FastBarcodeScanner.java index d925c5d..39e8c52 100644 --- a/fast-barcode-scanner/src/main/java/dk/schaumburgit/fastbarcodescanner/FastBarcodeScanner.java +++ b/fast-barcode-scanner/src/main/java/dk/schaumburgit/fastbarcodescanner/FastBarcodeScanner.java @@ -17,27 +17,47 @@ import android.annotation.TargetApi; import android.app.Activity; -import android.hardware.camera2.CaptureResult; -import android.media.Image; +import android.os.Handler; +import android.os.HandlerThread; import android.util.Log; import android.view.SurfaceView; import android.view.TextureView; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; + import java.security.InvalidParameterException; import java.sql.Timestamp; import java.util.Date; +import java.util.EnumSet; import dk.schaumburgit.stillsequencecamera.IStillSequenceCamera; import dk.schaumburgit.stillsequencecamera.camera.StillSequenceCamera; import dk.schaumburgit.stillsequencecamera.camera2.StillSequenceCamera2; import dk.schaumburgit.trackingbarcodescanner.TrackingBarcodeScanner; +/** + * The FastBarcodeScanner captures images from your front-facing camera at the fastest + * possible rate, scans them for barcodes and reports any changes to the caller + * via a listener callback. + * + * The image capture is done unobtrusively without any visible UI, using a background thread. + * + * For newer Android versions (Lollipop and later), the new, faster Camera2 API is supported. + * For older versions, FastBarcodeScanner falls back to using the older, slower camera API. + * + * When the Camera2 API is available, the FastBarcodeScanner can be created with a TextureView + * if on-screen preview is desired, or without for headless operation. + * + * For older Android versions, the FastBarcodeScanner *must* be created with a SurfaceView, + * and the SurfaceView *must* be visible on-screen. Setting the SurfaceView to 1x1 pixel + * will however make it effectively invisible. + * + * Regardless of Android version, the FastbarcodeScanner *must* be supplied with a reference + * to the current Activity (used for accessing e.g. the camera, and other system resources). + * + */ public class FastBarcodeScanner - implements IStillSequenceCamera.OnImageAvailableListener//, IStillSequenceCamera.CameraStateChangeListener { /** * Tag for the {@link Log}. @@ -45,6 +65,10 @@ public class FastBarcodeScanner private static final String TAG = "FastBarcodeScanner"; private Activity mActivity; + private Handler mBarcodeListenerHandler; + private HandlerThread mProcessingThread; + private Handler mProcessingHandler; + private Activity getActivity() { return mActivity; } @@ -52,6 +76,26 @@ private Activity getActivity() { private IStillSequenceCamera mImageSource; private TrackingBarcodeScanner mBarcodeFinder; + /** + * Creates a headless FastBarcodeScanner (i.e. one without any UI) + * + * FastBarcodeScanner instances created using this constructor will use + * the new, efficient Camera2 API for controlling the camera. + * + * This boosts performance by a factor 5x - but it only works on Android + * Lollipop (API version 21) and later. + * + * As an alternative, consider using the #FastBarcodeScanner constructor + * which will create a FastBarcodeScanner working on older versions of + * Android too - albeit much less efficiently. + * @param activity Non null + */ + @TargetApi(21) + public FastBarcodeScanner(Activity activity) + { + this(activity, (TextureView)null); + } + /** * Creates a FastBarcodeScanner using the given TextureView for preview. * @@ -74,12 +118,19 @@ public FastBarcodeScanner(Activity activity, TextureView textureView) { this.mActivity = activity; this.mBarcodeFinder = new TrackingBarcodeScanner(); - this.mImageSource = new StillSequenceCamera2(activity, textureView, mBarcodeFinder.GetPreferredFormats(), 1024*768); + this.mImageSource = new StillSequenceCamera2(activity, textureView, mBarcodeFinder.getPreferredImageFormats(), 1024*768); this.mImageSource.setup(); } /** + * Creates a FastBarcodeScanner using the deprecated Camera API supported + * on Android versions prior to Lollipop (API level lower than 21). * + * The created FastBarcodeScanner will display preview output in the supplied + * SurfaceView. This parameter *must* be non-null, and the referenced SurfaceView + * *must* be displayed on-screen, with a minimum size of 1x1 pixels. This is a + * non-negotiable requirement from the camera API (upgrade to API level 21 for + * true headless operation). * @param activity Non-null * @param surfaceView Non-null */ @@ -96,23 +147,90 @@ public FastBarcodeScanner(Activity activity, SurfaceView surfaceView) { this.mImageSource.setup(); } - public void StartScan() + /** + * Starts scanning on a background thread, calling the supplied listener whenever + * there's a *change* in the barcode seen (i.e. if 200 consecutive images contain + * the same barcode, only the first will generate a callback). + * + * "No barcode" is signalled with a null value via the callback. + * + * Example: After StartScan is called, the first 20 images contain no barcode, the + * next 200 have barcode A, the next 20 have nothing. This will generate the + * following callbacks: + * + * Frame#1: onBarcodeAvailable(null) + * Frame#21: onBarcodeAvailable("A") + * Frame#221: onBarcodeAvailable(null) + * + * @param listener A reference to the listener receiving the above mentioned callbacks + * @param callbackHandler Identifies the thread that the callbacks will be made on. + * Null means "use the thread that called StartScan()". + */ + public void StartScan(BarcodeDetectedListener listener, Handler callbackHandler) { - mImageSource.start(this); + mBarcodeListenerHandler = callbackHandler; + if (mBarcodeListenerHandler == null) + mBarcodeListenerHandler = new Handler(); + mBarcodeListener = listener; + + mProcessingThread = new HandlerThread("FastBarcodeScanner processing thread"); + mProcessingThread.start(); + mProcessingHandler = new Handler(mProcessingThread.getLooper()); + + mImageSource.start( + new IStillSequenceCamera.OnImageAvailableListener() + { + + @Override + public void onImageAvailable(int format, byte[] data, int width, int height) { + processImage(format, data, width, height); + } + + @Override + public void onError(Exception error) { + FastBarcodeScanner.this.onError(error); + } + + }, + mProcessingHandler + ); } + /** + * Stops the scanning process started by StartScan() and frees any shared system resources + * (e.g. the camera). StartScan() can always be called to restart. + * + * StopScan() and StartScan() are thus well suited for use from the onPause() and onResume() + * handlers of a calling application. + */ public void StopScan() { mImageSource.stop(); + + if (mProcessingThread != null) { + try { + mProcessingThread.quitSafely(); + mProcessingThread.join(); + mProcessingThread = null; + mProcessingHandler = null; + } catch (Exception e) { + e.printStackTrace(); + } + } + + mBarcodeListener = null; + mBarcodeListenerHandler = null; } + /** + * Disposes irrevocably of all resources. This instance cannot be used after close() is called. + */ public void close() { this.mImageSource.close(); } - @Override - public void onImageAvailable(int format, byte[] bytes, int width, int height) { + private void processImage(int format, byte[] bytes, int width, int height) { // Get the image data (we requested JPEG) into a byte buffer: Date first = new Date(); @@ -120,11 +238,11 @@ public void onImageAvailable(int format, byte[] bytes, int width, int height) { Date second = new Date(); try { String newBarcode = mBarcodeFinder.find(format, width, height, bytes); - Log.i(TAG, "Found barcode: " + newBarcode); + Log.v(TAG, "Found barcode: " + newBarcode); Date third = new Date(); // Tell the world: - callBarcodeListener(newBarcode); + onBarcodeFound(newBarcode); Date fourth = new Date(); if (false) Log.v( @@ -154,44 +272,19 @@ public void onImageAvailable(int format, byte[] bytes, int width, int height) { } } - private int nImagesProcessed = 0; - private void saveImage(byte[] bytes) { - File dir = getActivity().getExternalFilesDir(null); - nImagesProcessed++; - File saveAs = new File(dir, "qr" + nImagesProcessed + ".jpg"); - - FileOutputStream output = null; - try { - output = new FileOutputStream(saveAs); - output.write(bytes); - } catch (IOException e) { - e.printStackTrace(); - } finally { - if (null != output) { - try { - output.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - } - //********************************************************************* //* Managing Barcode events: //* ======================== + //* Only bother the listener if a *new* barcode is detected. //* + //* Furthermore, //********************************************************************* private BarcodeDetectedListener mBarcodeListener = null; - public void setBarcodeListener(BarcodeDetectedListener listener) - { - mBarcodeListener = listener; - } - private String mLastReportedBarcode = null; private int mNoBarcodeCount = 0; - private void callBarcodeListener(String barcode) + private final int NO_BARCODE_IGNORE_LIMIT = 5; + private void onBarcodeFound(String barcode) { //mBarcodeListener.onBarcodeAvailable(barcode); //Log.d(TAG, "Scanned " + barcode); @@ -201,10 +294,9 @@ private void callBarcodeListener(String barcode) if (barcode == null) { mNoBarcodeCount++; - if (mLastReportedBarcode != null && mNoBarcodeCount >= 5) - { + if (mLastReportedBarcode != null && mNoBarcodeCount >= NO_BARCODE_IGNORE_LIMIT) { mLastReportedBarcode = null; - mBarcodeListener.onBarcodeAvailable(mLastReportedBarcode); + _onBarcode(mLastReportedBarcode); } } else @@ -213,12 +305,32 @@ private void callBarcodeListener(String barcode) if (!barcode.equals(mLastReportedBarcode)) { mLastReportedBarcode = barcode; - if (mBarcodeListener != null) - mBarcodeListener.onBarcodeAvailable(mLastReportedBarcode); + _onBarcode(mLastReportedBarcode); } } } + private void _onBarcode(final String barcode) + { + if (mBarcodeListener != null) { + mBarcodeListenerHandler.post( + new Runnable() { + @Override + public void run() { + mBarcodeListener.onBarcodeAvailable(barcode); + } + } + ); + } + + } + + private void onError(Exception error) + { + if (mBarcodeListener != null) + mBarcodeListener.onError(error); + } + //********************************************************************* //********************************************************************* @@ -232,7 +344,7 @@ private void callBarcodeListener(String barcode) *

*

* When no barcodes have been detected in 3 consecutive frames, onBarcodeAvailable - * with a null barcode parameter. + * is called with a null barcode parameter (). *

*/ public interface BarcodeDetectedListener { @@ -242,6 +354,44 @@ public interface BarcodeDetectedListener { * @param barcode the barcode detected. */ void onBarcodeAvailable(String barcode); + + void onError(Exception error); + } + + //********************************************************************* + // Pass-through properties for the barcode scanner + //********************************************************************* + public double getRelativeTrackingMargin() { + return mBarcodeFinder.getRelativeTrackingMargin(); } + + public void setRelativeTrackingMargin(double relativeTrackingMargin) { + mBarcodeFinder.setRelativeTrackingMargin(relativeTrackingMargin); + } + + public int getNoHitsBeforeTrackingLoss() { + return mBarcodeFinder.getNoHitsBeforeTrackingLoss(); + } + + public void setNoHitsBeforeTrackingLoss(int noHitsBeforeTrackingLoss) { + mBarcodeFinder.setNoHitsBeforeTrackingLoss(noHitsBeforeTrackingLoss); + } + + public EnumSet getPossibleBarcodeFormats() { + return mBarcodeFinder.getPossibleBarcodeFormats(); + } + + public void setPossibleBarcodeFormats(EnumSet possibleFormats) { + mBarcodeFinder.setPossibleBarcodeFormats(possibleFormats); + } + + public boolean isUseTracking() { + return mBarcodeFinder.isUseTracking(); + } + + public void setUseTracking(boolean useTracking) { + mBarcodeFinder.setUseTracking(useTracking); + } + } diff --git a/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/IStillSequenceCamera.java b/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/IStillSequenceCamera.java index 32c87ff..582474a 100644 --- a/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/IStillSequenceCamera.java +++ b/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/IStillSequenceCamera.java @@ -2,20 +2,24 @@ import android.app.Activity; import android.media.Image; +import android.os.Handler; import android.view.TextureView; +import java.io.IOException; + /** * Created by Thomas Schaumburg on 21-11-2015. */ public interface IStillSequenceCamera { void setup(); - void start(OnImageAvailableListener listener); + void start(OnImageAvailableListener listener, Handler callbackHandler); void stop(); void close(); public interface OnImageAvailableListener { void onImageAvailable(int format, byte[] data, int width, int height); + void onError(Exception error); } } diff --git a/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera/StillSequenceCamera.java b/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera/StillSequenceCamera.java index 0791475..e183416 100644 --- a/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera/StillSequenceCamera.java +++ b/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera/StillSequenceCamera.java @@ -6,6 +6,7 @@ import android.hardware.Camera.CameraInfo; import android.hardware.Camera.PictureCallback; import android.media.Image; +import android.os.Handler; import android.util.Log; import android.view.SurfaceHolder; import android.view.SurfaceView; @@ -19,20 +20,42 @@ * Created by Thomas Schaumburg on 08-12-2015. */ public class StillSequenceCamera implements IStillSequenceCamera { + private static final String TAG = "StillSequenceCamera"; private int mCameraId = -1; private Camera mCamera; private final Activity mActivity; private final SurfaceView mPreview; private IStillSequenceCamera.OnImageAvailableListener mImageListener = null; + private Handler mCallbackHandler; + private final static int CLOSED = 0; + private final static int INITIALIZED = 1; + private final static int CAPTURING = 2; + private int mState = CLOSED; public StillSequenceCamera(Activity activity, SurfaceView preview) { mActivity = activity; mPreview = preview; + mState = CLOSED; } - public void setup() { - if (mCamera != null) + /** + * Selects a back-facing camera, opens it and starts focusing. + * + * The #start() method can be called immediately when this method returns + * + * If setup() returns successfully, the StillSequenceCamera enters the INITIALIZED state. + * + * @throws IllegalStateException if the StillSequenceCamera is in any but the CLOSED state + * @throws UnsupportedOperationException if no back-facing camera is available + * @throws RuntimeException if opening the camera fails (for example, if the + * camera is in use by another process or device policy manager has + * disabled the camera). + */ + public void setup() + throws UnsupportedOperationException, IllegalStateException + { + if (mCamera != null || mState != CLOSED) throw new IllegalStateException("StillSequenceCamera.setup() can only be called on a new instance"); // Open a camera: @@ -58,10 +81,32 @@ public void setup() { pars.setPictureSize(1024, 768); pars.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE); mCamera.setParameters(pars); + + mState = INITIALIZED; } - public void start(OnImageAvailableListener listener) { + /** + * Starts the preview (displaying it in the #SurfaceView provided in the constructor), + * and starts taking pictures as rapidly as possible. + * + * This continues until #stop() is called. + * + * If start() returns successfully, the StillSequenceCamera enters the CAPTURING state. + * + * @param listener Every time a picture is taken, this callback interface is called. + * + * @throws IllegalStateException if the StillSequenceCamera is in any but the INITIALIZED state + */ + public void start(OnImageAvailableListener listener, Handler callbackHandler) + throws IllegalStateException + { + if (mState != INITIALIZED) + throw new IllegalStateException("StillSequenceCamera.start() can only be called in the INITIALIZED state"); + mImageListener = listener; + mCallbackHandler = callbackHandler; + if (mCallbackHandler == null) + mCallbackHandler = new Handler(); if (mPreview.getHolder().getSurface() != null) { try { @@ -119,18 +164,63 @@ public void surfaceDestroyed(SurfaceHolder holder) { ); // deprecated setting, but required on Android versions prior to 3.0 mPreview.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + mState = CAPTURING; } + /** + * Stops the preview, and stops the capture of still images. + * + * If stop() returns successfully, the StillSequenceCamera enters the STOPPED state. + * + * @throws IllegalStateException if stop is called in any but the STARTED state + */ public void stop() + throws IllegalStateException { + if (mState == CLOSED) + return; + + if (mState != CAPTURING) + throw new IllegalStateException("StillSequenceCamera.stop() can only be called in the STARTED state"); + mImageListener = null; + mCallbackHandler = null; stopTakingPictures(); + + try { + mCamera.stopPreview(); + } catch (Exception e) { + // ignore: tried to stop a non-existent preview + } + + mState = INITIALIZED; + } + + public void close() { + if (mState == CLOSED) + return; + + if (mState == CAPTURING) + stop(); + + if (mState != INITIALIZED) + throw new IllegalStateException("StillSequenceCamera.stop() can only be called after start()"); + + mContinueTakingPictures = false; + if (mCamera != null) { + mCamera.release(); + mCamera = null; + } + mCameraId = -1; + mImageListener = null; + + mState = CLOSED; } private boolean mContinueTakingPictures = false; - private static final String TAG = "StillSequenceCamera"; private void startTakingPictures() + throws IllegalStateException { if (mContinueTakingPictures) return; @@ -142,23 +232,32 @@ private void startTakingPictures() takePicture(); } - private void stopTakingPictures() { + private void stopTakingPictures() + { mContinueTakingPictures = false; } - private void takePicture() { + private void takePicture() + { mCamera.takePicture( null, null, new PictureCallback() { @Override - public void onPictureTaken(byte[] jpegData, Camera camera) { - Camera.Size size = camera.getParameters().getPictureSize(); + public void onPictureTaken(final byte[] jpegData, Camera camera) { + final Camera.Size size = camera.getParameters().getPictureSize(); Log.i(TAG, "Captured JPEG " + jpegData.length + " bytes (" + size.width + "x" + size.height + ")"); if (mImageListener != null) { - mImageListener.onImageAvailable(ImageFormat.JPEG, jpegData, size.width, size.height); + mCallbackHandler.post( + new Runnable() { + @Override + public void run() { + mImageListener.onImageAvailable(ImageFormat.JPEG, jpegData, size.width, size.height); + } + } + ); } if (mContinueTakingPictures) { mCamera.startPreview(); @@ -168,15 +267,4 @@ public void onPictureTaken(byte[] jpegData, Camera camera) { } ); } - - public void close() { - mContinueTakingPictures = false; - if (mCamera != null) { - mCamera.release(); - mCamera = null; - } - mCameraId = -1; - mImageListener = null; - } - } diff --git a/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera2/FocusManager.java b/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera2/FocusManager.java index e01a15f..366138a 100644 --- a/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera2/FocusManager.java +++ b/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera2/FocusManager.java @@ -20,6 +20,7 @@ import android.media.Image; import android.media.ImageReader; import android.os.Handler; +import android.os.HandlerThread; import android.util.Log; import android.util.Size; import android.view.Surface; @@ -50,6 +51,7 @@ public FocusManager(Activity activity, TextureView textureView) private TextureView mTextureView; private Size mPreviewSize; private ImageReader mPreviewImageReader; + private FocusingStateMachine mStateMachine; /** * Sets up member variables related to the on-screen preview (if any). @@ -109,10 +111,8 @@ public void onSurfaceTextureUpdated(SurfaceTexture texture) { if (mTextureView.isAvailable()) configureTransform(); - - } else { - mPreviewImageReader = ImageReader.newInstance(320, 240, ImageFormat.YUV_420_888, 2); //fps * 10 min + mPreviewImageReader = ImageReader.newInstance(320, 240, ImageFormat.YUV_420_888, 2); mPreviewImageReader.setOnImageAvailableListener( new ImageReader.OnImageAvailableListener() { @Override @@ -124,8 +124,6 @@ public void onImageAvailable(ImageReader reader) { }, null); mPreviewSurface = mPreviewImageReader.getSurface(); } - - return; } catch (NullPointerException e) { // Currently an NPE is thrown when the Camera2API is used but not supported on the // device this code runs. @@ -146,10 +144,11 @@ public void onImageAvailable(ImageReader reader) { } public void close() { - } + if (mTextureView != null) + mTextureView.setSurfaceTextureListener(null); - public Surface getSurface() { - return mPreviewSurface; + if (mPreviewImageReader != null) + mPreviewImageReader.setOnImageAvailableListener(null, null); } /** @@ -244,36 +243,12 @@ private void configureTransform() { } } - - - - - - - - - - /* - * Camera states: - */ - private static final int STATE_IDLE = 0; - private static final int STATE_WAITING_LOCK = 1; - private static final int STATE_WAITING_PRECAPTURE = 2; - private static final int STATE_WAITING_NON_PRECAPTURE = 3; - private static final int STATE_PICTURE_TAKEN = 4; - private int mState = STATE_IDLE; - - /** - * {@link CaptureRequest.Builder} for the camera preview - */ - private CaptureRequest.Builder mPreviewRequestBuilder; - - /** - * {@link CaptureRequest} generated by {@link #mPreviewRequestBuilder} - */ - private CaptureRequest mPreviewRequest; private Surface mPreviewSurface = null; + public Surface getSurface() { + return mPreviewSurface; + } + //********************************************************************* //* The AF/AE state machine: //* ========================= @@ -302,73 +277,33 @@ private void configureTransform() { public static interface FocusListener { public void focusLocked(); + public void error(Exception error); } private CameraCaptureSession mCameraCaptureSession; - public void start(CameraCaptureSession cameraCaptureSession, Handler cameraHandler, FocusListener listener) + + /** + * + * @param cameraCaptureSession + * @param callbackHandler the handler on which the listener should be invoked, or + * {@code null} to use the current thread's {@link android.os.Looper + * looper}. + * @param listener + */ + public void start(CameraCaptureSession cameraCaptureSession, Handler callbackHandler, FocusListener listener) { mCameraCaptureSession = cameraCaptureSession; // When the session is ready, we start displaying the preview. try { Log.i(TAG, "StartFocusing"); - CameraDevice camera = cameraCaptureSession.getDevice(); - - // We set up a CaptureRequest.Builder with the output Surface. - mPreviewRequestBuilder - = camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); - mPreviewRequestBuilder.addTarget(getSurface()); - - // Auto focus should be continuous for camera preview. - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_MODE, - CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE - ); - - // Automatic exposure control, NO FLASH - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AE_MODE, - CaptureRequest.CONTROL_AE_MODE_ON - ); - - FocusingStateMachine stateMachine = new FocusingStateMachine(cameraCaptureSession, cameraHandler, listener); - - // Now, we start streaming frames from the camera: - mPreviewRequest = mPreviewRequestBuilder.build(); - cameraCaptureSession.setRepeatingRequest( - mPreviewRequest, - stateMachine, - cameraHandler - ); - - // This is how to tell the camera to attempt a focus lock: - mPreviewRequestBuilder.set( - CaptureRequest.CONTROL_AF_TRIGGER, - CameraMetadata.CONTROL_AF_TRIGGER_START - ); - - // Tell the state machine to wait for the focus lock. - mState = STATE_WAITING_LOCK; - - // Send a single request to the camera with the focus-lock instruction: - cameraCaptureSession.capture( - mPreviewRequestBuilder.build(), - stateMachine, - cameraHandler - ); -// } catch (CameraAccessException e) { -// e.printStackTrace(); + mStateMachine = new FocusingStateMachine(cameraCaptureSession, callbackHandler, listener); + mStateMachine.start(mPreviewSurface); } catch (Exception e) { if (cameraCaptureSession != null) { - try { - cameraCaptureSession.stopRepeating(); - } catch (CameraAccessException cae) { - } + stop();//cameraCaptureSession.stopRepeating(); } - mPreviewRequestBuilder = null; - mPreviewRequest = null; //mPreviewImageReader.setOnImageAvailableListener(null, null); mPreviewImageReader = null; - mState = STATE_IDLE; e.printStackTrace(); } } @@ -377,8 +312,115 @@ public void start(CameraCaptureSession cameraCaptureSession, Handler cameraHandl * Unlock the focus. This method should be called when still image capture sequence is * finished. */ - public void stop(Handler cameraHandler) { + public void stop() { try { + mStateMachine.close(); + mStateMachine = null; + } catch (Exception e) { + e.printStackTrace(); + } + } + + private class FocusingStateMachine extends CameraCaptureSession.CaptureCallback { + private final FocusListener mListener; + private final Handler mCallbackHandler; + private final CameraCaptureSession mCaptureSession; + + private HandlerThread mFocusingStateMachineThread; + private Handler mFocusingStateMachineHandler; + private CaptureRequest.Builder mPreviewRequestBuilder; + + /* + * Focusing states: + */ + private static final int STATE_IDLE = 0; + private static final int STATE_WAITING_LOCK = 1; + private static final int STATE_WAITING_PRECAPTURE = 2; + private static final int STATE_WAITING_NON_PRECAPTURE = 3; + private static final int STATE_PICTURE_TAKEN = 4; + private int mState = STATE_IDLE; + + FocusingStateMachine(CameraCaptureSession cameraCaptureSession, Handler callbackHandler, FocusListener listener) { + mState = STATE_IDLE; + mCaptureSession = cameraCaptureSession; + mListener = listener; + if (callbackHandler != null) { + mCallbackHandler = callbackHandler; + } else { + mCallbackHandler = new Handler(); + } + + mFocusingStateMachineThread = new HandlerThread("Camera Focus Background"); + mFocusingStateMachineThread.start(); + mFocusingStateMachineHandler = new Handler(mFocusingStateMachineThread.getLooper()); + + // We set up a CaptureRequest.Builder with the output Surface. + try { + mPreviewRequestBuilder + = mCaptureSession.getDevice().createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); + } + catch (CameraAccessException cae) + { + onError(cae); + } + } + + void start(Surface previewSurface) + { + if (mPreviewRequestBuilder == null) + throw new IllegalStateException("There was an error when setting up the FocusManager.FocusingStateMachine - did you ignore an exception from the constructor....?"); + + try { + mPreviewRequestBuilder.addTarget(previewSurface); + + // Auto focus should be continuous for camera preview. + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE + ); + + // Automatic exposure control, NO FLASH + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, + CaptureRequest.CONTROL_AE_MODE_ON + ); + + // Now, we start streaming frames from the camera: + mCaptureSession.setRepeatingRequest( + mPreviewRequestBuilder.build(), + mStateMachine, + mFocusingStateMachineHandler + ); + + // This is how to tell the camera to attempt a focus lock: + mPreviewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, + CameraMetadata.CONTROL_AF_TRIGGER_START + ); + + // Tell the state machine to wait for the focus lock. + mState = STATE_WAITING_LOCK; + + // Send a single request to the camera with the focus-lock instruction: + mCaptureSession.capture( + mPreviewRequestBuilder.build(), + mStateMachine, + mFocusingStateMachineHandler + ); + } + catch (CameraAccessException cae) + { + onError(cae); + } + catch (Exception e) + { + onError(e); + } + } + + public void close() + { + mState = STATE_IDLE; if (mPreviewRequestBuilder != null) { mPreviewRequestBuilder.set( CaptureRequest.CONTROL_AF_TRIGGER, @@ -390,21 +432,25 @@ public void stop(Handler cameraHandler) { ); if (mCameraCaptureSession != null) { - // Send request to camera: - mCameraCaptureSession.capture( - mPreviewRequestBuilder.build(), - null, // => no callbacks - cameraHandler - ); - - mCameraCaptureSession.stopRepeating(); - - //// TODO: is this used at all...? - //mCameraCaptureSession.setRepeatingRequest( - // mPreviewRequest, - // null, // => no callbacks - // cameraHandler - //); + try { + // Send request to camera: + mCameraCaptureSession.capture( + mPreviewRequestBuilder.build(), + null, // => no callbacks + null // no callbacks => callback handler irrelevant + ); + + mCameraCaptureSession.stopRepeating(); + + //// TODO: is this used at all...? + //mCameraCaptureSession.setRepeatingRequest( + // mPreviewRequest, + // null, // => no callbacks + // cameraHandler + //); + } catch (CameraAccessException cae) { + onError(cae); + } mCameraCaptureSession = null; } @@ -412,21 +458,16 @@ public void stop(Handler cameraHandler) { mPreviewRequestBuilder = null; } - mState = STATE_IDLE; - } catch (Exception e) { - e.printStackTrace(); - } - } - - private class FocusingStateMachine extends CameraCaptureSession.CaptureCallback { - private FocusListener mListener = null; - private Handler mCameraHandler = null; - private CameraCaptureSession mCaptureSession = null; - - FocusingStateMachine(CameraCaptureSession cameraCaptureSession, Handler cameraHandler, FocusListener listener) { - mCaptureSession = cameraCaptureSession; - mListener = listener; - mCameraHandler = cameraHandler; + if (mFocusingStateMachineThread != null) { + try { + mFocusingStateMachineThread.quitSafely(); + mFocusingStateMachineThread.join(); + mFocusingStateMachineThread = null; + mFocusingStateMachineHandler = null; + } catch (Exception e) { + e.printStackTrace(); + } + } } @Override @@ -464,7 +505,7 @@ private void process(CaptureResult result) { onFocusLocked(); mState = STATE_PICTURE_TAKEN; } else { - runPrecaptureSequence(mCaptureSession, mCameraHandler); + runPrecaptureSequence(); } } break; @@ -494,7 +535,7 @@ private void process(CaptureResult result) { /** * Run the precapture sequence for capturing a still image. */ - private void runPrecaptureSequence(CameraCaptureSession captureSession, Handler cameraHandler) { + private void runPrecaptureSequence() { try { // This is how to tell the camera to trigger. mPreviewRequestBuilder.set( @@ -503,10 +544,10 @@ private void runPrecaptureSequence(CameraCaptureSession captureSession, Handler ); // Tell #mCaptureCallback to wait for the precapture sequence to be set. mState = STATE_WAITING_PRECAPTURE; - captureSession.capture( + mCaptureSession.capture( mPreviewRequestBuilder.build(), null, // the repeating request will generate the necessary callbacks - cameraHandler + null // no callback => no handler needed ); } catch (CameraAccessException e) { e.printStackTrace(); @@ -515,11 +556,40 @@ private void runPrecaptureSequence(CameraCaptureSession captureSession, Handler private void onFocusLocked() { - Log.i(TAG, "lock"); + Log.i(TAG, "focus lock"); try { mCaptureSession.stopRepeating(); - if (mListener != null) - mListener.focusLocked(); + if (mListener != null) { + mCallbackHandler.post( + new Runnable() { + @Override + public void run() { + mListener.focusLocked(); + } + } + ); + } + } catch (CameraAccessException e) + { + throw new UnsupportedOperationException("Camera access required"); + } + } + + private void onError(final Exception error) + { + Log.e(TAG, "focusing error"); + try { + mCaptureSession.stopRepeating(); + if (mListener != null) { + mCallbackHandler.post( + new Runnable() { + @Override + public void run() { + mListener.error(error); + } + } + ); + } } catch (CameraAccessException e) { throw new UnsupportedOperationException("Camera access required"); diff --git a/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera2/StillSequenceCamera2.java b/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera2/StillSequenceCamera2.java index 8cf6418..c3b3d97 100644 --- a/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera2/StillSequenceCamera2.java +++ b/still-sequence-camera/src/main/java/dk/schaumburgit/stillsequencecamera/camera2/StillSequenceCamera2.java @@ -46,20 +46,49 @@ public class StillSequenceCamera2 implements IStillSequenceCamera { private FocusManager mFocusManager; private CaptureManager mImageCapture; + private final static int CLOSED = 0; + private final static int INITIALIZED = 1; + private final static int CAPTURING = 2; + private final static int CHANGING = 3; + private final static int FOCUSING = 4; + private final static int FAILED = 5; + private final static int ERROR = 6; + private int mState = CLOSED; + /** + * Creates a headless #StillSequenceCamera2 * - * @param activity + * @param activity The activity associated with the calling app. */ public StillSequenceCamera2(Activity activity) { this(activity, null, new int[] {ImageFormat.JPEG}, 1024*768); } + /** + * Creates a headless #StillSequenceCamera2 + * + * @param activity The activity associated with the calling app. + * @param prioritizedImageFormats The preferred formats to capture images in + * (see #ImageFormat for values) + * @param minPixels The preferred minimum number of pixels in the captured images + * (i.e. width*height) + */ public StillSequenceCamera2(Activity activity, int[] prioritizedImageFormats, int minPixels) { this(activity, null, prioritizedImageFormats, minPixels); } + /** + * Creates a #StillSequenceCamera2 with a preview + * + * @param activity The activity associated with the calling app. + * @param prioritizedImageFormats The preferred formats to capture images in + * (see #ImageFormat for values) + * @param minPixels The preferred minimum number of pixels in the captured images + * (i.e. width*height) + * @param textureView The #TextureView to display the preview in (use null for headless scanning) + */ public StillSequenceCamera2(Activity activity, TextureView textureView, int[] prioritizedImageFormats, int minPixels) { if (activity==null) @@ -72,10 +101,25 @@ public StillSequenceCamera2(Activity activity, TextureView textureView, int[] pr mFocusManager = new FocusManager(activity, textureView); mImageCapture = new CaptureManager(activity, prioritizedImageFormats, minPixels); + + mState = CLOSED; } + /** + * Chooses a back-facing camera satisfying the requirements from the constructor (i.e. format + * and resolution). + * + * + * + * @throws IllegalStateException if the StillSequenceCamera2 is in any but the CLOSED state. + */ @Override - public void setup() { + public void setup() + throws IllegalStateException + { + if (mState != CLOSED) + throw new IllegalStateException("StillSequenceCamera2.setup() can only be called in the CLOSED state"); + CameraManager manager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE); try { @@ -91,20 +135,10 @@ public void setup() { if (facing != null && facing == CameraCharacteristics.LENS_FACING_FRONT) { continue; } - - map = characteristics.get( - CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - if (map == null) { - continue; - } - mCameraId = cameraId; } - mImageCapture.setup(mCameraId); mFocusManager.setup(mCameraId); - - return; } catch (CameraAccessException e) { e.printStackTrace(); } catch (NullPointerException e) { @@ -113,49 +147,27 @@ public void setup() { Log.e(TAG, "Camera2 API is not supported"); throw new UnsupportedOperationException("Camera2 API is not supported"); } + + mState = INITIALIZED; } @Override - public void start(final OnImageAvailableListener listener) { - // This will send the camera into the Setup state, from which it will eventually - // move into the Focusing and then Capturing states: - - //if (mTextureView == null) { - // openCamera(); - //} else if (mTextureView.isAvailable()) { - // // When the screen is turned off and turned back on, the SurfaceTexture is already - // // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open - // // a camera and start preview from here (otherwise, we wait until the surface is ready in - // // the SurfaceTextureListener). - // openCamera(); - // configureTransform(); - //} else { - // mTextureView.setSurfaceTextureListener( - // new TextureView.SurfaceTextureListener() { - // @Override - // public void onSurfaceTextureAvailable(SurfaceTexture texture, int width, int height) { - // openCamera(); - // } - // @Override - // public void onSurfaceTextureSizeChanged(SurfaceTexture texture, int width, int height) { - // configureTransform(); - // } - // @Override - // public boolean onSurfaceTextureDestroyed(SurfaceTexture texture) { - // return true; - // } - // @Override - // public void onSurfaceTextureUpdated(SurfaceTexture texture) { - // } - // } - // ); - //} + public void start(final OnImageAvailableListener listener, Handler callbackHandler) + { + if (mState != INITIALIZED) + throw new IllegalStateException("StillSequenceCamera2.start() can only be called in the INITIALIZED state"); + + if (callbackHandler == null) + callbackHandler = new Handler(); + final Handler _callbackHandler = callbackHandler; mFocusThread = new HandlerThread("CameraBackground"); mFocusThread.start(); mFocusHandler = new Handler(mFocusThread.getLooper()); CameraManager manager = (CameraManager) mActivity.getSystemService(Context.CAMERA_SERVICE); + + mState = CHANGING; try { if (!mCameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) { throw new RuntimeException("Time out waiting to lock camera opening."); @@ -184,6 +196,7 @@ public void onConfigured(CameraCaptureSession cameraCaptureSession) { } Log.d(TAG, "CameraDevice configured"); mCaptureSession = cameraCaptureSession; + mState = FOCUSING; mFocusManager.start( mCaptureSession, mFocusHandler, @@ -191,8 +204,24 @@ public void onConfigured(CameraCaptureSession cameraCaptureSession) { @Override public void focusLocked() { //startCapturePhase(); - mImageCapture.start(mCaptureSession, mFocusHandler, listener); - mFocusManager.stop(mFocusHandler); + mState = CAPTURING; + mImageCapture.start(mCaptureSession, _callbackHandler, listener); + mFocusManager.stop(); + } + + @Override + public void error(final Exception error) { + mState = ERROR; + mFocusManager.stop(); + if (listener != null) + _callbackHandler.post( + new Runnable() { + @Override + public void run() { + listener.onError(error); + } + } + ); } } ); @@ -201,12 +230,23 @@ public void focusLocked() { @Override public void onConfigureFailed( CameraCaptureSession cameraCaptureSession) { + mState = ERROR; Log.e(TAG, "Failed"); + if (listener != null) + mFocusHandler.post( + new Runnable() { + @Override + public void run() { + listener.onError(null); + } + } + ); } }, null ); } catch (CameraAccessException e) { + mState = FAILED; throw new UnsupportedOperationException("Camera access required"); } } @@ -246,12 +286,21 @@ public void onError(CameraDevice cameraDevice, int error) { } @Override - public void stop() { + public void stop() + { + if (mState == CLOSED) + return; + + if (mState != CAPTURING) + throw new IllegalStateException("StillSequenceCamera2.stop() can only be called in the STARTED state"); + + mState = CHANGING; + new Thread(new Runnable() { public void run() { try { - mFocusManager.stop(mFocusHandler); + mFocusManager.stop(); mImageCapture.stop(); mCameraOpenCloseLock.acquire(); if (mCaptureSession != null) { @@ -278,13 +327,25 @@ public void run() { e.printStackTrace(); } } + mState = INITIALIZED; } }).start(); } @Override public void close() { + if (mState == CLOSED) + return; + + if (mState == CAPTURING) + stop(); + + if (mState != INITIALIZED) + throw new IllegalStateException("StillSequenceCamera2.close() can only be called in the INITIALIZED state"); + mFocusManager.close(); mImageCapture.close(); + + mState = CLOSED; } } diff --git a/tracking-barcode-scanner/src/main/java/dk/schaumburgit/trackingbarcodescanner/TrackingBarcodeScanner.java b/tracking-barcode-scanner/src/main/java/dk/schaumburgit/trackingbarcodescanner/TrackingBarcodeScanner.java index f95b641..70add76 100644 --- a/tracking-barcode-scanner/src/main/java/dk/schaumburgit/trackingbarcodescanner/TrackingBarcodeScanner.java +++ b/tracking-barcode-scanner/src/main/java/dk/schaumburgit/trackingbarcodescanner/TrackingBarcodeScanner.java @@ -25,19 +25,52 @@ import java.util.EnumSet; import java.util.Hashtable; +/** + * The TrackingBarcodeScanner class looks for barcodes in the images supplied by the called. + * + * What distinguishes it from other barcode scanners is the tracking algorithm used. The tracking + * algorithm is optimized for use with sequential images (like the frames in a video recording). + * Once it has found a barcode in one area, it will look in the same area first in the next frame + * - only if that fails will it look in the entire image. + * + * This optimization yields speed-ups of 2-5 times (i.e. finding a barcode falls from 100ms to + * 20ms) in optimum situations (meaning where the barcode moves relatively little between frames). + * + * In situations where the images are completely unrelated, the tracking may actually cause a + * slowdown, due to the time wasted on the initial scan. + * + * The tracking algorithm may be switched on and off dynamically, so you can experiment with it's + * applicability for you scenario (see UseTracking bellow). + * + * The tracking algorithm is affected by the following properties: + * + * UseTracking (boolean): switches the entire tracking on or off (default: true) + * + * RelativeTrackingMargin (double): Specifies the relative margin to around the previous hit when + * running the initial tracking scan. Large values allow relatively large movements between frames, + * at the cost of lowered performance. Smaller values are faster, but allow less movement before + * tracking is lost (default 1.0) + * + * NoHitsBeforeTrackingLoss (int): Due to bad frames (e.g. motion blur), no-hit scans may + * occasionally occur. This parameter specifies how many consecutive bad frames will cause + * a tracking loss (default 5). + * + * PreferredImageFormats (readonly, int[]): Specifies the image formats supported by + * TrackingBarcodeScanner - using values from the ImageFormats enum - in order of preference + * (default {YUV_420_888, JPEG}) + */ public class TrackingBarcodeScanner { - private double mRelativeTrackingMargin = 1.0; - private int mNoHitsBeforeTrackingLoss = 5; - private EnumSet mPossibleFormats = EnumSet.of(BarcodeFormat.QR_CODE); + /** * Tag for the {@link Log}. */ private static final String TAG = "BarcodeFinder"; - private static final int[] mPreferredFormats = {ImageFormat.YUV_420_888, ImageFormat.JPEG}; - public int[] GetPreferredFormats() - { - return mPreferredFormats; - } + + private static final int[] mPreferredImageFormats = {ImageFormat.YUV_420_888, ImageFormat.JPEG}; + private boolean mUseTracking = true; + private double mRelativeTrackingMargin = 1.0; + private int mNoHitsBeforeTrackingLoss = 5; + private EnumSet mPossibleBarcodeFormats = EnumSet.of(BarcodeFormat.QR_CODE); private QRCodeReader mReader = new QRCodeReader(); private Hashtable mDecodeHints; @@ -45,7 +78,7 @@ public TrackingBarcodeScanner() { mDecodeHints = new Hashtable(); mDecodeHints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE); - mDecodeHints.put(DecodeHintType.POSSIBLE_FORMATS, mPossibleFormats); + mDecodeHints.put(DecodeHintType.POSSIBLE_FORMATS, mPossibleBarcodeFormats); } public Date a; @@ -64,7 +97,8 @@ public String find(int imageFormat, int w, int h, byte[] bytes) case ImageFormat.JPEG: // from JPEG // ========= - // ZXing doesn't accept a JPEG-encoded byte array, so we let Java decode intoa Bitmap: + // ZXing doesn't accept a JPEG-encoded byte array, so we let Java + // decode into a Bitmap: Bitmap bm = BitmapFactory.decodeByteArray(bytes, 0, bytes.length); b = new Date(); int width = bm.getWidth(); @@ -99,13 +133,15 @@ public String find(int imageFormat, int w, int h, byte[] bytes) try { Result r = null; - // First try where we found the barcode before (much quicker that way): - if (mLatestMatch != null) { - Geometry.Rectangle crop = mLatestMatch.normalize(0, 0, bitmap.getWidth(), bitmap.getHeight()); - //Log.d(TAG, "CROP: looking in (" + crop.x + ", " + crop.y + ", " + crop.width + ", " + crop.height + ")"); - int left = crop.x; - int top = crop.y; - r = doFind(bitmap.crop(left, top, crop.width, crop.height)); + if (mUseTracking) { + // First try where we found the barcode before (much quicker that way): + if (mLatestMatch != null) { + Geometry.Rectangle crop = mLatestMatch.normalize(0, 0, bitmap.getWidth(), bitmap.getHeight()); + //Log.d(TAG, "CROP: looking in (" + crop.x + ", " + crop.y + ", " + crop.width + ", " + crop.height + ")"); + int left = crop.x; + int top = crop.y; + r = doFind(bitmap.crop(left, top, crop.width, crop.height)); + } } d = new Date(); @@ -187,5 +223,43 @@ private void rememberMatch(Result r) //Log.d(TAG, "CROP: (" + match.x + ", " + match.y + ", " + match.width + ", " + match.height + ")"); } } + + public double getRelativeTrackingMargin() { + return mRelativeTrackingMargin; + } + + public void setRelativeTrackingMargin(double mRelativeTrackingMargin) { + this.mRelativeTrackingMargin = mRelativeTrackingMargin; + } + + public int getNoHitsBeforeTrackingLoss() { + return mNoHitsBeforeTrackingLoss; + } + + public void setNoHitsBeforeTrackingLoss(int mNoHitsBeforeTrackingLoss) { + this.mNoHitsBeforeTrackingLoss = mNoHitsBeforeTrackingLoss; + } + + public EnumSet getPossibleBarcodeFormats() { + return mPossibleBarcodeFormats; + } + + public void setPossibleBarcodeFormats(EnumSet mPossibleFormats) { + this.mPossibleBarcodeFormats = mPossibleFormats; + mDecodeHints.put(DecodeHintType.POSSIBLE_FORMATS, mPossibleBarcodeFormats); + } + + public boolean isUseTracking() { + return mUseTracking; + } + + public void setUseTracking(boolean useTracking) { + this.mUseTracking = useTracking; + } + + public int[] getPreferredImageFormats() + { + return mPreferredImageFormats; + } }