Samstag, 19. Dezember 2015

Duplicate rows in SWT TableViewer

When working with SWT/JFace TableViewers problems occured with identical rows.

The following picture shows the TableViewer (left one). The input is a List<String>. The input contains 2 equal strings "Trenner" (marked red).


I implemented a delete button (red array, center). My first implementation used the viewer.getSelection() method to obtain the current selection. I iterated over the selected strings and removed them from the list and from the viewer.

List input = ...

IStructuredSelection sel = (IStructuredSelection) viewer.getSelection();

for (Iterator i=sel.iterator(); i.hasNext(); ) {
   String action = (String) i.next();
   input.remove(action);
   viewer.remove(action);
}

The code worked but failed when removing the second "Trenner" in the above scenario. The first "Trenner" was removed and the second remained in the TableViewer.

The reason is obvious: java.util.List.remove(Object o) and TableViewer.remove(Object o) both remove the first occurence of the specified object. So the first "Trenner" is removed.

As far as I see there is no way to do it with JFace. So I used SWT fallback:

List input = ...

int[] selection = viewer.getTable().getSelectionIndices();
    
if (selection.length > 0) {     
  for (int index: selection) {
     input.remove(index);
  }            

  // Refresh the viewer
  viewer.refresh();
}

The solution is to work with the getSelectionIndecies() of the SWT-Table. Iterate over the idicies and remove the objects from the java.util.List input.
After that call TableViewer.refresh() and you are done.


Freitag, 18. Dezember 2015

SWT Table or JFace TableViewer height problem with preloaded data

When implementing a new function to a SWT/JFace bases dialog I encountered a problem when data is loaded into the table before it is visible.

The TableViewer is placed in a GridLayout and should grab available vertical und horizontal space. Therefor I applied th follwing GridData:

viewer.getControl().setLayoutData(
    new GridData(SWT.FILL, SWT.FILL, true, true));

The layout was fine since I loaded no data via the setInput() method of the TableViewer. As soon as I loaded data into the TableViewer the Table height increased to display all rows without scrollbars. The scrollbars then appeared in DetailsPart where the TableViewer was placed in.

The following picture illustrates the problem:

Messy scrollbars

This is pretty aweful because the buttons also scroll up and the form is unusable.

I tried several code-changes to bring the scrollbars to the TableViewer. When data is loaded after the GUI showed up everything was fine. Unfortunately I could not manage to get it working in the JFace master/details pattern.

The I remembered the GridDataFactory and modified the setLayoutData call for the TableViewer:

viewer.getControl().setLayoutData(GridDataFactory.defaultsFor(
    viewer.getControl()).grab(true, true).align(SWT.FILL, SWT.FILL)
   .create());

And that did the trick!

The defaultsFor() does something more so the scrollbars always appear at the TableViewer even if a large amount of data is loaded before the GUI shows up.

Scrollbars display correctly at the TableViewer





Mittwoch, 9. Dezember 2015

Erstellen einer technischen Dokumentation mit DocBook

In einem älteren Blog habe ich bereits die Ergebnisse meiner Versuche mit Mediawiki und der PDF-Buchfunktion gepostet. Ziel der Übung war, eine Lösung zur Erstellung vielseitig nutzbarer technischer Dokumentationen zu finden.

Im wesentlichen brauche ich folgendes:

  1. Web-Version der Dokumentation
  2. PDF-Version der Dokumentation (auch zum Ausdrucken)
  3. Zentrale Pflege der Inhalte. Im Idealfall also eine vollständige Trennung des Inhalts von der Präsentation
  4. Unterstützung für Inhaltsverzeichnis, Bilder, Tabellen usw.

Vieles davon ließ sich mit MediaWiki und dem PDF BookGenerator lösen. Allerdings bin ich nun darauf gestoßen, dass Tabellen nicht unterstützt werden. Diese fehlen im PDF einfach.
Darüberhinaus ist das Einrichten der benötigten Dienste (Offline Content Generator und Parsiod) aufwändig und alles andere als selbsterklärend.

Ich habe also weiter gesucht und bin auf DocBook (XML) gestossen. Damit hatte ich vor ein paar Jahren schon einmal experimentiert, jedoch gab es auch dabei Schwierigkeiten die letztendlich zum Abbruch meiner Versuche führten.
Ich habe jetzt einen neuen Versuch gestartet.

Wie funktioniert DocBook - ganz kurz


Die Inhalte der Dokumentation werden in einem oder mehreren XML-Files verwaltet. Das Docbook-XML bietet viele Sprachelemente zur Erstellung von Kapiteln, Sektionen, Tabellen, Einbinden von Medien usw.
Das XML enthält jedoch nur inhaltliche Aspekte und keinerlei Formatierung. Wenn man sich mit den Tags beschäftigt merkt man sofort, dass dort schon eine Menge Gehirnschmalz drinsteckt ;)

Um das XML in irgendetwas direkt nutzbares zu überführen gibt es eine Reihe von XSLT-Stylesheets. Ich nutze im Moment die Stylesheets zur Erzeugung einer HTML-Version bzw. XSLFO. Aus dem XSLFO kann im nächsten Schritt ein PDF erzeugt werden.


Einfaches Beispiel


Das folgende Listing zeigt ein Buch (book) mit 2 Kapiteln, einem Bild und einer Tabelle.

 <?xml version="1.0" encoding="utf-8"?>  
 <!DOCTYPE book PUBLIC "-//OASIS//DTD DocBook V5.0//EN" "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">  
 <book lang="de">  
   <title>System Concept DMS</title>  
   <subtitle>Benutzerhandbuch</subtitle>  
     <bookinfo>  
         <title>System Concept DMS</title>  
         <subtitle>Benutzerhandbuch</subtitle>  
         <author>              
             <firstname>Peter</firstname>  
             <surname>Pinnau</surname>  
             <email>peter@pinnau.biz</email>  
         </author>  
         <date>2015-12-09</date>  
         <releaseinfo>Status: In Arbeit</releaseinfo>  
         <pubdate>2015-12-09</pubdate>  
     </bookinfo>  
   <chapter>  
       <title>Einleitung</title>  
 <para>Alle reden davon, nur wenige haben es: <emphasis role="bold">Das papierlose Büro</emphasis> - oder zumindest eine umfassende Digitalisierung und  
 systematische Ablage von Dokumenten. Unzählige Document Management Systeme (DMS) verschiedener Hersteller versprechen dazu die richtige Software zu liefern. Einige halten was sie versprechen.</para>  
 </chapter>  
 <chapter>  
     <title>Einführung Programmoberfläche</title>  
     <para>Hier fehlt der Text</para>  
     <table frame="all">  
         <caption>Sample HTML Table</caption>  
             <thead>  
                 <tr>  
                     <th>Head 1</th>  
                     <th>Head 2</th>  
                 </tr>  
             </thead>  
             <tbody>  
             <tr>  
                 <td>Body 1</td>  
                 <td>Body 2</td>  
             </tr>  
         </tbody>  
     </table>    
     <mediaobject>  
         <imageobject condition="web">  
             <imagedata fileref="../../img/web/db5d_ref09.png" format="PNG" scale="70"/>  
         </imageobject>
         <textobject>  
             <phrase>The Eiffel Tower</phrase>  
         </textobject>  
         <caption>  
             <para>Designed by Gustave Eiffel in 1889, The Eiffel Tower is one  
               of the most widely recognized buildings in the world.  
             </para>  
         </caption>  
     </mediaobject>          
 </chapter>  
 </book>  


Erzeugung HTML Version


Um aus dem zuvor gelisteten XML eine HTML Version zu generieren benötigt man zunächst die Docbook XSLT-Stylesheets. Diese gibt es hier:

http://sourceforge.net/projects/docbook/files/docbook-xsl

Ich habe mit der am 9.12.2015 aktuellen Version 1.79.0 gearbeitet. Mit Hilfe der Stylesheets wird das XML in HTML überführt. Hierzu benötigt man einen XSLT-Prozessor. Unter Ubuntu habe ich dazu den über die Paketverwaltung installierbaren saxon-xslt (Paket libsaxon-java) verwendet und das hat auf Anhieb funktioniert:

peter@gorgonzola:~ saxon-xslt xml/test.xml tools/docbook-xsl-1.79.0/html/chunk.xsl

Der erste Parameter ist die Input-Datei, der zweite das entsprechende Stylesheet. Im Unterordner html sind mehrere Stylesheets vorhanden. chunk.xsl erzeugt eine HTML-Datei pro Kapitel und eine index.html mit dem Inhaltsverzeichnis. Saxon legt die html-Files im aktuellen Verzeichnis ab.

Im Moment fehlen bei der Tabelle im HTML noch die Borders. Das ist jedoch sicher einfach lösbar.

Erzeugung PDF Version

Auch hierfür werden die Docbook XSLT-Stylesheets benötigt. Mit saxon wird das XML zunächst in XSLFO und dann mittels Apache FOP in ein PDF überführt. FOP kann unter Ubuntu ebenfalls einfach über die Paketverwaltung installiert werden (Paket fop).

XML nach XSLFO mittels saxon
peter@gorgonzola:~ saxon-xslt -o docu.fo xml/test.xml tools/docbook-xsl-1.79.0/fo/docbook.xsl

ACHTUNG: saxon-xslt braucht hier den -o Parameter, da das XSLFO anderenfalls auf STDOUT ausgegeben wird.

XSLFO nach PDF mittels fop
peter@gorgonzola:~ fop output/pdf/docu.fo output/pdf/docu.pdf


