2017-01-24 22 views
6

W my previous question, że przekształcony obraz:Jak obracać poszczególne litery obrazu we właściwej orientacji, aby uzyskać optymalny OCR?

enter image description here

w to:

enter image description here

które Tesserakt OCR interpretuje jak to:

1O351 

Umieszczenie ramką image

enter image description here

w rzeczywistości poprawia wynik OCR.

1CB51 

Jednak muszę wszystkie 5 znaków do OCR prawidłowo, tak eksperymentu użyłem Paint.NET obracać i wyrównać każdą poszczególną list do jego właściwej orientacji:

enter image description here

skutkuje poprawna odpowiedź:

1CB52 

Jak mam wykonać tę korektę w języku C#?

Zrobiłem trochę badań na różnych algorytmach wyrównanie tekstu, ale wszystkie one zakładają istnienie linii tekstu w obrazie źródłowym, linie, z których można czerpać kąt obrotu, ale które już zawierają właściwe rozstawienie i orientacja relacji między literami.

+0

Można znaleźć wszystkie pojedynczych liter i oddzielić je i obracać je wszystkie indywidualnie – TheLethalCoder

+0

Tak, to dość dużo ogólny opis co trzeba osiągnąć w rzeczywistym kodzie. Już wiem * co * muszę zrobić; teraz po prostu muszę wymyślić * jak * to zrobić. –

+0

