chapter 15 照相功能 - hscc homehscc.cs.nctu.edu.tw/~lincyu/android/chapter15.pdf ·...
TRANSCRIPT
Chapter 15 照相功能
作者: 林致孙
照相功能已成為手機的基本功能,智慧型手機當然也一定有照相的功能,本章首
先會說明如何存取相機,接著會對擴增實境做個簡介,也會提供一個簡單的程式
範例。
15.1 照相功能
讀者可能會問:手機上已經有相機程式了,為什麼還需要另外寫一個相機程式呢?
因為有時候照相可能只是您的應用程式的一部份,例如一個『旅遊心得記錄』應
用程式,使用者去某個景點觀光時,可能想寫些心得並放上景點的照片,最後並
將旅遊心得傳送至自己的部落格,如果能直接於應用程式內使用照相功能,應該
會更方便,因此這一節中我們將學習如何寫出基本的照片功能。請讀者引進光碟
中『\範例程式\Chapter15\CameraBasic』這個專案。CameraBasic.java的內容如下:
1 public class CameraBasic extends Activity {
2
3 private SurfaceView sv;
4 private SurfaceHolder sh;
5 private Camera camera;
6
7 private TextView tv;
8
9 private MySensorListener msl;
10 private SensorManager smgr;
11 private List<Sensor> slist;
12
13 @Override
14 public void onCreate(Bundle savedInstanceState) {
15 super.onCreate(savedInstanceState);
16
17 requestWindowFeature(Window.FEATURE_NO_TITLE);
18 getWindow().setFlags(WindowManager.
19 LayoutParams.FLAG_FULLSCREEN,
20 WindowManager.LayoutParams.
21 FLAG_FULLSCREEN);
22 setRequestedOrientation(ActivityInfo.
23 SCREEN_ORIENTATION_LANDSCAPE);
24
25 setContentView(R.layout.takepicture);
26
27 sv = (SurfaceView)findViewById(R.id.sv);
28 sh = sv.getHolder();
29 sh.addCallback(new MySHCallback());
30
31 sh.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
32
33 smgr = (SensorManager)getSystemService(
34 Context.SENSOR_SERVICE);
35 msl = new MySensorListener();
36
37 slist = smgr.getSensorList(Sensor.TYPE_ACCELEROMETER);
38
39 tv = new TextView(this);
40 tv.setTextSize(30);
41 tv.setText("相機是正的");
42
43 LayoutParams lp = new LayoutParams(
44 LayoutParams.FILL_PARENT,
45 LayoutParams.WRAP_CONTENT);
46
47 this.addContentView(tv, lp);
48 }
49
50
51 public boolean onKeyDown (int keyCode, KeyEvent event) {
52 if (keyCode != KeyEvent.KEYCODE_DPAD_CENTER) {
53 return super.onKeyDown(keyCode, event);
54 }
55 if (camera != null) {
56 camera.takePicture(null, null, jpeg);
57 }
58 return true;
59 }
60
61 private PictureCallback jpeg = new PictureCallback() {
62 public void onPictureTaken(byte[] data, Camera camera) {
63 Bitmap bm = BitmapFactory.
64 decodeByteArray(data, 0, data.length);
65
66 FileOutputStream fos = null;
67 try {
68 File file = new File("/sdcard/picture.jpg");
69 fos = new FileOutputStream(file);
70 BufferedOutputStream bos = new
71 BufferedOutputStream(fos);
72 bm.compress(Bitmap.CompressFormat.JPEG, 80, bos);
73 bos.flush();
74 bos.close();
75 Toast.makeText(CameraBasic.this, "照片已存檔",
76 Toast.LENGTH_SHORT).show();
77 camera.startPreview();
78 }catch (Exception e) {
79 Toast.makeText(CameraBasic.this, e.toString(),
80 Toast.LENGTH_SHORT).show();
81 }
82 }
83 };
84
85 @Override
86 protected void onResume() {
87 super.onResume();
88 if (slist.size() != 0)
89 smgr.registerListener(msl, slist.get(0),
90 SensorManager.SENSOR_DELAY_UI);
91 }
92
93 @Override
94 protected void onPause() {
95 super.onPause();
96 if (slist.size() != 0)
97 smgr.unregisterListener(msl, slist.get(0));
98 }
99
100 class MySHCallback implements SurfaceHolder.Callback {
101 public void surfaceCreated(SurfaceHolder holder) {
102 camera = Camera.open();
103
104 if (camera == null) {
105 Toast.makeText(CameraBasic.this, "camera is null",
106 Toast.LENGTH_SHORT).show();
107 finish();
108 }
109
110 Camera.Parameters params = camera.getParameters();
111 params.setPictureFormat(PixelFormat.JPEG);
112 params.setPictureSize(480, 320);
113 camera.setParameters(params);
114
115 try {
116 camera.setPreviewDisplay(sh);
117 } catch (Exception e) {
118 Toast.makeText(CameraBasic.this, e.toString(),
119 Toast.LENGTH_SHORT).show();
120 finish();
121 }
122 }
123 public void surfaceDestroyed(SurfaceHolder surfaceholder) {
124 camera.stopPreview();
125 camera.release();
126 }
127 public void surfaceChanged(SurfaceHolder surfaceholder,
128 int format , int w, int h) {
129 camera.startPreview();
130 }
131 }
132
133 class MySensorListener implements SensorEventListener {
134 public void onSensorChanged (SensorEvent event) {
135 if (event.sensor != slist.get(0)) return;
136 if (event.values[0] > 9.3 || event.values[1] > 9.3) {
137 tv.setText("相機是正的");
138 } else {
139 tv.setText("相機拿歪了喔");
140 }
141 }
142 public void onAccuracyChanged(Sensor sensor, int accuracy) {
143 }
144 }
145 }
首先在 17~23行,設定程式會以全螢幕且橫向的方式執行。這個應用程式的版面
設計描述檔內只有一個 SurfaceView,因此這個 SurfaceView會佔滿了整個螢幕,
SurfaceView在第八章有做過介紹了,SurfaceView介面元件可以想成是一塊畫布,
程式開發者可以在上面畫上任何東西,在這個程式中 SurfaceView是用來顯示從
相機取得的畫面。SurfaceView的存取要透過 SurfaceHolder物件,而 SurfaceHolde
類別的 addCallback方法能讓我們設定當 SurfaceView建立、改變與銷毀時該做
的事,因此我們可於 SurfaceView建立時打開相機、啟動預覽功能,而於銷毀時
關掉相機,相關的程式碼請讀者參閱 27~31行及 100~131行,相關的類別則有
Camera [1], SurfaceView [2], SurfaceHolder [3]等。
上述即為照相預覽功能的運作原理,接下來要說明程式的拍攝功能,程式中是設
定按下手機的軌跡球中心會做拍攝的動作,這部份可透過覆寫 Activity類別的
onKeyDown方法來完成[4],程式碼是位於 51~59行,當手機有按鍵被按下時,
這個方法會被呼叫,透過按鍵代碼(KeyCode)可以知道是哪一個鍵被按下(完整的
按鍵代碼可參考 KeyEvent類別的說明文件[5]),我們要處理的是按下軌跡球中心
(KEYCODE_DPAD_CENTER)的事件,所以若按鍵代碼不是
KEYCODE_DPAD_CENTER記得呼叫父類別的 onKeyDown方法來處理那些按
鍵按下的動作(程式第 53行),否則會發現其它按鍵等都無法使用了,而若按下
的是軌跡球中心,會呼叫 Camera類別的 takePicture方法[1]來做拍攝的動作,
takPicture方法有多重定義,程式採用了下面這個定義:
public final void takePicture (Camera.ShutterCallback shutter,
Camera.PictureCallback raw, Camera.PictureCallback jpeg)
第一個參數 shutter可以讓我們設定快門聲,如果我們實作了
Camera.ShutterCallback介面的 onShutter方法,並產生對應的物件填入第一個參
數,則使用者按下拍攝功能後,onShutter方法內的程式碼會被執行,如果我們
不需要快門聲,可以直接填 null。第二個參數 raw可以讓我們設定當原始圖檔準
備好時該做什麼事,如果我們實作了 Camera.PictureCallback介面的
onPictureTaken方法,並產生對應的物件填入第二個參數,則當按下拍攝功能,
且原始圖檔資料已準備好讀取時,onPictureTaken方法內的程式碼會被執行。第
三個參數 jpeg可以讓我們設定當壓縮圖檔準備好時該做什麼事,如果我們實作
了 Camera.PictureCallback介面的 onPictureTaken方法,並產生對應的物件填入第
三個參數,則當按下拍攝功能,且壓縮圖檔的資料已準備好讀取時,
onPictureTaken方法內的程式碼會被執行,程式在 61~83行利用匿名類別的技巧
產生了一個實作 Camera.PictureCallback介面的匿名類別的物件,並將該物件填
入 takePicture方法當第三個參數,因此當壓縮圖檔可讀取時,62~82行的程式碼
就會被執行,接下來便要對對此段程式碼做解說。首先,利用 BitmapFactor類別
的 decodeByteArray方法產生一個 Bitmap物件,再利用 Bitmap類別的 compress
方法產生檔案,筆者是建議讀者可先利用模仿的技巧修改範例程式,不必要求自
己一開始就要完全瞭解整個程式流程。最後要提醒讀者的是,takePicture方法會
停止預覽功能,因此程式在第 77行重新呼叫 Camera類別的 startPreview方法來
啟動預覽功能。
此程式除了基本的照相功能還附加了一個小功能:檢查手機是否有拿正,判斷方
法是利用加速度感測器,當使用者以直立的方式拿著手機時,Y軸的重力加速度
會接近9.8,而當使用者以橫向的方式拿著手機時,X軸的重力加速度會接近9.8,
程式是於 134~141行的地方做是否有偏斜的判斷。由於感測器不是本章的重點,
筆者就不再對感測器相關的程式碼做討論。然而相關的程式碼中有一個
addContentView方法是我們第一次看到的方法(第 47行),這是 Activity類別的方
法,是在不移除原來的 View的情況下,增加一個 View在上面,先前已經提過
我們利用了 SurfaceView來顯示鏡頭所拍攝到的畫面,我們希望當相機拿歪時,
能夠利用一個 TextView來提醒使用者使否拿歪了,可是又希望 SurfaceView依然
能呈現出來,此時就可利用 addContentView方法將 TextView加到 SurfaceView
上面,執行結果如下圖所示:
最後要提醒讀者的是,若要使用相機功能,必須於AndroidManifestmxl中做聲明,
如下所示:
<uses-permission android:name="android.permission.CAMERA" />
15.2 擴增實境初探
擴增實境簡單地說是於現實的環境中增加一些虛擬的資訊,例如一個汽車導航系
統可於道路上畫出虛擬的箭頭告訴使用者前進的方向,又或者一個山岳導覽系統
可以在使用者正在觀望的山岳旁告訴使用者該山岳的相關資訊。在早期的擴增實
境系統中頭戴式顯示(Head-mounted Display, HMD)裝置是需要的,此裝置具有外
部影像擷取、定位等功能,並能和電腦所產生的虛擬影像以畫面重疊的方式做結
合。然而隨著手持裝置的普及,利用手持裝置實作擴增實境也開始被人們所討論
[6]。而目前的智慧型手機大都提供了相機及定位功能,因此能讓我們做影像的
擷取及定位,剛好都是實作擴增實境需要的基本功能,本節將示範一個簡單的擴
增實境程式。
請讀者引進光碟中『\範例程式\Chapter15\AsiaAR』這個專案。這個專案是針對
亞洲大學的幾個場所(Spot)做介紹,手機的畫面分成兩個主要部份:
SurfaceView:用來顯示相機鏡頭所拍攝到的畫面。
TextView:用來告知使用者其所面對的是哪一棟建築物,例如當使用者的手
機的鏡頭面對行政大樓時,TextView就會告知使用者這裡是行政大樓。
下圖是一個範例:
這個程式會需要利用到 GPS與方位感測器(Orientation Sensor),原理如下,首先
建築物的經緯度是事先儲存於程式內的,假設行政大樓的經緯度為(XT, YT),接
著透過 GPS,程式可以知道使用者目前的所在經緯度,假設為(XU, YU),透過這
兩個座標,我們就能夠知道行政大樓位於使用者的哪一個方位,因此再透過方位
感測器就可以知道使用者是否正面對著行政大樓,例如行政大樓位於使用者的東
方,且使用者正好面對著東方,那麼程式就會於 TextView顯示『行政大樓』四
個字。上面的敘述可使用下面的圖來表示,使用者應能更容易理解:
在上圖中,(XT1, YT1)、(XT2, YT2)與(XT3, YT3)是程式預先輸入的,透過 GPS我們
可以獲得(XU, YU),因此便能計算出角度的值,而透過方位感測器,我們可以
得知的值,若與的差小於某一個門檻,程式就會認定使用者是面對著 Target
1。
現在我們可以開始深入程式碼來討論了。AsiaAR.java的內容如下:
1 public class AsiaAR extends Activity {
2
3 private SurfaceView sv;
4 private SurfaceHolder sh;
5 private TextView tv;
6
7 private MyLocationListener mll;
8 private MySensorListener msl;
9 private LocationManager lmgr;
10 private SensorManager smgr;
11 private List<Sensor> slist;
12 private float orientation;
13 private Location user;
14
15 private Camera camera;
16
17 @Override
18 public void onCreate(Bundle savedInstanceState) {
19 super.onCreate(savedInstanceState);
20
21 requestWindowFeature(Window.FEATURE_NO_TITLE);
22 setRequestedOrientation(
23 ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
24
25 setContentView(R.layout.main);
26
27 AsiaSpot.init();
28
29 sv = (SurfaceView)findViewById(R.id.sv);
30 sh = sv.getHolder();
31 sh.addCallback(new MySHCallback());
32
33 sh.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
34
35 tv = (TextView)findViewById(R.id.tv);
36
37 lmgr = (LocationManager)getSystemService(LOCATION_SERVICE);
38 mll = new MyLocationListener();
39
40 smgr =(SensorManager)
41 getSystemService(Context.SENSOR_SERVICE);
42 msl = new MySensorListener();
43
44 slist = smgr.getSensorList(Sensor.TYPE_ORIENTATION);
45 if (slist.size() == 0) {
46 Toast.makeText(this, "No orientation sensor",
47 Toast.LENGTH_SHORT).show();
48 finish();
49 }
50 orientation = (float)0.0;
51 }
52
53 class MySHCallback implements SurfaceHolder.Callback {
54 public void surfaceCreated(SurfaceHolder holder) {
55 camera = Camera.open();
56
57 if (camera == null) {
58 Toast.makeText(AsiaAR.this, "camera is null",
59 Toast.LENGTH_SHORT).show();
60 finish();
61 }
62
63 try {
64 camera.setPreviewDisplay(sh);
65 } catch (Exception e) {
66 Toast.makeText(AsiaAR.this, e.toString(),
67 Toast.LENGTH_SHORT).show();
68 finish();
69 }
70 }
71 public void surfaceDestroyed(SurfaceHolder surfaceholder) {
72 camera.stopPreview();
73 camera.release();
74 }
75 public void surfaceChanged(SurfaceHolder surfaceholder,
76 int format , int w, int h) {
77 camera.startPreview();
78 }
79 }
80
81 @Override
82 protected void onResume() {
83 super.onResume();
84 lmgr.requestLocationUpdates(LocationManager.GPS_PROVIDER,
85 0, 0, mll);
86 smgr.registerListener(msl, slist.get(0),
87 SensorManager.SENSOR_DELAY_UI);
88 }
89
90 @Override
91 protected void onPause() {
92 super.onPause();
93 lmgr.removeUpdates(mll);
94 smgr.unregisterListener(msl, slist.get(0));
95 }
96
97 private void adjustSpot() {
98 if (user == null) return;
99 Spot spot = AsiaSpot.getFrontSpot(orientation, user);
100 if (spot != null)
101 tv.setText(spot.name);
102 }
103
104 class MySensorListener implements SensorEventListener {
105 public void onSensorChanged (SensorEvent event) {
106 if (event.sensor == slist.get(0)) {
107 orientation = event.values[0]+event.values[2];
108 if (orientation >= 360.0)
109 orientation = orientation - 360.0f;
110 adjustSpot();
111 }
112 }
113 public void onAccuracyChanged(Sensor sensor, int accuracy) {
114 }
115 }
116
117 class MyLocationListener implements LocationListener {
118 @Override
119 public void onLocationChanged(Location location) {
120 if (location == null) return;
121
122 user = location;
123 adjustSpot();
124 }
125 @Override
126 public void onProviderDisabled(String provider) {
127 }
128 @Override
129 public void onProviderEnabled(String provider) {
130 }
131 @Override
132 public void onStatusChanged(String provider, int status,
133 Bundle extras) {
134 }
135 }
136 }
137
138 class AsiaSpot {
139 static ArrayList<Spot> spotlist;
140
141 static void init() {
142 spotlist = new ArrayList<Spot>();
143
144 spotlist.add(new Spot(24.045857, 120.686601,
145 new String("行政大樓")));
146 spotlist.add(new Spot(24.046838, 120.686460,
147 new String("管理大樓")));
148 spotlist.add(new Spot(24.045829, 120.686053,
149 new String("資訊大樓")));
150 spotlist.add(new Spot(24.046429, 120.685561,
151 new String("健康大樓")));
152 spotlist.add(new Spot(24.048395, 120.687341,
153 new String("體育館")));
154 }
155
156 static Spot getFrontSpot(float orientation, Location user) {
157 Location dest = new Location(user);
158 float target;
159
160 float mindist = Float.MAX_VALUE;
161 int spotI = -1;
162
163 for (int i = 0; i < spotlist.size(); i++) {
164 dest.setLatitude(spotlist.get(i).latitude);
165 dest.setLongitude(spotlist.get(i).longitude);
166 target = user.bearingTo(dest);
167
168 float degree = target-orientation;
169 if (degree < 0.0) degree = degree + 360.0f;
170 if (degree > 180.0) degree = 360.0f - degree;
171
172 if (degree < 10.0) {
173 float dist = user.distanceTo(dest);
174 if (dist < mindist) {
175 mindist = dist;
176 spotI = i;
177 }
178 }
179 }
180
181 if (spotI == -1) return null;
182 return spotlist.get(spotI);
183 }
184 }
185
186 class Spot {
187
188 Spot(double latitude, double longitude, String name) {
189 this.latitude = latitude;
190 this.longitude = longitude;
191 this.name = name;
192 }
193
194 double latitude;
195 double longitude;
196 String name;
197 }
這個 java檔內有三個主要類別:
AsiaAR:這是一個 Activity,其可以說是第十四章『OrientationApp』專案與
前一節『CameraBasic』專案的結合,使用到『CameraBasic』專案的部份,
就是利用預覽功能將鏡頭所拍攝的影像於 SurfaceView上播放。其餘的程式
碼則很類似『OrientationApp』專案,在『OrientationApp』專案中若使用者
的位置或者方位感測器的值發生變化,我們呼叫自訂的 adjustArrow去調整
箭頭方向,此處則呼叫 adjustSpot去設定 TextView應該顯示的景點名稱,,
adjustSpot方法是實作於 97~102行,程式相當簡單,首先呼叫 getFrontSpot
方法取得使用者前方的最近景點(後面會討論此方法的細節),接著把景點名
稱顯示於 TextView上,getFrontSpot會需要兩個參數,第一個參數 orientation
的值代表使用者面對的方位,會在每次方位感測器的值發生變化時做更新,
第二個參數 user的值代表使用者的位置,會在每次 GPS的值發生變化時做
更新。
Spot:程式碼位於 186~197行,一個 Spot類別的物件就代表一個場所,一
個場所會擁有經緯度的值及一個名稱,188~192行設計了一個建構子,能讓
我們在產生物件時,直接設定場所的經緯度與名稱。
AsiaSpot:程式碼位於 138~184行,這個類別會維護一個 Spot物件的動態陣
列,同時這個類別有兩個方法(Methods):
init方法:初始動態陣列。
getFrontSpot方法:其會傳回使用者正前方的 Spot物件。首先有兩個參
數會被傳入,第一個參數 orientation是藉由方位感測器所獲得的,即前
面示意圖中的值,第二個參數是使用者的位置。程式碼在 163~179會
去一一檢查所有景點,以便得知哪一個景點最接近使用者的正前方,變
數 user代表使用者的位置,變數 dest代表景點的位置,第 166行,程
式利用 Location類別的 bearingTo方法來獲得的值[7],而第 168行,
變數 degree即代表與的差,程式會收集所有 degree小於 10的景點,
再從那些景點中選出一個離使用者最近的景點。如果找不到符合的景點,
程式會回傳 null。
以上即為本程式的主要運作原理,最後還是要提醒讀者記得於應用程式描述檔
(AndroidManifest.xml)中聲明本程式需要精確的位置(亦即要使用 GPS)且需使用
相機。
15.3 摘要
本章我們介紹了 Android平台上相機功能的實作方法,首先介紹了基本的照相功
能,包含了預覽功能與拍照功能。在某些應用程式中,拍照可能只是其中一個必
須的小功能,因此讀者所開發的應用程式中若要使用拍照功能,便可參考此節的
程式。此外,本章也對擴增實境做了一個簡單的介紹,並提供一個簡單的範例告
訴讀者如何於 Android平台上實作出簡易的擴增實境應用。
15.4 作業
1. 修改『CameraBasic』這個專案,試著加入自己的快門聲。
2. 修改『AsiaAR』這個專案,將景點修改成自己所在附近的景點,並用實機
做測試。
15.5 參考資料
[1] Camera | Android Developers,
http://developer.android.com/reference/android/hardware/Camera.html
[2] SurfaceView | Android Developers,
http://developer.android.com/reference/android/view/SurfaceView.html
[3] SurfaceHolder | Android Developers,
http://developer.android.com/reference/android/view/SurfaceHolder.html
[4] Activity | Android Developers,
http://developer.android.com/reference/android/app/Activity.html
[5] KeyEvent | Android Developers,
http://developer.android.com/reference/android/view/KeyEvent.html
[6] Daniel Wagner, Thomas Pintaric, Florian Ledermann, and Dieter Schmalstieg,
“Towards Massively Multi-user Augmented Reality on Handheld Devices”, in proc.
of the third international conference on pervasive computing, 2005.
[7] Location | Android Developers,
http://developer.android.com/reference/android/location/Location.html