NLS - National language support


Da ich dem <book> das Attribut lang="de" mitgegeben habe, verwenden die Stylsheets automatisch deutsche Textkonstanten (z.B. Kapitel, Inhaltsverzeichnis, Anfang usw.).
Ohne das lang-Attribute werden die englischen Begriffe verwendet.


Fazit


Mit Docbook habe ich nun endlich eine solide Basis zur Erstellung von Dokumentationen gefunden. In allen wesentlichen Punkten waren die Tests erfolgreich. Detailprobleme lassen sich mit Sicherheit lösen.


Links


DocBook - creating a docbook:
http://www.docbook.org/tdg5/en/html/ch02.html

Docbook-XSLTStylesheets Download:
http://sourceforge.net/projects/docbook/files/docbook-xsl/




Freitag, 6. November 2015

Factory Reset Toshiba Satellite Werkseinstellungen

Vor einiger Zeit war es wieder mal so weit: ich habe aus der Verwandtschaft ein Windows Notebook übergeben bekommen. Nähere Problembeschreibung: "Geht nicht mehr."

Ich habe die Aktion lange aufgeschoben. Insgeheim habe ich gehofft, dass ein Hardwaredefekt vorliegt und das Ding gleich in die Tonne kann ;)  Morgen hat meine Tochter Geburtstag und da kommt die Verwandtschaft und möchte das Gerät wieder mitnehmen. Ich sollte gleich noch Windows 10 installieren weil das jetzt ja für ein Jahr kostenlos ist ;)

Nach dem Einschalten startete ein Windows 7 Home Premium. Nach dem Login poppten diverse Fenster mit allen möglichen Fehlermeldungen und teilweise auch komplett ohne Text auf.
Beim Klick auf den Start-Button oder den Explorer in der Taskleiste kam die Meldung, dass explorer.exe abgestürzt ist.
Sobald ich eine Fehlermeldung mit "OK" bestätigt habe erscheint sie erneut.

Die Diagnose ist damit einfach: Das System ist Schrott. Da ist mit Aufräumen nichts mehr zu machen.

Auf dem Desktop befinden sich Icons unzähliger Registry-Tuner, Free Games und was weiß ich noch alles.

Ich kann über diesen Quark inzwischen nur noch lachen, da ich ausschließlich Ubuntu nutze.

Ich boote erstmal von einer Xubuntu CD und schaue mal was auf dem Gerät an Daten vorhanden ist. Es sind nur ein paar Fotos. Der Rest ist Software Müll. Ich überlege kurz, einfach das Ubuntu zu installieren und die bisherige Windows Installation komplett zu ersetzen. Ich fürchte mich allerdings vor dem End-User-Support.

Überlegen, überlegen und ja! Das Ding hat eine Recovery Partition. Ich schaue im Internet wie man bei den Satellites einen Factory Reset machen kann. Das geht auch ohne eine CD:

Gerät ausschalten, die 0 (Null) Taste gedrückt halten und dann das Gerät einschalten. Dabei die 0 gedrückt halten. Man gelangt irgendwann in ein spezielles Menü. Dort weiter mit F8 zur Auswahl und dort kann man Computer reparieren wählen. Der Rest ist selbsterklärend.

Das Reset an sich geht fix. Danach startet ein frisches Windows und es werden diverse Treiber und Toshiba Programme nachinstalliert.

Damit das System dann Spaß macht muss man vieles davon aufwändig wieder entfernen. Firefox, einen Virenscanner und Updates müssen noch installiert werden. Alles in allem ein abendfüllendes Programm. Xubuntu wäre definitv schneller gegangen.

Windows 10 lassen wir mal. Dann wäre auch End-User-Support erforderlich.


Donnerstag, 24. September 2015

Übertragung von Dateien per USB zwischen Android 5.1 und Ubuntu 14.04

Im August 2015 hat mein altes Smartphone leider den Geist aufgegeben und ich habe mir ein neues gekauft.
Es wurde ein Motorola MotoG 3 mit Android 5.1.

Es kam nun der Tag an dem ich Video gemacht habe und die 1,8 GB große Datei zum Schneiden auf mein Notebook mit Ubuntu 14.04 kopieren wollte.
Ganz einfach: USB Kabel anstecken und los. Aber Pustekuchen!

Im Thunar (also dem Filebrowser auf meinem Xubuntu) gab es zwar das Gerät MotoG und ich konnte auch bis zu den Bildern navigieren aber dann ging es nicht weiter.
Nicht einmal ein Foto ließ sich öffnen.

Ich hatte zuvor noch eine Android Version bei der ich den internen Speicher als USB Mass Storage mounten konnte. Das geht mit Android 5.1. nicht mehr.

Es gibt nur noch MTP oder das ältere PTP. Mein Ubuntu kommt damit aber erstmal nicht so ohne Weiteres klar.
Es wäre noch ein Transfer über WLAN denkbar aber dafür müsste wieder auf dem Telefon irgendwas installiert werden. Ich bin genervt.

Kurz vor Abbruch der Aktion finde ich diese Seite:
https://linuxundich.de/gnu-linux/gerate-mit-android-3-0-oder-4-0-via-mtp-in-ubuntu-linux-einbinden/


Ich installiere mtpfs nach und im Anschluß kann ich das Telefon manuell über die Konsole per MTP mounten und die Datei in akzetabler Zeit kopieren.

Also zusammengefasst:

# Install mtpfs
sudo apt-get install mtpfs mtptools

# Mountpoint erstellen
mkdir ~/android

# Telefon mounten (vorher per USB anschließen und auf dem Phone MTP einstellen)
sudo mtpfs -o allow_other ~/android

# Datei kopieren
cp ~/android/...mp4 ~/Videos

# Umount
sudo umount android/


Mittwoch, 9. September 2015

Das Ziegenproblem

Irgendwie hat ein Kollege heute im Büro das Ziegenproblem angesprochen von welchem ich noch nie gehört hatte.

Details siehe hier:
http://de.wikipedia.org/wiki/Ziegenproblem

Zusammengefasst stelle man sich die gute alte GameShow Geh aufs Ganze mit dem Zonk vor: Es gibt 3 geschlossene Tore - hinter einem steht ein Auto als Gewinn, hinter den beiden anderen der Zonk als Niete.

Der Spieler soll sich ein Tor aussuchen. Der Moderator bringt nun etwas Spannung ins Spiel indem er eines der beiden anderen Tore öffnen läßt. Dahinter kommt der Zonk (die Ziege) zum Vorschein.

Nun soll der Kandidat seine Wahl noch einmal überdenken. Soll er bei seiner Wahl bleiben oder das andere geschlossene Tor wählen? Bei welchem der verbleibenden 2 geschlossenen Tore ist die Gewinnwahrscheinlichkeit größer?

Ich bin auch nicht schlauer als die meisten und sage 50:50 - egal. Es sind noch 2 Tore und eine neue Entscheidung - das Auto kann hinter dem einen und genau so gut hinter dem anderen Tor sein.

Mein Kollege sagt Auswahl wechseln, denn für das zuerst gewählte Tor liegt die Gewinnwahrscheinlichkeit bei 33,3% und bei dem anderen bei 66,6%.

Er kritzelt folgendes auf (Spieler wählt Tor 1 und Moderator öffnet 2 oder 3):


Das zu Anfang für jedes Tor eine Wahrscheinlichkeit von 33,3% (also 1 Drittel) besteht ist klar. Dass nach Auflösen eines Tores dessen Wahrscheinlichkeit zu dem geschlossenen Tor addiert wird leuchtet mir nicht ein. Ich glaube das nicht. Ich glaube weiter an 50:50.

Mein Kollege zeigt mir den o.g. Wikipedia Artikel, die Tabelle mit den 9 Möglichkeiten usw. Ich lese das alles aber das macht die Sache nur komplizierter und nicht durchschaubar. Ich lese, das es viele - teils prominente - Zweifler an den 66,6% gibt und bin überzeugt, den Gegenbeweis zu finden.

Als Programmierer wähle ich den für mich einfachsten Weg: In 10 Minuten entsteht ein kleines Java-Programm mit dem ich Spiele laufen lasse und zähle wie oft der Kandidat gewinnt, wenn er:

a) bei seinem zu Anfang gewählten Tor bleibt
b) zum anderen Tor wechselt

Natürlich lasse ich noch die Gesamtverteilung der Gewinnertore zählen, um die gleichmäßige Verteilung zu prüfen. Bei 10 Durchläufen lässt diese noch stark zu wünschen übrig, bei 100 wird es besser und bei 10.000 Spielen liegen alle Tore bei 33%, d.h. jedes Tor ist annähernd gleich oft ein Gewinnertor gewesen. Damit ist das Ergebnis repräsentativ.

Ich lese die Auswertung der Kandidaten-Entscheidung und kann es kaum glauben:

...
Game:  Auto   Ziege  Ziege
Game:  Ziege  Auto   Ziege
Game:  Auto   Ziege  Ziege
---- STATS -----------------------
1: 33%   -  3336
2: 33%   -  3320
3: 33%   -  3344
---- WIN Stats -------------------
No switch: 3355
Switch   : 6645

Wenn der Kandidat nicht wechselt (No switch) gewinnt er von 10.000 Spielen 3.355 Mal das Auto, wenn er wechselt (Switch) gewinnt er 6.645 Mal.
Ungläubig lasse ich die Simulation noch einmal laufen mit nahezu gleichem Ergebnis. Die 66,6% stimmen also.
Wechseln ist statistisch gesehen von Vorteil. Ich verstehe es natürlich noch immer nicht aber weiss jetzt das ich falsch liege und bei mir ein Denkfehler vorliegt.
Die Simulation lügt nicht. Das Programm hat stur 10.000 Spiele durchgespielt.


Die Sache lässt mich nicht los. Ich will es verstehen! Ich grübel und grübel und komme schließlich zur Position des Moderators. Dieser kann nicht unbedingt frei entscheiden, welches der beiden verbleibenden Tor er öffnet:

  1. Wenn der Spieler als erstes das Tor wählt hinter dem das Auto steht kann der Moderator entscheiden welches der beiden verbleibenden Tore er öffnet (beides Ziegentore)
  2. Falls der Spieler jedoch ein Tor mit der Ziege gewählt hat, hat der Moderator keine Wahl: Das zu öffnende Tor steht fest, denn das Auto-Tor ist tabu.

Grundsätzlich ist die Position des Moderators nicht relevant aber bei mir hat das zum Verständnis beigetragen:

Die Gewinnwahrscheinlichkeit des zuerst gewählten Tores liegt bei 33,3% - klar.

Daraus ergibt sich für den Moderator:

  1. 33,3%: Moderator hat freie Auswahl zwischen den beiden Ziegen-Toren:
    Für den Spieler bedeutet das: Wechsel => Ziege, Nicht-Wechsel => Auto
  2. 66,6%: Moderator hat keine Wahl, da nur noch ein Ziegentor übrig ist
    Für den Spieler bedeutet das: Wechsel => Auto, Nicht-Wechsel => Ziege
Damit habe ich die Kurve gekriegt und kann es nachvollziehen.

Wechsel bedeutet zu 66,6% Gewinn aber eben auch zu 33,3% Ziege.
Nicht-Wechsel bedeutet nur zu 33,3% Gewinn und zu 66,6% Ziege.


Ich  habe vor Schreiben dieses Posts noch folgenden Artikel gelesen:
http://www.zeit.de/2004/48/N-Ziegenproblem

Dort steht, dass tatsächlich die Perspektive des Moderators vielen beim Verständnis der Sache hilft. Immerhin bin ich darauf allein gekommen - wenigstens etwas ;)


Und noch ein Nachtag: Der größte Teil meines Simulationsprogramms war für die Entscheidung (oder Zwangswahl) des Moderators nötig.
Als das Programm fertig war habe ich festgestellt, dass zum Bestimmen des Ergebnisses das durch den Moderator geöffnete Tor unerheblich ist.

Dieser Kommentar bei Heise bringt es einfach auf den Punkt:
http://www.heise.de/forum/heise-online/News-Kommentare/Wo-ist-das-Auto-25-Jahre-Ziegenproblem/Aah-einfacher-als-ich-zuerst-dachte/posting-23691706/show/

Richtig: Wenn der Spieler sich nicht beeinflussen lässt und bei seiner ursprünglichen Wahl bleibt, bleibt auch seine Chance auf das Auto bei 33,3%. Das Öffnen eines der anderen Tore ist für seine Gewinnchance unerheblich.
Die verbleibenden 66,6% entfallen auf das andere Tor. Es geht gar nicht anders.


Und noch ein Nachtrag

http://www.siegfriedraisin.de/index.php?option=com_content&view=article&id=16%3Aloesung-das-ziegenproblem&catid=14%3Aloesungen-zu-den-raetseln&Itemid=30&lang=de


Dehnen wir das Problem auf 100 Tore aus. 1 Auto, 99 Ziegen. Der Spieler wählt ein Tor. Die Gewinnchance liegt bei 1%. Zu 99% befindet sich das Auto aber in irgendeinem der anderen Tore.

Der Moderator öffnet beliebig viele der 99 nicht gewählten Tore. Maximal 98 so dass wir wieder beim Ursprungsproblem sind.
Es leuchtet jetzt ein: Zu 99% befindet sich das Auto hinter dem 99. Tor. Dieses durfte der Moderator die ganze Zeit nicht öffnen weil dahinter das Auto ist. 1% bleibt weiterhin beim ursprünglich gewählten Tor.


Wie auch immer: Wenn man die Kommentare bei Heise überfliegt sieht man, dass noch immer viele Leute an 50:50 glauben.
Meine Simulation hat mir gezeigt, dass ich falsch liege. Der Rest kommt, wenn man seine Gedanken öffnet.

Dienstag, 8. September 2015

Aufbau eines lokalen Wiki für technische Dokumentation

Nachtrag: Ich habe inzwischen DocBook XML als die wesentliche bessere Möglichkeit entdeckt (siehe entsprechender Blogeintrag).
Ich lasse den Artikel aber einfach mal so stehen.



Auf der Suche nach einer soliden Basis zur Erstellung technischer Dokumentationen für Kunden-Systeme habe ich mir MediaWiki genauer angeschaut. MediaWiki ist das System welches auch hinter Wikipedia steckt. Es steht unter der GNU2 Lizenz zur Verfügung.

MediaWiki bietet folgende Vorteile für die Dokumentation:

  1. Leistungsfähige Web-Schnittstelle zur Recherche in der Dokumentation. Da so gut wie alle Leute Wikipedia kennen fällt die Navigation leicht.
  2. Dokumentation kann in Artikel aufgeteilt werden und ist dennoch zentral verfügbar.
  3. MediaWiki Installation kann lokal beim Kunden eingerichtet werden, so dass die Dokumentation nur intern abgerufen werden kann
  4. Es können mehrere Nutzer an der Dokumentation arbeiten
  5. Versionierung wird unterstützt.
  6. Schlankes System dass auf einer kleinen VM installiert werden kann
  7. Einfaches Backup der dahinterstehenden MySQL Datenbank
  8. Buch-Funktion: Aus dem Wiki kann ein buch-ähnliches PDF mit Inhaltsverzeichnis, Seitenzahlen usw. erzeugt werden. Dazu später mehr.

Buchfunktion


Erfahrungsgemäß möchten Kunden eine Druckversion der Doku oder zumindest ein ordentlich aufbereitetes PDF haben. Dies macht Sinn, denn was nützt bei einem Ausfall der IT eine Dokumentation, die sich auf dem ausgefallen System befindet.
MediaWiki bietet die Möglichkeit, verschiedene Artikel zu einem Buch zusammen zu führen. Auf wikipedia.de kann man das einfach ausprobieren:

  1. www.wikipedia.de aufrufen und einen Artikel aufrufen (z.B. Münchehofe).
  2. Im Menübereich (links) im Bereich Drucken/exportieren auf Buch erstellen klicken.
  3. Auf der nächsten Seite Buchfunktion starten klicken
  4. Man gelangt wieder zum Artikel und kann am Ende der Seite im Bereich Buchgenerator die Seite zum Buch hinzufügen
  5. Man kann auf diese Weise beliebig viele Artikel zum Buch hinzufügen
  6. Sobald alle Artikel hinzugefügt sind klickt man im Bereich Buchgenerator auf Buch zeigen.
  7. Dort kann man Titel und Untertitel festlegen, zwischen 1- oder 2-spaltigem Layout wählen und die Reihenfolge der Artikel ändern (mit der Maus verschieben).
  8. Im Bereich Herunterladen kann die Erstellung einer PDF-Version anstoßen. Diese steht kurze Zeit später zum Download bereit.
Einfach mal probieren. Das PDF sieht wirklich gut aus. Inhaltsverzeichnis, Teilüberschriften, Bilder, Quellen - alles ist drin. Das kann man schon abgeben.

Wer heute also eine Plagiatsarbeit abgeben möchte, klickt die Artikel einfach bei Wikipedia zusammen und macht gleich ein Buch daraus - fertig ;)

Lokale Installation


Eine lokale Mediawiki-Installation ist in einer Linux-Apache-MySQL-PHP Umgebung in wenigen Minuten startklar. Darauf gehe ich nicht weiter ein. Mehr hier:


Man wird aber schnell feststellen, dass der Buchgenerator fehlt. Diesen lokal zum Laufen zu bekommen empfand ich als relativ schwierig.

Ich habe 2 Möglichkeiten zur Erstellung von Büchern aus einem Wiki gefunden:

  1. Über Collection extension, Parsoid-Service und den Offline Content Generator (OCG): Diese Variante wird von wikipedia.org und wikipedia.de eingesetzt und scheint der in Zukunft favorisierte Weg zu sein. Kapitel und Teilüberschriften werden durchnummeriert und man kann zwischen 1- oder 2-spaltigem Seitenlayout wählen.
  2. Über Collection extension und mwlib von PediaPress: Kapitel und Teilüberschriften werden nicht durchnummeriert und es wird nur das 1-spaltige Layout unterstützt.

Installation Collection extenstion


Die Collection extension wird für beide Varianten benötigt. Diese ermöglicht das Sammeln der Artikel und erweitert die Wiki-Oberfläche um die Buchfunktion.

Installation der Collection extension:
https://www.mediawiki.org/wiki/Extension:Collection#Installation

Das Einbinden der extension geschieht in der LocalSettings.php im mediawiki Hauptverzeichnis:

require_once "$IP/extensions/Collection/Collection.php";


Je nach genutzter Variante (Parsiod+OCG oder mwlib) müssen noch weitere Anpassungen in der LocalSettings.php erfolgen (mehr nachfolgend).


Variante 1: Parsoid-Service und OCG


