summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorWei-Ta Chen <weita@google.com>2011-06-14 16:53:04 -0700
committerAngus Kong <shkong@google.com>2011-07-27 07:37:20 +0800
commitbdc7e2b461064b25b7f17b0941077fc97653093d (patch)
tree98bca1531a7e5eec6f7a531cd2a89901bb419e21
parentf8e7cdfca602c5f9699b54f25e1552fc20fadd15 (diff)
downloadLegacyCamera-bdc7e2b461064b25b7f17b0941077fc97653093d.zip
LegacyCamera-bdc7e2b461064b25b7f17b0941077fc97653093d.tar.gz
LegacyCamera-bdc7e2b461064b25b7f17b0941077fc97653093d.tar.bz2
Check in the Mosaic Stitching codes.
1. Camera setup moved to activity level. 2. Fixed releasing and acquiring camera process. 3. Unused .xml files removed. 4. Style issues fixed. Bug: 5031489 Change-Id: Ifd271588ca2168398e17f204f065681ead2d8f2f
-rw-r--r--AndroidManifest.xml11
-rw-r--r--jni/feature_stab/src/dbreg/dbreg.cpp2
-rw-r--r--res/layout/camera_control.xml1
-rw-r--r--res/layout/pano_control.xml32
-rw-r--r--res/layout/pano_views.xml43
-rw-r--r--res/layout/panorama.xml25
-rw-r--r--res/values/strings.xml3
-rw-r--r--src/com/android/camera/Storage.java2
-rw-r--r--src/com/android/camera/panorama/CaptureView.java131
-rw-r--r--src/com/android/camera/panorama/PanoUtil.java86
-rw-r--r--src/com/android/camera/panorama/PanoramaActivity.java268
-rw-r--r--src/com/android/camera/panorama/Preview.java464
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) {
+ }
+ }
+}