All Downloads are FREE. Search and download functionalities are using the official Maven repository.

boofcv.android.camera2.SimpleCamera2Activity Maven / Gradle / Ivy

Go to download

BoofCV is an open source Java library for real-time computer vision and robotics applications.

There is a newer version: 1.1.7
Show newest version
/*
 * Copyright (c) 2011-2018, Peter Abeles. All Rights Reserved.
 *
 * This file is part of BoofCV (http://boofcv.org).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package boofcv.android.camera2;

import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.graphics.SurfaceTexture;
import android.hardware.camera2.*;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.util.DisplayMetrics;
import android.util.Log;
import android.util.Size;
import android.util.SizeF;
import android.view.Surface;
import android.view.TextureView;
import android.view.View;
import android.widget.Toast;
import georegression.metric.UtilAngle;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Activity for collecting images from single camera on an Android device using the camera2 API.
 *
 * To start the camera invoke {@link #startCamera} inside your Activity's onCreate function.
 *
 * To customize it's behavior override the following functions:
 * 
    *
  • {@link #selectResolution}
  • *
  • {@link #onCameraResolutionChange}
  • *
  • {@link #configureCamera}
  • *
  • {@link #selectCamera}
  • *
  • {@link #processFrame}
  • *
  • {@link #onCameraOpened}
  • *
  • {@link #onCameraDisconnected}
  • *
* * Configuration variables *
    *
  • verbose
  • *
* * Specify the following permissions and features in AndroidManifest.xml *
 * {@code
 * 
 * 
 * }
* * @author Peter Abeles */ public abstract class SimpleCamera2Activity extends Activity { private static final String TAG = "SimpleCamera2"; private CameraCaptureSession mPreviewSession; protected TextureView mTextureView; protected View mView; //######## START Variables owned by lock private CameraOpen open = new CameraOpen(); //######## END // width and height of the view the camera is displayed in protected int viewWidth,viewHeight; // ratio of image and screen density protected float cameraToDisplayDensity; // Is this the first frame being processed. Sanity checks are done on the first frame private volatile boolean firstFrame; private volatile boolean canProcessImages; // If true there will be verbose output to Log protected boolean verbose = true; /** * An additional thread for running tasks that shouldn't block the UI. */ private HandlerThread mBackgroundThread; private Handler mBackgroundHandler; protected DisplayMetrics displayMetrics; /** * After this function is called the camera will be start. It might not start immediately * and there can be a delay. * @param view The view the camera is displayed inside or null if not displayed */ protected void startCameraTexture( TextureView view ) { if( verbose ) Log.i(TAG,"startCamera(TextureView="+(view!=null)+")"); this.mTextureView = view; this.mView = null; this.mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); } protected void startCameraView( View view) { if( verbose ) Log.i(TAG,"startCamera(View="+(view!=null)+")"); this.mView = view; this.mTextureView = null; view.addOnLayoutChangeListener(mViewLayoutChangeListener); } protected void startCamera() { if( verbose ) Log.i(TAG,"startCamera()"); this.mView = null; this.mTextureView =null; runOnUiThread(()->openCamera(0,0)); } /** * Starts a background thread and its {@link Handler}. */ private void startBackgroundThread() { mBackgroundThread = new HandlerThread("CameraBackground"); mBackgroundThread.start(); mBackgroundHandler = new Handler(mBackgroundThread.getLooper()); } /** * Stops the background thread and its {@link Handler}. */ private void stopBackgroundThread() { mBackgroundThread.quitSafely(); try { mBackgroundThread.join(); mBackgroundThread = null; mBackgroundHandler = null; } catch (InterruptedException e) { e.printStackTrace(); } } @Override protected void onCreate(Bundle savedInstanceState) { if( verbose ) Log.i(TAG,"onCreate()"); super.onCreate(savedInstanceState); displayMetrics = Resources.getSystem().getDisplayMetrics(); } @Override protected void onResume() { if( verbose ) Log.i(TAG,"onResume()"); super.onResume(); // When attached to a change listener below it's possible for the activity to be shutdown and a change // in layout be broadcast after that. In that situation we don't want the camera to be opened! startBackgroundThread(); // At this point in time the camera should be closed. It might not be due to rapid transitions between // onResume() and onPause(). Not sure why that happens but it's in the error reports. open.mLock.lock(); try { switch (open.state) { // not sure how to recover from this. This means there's an asynch task that will execute sometime // in the future. It might have out of date information so I can't just let it be. case OPENING: throw new RuntimeException("Camera shouldn't be in opening state when starting onResume()"); // It wants to be closed so let's just finish that case CLOSING:{ if( verbose ) Log.i(TAG, " camera is closing. Going to just close it now. device="+ (open.mCameraDevice==null)); if( open.mCameraDevice != null ) { open.closeCamera(); } } break; case OPEN: throw new RuntimeException("Camera is opened. Was not cleaned up correctly onPause()"); case CLOSED: // the state it should be in! break; default: throw new RuntimeException("New state was added and this needs to be updated. "+open.state); } // If everything went well above it's now in the opening state. This is set now because some of the options // might finish up later on. It's possible for a close request to come in before that has happened. open.state = CameraState.OPENING; } finally { open.mLock.unlock(); } if( mTextureView != null ) { if (mTextureView.isAvailable()) { openCamera(mTextureView.getWidth(), mTextureView.getHeight()); } else { mTextureView.setSurfaceTextureListener(mSurfaceTextureListener); } } else if( mView != null ) { if( mView.getWidth() != 0 && mView.getHeight() != 0 ) { openCamera(mView.getWidth(), mView.getHeight()); } else { mView.addOnLayoutChangeListener(mViewLayoutChangeListener); } } else if( open.mCameraDevice == null ) { startCamera(); } } @Override protected void onPause() { if( verbose ) Log.i(TAG,"onPause()"); closeCamera(); stopBackgroundThread(); super.onPause(); } /** * Selects the camera resolution from the list of possible values. By default it picks the * resolution which best fits the texture's aspect ratio. If there's a tie the area is * maximized. * * @param widthTexture Width of the texture the preview is displayed inside of. <= 0 if no view * @param heightTexture Height of the texture the preview is displayed inside of. <= 0 if no view * @param resolutions array of possible resolutions * @return index of the resolution */ protected int selectResolution( int widthTexture, int heightTexture, Size[] resolutions ) { int bestIndex = -1; double bestAspect = Double.MAX_VALUE; double bestArea = 0; double textureAspect = widthTexture > 0 ? widthTexture/(double)heightTexture:0; for( int i = 0; i < resolutions.length; i++ ) { Size s = resolutions[i]; int width = s.getWidth(); int height = s.getHeight(); double aspectScore = widthTexture > 0 ? Math.abs(width - height*textureAspect)/width:1; if( aspectScore < bestAspect ) { bestIndex = i; bestAspect = aspectScore; bestArea = width*height; } else if( Math.abs(aspectScore-bestArea) <= 1e-8 ) { bestIndex = i; double area = width*height; if( area > bestArea ) { bestArea = area; } } } return bestIndex; } /** * Called when the camera's resolution has changed. This function can be called more than once * each time a camera is opened, e.g. requested resolution does not match actual. */ protected void onCameraResolutionChange( int cameraWidth , int cameraHeight , int orientation ) { if( verbose ) Log.i(TAG,"onCameraResolutionChange( "+cameraWidth+" , "+cameraHeight+")"); } /** * Override to do custom configuration of the camera's settings. By default the camera * is put into auto mode. * * @param captureRequestBuilder used to configure the camera */ protected void configureCamera( CaptureRequest.Builder captureRequestBuilder ) { if( verbose ) Log.i(TAG,"configureCamera() default function"); captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); captureRequestBuilder.set(CaptureRequest.CONTROL_MODE, CaptureRequest.CONTROL_AE_MODE_ON); } /** * By default this will select the backfacing camera. override to change the camera it selects. */ protected boolean selectCamera( String id , CameraCharacteristics characteristics ) { if( verbose ) Log.i(TAG,"selectCamera() default function"); Integer facing = characteristics.get(CameraCharacteristics.LENS_FACING); return facing == null || facing != CameraCharacteristics.LENS_FACING_FRONT; } /** * Process a single frame from the video feed. Image is automatically * closed after this function exists. No need to invoke image.close() manually. * * All implementations of this function must run very fast. Less than 5 milliseconds is a good * rule of thumb. If longer than that then you should spawn a thread and process the * image inside of that. */ protected abstract void processFrame( Image image ); /** * Tries to open a {@link CameraDevice}. The result is listened by `mStateCallback`. */ @SuppressWarnings("MissingPermission") protected void openCamera(int widthTexture, int heightTexture) { if( verbose ) Log.i(TAG,"openCamera( texture: "+widthTexture+"x"+heightTexture+") activity="+getClass().getSimpleName()); if (Looper.getMainLooper().getThread() != Thread.currentThread()) { throw new RuntimeException("Attempted to openCamera() when not in the main looper thread!"); } if (isFinishing()) { return; } CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); if( manager == null ) throw new RuntimeException("Null camera manager"); // The camera should be released here until a camera has been successfully initialized boolean releaseLock = true; try { if (!open.mLock.tryLock(2500, TimeUnit.MILLISECONDS)) { throw new RuntimeException("Time out waiting to lock camera opening."); } if( open.state == CameraState.CLOSING) { if( verbose ) Log.d(TAG, "Close request was made after the open request. Aborting and closing. device=" +(open.mCameraDevice==null)); if( open.mCameraDevice != null ) { open.closeCamera(); } open.state = CameraState.CLOSED; open.clearCamera(); return; } else if( open.state == CameraState.CLOSED || open.state == CameraState.OPENING) { // These are the two states it should be in. if it wasn't opening before it is now open.state = CameraState.OPENING; } else { throw new RuntimeException("Unexpected state="+open.state); } if( mBackgroundHandler == null ) { if( verbose ) Log.i(TAG,"Background handler is null. Aborting."); return; } if( open.mCameraDevice != null ) { throw new RuntimeException("Tried to open camera with one already open"); } // Save the size of the component the camera feed is being displayed inside of this.viewWidth = widthTexture; this.viewHeight = heightTexture; this.cameraToDisplayDensity = 0; this.firstFrame = true; String[] cameras = manager.getCameraIdList(); for( String cameraId : cameras ) { CameraCharacteristics characteristics = manager.getCameraCharacteristics(cameraId); if(!selectCamera(cameraId,characteristics)) continue; StreamConfigurationMap map = characteristics. get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); if (map == null) { continue; } Size[] sizes = map.getOutputSizes(ImageFormat.YUV_420_888); int which = selectResolution(widthTexture, heightTexture,sizes); if( which < 0 || which >= sizes.length ) continue; open.mCameraSize = sizes[which]; open.cameraId = cameraId; open.mSensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); this.cameraToDisplayDensity = displayDensityAdjusted(); if( verbose ) Log.i(TAG,"selected cameraId="+cameraId+" orientation="+open.mSensorOrientation+ " res="+open.mCameraSize.getWidth()+"x"+open.mCameraSize.getHeight()); open.mCameraCharacterstics = characteristics; onCameraResolutionChange( open.mCameraSize.getWidth(), open.mCameraSize.getHeight(), open.mSensorOrientation); open.mPreviewReader = ImageReader.newInstance( open.mCameraSize.getWidth(), open.mCameraSize.getHeight(), ImageFormat.YUV_420_888, 2); // Do the processing inside the the handler thread instead of the looper thread to avoid // grinding the UI to a halt open.mPreviewReader.setOnImageAvailableListener(onAvailableListener, mBackgroundHandler); configureTransform(widthTexture, heightTexture); manager.openCamera(cameraId, mStateCallback, null); releaseLock = false; return; } if( handleNoCameraSelected() ) { Toast.makeText(this, "No camera selected!", Toast.LENGTH_LONG).show(); finish(); } } catch (CameraAccessException e) { Toast.makeText(this, "Cannot access the camera.", Toast.LENGTH_SHORT).show(); if( handleCameraOpenException(e)) { finish(); } } catch (NullPointerException e) { e.printStackTrace(); Log.e(TAG,"Null pointer in openCamera()"); if( handleCameraOpenException(e)) { // Currently an NPE is thrown when the Camera2API is used but not supported on the // device this code runs. Toast.makeText(this, "Null pointer. Camera2 API not supported?", Toast.LENGTH_LONG).show(); finish(); } } catch (InterruptedException e) { throw new RuntimeException("Interrupted while trying to lock camera opening."); } finally { if( releaseLock ) open.mLock.unlock(); } } /** * Called if no camera was selected when trying to open a camera * @return true means finish() the activity and show toast */ protected boolean handleNoCameraSelected() { return true; } /** * An exception happened while trying to open the camera * @return true means finish() the activity and show toast */ protected boolean handleCameraOpenException( Exception e ) { return true; } /** * Re-opens the camera with the same settings at the specified resolution. It is assumed that you know * what you're doing and that this is a valid resolution. * * WARNING: UNTESTED */ protected void reopenCameraAtResolution(int cameraWidth, int cameraHeight) { if (Looper.getMainLooper().getThread() != Thread.currentThread()) { throw new RuntimeException("Attempted to reopenCameraAtResolution main looper thread!"); } boolean releaseLock = true; open.mLock.lock(); try { if (verbose) Log.i(TAG, "Reopening camera is null == " + (open.mCameraDevice == null)+" state="+open.state+ " activity="+getClass().getSimpleName()); if( open.state != CameraState.OPEN ) throw new RuntimeException("BUG! Attempted to re-open camera when not open"); if (null == open. mCameraDevice) { throw new RuntimeException("Can't re-open a closed camera"); } closePreviewSession(); open.mCameraSize = null; firstFrame = true; CameraManager manager = (CameraManager) getSystemService(Context.CAMERA_SERVICE); if (manager == null) throw new RuntimeException("Null camera manager"); try { open.mPreviewReader = ImageReader.newInstance( cameraWidth, cameraHeight, ImageFormat.YUV_420_888, 2); // Do the processing inside the the handler thread instead of the looper thread to avoid // grinding the UI to a halt open.mPreviewReader.setOnImageAvailableListener(onAvailableListener, mBackgroundHandler); configureTransform(viewWidth, viewHeight); manager.openCamera(open.cameraId, mStateCallback, null); releaseLock = false; } catch (IllegalArgumentException e) { Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show(); finish(); } catch (CameraAccessException e) { e.printStackTrace(); } } finally { if(releaseLock) open.mLock.unlock(); } } /** * Closes the camera. Returns true if the camera was not already closed and it closed it * @return */ protected boolean closeCamera() { if( verbose ) Log.i(TAG,"closeCamera() activity="+getClass().getSimpleName()); if (Looper.getMainLooper().getThread() != Thread.currentThread()) { throw new RuntimeException("Attempted to close camera not on the main looper thread!"); } boolean closed = false; // if( verbose ) { // StackTraceElement[] trace = new RuntimeException().getStackTrace(); // for (int i = 0; i < Math.min(trace.length, 3); i++) { // System.out.println("[ " + i + " ] = " + trace[i].toString()); // } // } // NOTE: Since open can only be called in the main looper this won't be enough to prevent // it from closing before it opens. That's why open.state exists open.mLock.lock(); try { if( verbose ) Log.i(TAG,"closeCamera: camera="+(open.mCameraDevice==null)+" state="+open.state); closePreviewSession(); // close has been called while trying to open the camera! if( open.state == CameraState.OPENING ) { // If it's in this state that means an asych task is opening the camera. By changing the state // to closing it will not abort that process when the task is called. open.state = CameraState.CLOSING; if( open.mCameraDevice != null ) { throw new RuntimeException("BUG! Camera is opening and should be null until opened"); } } else { if (null != open.mCameraDevice) { closed = true; open.closeCamera(); } open.state = CameraState.CLOSED; open.clearCamera(); } } finally { open.mLock.unlock(); } return closed; } /** * Start the camera preview. */ private void startPreview() { // Sanity check. Parts of this code assume it's on this thread. If it has been put into a handle // that's fine just be careful nothing assumes it's on the main looper if (Looper.getMainLooper().getThread() != Thread.currentThread()) { throw new RuntimeException("Not on main looper! Modify code to remove assumptions"); } if( verbose ) { Log.i(TAG,"startPreview()"); } try { open.mLock.lock(); if (null == open.mCameraDevice || null == open.mCameraSize) { Log.i(TAG," aborting startPreview. Camera not open yet."); return; } closePreviewSession(); List surfaces = new ArrayList<>(); open.mPreviewRequestBuilder = open.mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); if( mTextureView != null && mTextureView.isAvailable() ) { SurfaceTexture texture = mTextureView.getSurfaceTexture(); assert texture != null; texture.setDefaultBufferSize(open.mCameraSize.getWidth(), open.mCameraSize.getHeight()); // Display the camera preview into this texture Surface previewSurface = new Surface(texture); surfaces.add(previewSurface); open.mPreviewRequestBuilder.addTarget(previewSurface); } // This is where the image for processing is extracted from Surface readerSurface = open.mPreviewReader.getSurface(); surfaces.add(readerSurface); open.mPreviewRequestBuilder.addTarget(readerSurface); configureCamera(open.mPreviewRequestBuilder); open.mCameraDevice.createCaptureSession(surfaces, new CameraCaptureSession.StateCallback() { @Override public void onConfigured(@NonNull CameraCaptureSession session) { mPreviewSession = session; updatePreview(); } @Override public void onConfigureFailed(@NonNull CameraCaptureSession session) { Log.i(TAG,"CameraCaptureSession.onConfigureFailed()"); Toast.makeText(SimpleCamera2Activity.this, "Failed", Toast.LENGTH_SHORT).show(); } }, null); } catch (CameraAccessException e) { e.printStackTrace(); } finally { open.mLock.unlock(); } } /** * Update the camera preview. {@link #startPreview()} needs to be called in advance. */ private void updatePreview() { if (null == open.mCameraDevice) { return; } open.mLock.lock(); try { mPreviewSession.setRepeatingRequest(open.mPreviewRequestBuilder.build(), null, mBackgroundHandler); } catch (CameraAccessException e) { e.printStackTrace(); } finally { open.mLock.unlock(); } } /** * Configures the necessary {@link Matrix} transformation to `mTextureView`. * This method should not to be called until the camera preview size is determined in * openCamera, or until the size of `mTextureView` is fixed. * * @param viewWidth The width of `mTextureView` * @param viewHeight The height of `mTextureView` */ private void configureTransform(int viewWidth, int viewHeight) { int cameraWidth,cameraHeight; try { open.mLock.lock(); if (null == mTextureView || null == open.mCameraSize) { return; } cameraWidth = open.mCameraSize.getWidth(); cameraHeight = open.mCameraSize.getHeight(); } finally { open.mLock.unlock(); } int rotation = getWindowManager().getDefaultDisplay().getRotation(); Matrix matrix = new Matrix(); RectF viewRect = new RectF(0, 0, viewWidth, viewHeight); RectF bufferRect = new RectF(0, 0, cameraHeight, cameraWidth);// TODO why w/h swapped? float centerX = viewRect.centerX(); float centerY = viewRect.centerY(); if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) { bufferRect.offset(centerX - bufferRect.centerX(), centerY - bufferRect.centerY()); matrix.setRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.FILL); float scale = Math.max( (float) viewHeight / cameraHeight, (float) viewWidth / cameraWidth); matrix.postScale(scale, scale, centerX, centerY); matrix.postRotate(90 * (rotation - 2), centerX, centerY); } mTextureView.setTransform(matrix); } private void closePreviewSession() { if( verbose ) Log.i(TAG,"closePreviewSession"); if (mPreviewSession != null) { mPreviewSession.close(); mPreviewSession = null; } } private View.OnLayoutChangeListener mViewLayoutChangeListener = new View.OnLayoutChangeListener() { @Override public void onLayoutChange(View view, int left, int top, int right, int bottom, int leftWas, int topWas, int rightWas, int bottomWas) { int width = right-left; int height = bottom-top; if( verbose ) Log.i(TAG,"onLayoutChange() TL="+top+"x"+left+" view="+width+"x"+height+" mCameraSize="+(open.mCameraSize!=null)); if( open.mCameraSize == null ) { openCamera(width,height); } view.removeOnLayoutChangeListener(this); } }; private TextureView.SurfaceTextureListener mSurfaceTextureListener = new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { if( verbose ) Log.i(TAG,"onSurfaceTextureAvailable() view="+width+"x"+height+" mCameraSize="+(open.mCameraSize!=null)); openCamera(width, height); } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) { if( verbose ) Log.i(TAG,"onSurfaceTextureSizeChanged() view="+width+"x"+height); configureTransform(width, height); } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { } }; private CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(@NonNull CameraDevice cameraDevice) { if( verbose ) Log.i(TAG,"CameraDevice Callback onOpened() id="+cameraDevice.getId()+" camera="+open.state); if( !open.mLock.isLocked() ) throw new RuntimeException("Camera not locked!"); if( open.mCameraDevice != null ) throw new RuntimeException("onOpen() and mCameraDevice is not null"); boolean success = false; try { open.mCameraDevice = cameraDevice; if (open.state == CameraState.OPENING) { open.state = CameraState.OPEN; startPreview(); if (null != mTextureView) { configureTransform(mTextureView.getWidth(), mTextureView.getHeight()); } success = true; } else if (open.state == CameraState.CLOSING) { // Closed was called when trying to open the camera // abort opening and immediately close it open.closeCamera(); } else { open.mCameraDevice = null; throw new RuntimeException("Unexpected camera state "+open.state); } } finally { open.mLock.unlock(); } if( success ) onCameraOpened(cameraDevice); } @Override public void onDisconnected(@NonNull CameraDevice cameraDevice) { if( verbose ) Log.i(TAG,"CameraDevice Callback onDisconnected() id="+cameraDevice.getId()); boolean unexpected = !open.mLock.isLocked(); if( unexpected ) { open.mLock.lock(); } try { open.mCameraDevice = cameraDevice; open.closeCamera(); } finally { open.mLock.unlock(); } onCameraDisconnected(cameraDevice); if( unexpected) { // the camera disconnected and no request to disconnect it was made by // the application. not really sure what to do here. Restarting the activity // seems reasonable Log.e(TAG," Camera disconnection was unexpected. Restarting activity"); recreate(); } } @Override public void onError(@NonNull CameraDevice cameraDevice, int error) { if( verbose ) Log.e(TAG,"CameraDevice Callback onError() error="+error); boolean unexpected = !open.mLock.isLocked(); if( unexpected ) { open.mLock.lock(); } try { open.mCameraDevice = cameraDevice; open.closeCamera(); } finally { open.mLock.unlock(); } // If the camera was locked that means it has an error when trying to open it if( unexpected ) Log.e(TAG," No lock applied to the camera. Unexpected problem?"); finish(); } }; /** * Invoked when the camera has been opened */ protected void onCameraOpened( @NonNull CameraDevice cameraDevice ){} /** * Invoked when the camera has been disconnected */ protected void onCameraDisconnected( @NonNull CameraDevice cameraDevice ){} /** * Estimates the camera's horizontal and vertical FOV by picking a nominal value. * Determining the actual FOV is a much more complex process. */ public double[] cameraNominalFov() { open.mLock.lock(); try { SizeF sensorSize = null; float[] focalLengths = null; // This might be called before the camera is open if( open.mCameraCharacterstics != null ) { sensorSize = open.mCameraCharacterstics.get(CameraCharacteristics.SENSOR_INFO_PHYSICAL_SIZE); focalLengths = open.mCameraCharacterstics.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS); } if (focalLengths == null || sensorSize == null ) { // 60 degrees seems reasonable for a random guess double hfov = UtilAngle.radian(60); if( open.mCameraSize == null ) { Log.w(TAG,"Requesting FOV when the camera isn't open yet!"); return new double[]{hfov, hfov}; } else { double vfov = hfov * open.mCameraSize.getHeight() / open.mCameraSize.getWidth(); return new double[]{hfov, vfov}; } } else { double hfov = 2 * Math.atan(sensorSize.getWidth() / (2 * focalLengths[0])); double vfov = 2 * Math.atan(sensorSize.getHeight() / (2 * focalLengths[0])); return new double[]{hfov, vfov}; } } finally { open.mLock.unlock(); } } // This is run in the background handler and not the looper private ImageReader.OnImageAvailableListener onAvailableListener = imageReader -> { if( imageReader.getMaxImages() == 0 ) { Log.e(TAG,"No images available. Has image.close() not been called?"); return; } Image image = imageReader.acquireLatestImage(); if (image == null) return; try { // safely acquire the camera resolution int cameraWidth, cameraHeight, cameraOrientation; open.mLock.lock(); try { if (open.mCameraSize == null || open.state != CameraState.OPEN) return; cameraWidth = open.mCameraSize.getWidth(); cameraHeight = open.mCameraSize.getHeight(); cameraOrientation = open.mSensorOrientation; } finally { open.mLock.unlock(); } if (firstFrame) { firstFrame = false; canProcessImages = false; // sometimes we request a resolution and Android say's f-you and gives us something else even if it's // in the valid list. Re-adjust everything to what the actual resolution is if (cameraWidth != image.getWidth() || cameraHeight != image.getHeight()) { Log.e(TAG, "Android broke resolution contract. Actual=" + image.getWidth() + "x" + image.getHeight() + " Expected=" + cameraWidth + "x" + cameraHeight); open.mLock.lock(); try { if (open.mCameraSize == null) return; open.mCameraSize = new Size(image.getWidth(), image.getHeight()); } finally { open.mLock.unlock(); } runOnUiThread(() -> { configureTransform(viewWidth, viewHeight); onCameraResolutionChange(cameraWidth, cameraHeight, cameraOrientation); canProcessImages = true; }); } else { canProcessImages = true; } } if (canProcessImages) { processFrame(image); } } catch( IllegalStateException e ) { // Looks like there are situations where a camera is closed and the images // freed but this function gets called anyways. if that happens any access to // the image will cause an IllegalStateException be thrown. if( verbose ) { Log.e(TAG,"OnImageAvailableListener exception="+e.getMessage()); } handleOnImageAvailableException(e); } finally { // WARNING: It's not documented if Image is thread safe or not. it's implied that it because // Google's examples show it being closed and processed in a thread other than looper. image.close(); } }; /** * An exception was thrown inside of OnImageAvailableListener. See code comments for details */ protected void handleOnImageAvailableException( RuntimeException e ) {} /** * Some times the size of a font of stroke needs to be specified in the input image * but then gets scaled to image resolution. This compensates for that. */ private float displayDensityAdjusted() { open.mLock.lock(); try { if (open.mCameraSize == null) return displayMetrics.density; int rotation = getWindowManager().getDefaultDisplay().getRotation(); int screenWidth = (rotation == 0 || rotation == 2) ? displayMetrics.widthPixels : displayMetrics.heightPixels; int cameraWidth = open.mSensorOrientation == 0 || open.mSensorOrientation == 180 ? open.mCameraSize.getWidth() : open.mCameraSize.getHeight(); return displayMetrics.density * cameraWidth / screenWidth; } finally { open.mLock.unlock(); } } /** * All these variables are owned by the camera open lock */ static class CameraOpen { ReentrantLock mLock = new ReentrantLock(); CameraState state = CameraState.CLOSED; CameraDevice mCameraDevice; Size mCameraSize; // size of camera preview String cameraId; // the camera that was selected to view int mSensorOrientation; // sensor's orientation // describes physical properties of the camera CameraCharacteristics mCameraCharacterstics; // Image reader for capturing the preview private ImageReader mPreviewReader; private CaptureRequest.Builder mPreviewRequestBuilder; public void closeCamera() { state = CameraState.CLOSED; mCameraDevice.close(); mPreviewReader.close(); // TODO do targets need to be removed from mPreviewRequestBuilder? clearCamera(); } public void clearCamera() { mCameraCharacterstics = null; mCameraDevice = null; mCameraSize = null; cameraId = null; mSensorOrientation = 0; mPreviewReader = null; mPreviewRequestBuilder = null; } } protected enum CameraState { CLOSED, /** * The camera enters into this state the second a request is made to open the camera. At this point * none of the device isn't known. */ OPENING, OPEN, /** * When in the closing state that means the camera was in the opening state when a close request was * sent. At various points in the opening process it should see if its in this state. If the camera * device is not null then the camera should be shut down. If null then just set the state to closed. */ CLOSING } public void setVerbose(boolean verbose) { this.verbose = verbose; } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy