Freitag, 10. Juni 2016

Barcodeerkennung mit OpenCV und ZXing

Im Jahr 2015 habe ich ein Projekt zur automatisierten Übertragung von Dokumenten aus einem Warenwirtschaftssystem in ein DMS realisiert.
Dabei mussten auch unterschriebene Papierdokumente als Stapelscan automatisiert verarbeitet werden. Auf die Dokumente wurde dazu ein Barcode aufgedruckt mit dessen Hilfe eine Trennung des Stapels sowie die automatische Vorgangszuordung im DMS erfolgen konnte.

Die Barcodeerkennung habe ich mit ZXing (Zebra Crossing) realisiert. Nach Justierung der Scaneinstellungen funktioniert das sehr gut, da die Barcodes an festen Positionen auf den Dokumenten aufgedruckt sind.

Im Jahr 2016 ging es um eine ähnlich Sache. Einziger Unterschied: Es werden Papierdokumente gescannt, die per Briefpost eingehen. Auf jede erste Seite wird ein Barcodeaufkleber aus einem Etikettendrucker geklebt.
Im Anschluss werden die Dokumente im Stapel gescannt. Bei der Stapelverarbeitung muss zunächst eine Trennung anhand der Barcodes erfolgen. Danach wird über die im Barcode kodierte Nummer eine automatisierte Verarbeitung angestossen.

Leider haben erste Versuche mit der ZXing-Lösung eine sehr schlechte Erkennung geliefert. Da die Codes an freie Stellen auf den Dokumenten geklebt werden, steht die Position nicht fest. Der Barcode kann sich theoretisch überall befinden.
Nach einigen Versuchen stellte sich schnell heraus, dass ZXing frei positionierte Barcodes nur sehr mäßig findet.

Ich habe dann recherchiert, ob OpenCV eine Möglichkeit zum Auffinden der Barcodes bietet. Ich bin auf folgenden Artikel gestossen:

http://www.pyimagesearch.com/2014/11/24/detecting-barcodes-images-python-opencv

Für mein Projekt musste ich diese Vorgehensweise etwas verändern aber grundsätzlich verwende ich dieselbe Technik.

Ausgangsbasis


Ausgangsbasis sind mehrseitige PDF-Dateien mit 300dpi schwarz/weiss Scans. Das folgende Bild zeigt ein Beispiel.

Gescannte Seite mit Barcodes

Der obere Barcode (316000003) wurde aufgeklebt. Der andere ist fest aufgedruckt. Im aktuellen Projekt ist nur der Aufkleber relevant.

PDF Verarbeitung


Der Scan liegt als mehrseitiges PDF vor. Die PDF-Dateien verarbeite ich mit Apache PDFBox zu je einem BufferedImage pro Seite.

// Create PDFRenderer
PDFRenderer renderer = new PDFRenderer(pdDocument);  
  
// Scale = resolution / 72DPI
BufferedImage bi = renderer.renderImage(pageNum,
   resolution / (float) 72, ImageType.GRAY);


Zur Weiterverarbeitung mit OpenCV muss das BufferedImage in eine OpenCV Mat überführt
werden. Da beim PDF-Rendering der Type ImageType.GRAY übergeben wurde, kann das BufferedImage einfach in eine CvType.CV_8UC1 Mat überführt werden:

mat = new Mat(bi.getHeight(), bi.getWidth(), CvType.CV_8UC1);
mat.put(0, 0, ((DataBufferByte) bi.getRaster().getDataBuffer()).getData());


Finden möglicher Barcodes in einem Dokument (OpenCV)


Im ersten Schritt sollen mit OpenCV Teilbereiche des Dokuments erkannt werden, in denen ein Barcode vermutet wird. Nach vielen Versuchen erwies es sich als vorteilhaft, das Ausgangsdokument zunächst mit einem Weichzeichner zu bearbeiten.

Nach weiteren Tests hat sich der Weichzeichner als kontraproduktiv erwiesen. Im Endeffekt werden dadurch die scharfen Ränder, die zur Erkennung des Barcodes wichtig sind, verwischt.


Im nächsten Schritt wird wie im o.g. Link der Scharr-Filter angewendet. Dieser arbeitet mit vertikalen bzw. horizontalen Gradienten. Barcode Bereiche haben hohe vertikale Gradienten (senkrechte Striche).


// Scharr
Mat gradX = new Mat();
Mat gradY = new Mat();

// horizontal    
Imgproc.Sobel(blurMat, gradX, -1, 1, 0, -1, 1, 0);
// vertical
Imgproc.Sobel(blurMat, gradY, -1, 0, 1, -1, 1, 0);

// Subtract
Core.subtract(gradX, gradY, mat);

Core.convertScaleAbs(mat, mat2);

Durch Subtraktion werden waagerechte Linien schwächer. Die letzte Code-Zeile habe ich von der Website übernommen. Was diese genau bewirkt weiss ich noch nicht ;)
Die folgende Abbildung zeigt das Zwischenergebnis in mat2. Man erkennt, dass vertikale Linien betont sind und horizontale Linien geschwächt bzw. eliminiert sind.

Verarbeitung mit Scharr Algorithmus

Wie auf der Website beschrieben sollen nun Rechtecke geschlossen werden. Angepasst auf die 300dpi und die vorkommenden Barcodes haben sich x=30 und y=5 bewährt.
Bei dem anderen Barcode stehen die Balken teilweise recht weit auseinander. Ein kleineres X führt dazu, dass 2 getrennte Bereiche erkannt werden.

// Close rectangles
Mat kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(30,5));
Imgproc.morphologyEx(mat2, mat, Imgproc.MORPH_CLOSE, kernel);  