Zusätzlich zur Collection extension müssen folgende Dinge installiert werden:
  1. Offline Content Generator (OCG) bundler: Der bundler packt die gesammelten Artikel inklusive der Bilder usw. in ein ZIP-Archiv.
    Die Inhalte werden dabei als JSON bzw. SQLLite Datenbankdateien abgelegt.
  2. OCG-latexer: Der ocg-latexer erstellt aus dem vom ocg-bundler gelieferten Archiv mit Hilfe von Latex ein PDF Dokument
  3. OCG-service: Der ocg-service stellt einen Server bereit, der ocg-bundler und ocg-latexer steuert. Die Collection extension stellt eine Verbindung mit dem ocg-service her, um die PDF-Erzeugung anzufordern.
  4. Parsoid Service: Der ocg-bundler benötigt entweder eine Parsoid-Service oder ein RESTFUL-API, um die Daten aus der MediaWiki Installation abzufragen. Ich habe den Parsoid Service verwendet. Dieser stellt ein API für den Zugriff auf das Wiki zur Verfügung.
Für eine einfache lokale Installation ist das ganz schön aufwändig. Die ganze Sache zum Arbeiten zu überreden ist ebenfalls nicht ganz einfach.

Die Installation der OCG-Komponenten ist hier beschrieben:

http://wikitech.wikimedia.org/wiki/OCG

Konfigurieren und Starten des OCG-Service


Das Wiki stellt eine Verbindung mit dem OCG-Service her, um die gewählten Artikel in ein PDF zu bringen. Damit das funktioniert, muss der OCG-Service gestartet werden.
Der OCG-Service nutzt wiederum den Parsoid Service. Standardmäßig wird der online verfügbare Wikipedia Parsoid-Service genutzt, der aber natrürlich keinen Zugriff auf ein lokal installiertes Wiki hat.
Daher muss vor Starten des OCG-Service im Verzeichnis mw-ocg-service ein Konfigurationsdatei localsettings.php mit folgendem Inhalt erstellt werden:

module.exports = function(config) {
  // host + port where PARSOID is running
  config.backend.bundler.parsoid_api = "http://localhost:8000";
  // the prefix here should match $wgDBname in your LocalSettings.php
  config.backend.bundler.parsoid_prefix = "localhost";
  // Use the Parsoid "v3" API
  config.backend.bundler.additionalArgs = [ '--api-version=parsoid3' ];
}

Beim Start des OCG-Service wird mit der Option -c die Konfigurationsdatei angegeben:

peter@gorgonzola:~$ ./mw-ocg-service.js -c localsettings.js

Der OCG-Service sollte danach ohne Fehlermeldungen hochfahren.


Konfigurieren und Starten des Parsoid Service


Ich habe Parsoid aud dem GIT Repository geholt und eingerichtet:
https://www.mediawiki.org/wiki/Parsoid/Developer_Setup

Die Installation des Parsoid Service aus einem Ubuntu Repo wird hier beschrieben:
https://www.mediawiki.org/wiki/Parsoid/Setup


Wie im Developer Setup beschrieben habe ich die Datei api/localsettings.js aus der Vorlagedatei erstellt und ungepasst.

Der Start des Parsoid Service erfolgt über:

peter@gorgonzola:~$ node api/server.js

Der Dienst muss ohne Fehlermeldungen hochfahren.

Anpassen der LocalSettings.php

Im  mediawiki Hauptverzeichnis müssen noch folgende Zeilen in der LocalSettings.php am Ende ergänzt werden:

$wgCollectionFormatToServeURL['rdf2latex'] =
$wgCollectionFormatToServeURL['rdf2text'] = 'http://localhost:17080';

// MediaWiki namespace is not a good default
$wgCommunityCollectionNamespace = NS_PROJECT;

// Sidebar cache doesn't play nice with this
$wgEnableSidebarCache = false;

$wgCollectionFormats = array(
    'rdf2latex' => 'PDF',
        'rdf2text' => 'Plain text',
);
 
$wgLicenseURL = "http://creativecommons.org/licenses/by-sa/3.0/";
$wgCollectionPortletFormats = array( 'rdf2latex', 'rdf2text' );
Damit wird der OCG-Service in die Wiki-Oberfläche eingebunden (localhost:17080).

Probleme mit Bildern


Ich hatte zunächst Probleme mit den eingebundenen Bildern. Diese werden im Wiki-Artikel in der Regel so eingebunden:

[[Datei:Bild.jpg|mini|Bildtitel]]

Entscheidend ist die Größenangabe mini. Ohne diese wird das Bild 1:1 mit 72dpi in das PDF aufgenommen und ragt entweder aus dem Dokument heraus (bei höher aufgelösten Bildern) oder ist unscharf (bei kleinen Bildern).

Mit der Größenangabe mini hatte ich zunächst das Problem, dass die Bilder gar nicht mehr im PDF auftauchten.
Nach intensiver Suche habe ich herausgefunden, dass der OCG die Bilder mit der Größe mini in einer Breite (Auflösung) von 1200px anfordert. Wenn die Bilder nicht in dieser Auflösung vorliegen, bekommt der OCG anstelle des Bildes einen HTML 404 und diese Textdatei kann im weiteren Verlauf der Verarbeitung nicht in das PDF eingebunden werden.

Fürs erste lade ich also alle Bilder mit width=1200px hoch.


Variante 2: PediePress mwlib


Habe ich mir nicht mehr angeschaut.


Fazit

Die Buchfunktion ist eine schöne Sache. Allerdings ist das Aufsetzen kompliziert. Einige Dinge (wie z.B. Tabellen) werden nicht dargestellt.
Ausserdem bleibt das Problem, dass ein Wiki kein Inhaltsverzeichnis besitzt. Es handelt sich um eigenständige Artikel, die zur Erstellung eines Buches immer in einer bestimmten Reihenfolge zusammengestellt werden müssen.

Ich habe weiter gesucht und inzwischen DocBook XML als die für meine Zwecke bessere Variante entdeckt (siehe entsprechender Blog Artikel).

Freitag, 7. August 2015

JFace TableViewer with java.util.Map input

I am implementing a SWT dialog to edit some configuration settings. The configuration has a java.util.Map<String, String> which contains additional parameters.

The parameters Map should be presented/edited by a JFace TableViewer:

  • Column 1: Parameter name => map key
  • Column 2: Parameter value => corresponding value
Editing can then be down "in table" by implementing EditingSupport or via a small popup dialog.

Screenshot: settings dialog with java.util.Map based JFace TableViewer
Unfortunately org.eclipse.jface.viewers.ArrayContentProvider cannot deal with java.util.Map. The table stayed empty.

I did a quick search for another ContentProvider that supports java.util.Map - but could not find anything.

Here comes my own implementation:

 /**  
  * IStructuredContentProvider for java.util.Map  
  *   
  * @author Peter Pinnau  
  *  
  */  
 class MapContentProvider implements IStructuredContentProvider {  
   
      @Override  
      public void dispose() { }  
   
      @Override  
      public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { }  
   
      @SuppressWarnings("rawtypes")  
      @Override  
      public Object[] getElements(Object inputElement) {  
           if (inputElement == null) return null;  
             
           if (inputElement instanceof Map) {  
                return ((Map) inputElement).entrySet().toArray();  
           }  
        
           throw new RuntimeException("Invalid input für MapContentProvider: " + inputElement.getClass());  
      }  
 }  

The ContentProvider simply returns java.util.Map.entrySet().toArray().

The implementation for the TableViewer lable providers is straight forward:

 viewerParameter = new TableViewer(composite, SWT.FULL_SELECTION);                 
 viewerParameter.getTable().setHeaderVisible(true);  
   
 // Set the MapContentProvider  
 viewerParameter.setContentProvider( new MapContentProvider() );  
             
 // Column for map-key  
 TableViewerColumn column = new TableViewerColumn(viewerParameter, SWT.NONE);  
 column.getColumn().setText("Parameter");  
 column.getColumn().setWidth(150);  
 column.setLabelProvider( new CellLabelProvider() {                 
      @Override  
      public void update(ViewerCell cell) {       
           @SuppressWarnings("rawtypes")  
           Object value = ((Entry) cell.getElement()).getKey();  
             
           if (value != null)                      
                cell.setText(value.toString());  
           else  
                cell.setText("");  
      }  
 });  
   
 // Column for map-value  
 column = new TableViewerColumn(viewerParameter, SWT.NONE);  
 column.getColumn().setText("Wert");  
 column.getColumn().setWidth(250);  
 column.setLabelProvider( new CellLabelProvider() {                 
      @Override  
      public void update(ViewerCell cell) {       
           @SuppressWarnings("rawtypes")  
           Object value = ((Entry) cell.getElement()).getValue();  
             
           if (value != null)                      
                cell.setText(value.toString());  
           else  
                cell.setText("");  
      }  
 });  
   
 // Pass a java.util.Map as input  
 viewerParameter.setInput( mapObject );  
   


Donnerstag, 30. Juli 2015

DELL Venue 11 geht nach Einschalten ständig in Test Modus

Heute habe ich einen DELL Venue 11 (Tablet PC) auf den Tisch bekommen. Beim Starten außerhalb der Docking Station ging das Gerät immer ins BIOS und ließ dort irgendwelche Grafik- und Speichertests laufen. Nach Abbruch der Tests startete das Tablet soft neu und man kam ins Windows.

Schwieriges Ding ...

Ich habe die Tests einfach mal zu Ende laufen lassen in der Hoffnung dass die Kiste danach Ruhe gibt => leider Fehlanzeige. Nach Ausschalten und Reboot wieder das gleiche.

