OCR библиотека для WinRT приложений. Часть 2.

В первой части мы начали подготавливать почву для базового приложения. Собственно в это части мы его закончим и научимся пользоваться данной библиотекой.

Нашим следующим этапом было связывание нашей 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. Ниже расположены три простых текстовых блока и кнопка, по нажатию на которую мы будем запускать распознавание.

Я буду возвращаться к канвасу, но его суть заключается в обеспечении поверхности рисования поверх изображения

Запустим и посмотрим, что же у нас получилось в итоге:

123

Добавим 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…

Advertisements
Tagged with: , , ,
Опубликовано в Development, Windows 8.1
One comment on “OCR библиотека для WinRT приложений. Часть 2.
  1. […] затем связать ее с представлением… Но это уже будет в следующей части данной […]

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: