Kinect for Windows SDK開發(fā)入門(十三):語音識(shí)別 下
1. 使用定向麥克風(fēng)進(jìn)行波速追蹤(Beam Tracking for a Directional Microphone)
可以使用這4個(gè)麥克風(fēng)來模擬定向麥克風(fēng)產(chǎn)生的效果,這個(gè)過程稱之為波束追蹤(beam tracking),為此我們新建一個(gè)WPF項(xiàng)目,過程如下:
1. 創(chuàng)建一個(gè)名為KinectFindAudioDirection的WPF項(xiàng)目。
2. 添加對(duì)Microsoft.Kinect.dll和Microsoft.Speech.dll的引用。
3. 將主窗體的名稱改為“Find Audio Direction”
4. 在主窗體中繪制一個(gè)垂直的細(xì)長矩形。
界面上的細(xì)長矩形用來指示某一時(shí)刻探測到的說話者的語音方向。矩形有一個(gè)旋轉(zhuǎn)變換,在垂直軸上左右擺動(dòng),以表示聲音的不同來源方向。前端頁面代碼:
<Rectangle Fill="#1BA78B" HorizontalAlignment="Left" Margin="240,41,0,39" Stroke="Black" Width="10" RenderTransformOrigin="0.5,0"> <Rectangle.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform Angle="{Binding BeamAngle}"/> <TranslateTransform/> </TransformGroup> </Rectangle.RenderTransform></Rectangle>
上圖是程序的UI界面。后臺(tái)邏輯代碼和之前的例子大部分都是相同的。首先實(shí)例化一個(gè)KinectAudioSource對(duì)象,然后將主窗體的DataContext賦值給本身。將BeamAngleMode設(shè)置為Adaptive,使得能夠自動(dòng)追蹤說話者的聲音。我們需要編寫KinectAudioSource對(duì)象的BeamChanged事件對(duì)應(yīng)的處理方法。當(dāng)用戶的說話時(shí),位置發(fā)生變化時(shí)就會(huì)觸發(fā)該事件。我們需要?jiǎng)?chuàng)建一個(gè)名為BeamAngle的屬性,使得矩形的RotateTransform可以綁定這個(gè)屬性。
public partial class MainWindow : Window, INotifyPropertyChanged{ public MainWindow() { InitializeComponent(); this.DataContext = this; this.Loaded += delegate { ListenForBeamChanges(); }; } private KinectAudioSource CreateAudioSource() { var source = KinectSensor.KinectSensors[0].AudioSource; source.NoiseSuppression = true; source.AutomaticGainControlEnabled = true; source.BeamAngleMode = BeamAngleMode.Adaptive; return source; } private void ListenForBeamChanges() { KinectSensor.KinectSensors[0].Start(); var audioSource = CreateAudioSource(); audioSource.BeamAngleChanged += audioSource_BeamAngleChanged; audioSource.Start(); } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propName) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propName)); } private double _beamAngle; public double BeamAngle { get { return _beamAngle; } set { _beamAngle = value; OnPropertyChanged("BeamAngle"); } }}
以上代碼中,還需要對(duì)BeamChanged事件編寫對(duì)應(yīng)的處理方法。每次當(dāng)波束的方向發(fā)生改變時(shí),就更改BeamAngle的屬性。SDK中使用弧度表示角度。所以在事件處理方法中我們需要將弧度換成度。為了能達(dá)到說話者移到左邊,矩形條也能夠向左邊移動(dòng)的效果,我們需要將角度乘以一個(gè)-1。代碼如下:
void audioSource_BeamAngleChanged(object sender, BeamAngleChangedEventArgs e){ BeamAngle = -1 * e.Angle;}
運(yùn)行程序,然后在房間里不同地方走動(dòng),可以看到矩形條會(huì)根據(jù)你的位置左右擺動(dòng)。
2. 語音命令識(shí)別在這一部分,我們將會(huì)結(jié)合KinectAudioSource和SpeechRecognitionEngine來演示語音命令識(shí)別的強(qiáng)大功能。為了展示語音命令能夠和骨骼追蹤高效結(jié)合,我們會(huì)使用語音命令向窗體上繪制圖形,并使用命令移動(dòng)這些圖形到光標(biāo)的位置。命令類似如下:
Create a yellow circle, there.
Create a cyan triangle, there.
Put a magenta square, there.
Create a blue diamond, there.
Move that ... there.
Put that ... there.
Move that ... below that.
Move that ... west of the diamond.
Put a large green circle ... there.
程序界面大致如下:
和之前的應(yīng)用程序一樣,首先創(chuàng)建一下項(xiàng)目的基本結(jié)構(gòu):
1. 創(chuàng)建一個(gè)名為KinectPutThatThere的WPF項(xiàng)目。
2. 添加對(duì)Microsoft.Kinect.dll和Microsoft.Speech.dll的引用。
3. 將主窗體的名稱改為“Put That There”
4. 添加一個(gè)名為CrossHairs.xaml的用戶自定義控件。
CrossHair用戶控件簡單的以十字光標(biāo)形式顯示當(dāng)前用戶右手的位置。下面的代碼顯示了這個(gè)自定義控件的XAML文件。注意到對(duì)象于容器有一定的偏移使得十字光標(biāo)的中心能夠處于Grid的零點(diǎn)。
<Grid Height="50" Width="50" RenderTransformOrigin="0.5,0.5"> <Grid.RenderTransform> <TransformGroup> <ScaleTransform/> <SkewTransform/> <RotateTransform/> <TranslateTransform X="-25" Y="-25"/> </TransformGroup> </Grid.RenderTransform> <Rectangle Fill="#FFF4F4F5" Margin="22,0,20,0" Stroke="#FFF4F4F5"/> <Rectangle Fill="#FFF4F4F5" Margin="0,22,0,21" Stroke="#FFF4F4F5"/> </Grid>
在應(yīng)用程序的主窗體中,將根節(jié)點(diǎn)從grid對(duì)象改為canvas對(duì)象。Canvas對(duì)象使得將十字光標(biāo)使用動(dòng)畫滑動(dòng)到手的位置比較容易。在主窗體上添加一個(gè)CrossHairs自定義控件。在下面的代碼中,我們可以看到將Canvas對(duì)象嵌套在了一個(gè)Viewbox控件中。這是一個(gè)比較老的處理不同屏幕分辨率的技巧。ViewBox控件會(huì)自動(dòng)的將內(nèi)容進(jìn)行縮放以適應(yīng)實(shí)際屏幕的大小。設(shè)置MainWindows的背景色,并將Canvas的顏色設(shè)置為黑色。然后在Canvas的底部添加兩個(gè)標(biāo)簽。一個(gè)標(biāo)簽用來顯示SpeechRecognitionEngine將要處理的語音指令,另一個(gè)標(biāo)簽顯示匹配正確的置信度。CrossHair自定義控件綁定了HandTop和HandLeft屬性。兩個(gè)標(biāo)簽分別綁定了HypothesizedText和Confidence屬性。代碼如下:
<Window x:Class="KinectPutThatThere.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:KinectPutThatThere" Title="Put That There" Background="Black"> <Viewbox> <Canvas x:Name="MainStage" Height="1080" Width="1920" Background="Black" VerticalAlignment="Bottom"> <local:CrossHairs Canvas.Top="{Binding HandTop}" Canvas.Left="{Binding HandLeft}" /> <Label Foreground="White" Content="{Binding HypothesizedText}" Height="55" FontSize="32" Width="965" Canvas.Left="115" Canvas.Top="1025" /> <Label Foreground="Green" Content="{Binding Confidence}" Height="55" Width="114" FontSize="32" Canvas.Left="0" Canvas.Top="1025" /> </Canvas> </Viewbox></Window>
在后臺(tái)邏輯代碼中,讓MainWindows對(duì)象實(shí)現(xiàn)INofityPropertyChanged事件并添加OnPropertyChanged幫助方法。我們將創(chuàng)建4個(gè)屬性用來為前臺(tái)UI界面進(jìn)行綁定。
public partial class MainWindow : Window, INotifyPropertyChanged{ private double _handLeft; public double HandLeft { get { return _handLeft; } set { _handLeft = value; OnPropertyChanged("HandLeft"); } } private double _handTop; public double HandTop { get { return _handTop; } set { _handTop = value; OnPropertyChanged("HandTop"); } } private string _hypothesizedText; public string HypothesizedText { get { return _hypothesizedText; } set { _hypothesizedText = value; OnPropertyChanged("HypothesizedText"); } } private string _confidence; public string Confidence { get { return _confidence; } set { _confidence = value; OnPropertyChanged("Confidence"); } } public event PropertyChangedEventHandler PropertyChanged; private void OnPropertyChanged(string propertyName) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } }}
添加CreateAudioSource方法,在該方法中,將KinectAudioSource對(duì)象的AutoGainControlEnabled的屬性設(shè)置為false。
private KinectAudioSource CreateAudioSource(){ var source = KinectSensor.KinectSensors[0].AudioSource; source.AutomaticGainControlEnabled = false; source.EchoCancellationMode = EchoCancellationMode.None; return source;}
接下來實(shí)現(xiàn)骨骼追蹤部分邏輯來獲取右手的坐標(biāo),相信看完骨骼追蹤那兩篇文章后這部分的代碼應(yīng)該會(huì)比較熟悉。首先創(chuàng)建一個(gè)私有字段_kinectSensor來保存當(dāng)前的KienctSensor對(duì)象,同時(shí)創(chuàng)建SpeechRecognitionEngine對(duì)象。在窗體的構(gòu)造函數(shù)中,對(duì)這幾個(gè)變量進(jìn)行初始化。例外注冊(cè)骨骼追蹤系統(tǒng)的Skeleton事件并將主窗體的DataContext對(duì)象賦給自己。
KinectSensor _kinectSensor;SpeechRecognitionEngine _sre;KinectAudioSource _source;public MainWindow(){ InitializeComponent(); this.DataContext = this; this.Unloaded += delegate { _kinectSensor.SkeletonStream.Disable(); _sre.RecognizeAsyncCancel(); _sre.RecognizeAsyncStop(); _sre.Dispose(); }; this.Loaded += delegate { _kinectSensor = KinectSensor.KinectSensors[0]; _kinectSensor.SkeletonStream.Enable(new TransformSmoothParameters() { Correction = 0.5f, JitterRadius = 0.05f, MaxDeviationRadius = 0.04f, Smoothing = 0.5f }); _kinectSensor.SkeletonFrameReady += nui_SkeletonFrameReady; _kinectSensor.Start(); StartSpeechRecognition(); };}
在上面的代碼中,我們添加了一些TransformSmoothParameters參數(shù)來使得骨骼追蹤更加平滑。nui_SkeletonFrameReady方法如下。方式使用骨骼追蹤數(shù)據(jù)來獲取我們感興趣的右手的關(guān)節(jié)點(diǎn)位置。這部分代碼和之前文章中的類似。大致流程是:遍歷當(dāng)前處在追蹤狀態(tài)下的骨骼信息。然后找到右手關(guān)節(jié)點(diǎn)的矢量信息,然后使用SkeletonToDepthImage來獲取相對(duì)于屏幕尺寸的X,Y坐標(biāo)信息。
void nui_SkeletonFrameReady(object sender, SkeletonFrameReadyEventArgs e){ using (SkeletonFrame skeletonFrame = e.OpenSkeletonFrame()) { if (skeletonFrame == null) return; var skeletons = new Skeleton[skeletonFrame.SkeletonArrayLength]; skeletonFrame.CopySkeletonDataTo(skeletons); foreach (Skeleton skeletonData in skeletons) { if (skeletonData.TrackingState == SkeletonTrackingState.Tracked) { Microsoft.Kinect.SkeletonPoint rightHandVec = skeletonData.Joints[JointType.HandRight].Position; var depthPoint = _kinectSensor.MapSkeletonPointToDepth(rightHandVec , DepthImageFormat.Resolution640x480Fps30); HandTop = depthPoint.Y * this.MainStage.ActualHeight / 480; HandLeft = depthPoint.X * this.MainStage.ActualWidth / 640; } } }}
上面是多有設(shè)計(jì)到手部追蹤的代碼。一旦我們?cè)O(shè)置好HandTop和HandLeft屬性后,UI界面上的十字光標(biāo)位置就會(huì)自動(dòng)更新。
接下來我們需要實(shí)現(xiàn)語音識(shí)別部分的邏輯。SpeechRecognitionEngine中的StartSpeechRecognition方法必須找到正確的語音識(shí)別庫來進(jìn)行語音識(shí)別。下面的代碼展示了如何設(shè)置語音識(shí)別庫預(yù)計(jì)如何將KinectAudioSource傳遞給語音識(shí)別引起。我們還添加了SpeechRecognized,SpeechHypothesized以及SpeechRejected事件對(duì)應(yīng)的方法。SetInputToAudioStream中的參數(shù)和前篇文章中的含義一樣,這里不多解釋了。注意到SpeechRecognitionEngine和KinectAudioSource都是Disposable類型,因此在整個(gè)應(yīng)用程序的周期內(nèi),我們要保證這兩個(gè)對(duì)象都處于打開狀態(tài)。
private void StartSpeechRecognition(){ _source = CreateAudioSource(); Func<RecognizerInfo, bool> matchingFunc = r => { string value; r.AdditionalInfo.TryGetValue("Kinect", out value); return "True".Equals(value, StringComparison.InvariantCultureIgnoreCase) && "en-US".Equals(r.Culture.Name, StringComparison.InvariantCultureIgnoreCase); }; RecognizerInfo ri = SpeechRecognitionEngine.InstalledRecognizers().Where(matchingFunc).FirstOrDefault(); _sre = new SpeechRecognitionEngine(ri.Id); CreateGrammars(ri); _sre.SpeechRecognized += sre_SpeechRecognized; _sre.SpeechHypothesized += sre_SpeechHypothesized; _sre.SpeechRecognitionRejected += sre_SpeechRecognitionRejected; Stream s = _source.Start(); _sre.SetInputToAudioStream(s, new SpeechAudioFormatInfo( EncodingFormat.Pcm, 16000, 16, 1, 32000, 2, null)); _sre.RecognizeAsync(RecognizeMode.Multiple);}
要完成程序邏輯部分,我們還需要處理語音識(shí)別時(shí)間以及語音邏輯部分,以使得引擎能夠直到如何處理和執(zhí)行我們的語音命令。SpeechHypothesized以及SpeechRejected事件代碼如下,這兩個(gè)事件的邏輯很簡單,就是更新UI界面上的label。SpeechRecognized事件有點(diǎn)復(fù)雜,他負(fù)責(zé)處理傳進(jìn)去的語音指令,并對(duì)識(shí)別出的指令執(zhí)行相應(yīng)的操作。另外,該事件還負(fù)責(zé)創(chuàng)建一些GUI對(duì)象(實(shí)際就是命令模式),我們必須使用Dispatcher對(duì)象來發(fā)揮InterpretCommand到主UI線程中來。
void sre_SpeechRecognitionRejected(object sender, SpeechRecognitionRejectedEventArgs e){ HypothesizedText += " Rejected"; Confidence = Math.Round(e.Result.Confidence, 2).ToString();}void sre_SpeechHypothesized(object sender, SpeechHypothesizedEventArgs e){ HypothesizedText = e.Result.Text;}void sre_SpeechRecognized(object sender, SpeechRecognizedEventArgs e){ Dispatcher.BeginInvoke(new Action<SpeechRecognizedEventArgs>(InterpretCommand), e);}
現(xiàn)在到了程序核心的地方。創(chuàng)建語法邏輯并對(duì)其進(jìn)行解析。本例中的程序識(shí)別普通的以“put”或者“create”開頭的命令。前面是什么我們不關(guān)心,緊接著應(yīng)該是一個(gè)顏色,然后是一種形狀,最后一個(gè)詞應(yīng)該是“there”。下面的代碼顯示了創(chuàng)建的語法。
private void CreateGrammars(RecognizerInfo ri){ var colors = new Choices(); colors.Add("cyan"); colors.Add("yellow"); colors.Add("magenta"); colors.Add("blue"); colors.Add("green"); colors.Add("red"); var create = new Choices(); create.Add("create"); create.Add("put"); var shapes = new Choices(); shapes.Add("circle"); shapes.Add("triangle"); shapes.Add("square"); shapes.Add("diamond"); var gb = new GrammarBuilder(); gb.Culture = ri.Culture; gb.Append(create); gb.AppendWildcard(); gb.Append(colors); gb.Append(shapes); gb.Append("there"); var g = new Grammar(gb); _sre.LoadGrammar(g); var q = new GrammarBuilder{ Culture = ri.Culture }; q.Append("quit application"); var quit = new Grammar(q); _sre.LoadGrammar(quit);}
上面的代碼中,我們首先創(chuàng)建一個(gè)Choices對(duì)象,這個(gè)對(duì)象會(huì)在命令解析中用到。在程序中我們需要顏色和形狀對(duì)象。另外,第一個(gè)單詞是“put”或者“create”,因此我們也創(chuàng)建Choices對(duì)象。然后使用GrammarBuilder類將這些對(duì)象組合到一起。首先是”put”或者“create”然后是一個(gè)占位符,因?yàn)槲覀儾魂P(guān)心內(nèi)容,然后是一個(gè)顏色Choices對(duì)象,然后是一個(gè)形狀Choices對(duì)象,最后是一個(gè)“there”單詞。
我們將這些語法規(guī)則加載進(jìn)語音識(shí)別引擎。同時(shí)我們也需要有一個(gè)命令來停止語音識(shí)別引擎。因此我們創(chuàng)建了第二個(gè)語法對(duì)象,這個(gè)對(duì)象只有一個(gè)”Quit”命令。然后也將這個(gè)語法規(guī)則加載到引擎中。
一旦識(shí)別引擎確定了要識(shí)別的語法,真正的識(shí)別工作就開始了。被識(shí)別的句子必須被解譯,出別出來想要的指令后,我們必須決定如何進(jìn)行下一步處理。下面的代碼展示了如何處理識(shí)別出的命令,以及如何根據(jù)特定的指令來講圖形元素繪制到UI界面上去。
private void InterpretCommand(SpeechRecognizedEventArgs e){ var result = e.Result; Confidence = Math.Round(result.Confidence, 2).ToString(); if (result.Confidence < 95 && result.Words[0].Text == "quit" && result.Words[1].Text == "application") { this.Close(); } if (result.Words[0].Text == "put" || result.Words[0].Text == "create") { var colorString = result.Words[2].Text; Color color; switch (colorString) { case "cyan": color = Colors.Cyan; break; case "yellow": color = Colors.Yellow; break; case "magenta": color = Colors.Magenta; break; case "blue": color = Colors.Blue; break; case "green": color = Colors.Green; break; case "red": color = Colors.Red; break; default: return; } var shapeString = result.Words[3].Text; Shape shape; switch (shapeString) { case "circle": shape = new Ellipse(); shape.Width = 150; shape.Height = 150; break; case "square": shape = new Rectangle(); shape.Width = 150; shape.Height = 150; break; case "triangle": var poly = new Polygon(); poly.Points.Add(new Point(0, 0)); poly.Points.Add(new Point(150, 0)); poly.Points.Add(new Point(75, -150)); shape = poly; break; case "diamond": var poly2 = new Polygon(); poly2.Points.Add(new Point(0, 0)); poly2.Points.Add(new Point(75, 150)); poly2.Points.Add(new Point(150, 0)); poly2.Points.Add(new Point(75, -150)); shape = poly2; break; default: return; } shape.SetValue(Canvas.LeftProperty, HandLeft); shape.SetValue(Canvas.TopProperty, HandTop); shape.Fill = new SolidColorBrush(color); MainStage.Children.Add(shape); }}
方法中,我們首先檢查語句識(shí)別出的單詞是否是”Quit”如果是的,緊接著判斷第二個(gè)單詞是不是”application”如果兩個(gè)條件都滿足了,就不進(jìn)行繪制圖形,直接返回。如果有一個(gè)條件不滿足,就繼續(xù)執(zhí)行下一步。
InterpretCommand方法然后判斷第一個(gè)單詞是否是“create”或者“put”,如果不是這兩個(gè)單詞開頭就什么也不執(zhí)行。如果是的,就判斷第三個(gè)單詞,并根據(jù)識(shí)別出來的顏色創(chuàng)建對(duì)象。如果第三個(gè)單詞沒有正確識(shí)別,應(yīng)用程序也停止處理。否則,程序判斷第四個(gè)單詞,根據(jù)接收到的命令創(chuàng)建對(duì)應(yīng)的形狀。到這一步,基本的邏輯已經(jīng)完成,最后第五個(gè)單詞用來確定整個(gè)命令是否正確。命令處理完了之后,將當(dāng)前受的X,Y坐標(biāo)賦給創(chuàng)建好的對(duì)象的位置。
運(yùn)行程序后,按照語法設(shè)定的規(guī)則就可以通過語音以及骨骼追蹤協(xié)作來創(chuàng)建對(duì)象了。
有一點(diǎn)遺憾的是,Kinect的語音識(shí)別目前不支持中文,即使在5月份發(fā)布的SDK1.5版本增加的識(shí)別語言種類中也沒有中文,也有可能是因?yàn)槟壳癤box和kinect沒有在中國大陸銷售的原因吧。所以大家只有用英語進(jìn)行測試了。
3. 結(jié)語本文接上文,用一個(gè)顯示語音來源方向的例子展示了語音識(shí)別中的方向識(shí)別。用一個(gè)簡單的語音識(shí)別結(jié)合骨骼追蹤來演示了如何根據(jù)語音命令來執(zhí)行一系列操作。在語音命令識(shí)別中詳細(xì)介紹了語音識(shí)別語法對(duì)象邏輯的建立,以及語音識(shí)別引擎相關(guān)事件的處理。