Also weitersuchen. Es stellte sich heraus, dass nicht das richtige BIOS sondern ein spezieller DELL Testmodus gestartet wird. Also mal einen Blick ins BIOS werfen. Bei dem Tablet PC muss man dazu erst die Ein-Taste drücken und loslassen und danach die Lautstärke "-" Taste gedrückt halten.

Aber was ist das? Zum Tablet gehört so ein stylischer Leder Umschlag. Das Gerät steckte falsch herum in diesem Etui. Prinzipiell ist das Gerät auch so nutzbar außer dass der Umschlag dann ständig gegen die Lautstärke "-" drückt.
Und was passiert wenn man zu Beginn Einschalten und Lautstärke "-" gleichzeitig drückt => genau man ruft den DELL Systemtest Modus auf.

Montag, 29. Juni 2015

Probleme bei der Bearbeitung von Verschlagwortungsmasken in ELO Enterprise

Die Lösung eines einfachen Problems hat jetzt so viel Zeit gekostet, dass eine Sofort-Dokumentation erforderlich ist.

Was habe ich gemacht?


Ganz einfach: Ich habe eine neue Verschlagwortungsmaske über den ELO Windows Client angelegt. Das hat grundsätzlich alles funktioniert und ich konnte die Maske auch verwenden.

Wo entstand das Problem?


In der betroffenen ELO Installation exisitieren eine Reihe von Skripten und Zusatztools, die diverse Aufgaben erledigen.
Aus zunächst völlig unerklärlichen Gründen gab es bei Dokumenten mit der neuen Maske Schwierigkeiten. Diese äußerten sich immer, wenn mit dem ELO IndexServer API (eloix) auf die Verschlagwortungsfelder zugegriffen werden sollte:


  1. Auf die mit Daten belegten Felder konnte auch über eloIX zugegriffen werden
  2. Die leeren Felder waren allerdings nicht im ObjKeys-Array enthalten, so dass die Elementanzahl in diesem Array je nach Anzahl der belegten Felder schwankte.
    Da einige Zusatztools über die Feldnummer auf bestimmte Indexfelder zugreifen, kommt es dabei natürlich zu Problemen.

Ich konnte mir das zunächst absolut nicht erklären. Ich habe dann einen Blick in die ELO Administration Konsole geworfen und da fehlte die neue Maske.

Der ELO Tomcat Server cached also die Maskendefinitionen und der ELO Windows Client führt Änderungen der Maskendefinition nicht über den IndexServer aus sondern direkt über seinen ODBC-Link.

Ein Neustart des ELO Tomcats schaffte das Problem aus der Welt. Danach enthielt das ObjKeys Array auch bei Dokumenten der neuen Maske für jedes Indexfeld ein Element - auch für die nicht belegten Indexfelder.

Sonntag, 28. Juni 2015

Logarithmische Skala - anschaulich erklärt

Zwischendurch mal ein weitestgehend sinnfreier Beitrag zur Auflockerung. Ich habe mir vor knapp 2 Jahren ein neues Mountain Bike gekauft und fahre seitdem relativ viel damit.
Natürlich habe ich einen Tachometer am Rad und verfolge, wie die gefahrenen Kilometer mehr werden.
So ein bisschen arbeitet man beim Radfahren immer an gewissen Kilometer Marken - ich zumindest. Die ersten 100 sind schnell voll.
Es geht dann zügig weiter und die 1000-Kilometer-Marke kommt in Reichweite. Nach 2 Jahren kommen nun die 10.000 Kilometer in den Zielfokus. Das hat gedauert.
Diese Marken folgen einer logarithmischen Skala und da wir Menschen im Dezimal-Zahlensystem denken ist die Basis der dekadische Logarithmus (Basis 10):


10er Exponent   Kilometer
0  (10 hoch 0)         1
1  (10 hoch 1)        10
2  (10 hoch 2)       100
3  (10 hoch 3)      1000
4  (10 hoch 4)     10000
5  (10 hoch 5)    100000
usw.

Die Kilometer Meilensteine anhand der belegten Tachostellen folgen also einer logarithmischen 10er-Skala (0,1,2,3 usw.). Und es liegt in der Natur der Sache, dass es zunehmend aufwändiger wird, einen weiteren Wert auf der Skala zu schaffen.
Während ich von der 0 zur 1 nur 9 km fahren musste, waren es von der 1 zur 2 schon 90 und von der 2 zur 3 dann 900 km.
Um meinen aktuellen Meilenstein, die 10000 km (also die 4 auf der Logarithmus Skala) zu erreichen, muss ich seit der 3  9000 km zurücklegen.

Die 4 auf der Logarithmus Skala habe ich in spätestens 2 Monaten. Und die 5? Tja, bei jetzt aufgerundet 10.000 km in 2 Jahren müsste ich 18 Jahre in dieser Intensität weiterradeln, um die 90.000 km zur 5 (also 100.000 km) zu schaffen. Ich bin jetzt 35 - also bis zum Alter von 53 Jahren weiterradeln und vor allem nicht nachlassen!

Die 100.000 km sind also möglicherweise noch drin. Wie viel Jahre ich für die 6 - also 1 Million Kilometer - radeln müsste ist die Hausaufgabe.

Logarithmische Skala im Alltag:


Dezibel (db): Basis dieser logarithmischen Skala ist ebenfalls 10. Daher ist auch klar, warum zwischen 40 und 50db absolut gesehen ein wesentlich kleinerer Unterschied besteht als zwischen 50 und 60db.

Richterskale (Erdbeben): Basis 10

pH-Wert: Basis dieser logarithmischen Skala ist ebenfalls 10. Der pH-Wert gibt an, wie sauer bzw. basisch eine chemische Lösung ist. pH = 7 ist neutral, d.h. weder basisch noch sauer. pH < 7 ist sauer, pH > 7 basisch.
Da es sich um eine logarithmische Skala handelt verhält sich auch hier wie bei den Fahhrad Meilensteinen:
Während die Werte zwischen 7 und 2,5 von allerhand säuerlichen Lebensmitteln erreicht werden (Kaffee = 5, Wein = 4, Zitronensaft = 2,4), liegt Magensäure zwischen 1 und 2. Zwischen 0 und 1 liegen dann sämtliche harten Sachen, wie z.B. Batteriesäure.


Samstag, 27. Juni 2015

Aufbewahrung von Versionen mit rsync

Für die Sicherung meiner Daten nutze ich rsync. Ich habe dazu 2 Skripte:

  1. rsync aller wichtigen Datenverzeichnisse von meinem Notebook per SSH auf einen Server, der bei mir zu Hause steht. Diese Sicherung führe ich häufig durch.
  2. Der Server verfügt über 2 identische Platten, von denen jedoch normalerweise nur eine eingebaut ist. Die andere liegt an einem sicheren Ort. Hin und wieder hole ich die zweite Platte, stecke diese ein und führe dann mit rsync einen Abgleich von der ersten auf die zweite Platte durch. Danach geht die Platte wieder zurück auf den Lagerplatz.

Falls hier wirklich mal die ganz Bude abbrennen sollte kann ich mit der Offline-Platte zumindest wieder an einem definierten Punkt beginnen. Ich hoffe natürlich, dass ich die nie brauche.

Seit ich mein eigenes Document Management System einsetze und den überwiegenden Teil der Papierdokumente vernichte hat sich mein Anspruch an die Datensicherung etwas erhöht. Bisher löscht rsync gelöschte Dateien auch in der Sicherung (Option --delete). Geänderte Dateien werden in der Sicherung einfach überschrieben.
Ich wollte nun eine minimale Versionierung in der Sicherung haben.

GIT


Ich habe erst einen Versuch mit GIT gemacht. Das Erstellen eines lokalen Repositories ist ganz einfach und das initiale Hinzufügen der bereits vorhandenen Dateien ebenfalls. Danach wird es aber schwieriger:

  1. Neu hinzugekommene Dateien müssen mit git add XYZ in das Repository aufgenommen werden
  2. Gelöschte Dateien müssen mit git rm XYZ aus dem Repository entfernt werden
  3. Zum Festschreiben aller Änderungen muss außerdem ein git commit aufgerufen werden.

Die ersten beiden Punkte bedeuten Zusatzaufwand für jede Datei und das ist beim Umgang mit Dokumenten nicht praktikabel.

rsync --backup und --backup-dir


Ich bin dann auf zwei rsync Optionen gestossen:

  1. --backup: Weist rsync an, Sicherungskopien geänderter und gelöschter Dateien zu erstellen
  2. --backup-dir: Gibt an in welchem Verzeichnis die Kopien abgelegt werden. Das Verzeichnis ist relativ zum Zielpfad

Ich habe mein rsync Skrip entsprechend angepasst:


# Datum ermitteln fuer backup change Verzeichnis
DATE=`date +%Y_%m_%d-%H_%M_%S`
REVISION_DIR="revisions_$DATE"

# rsync starten (mit --backup und --backup-dir)
rsync -e ssh -avzP --delete --backup --backup-dir=$REVISION_DIR /home/peter/DocumentManagement/archiv peter@parmesan:/data/disk1/home/peter/archiv_backup/


Zunächst wird ein Verzeichnisname für das BACKUP-DIR erzeugt. Über date +%Y_%m_%d-%H_%M_%S wird dazu Datum+Uhrzeit in eine Variabel geschrieben. REVISION_DIR enthält den fertigen Verzeichnisnamen.
Das Listing des Sicherungsverzeichnis auf dem Server sieht so aus:

