/// <summary> /// Initilize a new instance of the CardImage class /// </summary> /// <param name="image">Card Image</param> /// <param name="cardTitleImage">Card Title Image (Sub Image of image)</param> public CardImage(Guid cardId, Mat image, CardTitleImage cardTitleImage) : base(image) { _cardId = cardId; _cardTitleImage = cardTitleImage; _angle = cardTitleImage.Angle; _cardFrame = cardTitleImage.CardFrame; }
/// <summary> /// Initilize a new instance of the CardTitleImage class /// </summary> /// <param name="image">Image</param> internal CardTitleImage(Guid cardId, Guid cardTitleId, Mat image, MTGCardFrame cardTitleType, IEnumerable <LevenshteinResults> levenshteinResults, double angle) : base(image) { _cardId = cardId; _cardTitleId = cardTitleId; _cardFrame = cardTitleType; _angle = angle; _levenshteinResults = levenshteinResults.ToArray(); }
internal static Rectangle BoundingRectangle(MTGCardFrame mcf, Size cardSize) { return(new Rectangle( (int)(LeftMargin[mcf] * cardSize.Width), (int)(TopMargin[mcf] * cardSize.Height), (int)((RightMargin[mcf] - LeftMargin[mcf]) * cardSize.Width), (int)((BottomMargin[mcf] - TopMargin[mcf]) * cardSize.Height))); }
/// <summary> /// Get a filtered image that is good for OCR /// </summary> public Mat GetFilteredImage(MTGCardFrame cardTitleType, double angle, ThresholdParm thresoldParameter, CannyParam cannyParameter) { if (_filteredImage.image == null || _filteredImage.cannyParam != cannyParameter || _filteredImage.angle != angle) { using (Mat titleImage = GetImage()) { Size titleSize = titleImage.Size; int minX = titleImage.Width; int maxX = titleImage.Height; int minY = 0; int maxY = 0; // Get the Canny Image To Make A Contour Set For the Letters using (Mat titleCannyImage = GetCannyImage(() => { return(GetThresholdImage(angle, thresoldParameter)); }, angle, cannyParameter)) { using (Mat maskedImage = new Mat(titleSize, DepthType.Cv8U, 1)) { // White/black out the mask. Legacy cards are white letters on color, the new style is black letters in a colored title box. maskedImage.SetTo(cardTitleType == MTGCardFrame.M15 ? black : white); using (VectorOfVectorOfPoint contours = new VectorOfVectorOfPoint()) { CvInvoke.FindContours(titleCannyImage, contours, hierarchy: null, mode: retrType, method: chainApproxMethod); for (int i = 1; i < contours.Size; i++) { using (VectorOfPoint contour = contours[i]) { Rectangle rect = CvInvoke.BoundingRectangle(contour); double relativeY = (double)rect.Y / (double)titleSize.Height; double relativeX = (double)rect.X / (double)titleSize.Width; double relativeBottom = ((double)rect.Y + (double)rect.Height) / (double)titleSize.Height; double relativeRight = ((double)rect.X + (double)rect.Width) / (double)titleSize.Width; double relativeWidth = (double)rect.Width / (double)titleSize.Width; double relativeHeight = (double)rect.Height / (double)titleSize.Height; // The Characters Start In the 10 % of the Title Image, and Complete Within the if (((relativeY > .10) && (relativeY < .80) && (relativeBottom > .30) && (relativeBottom < .95) && (relativeRight > .025) && (relativeX < .95)) && (relativeWidth < .09) && (relativeHeight > .07)) { // Make a mask of rectangles that represent the title. CvInvoke.Rectangle(maskedImage, rect, new MCvScalar(255, 255, 255), -1); // Keep Track Of the Border Area Which Comprises The Letters minX = Math.Min(rect.X, minX); minY = Math.Min(rect.Y, minY); maxX = Math.Max(rect.X + rect.Width, maxX); maxY = Math.Max(rect.Y + rect.Height, maxY); } } } } // The result is the same depth/channels as the thresold using (Mat result = GetThresholdImage(angle, thresoldParameter)) { // Clear the result result.SetTo(cardTitleType == MTGCardFrame.M15 ? black : white); // Use the Mask To Take Just The Title From the thresoldImage GetThresholdImage(angle, thresoldParameter).CopyTo(result, maskedImage); _filteredImage.image = result.Clone(); _filteredImage.cannyParam = cannyParameter; _filteredImage.angle = angle; } } } } } return(_filteredImage.image.Clone()); }
//public IEnumerable<Mat> FindSetIcons() //{ // List<Mat> results = new List<Mat>(); // using (var contours = new VectorOfVectorOfPoint()) // { // foreach (var angle in CardImage.IconAngles()) // { // foreach (var cannyParameter in CardImage.CannyIconParameters()) // { // Debug.WriteLine("Find Set Icon Image Angle: {0} Canny: {1} ...", angle, cannyParameter); // using (Mat cannyImage = GetCannyImage(() => { return GetGreyImage(angle); }, angle, cannyParameter)) // { // // Find All the Contours // CvInvoke.FindContours(cannyImage, contours, hierarchy: null, mode: RetrType.List, method: ChainApproxMethod.ChainApproxNone); // List<Rectangle> rectangleList = new List<Rectangle>(); // // Create a List of Rectangles From the Contours // for (int idx = 0; idx < contours.Size; idx++) // { // Rectangle rectangle = CvInvoke.MinAreaRect(contours[idx]).MinAreaRect(); // rectangleList.Add(rectangle); // } // // Only add those which fall in the Icon "Zone" // rectangleList = rectangleList.Where(r => CardExpansionSymbolFilter.ExpansionSymbolFilterPass1(_cardFrame, cannyImage.Size, r)).ToList(); // // Output Contoured Image For Debugging // this.FireImageEvent(null, _cardId, _cardId, ImageType.CardContoured, angle: angle, X: 0, Y: 0, // rectangles: rectangleList, color: blue, thickness: 1, cannyParameter: cannyParameter, postFunc: new Action<Mat>((image) => // { // // After Drawing the Rectangles, Add The Margin Lines // CvInvoke.Line(image, new Point((int)(image.Width * CardExpansionSymbolFilter.LeftMargin[_cardFrame]), 0), // new Point((int)(image.Width * CardExpansionSymbolFilter.LeftMargin[_cardFrame]), image.Height), white, thickness: 1); // CvInvoke.Line(image, new Point((int)(image.Width * CardExpansionSymbolFilter.RightMargin[_cardFrame]), 0), // new Point((int)(image.Width * CardExpansionSymbolFilter.RightMargin[_cardFrame]), image.Height), white, thickness: 1); // CvInvoke.Line(image, new Point(0, (int)(image.Height * CardExpansionSymbolFilter.TopMargin[_cardFrame])), // new Point(image.Width, (int)(image.Height * CardExpansionSymbolFilter.TopMargin[_cardFrame])), white, thickness: 1); // CvInvoke.Line(image, new Point(0, (int)(image.Height * CardExpansionSymbolFilter.BottomMargin[_cardFrame])), // new Point(image.Width, (int)(image.Height * CardExpansionSymbolFilter.BottomMargin[_cardFrame])), white, thickness: 1); // })); // // Merge all possible rectangles that could be icons together // var intersectedRectangles = MergedRectangles(rectangleList.ToArray()) // .Distinct() // .Except(rectangleList); // rectangleList.AddRange(intersectedRectangles); // // Make a second pass at filtering to reduce the rectangles into only those // // that could possibly be icons based on aspect ratio, size, etc... // rectangleList = rectangleList.Where(r => CardExpansionSymbolFilter.ExpansionSymbolFilterPass2(_cardFrame, cannyImage.Size, r)).ToList(); // // Sort Each Rectangle From Biggest To Smallest // rectangleList.Sort((r1, r2) => // { // float area1 = r1.Width * r1.Height; // float area2 = r2.Width * r2.Height; // return area2.CompareTo(area1); // }); // // Skip mat creation if there are no rectangles // if (rectangleList.Count > 0) // { // using (Mat image = GetGreyImage(angle)) // { // foreach (Rectangle rectangle in rectangleList) // { // using (Mat cropped = new Mat(image, rectangle)) // { // // Output cropped image for debugging. // Image.FireImageEvent(this, _cardId, _cardId, ImageType.SetIcon, // cropped, angle: angle, X: rectangle.X + (rectangle.Width / 2), Y: rectangle.Y + (rectangle.Height / 2), // cannyParameter: cannyParameter); // results.Add(cropped.Clone()); // } // } // } // } // } // } // } // } // // Sort to biggest first, this make sure FirstDefault() take the most likely one // results.Sort((m1, m2) => // { // float area1 = m1.Size.Height * m1.Size.Width; // float area2 = m2.Size.Height * m2.Size.Width; // return area2.CompareTo(area1); // }); // return results; //} /// <summary> /// Try To Find The Title In the Image /// </summary> /// <param name="cardImage">Image</param> /// <param name="countor">Contour To Test</param> /// <param name="result">Resulting Title Image</param> /// <returns>True If Title Is Found</returns> private static bool TryFindTitle(Guid cardId, Guid cardTitleId, Mat cardImage, double angle, CannyParam cannyParameter, VectorOfPoint countor, out Mat result, out MTGCardFrame cardTitleType) { result = null; cardTitleType = MTGCardFrame.M15; RotatedRect rotatedRect = CvInvoke.MinAreaRect(countor); // Prevent Divide By Zero if (rotatedRect.Size.Height == 0) { return(false); } float width = rotatedRect.Size.Width; float height = rotatedRect.Size.Height; float heightRatio = rotatedRect.Size.Height / (float)cardImage.Size.Height; float relativeCenterY = rotatedRect.Center.Y / (float)cardImage.Size.Height; Rectangle box = rotatedRect.MinAreaRect(); // Prevent Divide By Zero if (box.Size.Width == 0) { return(false); } float widthRatio = (float)box.Size.Width / (float)cardImage.Size.Width; float aspectRatio = (float)box.Size.Height / (float)box.Size.Width; float area = (float)box.Size.Height * (float)box.Size.Width; float imageArea = (float)cardImage.Size.Height * (float)cardImage.Size.Width; float relativeArea = area / imageArea; // Title bar should have a height if (height < 1.0F) { return(false); } // Box Should Be Inside the Image if ((box.Y < 0) || (box.X < 0) || ((box.X + box.Width) > cardImage.Size.Width) || ((box.Y + box.Height) > cardImage.Size.Height)) { return(false); } // Name bar should center in the top 15% of the image, this is the new style cards with the name "boxed" in the image if (relativeCenterY < .15F) { // Title Bar Should Be Wider Than 80% of the Image Width if (widthRatio < .80F) { return(false); } using (Mat cropped = new Mat(cardImage, box)) { Image.FireImageEvent(null, cardId, cardId, ImageType.TitleCropped, cropped, angle: angle, X: box.X, Y: box.Y, cannyParameter: cannyParameter); } // Title Bar Should Be 6% of the Card Height if (heightRatio < .048 || heightRatio > .077) { return(false); } Debug.WriteLine("Title Contour ({14}) - Center: {0}/{1} Relative Center: ({9}%)/({10}%) Width: {2} ({11}%) Height: {3} ({12}%) Area: {4} ({13}%) : AspectRatio: {5}, Angle: {6} Image Size: {7}/{8}", rotatedRect.Center.X, rotatedRect.Center.Y, rotatedRect.Size.Width, rotatedRect.Size.Height, area, aspectRatio, rotatedRect.Angle, cardImage.Size.Width, cardImage.Size.Height, (rotatedRect.Center.X / cardImage.Size.Width) * 100.0, relativeCenterY * 100.0, widthRatio * 100.0, heightRatio * 100.0, relativeArea * 100.0, cardTitleId); using (Mat cropped = new Mat(cardImage, box)) { Image.FireImageEvent(null, cardId, cardTitleId, ImageType.TitleCropped, cropped, angle: angle, X: box.X, Y: box.Y, cannyParameter: cannyParameter); result = cropped.Clone(); return(true); } } else if (relativeCenterY < .50F) { // Assume that this card is the older style card with the name above the photo, but not "boxed" // cardTitleType = MTGCardFrame.Original; // Using the Aspect Ratio Find the Art Box if (aspectRatio < .75F || aspectRatio > .85F) { return(false); } // The art relative area to the card should be above 35% if (relativeArea < .35F) { return(false); } Debug.WriteLine("Title Contour ({14}) - Center: {0}/{1} Relative Center: ({9}%)/({10}%) Width: {2} ({11}%) Height: {3} ({12}%) Area: {4} ({13}%) : AspectRatio: {5}, Angle: {6} Image Size: {7}/{8}", rotatedRect.Center.X, rotatedRect.Center.Y, rotatedRect.Size.Width, rotatedRect.Size.Height, area, aspectRatio, rotatedRect.Angle, cardImage.Size.Width, cardImage.Size.Height, (rotatedRect.Center.X / cardImage.Size.Width) * 100.0, relativeCenterY * 100.0, widthRatio * 100.0, heightRatio * 100.0, relativeArea * 100.0, cardTitleId); int borderHeight = (int)((double)box.Y * .45); // Create a box that is as wide as the art, and directly above the art to // the top of the card Rectangle titleBox = new Rectangle(box.X, borderHeight, box.Width, box.Y - borderHeight); heightRatio = titleBox.Size.Height / (float)cardImage.Size.Height; // Title Bar Should Be 6% of the Card Height if (heightRatio < .050 || heightRatio > .065) { return(false); } using (Mat cropped = new Mat(cardImage, titleBox)) { Image.FireImageEvent(null, cardId, cardId, ImageType.TitleCropped, cropped, angle: angle, X: titleBox.X, Y: titleBox.Y, cannyParameter: cannyParameter); result = cropped.Clone(); return(true); } } return(false); }
/// <summary> /// Initilize a new instance of the CardImage class /// </summary> public CardImage(Guid cardId, Mat image, double angle, MTGCardFrame cardFrame) : base(image) { _cardId = cardId; _angle = angle; _cardFrame = cardFrame; }
internal static IEnumerable <LevenshteinResults> GetLowLevenshteinDistanceCards(Guid cardId, Guid cardTitleId, float X, float Y, MTGCardFrame cardTitleType, Image image) { List <LevenshteinResults> results = new List <LevenshteinResults>(); foreach (var thresholdParameter in GetThresholdParamters()) { foreach (var cannyParameter in GetCannyParameters()) { using (var filteredImage = image.GetFilteredImage(cardTitleType, angle: 0, thresoldParameter: thresholdParameter, cannyParameter: cannyParameter)) { Image.FireImageEvent(null, cardId, cardTitleId, ImageType.TitleFiltered, filteredImage, angle: 0, X: X, Y: Y, cannyParameter: cannyParameter, thresholdParmeter: thresholdParameter); { var lldc = GetLowLevenshteinDistanceCards(filteredImage); // Short Circuit if (lldc.Any(lr => lr.Distance < LevenshteinDistanceShortCircuit)) { return(lldc.Where(lr => lr.Distance < LevenshteinDistanceShortCircuit)); } results.AddRange(lldc); } using (Mat erodeImage = new Mat()) { CvInvoke.Erode(filteredImage, erodeImage, null, new Point(-1, -1), 1, BorderType.Constant, cardTitleType == MTGCardFrame.M15 ? white : black); Image.FireImageEvent(null, cardId, cardTitleId, ImageType.TitleErode, erodeImage, angle: 0, X: 0, Y: 0, cannyParameter: cannyParameter, thresholdParmeter: thresholdParameter); var lldc = GetLowLevenshteinDistanceCards(erodeImage); // Short Circuit if (lldc.Any(lr => lr.Distance < LevenshteinDistanceShortCircuit)) { return(lldc.Where(lr => lr.Distance < LevenshteinDistanceShortCircuit)); } results.AddRange(lldc); } using (Mat dilateImage = new Mat()) { CvInvoke.Dilate(filteredImage, dilateImage, null, new Point(-1, -1), 1, BorderType.Constant, cardTitleType == MTGCardFrame.M15 ? white : black); Image.FireImageEvent(null, cardId, cardTitleId, ImageType.TitleDilate, dilateImage, angle: 0, X: 0, Y: 0, cannyParameter: cannyParameter, thresholdParmeter: thresholdParameter); var lldc = GetLowLevenshteinDistanceCards(dilateImage); // Short Circuit if (lldc.Any(lr => lr.Distance < LevenshteinDistanceShortCircuit)) { return(lldc.Where(lr => lr.Distance < LevenshteinDistanceShortCircuit)); } results.AddRange(lldc); } } } } // Find all cards with the minimum number of character changes (the result of the Levenshtein Distance) // alogorithm and return them double min = results.Min(result => result.PercentDistance); return(results.Where(result => result.PercentDistance == min).Distinct()); }