Das folgende Bild zeigt das Zwischenergebnis. Es sind viele geschlossene Rechtecke entstanden. Die beiden Barcodes fallen jetzt schon sehr deutlich auf so dass also auch der Rechner sie nun bald sehen wird ;)

Rechtecke wurden geschlossen

Wie auf der Website beschrieben kommen nun 2 Techniken zum Einsatz, um alles ausser den Barcodes zu eliminieren.

Imgproc.erode(mat, mat2, Imgproc.getStructuringElement(
    Imgproc.MORPH_RECT, new Size(50,20)), new Point(-1, -1), 4);

Zunächst werden durch 4-mailige Erosion (Iteration=4) weisse Bereiche entfernt bzw. verkleinert. Das Zwischenergebnis enthält ein fast komplett schwarzes Bild. Die verwendete Size (x,y) muss so angepasst werden, dass möglichst keine falschen Bereiche übrig bleiben aber die Barcodes nicht vernichtet werden.
Im Endeffekt muss man hier eher defensiv vorgehen. Es dürfen keine Barcodes verloren gehen. Ein irrtümlich erkannter Barcode-Bereich liefert zum Abschluss einfach keinen gültigen Barcode und wird verworfen.

Das folgende Bild zeigt das Erode Ergebnis.

Ergebnis nach Erosion
Da die Barcodebereiche stark verkleinert wurden müssen diese im nächsten Schritt wieder erweitert werden.
Die geschieht mit der Funktion dilate (Pendant zu erode). Die Aufrufparameter müssen an die Erosions-Parameter angepasst werden.


Imgproc.dilate(mat2, mat, Imgproc.getStructuringElement(
    Imgproc.MORPH_RECT, new Size(50,20)), new Point(-1, -1), 4);


Im Ergebnis sind die Bereiche der Barcodes weiss und der Rest schwarz.

Erweitertes Bild

Nun wird eine Konturensuche durchgeführt. Die Grenzlinien zwischen schwarz und weiss liefern je eine Kontur.
Die gefundenen Konturen werden durchlaufen und das einschließende Rechteck (BoundingBox) gebildet. Dieses Rechteck wird links und rechts um 10% erweitert.
Mit Hilfe des Rechtecks wird der Teilbereich aus dem Originalbild ausgeschnitten und wieder in ein BufferedImage überführt.
Das BufferedImage kann dann mit ZXing weiter verarbeitet werden. Dazu ggf. später noch etwas wobei dies eine einfache Sache ist und online gut erklärt wird.

// Ergebnisliste für Konturensuche
List contours = new ArrayList<>();
    
Imgproc.findContours(mat, contours, new Mat(),
    Imgproc.RETR_EXTERNAL, Imgproc.CHAIN_APPROX_SIMPLE);
    
for (Iterator j = contours.iterator(); j.hasNext(); ) {
  MatOfPoint contour = j.next();
   
  // Bounding Box der Kontur
  Rect boundingBox = Imgproc.boundingRect(contour);
  
  int increase = (int) (boundingBox.width * 0.1);
   
  boundingBox.x -= increase;    
  if (boundingBox.x < 0) boundingBox.x = 0;
       
  boundingBox.width += 2* increase;
   
  if (boundingBox.x + boundingBox.width > blurMat.width()) {
     boundingBox.width = blurMat.width() - boundingBox.x;
  }

  // Extract area from blurred original image       
  Mat codeMat = blurMat.submat(boundingBox);

  // Create BufferedImage   
  BufferedImage bi = new BufferedImage(codeMat.width(), codeMat.height(),
     BufferedImage.TYPE_BYTE_GRAY);
   
  byte[] data = ((DataBufferByte) codeImage.getRaster().
        getDataBuffer()).getData();
  codeMat.get(0, 0, data);
   
  // Add BufferedImage to result or whatever ...   
  result.add(codeImage);
}


Mit den bisher getesteten Beispieldokumenten funktioniert die Erkennung zu 100%.

Auslieferung von OpenCV


Mein Entwicklungssystem ist ein Ubuntu 14.04 mit Eclipse. OpenCV habe ich über die Paketverwaltung installiert und in Eclipse als User Library eingerichtet. Mehr dazu hier:

http://pinnau.blogspot.de/2015/06/zahlen-in-explosionszeichnungen.html

Die fertige Barcode Routine soll als möglichst schlankes Programm beim Kunden unter Windows laufen.
Nach dem Download von OpenCV für Windows habe ich zunächst einen Schreck bekommen. Das entpackte Paket ist 2,7 GB groß.
Man benötigt für ein Java-Programm (32-bit) aber nur ganze 2 Dateien aus der OpenCV Installation:

  1. Java Library: build/java/opencv-VERSION.jar
  2. JNI Library: build/java/x86/opencv_javaVERSION.dll

Die Java Library muss logischerweise im Classpath liegen. Der Pfad zum Verzeichnis der JNI-Library muss der JVM über -Djava.library.path mitgeteilt werden. Relative Pfade werden dabei unterstützt. Ich habe die DLL einfach in ein Unterverzeichnis native gelegt:

java -Djava.library.path=native -cp lib/opencv-2413.jar biz.pinnau.barcode.MainClass ...



Fazit


Fürs erste bin ich zufrieden. Die Sache läuft. Ich hätte nicht gedacht dass man zum Auffinden von frei positionierten Barcodes einen solchen Zirkus machen muss.
Wenn ich dann an autonom fahrende Autos in der Stadt denke wird mit Angst und Bange ;)

Sobald das Projekt an den Start geht werde ich sehen wo es noch klemmt.


Keine Kommentare:

Kommentar veröffentlichen