截至Windows Phone Developer Tools Beta版為止,WP7模擬器還很陽春,功能有限。而我一直有個想法,模擬器何不利用PC Webcam來模擬拍照動作? 這樣不是更逼真有趣嗎?

前陣子,在MSDN CODING4FUN上讀到一篇介紹WP7照片後製程式的有趣文章(PicFx),其中展示了利用照相功能取得影像的程式寫法,勾起了貪玩念頭--反正手邊有Webcam,不如就為WP7模擬器安裝一個土砲鏡頭吧!

展示影片

要幫WP7模擬器搞個土砲攝影鏡頭並不難,首先我們需要一個WinForm程式(姑且稱它WebCam Gateway好了),執行簡單的HTTP Server功能(先前提過用100行C#寫的MicroHttpSever應可勝任),它負責接受特定Port傳來的HTTP Request,並將當下Webcam擷取的影像轉成圖檔傳回去。而在WP7程式端,透過WebClient物件便可發出HTTP Request取回圖檔,跟CameraCaptureTask.Completed 事件的運作模式差不多,如此就能將兩邊整合在一起。

WebCam Gateway 程式

WebCam Gateway程式只有兩大使命: 從Webcam擷取影像並擔任HTTP伺服器!  提到Webcam程式開發,網路上查到較簡便的做法是利用WIA元件,只可惜Windows Vista起移除了WIA的影像支援功能,建議改用WPD API。我對Webcam一無所知,WPD API看起來又頗複雜,索性偷懶,在DirectShow.NET程式庫中,找了一個現成範例--DxSnap,正好示範了由Webcam抓圖片的程式寫法。打開DxSnap專案,加入MicroHttpServer再稍做修改,WebCam Gateway就大功告成了。

 
圖: 用IE輸入特定URL,測試從WebCam Gateway取回照片 (點圖放大)

CameraProxy類別

到目前為止,我們已可透過瀏覽器取得Webcam影像檔,餘下來的工作,就是在WP7程式裡用WebClient模擬瀏覽器行為即可。我寫了一個CameraProxy類別將連線外部HTTP伺服器、取回圖檔等細節都封裝起來,並設法讓它的行為愈像CameraCaptureTask愈好。因此它也具有一個Show()方法,一個Completed(object sender, PhotoResult e)事件,介面規格幾乎跟CameraCaptureTask一模一樣,甚至我們可直接共用原本CameraCaptureTask.Completed的事件處理程式來接收WebCam Gateway傳回的圖檔,減少程式配合修改的幅度。但很不幸地,PhotoResult.ChosenPhoto屬性被設成唯讀,我們無法直接用它傳回Webcam影像檔! 我的解法是另外宣告一個PhotoResultHacking類別繼承PhotoResult,並在其中宣告一個CameraPhotoStream屬性,在CameraCaptureTask.Completed事件中,便可透過偵測參數型別與轉型取出圖檔資料。

以下是CameraProxy程式碼:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Microsoft.Phone.Tasks;
using System.IO;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
 
namespace Darkthread
{
    public class CameraProxy
    {
        private string gatewayUrl = null;
        private IApplicationBar appBar = null; 
        private Border mask = new Border()
        {
            Background = new SolidColorBrush(Colors.White),
            Opacity = 0.7,
            HorizontalAlignment = HorizontalAlignment.Stretch,
            VerticalAlignment = VerticalAlignment.Stretch,
            Visibility = Visibility.Collapsed
        };
        //Constructor, url for WebCam Gateway, page for UI blocking
        public CameraProxy(string url, PhoneApplicationPage page)
        {
            gatewayUrl = url;
            if (page != null)
            {
                appBar = page.ApplicationBar;
                //Try to get LayoutRoot element
                Panel pnl = 
                    VisualTreeHelper.GetChild(page, 0) as Panel;
                //If the LayoutRoot is a container
                if (pnl != null) //Add a mask element
                    pnl.Children.Add(mask);
                else
                    mask = null;
            }
        }
        //Completed event compatible with CameraCaptureTask
        public event Action<object, PhotoResult> Completed;
       
        public void Show()
        {
            WebClient wc = new WebClient();
            wc.OpenReadCompleted += (sender, e) =>
            {
                try
                {
                    //e.Result is the stream that contains image we need
                    PhotoResultHacking pr = new PhotoResultHacking(
                        Microsoft.Phone.Tasks.TaskResult.OK, e.Result);
                    if (Completed != null)
                        Completed(this, pr);
                }
                catch
                {
                    //If any exception, return TaskResult.Cancel
                    PhotoResult epr = new PhotoResult(TaskResult.Cancel);
                    if (Completed != null)
                        Completed(this, epr);
                }
                finally
                {
                    //Remove the blocking mask
                    if (mask != null)
                        mask.Visibility = Visibility.Collapsed;
                    //Show the ApplicationBar
                    if (appBar != null)
                        appBar.IsVisible = true;
                }
            };
            //Add a mask to block the UI
            if (mask != null)
                mask.Visibility = Visibility.Visible;
            //Hide ApplicationBar to block the UI
            if (appBar != null)
                appBar.IsVisible = false;
            wc.OpenReadAsync(new Uri(gatewayUrl));
        }
    }
    //A **compatible** PhotoResult providing writable property
    public class PhotoResultHacking : PhotoResult
    {
        public Stream CameraPhotoStream;
        public PhotoResultHacking(TaskResult result, Stream stream) :
            base(result)
        {
            CameraPhotoStream = stream;
        }
    }
}

使用說明

現在來稍微調整一下PicFx專案,示範如何使用CameraProxy。

首先,在Initialize()建立CameraProxy物件,並偷CameraCaptureTask原本的Completed的事件處理函數來用。

        //**CameraProxy**
        Darkthread.CameraProxy cameraProxy;
 
        private void Initialize()
        {
            // Init tasks
            cameraCaptureTask = new CameraCaptureTask();
            cameraCaptureTask.Completed += PhotoProviderTaskCompleted;
 
            //**CameraProxy**
            //Create CameraProxy object and shared the same event handler
            //with cameraCaptureTask.Completed
            //PS: 192.168.1.136 is the local IP address of host
            cameraProxy = new Darkthread.CameraProxy(
                "http://192.168.1.136:1688/", this);
            cameraProxy.Completed += PhotoProviderTaskCompleted;

在原本專案裡,照相鈕在模擬器模式下是被停用的,我們修改程式解開封印。

            if (btn != null)
            {
                //**CameraProxy**
                //btn.IsEnabled = Microsoft.Devices.Environment.DeviceType 
                //                != DeviceType.Emulator;
                btn.IsEnabled = true;
            }

當照相鈕被按下時,程式會判斷是否處於模擬器狀態,決定要呼叫cameraProxy.Show()還是cameraCaptureTask.Show()。而cameraProxy, cameraCaptureTask, 及photoChooserTask在選完圖檔或照完相後都交由同一個事件函數PhotoProviderTaskCompleted處理,因此在其中我們需加入額外邏輯: 當圖片由cameraProxy傳回時,要將result參數轉型成PhotoResultHacking,再透過CameraPhotoStream屬性取出影像檔。

還有最後一部分要調,改得有點醜但也莫可奈何。原因是CameraCaptureTask及PhotoChooser是由OS掌管的作業,當挑選圖檔或照相時,我們的應用程式會被中斷,在取得影像資料後,程式才會再次啟動,並觸發 Initialize()及LayoutUpdated()等初始化事件。然而,要從WP7程式內部去中止自己再重新啟動看來是不可能的任務,因此我們無法模擬出跟CameraCaptureTask及PhotoChooser完全一樣的行為,原本安排在初始事件中的邏輯就漏跑了。解決之道是另外將原本預期在Initialize()及LayoutUpdated()裡要執行的邏輯抽取出來,接在取得CameraProxy影像檔後執行。

        //**CameraProxy** Add a flag to identify if it's in emulator mode
        private bool EmulatorMode
        {
            get
            {
                return Microsoft.Devices.Environment.DeviceType 
                       == DeviceType.Emulator;
            }
        }
 
        private void ApplicationBarIconCameraButton_Click(object sender, EventArgs e)
        {
            //Trigger CameraCaptureTask or CameraProxy depends on EmulatorMode flag
            if (EmulatorMode)
                cameraProxy.Show();
            else
                cameraCaptureTask.Show();
        }
 
        private void PhotoProviderTaskCompleted(object sender, PhotoResult e)
        {
            // Load the photo from the task result
            if (e != null && e.TaskResult == TaskResult.OK)
            {
                var bmpi = new BitmapImage();
                //**CameraProxy** add branch code for CameraProxy
                bool extCamera = (EmulatorMode && 
                                  e is Darkthread.PhotoResultHacking);
                if (extCamera)
                    bmpi.SetSource(
                        ((Darkthread.PhotoResultHacking)e).CameraPhotoStream);
                else 
                    bmpi.SetSource(e.ChosenPhoto);
                original = new WriteableBitmap(bmpi);
                //**CameraProxy**
                //Real CameraCaptureTask will terminate the application
                //and cause Loaded event, but CameraProxy won't,
                //so we have to add the logic after camera capture here.
                //Ugly, but seems no better way
                if (extCamera)
                {
                    ShowImage(original);
                    ResizeAndShowImage(original);
                }
            }
        }

好了,現在我們的WP7模擬器可以真的拿來照相了,很酷吧!

WebCam Gateway的原始碼、CameraProxy.cs及PicFx專案中被修改過的MainPage.xaml.cs,可以由這裡下載,有興趣的朋友可以拿回去玩,並歡迎提供回饋意見。


Comments

# by 小中中

光土砲兩個字就該按個讚了~~~!!! XD

# by Andy

不知道 要怎麼連絡你? 有MSN之類的東西嗎? 有些問題想請問一下

# by kobis

想請問黑大 目前wp7有開放鏡頭的相關api嗎? 想試著把黑大的這組程式碼轉成使用HD7本身鏡頭

# by Jeffrey

to kobis, 用Silverlight開發的話,我目前知道只能透過CameraCaptureTask.Show()觸發照相功能,然後在Completed事件中收照片,似乎沒有其他進一步操控鏡頭的做法。

# by DARK

黑大妳好,我執行你寫的程式結果出現"Init Failed:找不到中介篩選器的組合,所以無法連接",小弟不才,請大大指教~^^"

# by Jeffrey

to DARK, 這種訊息看起來較像Webcam的相關程式沒有安裝好,重新安裝看看,並使用其他軟體(例如: MSN, Live Messenger)驗證看看Webcam的功能是否正常。

# by DARK

感謝黑大的指教,目前問題好像出在於需要用虛擬WEBCAM才能使用,想請問黑大用的是哪款大廠的WEBCAM

# by Jeffrey

to DARK, 我用的WebCam是Microsoft LifeCam VX-7000。( 影片中WebCam Gateway程式的標題列也有顯示出來,可以當成佐證,:P )

# by Amos

感謝版主 非常有幫助的文章!!

Post a comment