/// <summary> /// Search the Card Image For The Title And Result It As a Wrapped Class (CardTitleImage) /// </summary> internal static CardTitleImage FindBestTitleImage(Guid cardId, Image cardImage, float X, float Y, IEnumerable <CannyParam> cannyParameters, IEnumerable <double> angles) { List <CardTitleImage> potentialCardTitleImages = new List <CardTitleImage>(); ContourCache contourCache = new ContourCache(); using (var contours = new VectorOfVectorOfPoint()) { foreach (var angle in angles) { foreach (var cannyParameter in cannyParameters) { Debug.WriteLine(string.Format("Find Title Image Canny {0} Angle {1}...", cannyParameter, angle)); using (Mat cannyImage = cardImage.GetCannyImage(() => { return(cardImage.GetGreyImage(angle)); }, angle, cannyParameter)) { CvInvoke.FindContours(cannyImage, contours, hierarchy: null, mode: RetrType.List, method: ChainApproxMethod.ChainApproxNone); var sortedContours = new List <VectorOfPoint>(); for (int idx = 0; idx < contours.Size; idx++) { sortedContours.Add(contours[idx]); } double optimalAspectRatio = ((MaxTitleAspectRatio - MinTitleAspectRatio) / 2.0) + MinTitleAspectRatio; // Sort Each Contour By Delta From the Optiomal Aspect Ratio, Smallest First sortedContours.Sort((c1, c2) => { RotatedRect rotatedRect1 = CvInvoke.MinAreaRect(c1); double aspectRatio1 = (double)(rotatedRect1.Size.Height / rotatedRect1.Size.Width); RotatedRect rotatedRect2 = CvInvoke.MinAreaRect(c2); double aspectRatio2 = (double)(rotatedRect2.Size.Height / rotatedRect2.Size.Width); double diff1 = Math.Abs(aspectRatio1 - optimalAspectRatio); double diff2 = Math.Abs(aspectRatio2 - optimalAspectRatio); return(diff1.CompareTo(diff2)); }); using (Mat rotatedImage = cardImage.GetRotatedImage(angle)) { cardImage.FireImageEvent(null, cardId, cardId, ImageType.CardContoured, angle: angle, X: X, Y: Y, contours: contours, cannyParameter: cannyParameter); // Find The Contour That Matchs What A Title Should Look Like foreach (VectorOfPoint countour in sortedContours) { // Keep a list of contours that have been tested and don't test those again // that allows us to go through a bunch of test thresholds to determine // if they will yield new contours. if (contourCache.Contains(countour, angle)) { continue; } // Store all the rotated rects that have been examined contourCache.Add(countour, angle); Mat result = null; MTGCardFrame cardTitleType; try { Guid cardTitleId = Guid.NewGuid(); if (TryFindTitle(cardId, cardTitleId, rotatedImage, angle, cannyParameter, countour, result: out result, cardTitleType: out cardTitleType)) { // Wrap the Mat Found Image image = new Image(result); // Get the MTG card with the minimum Levenshtein distance for the text parsed // from the image, this will be the closet MTG card to the image var levenshteinDistanceResults = CardTitleImage.GetLowLevenshteinDistanceCards(cardId, cardTitleId, X, Y, cardTitleType, image); // Create a class from the card title image, the card title type, and the minimum levenshtien distance cards CardTitleImage cardTitleImage = new CardTitleImage(cardId, cardTitleId, result, cardTitleType, levenshteinDistanceResults, angle); // Direct Match Short Circuit if (levenshteinDistanceResults.Any(ldr => ldr.Distance < LevenshteinDistanceShortCircuit)) { return(cardTitleImage); } // Add it to the potential card titles potentialCardTitleImages.Add(cardTitleImage); } } finally { if (result != null) { result.Dispose(); } } } } } } } } if (potentialCardTitleImages.Count == 0) { return(null); } // Find The minimum number of character changes (the result of the Levenshtein Distance) alogorithm double minLevenshteinResults = potentialCardTitleImages.Min(cti => cti.LevenshteinResults.Min(lr => lr.PercentDistance)); // Find all unique cards that have this distance var potentialCardNames = potentialCardTitleImages.SelectMany(cti => cti.LevenshteinResults) .Where(lr => lr.PercentDistance == minLevenshteinResults) .Select(lr => lr.Card.Name) .Distinct() .ToArray(); if (potentialCardNames.Length == 0) { // There are no cards that match the image return(null); } else if (potentialCardNames.Length > 1) { // Inconclusive Results // There is more than one card with a low Levenshtein score that matches the images presented. return(null); } else { // Winner Winner Chicken Dinner string name = potentialCardNames[0]; return(potentialCardTitleImages.FirstOrDefault(cti => cti.LevenshteinResults.Any(lr => lr.Card.Name.Equals(name)))); } }
/// <summary> /// Find All Cards In Field /// </summary> /// <returns>List of Cards</returns> public IEnumerable <CardImage> FindCards(Guid cardId) { ContourCache contourCache = new ContourCache(); // Before contouring field image for cards, try using the whole field image // assuming it is a card using (Mat image = GetImage()) { float aspectRatio = (float)image.Width / (float)image.Height; // Find the Card Aspect Raito if ((aspectRatio >= MinCardAspectRatio) && (aspectRatio <= MaxCardAspectRatio)) { Image.FireImageEvent(null, cardId, cardId, ImageType.CardCropped, image, angle: 0, X: image.Size.Width / 2.0F, Y: image.Size.Height / 2.0F); var cardTitleImage = CardImage.FindBestTitleImage(cardId, this, X: image.Size.Width / 2.0F, Y: image.Size.Height / 2.0F, cannyParameters: CardImage.CannyParameters(), angles: CardImage.Angles()); if (cardTitleImage != null) { var cardImage = new CardImage(cardId, image, cardTitleImage); yield return(cardImage); } } } // Iterate though canny parameters looking for contours that will yield cards foreach (var cannyParameter in GetCannyParameters()) { FireImageEvent(this, cardId, _fieldId, ImageType.FieldContoured, angle: 0, X: 0, Y: 0, cannyParameter: cannyParameter); Debug.WriteLine("Find Card Image: {0}...", cannyParameter); using (Mat cannyImage = GetCannyImage(() => { return(GetGreyImage(angle: 0)); }, angle: 0, cannyParameter: cannyParameter)) { using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint()) { CvInvoke.FindContours(cannyImage, contours, hierarchy: null, mode: retrType, method: chainApproxMethod); var sortedContours = new List <VectorOfPoint>(); for (int idx = 0; idx < contours.Size; idx++) { sortedContours.Add(contours[idx]); } // Sort Each Contour By Area, Largest First sortedContours.Sort((v1, v2) => { RotatedRect rotatedRect1 = CvInvoke.MinAreaRect(v1); float area1 = rotatedRect1.Size.Width * rotatedRect1.Size.Height; RotatedRect rotatedRect2 = CvInvoke.MinAreaRect(v2); float area2 = rotatedRect2.Size.Width * rotatedRect2.Size.Height; return(area2.CompareTo(area1)); }); // Iterate Each Contour Trying To Determine If It Is a Card foreach (VectorOfPoint countour in sortedContours) { // Keep a list of contours that have been tested and don't test those again // that allows us to quickly go through a bunch of test thresholds to determine // if they will yield new contours. if (contourCache.Contains(countour, angle: 0)) { continue; } // Store all the rotated rects that have been examined contourCache.Add(countour, angle: 0); Mat imageResult = null; try { if (TryFindCard(cardId, this.Size, countour, result: out imageResult)) { RotatedRect rotatedRect1 = CvInvoke.MinAreaRect(countour); Image.FireImageEvent(null, cardId, cardId, ImageType.CardCropped, imageResult, angle: 0, X: rotatedRect1.Center.X, Y: rotatedRect1.Center.Y, cannyParameter: cannyParameter); // The card image (result) is in portrait mode, however it could be upsidedown using (Image image = new Image(imageResult)) { var cardTitleImage = CardImage.FindBestTitleImage(cardId, image, X: rotatedRect1.Center.X, Y: rotatedRect1.Center.Y, cannyParameters: CardImage.CannyParameters(), angles: CardImage.Angles()); if (cardTitleImage != null) { var cardImage = new CardImage(cardId, imageResult, cardTitleImage); yield return(cardImage); } } } } finally { if (imageResult != null) { imageResult.Dispose(); } } } } } } }