diff options
-rw-r--r-- | AndroidManifest.xml | 11 | ||||
-rw-r--r-- | jni/feature_stab/src/dbreg/dbreg.cpp | 2 | ||||
-rw-r--r-- | res/layout/camera_control.xml | 1 | ||||
-rw-r--r-- | res/layout/pano_control.xml | 32 | ||||
-rw-r--r-- | res/layout/pano_views.xml | 43 | ||||
-rw-r--r-- | res/layout/panorama.xml | 25 | ||||
-rw-r--r-- | res/values/strings.xml | 3 | ||||
-rw-r--r-- | src/com/android/camera/Storage.java | 2 | ||||
-rw-r--r-- | src/com/android/camera/panorama/CaptureView.java | 131 | ||||
-rw-r--r-- | src/com/android/camera/panorama/PanoUtil.java | 86 | ||||
-rw-r--r-- | src/com/android/camera/panorama/PanoramaActivity.java | 268 | ||||
-rw-r--r-- | src/com/android/camera/panorama/Preview.java | 464 |
12 files changed, 1060 insertions, 8 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 02bcba3..5e13b60 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -20,6 +20,7 @@ <application android:icon="@mipmap/ic_launcher_camera" android:label="@string/camera_label" android:taskAffinity="" + android:theme="@style/ThemeCamera" android:hardwareAccelerated="true"> <receiver android:name="com.android.camera.CameraButtonIntentReceiver"> <intent-filter> @@ -28,10 +29,8 @@ </receiver> <activity android:name="com.android.camera.Camera" android:configChanges="orientation|keyboardHidden" - android:theme="@style/ThemeCamera" android:screenOrientation="landscape" android:clearTaskOnLaunch="true" - android:taskAffinity="android.task.camera" android:windowSoftInputMode="stateAlwaysHidden|adjustPan"> <intent-filter> <action android:name="android.intent.action.MAIN" /> @@ -51,10 +50,8 @@ android:label="@string/video_camera_label" android:configChanges="orientation|keyboardHidden" android:icon="@mipmap/ic_launcher_video_camera" - android:theme="@style/ThemeCamera" android:screenOrientation="landscape" android:clearTaskOnLaunch="true" - android:taskAffinity="android.task.camcorder" android:windowSoftInputMode="stateAlwaysHidden|adjustPan"> <intent-filter> <action android:name="android.media.action.VIDEO_CAMERA" /> @@ -65,6 +62,12 @@ <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> + <activity android:name="com.android.camera.panorama.PanoramaActivity" + android:configChanges="orientation|keyboardHidden" + android:screenOrientation="landscape" + android:clearTaskOnLaunch="true" + android:windowSoftInputMode="stateAlwaysHidden|adjustPan"> + </activity> </application> </manifest> diff --git a/jni/feature_stab/src/dbreg/dbreg.cpp b/jni/feature_stab/src/dbreg/dbreg.cpp index b87cbbd..fb42838 100644 --- a/jni/feature_stab/src/dbreg/dbreg.cpp +++ b/jni/feature_stab/src/dbreg/dbreg.cpp @@ -763,7 +763,6 @@ void db_FrameToReferenceRegistration::GenerateQuarterResImage(const unsigned cha if ( (smooth_val < 0) || (smooth_val > 255)) { return; - //throw(std::exception()); } } @@ -787,7 +786,6 @@ void db_FrameToReferenceRegistration::GenerateQuarterResImage(const unsigned cha if ( (smooth_val < 0) || (smooth_val > 255)) { return; - //throw(std::exception()); } } diff --git a/res/layout/camera_control.xml b/res/layout/camera_control.xml index a7dee7d..f2790fb 100644 --- a/res/layout/camera_control.xml +++ b/res/layout/camera_control.xml @@ -15,7 +15,6 @@ --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:orientation="vertical" android:layout_height="match_parent" android:layout_width="76dp" android:paddingTop="13dp" diff --git a/res/layout/pano_control.xml b/res/layout/pano_control.xml new file mode 100644 index 0000000..530ffc6 --- /dev/null +++ b/res/layout/pano_control.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="match_parent" + android:layout_width="76dp" + android:paddingTop="13dp"> + + <include layout="@layout/review_thumbnail"/> + + <com.android.camera.ShutterButton android:id="@+id/pano_shutter_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:clickable="true" + android:focusable="true" + android:src="@drawable/btn_ic_camera_shutter" + android:background="@drawable/btn_shutter"/> +</RelativeLayout> diff --git a/res/layout/pano_views.xml b/res/layout/pano_views.xml new file mode 100644 index 0000000..ef7738d --- /dev/null +++ b/res/layout/pano_views.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:camera="http://schemas.android.com/apk/res/com.android.camera" + android:id="@+id/pano_views_layout" + android:gravity="center" + android:layout_height="match_parent" + android:layout_width="match_parent" + android:layout_weight="1" + android:layout_marginLeft="2dp"> + <FrameLayout android:id="@+id/pano_preview_layout" + android:gravity="center" + android:layout_width="match_parent" + android:layout_height="720dp"> + <com.android.camera.panorama.Preview + android:id="@+id/pano_preview" + android:layout_gravity="center" + android:layout_width="240dp" + android:layout_height="180dp"/> + <com.android.camera.panorama.CaptureView android:id="@+id/pano_capture_view" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + </FrameLayout> + <ImageView android:id="@+id/pano_reviewarea" + android:scaleType="center" + android:layout_width="match_parent" + android:layout_height="match_parent" /> +</FrameLayout> + diff --git a/res/layout/panorama.xml b/res/layout/panorama.xml new file mode 100644 index 0000000..b92d81d --- /dev/null +++ b/res/layout/panorama.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:camera="http://schemas.android.com/apk/res/com.android.camera" + android:id="@+id/panorama_root" + android:orientation="horizontal" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <include layout="@layout/pano_views"/> + <include layout="@layout/pano_control"/> +</LinearLayout> diff --git a/res/values/strings.xml b/res/values/strings.xml index 7713e51..8772f3a 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -271,6 +271,9 @@ <!-- Video Camera format string for new video files. Passed to java.text.SimpleDateFormat. --> <string name="video_file_name_format" translatable="false">"'VID'_yyyyMMdd_HHmmss"</string> + <!-- Filename prefix for panorama output. --> + <string name="pano_file_name_format" translatable="false">"'PANO'_yyyyMMdd_HHmmss"</string> + <!-- The messsage shown when video record reaches size limit. --> <string name="video_reach_size_limit">Size limit reached.</string> diff --git a/src/com/android/camera/Storage.java b/src/com/android/camera/Storage.java index 86fb443..e7c1553 100644 --- a/src/com/android/camera/Storage.java +++ b/src/com/android/camera/Storage.java @@ -29,7 +29,7 @@ import android.util.Log; import java.io.File; import java.io.FileOutputStream; -class Storage { +public class Storage { private static final String TAG = "CameraStorage"; public static final String DCIM = diff --git a/src/com/android/camera/panorama/CaptureView.java b/src/com/android/camera/panorama/CaptureView.java new file mode 100644 index 0000000..3351527 --- /dev/null +++ b/src/com/android/camera/panorama/CaptureView.java @@ -0,0 +1,131 @@ +/* + * 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.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.util.AttributeSet; +import android.util.Log; +import android.view.View; + +class CaptureView extends View { + private static final String TAG = "CaptureView"; + + private Canvas mCanvas; + private Bitmap mCanvasBitmap; + private String mStatusText = ""; + private int mStartAngle = 0; + private int mSweepAngle = 0; + private int mWidth; + private int mHeight; + private Bitmap mBitmap = null; + private Matrix mM = null; + private Matrix mMLast = null; + private final Paint mPaint = new Paint(); + // Origin of the coordinate for appending a new alpha bitmap. + // mCanvasBitmap is 2000x2000, but the origin is set to (800, 800). + // All the alpha bitmaps grow from this origin. + float mAlphaOriginX; + float mAlphaOriginY; + + + public CaptureView(Context context, AttributeSet attrs) { + super(context, attrs); + mM = new Matrix(); + mMLast = new Matrix(); + mMLast.reset(); + mPaint.setStyle(Paint.Style.FILL); + mPaint.setColor(Color.RED); + mPaint.setAntiAlias(true); + mPaint.setTextSize(40); + mPaint.setTypeface(Typeface.create((Typeface) null, Typeface.BOLD)); + mPaint.setTextAlign(Align.CENTER); + } + + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + if (mCanvasBitmap != null) { + mCanvasBitmap.recycle(); + } + Log.v(TAG, "onSizeChanged: W = " + w + ", H = " + h); + // TODO: 2000x2000 is a temporary setting from SRI's code. Should be fixed once the code is + // refactored. + mCanvasBitmap = Bitmap.createBitmap(2000, 2000, Bitmap.Config.ARGB_8888); + mCanvas = new Canvas(); + mCanvas.setBitmap(mCanvasBitmap); + mAlphaOriginX = mCanvasBitmap.getWidth() * 0.4f; + mAlphaOriginY = mCanvasBitmap.getHeight() * 0.4f; + } + + public void destroy() { + if (mCanvasBitmap != null) { + mCanvasBitmap.recycle(); + } + } + + public void setStartAngle(int angle) { + mStartAngle = angle; + } + + public void setSweepAngle(int angle) { + mSweepAngle = angle; + } + + public void setStatusText(String text) { + mStatusText = text; + } + + public void setBitmap(Bitmap bitmap, Matrix m) { + mBitmap = bitmap; + mM = m; + } + + public void setBitmap(Bitmap bitmap) { + mBitmap = bitmap; + } + + @Override + protected void onDraw(Canvas canvas) { + mWidth = getWidth(); + mHeight = getHeight(); + + // Draw bitmaps according to the calculated panorama transformation. + if (mBitmap != null) { + mM.postTranslate(mAlphaOriginX, mAlphaOriginY); + mCanvas.drawBitmap(mBitmap, mM, mPaint); + + Matrix mInverse = mM; + mM.invert(mInverse); + mInverse.postTranslate(mWidth / 2 - mBitmap.getWidth() / 2, + mHeight / 2 - mBitmap.getHeight() / 2); + + canvas.drawBitmap(mCanvasBitmap, mInverse, mPaint); + + RectF rect = new RectF(mWidth / 2 - 100, 3 * mHeight / 4, + mWidth / 2 + 100, 3 * mHeight / 4 + 200); + canvas.drawText(mStatusText, mWidth / 2, mHeight / 2, mPaint); + canvas.drawArc(rect, -90 + mStartAngle, mSweepAngle, true, mPaint); + canvas.drawArc(rect, -90 - mStartAngle, mSweepAngle > 0 ? 2 : 0, true, mPaint); + } + } +} diff --git a/src/com/android/camera/panorama/PanoUtil.java b/src/com/android/camera/panorama/PanoUtil.java new file mode 100644 index 0000000..ef778a3 --- /dev/null +++ b/src/com/android/camera/panorama/PanoUtil.java @@ -0,0 +1,86 @@ +/* + * 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 java.text.SimpleDateFormat; +import java.util.Date; + +public class PanoUtil { + public static String createName(String format, long dateTaken) { + Date date = new Date(dateTaken); + SimpleDateFormat dateFormat = new SimpleDateFormat(format); + return dateFormat.format(date); + } + + // TODO: Add comments about the range of these two arguments. + public static double calculateDifferenceBetweenAngles(double firstAngle, + double secondAngle) { + double difference1 = (secondAngle - firstAngle) % 360; + if (difference1 < 0) { + difference1 += 360; + } + + double difference2 = (firstAngle - secondAngle) % 360; + if (difference2 < 0) { + difference2 += 360; + } + + return Math.min(difference1, difference2); + } + + public static void decodeYUV420SPQuarterRes(int[] rgb, byte[] yuv420sp, int width, int height) { + final int frameSize = width * height; + + for (int j = 0, ypd = 0; j < height; j += 4) { + int uvp = frameSize + (j >> 1) * width, u = 0, v = 0; + for (int i = 0; i < width; i += 4, ypd++) { + int y = (0xff & ((int) yuv420sp[j * width + i])) - 16; + if (y < 0) { + y = 0; + } + if ((i & 1) == 0) { + v = (0xff & yuv420sp[uvp++]) - 128; + u = (0xff & yuv420sp[uvp++]) - 128; + uvp += 2; // Skip the UV values for the 4 pixels skipped in between + } + int y1192 = 1192 * y; + int r = (y1192 + 1634 * v); + int g = (y1192 - 833 * v - 400 * u); + int b = (y1192 + 2066 * u); + + if (r < 0) { + r = 0; + } else if (r > 262143) { + r = 262143; + } + if (g < 0) { + g = 0; + } else if (g > 262143) { + g = 262143; + } + if (b < 0) { + b = 0; + } else if (b > 262143) { + b = 262143; + } + + rgb[ypd] = 0xff000000 | ((r << 6) & 0xff0000) | ((g >> 2) & 0xff00) | + ((b >> 10) & 0xff); + } + } + } +} diff --git a/src/com/android/camera/panorama/PanoramaActivity.java b/src/com/android/camera/panorama/PanoramaActivity.java new file mode 100644 index 0000000..5d18525 --- /dev/null +++ b/src/com/android/camera/panorama/PanoramaActivity.java @@ -0,0 +1,268 @@ +/* + * 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.app.Activity; +import android.content.Context; +import android.graphics.PixelFormat; +import android.hardware.Camera.Parameters; +import android.hardware.Camera.Size; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.WindowManager; +import android.widget.ImageView; + +import com.android.camera.CameraDisabledException; +import com.android.camera.CameraHardwareException; +import com.android.camera.CameraHolder; +import com.android.camera.R; +import com.android.camera.ShutterButton; +import com.android.camera.Util; + +import java.util.List; + +public class PanoramaActivity extends Activity { + public static final int DEFAULT_SWEEP_ANGLE = 60; + public static final int DEFAULT_BLEND_MODE = Mosaic.BLENDTYPE_HORIZONTAL; + public static final int DEFAULT_CAPTURE_PIXELS = 960 * 720; + + private static final String TAG = "PanoramaActivity"; + private static final float NS2S = 1.0f / 1000000000.0f; // TODO: commit for this constant. + + private Preview mPreview; + private ImageView mReview; + private CaptureView mCaptureView; + private ShutterButton mShutterButton; + private int mPreviewWidth; + private int mPreviewHeight; + private android.hardware.Camera mCameraDevice; + private SensorManager mSensorManager; + private Sensor mSensor; + + @Override + public void onCreate(Bundle icicle) { + super.onCreate(icicle); + + getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON, + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + createContentView(); + + mSensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE); + + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE); + if (mSensor == null) { + mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_ORIENTATION); + } + } + + private void setupCamera() { + openCamera(); + Parameters parameters = mCameraDevice.getParameters(); + setupCaptureParams(parameters); + configureCamera(parameters); + } + + private void openCamera() { + try { + mCameraDevice = Util.openCamera(this, CameraHolder.instance().getBackCameraId()); + } catch (CameraHardwareException e) { + Util.showErrorAndFinish(this, R.string.cannot_connect_camera); + return; + } catch (CameraDisabledException e) { + Util.showErrorAndFinish(this, R.string.camera_disabled); + return; + } + } + + private boolean findBestPreviewSize(List<Size> supportedSizes, boolean need4To3, + boolean needSmaller) { + int pixelsDiff = DEFAULT_CAPTURE_PIXELS; + boolean hasFound = false; + for (Size size: supportedSizes) { + int h = size.height; + int w = size.width; + // we only want 4:3 format. + int d = DEFAULT_CAPTURE_PIXELS - h * w; + if (needSmaller && d < 0) { // no bigger preview than 960x720. + continue; + } + if (need4To3 && (h * 4 != w * 3)) { + continue; + } + d = Math.abs(d); + if (d < pixelsDiff) { + mPreviewWidth = w; + mPreviewHeight = h; + pixelsDiff = d; + hasFound = true; + } + } + return hasFound; + } + + private void setupCaptureParams(Parameters parameters) { + List<Size> supportedSizes = parameters.getSupportedPreviewSizes(); + if (!findBestPreviewSize(supportedSizes, true, true)) { + Log.w(TAG, "No 4:3 ratio preview size supported."); + if (!findBestPreviewSize(supportedSizes, false, true)) { + Log.w(TAG, "Can't find a supported preview size smaller than 960x720."); + findBestPreviewSize(supportedSizes, false, false); + } + } + Log.v(TAG, "preview h = " + mPreviewHeight + " , w = " + mPreviewWidth); + parameters.setPreviewSize(mPreviewWidth, mPreviewHeight); + + List<int[]> frameRates = parameters.getSupportedPreviewFpsRange(); + int last = frameRates.size() - 1; + int minFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MIN_INDEX]; + int maxFps = (frameRates.get(last))[Parameters.PREVIEW_FPS_MAX_INDEX]; + parameters.setPreviewFpsRange(minFps, maxFps); + Log.v(TAG, "preview fps: " + minFps + ", " + maxFps); + } + + public int getPreviewBufSize() { + PixelFormat pixelInfo = new PixelFormat(); + PixelFormat.getPixelFormatInfo(mCameraDevice.getParameters().getPreviewFormat(), pixelInfo); + // TODO: remove this extra 32 byte after the driver bug is fixed. + return (mPreviewWidth * mPreviewHeight * pixelInfo.bitsPerPixel / 8) + 32; + } + + private void configureCamera(Parameters parameters) { + mCameraDevice.setParameters(parameters); + + int bufSize = getPreviewBufSize(); + Log.v(TAG, "BufSize = " + bufSize); + for (int i = 0; i < 10; i++) { + try { + mCameraDevice.addCallbackBuffer(new byte[bufSize]); + } catch (OutOfMemoryError e) { + Log.v(TAG, "Buffer allocation failed: buffer " + i); + break; + } + } + } + + private void createContentView() { + setContentView(R.layout.panorama); + + mPreview = (Preview) findViewById(R.id.pano_preview); + mCaptureView = (CaptureView) findViewById(R.id.pano_capture_view); + mCaptureView.setStartAngle(-DEFAULT_SWEEP_ANGLE / 2); + mCaptureView.setVisibility(View.INVISIBLE); + + mReview = (ImageView) findViewById(R.id.pano_reviewarea); + mReview.setVisibility(View.INVISIBLE); + + mShutterButton = (ShutterButton) findViewById(R.id.pano_shutter_button); + mShutterButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + mPreview.setCaptureStarted(DEFAULT_SWEEP_ANGLE, DEFAULT_BLEND_MODE); + } + }); + } + + public void showResultingMosaic(String uri) { + Uri parsed = Uri.parse(uri); + mReview.setImageURI(parsed); + mReview.setVisibility(View.VISIBLE); + mPreview.setVisibility(View.INVISIBLE); + mCaptureView.setVisibility(View.INVISIBLE); + } + + @Override + protected void onPause() { + super.onPause(); + mPreview.onPause(); + mSensorManager.unregisterListener(mListener); + releaseCamera(); + } + + @Override + protected void onResume() { + super.onResume(); + + /* + * It is not necessary to get accelerometer events at a very high + * rate, by using a slower rate (SENSOR_DELAY_UI), we get an + * automatic low-pass filter, which "extracts" the gravity component + * of the acceleration. As an added benefit, we use less power and + * CPU resources. + */ + mSensorManager.registerListener(mListener, mSensor, SensorManager.SENSOR_DELAY_UI); + + setupCamera(); + mPreview.setCameraDevice(mCameraDevice); + mCameraDevice.startPreview(); + } + + private void releaseCamera() { + if (mCameraDevice != null){ + CameraHolder.instance().release(); + mCameraDevice = null; + } + } + + private final SensorEventListener mListener = new SensorEventListener() { + private float mCompassCurrX; // degrees + private float mCompassCurrY; // degrees + private float mTimestamp; + + public void onSensorChanged(SensorEvent event) { + + if (event.sensor.getType() == Sensor.TYPE_GYROSCOPE) { + if (mTimestamp != 0) { + final float dT = (event.timestamp - mTimestamp) * NS2S; + mCompassCurrX += event.values[1] * dT * 180.0f / Math.PI; + mCompassCurrY += event.values[0] * dT * 180.0f / Math.PI; + } + mTimestamp = event.timestamp; + + } else if (event.sensor.getType() == Sensor.TYPE_ORIENTATION) { + mCompassCurrX = event.values[0]; + mCompassCurrY = event.values[1]; + } + + if (mPreview != null) { + mPreview.updateCompassValue(mCompassCurrX, mCompassCurrY); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + }; + + public int getPreviewFrameWidth() { + return mPreviewWidth; + } + + public int getPreviewFrameHeight() { + return mPreviewHeight; + } + + public CaptureView getCaptureView() { + return mCaptureView; + } +} 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) { + } + } +} |