Wygląda na to, że próbujesz złamać CAPTCHA. Nawet jeśli ci się uda, istnieją teraz inne typy CAPTCHA, które nie będą tak łatwe do złamania, jak na przykład system reCAPTCHA "Nie jestem robotem" od Google, a nawet [systemy tekstowe takie jak te pokazane w tym forum] (https://www.leadtools.com/support/forum/posts/t10685-HOWTO-Generate-CAPTCHA-using-LEAD-image-processing-functions). –

Odpowiedz

9

Możesz użyć kodu w następującej code project article, aby posegmentować poszczególne znaki. Jednak przy próbie wygięcia tych postaci pojedynczo każdy wynik, który otrzymasz, nie będzie zbyt dobry, ponieważ nie ma zbyt wielu informacji, które można by wykorzystać.

Próbowałem używać AForge.NET s HoughLineTransformation class i mam kąty w zakresie 80 - 90 stopni. Tak próbowałem przy użyciu następującego kodu do ich prostowanie:

private static Bitmap DeskewImageByIndividualChars(Bitmap targetBitmap) 
{ 
    IDictionary<Rectangle, Bitmap> characters = new CCL().Process(targetBitmap); 

    using (Graphics g = Graphics.FromImage(targetBitmap)) 
    { 
     foreach (var character in characters) 
     { 
      double angle; 

      BitmapData bitmapData = character.Value.LockBits(new Rectangle(Point.Empty, character.Value.Size), ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed); 
      try 
      { 
       HoughLineTransformation hlt = new HoughLineTransformation(); 
       hlt.ProcessImage(bitmapData); 

       angle = hlt.GetLinesByRelativeIntensity(0.5).Average(l => l.Theta); 
      } 
      finally 
      { 
       character.Value.UnlockBits(bitmapData); 
      } 

      using (Bitmap bitmap = RotateImage(character.Value, 90 - angle, Color.White)) 
      { 
       g.DrawImage(bitmap, character.Key.Location); 
      } 
     } 
    } 

    return targetBitmap; 
} 

Z RotateImage method taken from here. Jednak wyniki nie wydają się być najlepszym. Może możesz spróbować je ulepszyć.

Oto kod z artykułu projektu, w celach informacyjnych. Zrobiłem kilka zmian do niego tak, że zachowuje się nieco bezpieczniejsze, takie jak dodawanie try-finally wokół LockBits i usuwania obiektów prawidłowo za pomocą instrukcji using itp

using System.Collections.Generic; 
using System.Drawing; 
using System.Drawing.Imaging; 
using System.Linq; 

namespace ConnectedComponentLabeling 
{ 
    public class CCL 
    { 
     private Bitmap _input; 
     private int[,] _board; 

     public IDictionary<Rectangle, Bitmap> Process(Bitmap input) 
     { 
      _input = input; 
      _board = new int[_input.Width, _input.Height]; 

      Dictionary<int, List<Pixel>> patterns = Find(); 
      var images = new Dictionary<Rectangle, Bitmap>(); 

      foreach (KeyValuePair<int, List<Pixel>> pattern in patterns) 
      { 
       using (Bitmap bmp = CreateBitmap(pattern.Value)) 
       { 
        images.Add(GetBounds(pattern.Value), (Bitmap)bmp.Clone()); 
       } 
      } 

      return images; 
     } 

     protected virtual bool CheckIsBackGround(Pixel currentPixel) 
     { 
      return currentPixel.color.A == 255 && currentPixel.color.R == 255 && currentPixel.color.G == 255 && currentPixel.color.B == 255; 
     } 

     private unsafe Dictionary<int, List<Pixel>> Find() 
     { 
      int labelCount = 1; 
      var allLabels = new Dictionary<int, Label>(); 

      BitmapData imageData = _input.LockBits(new Rectangle(0, 0, _input.Width, _input.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb); 
      try 
      { 
       int bytesPerPixel = 3; 

       byte* scan0 = (byte*)imageData.Scan0.ToPointer(); 
       int stride = imageData.Stride; 

       for (int i = 0; i < _input.Height; i++) 
       { 
        byte* row = scan0 + (i * stride); 

        for (int j = 0; j < _input.Width; j++) 
        { 
         int bIndex = j * bytesPerPixel; 
         int gIndex = bIndex + 1; 
         int rIndex = bIndex + 2; 

         byte pixelR = row[rIndex]; 
         byte pixelG = row[gIndex]; 
         byte pixelB = row[bIndex]; 

         Pixel currentPixel = new Pixel(new Point(j, i), Color.FromArgb(pixelR, pixelG, pixelB)); 

         if (CheckIsBackGround(currentPixel)) 
         { 
          continue; 
         } 

         IEnumerable<int> neighboringLabels = GetNeighboringLabels(currentPixel); 
         int currentLabel; 

         if (!neighboringLabels.Any()) 
         { 
          currentLabel = labelCount; 
          allLabels.Add(currentLabel, new Label(currentLabel)); 
          labelCount++; 
         } 
         else 
         { 
          currentLabel = neighboringLabels.Min(n => allLabels[n].GetRoot().Name); 
          Label root = allLabels[currentLabel].GetRoot(); 

          foreach (var neighbor in neighboringLabels) 
          { 
           if (root.Name != allLabels[neighbor].GetRoot().Name) 
           { 
            allLabels[neighbor].Join(allLabels[currentLabel]); 
           } 
          } 
         } 

         _board[j, i] = currentLabel; 
        } 
       } 
      } 
      finally 
      { 
       _input.UnlockBits(imageData); 
      } 

      Dictionary<int, List<Pixel>> patterns = AggregatePatterns(allLabels); 

      patterns = RemoveIntrusions(patterns, _input.Width, _input.Height); 

      return patterns; 
     } 

     private Dictionary<int, List<Pixel>> RemoveIntrusions(Dictionary<int, List<Pixel>> patterns, int width, int height) 
     { 
      var patternsCleaned = new Dictionary<int, List<Pixel>>(); 

      foreach (var pattern in patterns) 
      { 
       bool bad = false; 
       foreach (Pixel item in pattern.Value) 
       { 
        //Horiz 
        if (item.Position.X == 0) 
         bad = true; 

        else if (item.Position.Y == width - 1) 
         bad = true; 

        //Vert 
        else if (item.Position.Y == 0) 
         bad = true; 

        else if (item.Position.Y == height - 1) 
         bad = true; 
       } 

       if (!bad) 
        patternsCleaned.Add(pattern.Key, pattern.Value); 

      } 

      return patternsCleaned; 
     } 

     private IEnumerable<int> GetNeighboringLabels(Pixel pix) 
     { 
      var neighboringLabels = new List<int>(); 

      for (int i = pix.Position.Y - 1; i <= pix.Position.Y + 2 && i < _input.Height - 1; i++) 
      { 
       for (int j = pix.Position.X - 1; j <= pix.Position.X + 2 && j < _input.Width - 1; j++) 
       { 
        if (i > -1 && j > -1 && _board[j, i] != 0) 
        { 
         neighboringLabels.Add(_board[j, i]); 
        } 
       } 
      } 

      return neighboringLabels; 
     } 

     private Dictionary<int, List<Pixel>> AggregatePatterns(Dictionary<int, Label> allLabels) 
     { 
      var patterns = new Dictionary<int, List<Pixel>>(); 

      for (int i = 0; i < _input.Height; i++) 
      { 
       for (int j = 0; j < _input.Width; j++) 
       { 
        int patternNumber = _board[j, i]; 
        if (patternNumber != 0) 
        { 
         patternNumber = allLabels[patternNumber].GetRoot().Name; 

         if (!patterns.ContainsKey(patternNumber)) 
         { 
          patterns[patternNumber] = new List<Pixel>(); 
         } 

         patterns[patternNumber].Add(new Pixel(new Point(j, i), Color.Black)); 
        } 
       } 
      } 

      return patterns; 
     } 

     private unsafe Bitmap CreateBitmap(List<Pixel> pattern) 
     { 
      int minX = pattern.Min(p => p.Position.X); 
      int maxX = pattern.Max(p => p.Position.X); 

      int minY = pattern.Min(p => p.Position.Y); 
      int maxY = pattern.Max(p => p.Position.Y); 

      int width = maxX + 1 - minX; 
      int height = maxY + 1 - minY; 

      Bitmap bmp = DrawFilledRectangle(width, height); 

      BitmapData imageData = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb); 
      try 
      { 
       byte* scan0 = (byte*)imageData.Scan0.ToPointer(); 
       int stride = imageData.Stride; 

       foreach (Pixel pix in pattern) 
       { 
        scan0[((pix.Position.X - minX) * 3) + (pix.Position.Y - minY) * stride] = pix.color.B; 
        scan0[((pix.Position.X - minX) * 3) + (pix.Position.Y - minY) * stride + 1] = pix.color.G; 
        scan0[((pix.Position.X - minX) * 3) + (pix.Position.Y - minY) * stride + 2] = pix.color.R; 
       } 
      } 
      finally 
      { 
       bmp.UnlockBits(imageData); 
      } 

      return bmp; 
     } 

     private Bitmap DrawFilledRectangle(int x, int y) 
     { 
      Bitmap bmp = new Bitmap(x, y); 
      using (Graphics graph = Graphics.FromImage(bmp)) 
      { 
       Rectangle ImageSize = new Rectangle(0, 0, x, y); 
       graph.FillRectangle(Brushes.White, ImageSize); 
      } 

      return bmp; 
     } 

     private Rectangle GetBounds(List<Pixel> pattern) 
     { 
      var points = pattern.Select(x => x.Position); 

      var x_query = points.Select(p => p.X); 
      int xmin = x_query.Min(); 
      int xmax = x_query.Max(); 

      var y_query = points.Select(p => p.Y); 
      int ymin = y_query.Min(); 
      int ymax = y_query.Max(); 

      return new Rectangle(xmin, ymin, xmax - xmin, ymax - ymin); 
     } 
    } 
} 

Z powyższego kodu mam następujący wkład/wyjście:

InputOutput

jak widać B obrócił się całkiem dobrze, ale inni nie są tak dobre.


Alternatywą dla próby wyrównywania pojedynczych znaków jest znalezienie tam lokalizacji za pomocą powyższej procedury segmentacji. Następnie przekazujesz każdą poszczególną postać do swojego silnika rozpoznawania osobno i sprawdzasz, czy to poprawia twoje wyniki.


Użyłem następujący sposób znaleźć kąt znaku używając List<Pixel> od wewnątrz klasy CCL. Działa, znajdując kąt między punktami "dolny lewy" i "dolny prawy". Nie testowałem, jeśli działa, jeśli postać jest obracana na odwrót.

private double GetAngle(List<Pixel> pattern) 
{ 
    var pixels = pattern.Select(p => p.Position).ToArray(); 

    Point bottomLeft = pixels.OrderByDescending(p => p.Y).ThenBy(p => p.X).First(); 
    Point rightBottom = pixels.OrderByDescending(p => p.X).ThenByDescending(p => p.Y).First(); 

    int xDiff = rightBottom.X - bottomLeft.X; 
    int yDiff = rightBottom.Y - bottomLeft.Y; 

    double angle = Math.Atan2(yDiff, xDiff) * 180/Math.PI; 

    return -angle; 
} 

Uwaga mój kod rysunek jest nieco uszkodzony tak dlatego 5 jest odcięta po prawej stronie, ale kod ten daje następujący wynik:

Output

pamiętać, że B a 5 są obrócone bardziej niż można się spodziewać z powodu ich krzywizny.


Korzystanie poniższy kod poprzez uzyskanie kąta z lewej i prawej krawędzi, a następnie wybranie najlepszego z nich, obroty wydaje się być lepsze. Uwaga: Przetestowałem go tylko z literami, które wymagają obrotu w kierunku zgodnym z ruchem wskazówek zegara, więc jeśli będą musiały przejść w przeciwnym kierunku, może to nie działać zbyt dobrze.

To również "ćwiartuje" piksele, aby każdy piksel został wybrany z kwadrantu, aby nie uzyskać dwóch, które znajdują się zbyt blisko.

Ideą wyboru najlepszego kąta jest to, czy są one podobne, w tym momencie w obrębie 1,5 stopnia względem siebie, ale można je łatwo zaktualizować, wyrównać. W przeciwnym razie wybieramy ten, który jest najbliżej zera.

private double GetAngle(List<Pixel> pattern, Rectangle bounds) 
{ 
    int halfWidth = bounds.X + (bounds.Width/2); 
    int halfHeight = bounds.Y + (bounds.Height/2); 

    double leftEdgeAngle = GetAngleLeftEdge(pattern, halfWidth, halfHeight); 
    double rightEdgeAngle = GetAngleRightEdge(pattern, halfWidth, halfHeight); 

    if (Math.Abs(leftEdgeAngle - rightEdgeAngle) <= 1.5) 
    { 
     return (leftEdgeAngle + rightEdgeAngle)/2d; 
    } 

    if (Math.Abs(leftEdgeAngle) > Math.Abs(rightEdgeAngle)) 
    { 
     return rightEdgeAngle; 
    } 
    else 
    { 
     return leftEdgeAngle; 
    } 
} 

private double GetAngleLeftEdge(List<Pixel> pattern, double halfWidth, double halfHeight) 
{ 
    var topLeftPixels = pattern.Select(p => p.Position).Where(p => p.Y < halfHeight && p.X < halfWidth).ToArray(); 
    var bottomLeftPixels = pattern.Select(p => p.Position).Where(p => p.Y > halfHeight && p.X < halfWidth).ToArray(); 

    Point topLeft = topLeftPixels.OrderBy(p => p.X).ThenBy(p => p.Y).First(); 
    Point bottomLeft = bottomLeftPixels.OrderByDescending(p => p.Y).ThenBy(p => p.X).First(); 

    int xDiff = bottomLeft.X - topLeft.X; 
    int yDiff = bottomLeft.Y - topLeft.Y; 

    double angle = Math.Atan2(yDiff, xDiff) * 180/Math.PI; 

    return 90 - angle; 
} 

private double GetAngleRightEdge(List<Pixel> pattern, double halfWidth, double halfHeight) 
{ 
    var topRightPixels = pattern.Select(p => p.Position).Where(p => p.Y < halfHeight && p.X > halfWidth).ToArray(); 
    var bottomRightPixels = pattern.Select(p => p.Position).Where(p => p.Y > halfHeight && p.X > halfWidth).ToArray(); 

    Point topRight = topRightPixels.OrderBy(p => p.Y).ThenByDescending(p => p.X).First(); 
    Point bottomRight = bottomRightPixels.OrderByDescending(p => p.X).ThenByDescending(p => p.Y).First(); 

    int xDiff = bottomRight.X - topRight.X; 
    int yDiff = bottomRight.Y - topRight.Y; 

    double angle = Math.Atan2(xDiff, yDiff) * 180/Math.PI; 

    return Math.Abs(angle); 
} 

To teraz produkuje następujące wyjście, ponownie mój kod rysunku jest lekko zepsuty. Zauważ, że model C wygląda na niezbyt dobrze wyprofilowany, ale uważnie przyglądając się, to tylko jego kształt spowodował, że tak się stało.

Output


poprawiłem kod rysowania, a także próbowali dostać znaki na tej samej linii bazowej:

private static Bitmap DeskewImageByIndividualChars(Bitmap bitmap) 
{ 
    IDictionary<Rectangle, Tuple<Bitmap, double>> characters = new CCL().Process(bitmap); 

    Bitmap deskewedBitmap = new Bitmap(bitmap.Width, bitmap.Height, bitmap.PixelFormat); 
    deskewedBitmap.SetResolution(bitmap.HorizontalResolution, bitmap.VerticalResolution); 

    using (Graphics g = Graphics.FromImage(deskewedBitmap)) 
    { 
     g.FillRectangle(Brushes.White, new Rectangle(Point.Empty, deskewedBitmap.Size)); 

     int baseLine = characters.Max(c => c.Key.Bottom); 
     foreach (var character in characters) 
     { 
      int y = character.Key.Y; 
      if (character.Key.Bottom != baseLine) 
      { 
       y += (baseLine - character.Key.Bottom - 1); 
      } 

      using (Bitmap characterBitmap = RotateImage(character.Value.Item1, character.Value.Item2, Color.White)) 
      { 
       g.DrawImage(characterBitmap, new Point(character.Key.X, y)); 
      } 
     } 
    } 

    return deskewedBitmap; 
} 

To wtedy daje następujący wynik. Uwaga: każdy znak nie znajduje się dokładnie na tej samej linii bazowej, ponieważ dno poprzedzające obrót zostało użyte do jego rozwiązania. Aby poprawić kod przy użyciu linii bazowej z rotacji po zakończeniu byłoby potrzebne. Również progowanie obrazu przed wykonaniem linii bazowej mogłoby pomóc.

Inną poprawą byłoby obliczenie Right każdej z obróconych miejsc znaków, więc podczas rysowania następnego nie nakłada się na poprzednie i nie obcina bitów. Ponieważ, jak widać na wyjściu, 2 jest lekko przycinany do 5.

Dane wyjściowe są teraz bardzo podobne do danych utworzonych ręcznie w OP.

Output

+0

Dzięki za to. Zauważ, że uzyskasz lepsze wyniki, obracając za pomocą lewego górnego i lewego dolnego punktu. W przypadku C jest to prawy górny prawy i dolny prawy. Nadal staram się wymyślić, jak wybrać najlepszy zestaw punktów. –

+0

@RobertHarvey Napisałem tylko krótki kod, aby pokazać, że jestem w pracy, więc nie mam na to czasu. Zawsze możesz wyliczyć wszystkie punkty i kąty i je uśrednić, jednocześnie usuwając skrajności, chociaż jest to bardziej brutalna siła niż sprytne rozwiązanie. – TheLethalCoder

+0

Doceniam twój czas. Historycznie dawałem nagrody dla tych, którzy pracowali nad problemami, które opublikowałem. Mam zamiar zrobić to samo tutaj. –