diff options
Diffstat (limited to 'src/com/android/camera/panorama/Preview.java')
-rw-r--r-- | src/com/android/camera/panorama/Preview.java | 464 |
1 files changed, 464 insertions, 0 deletions
diff --git a/src/com/android/camera/panorama/Preview.java b/src/com/android/camera/panorama/Preview.java new file mode 100644 index 0000000..2d5ddd2 --- /dev/null +++ b/src/com/android/camera/panorama/Preview.java @@ -0,0 +1,464 @@ +/* + * Copyright (C) 2011 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.android.camera.panorama; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.ImageFormat; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.YuvImage; +import android.hardware.Camera; +import android.media.MediaScannerConnection; +import android.media.MediaScannerConnection.MediaScannerConnectionClient; +import android.net.Uri; +import android.os.Handler; +import android.util.AttributeSet; +import android.util.Log; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; + +import com.android.camera.R; +import com.android.camera.Storage; + +import java.io.File; +import java.io.FileOutputStream; +import java.util.ArrayList; + +class Preview extends SurfaceView implements SurfaceHolder.Callback, + Camera.PreviewCallback { + private static final String TAG = "Preview"; + private static final boolean LOGV = true; + private static final int NUM_FRAMES_IN_BUFFER = 2; + private static final int MAX_NUMBER_OF_FRAMES = 100; + private static final int DOWN_SAMPLE_SIZE = 4; + + private final Object mLockFillIn = new Object(); // Object used for synchronization of mFillIn + private final byte[][] mFrames = new byte[NUM_FRAMES_IN_BUFFER][]; // Space for N frames + private final long [] mFrameTimestamp = new long[NUM_FRAMES_IN_BUFFER]; + + private PanoramaActivity mActivity; + private Mosaic mMosaicer; + private LowResFrameProcessor mLowResProcessor = null; + private SurfaceHolder mHolder; + + private android.hardware.Camera mCameraDevice; + + private Bitmap mLRBitmapAlpha = null; + private Matrix mTransformationMatrix = null; + + private int mFillIn = 0; + private long mLastProcessedFrameTimestamp = 0; + private int mTotalFrameCount = 0; + private int[] mColors = null; + + private int mPreviewWidth; + private int mPreviewHeight; + + private float mTranslationLastX; + private float mTranslationLastY; + private float mTranslationRate; + + private ScannerClient mScannerClient; + + private String mCurrentImagePath = null; + private long mTimeTaken; + + // Need handler for callbacks to the UI thread + private final Handler mHandler = new Handler(); + + // Create runnable for posting + private final Runnable mUpdateResults = new Runnable() { + public void run() { + mActivity.showResultingMosaic("file://" + mCurrentImagePath); + mScannerClient.scanPath(mCurrentImagePath); + } + }; + + public Preview(Context context, AttributeSet attrs) { + super(context, attrs); + + mActivity = (PanoramaActivity) getContext(); + + mMosaicer = new Mosaic(); + mScannerClient = new ScannerClient(getContext()); + + // Install a SurfaceHolder.Callback so we get notified when the + // underlying surface is created and destroyed. + mHolder = getHolder(); + mHolder.addCallback(this); + mHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + } + + private class LowResFrameProcessor { + private CaptureView mCaptureView; + int mLastProcessFrameIdx = -1; + int mCurrProcessFrameIdx = -1; + boolean mRun = true; + + private float mCompassValueX; + private float mCompassValueY; + private float mCompassValueXStart; + private float mCompassValueYStart; + private int mCompassThreshold; + private int mTraversedAngleX; + private int mTraversedAngleY; + + + public LowResFrameProcessor(int sweepAngle, CaptureView overlayer) { + mCompassThreshold = sweepAngle; + mCaptureView = overlayer; + } + + // Processes the last filled image frame through the mosaicer and + // updates the UI to show progress. + // When done, processes and displays the final mosaic. + public void runEachFrame() { + mCurrProcessFrameIdx = getLastFilledIn(); + + // Check that we are trying to process a frame different from the + // last one processed (useful if this class was running asynchronously) + if (mCurrProcessFrameIdx != mLastProcessFrameIdx) { + mLastProcessFrameIdx = mCurrProcessFrameIdx; + + if (LOGV) Log.v(TAG, "Processing: [" + mCurrProcessFrameIdx + "]"); + + // Access the image data and the timestamp associated with it... + byte[] data = mFrames[mCurrProcessFrameIdx]; + long timestamp = mFrameTimestamp[mCurrProcessFrameIdx]; + + // Keep track of what compass bearing we started at... + if (mTotalFrameCount == 0) { // First frame + mCompassValueXStart = mCompassValueX; + mCompassValueYStart = mCompassValueY; + } + + // By what angle has the camera moved since start of capture? + mTraversedAngleX = (int) PanoUtil.calculateDifferenceBetweenAngles( + mCompassValueX, mCompassValueXStart); + mTraversedAngleY = (int) PanoUtil.calculateDifferenceBetweenAngles( + mCompassValueY, mCompassValueYStart); + + if (mTotalFrameCount <= MAX_NUMBER_OF_FRAMES + && mTraversedAngleX < mCompassThreshold + && mTraversedAngleY < mCompassThreshold) { + // If we are still collecting new frames for the current mosaic, + // process the new frame. + processFrame(data, timestamp); + + // Publish progress of the ongoing processing + publishProgress(0); + } else { + // Publish progress that we are done with capture + publishProgress(1); + + // Background-process the final blending of the mosaic so + // that the UI is not blocked. + Thread t = new Thread() { + @Override + public void run() { + generateAndStoreFinalMosaic(false); + } + }; + t.start(); + + mRun = false; + } + } + } + + // Sets the screen layout before starting each fresh capture. + protected void onPreExecute() { + if (mTotalFrameCount == 0) { + mCaptureView.setVisibility(View.VISIBLE); + } + } + + // Updates the GUI with ongoing updates if values[0]==0 and + // with the constructed mosaic for values[0]==1. + public void publishProgress(Integer... values) { + long t1 = System.currentTimeMillis(); + + if (values[0] == 0) { // Ongoing + // This updates the real-time mosaic display with the current image frame and the + // transformation matrix to warp it by. + mCaptureView.setBitmap(mLRBitmapAlpha, mTransformationMatrix); + + // Update the sweep-angle sector display and show "SLOW DOWN" message if the user + // is moving the camera too fast + if (mTranslationRate > 150) { + // TODO: remove the text and draw implications according to the UI spec. + mCaptureView.setStatusText("S L O W D O W N"); + mCaptureView.setSweepAngle( + Math.max(mTraversedAngleX, mTraversedAngleY) + 1); + mCaptureView.invalidate(); + } else { + mCaptureView.setStatusText(""); + mCaptureView.setSweepAngle( + Math.max(mTraversedAngleX, mTraversedAngleY) + 1); + mCaptureView.invalidate(); + } + } else { // Done + setVisibility(View.INVISIBLE); + mCaptureView.setVisibility(View.INVISIBLE); + mCaptureView.setBitmap(null); + mCaptureView.setStatusText(""); + mCaptureView.setSweepAngle(0); + mCaptureView.invalidate(); + } + + long t2 = System.currentTimeMillis(); + } + + public void updateCompassValue(float valueX, float valueY) { + mCompassValueX = valueX; + mCompassValueY = valueY; + } + } + + public void updateCompassValue(float valueX, float valueY) { + if (mLowResProcessor != null) { + mLowResProcessor.updateCompassValue(valueX, valueY); + } + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + } + + private void setupMosaicer() { + mPreviewWidth = mActivity.getPreviewFrameWidth(); + mPreviewHeight = mActivity.getPreviewFrameHeight(); + + mMosaicer.setSourceImageDimensions(mPreviewWidth, mPreviewHeight); + + mColors = new int[(mActivity.getPreviewFrameWidth() / DOWN_SAMPLE_SIZE) + * (mActivity.getPreviewFrameHeight() / DOWN_SAMPLE_SIZE)]; + mLRBitmapAlpha = Bitmap.createBitmap((mPreviewWidth / DOWN_SAMPLE_SIZE), + (mPreviewHeight / DOWN_SAMPLE_SIZE), Config.ARGB_8888); + mTransformationMatrix = new Matrix(); + + int bufSize = mActivity.getPreviewBufSize(); + for (int i = 0; i < NUM_FRAMES_IN_BUFFER; i++) { + mFrames[i] = new byte[bufSize]; + } + } + + public void setCameraDevice(android.hardware.Camera camera) { + setupMosaicer(); + + mCameraDevice = camera; + // Preview callback used whenever new viewfinder frame is available + mCameraDevice.setPreviewCallbackWithBuffer(this); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, int w, int h) { + try { + mCameraDevice.setPreviewDisplay(holder); + } catch (Throwable ex) { + throw new RuntimeException("setPreviewDisplay failed", ex); + } + } + + public void onPreviewFrame(final byte[] data, Camera camera) { + long t1 = System.currentTimeMillis(); + synchronized (mLockFillIn) { + mFrameTimestamp[mFillIn] = t1; + System.arraycopy(data, 0, mFrames[mFillIn], 0, data.length); + } + incrementFillIn(); + + if (mLowResProcessor != null && mLowResProcessor.mRun) { + mLowResProcessor.runEachFrame(); + } + + // The returned buffer needs be added back to callback buffer again. + if (mCameraDevice != null) { + mCameraDevice.addCallbackBuffer(data); + } + } + + public void processFrame(final byte[] data, long now) { + float deltaTime = (float) (now - mLastProcessedFrameTimestamp) / 1000.0f; + mLastProcessedFrameTimestamp = now; + + long t1 = System.currentTimeMillis(); + + float[] frameData = mMosaicer.setSourceImage(data); + + mTotalFrameCount = (int) frameData[9]; + float translationCurrX = frameData[2]; + float translationCurrY = frameData[5]; + + long t2 = System.currentTimeMillis(); + + Log.v(TAG, "[ " + deltaTime + " ] AddFrame: " + (t2 - t1)); + + t1 = System.currentTimeMillis(); + mTransformationMatrix.setValues(frameData); + + int outw = mPreviewWidth / DOWN_SAMPLE_SIZE; + int outh = mPreviewHeight / DOWN_SAMPLE_SIZE; + + PanoUtil.decodeYUV420SPQuarterRes(mColors, data, mPreviewWidth, mPreviewHeight); + + mLRBitmapAlpha.setPixels(mColors, 0, outw, 0, 0, outw, outh); + + t2 = System.currentTimeMillis(); + Log.v(TAG, "GenerateLowResBitmap: " + (t2 - t1)); + + mTranslationRate = Math.max(Math.abs(translationCurrX - mTranslationLastX), + Math.abs(translationCurrY - mTranslationLastY)) / deltaTime; + mTranslationLastX = translationCurrX; + mTranslationLastY = translationCurrY; + } + + public void generateAndStoreFinalMosaic(boolean highRes) { + long t1 = System.currentTimeMillis(); + + mMosaicer.createMosaic(highRes); + + mCurrentImagePath = Storage.DIRECTORY + "/" + PanoUtil.createName( + mActivity.getResources().getString(R.string.pano_file_name_format), mTimeTaken); + + if (highRes) { + mCurrentImagePath += "_HR.jpg"; + } else { + mCurrentImagePath += "_LR.jpg"; + } + + long t2 = System.currentTimeMillis(); + long dur = (t2 - t1) / 1000; + + try { + File mosDirectory = new File(Storage.DIRECTORY); + // have the object build the directory structure, if needed. + mosDirectory.mkdirs(); + + byte[] imageData = mMosaicer.getFinalMosaicNV21(); + int len = imageData.length - 8; + + int width = (imageData[len + 0] << 24) + ((imageData[len + 1] & 0xFF) << 16) + + ((imageData[len + 2] & 0xFF) << 8) + (imageData[len + 3] & 0xFF); + int height = (imageData[len + 4] << 24) + ((imageData[len + 5] & 0xFF) << 16) + + ((imageData[len + 6] & 0xFF) << 8) + (imageData[len + 7] & 0xFF); + Log.v(TAG, "ImLength = " + (len) + ", W = " + width + ", H = " + height); + + YuvImage yuvimage = new YuvImage(imageData, ImageFormat.NV21, width, height, null); + FileOutputStream out = new FileOutputStream(mCurrentImagePath); + yuvimage.compressToJpeg(new Rect(0, 0, width, height), 100, out); + out.close(); + + // Now's a good time to run the GC. Since we won't do any explicit + // allocation during the test, the GC should stay dormant and not + // influence our results. + System.runFinalization(); + System.gc(); + + mHandler.post(mUpdateResults); + } catch (Exception e) { + Log.e(TAG, "exception in storing final mosaic", e); + } + } + + public void setCaptureStarted(int sweepAngle, int blendType) { + // Reset values so we can do this again. + mMosaicer.reset(); + mTotalFrameCount = 0; + mLastProcessedFrameTimestamp = 0; + + mTimeTaken = System.currentTimeMillis(); + CaptureView captureView = mActivity.getCaptureView(); + captureView.setVisibility(View.VISIBLE); + mLowResProcessor = new LowResFrameProcessor(sweepAngle - 5, captureView); + + mLowResProcessor.onPreExecute(); + } + + /** + * This must be called when the activity pauses (in Activity.onPause). + */ + public void onPause() { + mMosaicer.reset(); + } + + private int getLastFilledIn() { + synchronized (mLockFillIn) { + if (mFillIn > 0) { + return mFillIn - 1; + } else { + return NUM_FRAMES_IN_BUFFER - 1; + } + } + } + + private void incrementFillIn() { + synchronized (mLockFillIn) { + mFillIn = ((mFillIn + 1) >= NUM_FRAMES_IN_BUFFER) ? 0 : (mFillIn + 1); + } + } + + /** + * Inner class to tell the gallery app to scan the newly created mosaic images. + * TODO: insert the image to media store. + */ + private static final class ScannerClient implements MediaScannerConnectionClient { + ArrayList<String> mPaths = new ArrayList<String>(); + MediaScannerConnection mScannerConnection; + boolean mConnected; + Object mLock = new Object(); + + public ScannerClient(Context context) { + mScannerConnection = new MediaScannerConnection(context, this); + } + + public void scanPath(String path) { + synchronized (mLock) { + if (mConnected) { + mScannerConnection.scanFile(path, null); + } else { + mPaths.add(path); + mScannerConnection.connect(); + } + } + } + + @Override + public void onMediaScannerConnected() { + synchronized (mLock) { + mConnected = true; + if (!mPaths.isEmpty()) { + for (String path : mPaths) { + mScannerConnection.scanFile(path, null); + } + mPaths.clear(); + } + } + } + + @Override + public void onScanCompleted(String path, Uri uri) { + } + } +} |