/* * Copyright (C) 2014 The Android Open Source Project * * 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 com.example.android.hdrviewfinder; import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureRequest; import android.hardware.camera2.CaptureResult; import android.hardware.camera2.TotalCaptureResult; import android.hardware.camera2.params.StreamConfigurationMap; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.provider.Settings; import android.renderscript.RenderScript; import android.support.annotation.NonNull; import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; import android.support.v7.app.AppCompatActivity; import android.util.Log; import android.util.Size; import android.view.GestureDetector; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceHolder; import android.view.View; import android.widget.Button; import android.widget.TextView; import java.util.ArrayList; import java.util.List; /** * A small demo of advanced camera functionality with the Android camera2 API. * * <p>This demo implements a real-time high-dynamic-range camera viewfinder, * by alternating the sensor's exposure time between two exposure values on even and odd * frames, and then compositing together the latest two frames whenever a new frame is * captured.</p> * * <p>The demo has three modes: Regular auto-exposure viewfinder, split-screen manual exposure, * and the fused HDR viewfinder. The latter two use manual exposure controlled by the user, * by swiping up/down on the right and left halves of the viewfinder. The left half controls * the exposure time of even frames, and the right half controls the exposure time of odd frames. * </p> * * <p>In split-screen mode, the even frames are shown on the left and the odd frames on the right, * so the user can see two different exposures of the scene simultaneously. In fused HDR mode, * the even/odd frames are merged together into a single image. By selecting different exposure * values for the even/odd frames, the fused image has a higher dynamic range than the regular * viewfinder.</p> * * <p>The HDR fusion and the split-screen viewfinder processing is done with RenderScript; as is the * necessary YUV->RGB conversion. The camera subsystem outputs YUV images naturally, while the GPU * and display subsystems generally only accept RGB data. Therefore, after the images are * fused/composited, a standard YUV->RGB color transform is applied before the the data is written * to the output Allocation. The HDR fusion algorithm is very simple, and tends to result in * lower-contrast scenes, but has very few artifacts and can run very fast.</p> * * <p>Data is passed between the subsystems (camera, RenderScript, and display) using the * Android {@link android.view.Surface} class, which allows for zero-copy transport of large * buffers between processes and subsystems.</p> */ public class HdrViewfinderActivity extends AppCompatActivity implements SurfaceHolder.Callback, CameraOps.ErrorDisplayer, CameraOps.CameraReadyListener { private static final String TAG = "HdrViewfinderDemo"; private static final String FRAGMENT_DIALOG = "dialog"; private static final int REQUEST_PERMISSIONS_REQUEST_CODE = 34; /** * View for the camera preview. */ private FixedAspectSurfaceView mPreviewView; /** * Root view of this activity. */ private View rootView; /** * This shows the current mode of the app. */ private TextView mModeText; // These show lengths of exposure for even frames, exposure for odd frames, and auto exposure. private TextView mEvenExposureText, mOddExposureText, mAutoExposureText; private Handler mUiHandler; private CameraCharacteristics mCameraInfo; private Surface mPreviewSurface; private Surface mProcessingHdrSurface; private Surface mProcessingNormalSurface; CaptureRequest.Builder mHdrBuilder; ArrayList<CaptureRequest> mHdrRequests = new ArrayList<CaptureRequest>(2); CaptureRequest mPreviewRequest; RenderScript mRS; ViewfinderProcessor mProcessor; CameraManager mCameraManager; CameraOps mCameraOps; private int mRenderMode = ViewfinderProcessor.MODE_NORMAL; // Durations in nanoseconds private static final long MICRO_SECOND = 1000; private static final long MILLI_SECOND = MICRO_SECOND * 1000; private static final long ONE_SECOND = MILLI_SECOND * 1000; private long mOddExposure = ONE_SECOND / 33; private long mEvenExposure = ONE_SECOND / 33; private Object mOddExposureTag = new Object(); private Object mEvenExposureTag = new Object(); private Object mAutoExposureTag = new Object(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); rootView = findViewById(R.id.panels); mPreviewView = (FixedAspectSurfaceView) findViewById(R.id.preview); mPreviewView.getHolder().addCallback(this); mPreviewView.setGestureListener(this, mViewListener); Button helpButton = (Button) findViewById(R.id.help_button); helpButton.setOnClickListener(mHelpButtonListener); mModeText = (TextView) findViewById(R.id.mode_label); mEvenExposureText = (TextView) findViewById(R.id.even_exposure); mOddExposureText = (TextView) findViewById(R.id.odd_exposure); mAutoExposureText = (TextView) findViewById(R.id.auto_exposure); mUiHandler = new Handler(Looper.getMainLooper()); mRS = RenderScript.create(this); // When permissions are revoked the app is restarted so onCreate is sufficient to check for // permissions core to the Activity's functionality. if (!checkCameraPermissions()) { requestCameraPermissions(); } else { findAndOpenCamera(); } } @Override protected void onResume() { super.onResume(); } @Override protected void onPause() { super.onPause(); // Wait until camera is closed to ensure the next application can open it if (mCameraOps != null) { mCameraOps.closeCameraAndWait(); mCameraOps = null; } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.main, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.info: { MessageDialogFragment.newInstance(R.string.intro_message) .show(getFragmentManager(), FRAGMENT_DIALOG); break; } } return super.onOptionsItemSelected(item); } private GestureDetector.OnGestureListener mViewListener = new GestureDetector.SimpleOnGestureListener() { @Override public boolean onDown(MotionEvent e) { return true; } @Override public boolean onSingleTapUp(MotionEvent e) { switchRenderMode(1); return true; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (mRenderMode == ViewfinderProcessor.MODE_NORMAL) return false; float xPosition = e1.getAxisValue(MotionEvent.AXIS_X); float width = mPreviewView.getWidth(); float height = mPreviewView.getHeight(); float xPosNorm = xPosition / width; float yDistNorm = distanceY / height; final float ACCELERATION_FACTOR = 8; double scaleFactor = Math.pow(2.f, yDistNorm * ACCELERATION_FACTOR); // Even on left, odd on right if (xPosNorm > 0.5) { mOddExposure *= scaleFactor; } else { mEvenExposure *= scaleFactor; } setHdrBurst(); return true; } }; /** * Show help dialogs. */ private View.OnClickListener mHelpButtonListener = new View.OnClickListener() { public void onClick(View v) { MessageDialogFragment.newInstance(R.string.help_text) .show(getFragmentManager(), FRAGMENT_DIALOG); } }; /** * Return the current state of the camera permissions. */ private boolean checkCameraPermissions() { int permissionState = ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA); // Check if the Camera permission is already available. if (permissionState != PackageManager.PERMISSION_GRANTED) { // Camera permission has not been granted. Log.i(TAG, "CAMERA permission has NOT been granted."); return false; } else { // Camera permissions are available. Log.i(TAG, "CAMERA permission has already been granted."); return true; } } /** * Attempt to initialize the camera. */ private void initializeCamera() { mCameraManager = (CameraManager) getSystemService(CAMERA_SERVICE); if (mCameraManager != null) { mCameraOps = new CameraOps(mCameraManager, /*errorDisplayer*/ this, /*readyListener*/ this, /*readyHandler*/ mUiHandler); mHdrRequests.add(null); mHdrRequests.add(null); } else { Log.e(TAG, "Couldn't initialize the camera"); } } private void requestCameraPermissions() { boolean shouldProvideRationale = ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA); // Provide an additional rationale to the user. This would happen if the user denied the // request previously, but didn't check the "Don't ask again" checkbox. if (shouldProvideRationale) { Log.i(TAG, "Displaying camera permission rationale to provide additional context."); Snackbar.make(rootView, R.string.camera_permission_rationale, Snackbar .LENGTH_INDEFINITE) .setAction(R.string.ok, new View.OnClickListener() { @Override public void onClick(View view) { // Request Camera permission ActivityCompat.requestPermissions(HdrViewfinderActivity.this, new String[]{Manifest.permission.CAMERA}, REQUEST_PERMISSIONS_REQUEST_CODE); } }) .show(); } else { Log.i(TAG, "Requesting camera permission"); // Request Camera permission. It's possible this can be auto answered if device policy // sets the permission in a given state or the user denied the permission // previously and checked "Never ask again". ActivityCompat.requestPermissions(HdrViewfinderActivity.this, new String[]{Manifest.permission.CAMERA}, REQUEST_PERMISSIONS_REQUEST_CODE); } } /** * Callback received when a permissions request has been completed. */ @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { Log.i(TAG, "onRequestPermissionResult"); if (requestCode == REQUEST_PERMISSIONS_REQUEST_CODE) { if (grantResults.length <= 0) { // If user interaction was interrupted, the permission request is cancelled and you // receive empty arrays. Log.i(TAG, "User interaction was cancelled."); } else if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { // Permission was granted. findAndOpenCamera(); } else { // Permission denied. // In this Activity we've chosen to notify the user that they // have rejected a core permission for the app since it makes the Activity useless. // We're communicating this message in a Snackbar since this is a sample app, but // core permissions would typically be best requested during a welcome-screen flow. // Additionally, it is important to remember that a permission might have been // rejected without asking the user for permission (device policy or "Never ask // again" prompts). Therefore, a user interface affordance is typically implemented // when permissions are denied. Otherwise, your app could appear unresponsive to // touches or interactions which have required permissions. Snackbar.make(rootView, R.string.camera_permission_denied_explanation, Snackbar .LENGTH_INDEFINITE) .setAction(R.string.settings, new View.OnClickListener() { @Override public void onClick(View view) { // Build intent that displays the App settings screen. Intent intent = new Intent(); intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); Uri uri = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null); intent.setData(uri); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent); } }) .show(); } } } private void findAndOpenCamera() { boolean cameraPermissions = checkCameraPermissions(); if (cameraPermissions) { String errorMessage = "Unknown error"; boolean foundCamera = false; initializeCamera(); if (cameraPermissions && mCameraOps != null) { try { // Find first back-facing camera that has necessary capability. String[] cameraIds = mCameraManager.getCameraIdList(); for (String id : cameraIds) { CameraCharacteristics info = mCameraManager.getCameraCharacteristics(id); int facing = info.get(CameraCharacteristics.LENS_FACING); int level = info.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); boolean hasFullLevel = (level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL); int[] capabilities = info .get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); int syncLatency = info.get(CameraCharacteristics.SYNC_MAX_LATENCY); boolean hasManualControl = hasCapability(capabilities, CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR); boolean hasEnoughCapability = hasManualControl && syncLatency == CameraCharacteristics.SYNC_MAX_LATENCY_PER_FRAME_CONTROL; // All these are guaranteed by // CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_FULL, but checking // for only the things we care about expands range of devices we can run on. // We want: // - Back-facing camera // - Manual sensor control // - Per-frame synchronization (so that exposure can be changed every frame) if (facing == CameraCharacteristics.LENS_FACING_BACK && (hasFullLevel || hasEnoughCapability)) { // Found suitable camera - get info, open, and set up outputs mCameraInfo = info; mCameraOps.openCamera(id); configureSurfaces(); foundCamera = true; break; } } if (!foundCamera) { errorMessage = getString(R.string.camera_no_good); } } catch (CameraAccessException e) { errorMessage = getErrorString(e); } if (!foundCamera) { showErrorDialog(errorMessage); } } } } private boolean hasCapability(int[] capabilities, int capability) { for (int c : capabilities) { if (c == capability) return true; } return false; } private void switchRenderMode(int direction) { if (mCameraOps != null) { mRenderMode = (mRenderMode + direction) % 3; mModeText.setText(getResources().getStringArray(R.array.mode_label_array)[mRenderMode]); if (mProcessor != null) { mProcessor.setRenderMode(mRenderMode); } if (mRenderMode == ViewfinderProcessor.MODE_NORMAL) { mCameraOps.setRepeatingRequest(mPreviewRequest, mCaptureCallback, mUiHandler); } else { setHdrBurst(); } } } /** * Configure the surfaceview and RS processing. */ private void configureSurfaces() { // Find a good size for output - largest 16:9 aspect ratio that's less than 720p final int MAX_WIDTH = 1280; final float TARGET_ASPECT = 16.f / 9.f; final float ASPECT_TOLERANCE = 0.1f; StreamConfigurationMap configs = mCameraInfo.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); Size[] outputSizes = configs.getOutputSizes(SurfaceHolder.class); Size outputSize = outputSizes[0]; float outputAspect = (float) outputSize.getWidth() / outputSize.getHeight(); for (Size candidateSize : outputSizes) { if (candidateSize.getWidth() > MAX_WIDTH) continue; float candidateAspect = (float) candidateSize.getWidth() / candidateSize.getHeight(); boolean goodCandidateAspect = Math.abs(candidateAspect - TARGET_ASPECT) < ASPECT_TOLERANCE; boolean goodOutputAspect = Math.abs(outputAspect - TARGET_ASPECT) < ASPECT_TOLERANCE; if ((goodCandidateAspect && !goodOutputAspect) || candidateSize.getWidth() > outputSize.getWidth()) { outputSize = candidateSize; outputAspect = candidateAspect; } } Log.i(TAG, "Resolution chosen: " + outputSize); // Configure processing mProcessor = new ViewfinderProcessor(mRS, outputSize); setupProcessor(); // Configure the output view - this will fire surfaceChanged mPreviewView.setAspectRatio(outputAspect); mPreviewView.getHolder().setFixedSize(outputSize.getWidth(), outputSize.getHeight()); } /** * Once camera is open and output surfaces are ready, configure the RS processing * and the camera device inputs/outputs. */ private void setupProcessor() { if (mProcessor == null || mPreviewSurface == null) return; mProcessor.setOutputSurface(mPreviewSurface); mProcessingHdrSurface = mProcessor.getInputHdrSurface(); mProcessingNormalSurface = mProcessor.getInputNormalSurface(); List<Surface> cameraOutputSurfaces = new ArrayList<Surface>(); cameraOutputSurfaces.add(mProcessingHdrSurface); cameraOutputSurfaces.add(mProcessingNormalSurface); mCameraOps.setSurfaces(cameraOutputSurfaces); } /** * Start running an HDR burst on a configured camera session */ public void setHdrBurst() { mHdrBuilder.set(CaptureRequest.SENSOR_SENSITIVITY, 1600); mHdrBuilder.set(CaptureRequest.SENSOR_FRAME_DURATION, ONE_SECOND / 30); mHdrBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, mEvenExposure); mHdrBuilder.setTag(mEvenExposureTag); mHdrRequests.set(0, mHdrBuilder.build()); mHdrBuilder.set(CaptureRequest.SENSOR_EXPOSURE_TIME, mOddExposure); mHdrBuilder.setTag(mOddExposureTag); mHdrRequests.set(1, mHdrBuilder.build()); mCameraOps.setRepeatingBurst(mHdrRequests, mCaptureCallback, mUiHandler); } /** * Listener for completed captures * Invoked on UI thread */ private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() { public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { // Only update UI every so many frames // Use an odd number here to ensure both even and odd exposures get an occasional update long frameNumber = result.getFrameNumber(); if (frameNumber % 3 != 0) return; long exposureTime = result.get(CaptureResult.SENSOR_EXPOSURE_TIME); // Format exposure time nicely String exposureText; if (exposureTime > ONE_SECOND) { exposureText = String.format("%.2f s", exposureTime / 1e9); } else if (exposureTime > MILLI_SECOND) { exposureText = String.format("%.2f ms", exposureTime / 1e6); } else if (exposureTime > MICRO_SECOND) { exposureText = String.format("%.2f us", exposureTime / 1e3); } else { exposureText = String.format("%d ns", exposureTime); } Object tag = request.getTag(); Log.i(TAG, "Exposure: " + exposureText); if (tag == mEvenExposureTag) { mEvenExposureText.setText(exposureText); mEvenExposureText.setEnabled(true); mOddExposureText.setEnabled(true); mAutoExposureText.setEnabled(false); } else if (tag == mOddExposureTag) { mOddExposureText.setText(exposureText); mEvenExposureText.setEnabled(true); mOddExposureText.setEnabled(true); mAutoExposureText.setEnabled(false); } else { mAutoExposureText.setText(exposureText); mEvenExposureText.setEnabled(false); mOddExposureText.setEnabled(false); mAutoExposureText.setEnabled(true); } } }; /** * Callbacks for the FixedAspectSurfaceView */ @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { mPreviewSurface = holder.getSurface(); setupProcessor(); } @Override public void surfaceCreated(SurfaceHolder holder) { // ignored } @Override public void surfaceDestroyed(SurfaceHolder holder) { mPreviewSurface = null; } /** * Callbacks for CameraOps */ @Override public void onCameraReady() { // Ready to send requests in, so set them up try { CaptureRequest.Builder previewBuilder = mCameraOps.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); previewBuilder.addTarget(mProcessingNormalSurface); previewBuilder.setTag(mAutoExposureTag); mPreviewRequest = previewBuilder.build(); mHdrBuilder = mCameraOps.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); mHdrBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_OFF); mHdrBuilder.addTarget(mProcessingHdrSurface); switchRenderMode(0); } catch (CameraAccessException e) { String errorMessage = getErrorString(e); showErrorDialog(errorMessage); } } /** * Utility methods */ @Override public void showErrorDialog(String errorMessage) { MessageDialogFragment.newInstance(errorMessage).show(getFragmentManager(), FRAGMENT_DIALOG); } @Override public String getErrorString(CameraAccessException e) { String errorMessage; switch (e.getReason()) { case CameraAccessException.CAMERA_DISABLED: errorMessage = getString(R.string.camera_disabled); break; case CameraAccessException.CAMERA_DISCONNECTED: errorMessage = getString(R.string.camera_disconnected); break; case CameraAccessException.CAMERA_ERROR: errorMessage = getString(R.string.camera_error); break; default: errorMessage = getString(R.string.camera_unknown, e.getReason()); break; } return errorMessage; } }