peter@parmesan:~/archiv_backup$ ls -1
archiv
revisions_2015_06_27-22_05_28
revisions_2015_06_27-22_06_35

Im Unterverzeichnis archiv befindet sich der aktuell gesicherte Stand. Die revisions_... Unterverzeichnisse enthalten jeweils geänderte und gelöschte Dateien des jeweiligen Sicherungslaufs. rsync baut auch in den BACKUP-DIRs die gesamte Verzeichnisstruktur bis zu den entsprechenden Files auf.

FAZIT: Über die rsync Optionen --backup und --backup-dir konnte ich meine Sicherung so anpassen, dass generell keine Dateien überschrieben oder gelöscht werden.
Für jeden Sicherungslauf wird ein Backup-Verzeichnis erstellt und dort werden die Ursprungsversionen aller veränderten und gelöschten Dateien gesichert.

Dienstag, 16. Juni 2015

WSUS: Voll gelaufene SQLExpress verkleinern

Anderer Kunde ähnliche Probleme. Der WSUS läuft hier auf einem separaten Windows System was die gefühlte Performance gegenüber einer SBS Installation schonmal um Größenordnungen erhöht.

Ich muss tätig werden, da der WSUS offenbar keine Updates mehr anbietet. Ein Blick in die Verwaltungskonsole offenbart, dass die Synchronisierungen seit längerem fehlschlagen. Der Fehler ist wie immer nichtssagend.

Festplattenplatz ist genügend frei. Ich starte zunächst das System durch und starte dann eine manuelle Synchronisierung. Diese läuft und läuft und läuft und steht bei 14%.

Das wird nichts mehr. Ich schaue die Windows Protokolle durch und dort findet sich nun eine Spur:

Speicherplatz für das 'dbo.tbEventInstance'.'PK__tbEventInstance__2C3E80C8'-Objekt in der 'SUSDB'-Datenbank konnte nicht zugeordnet werden, da die Dateigruppe 'PRIMARY' voll ist. Speicherplatz kann durch Löschen nicht benötigter Dateien, Löschen von Objekten in der Dateigruppe, Hinzufügen von Dateien zur Dateigruppe oder Festlegen der automatischen Vergrößerung für vorhandene Dateien in der Dateigruppe gewonnen werden.

Für Zugriff auf SUSDB über Management Studio folgende Verbindung manuell eingeben:

\\.\pipe\mssql$microsoft##ssee\sql\query

Über das SQL Server Management Studio Express schaue ich mir die Datendateieinstellungen für die SUSDB an. Es ist Autoextend eingestellt. Ein Versuch, die Datendatei manuell zu vergrößern schlägt fehl:

Der Fehler lautete sinngemäß, dass die Datenbank bereits die maximal lizensierte Größe von 4096MB hat und nicht vergrößert werden kann. Die hier installierte SQL Server Express Edition kann also nicht mehr. Super.

Man kann über das Management Studio unter SUSDB => Tasks => Verkleinern => Datenbank versuchen, die Datenbank zu verkleinern. Im Dialog wird angezeigt wie viel Speicher frei gegeben werden kann. In meinem Fall leider 0%.
Nach weiterer Recherche habe ich einfach mal den Bereinigungsassistent des WSUS gestartet. Beim vorherigen WSUS (anderer Kunde mit SBS) hat der nichts geschafft.

Hier sieht es nun anders aus. Der Assistent läuft zwar scheinbar wieder endlos aber im Hintergrund tut sich auch was. Ich habe ab und zu mal im Management Studio unter SUSDB => Tasks => Verkleinern => Datenbank geschaut und der freizugebene Speicher wurde langsam größer. Natürlich habe ich die Verkleinerung noch nicht gestartet, da der WSUS ja auf der DB arbeitet.
Aktuell werden bereits 100MB (2%) angezeigt. Der Assistent läuft jetzt ca. 20 Minuten und läuft weiter. Ich hoffe, dass er über Nacht noch ein paar 100MB schafft.

Morgen geht es weiter ...



Montag, 1. Juni 2015

Zahlen in Explosionszeichnungen erkennen mit OpenCV Java

Einleitung


In Vorbereitung auf ein möglicherweise kommendes Projekt aber wohl in erster Linie aus technischem Interesse habe ich mich mit der Erkennung von Ziffern in Ersatzteil Zeichnungen beschäftigt.
Ziel ist es, die in der Grafik enthaltenen Ziffern inklusive Koordinaten zu erkennen, um damit eine interaktive Darstellung der Grafik zu implementieren.

Im ersten Versuch habe ich ein paar freie OCR Tools auf die Bilder angesetzt. Die Ergebnisse waren schlecht. Dies hatte verschiedene Gründe:

  1. Bescheidene Qualität des Bildmaterials
  2. OCR-Tools sind auf Fließtext optimiert und nicht auf Grafiken, in denen an verschiedenen Stellen nur vereinzelte Zahlen stehen

Ein Bekannter hat mir dann von OpenCV erzählt. Dabei handelt es sich um eine Bibliothek mit Funktionen und Algorithmen zur Bildverarbeitung, Objekterkennung usw.
Es ist eine C-Bibliothek mit Schnittstellen für C, C++, Python und Java. Ich habe das Java JNI Interface genutzt.

Bevor es losgeht schonmal ein Bild mit dem was am Ende herauskommt. Das ist eine alte Explosionszeichnung aus den 50er Jahren. Ich habe diese gescannt und das fertige Programm damit gefüttert.
Erkannte Zahlen sind rot eingerahmt und der Zahlenwert steht darüber. Blau eingerahmte Objekte sind als mögliche Zahl erkannt worden aber brachten keine ausreichende Übereinstimmung:

Bild 1: Verarbeitete Zeichnung mit erkannten Ziffern

Wie funktioniert es grundsätzlich?


Inspiriert wurde ich durch folgenden Link:

Abgeleitet aus dem was dort steht soll meine Routine folgendes tun:

  1. Input Bild laden
  2. Konturensuche durchführen. Konturen sind geschlossene Linien. Bei der "1" läuft die Kontur ein Mal um die Zahl herum. Eine "6" hat zwei Konturen - aussen herum und der Bauch innen. Eine "8" hat drei Konturen.
  3. Größenprüfung: Zu große bzw. zu kleine Konturen werden verworfen
  4. Klassifizierung: Für jede Kontur wird ermittelt, ob und um welche Ziffer es sich handelt.
  5. Zusammenfassung benachbarter Ziffern: Aus "1" und "0" nebeneinander wird eine "10".
  6. Erzeugen des Ausgabebildes
Die Klassifizierung erfolgt durch Ermittlung des nearest Neighbours. Diese Methode muss angelernt werden, d.h. es muss zu einer bestimmten Menge Ziffernbilder vorgegeben werden welche Ziffer das Bild zeigt.


Einrichtung Betriebssystem und IDE


Ich nutze Ubuntu 14.04 und Eclipse. Das OpenCV Java Interface habe ich über die Paketverwaltung installiert (2 Pakete):

sudo apt-get install libopencv2.4-java libopencv2.4-jni

In Eclipse legt man am einfachsten eine User Library an. Wie das geht steht hier:

http://docs.opencv.org/doc/tutorials/introduction/java_eclipse/java_eclipse.html

Wenn man wie ich die Standard Ubuntu Pakete verwendet, müssen bei der User Library folgende Pfade eingestellt werden:

JAR: /usr/share/OpenCV/java/opencv-248.jar
Native library location: /usr/lib/jni

Die native library location kann man einstellen, wenn man die angelegte User library aufklappt (im Preferences Dialog). Ich habe das nicht gleich gesehen und das hat etwas Zeit gekostet.

Vorbereitung der Bilder


Nach vielen Versuchen hat sich folgende Vorgehensweise zum Erstellen des Eingabematerials bewährt:

  1. Scannen der Zeichnung mit 300 DPI, Graustufen
  2. Festlegen des relevanten Ausschnitts
  3. Konvertierung in schwarz/weiss Bild mit convert (imagemagick)
    peter@gorgonzola:~$ convert seite5.png -threshold 67% 5.png
Die hier gezeigten Bilder haben im Original einen graubraunen leicht gefleckten Hintergrund. Dies führt bei der Konturensuche zu einem enormen Aufwand, da sehr viel kleine Konturen erkannt werden. Durch convert -threshold 67% wird dieser Hintergrund weiss ohne das die Ziffern verschwinden.
Je nach Eingabematerial muss man die Aufbereitung anpassen. Eine Lösung für alles gibt es in diesem Fall nicht.

Bild 2: Konvertierung Graustufen in S/W mit 67% threshold

Einlesen des Eingabematerials, Konturensuche

Jetzt kann es losgehen. Das folgende Snippet lädt ein Eingabebild und führt eine Konturensuche durch. Die Konturensuche benötigt ein BINARY image. Da ich S/W-Bilder verwende kann ich diese 1:1 für die Konturensuche verwenden.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// OpenCV Library laden - einmalig beim Programmstart
System.loadLibrary( Core.NATIVE_LIBRARY_NAME );

// Input laden (UNCHANGED, da bereits S/W)  
Mat image = Highgui.imread(inputFilename, Highgui.CV_LOAD_IMAGE_UNCHANGED);

// Ergebnisliste für Konturensuche
List contours = new ArrayList<>();
        
// Konturen suchen
Imgproc.findContours(image, contours, new Mat(), Imgproc.RETR_TREE,  Imgproc.CHAIN_APPROX_NONE);

