微軟認知服務包括了影像、語音、語言、搜索、知識五大領域,經過對這些認知服務的獨立或者組合使用,能夠解決不少現實世界中的問題。做爲AI小白,咱們能夠選擇艱難地攀登崇山峻嶺,也能夠選擇像牛頓同樣站在巨人的肩膀上。本章節的內容就以"漫畫翻譯"爲例,介紹如何靈活使用微軟認知服務來實現本身的AI夢想。html
日本漫畫很是著名,如海賊王,神探柯南等系列漫畫在中國的少年一代中是很是普及。國內專門有一批志願者,全手工翻譯這些漫畫爲中文版本,過程艱辛複雜,花費時間很長。可否使用AI來幫助加快這個過程呢?前端
小提示:漫畫是有版權的,請你們要在尊重版權的前提下作合法的事。算法
漫畫翻譯,要作的事情有三步:express
下圖是展現最後的翻譯效果,左側是原漫畫,右側是翻譯成中文的結果:json
安裝Windows 10版本 1803,低一些的Windows 10版本也可使用。Windows 7也能夠運行本示例程序,但不建議使用,Windows 7的官方技術支持到2020/01/14結束。canvas
小提示:若是您的機器不能運行Windows 10,說明硬件性能仍是有些不夠的。AI是創建在軟硬件快速發展的基礎上的,不建議您使用低配置的機器來作AI知識的學習。api
安裝Visual Studio 2017 Community。點擊這裏下載,對於本案例,安裝時選擇".NET桌面開發"便可知足要求。服務器
點擊進入此頁面:網絡
在上圖所示頁面中"計算機影像"下點擊"免費試用":app
根據本身的實際狀況選擇以上三個選項之一,這裏以選擇第一個"來賓"選項爲例:
選擇一個熱愛的國家/地區,在上下兩個複選框上("我贊成","我接受")都打勾,點擊"下一步":
上圖中以選擇"Microsoft"帳戶爲例繼續:
最後獲得了上面這個頁面,這裏的密鑰(Key)和終結點(Endpoint)要在程序中使用,請保存好!
小提示:上面例子中的密鑰只能再使用1天了,由於是7天的免費試用版本。因此當你的程序之前運行正常,某一天突然從服務器不能獲得正常的返回值時而且獲得錯誤代碼Unauthorized (401),請首先檢查密鑰狀態。
小提示:當試用的Key過時後,你是沒法再申請試用Key的,只能申請正式的Key,這就要經過Azure門戶。在Azure門戶中申請好Computer Vision服務(包括OCR服務)的Key後,它會告訴你Endpoint是…../vision/v1.0,這個不用管它,在code裏還保持……/vision/v2.0就能夠了,二者的Key是通用的。
用本身的Azure帳號登陸Azure門戶:
在上圖中點擊左側的"All resources":
在上圖中點擊上方的 "+ Add"圖標來建立資源,獲得資源列表以下 :
在上圖中點擊右側列表中的"AI + Machine Learning",獲得下圖的具體服務項目列表:
這裏有個坑,文本翻譯不在右側的列表中,須要點擊右上方的"See all"來展開全部項目:
哦,好吧,仍是沒有!保持耐心,繼續點擊Cognitive Services欄目的右側的"More"按鈕,獲得更詳細的列表:
仍是沒有?卷滾一下看看?到底,到底!OK,終於有了Translator Text,就是Ta:
建立這個服務時,咱們選擇F0就能夠了。若是要是作商用軟件的話,你能夠選擇S1或其餘,100萬個字符才花10美圓,不貴不貴!
是否是以上申請Key的過程太複雜了?那是由於Azure內容龐雜,網頁設計層次太多!其實這個過程是能夠簡化的,由於咱們有個Visual Studio Tools for AI擴展包!
打開VS2017,菜單上選擇"工具(Tools)->擴展和更新(Extensions and Updates)",在彈出的對話框左側選擇"聯機(Online)",在右側上方輸入"AI" 進行搜索,會看到"Microsoft Visual Studio Tools for AI"擴展包,下載完畢後關閉VS,這個擴展包就會自動安裝。
安裝完畢後,再次打開VS2017,點擊菜單View->Server Explorer。若是安裝了Tools for AI,此時會看到如下界面:
在AI Tools->Azure Cognitive Services下,能夠看到我已經申請了2個service,ComputerVisionAPI和TranslateAPI就是咱們想要的,這兩個名字是本身在申請服務時指定的。
假設你尚未這兩個服務,那麼在Azure Cognitive Services上鼠標右鍵,而後選擇Create New Cognitive Service,出現如下對話框:
在每一個下拉框中顯示的內容可能會每一個人都不同,絕大多數是用下拉框完成填充的,很方便。假設我想申請TextTranslation服務,那麼我在Service Name上填寫一個本身能看懂的名字就好了,好比我填寫了"TranslateAPI",這樣比較直接。同理能夠建立ComputerVisionAPI服務。服務的名字不會在Code中使用。
咱們廢了老鼻子勁,獲得瞭如下兩個REST API的Endpoint和相關的Key:
OCR服務
Endpoint: https://westcentralus.api.cognitive.microsoft.com/vision/v2.0
Text Translate文本翻譯服務
Endpoint: https://api.cognitive.microsofttranslator.com/translate?api-version=3.0
小提示:以上兩個Endpoint的URL是目前最新的版本,請不要使用舊的版本如v1.0等等。
我們是洗洗睡了,仍是寫代碼?看天色還早,繼續寫代碼吧!
構建這個PC桌面應用,咱們須要幾個步驟:
在獲得第一次的顯示結果後,通過測試,有很大可能會根據結果再對界面進行調整,實際上也是一個局部的軟件工程中的迭代開發。
啓動Visual Studio 2017, 建立一個基於C#語言的WPF(Windows Presentation Foundation)項目:
WPF是一個很是成熟的技術,在有界面展現和交互的狀況下,使用XAML設計/渲染引擎,比WinForm程序要強101倍,再加上有C#語言利器的幫助,是寫PC桌面前端應用的最佳組合。
給Project起個名字,好比叫"CartoonTranslate",選擇最新的.NET Framework (4.6以上),而後點擊"OK"。咱們先一塊兒來設計一下界面:
Input URL:用於輸入互聯網上的一張漫畫圖片的URL
Engine:指的是兩個不一樣的算法引擎,其中,OCR舊引擎能夠支持25種語言,識別效果能夠接受;而Recognize Text新引擎目前只能支持英文,但效果比較好。
Language:制定當前要翻譯的漫畫的語言,咱們只以英文和日文爲例,其它國家的漫畫相對較少,但一通百通,同樣能夠支持。
右側的一堆Button瞭解一下:
Show:展現Input URL中的圖片到下面的圖片區
OCR:調用OCR服務
Translate:調用文本翻譯服務,將日文或者英文翻譯成中文
下側大面積的圖片區瞭解一下:
Source Image:原始漫畫圖片
Target Image:翻譯成中文對白後的漫畫圖片
咱們在MainWindow.xaml文件裏面填好如下code:
<Window x:Class="CartoonTranslate.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:CartoonTranslate" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <StackPanel Orientation="Horizontal" Grid.Row="0"> <TextBlock Grid.Row="0" Text="Input URL:"/> <TextBox x:Name="tb_Url" Grid.Row="1" Width="600" Text="http://stat.ameba.jp/user_images/20121222/18/secretcube/2e/19/j/o0800112012341269548.jpg"/> <Button x:Name="btn_Show" Content="Show" Click="btn_Show_Click" Width="100"/> <Button x:Name="btn_OCR" Content="OCR" Click="btn_OCR_Click" Width="100"/> <Button x:Name="btn_Translate" Content="Translate" Click="btn_Translate_Click" Width="100"/> </StackPanel> <StackPanel Grid.Row="1" Orientation="Horizontal"> <TextBlock Text="Engine:"/> <RadioButton x:Name="rb_V1" GroupName="gn_Engine" Content="OCR" Margin="20,0" IsChecked="True" Click="rb_V1_Click"/> <RadioButton x:Name="rb_V2" GroupName="gn_Engine" Content="Recognize Text" Click="rb_V2_Click"/> <TextBlock Text="Language:" Margin="20,0"/> <RadioButton x:Name="rb_English" GroupName="gn_Language" Content="English"/> <RadioButton x:Name="rb_Japanese" GroupName="gn_Language" Content="Japanese" IsChecked="True" Margin="20,0"/> </StackPanel> <Grid Grid.Row="3"> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="40"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <TextBlock Grid.Column="0" Text="Source Image" VerticalAlignment="Center" HorizontalAlignment="Center"/> <TextBlock Grid.Column="2" Text="Target Image" VerticalAlignment="Center" HorizontalAlignment="Center"/> <Image x:Name="imgSource" Grid.Column="0" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/> <Image x:Name="imgTarget" Grid.Column="2" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top"/> <Canvas x:Name="canvas_1" Grid.Column="0"/> <Canvas x:Name="canvas_2" Grid.Column="2"/> </Grid> </Grid> </Window>
關於XAML語法的問題不在本文的討論範圍以內。上面的XAML寫好後,編譯時會出錯,由於裏面定義了不少事件,在C#文件中尚未實現。因此,咱們如今把事件代碼補上。
局部變量定義(在MainWindow.xaml.cs的MainWindow class裏面):
// using 「OCR」 or 「Recognize Text」 private string Engine; // source language, English or Japanese private string Language; // OCR result object private OcrResult.Rootobject ocrResult;
點擊Show按鈕的事件,把URL中的漫畫的地址所指向的圖片加載到窗口中顯示:
private void btn_Show_Click(object sender, RoutedEventArgs e) { if (!Uri.IsWellFormedUriString(this.tb_Url.Text, UriKind.Absolute)) { // show warning message return; } // show image at imgSource BitmapImage bi = new BitmapImage(); bi.BeginInit(); bi.UriSource = new Uri(this.tb_Url.Text); bi.EndInit(); this.imgSource.Source = bi; this.imgTarget.Source = bi; }
在上面的代碼中,同時給左右兩個圖片區域賦值,顯示兩張同樣的圖片。
點擊OCR按鈕的事件,會調用OCR REST API,而後根據返回結果把全部識別出來的文字用紅色的矩形框標記上:
private async void btn_OCR_Click(object sender, RoutedEventArgs e) { this.Engine = GetEngine(); this.Language = GetLanguage(); if (Engine == "OCR") { ocrResult = await CognitiveServiceAgent.DoOCR(this.tb_Url.Text, Language); foreach (OcrResult.Region region in ocrResult.regions) { foreach (OcrResult.Line line in region.lines) { if (line.Convert()) { Rectangle rect = new Rectangle() { Margin = new Thickness(line.BB[0], line.BB[1], 0, 0), Width = line.BB[2], Height = line.BB[3], Stroke = Brushes.Red, //Fill =Brushes.White }; this.canvas_1.Children.Add(rect); } } } } else { } }
在上面的代碼中,經過調用DoOCR()自定義函數返回了反序列化好的類,再依次把返回結果集中的每一個矩形生成一個Rectangle圖形類,它的left和top用Margin的方式來定義,width和height直接賦值便可,把這些Rectangle圖形類的實例添加到canvas_1的Visual Tree裏便可顯示出來(這個就是WPF的好處啦,不用處理繪圖事件,但性能不如用Graphics類直接繪圖)。
點擊Translate按鈕的事件:
private async void btn_Translate_Click(object sender, RoutedEventArgs e) { List<string> listTarget = await this.Translate(); this.ShowTargetText(listTarget); } private async Task<List<string>> Translate() { List<string> listSource = new List<string>(); List<string> listTarget = new List<string>(); if (this.Version == "OCR") { foreach (OcrResult.Region region in ocrResult.regions) { foreach (OcrResult.Line line in region.lines) { listSource.Add(line.TEXT); if (listSource.Count >= 25) { List<string> listOutput = await CognitiveServiceAgent.DoTranslate(listSource, Language, "zh-Hans"); listTarget.AddRange(listOutput); listSource.Clear(); } } } if (listSource.Count > 0) { List<string> listOutput = await CognitiveServiceAgent.DoTranslate(listSource, Language, "zh-Hans"); listTarget.AddRange(listOutput); } } return listTarget; } private void ShowTargetText(List<string> listTarget) { int i = 0; foreach (OcrResult.Region region in ocrResult.regions) { foreach (OcrResult.Line line in region.lines) { string translatedLine = listTarget[i]; Rectangle rect = new Rectangle() { Margin = new Thickness(line.BB[0], line.BB[1], 0, 0), Width = line.BB[2], Height = line.BB[3], Stroke = null, Fill =Brushes.White }; this.canvas_2.Children.Add(rect); TextBlock tb = new TextBlock() { Margin = new Thickness(line.BB[0], line.BB[1], 0, 0), Height = line.BB[3], Width = line.BB[2], Text = translatedLine, FontSize = 16, TextWrapping = TextWrapping.Wrap, Foreground = Brushes.Red }; this.canvas_2.Children.Add(tb); i++; } } }
上面這段代碼中,包含了兩個函數:this.Translate()和this.ShowTargetText()。
咱們先看第一個函數:最難理解的地方多是有個"25"數字,這是由於Translate API容許一次提交多個字符串並一塊兒返回結果,這樣比你提交25次字符串要快的多。翻譯好的結果按順序放在listOutput裏,供後面使用。
再看第二個函數:先根據原始文字的矩形區域,生成一些白色的實心矩形,把它們貼在右側的目標圖片上,達到把原始文字覆蓋(扣去)的目的。而後再根據每一個原始矩形生成一個TextBlock,設定好它的位置和尺寸,再設置好翻譯後的結果(translatedLine),這樣就能夠把中文文字貼到圖上了。
點擊Radio Button的事件:
private void rb_V1_Click(object sender, RoutedEventArgs e) { this.rb_Japanese.IsEnabled = true; } private void rb_V2_Click(object sender, RoutedEventArgs e) { this.rb_English.IsChecked = true; this.rb_Japanese.IsChecked = false; this.rb_Japanese.IsEnabled = false; } private string GetLanguage() { if (this.rb_English.IsChecked == true) { return "en"; } else { return "ja"; } } private string GetEngine() { if (this.rb_V1.IsChecked == true) { return "OCR"; } else { return "RecText"; } }
咱們須要在CatroonTranslate工程中添加如下三個.cs文件:
CognitiveServiceAgent.cs文件完成與REST API交互的工做,包括調用OCR服務的和調用翻譯服務的代碼:
using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; using System.Web; namespace CartoonTranslate { class CognitiveServiceAgent { const string OcrEndPointV1 = "https://westcentralus.api.cognitive.microsoft.com/vision/v2.0/ocr?detectOrientation=true&language="; const string OcrEndPointV2 = "https://westcentralus.api.cognitive.microsoft.com/vision/v2.0/recognizeText?mode=Printed"; const string VisionKey1 = "4c20ac56e1e7459a05e1497270022b"; const string VisionKey2 = "97992f0987e4be6b5be132309b8e57"; const string UrlContentTemplate = "{{\"url\":\"{0}\"}}"; const string TranslateEndPoint = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&from={0}&to={1}"; const string TKey1 = "04023df3a4c499b1fc82510b48826c"; const string TKey2 = "9f76381748549cb503dae4a0d80a80"; public static async Task<List<string>> DoTranslate(List<string> text, string fromLanguage, string toLanguage) { try { using (HttpClient hc = new HttpClient()) { hc.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", TKey1); string jsonBody = CreateJsonBodyElement(text); StringContent content = new StringContent(jsonBody, Encoding.UTF8, "application/json"); string uri = string.Format(TranslateEndPoint, fromLanguage, toLanguage); HttpResponseMessage resp = await hc.PostAsync(uri, content); string json = await resp.Content.ReadAsStringAsync(); var ro = Newtonsoft.Json.JsonConvert.DeserializeObject<List<TranslateResult.Class1>>(json); List<string> list = new List<string>(); foreach(TranslateResult.Class1 c in ro) { list.Add(c.translations[0].text); } return list; } } catch (Exception ex) { Debug.WriteLine(ex.Message); return null; } } private static string CreateJsonBodyElement(List<string> text) { var a = text.Select(t => new { Text = t }).ToList(); var b = JsonConvert.SerializeObject(a); return b; } /// <summary> /// /// </summary> /// <param name="imageUrl"></param> /// <param name="language">en, ja, zh</param> /// <returns></returns> public static async Task<OcrResult.Rootobject> DoOCR(string imageUrl, string language) { try { using (HttpClient hc = new HttpClient()) { ByteArrayContent content = CreateHeader(hc, imageUrl); var uri = OcrEndPointV1 + language; HttpResponseMessage resp = await hc.PostAsync(uri, content); string result = string.Empty; if (resp.StatusCode == System.Net.HttpStatusCode.OK) { string json = await resp.Content.ReadAsStringAsync(); Debug.WriteLine(json); OcrResult.Rootobject ro = Newtonsoft.Json.JsonConvert.DeserializeObject<OcrResult.Rootobject>(json); return ro; } } return null; } catch (Exception ex) { Debug.Write(ex.Message); return null; } } private static ByteArrayContent CreateHeader(HttpClient hc, string imageUrl) { hc.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", VisionKey1); string body = string.Format(UrlContentTemplate, imageUrl); byte[] byteData = Encoding.UTF8.GetBytes(body); var content = new ByteArrayContent(byteData); content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); return content; } } }
小提示:以上兩個Key是沒法直接使用的,請使用本身申請的Key。
其中,DoTranslate()函數和DoOCR()函數都是HTTP調用,很容易理解。只有CreateJsonBodyElement函數須要解釋一下。前面提到過咱們一次容許給服務器提交25個字符串作批量翻譯,所以傳進來的是個List<string>,通過這個函數的簡單處理,會獲得如下JSON格式的數據做爲HTTP的Body:
// JSON Data as Body [ {「Text」 : 」第1個字符串」}, {「Text」 : 」第2個字符串」}, …….. {「Text」 : 」第25個字符串」}, ]
OcrResult.cs文件是OCR服務返回的JSON數據所對應的類,用於反序列化:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace CartoonTranslate.OcrResult { public class Rootobject { public string language { get; set; } public string orientation { get; set; } public float textAngle { get; set; } public Region[] regions { get; set; } } public class Region { public string boundingBox { get; set; } public Line[] lines { get; set; } } public class Line { public string boundingBox { get; set; } public Word[] words { get; set; } public int[] BB { get; set; } public string TEXT { get; set; } public bool Convert() { CombineWordToSentence(); return ConvertBBFromString2Int(); } private bool ConvertBBFromString2Int() { string[] tmp = boundingBox.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries); if (tmp.Length == 4) { BB = new int[4]; for (int i = 0; i < 4; i++) { int.TryParse(tmp[i], out BB[i]); } return true; } return false; } private void CombineWordToSentence() { StringBuilder sb = new StringBuilder(); foreach (Word word in words) { sb.Append(word.text); } this.TEXT = sb.ToString(); } } public class Word { public string boundingBox { get; set; } public string text { get; set; } } }
須要說明的是,服務器返回的boundingBox是個string類型,在後面使用起來不太方便,須要把它轉換成整數,因此增長了CovertBBFromString2Int()函數。還有就是返回的是一個個的詞(Word),而不是一句話,因此增長了CombineWordToSentence()來把詞連成句子。
TranslateResult.cs文件翻譯服務返回的JSON所對應的類,用於反序列化:
namespace CartoonTranslate.TranslateResult { public class Class1 { public Translation[] translations { get; set; } } public class Translation { public string text { get; set; } public string to { get; set; } } }
小提示:在VS2017中,這種類不須要手工鍵入,能夠在Debug模式下先把返回的JSON拷貝下來,而後新建一個.cs文件,在裏面用Paste Special從JSON直接生成類就能夠了。
好啦,大功告成!如今要作的事就是點擊F5來編譯執行程序。若是一切順利的話,將會看到界面設計部分所展現的窗口。
咱們第一步先點擊"Show"按鈕,會獲得:
再點擊"OCR"按鈕,等兩三秒(取決於網絡速度),會看到左側圖片中紅色的矩形圍攏的一些文字。有些文字沒有被識別出來的話,就沒有紅色矩形。
最後點擊"Translate"按鈕,稍等一小會兒,會看到右側圖片的變化:
Wow! 大部分的日文被翻譯成了中文,並且位置也擺放得很合適。
目前的代碼中沒有不少容錯機制,好比當服務器返回錯誤時,訪問API的代碼會返回一個NULL對象,在上層沒有作處理,直接崩潰。再好比,當用戶不按照從左到右的順序點擊上面三個button時,會產生意想不到的狀況。
本應用處理單頁的漫畫,而且提供了交互,目的是讓你們直觀理解工做過程,實際上這個過程能夠作成批量自動化的,也就是輸入一大堆URL,作後臺識別/翻譯/從新生成圖片後,把圖片批量保存在本地,再進行後處理。
固然,識別引擎不是萬能的,不少時候不可能準確識別/翻譯出全部對白文字。因此,能夠考慮提供一個相似本應用的交互工具,讓漫畫翻譯從業者在機器處理以後,對有錯誤的地方進行糾正。
小提示:請嚴格遵照知識產權保護法!在合法的狀況下作事。
還記得前面提到過新舊引擎的話題嗎?咱們在界面上作了一個Radio Button "Recognize Text",可是並無使用它。由於這個新引擎目前還只能支持英文的OCR,因此,若是你們對漫威Marvel漫畫(英文爲主)感興趣的話,就能夠用到這個引擎啦,與舊OCR引擎相比,不能同日而語,超級棒!
舊OCR引擎的文檔在這裏:https://westus.dev.cognitive.microsoft.com/docs/services/5adf991815e1060e6355ad44/operations/56f91f2e778daf14a499e1fc
新Recognize Text引擎的文檔在這裏:
新的引擎在API交互設計上,有一個不一樣的地方:當你提交一個請求後,服務器會馬上返回Accepted (202),而後給你一個URL,用於查詢狀態的。因而須要在客戶端程序裏設個定時器,每隔一段時間(好比200ms),訪問那個URL,來得到最終的OCR結果。
返回的結果JSON格式也有所不一樣,你們能夠本身試着實現一下:
在下圖中,如綠色橢圓區域所示,OCR引擎犯了一個小錯誤,它把上下兩個不一樣對白氣泡的文字框在了一塊兒。
這個是能夠在本身的程序裏作後期糾錯處理來矯正的。你們能夠仔細分析OCR的返回結果,看看如何實現。文檔在這裏:
觀察力好的同窗,可能會發現一個問題,以下圖所示,左側圖的一個對白氣泡裏,有四句話,但其實它們是一句話,分開寫到4列而已。
這種狀況帶來的問題是:這四句話分別送給翻譯引擎作翻譯,會形成先後不連貫,語句不通順。能夠考慮的解決方案是,先根據矩形的位置信息,把這四句話合併成同一句話,再送給翻譯引擎。這就是標準的聚類問題,經過搜索引擎能夠找到一大堆參考文檔,好比這些:
https://blog.csdn.net/summer_upc/article/details/51475512
https://www.ibm.com/developerworks/cn/analytics/library/ba-1607-clustering-algorithm/index.html