Wie im letzten Artikel schon angemerkt, soll es wieder ein wenig in Richtung der Wurzeln dieses Blogs gehen. Konkret heißt das mehr Code-Beispiele und Tutorials.
Dieser Artikel ist ein weiterer Schritt in diese Richtung. Nachdem wir im letzten Artikel ein C#-Tool geschrieben haben, mit dem man die Gewinnwahrscheinlichkeit einer Pokerhand ausrechnen kann, befassen wir uns in diesem Artikel mit den Themen Bildverarbeitung und maschinellem Sehen in C#.
Das Tool, welches wir heute entwickeln, analysiert Screenshots eines Pokerspiels und kann die Karten inklusive Kartenwert und Kartenfarbe automatisiert auslesen. Hierzu nutzen wir die ziemlich mächtige OpenSource-Bibliothek “AForge.NET”, welche etliche Klassen und Methoden für die Bereiche maschinelles Sehen und Künstliche Intelligenz bietet.
Vor dem Coding
Bevor es mit der Programmierung losgeht, müssen noch ein, zwei Dinge erledigt werden. Zum einen brauchen wir eine Palette an Screenshots mit Spielkarten, die wir analysieren können und zum anderen müssen wir uns die benötigten AForge-Dlls beschaffen.
Für die Screenshots habe ich mir das erstbeste Online-Casino geschnappt, das mir zwischen die Finger kam. Im Prinzip ist es jedoch egal, aus welchem Casino die Screenshots stammen, solange die Karten komplett auf dem Screenshot sind und nicht durch andere Elemente verdeckt werden.
Bei der Wahl des Online Casinos hilft eine Google-Suche oder ein Blick in eine der Casino-Vergleichslisten. Die meisten Casinos bieten auch einen Spielgeld-Modus an, sodass für das Anlegen der Screenshots keine Kosten entstehen.
Ist der Anbieter gefunden, sollten zumindest so viele Screenshots gemacht werden, dass mindestens jede Kartenfarbe und jeder Kartenwert einmal vorkommt.
Die AForge-Bibliothek kann direkt von der AForge-Webseite geladen werden. Von den vier Download-Links sollte jener mit “Libraries only” im Titel gewählt werden. (Wer sich gerne alles installieren möchte, kann auch den Installer laden – nötig ist das jedoch nicht.)
Projekt anlegen – das GUI-Layout
Wenn die Vorbereitungen getroffen sind, kann es mit der Entwicklung losgehen. Entwickelt wird, wie fast immer hier im Blog, mit Microsofts Visual Studio.
Als erstes legen wir ein WinForms-Projekt an – ich habe es “KartenDetektor” genannt – und gestalten die Programmoberfläche.
Um sich einen besseren Überblick über die einzelnen Teilbilder der Bildanalyse machen zu können, habe ich für die verschiedenen Analyseschritte eigene PictureBoxen hinzugefügt, sodass die Ergebnisse der einzelnen Schritte separat betrachtet werden können.
Im Endeffekt würden auch weit weniger PictureBoxen reichen, zur (eventuellen) Fehlersuche und während der Entwicklung macht die große Anzahl aber in der Tat Sinn.
Das Layout, mit dem wir starten, kann dem links-stehenden Screenshot entnommen werden. Es besteht aus 3 GroupBoxen, die jeweils 5 PictureBoxen und 5 TextBoxen enthalten. Eine Gruppe dient für die Kartenfarben-Erkennung, eine für die Kartenwerte und eine für die Karten in der Gesamtansicht.
Weiter habe ich 2 Pictureboxen zur Ansicht des Screenshots (in der normalen und der bearbeiten Variante) hinzugefügt. Das letzte Nutzerelement ist ein Button, mit dem man die verschiedenen Screenshots durchschalten kann.
Nach dem die GUI-Elemente platziert sind, legen wir noch zwei Eventhandler an – einen für das FormLoad-Event und einen für das ButtonClick-Event.
Als letzten Schritt fügen wir über “Verweise->Verweis hinzufügen” die drei, für unser Projekt benötigten, AForge-Dlls “AForge.dll”, “AForge.Imaging.dll” und “AForge.Math.dll” hinzu.
Using-Direktiven, Hilfsklassen und Strukturen
Nachdem die Arbeiten im Form-Designer abgeschlossen sind, können wir uns an den Code wagen. Als erstes fügen wir die nötigen using-Direktiven hinzu.
using AForge; using AForge.Imaging; using AForge.Imaging.Filters; using AForge.Math.Geometry; using System.Drawing.Imaging;
Im Kopfbereich unserer Form-Klasse legen wir außerdem noch eine Liste an, in der wir später die Dateinamen der Screenshots ablegen wollen.
//Liste für die Eingabe-Screenshots List<string> files = new List<string>();
Damit wir im Logikteil nachher etwas geschickter arbeiten können, brauchen wir noch ein paar Hilfsstrukturen/-Klassen. Zum einen eine Klasse, die eine Spielkarte samt Eigenschaften und Grafiken abbildet und zum anderen eine Struktur, die Kartenfarben und -werte abbilden kann.
//Spielkarten Klasse - enthält alle Informationen zu einer Karte private class Card { //Bild der Karte public Bitmap cardImg { get; set; } //Bildausschnitt mit der Kartenfarbe public Bitmap identifierSuitImg { get; set; } //Bildausschnitt mit dem Wert public Bitmap identifierValueImg { get; set; } //Analysierte Kartenfarbe public string suit { get; set; } //Analysierter Wert public string value { get; set; } } //Identitätsstruktur private struct Identifier { //Name des Samples (z.B. Karo, Ass, ...) public string name { get; set; } //Sample-Grafik public Bitmap sample { get; set; } }
Folgende, weitere Methode wird im späteren Verlauf des Artikels benötigt und dort dann auch weiter erklärt.
public static Bitmap normalizeForEqualityCheck(Bitmap source) { //Den ersten schwarzen Pixel von rechts erkennen (=linke Schnittkante) int left = -1, right = -1, up = -1, down = -1; int clip = 255; for (int x = 0; x < source.Width; x++) { for (int y = 0; y < source.Height; y++) { var col = source.GetPixel(x, y); if ((col.R + col.B + col.B) / 3f < clip) { left = x; break; } } if (left != -1) break; } //Den ersten schwarzen Pixel von rechts erkennen (=rechte Schnittkante) for (int x = source.Width-1; x > 0; x--) { for (int y = 0; y < source.Height; y++) { var col = source.GetPixel(x, y); if ((col.R + col.B + col.B) / 3f < clip) { right = x; break; } } if (right != -1) break; } //Den ersten schwarzen Pixel von oben erkennen (=obere Schnittkante) for (int y = 0; y < source.Height; y++) { for (int x = 0; x < source.Width; x++) { var col = source.GetPixel(x, y); if ((col.R + col.B + col.B) / 3f < clip) { up = y; break; } } if (up != -1) break; } //Den ersten schwarzen Pixel von unten erkennen (=unten Schnittkante) for (int y = source.Height-1; y > 0; y--) { for (int x = 0; x < source.Width; x++) { var col = source.GetPixel(x, y); if ((col.R + col.B + col.B) / 3f < clip) { down = y; break; } } if (down != -1) break; } //Grafik an den Schnittkanten zuschneiden Crop cropSuit = new Crop(new Rectangle(left, up, right-left, down-up)); var output = cropSuit.Apply(source); //Grafik auf Norm-Maße strecken ResizeBilinear resizer = new ResizeBilinear(60, 60); return resizer.Apply(output); }
Nun kann es mit der Programmierung der Logik losgehen. Wir beginnen analog zur Programmablauflogik mit FormLoad-Event.
Der Programmstart
Beim Programmstart werden alle Screenshots aus dem Verzeichnis “\samples\n” (relativ zum Ausführungsordner) des KartenDetektors in eine Liste geladen.
private void Form1_Load(object sender, EventArgs e) { //Dateinamen der Screenshots in die Liste laden files = System.IO.Directory.GetFiles(Application.StartupPath + "\\samples\\n").ToList(); //Ersten Screenshot in Bitmap-Objekt laden Bitmap inputBmp = (Bitmap)Bitmap.FromFile(files[0]); //Analyse durchführen analyzeCards(inputBmp); //Screenshot aus Liste entfernen files.RemoveAt(0); }
Nach dem Laden der Screenshots wird die erste Datei geöffnet, in eine Bitmap geladen und an die Analysemethode übergeben.
Der Kartenzuschnitt
Kommen wir nun zum Herzstück des Programms. Der Methode zur Kartenanalyse. Da in der Methode relativ viel passiert, werden wir sie Stück für Stück aufbauen. Solltet ihr dabei einmal den Überblick verlieren, könnt ihr euch das komplette, lauffähige Demoprojekt am Ende dieses Artikels herunterladen.
Die Methode nimmt als einziges Argument eine Bitmap an, welche den Screenshot vom Spieltisch enthalten sollte.
Danach wird der Screenshot auf zuvor definierter Maß (1364×726 px) gestreckt. Dies geschieht, da im weitere Verlauf Bildausschnitte verwendet werden, die relativ zu den Screenshot-Abmessungen stehen. Von daher bringen wir alle Screenshots erst einmal auf dieselbe Größe.
Ist die einheitliche Größe gegeben, legen wir eine Arbeitskopie (processedBmp) an.
public void analyzeCards(Bitmap inputBmp) { //Liste für Spielkarten-Objekte anlegen List<Card> cards = new List<Card>(); //Screenshot auf Standardformat ziehen, damit unabhängig von der Auflösung //die nachfolgenden Koordinaten stimmen ResizeBilinear filterRes = new ResizeBilinear(1364, 726); inputBmp = filterRes.Apply(inputBmp); //Arbeitskopie des Screenshots anlegen Bitmap processedBmp = inputBmp.Clone() as Bitmap;
Nun wird die Arbeitskopie erst mit einem Farbfilter, dann mit einem Graustufenfilter und schlussendlich mit einem Schwarz-Weiß-Filter bearbeitet. Dies ist nötig, da wir im Anschluss einen sogenannten Blobcounter anwendenden wollen und dieser Grafiken in s/w bevorzugt verarbeitet.
//Farbfilter für die Arbeitskopie anlegen und anwenden, um //bessere Ergebnisse bei der S/W-Konvertierung zu erhalten ColorFiltering filter = new ColorFiltering(); filter.Red = new AForge.IntRange(250, 255); filter.Green = new AForge.IntRange(250, 255); filter.Blue = new AForge.IntRange(250, 255); filter.ApplyInPlace(processedBmp); //Arbeitskopie über Graustaufen nach S/W konvertieren FiltersSequence seq = new FiltersSequence(); seq.Add(Grayscale.CommonAlgorithms.BT709); seq.Add(new IterativeThreshold()); processedBmp = seq.Apply(processedBmp);
Der Blobcounter kann Objekte innerhalb einer Bitmap erkennen. Wir wollen ihn Nutzen, um die Spielkarten bzw. dessen Positionen innerhalb des Screenshots zu erkennen.
Um bessere Ergebnisse zu erhalten, konfigurieren wir den Blobcounter vorab über die Parameter “MinHeight”, “MaxHeight”, “MinWidth”, “MaxWidth” und FilterBlobs.
Die Min-Max-Parameter geben an, wie groß bzw. klein ein gefundener Blob maximal bzw. minimal sein darf. Hierdurch erreichen wir, dass möglichst nur die Spielkarten auf dem Screenshot als Blob gefunden werden und nicht etwa andere, kleinere oder größere Gegenstände auf dem Screenshot.
Der Parameter “FilterBlobs” wiederum beschreibt nur, um die in den Min-Max-Filtern gesetzten Maße angewendet werden sollen.
Abschließend entfernen wir die ersten beiden Ergebnisse aus der Blob-Liste, da es sich hierbei um die zwei verdeckten Karten auf dem Screenshot handelt. (Dies kann jedoch von Poker-Applikation zu Poker-Applikation abweichen. Da ist ausprobieren angesagt.)
//Spielkarten per Blobcounter finden. Durch MinHeight und MaxHeight wird die //Größe der gefunden Blobs so eingeschränkt, dass nur Karten gefunden werden BlobCounter extractor = new BlobCounter(); extractor.FilterBlobs = true; extractor.MinWidth = extractor.MinHeight = 40; extractor.MaxWidth = extractor.MaxHeight = 70; extractor.ProcessImage(processedBmp); var blobs = extractor.GetObjectsInformation().ToList(); //Die ersten beiden Blobs entfernen (da dies die zwei verdeckten Karten //auf dem Screenshot sind. blobs.RemoveRange(0, 2);
Nun haben wir also den gestreckten Original-Screenshot (inputBmp), das S/W-Abbild davon (processedBmp) und eine Liste mit den Koordinaten der Spielkarten auf dem Screenshot (blobs).
Im nächsten Schritt durchlaufen wir alle gefunden Blobs/alle Spielkarten, um diese zu analysieren.
Für jeden Blob/jede Spielkarte ermitteln wir die Punkte, die die Kontur der Spielkarte bilden (edgePoints) sowie die 4 Eckpunkte der Spielkarte (corners). Mit dem Wissen über die Eckpunkte können wir nun den Ausschnitt aus dem Screenshot erzeugen, der die Spielkarte darstellt und diesen mittels QuadrilateralTransformation in ein rechteckiges Format entzerren.
Im Anschluss prüfen wir, ob die Spielkarten-Grafik auf der Seite liegt, und stellen sie ggf. per Drehung (Rotate270FlipNone) auf.
Abschließend strecken wir die Karte per (ResizeBilinear) auf eine definierte Größe, sodass alle Spielkarten/Blobs gleich groß sein werden.
Die bearbeitete Spielkarten-Grafik legen wir in einer neuen Instanz der Card-Klasse ab.
//Graphics-Object vom Screenshot (nicht der S/W-Arbeitskopie) anlegen, //um die gefunden Blobs (=Spielkarten) einzeichnen zu können Graphics gfx = Graphics.FromImage(inputBmp); //Durch alle Blobs (=gefunden Kartenpositionen) iterieren foreach (Blob blob in blobs) { //Eckpunkte des Spielkarten-Blobs in Liste schreiben List<IntPoint> edgePoints = extractor.GetBlobsEdgePoints(blob); //Spielkarten-Ecken finden List<IntPoint> corners = PointsCloud.FindQuadrilateralCorners(edgePoints); //Spielkarte in Rechteck tranformieren/entzerren QuadrilateralTransformation quadTransformer = new QuadrilateralTransformation(); quadTransformer.SourceQuadrilateral = corners; quadTransformer.AutomaticSizeCalculaton = true; Bitmap tempImg = quadTransformer.Apply(inputBmp); //Wenn die Spielkarte auf der Seite liegt, Karte drehen/aufrichten if (tempImg.Width > tempImg.Height) tempImg.RotateFlip(RotateFlipType.Rotate270FlipNone); //Spielkarte auf Standardmaße strecken, damit die nachfolgenden //Koordinaten auf alle Spielkarten-Grafiken anwendbar sind ResizeBilinear resizer = new ResizeBilinear(200, 300); tempImg = resizer.Apply(tempImg); //Neue Spielkarten-Objekt anlegen und die Spielkarten-Grafik hinzufügen var card = new Card() { cardImg = tempImg };
Nun haben wir den ersten Zuschnitt einer Spielkarte, den wir jetzt tiefer analysieren können. Im ersten Schritt wollen wir den Wert der Spielkarte (z.B. 2, 6 ,J ,K , Ass) ermitteln. Hierzu fertigen wir wieder einen kleineren Zuschnitt der Karte an, in dem sich der Wert befindet.
Danach vergleichen wir den Zuschnitt per ExhaustiveTemplateMatching mit allen möglichen Kartenwerten. Dies Samples zum Vergleich haben ich zuvor aus den Screenshots extrahiert und als Ressource im Programm hinterlegt.
Bei jedem Abgleich überprüfen wir, ob der Ähnlichkeitsfaktor größer als der bisher größte Treffer ist. Wenn dem so ist, nehmen wir den Wert als vermutlichen Treffer an.
//Bildauschnitt mit dem Kartenwert erzeugen Crop cropValue = new Crop(new Rectangle(0, 0, 70, 60)); Bitmap tempValueImg = cropValue.Apply(tempImg); //Wert-Ausschnitt nach S/W konvertieren tempValueImg = seq.Apply(tempValueImg); //Wert-Ausschnitt im Karten-Objekt ablegen card.identifierValueImg = tempValueImg; //Liste mit Wert-Samples erstellen List<Identifier> valueSamples = new List<Identifier>() { new Identifier(){ name = "2", sample = KartenDetektor.Properties.Resources._2 }, new Identifier(){ name = "3", sample = KartenDetektor.Properties.Resources._3 }, new Identifier(){ name = "4", sample = KartenDetektor.Properties.Resources._4 }, new Identifier(){ name = "5", sample = KartenDetektor.Properties.Resources._5 }, new Identifier(){ name = "6", sample = KartenDetektor.Properties.Resources._6 }, new Identifier(){ name = "7", sample = KartenDetektor.Properties.Resources._7 }, new Identifier(){ name = "8", sample = KartenDetektor.Properties.Resources._8 }, new Identifier(){ name = "9", sample = KartenDetektor.Properties.Resources._9 }, new Identifier(){ name = "10", sample = KartenDetektor.Properties.Resources._10 }, new Identifier(){ name = "Bube", sample = KartenDetektor.Properties.Resources.j }, new Identifier(){ name = "Dame", sample = KartenDetektor.Properties.Resources.q }, new Identifier(){ name = "König", sample = KartenDetektor.Properties.Resources.k }, new Identifier(){ name = "Ass", sample = KartenDetektor.Properties.Resources._as } }; //Alle Samples auf den Wert-Ausschnitt anwenden float maxSimilar = 0f; foreach (var valueSample in valueSamples) { //Ähnlichkeit zwischen Sample und Wert-Ausschnitt ermitteln ExhaustiveTemplateMatching tm = new ExhaustiveTemplateMatching(0); TemplateMatch[] matchings = tm.ProcessImage(card.identifierValueImg, valueSample.sample); //Wenn das aktuell getestete Sample besser passt, als das bisher beste, //setze den Wert als identifizierten Kartenwert if (matchings.Length > 0 && matchings[0].Similarity > maxSimilar) { maxSimilar = matchings[0].Similarity; card.value = valueSample.name; } }
Nun haben wir den Spielkarten-Zuschnitt, den Wert-Zuschnitt und den ermittelten Wert in unserer card-Instanz gespeichert.
Im nächsten Schritt geht es um die Ermittlung der Kartenfarbe (engl. “suit”). Theoretisch verläuft die Wertermittlung analog zur Analyse des Kartenwerts. Der einzige Unterschied ist, dass wir den Zuschnitt als auch das Sample mit der Funktion normalizeForEqualityCheck() zuvor noch exakter zuschneiden und auf eine Normgröße bringen, damit die Trefferquote beim Abgleich besser wird. (Der Code für die normalizeForEqualityCheck-Funktion befindet sich im Abschnitt “Hilfsklassen und Strukturen” dieses Artikels.)
Die “Normalisierung” ist bei den Kartenfarben nötig, da sich die Bilder im Gegenteil zu den Werten stark ähneln. Zudem ist die Auflösung der Flash-Applikation des Online Casino’s nicht allzu hoch, sodass die Ausschnitte nur eine sehr geringe Auflösung haben. Beide Faktoren erschweren den Vergleich per Sample, sodass die Samples möglichst genau zugeschnitten werden müssen.
//Bildauschnitt mit der Kartenfarbe erzeugen Crop cropSuit = new Crop(new Rectangle(0, 60, 70, 65)); Bitmap tempSuitImg = cropSuit.Apply(tempImg); //Kartenfarben-Ausschnitt nach S/W konvertieren tempSuitImg = seq.Apply(tempSuitImg); //Kartenfarben-Ausschnitt exakt zuschneiden und normalisieren tempSuitImg = normalizeForEqualityCheck(tempSuitImg); //Kartenfarben-Ausschnitt im Karten-Objekt ablegen card.identifierSuitImg = tempSuitImg; //Liste mit Kartenfarben-Samples erstellen List<Identifier> suitSamples = new List<Identifier>() { new Identifier(){ name = "Karo", sample = KartenDetektor.Properties.Resources.diamonds }, new Identifier(){ name = "Herz", sample = KartenDetektor.Properties.Resources.hearts }, new Identifier(){ name = "Pik", sample = KartenDetektor.Properties.Resources.spades }, new Identifier(){ name = "Kreuz", sample = KartenDetektor.Properties.Resources.clubs } }; //Alle Samples auf den Kartenfarben-Ausschnitt anwenden maxSimilar = 0f; foreach (var suitSample in suitSamples) { //Ähnlichkeit zwischen Sample und Kartenfarben-Ausschnitt ermitteln ExhaustiveTemplateMatching tm = new ExhaustiveTemplateMatching(0); //Kartenfarben-Sample exakt zuschneiden, normalisieren und mit Ausschnitt abgleichen TemplateMatch[] matchings = tm.ProcessImage(card.identifierSuitImg, normalizeForEqualityCheck(suitSample.sample)); //Wenn das aktuell getestete Sample besser passt, als das bisher beste, //setze die Farbe als identifizierte Kartenfarbe if (matchings.Length > 0 && matchings[0].Similarity > maxSimilar) { maxSimilar = matchings[0].Similarity; card.suit = suitSample.name; } }
Nun haben wir unsere erste Karte analysiert. Wir haben sowohl den Grafikausschnitt, der die Karte darstellt als auch die Kartenfarbe sowie den Kartenwert im card-Objekt hinterlegt.
Nun fügen wir die fertig analysierte Karte der Kartenliste hinzu und zeichnen (nur der Übersicht halber) die Kartenkonturen im Ausgangs-Screenshot ein.
//Spielkarte der Spielkartenliste hinzufügen cards.Add(card); //Karte zur Visualisierung auf dem Screenshot markieren foreach (var c in edgePoints) { gfx.DrawRectangle(Pens.Red, new Rectangle(c.X, c.Y, 1, 1)); } }
Der Analyse-Teil ist hiermit abgeschlossen. Nach dem Einzeichen, wird der nächste Blob/die nächste Karte analysiert.
Sind alle Karten analysiert, werden die Ergebnisse in den Picture- als auch TextBoxen ausgegeben.
//Kartenfarben-Ausschnitte in PictureBoxen ausgeben //und ermittelte Farben in Textboxen anzeigen pictureBoxSuit1.BackgroundImage = cards[0].identifierSuitImg; textBoxSuit1.Text = cards[0].suit; pictureBoxSuit2.BackgroundImage = cards[1].identifierSuitImg; textBoxSuit2.Text = cards[1].suit; pictureBoxSuit3.BackgroundImage = cards[2].identifierSuitImg; textBoxSuit3.Text = cards[2].suit; pictureBoxSuit4.BackgroundImage = cards[3].identifierSuitImg; textBoxSuit4.Text = cards[3].suit; pictureBoxSuit5.BackgroundImage = cards[4].identifierSuitImg; textBoxSuit5.Text = cards[4].suit; //Kartenwerte-Ausschnitte in PictureBoxen ausgeben //und ermittelte Werte in Textboxen anzeigen pictureBoxValue1.BackgroundImage = cards[0].identifierValueImg; textBoxValue1.Text = cards[0].value; pictureBoxValue2.BackgroundImage = cards[1].identifierValueImg; textBoxValue2.Text = cards[1].value; pictureBoxValue3.BackgroundImage = cards[2].identifierValueImg; textBoxValue3.Text = cards[2].value; pictureBoxValue4.BackgroundImage = cards[3].identifierValueImg; textBoxValue4.Text = cards[3].value; pictureBoxValue5.BackgroundImage = cards[4].identifierValueImg; textBoxValue5.Text = cards[4].value; //Karten-Grafiken in PictureBoxen ausgeben //und ermittelte Farben & Werte in Textboxen anzeigen pictureBoxCard1.BackgroundImage = cards[0].cardImg; textBoxCard1.Text = cards[0].suit + "-" + cards[0].value; pictureBoxCard2.BackgroundImage = cards[1].cardImg; textBoxCard2.Text = cards[1].suit + "-" + cards[1].value; pictureBoxCard3.BackgroundImage = cards[2].cardImg; textBoxCard3.Text = cards[2].suit + "-" + cards[2].value; pictureBoxCard4.BackgroundImage = cards[3].cardImg; textBoxCard4.Text = cards[3].suit + "-" + cards[3].value; pictureBoxCard5.BackgroundImage = cards[4].cardImg; textBoxCard5.Text = cards[4].suit + "-" + cards[4].value; //Screenshot mit eingezeichneten Karten und Arbeitskopie anzeigen pictureBoxScreenshot.BackgroundImage = inputBmp; pictureBoxScreenshotProcessed.BackgroundImage = processedBmp; }
Nun sind wir mit der Analysefunktion fertig. Das letzte Stückchen Code ist für den ClickEvent-Handler, der den nächsten Screenshot laden soll. Im Prinzip gleicht er dem Code aus dem FormLoad-Event.
private void button1_Click(object sender, EventArgs e) { //Solange noch Screenshots vorhanden sind... if (files.Count() > 0) { //...lade Screenshot in Bitmap und starte Analyse Bitmap inputBmp = (Bitmap)Bitmap.FromFile(files[0]); analyzeCards(inputBmp); files.RemoveAt(0); } else { //Keine Screenshots mehr in der Liste MessageBox.Show("Alle Samples durch."); } }
Allen, die sich bis hierhin erfolgreich durchgekämpft haben, wünsche ich schon mal herzlichen Glückwünsch und sage: “Hut ab!”.
Download: Karten Detektor Visual Studio Projekt
Wer zwischenzeitlich ausgestiegen ist, kann sich das komplette Projekt über oben-stehenden Link herunterladen und nachforschen, wo es klemmt.
Fazit und Ausblick
Wie schon im letzten Artikel gezeigt ist das Themenfeld der Kartenspiele wunderbar für diverse Programmierübungen geeignet.
Wer nach dem Artikel nun Lust auf mehr verspürt, dem seien folgende Anregungen zur Erweiterung des Programms gegeben.
- Analysieren von mehr als 5 Karten
- Kombinieren dieses Programms mit der Gewinnwahrscheinlichkeitsberechnung aus dem letzten Artikel hier im Blog
- Verschönerung des GUIs
Wenn ihr weitere Ideen, Kritik oder (gerne auch) Lob habt, dann lasst es mich wissen und schreibt einen Kommentar. Ich freue mich auf euer Feedback!
The Zune concentrates on being a Portable Media Player. Not a web browser. Not a game machine. If you’re still on the fence: grab your favorite earphones, head down to a Best Buy and ask to plug them into a Zune then an iPod and see which one sounds better to you
Hallo, toller Artikel. Vielen Dank dafür. Hast Du zufällig Erfahrungen mit machinellem Lernen / Sehen im Java-Umfeld? Kannte bisher nur openCV, und das auch nur aus der Ferne… AForge erscheint mir ja doch recht umgänglich… wir benötigen aktuell für ein Studi-Projekt auch etwas in der Richtung… die Plattform, für die wir entwickeln, bietet jedoch nur Java an, auch zum Ansprechen der Hardware (zumindest ohne größeren Aufwand vorzunehmen)
Hallo Julian,
du könntest dir das Catalano Framework ansehen. Das ist ein Spin-Off von AForge in Java. (Projektseite: https://code.google.com/p/catalano-framework/ )
Da ich jedoch selbst nur selten in Java programmiere, habe ich noch keine Erfahrung mit Catalano. Solltest du es testen, würde ich mich freuen, wenn du ein kurzes Feedback da lässt, ob es geklappt hat und wie schwer die Umsetzung war.
Viele Grüße
Hallo!
wir haben uns mittlerweile für JAI (Java Advanced Imaging, http://www.oracle.com/technetwork/articles/javaee/jai-142803.html) entschieden – da bestanden schon innerhalb der Projektgruppe Kenntnisse. Werde mir trotzdem bei Gelegenheit mal Catalano anschauen.
Weiter so mit deinem Blog, ist prima!
Viele Grüße!
Danke für den super ausführlichen Guide. Habe auch mal versucht so einen Tool mit angebauter Statistik zu bauen bin aber kläglich gescheitert. Werde es mal mit deiner Anleitung probieren.