В первой части мы начали подготавливать почву для базового приложения. Собственно в это части мы его закончим и научимся пользоваться данной библиотекой.
Нашим следующим этапом было связывание нашей ViewModel’и с представлением:
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:App253" xmlns:img="Windows.UI.Xaml.Media.Imaging" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:Converters="using:App253.Converters" x:Class="App253.MainPage" mc:Ignorable="d" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> <Page.BottomAppBar> <CommandBar IsOpen="True"> <AppBarButton Icon="ZoomIn" Label="recognise" Command="{Binding RecogniseCommand}" /> </CommandBar> </Page.BottomAppBar> <Grid x:Name="LayoutRoot"> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Margin="19,0,0,0"> <TextBlock Text="OCR TEST APP" Style="{ThemeResource TitleTextBlockStyle}" Margin="0,12,0,0" /> <TextBlock Text="cards" Margin="0,-6.5,0,26.5" Style="{ThemeResource HeaderTextBlockStyle}" CharacterSpacing="{ThemeResource PivotHeaderItemCharacterSpacing}" /> </StackPanel> <Grid Grid.Row="1" x:Name="ContentRoot" Margin="19,9.5,19,0"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <local:IncrementalFlipView ItemsSource="{Binding Items}" SelectedValue="{Binding SelectedPhoto, Mode=TwoWay}" IsEnabled="{Binding IsIdle}" x:Name="flipView"> <local:IncrementalFlipView.ItemTemplate> <DataTemplate> <!-- What I want here is for the Image to size itself and for the Canvas to be the same size as the Image no matter what the image does --> <Grid VerticalAlignment="Center" HorizontalAlignment="Center" Width="Auto" Height="Auto"> <Image Source="{Binding ImageUrl}" Stretch="Uniform"> </Image> <Canvas HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RenderTransformOrigin="0.5,0.5"> <Canvas.RenderTransform> <RotateTransform Angle="0" /> </Canvas.RenderTransform> </Canvas> <TextBlock Text="{Binding Title}" /> </Grid> </DataTemplate> </local:IncrementalFlipView.ItemTemplate> </local:IncrementalFlipView> <StackPanel Orientation="Vertical" Grid.Row="1" VerticalAlignment="Top" Margin="0,0,0,40"> <TextBlock Text="name" Style="{StaticResource TitleTextBlockStyle}" /> <TextBlock Text="{Binding Name, FallbackValue=unset, TargetNullValue=unset}" Style="{StaticResource ListViewItemContentTextBlockStyle}" Margin="10,0,0,0" /> <TextBlock Text="email" Style="{StaticResource TitleTextBlockStyle}" /> <TextBlock Text="{Binding Email, FallbackValue=unset, TargetNullValue=unset}" Style="{StaticResource ListViewItemContentTextBlockStyle}" Margin="10,0,0,0" /> <TextBlock Text="phone" Style="{StaticResource TitleTextBlockStyle}" /> <TextBlock Text="{Binding Phone, FallbackValue=unset, TargetNullValue=unset}" Style="{StaticResource ListViewItemContentTextBlockStyle}" Margin="10,0,0,0" /> </StackPanel> </Grid> </Grid> </Page>
и так, в итоге у нас получилось: IncrementalFlipView, который отображает товары и показывает изображения, а также выводит название в TextBlock. Ниже расположены три простых текстовых блока и кнопка, по нажатию на которую мы будем запускать распознавание.
Я буду возвращаться к канвасу, но его суть заключается в обеспечении поверхности рисования поверх изображения
Запустим и посмотрим, что же у нас получилось в итоге:
Добавим OCR.
Пока все идет хорошо, но этот пост должен был быть о использованием библиотеки OCR, а не о создании кастомных контролов и т.п, поэтому собственно я и добавлял ссылку на «Microsoft OCR» библиотеку.
Затем я добавил небольшой класс для представления изображения:
class CurrentImageInfo { public uint Width { get; set; } public uint Height { get; set; } public byte[] Pixels { get; set; } }
Добавим немного переменных:
CurrentImageInfo currentImageInfo; HttpClient httpClient; OcrEngine ocrEngine;
Изменим немного конструктор:
public ViewModel() { this.Items = new FlickrBusinessCardPhotoResultCollection(); this.recogniseCommand = new SimpleCommand(this.OnRecognise); this.IsIdle = true; this.httpClient = new HttpClient(); this.ocrEngine = new OcrEngine(OcrLanguage.English); }
И наконец-то начнем реализовывать метод OnRecognise:
async void OnRecognise() { this.IsIdle = false; try { if (this.selectedPhoto != null) { // I've deliberately avoided downloading any image bits until this point. // We (probably) have the image on the screen. However, that's hidden // inside an Image control which I'm letting do the URL->Image work // for me (as well as any caching it decides to do). // But, now, I actually need the bytes of the image itself and I can't // just grab them out of the image control so we go back to the web. try { await this.DownloadImageBitsAsync(); OcrResult ocrResult = await this.RunOcrAsync(); if (ocrResult != null) { } } catch { // TBD... } } } finally { this.IsIdle = true; }
и конечно же функция по загрузке изображения:
async Task DownloadImageBitsAsync() { this.currentImageInfo = null; // TODO: do I really have to do all this to get the pixels, width, height or // can I shortcut it somehow? using (var inputStream = await this.httpClient.GetInputStreamAsync(new Uri(this.SelectedPhoto.ImageUrl))) { using (InMemoryRandomAccessStream memoryStream = new InMemoryRandomAccessStream()) { await RandomAccessStream.CopyAsync(inputStream, memoryStream); memoryStream.Seek(0); BitmapDecoder decoder = await BitmapDecoder.CreateAsync(memoryStream); PixelDataProvider provider = await decoder.GetPixelDataAsync(); this.currentImageInfo = new CurrentImageInfo() { Width = decoder.PixelWidth, Height = decoder.PixelHeight, Pixels = provider.DetachPixelData() }; } } }
и наконец-то использование искомой библиотеки:
async Task<OcrResult> RunOcrAsync() { var results = await this.ocrEngine.RecognizeAsync( this.currentImageInfo.Height, this.currentImageInfo.Width, this.currentImageInfo.Pixels); return (results); }
По-моему использование библиотеки ОЧЕНЬ простое. Наш вариант ни чем не отличается по сложности — выкачали изображение — передали на распознавание!
Получение результатов и рисование квадратов вокруг совпадений.
С точки зрения отображения результатов — я написал небольшой класс, который поможет мне распарсить номера телефонов, адреса электронной почты и простые имена:
namespace App253 { using System.Collections.Generic; using System.Text.RegularExpressions; enum RecognitionType { Other, Email, Phone, Name } static class CardTextRecogniser { public static RecognitionType Recognise(string businessCardText) { RecognitionType type = RecognitionType.Other; foreach (var expression in expressions) { if (Regex.IsMatch(businessCardText, expression.Value)) { type = expression.Key; break; } } return(type); } static Dictionary<RecognitionType, string> expressions = new Dictionary<RecognitionType,string>() { // regex taken from MSDN: http://msdn.microsoft.com/en-us/library/01escwtf(v=vs.110).aspx { RecognitionType.Email, @"^(?("")("".+?(?<!\\)""@)|(([0-9a-z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-z])@))" + @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-z][-\w]*[0-9a-z]*\.)+[a-z0-9][\-a-z0-9]{0,22}[a-z0-9]))$" }, // regex taken from regex lib: http://regexlib.com/REDetails.aspx?regexp_id=296 { RecognitionType.Phone, @"^(\+[1-9][0-9]*(\([0-9]*\)|-[0-9]*-))?[0]?[1-9][0-9\- ]*$" }, // regex taken from regex lib: http://regexlib.com/REDetails.aspx?regexp_id=247 { RecognitionType.Name, @"^([ \u00c0-\u01ffa-zA-Z'])+$" }, }; } }
ничего заумного там не происходит. Далее рассмотрим выделение необходимого текста на изображении. При этом нельзя забывать, что изображение, которое отображается на экране, возможно, имеет немного другие размеры по отношению к тому, которое мы получили из интернета и передали на распознание OCR.
Именно поэтому я написал следующий класс-маппер:
namespace App253 { using Windows.Foundation; class CoordinateMapper { public CoordinateMapper(Rect sourceSpace, Rect destSpace) { this.sourceSpace = sourceSpace; this.destSpace = destSpace; } public Point MapPoint(Point source) { double x = (source.X - sourceSpace.Left) / sourceSpace.Width; x = destSpace.Left + (x * (destSpace.Width)); double y = (source.Y - sourceSpace.Top) / (sourceSpace.Height); y = destSpace.Left + (y * (destSpace.Height)); return(new Point() { X = x, Y = y}); } public double MapWidth(double width) { return(width / sourceSpace.Width * destSpace.Width); } public double MapHeight(double height) { return (height / sourceSpace.Height * destSpace.Height); } Rect sourceSpace; Rect destSpace; } }
После я сделал небольшие изменений в коде страницы и передал ItemContainerGenerator логике, и добавил немного переменных во ViewModel.
static SolidColorBrush redBrush = new SolidColorBrush(Colors.Red); Canvas drawCanvas; CurrentImageInfo currentImageInfo;
Далее, при каждом изменении выбранного (отображаемого) элемента будем обнулять значение вспомогательных полей:
public FlickrPhotoResult SelectedPhoto { get { return (this.selectedPhoto); } set { this.selectedPhoto = value; // modified this.InitialiseDrawCanvas(); } }
метод выглядит следующим образом:
void InitialiseDrawCanvas() { this.drawCanvas = null; if (this.SelectedPhoto != null) { FlipViewItem fvi = (FlipViewItem)this.itemContainerGenerator.ContainerFromItem(this.SelectedPhoto); this.drawCanvas = (Canvas)fvi.GetDescendantByType(typeof(Canvas)); this.drawCanvas.Children.Clear(); this.Phone = this.Email = this.Name = string.Empty; } }
Отображение результатов
В итоге я немного модифицировал метод распознования:
async void OnRecognise() { this.IsIdle = false; try { if (this.selectedPhoto != null) { // I've deliberately avoided downloading any image bits until this point. // We (probably) have the image on the screen. However, that's hidden // inside an Image control which I'm letting do the URL->Image work // for me (as well as any caching it decides to do). // But, now, I actually need the bytes of the image itself and I can't // just grab them out of the image control so we go back to the web. try { await this.DownloadImageBitsAsync(); OcrResult ocrResult = await this.RunOcrAsync(); if (ocrResult != null) { // Modified this.DrawOcrResults(ocrResult); this.ApplyPatternMatching(ocrResult); } } catch { // TBD... } } } finally { this.IsIdle = true; } }
и собственно отрисовка:
void DrawOcrResults(OcrResult ocrResult) { this.RepeatForOcrWords(ocrResult, (result, word) => { Rectangle rectangle = MakeOcrDrawRectangle(ocrResult, word); this.drawCanvas.Children.Add(rectangle); }); } void RepeatForOcrWords(OcrResult ocrResult, Action<OcrResult, OcrWord> repeater) { if (ocrResult.Lines != null) { foreach (var line in ocrResult.Lines) { foreach (var word in line.Words) { repeater(ocrResult, word); } } } } Rectangle MakeOcrDrawRectangle(OcrResult ocrResult, OcrWord word) { // Avoided using CompositeTransform here as I could never quite get my // combination of Scale/Rotate/Translate to work right for a given // RenderTransformOrigin. Probably my fault but it was easier to // just do it myself. CoordinateMapper mapper = new CoordinateMapper(new Rect(0, 0, this.currentImageInfo.Width, this.currentImageInfo.Height), new Rect(0, 0, this.drawCanvas.ActualWidth, this.drawCanvas.ActualHeight)); Rectangle r = new Rectangle() { Width = mapper.MapWidth(word.Width), Height = mapper.MapHeight(word.Height), RenderTransformOrigin = new Point(0.5, 0.5) }; r.Stroke = redBrush; r.StrokeThickness = 1; Point mappedPoint = mapper.MapPoint(new Point(word.Left, word.Top)); Canvas.SetLeft(r, mappedPoint.X); Canvas.SetTop(r, mappedPoint.Y); RotateTransform rotate = this.drawCanvas.RenderTransform as RotateTransform; rotate.Angle = 0.0d - ocrResult.TextAngle ?? 0.0d; return r; }
ну и конечно же не забываем выводить данные на экран:
void ApplyPatternMatching(OcrResult ocrResult) { this.RepeatForOcrWords(ocrResult,(result, word) => { switch (CardTextRecogniser.Recognise(word.Text)) { case RecognitionType.Other: break; case RecognitionType.Email: this.Email = word.Text; break; case RecognitionType.Phone: this.Phone = word.Text; break; case RecognitionType.Name: this.Name = word.Text; break; default: break; } } ); }
Ну вот и все! Не правда ли все очень просто?
Ссылка на источник: OCR Library for WinRT Apps–Recognising Business Cards…
[…] затем связать ее с представлением… Но это уже будет в следующей части данной […]