Hinweis: findContours verändert das übergebene image. Es ist danach mehr oder weniger invertiert. Wenn man also mit dem image im Anschluß noch etwas vor hat, muss man findContours eine Kopie der Mat übergeben:

// Kopie erstellen, da Konturensuche das Eingabebild invertiert
Mat copy = new Mat();
image.copyTo(copy);

Größenprüfung, Extrahierung Teilbild und Weichzeichner


Im Anschluß werden die Konturen durchlaufen und anhand der BoundingBox (kleinstes einschließendes Rechteck) eine Größenprüfung vorgenommen.

Über subMat() werden die Teilbilder der Konturen extrahiert und mittels resize() auf eine fixe Größe gebracht. Die feste Größe ist für die Klassifizierung (Nearest Neighbour) notwendig. Auf die Teilbilder wird ein Gaußscher Weichzeichner angewendet. Durch das Weichzeichnen werden sich die Bilder ähnlicher, da die Kanten verwischen. Dies bringt bei der Klassifizierung bessere Ergebnisse. Wenn der Weichzeichner zu stark eingestellt wird, kommt es aber zu Falscherkennungen, d.h. Kleinteile werden als Ziffern erkannt.
Die besten Einstellungen hängen wieder vom Eingabematerial ab.



 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Größenfilter
Rect sizeFilter = new Rect(new Point(8, 16), new Point(50, 32));

// Feste Größe für Teilbilder
Size digitSize = new Size(15,15);

// Gefundene Konturen durchlaufen
for (Iterator i = contours.iterator(); i.hasNext(); ) {
  MatOfPoint contour = i.next();
   
  // Bounding Box der Kontur
  Rect box = Imgproc.boundingRect(contour);
      
  // Größenfilter für Kontur
  if (box.width < sizeFilter.tl().x ||
      box.width > sizeFilter.br().x || 
      box.height < sizeFilter.tl().y ||
      box.height > sizeFilter.br().y) {
        i.remove();
     continue;
  }
   
  // Einzelbild
  Mat subImg = new Mat();   
   
  Imgproc.resize(image.submat(box), subImg, digitSize);
   
  // Weichzeichner auf Einzelbild anwenden
  blurImage = new Mat();    
  Imgproc.GaussianBlur(subImg, blurImage, new Size(5,5), 3);

  // Einzelbild abspeichern (z.B. zum Anlernen des Nearest Neighbour)
  Highgui.imwrite("path/to/image.png", blurImage);
}

Anlernen der Nearest Neighbour Klassifizierung



Damit die Teilbilder klassifziert werden können (Erkennung der Ziffer) wird zunächst eine Menge an Teilbildern benötigt, zu den die entsprechende Ziffer vorgegeben wird.

Um dies einfach zu realisieren bin ich folgenden Weg gegangen:

  1. Erstellung einer Main-Klasse zum Erzeugen von Trainingsbildern
  2. Konturensuche in allen EIngabebildern.
  3. Speichern der Teilbilder in einem Unterverzeichnis (train/Nummer des Eingabebildes). Als Dateiname wird eine fortlaufende Nummer verwendet.
  4. Erzeugung einer Java-Properties Datei unter train/Nummer des Eingabebildes.txt. Diese Properties Datei enthält leere Eigenschaften für alle erzeugten Einzelbilder.

Zum Anlernen öffnet man die properties Datei und notiert dort die Ziffern, die auf den entsprechenden Teilbilder zu sehen sind.

Bild 3: Einzelbilder nummeriert und Properties Datei mit Ziffern zum Anlernen
Die Abbildung zeigt die Properties Datei und dahinter den Ordner mit den Einzelbildern. Bild 0 zeigt eine 9, Bild 1 eine 1. Die Bilder 2 bis 7 sind keine Ziffern. Bild 8 zeigt eine 2 usw.

NACHTRAG: Die Abbildung enthält einen Fehler. Bild 0 zeigt keine "9".

Hierzu ist natürlich etwas Basis Programmierarbeit erforderlich auf die ich jetzt nicht weiter eingehe.

Zum Anlernen der Klassifizierung müssen die Informationen aus der Properties Datei zusammen mit den Einzelbildern wieder in OpenCV Matrizes geladen werden (Mat).
Da dies nicht wirklich selbsterklärend ist hier der gesamte Code, um das Model zur Klassifizierung zu erzeugen:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
public CvKNearest getModel() throws Exception {
  // Trainingsmaterial einlesen
  File trainDir = new File(TRAIN_DIR);
  // Mat mit den angelernten Einzelbildern (alle hintereinander)  
  Mat trainingImages = new Mat();
  // Mat mit den entsprechenden Ziffern als double (hintereinander)
  Mat trainingLabels = new Mat();

  // Verzeichnisse mit Einzelbildern durchlaufen (1 pro Eingabebild)
  for (File f: trainDir.listFiles()) {
    // Only directories
    if (!f.isDirectory())
      continue;
   
    // Get imgId from dirname
   int imgId = StringHelper.toInt(f.getName());   
   if (imgId == -1) continue;
   
   // Trainingfile
   File propFile = new File(trainDir, imgId + ".txt");
   // No trainingfile found => ignore dir
   if (!(propFile.isFile())) continue;
   
   // Load training properties
   Properties props = new Properties();
   props.load(new FileInputStream(propFile));
   
   // List PNGs train/imgid
   for (File imgFile: f.listFiles()) {
     // Alles ausser png ignorieren    
     if (!imgFile.getName().endsWith(".png"))
       continue;
    
     int trainImgId = StringHelper.getIntFromFileName(imgFile);    
     if (trainImgId == -1) continue;   
       // Vorgabewert für Einzelbild
       String propValue = props.getProperty(getPropName(trainImgId));
     
       // Einzelbilder ohne Vorgabe ignorieren
       if (propValue == null || propValue.isEmpty()) continue;
   
       try {
         int propValueInt = Integer.parseInt(propValue);

         // Einzelbild laden
         Mat imgData = Highgui.imread(imgFile.getAbsolutePath(), 
            Highgui.CV_LOAD_IMAGE_GRAYSCALE);
 
         // Einzelbild in Mat schreiben (reshape setzt Dimension)    
         trainingImages.push_back(imgData.reshape(1, 1));
 
         // Vorgabeziffer in Mat schreiben
         Mat mat = new Mat(new Size(1,1), CvType.CV_32FC1);
         mat.setTo(new Scalar(propValueInt));
         
         trainingLabels.push_back(mat);
       } catch (NumberFormatException e) {
         // Ignore non int prop values
       }
     }   
   }
   
   // Matrixtyp setzen (erforderlich für CvKNearest)  
   trainingImages.convertTo(trainingImages, CvType.CV_32FC1);

   // Model erzeugen    
   CvKNearest model = new CvKNearest();  

   // Model anlernen
   model.train(trainingImages, trainingLabels);  
    
   return model;
}


Mit der Anweisung

trainingImages.push_back(imgData.reshape(1, 1));

werden die angelernten Teilbilder in eine große Matrix hintereinander geschrieben. Die Methode CvKNearest.train() erwartet dies so. Zuvor muss noch mit

trainingImages.convertTo(trainingImages, CvType.CV_32FC1);

eine Matrix des richtigen Typs erzeugt werden.


Durchführen der Klassifizierung (Erkennung von Ziffern)



Das angelernte Model kann nun verwendet werden, um Einzelbilder zu klassifizieren. Die Methode CvKNearest.find_nearest() ermittelt dazu zu einem Bild die beste Übereinstimmung aus dem angelernten Material.
Außerdem kann der Abweichungssgrad abgefragt werden. Anhand der Abweichung entscheide ich, ob ich die Ziffer akzeptiere. Bei zu hoher Abweichung zeigt das Bild in der Regel keine Ziffer sondern ein kleines Einzelteil o.ä.

Um die Klassifizierung auszuführen müssen die zu klassifizierenden Bilder wieder hintereinander in eine Matrix geschrieben werden (wie beim Anlernen).
Wie man CvKNearest nutzt und auf die Ergebnisse zugreift sollte in folgendem Beispiel klar werden:
Hinweis: Die Klasse Match ist eine kleine Hilfsklasse, die ein Ergebnis der Konturensuche enthält.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// Ergebnisse der Klassifizierung (Zahlen)
Mat results = new Mat();
// Abweichungen
Mat distances = new Mat();

// Zu klassifizierende Bilder  
Mat samples = new Mat();

// Gefundene Bilder durchlaufen  
for (Match box: boxes) {   
  // Bilder hintereinanderschreiben
  samples.push_back(box.img.reshape(1, 1));
}
    
samples.convertTo(samples, CvType.CV_32FC1);

// Klassifizierung ausführen
model.find_nearest(samples, 1, results, new Mat(), distances);

for (int i=0; i<boxes.size(); i++) {
  Match box = boxes.get(i);

  // Ergebnis Klassifizierung   
  double[] digit = results.get(i, 0);
  int digitInt = (int) digit[0];
  
  // Abweichung   
  double[] dists = distances.get(i, 0);
  // Prüfung der Abweichung     
  if (dists[0] > 200000) {
    continue; // Zu groß => ignorieren
  }
   
  box.digit = digitInt;
}


Zusammenfassen von mehrstelligen Zahlen



Dicht nebeneinander stehende Ziffern auf gleicher Höhe müssen zu mehrstelligen Zahlen zusammengefasst werden. Also aus "1" "0" muss eine "10" werden.
Die lässt sich realtiv einfach umsetzen:

  1. Erkannte Ziffern durchlaufen
  2. In der Schleife übrige Ziffern durchlaufen und X und Y-Differenz berechnen. Sobald Schwellenwerde unterschritten werden kann man davon ausgehen, dass die Ziffern zusammengehören.
  3. Box der ersten Ziffer um die Box der zweiten erweitern.
  4. Erste Ziffer * 10 + zweite Ziffer der ersten Box als Zahl zuweisen
  5. Zahl bei zweiter Box auf -1 setzen (keine).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Gefundene Konturen durchlaufen
for (Match box: boxes) {
  // Falls zur Kontur keine Ziffer erkannt wurde => ignorieren
  if (box.digit == -1) continue;

  // Übrige Konturen durchlaufen   
  for (Match box2: boxes) {    
    if (box == box2) continue;    
    if (box2.digit == -1) continue;

    // X-Differenz    
    double diffX = box.box.tl().x - box2.box.tl().x;
    // Schwellenwert X-Differenz
    if (diffX < -9 || diffX > 0)
      continue;

    // Y-Differenz    
    double diffY = box.box.tl().y - box2.box.tl().y;

    // Schwellenwert X-Differenz    
    if (diffY > 4 || diffY < -4) continue;
    
    // Ziffern gehören zusammen
    
    // Box1 erweitern => Boundingbox für beide Boxes bestimmen
    MatOfPoint points = new MatOfPoint(box.box.tl(), box.box.br(), 
       box2.box.tl(), box2.box.br());

    Rect newBox = Imgproc.boundingRect(points);
    box.box = newBox;

    // Wert berechnen
    box.digit = box.digit * 10 + box2.digit;
    
    // Wert aus Box2 entfernen
    box2.digit = -1;
  }
}

Darstellung der Ergebnisse


Im letzten Schritt werden die gefundenen Konturen und die erkannten Ziffern in einem Ausgabebild dargestellt. Man möchte ja auch sehen was das Programm kann.


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Input laden (FARBE)  
Mat image = Highgui.imread(inputFilename, Highgui.CV_LOAD_IMAGE_COLOR);

// Gefundene Konturen druchlaufen
for (int i=0; i<boxes.size(); i++) {
  Match box = boxes.get(i);

  // Kontur ohne erkannte Ziffer
  if (box.digit == -1) {
    // Blaues Rechteck
    Core.rectangle(image, box.box.tl(), box.box.br(), colorGreen, 2);
  } else { // Kontur mit erkannter Ziffer
    // Rotes Rechteck
    Core.rectangle(image, box.box.tl(), box.box.br(), colorRed, 2);

    // Ziffer als Text   
    Core.putText(image, "" + box.digit, box.box.tl(), Core.FONT_ITALIC,
    sizeFilter.tl().y / 20, colorRed, 2);
  }
}

// Ausgabebild schreiben
Highgui.imwrite(OUTPUT_DIR + "/" + imgId + ".jpg", image);


Offene Probleme

Die Routine hat leider ein paar Schwachstellen, die je nach Eingabematerial mehr oder weniger stark zum Tragen kommen:


  1. Die Ziffern müssen frei stehen. Wenn z.B. ein Zeigestrich bis an die Zahl heranreicht wird dieser mit in die Kontur einbezogen. Als Lösung kann man die Schwellenwerte für die Konturengröße höher setzen und die entsprechenden Einzelbilder mit Strich anlernen.
    Eine andere Lösung ist eine Vorbearbeitung der Eingabebilder bei der man bis an die Zahlen heranreichende Striche kürzt.
  2. Wenn Ziffern zu dicht zusammen stehen, werden diese ebenfalls zu einer Kontur zusammen gefasst. Auch hier kann man durch Erhöhung der Schwellenwerte und Anlernen des entsprechenden Einzelbildes eine Lösung erreichen.
  3. Sobald die Ziffern in größere Teile hineinragen scheitert die Routine ebenfalls.
Ein weiteres Problem sind alle Pixelangaben im Programm. Diese korrespondieren natürlich mit der Schriftgröße im Eingabematerial. Folgende Pixelangaben müssen in Abhängigkeit der Zifferngröße in Pixel angepasst werden:

  1. Größenfilter für Bounding Boxes
  2. Größe der Einzelbilder für CvKNearest
  3. Einstellung für Weichzeichner. Je größer die Ziffern sind, um so stärker kann der Weichzeichner eingestellt werden.
  4. Toleranzschwellen (x/y) zum Zusammenfassen von Ziffern.
  5. Toleranzschwelle für Abweichungsgrad. Je größer die Einzelbilder sind, um so höher werden die Abweichungen bei gleicher Ähnlichkeit. Möglicherweise wird die Abweichung pro Pixel quadriert und aufsummiert bzw. etwas vergleichbares.
    Daher muss der Schwellenwert erhöht werden sobald die Einzelbildgröße erhöht wird.


In einem ersten Schritt könnte man dem Programm die Schriftgröße in Pixel als Parameter mitgeben und die übrigen Einstellungen und Schwellenwerte werden daraus ermittelt. Das wäre ein Anfang.


Zur Erkennung nicht 100% frei stehender Ziffern ist mir auch noch etwas eingefallen: Sobald die Bounding Box für eine Ziffer deutlich zu groß ist, werden die 4 Ecken in Zifferngröße weiterverarbeitet. Mit dieser Lösung würde man zusammenstehende mehrstellig Zahlen trennen (zumindest 2-stellige). Falls von einer Seite ein Strich o.ä. bis an die Zahl reicht würde man diesen ebenfalls abschneiden.
Diese Idee werde ich bei Gelegenheit auf jeden Fall noch einarbeiten.

Fazit

Damit die hier gezeigte Erkennung gut arbeitet, braucht man eine größere Anzahl Eingabebilder mit sehr ähnlichem Schriftbild. Die Ziffern müssen frei stehen.

Ich habe mit vier Beispielbildern gearbeitet. Ich musste allerdings bei diesen Bildern ein paar Striche retuschieren, da diese bis an die Ziffern reichten.
Bild 1 und 2 habe ich komplett manuell klassifiziert und damit das CvKNearest Model angelernt.

Bei Bild 3 (siehe 1. Bild im Blog oben)  betrug die Erkennungsrate gut 90%. Man sieht, dass einige mehrstellige Ziffern als eine Kontur erkannt und damit nicht klassifiziert wurden.
Bei Bild 4 wurden alle Ziffern erkannt - also 100%.

Bild 4: 100% Erkennung ohne Vorgabe


Man kann die Verfahrensweise bestimmt noch an der ein oder anderen Stelle verbessern. Mal schauen ob daraus ein Projekt wird.
Auf jeden Fall ein spannendes Thema.


Wer bis hierhin durchgehalten hat: Vielen Dank für Euer Interesse und bis bald ...

Haltet durch!



1. Verbesserung - Aufteilen übergroßer Konturen


Sobald Ziffern nicht 100% freistehen treten die folgenden Probleme auf:

  1. Sich berührende mehrstellige Ziffern werden als eine Kontur erkannt und müssen manuell klassifiziert werden.
  2. Falls Striche o.ä. eine Ziffer berühren wird die Kontur ebenfalls erweitert und die Klassifizierung erschwert bzw. unmöglich.
Um diese Fehler zu reduzieren habe ich die Größenprüfung der Konturen umgearbeitet:
  1. Zu kleine Konturen werden verworfen (zu klein für eine Ziffer)
  2. Konturen die 30% breiter als die eingestellte Ziffernbreite und/ oder 30% höher als die eingestellte Ziffernhöhe sind werden geteilt und einzeln verarbeitet.
  3. Je nach Überschreitung werden 4 Eckbereiche, Links/Rechts oder Oben/Unten getrennt.

Bild 5: Aufgeteilte Kontur - linker und rechter Bereich
Das Bild zeigt die Ziffern der 36. Diese berühren sich und werden daher als eine gesamte Kontur gefunden. Da die Breite der Bounding-Box mehr als 30% über der eingestellten Ziffernbreite (hier 16 Pixel liegt), die Höhe der Bounding-Box jedoch nicht diesen Schwellenwert überschreitet wird die Box in den linken und rechten Bereich geteilt und getrennt verarbeitet.
Nach der Klassifizierung wurden die Ziffern wieder zur 36 vereint, da die Boxes als zusammengehörend erkannt wurden (erkennbar am roten und blauen Rahmen sowie den roten Ziffern).

Im Test brachte diese Verbesserung sehr gute Resulate. Die Erkennungsrate in den beiden unbekannten Bildern liegt jetzt bei 100%.




2. Verbesserung - Erneute Konturensuche in den geteilten Konturen

Um die Erkennung noch weiter zu verbessern könnte in den mit Verbesserung 1 aufgeteilten Bereichen eine erneute Konturensuche durchgeführt werden.
Sobald Ziffern durch Striche o.ä. mit größeren Objekten verbunden sind, wird die Gesamtkontur zwar in die 4 Ecken geteilt, jedoch befindet sich dort nicht unbedingt die Ziffer.
Diese befindet sich wahrscheinlich in einem der 4 Randstreifen und müsste dort mit einer erneuten Konturensuche gefunden werden.

Bild 6: Randbereiche (rot) einer übergroßen Kontur in denen eine neue Konturensuche gemacht werden müsste
Diese Verbesserung habe ich noch nicht umgesetzt. Es besteht auch das Risiko, dass sich in den Randbereichen weitere Ziffern befinden, die nicht zu der Konture gehören aus der die Bounding-Box ermittelt wurde.
Diese werden dann doppelt gefunden und müssten aussortiert werden.