From a8536393a0460cd27069b40ead4e81cefd491849 Mon Sep 17 00:00:00 2001 From: Nikolaj Olsson Date: Sun, 31 May 2020 21:35:27 +0200 Subject: [PATCH] Improve "exanded hit" for nOCR + work on casing --- libse/NikseBitmap.cs | 15 +- .../VobSubNOcrCharacterInspect.Designer.cs | 14 ++ src/Forms/Ocr/VobSubNOcrCharacterInspect.cs | 24 ++- src/Forms/Ocr/VobSubNOcrTrain.cs | 4 +- src/Forms/Ocr/VobSubOcr.cs | 65 ++++-- src/Logic/Ocr/NOcrDb.cs | 199 +++++++++++------- 6 files changed, 210 insertions(+), 111 deletions(-) diff --git a/libse/NikseBitmap.cs b/libse/NikseBitmap.cs index e71bc46b5..727855e58 100644 --- a/libse/NikseBitmap.cs +++ b/libse/NikseBitmap.cs @@ -958,22 +958,13 @@ namespace Nikse.SubtitleEdit.Core _bitmapData[_pixelAddress + 3] = color.A; } - public void SetPixelNext(Color color) - { - _pixelAddress += 4; - _bitmapData[_pixelAddress] = color.B; - _bitmapData[_pixelAddress + 1] = color.G; - _bitmapData[_pixelAddress + 2] = color.R; - _bitmapData[_pixelAddress + 3] = color.A; - } - public Bitmap GetBitmap() { var bitmap = new Bitmap(Width, Height, PixelFormat.Format32bppArgb); - var bitmapdata = bitmap.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); - var destination = bitmapdata.Scan0; + var bitmapData = bitmap.LockBits(new Rectangle(0, 0, Width, Height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); + var destination = bitmapData.Scan0; Marshal.Copy(_bitmapData, 0, destination, _bitmapData.Length); - bitmap.UnlockBits(bitmapdata); + bitmap.UnlockBits(bitmapData); return bitmap; } diff --git a/src/Forms/Ocr/VobSubNOcrCharacterInspect.Designer.cs b/src/Forms/Ocr/VobSubNOcrCharacterInspect.Designer.cs index 82502c926..8ffab0cfa 100644 --- a/src/Forms/Ocr/VobSubNOcrCharacterInspect.Designer.cs +++ b/src/Forms/Ocr/VobSubNOcrCharacterInspect.Designer.cs @@ -166,6 +166,7 @@ this.toolStripMenuItem3 = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripMenuItemMusicSymbol1 = new System.Windows.Forms.ToolStripMenuItem(); this.toolStripMenuItemMusicSymbol2 = new System.Windows.Forms.ToolStripMenuItem(); + this.labelStatus = new System.Windows.Forms.Label(); this.groupBoxInspectItems.SuspendLayout(); ((System.ComponentModel.ISupportInitialize)(this.pictureBoxInspectItem)).BeginInit(); this.contextMenuStripAddBetterMultiMatch.SuspendLayout(); @@ -1215,11 +1216,22 @@ this.toolStripMenuItemMusicSymbol2.Size = new System.Drawing.Size(134, 22); this.toolStripMenuItemMusicSymbol2.Text = "♫"; // + // labelStatus + // + this.labelStatus.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Bottom | System.Windows.Forms.AnchorStyles.Left))); + this.labelStatus.AutoSize = true; + this.labelStatus.Location = new System.Drawing.Point(15, 424); + this.labelStatus.Name = "labelStatus"; + this.labelStatus.Size = new System.Drawing.Size(59, 13); + this.labelStatus.TabIndex = 38; + this.labelStatus.Text = "labelStatus"; + // // VobSubNOcrCharacterInspect // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.ClientSize = new System.Drawing.Size(826, 446); + this.Controls.Add(this.labelStatus); this.Controls.Add(this.groupBoxInspectItems); this.Controls.Add(this.groupBoxCurrentCompareImage); this.Controls.Add(this.buttonOK); @@ -1241,6 +1253,7 @@ ((System.ComponentModel.ISupportInitialize)(this.pictureBoxCharacter)).EndInit(); this.contextMenuStripLetters.ResumeLayout(false); this.ResumeLayout(false); + this.PerformLayout(); } @@ -1383,5 +1396,6 @@ private System.Windows.Forms.ToolStripMenuItem toolStripMenuItem3; private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemMusicSymbol1; private System.Windows.Forms.ToolStripMenuItem toolStripMenuItemMusicSymbol2; + private System.Windows.Forms.Label labelStatus; } } \ No newline at end of file diff --git a/src/Forms/Ocr/VobSubNOcrCharacterInspect.cs b/src/Forms/Ocr/VobSubNOcrCharacterInspect.cs index d6ca609ac..a9c2fd0b6 100644 --- a/src/Forms/Ocr/VobSubNOcrCharacterInspect.cs +++ b/src/Forms/Ocr/VobSubNOcrCharacterInspect.cs @@ -27,6 +27,7 @@ namespace Nikse.SubtitleEdit.Forms.Ocr InitializeComponent(); UiUtil.FixFonts(this); labelImageSize.Text = string.Empty; + labelStatus.Text = string.Empty; foreach (ToolStripItem toolStripItem in contextMenuStripLetters.Items) { @@ -95,7 +96,7 @@ namespace Nikse.SubtitleEdit.Forms.Ocr } else { - var match = vobSubOcr.GetNOcrCompareMatchNew(item, nbmp, nOcrDb, false, false); + var match = vobSubOcr.GetNOcrCompareMatchNew(item, nbmp, nOcrDb, false, false, index, _imageList); if (match == null) { _indexLookup.Add(listBoxInspectItems.Items.Count, index); @@ -267,18 +268,30 @@ namespace Nikse.SubtitleEdit.Forms.Ocr { _nocrChar.Text = textBoxText.Text; _nocrChar.Italic = checkBoxItalic.Checked; - _vobSubOcr.SaveNOcrWithCurrentLanguage(); - MessageBox.Show("nOCR saved!"); + ShowStatus("Character updated"); } } + private void ShowStatus(string text) + { + labelStatus.Text = text; + labelStatus.Refresh(); + System.Threading.SynchronizationContext.Current.Post(TimeSpan.FromMilliseconds(1500), () => + { + if (!IsDisposed) + { + labelStatus.Text = string.Empty; + labelStatus.Refresh(); + } + }); + } + private void buttonDelete_Click(object sender, EventArgs e) { if (_nocrChar != null) { _nocrChars.Remove(_nocrChar); - _vobSubOcr.SaveNOcrWithCurrentLanguage(); - MessageBox.Show("nOCR saved!"); + ShowStatus("Character deleted"); } } @@ -352,7 +365,6 @@ namespace Nikse.SubtitleEdit.Forms.Ocr } _nocrChars.Add(vobSubOcrNOcrCharacter.NOcrChar); - _vobSubOcr.SaveNOcrWithCurrentLanguage(); DialogResult = DialogResult.OK; } } diff --git a/src/Forms/Ocr/VobSubNOcrTrain.cs b/src/Forms/Ocr/VobSubNOcrTrain.cs index 9fae1c4d4..abc23f36a 100644 --- a/src/Forms/Ocr/VobSubNOcrTrain.cs +++ b/src/Forms/Ocr/VobSubNOcrTrain.cs @@ -193,7 +193,7 @@ namespace Nikse.SubtitleEdit.Forms.Ocr { // e.g. quote (") var expandItem = VobSubOcr.GetExpandedSelectionNew(nikseBitmap, new List { list[2], list[3] }); - var match = nOcrD.GetMatchExpanded(nikseBitmap, expandItem); + var match = nOcrD.GetMatchExpanded(nikseBitmap, expandItem, 2, list); if (match != null && match.Text == s) { numberOfCharactersSkipped++; @@ -216,7 +216,7 @@ namespace Nikse.SubtitleEdit.Forms.Ocr { // e.g. "%" var expandItem = VobSubOcr.GetExpandedSelectionNew(nikseBitmap, new List { list[2], list[3], list[4] }); - var match = nOcrD.GetMatchExpanded(nikseBitmap, expandItem); + var match = nOcrD.GetMatchExpanded(nikseBitmap, expandItem, 2, list); if (match != null && match.Text == s) { numberOfCharactersSkipped++; diff --git a/src/Forms/Ocr/VobSubOcr.cs b/src/Forms/Ocr/VobSubOcr.cs index 11cffe23c..e3d76b2e1 100644 --- a/src/Forms/Ocr/VobSubOcr.cs +++ b/src/Forms/Ocr/VobSubOcr.cs @@ -2711,35 +2711,65 @@ namespace Nikse.SubtitleEdit.Forms.Ocr } if (result.Text == "V" || result.Text == "W" || result.Text == "U" || result.Text == "S" || - result.Text == "Z" || result.Text == "Ź" || result.Text == "Ż" || result.Text == "O" || result.Text == "Ó" || - result.Text == "X" || result.Text == "Ø" || result.Text == "C" || result.Text == "Ć") + result.Text == "Z" || result.Text == "O" || + result.Text == "X" || result.Text == "Ø" || result.Text == "C") { + var averageLowercase = _binOcrLowercaseHeightsTotal / _binOcrLowercaseHeightsTotalCount; + var averageUppercase = _binOcrUppercaseHeightsTotal / _binOcrUppercaseHeightsTotalCount; if (_binOcrLowercaseHeightsTotalCount > 2 && _binOcrUppercaseHeightsTotalCount > 2 && - Math.Abs(_binOcrLowercaseHeightsTotal / _binOcrLowercaseHeightsTotalCount - targetItem.NikseBitmap.Height) < - Math.Abs(_binOcrUppercaseHeightsTotal / _binOcrUppercaseHeightsTotalCount - targetItem.NikseBitmap.Height)) + Math.Abs(averageLowercase - targetItem.NikseBitmap.Height) < + Math.Abs(averageUppercase - targetItem.NikseBitmap.Height)) { result.Text = result.Text.ToLowerInvariant(); } } else if (result.Text == "v" || result.Text == "w" || result.Text == "u" || result.Text == "s" || - result.Text == "z" || result.Text == "ź" || result.Text == "ż" || result.Text == "ó" || - result.Text == "o" || result.Text == "x" || result.Text == "ø" || result.Text == "c" || result.Text == "ć") + result.Text == "z" || + result.Text == "o" || result.Text == "x" || result.Text == "ø" || result.Text == "c") { + var averageLowercase = _binOcrLowercaseHeightsTotal / _binOcrLowercaseHeightsTotalCount; + var averageUppercase = _binOcrUppercaseHeightsTotal / _binOcrUppercaseHeightsTotalCount; if (_binOcrLowercaseHeightsTotalCount > 2 && _binOcrUppercaseHeightsTotalCount > 2 && - Math.Abs(_binOcrLowercaseHeightsTotal / _binOcrLowercaseHeightsTotalCount - targetItem.NikseBitmap.Height) > - Math.Abs(_binOcrUppercaseHeightsTotal / _binOcrUppercaseHeightsTotalCount - targetItem.NikseBitmap.Height)) + Math.Abs(averageLowercase - targetItem.NikseBitmap.Height) > + Math.Abs(averageUppercase - targetItem.NikseBitmap.Height)) + { + result.Text = result.Text.ToUpperInvariant(); + } + } + + // Polish + else if (result.Text == "Ć" || result.Text == "Ź" || result.Text == "Ż" || result.Text == "Ą" || result.Text == "Ó" || result.Text == "Ś") + { + var averageLowercase = _binOcrLowercaseHeightsTotal / (double)_binOcrLowercaseHeightsTotalCount * 1.5; + var averageUppercase = _binOcrUppercaseHeightsTotal / (double)_binOcrUppercaseHeightsTotalCount * 1.3; + if (_binOcrLowercaseHeightsTotalCount > 2 && + _binOcrUppercaseHeightsTotalCount > 2 && + Math.Abs(averageLowercase - targetItem.NikseBitmap.Height) < + Math.Abs(averageUppercase - targetItem.NikseBitmap.Height)) + { + result.Text = result.Text.ToLowerInvariant(); + } + } + else if (result.Text == "ć" || result.Text == "ź" || result.Text == "ż" || result.Text == "ą" || result.Text == "ó" || result.Text == "ś") + { + var averageLowercase = _binOcrLowercaseHeightsTotal / (double)_binOcrLowercaseHeightsTotalCount * 1.5; + var averageUppercase = _binOcrUppercaseHeightsTotal / (double)_binOcrUppercaseHeightsTotalCount * 1.3; + if (_binOcrLowercaseHeightsTotalCount > 2 && + _binOcrUppercaseHeightsTotalCount > 2 && + Math.Abs(averageLowercase - targetItem.NikseBitmap.Height) > + Math.Abs(averageUppercase - targetItem.NikseBitmap.Height)) { result.Text = result.Text.ToUpperInvariant(); } } } - internal CompareMatch GetNOcrCompareMatchNew(ImageSplitterItem targetItem, NikseBitmap parentBitmap, NOcrDb nOcrDb, bool tryItalicScaling, bool deepSeek) + internal CompareMatch GetNOcrCompareMatchNew(ImageSplitterItem targetItem, NikseBitmap parentBitmap, NOcrDb nOcrDb, bool tryItalicScaling, bool deepSeek, int index, List list) { deepSeek = true; - var expandedResult = nOcrDb.GetMatchExpanded(parentBitmap, targetItem); + var expandedResult = nOcrDb.GetMatchExpanded(parentBitmap, targetItem, index, list); if (expandedResult != null) { return new CompareMatch(expandedResult.Text, expandedResult.Italic, expandedResult.ExpandCount, null, expandedResult) { ImageSplitterItem = targetItem }; @@ -3608,7 +3638,7 @@ namespace Nikse.SubtitleEdit.Forms.Ocr { if (_nOcrDb != null && _nOcrDb.OcrCharacters.Count > 0) { - match = GetNOcrCompareMatchNew(item, parentBitmap, _nOcrDb, true, true); + match = GetNOcrCompareMatchNew(item, parentBitmap, _nOcrDb, true, true, index, list); } } @@ -3904,7 +3934,7 @@ namespace Nikse.SubtitleEdit.Forms.Ocr item.Y += nbmp.CropTopTransparent(0); nbmp.CropTransparentSidesAndBottom(0, true); nbmp.ReplaceTransparentWith(Color.Black); - GetNOcrCompareMatchNew(item, nikseBitmap, _nOcrDb, false, false); + GetNOcrCompareMatchNew(item, nikseBitmap, _nOcrDb, false, false, list.IndexOf(item), list); } } } @@ -3930,7 +3960,7 @@ namespace Nikse.SubtitleEdit.Forms.Ocr } var list = NikseBitmapImageSplitter.SplitBitmapToLettersNew(nbmpInput, (int)numericUpDownNumberOfPixelsIsSpaceNOCR.Value, checkBoxRightToLeft.Checked, Configuration.Settings.VobSubOcr.TopToBottom, minLineHeight); - foreach (ImageSplitterItem item in list) + foreach (var item in list) { item.NikseBitmap?.ReplaceTransparentWith(Color.Black); } @@ -4005,7 +4035,7 @@ namespace Nikse.SubtitleEdit.Forms.Ocr } else { - var match = GetNOcrCompareMatchNew(item, nbmpInput, _nOcrDb, checkBoxNOcrItalic.Checked, !checkBoxNOcrCorrect.Checked); + var match = GetNOcrCompareMatchNew(item, nbmpInput, _nOcrDb, checkBoxNOcrItalic.Checked, !checkBoxNOcrCorrect.Checked, index, list); if (match == null) { _vobSubOcrNOcrCharacter.Initialize(bitmap, item, _manualOcrDialogPosition, _italicCheckedLast, false, string.Empty); @@ -8251,6 +8281,13 @@ namespace Nikse.SubtitleEdit.Forms.Ocr { Cursor = Cursors.WaitCursor; SaveNOcrWithCurrentLanguage(); + _nOcrDb.LoadOcrCharacters(); + Cursor = Cursors.Default; + } + else + { + Cursor = Cursors.WaitCursor; + _nOcrDb.LoadOcrCharacters(); Cursor = Cursors.Default; } diff --git a/src/Logic/Ocr/NOcrDb.cs b/src/Logic/Ocr/NOcrDb.cs index c5a7c4b41..d5ca7df6f 100644 --- a/src/Logic/Ocr/NOcrDb.cs +++ b/src/Logic/Ocr/NOcrDb.cs @@ -87,7 +87,7 @@ namespace Nikse.SubtitleEdit.Logic.Ocr } } - public NOcrChar GetMatchExpanded(NikseBitmap nikseBitmap, ImageSplitterItem targetItem) + public NOcrChar GetMatchExpanded(NikseBitmap nikseBitmap, ImageSplitterItem targetItem, int listIndex, List list) { int w = targetItem.NikseBitmap.Width; for (var i = 0; i < OcrCharactersExpanded.Count; i++) @@ -97,125 +97,131 @@ namespace Nikse.SubtitleEdit.Logic.Ocr { bool ok = true; var index = 0; - - if (true) + while (index < oc.LinesForeground.Count && ok) { - - while (index < oc.LinesForeground.Count && ok) + var op = oc.LinesForeground[index]; + foreach (var point in op.GetPoints()) { - var op = oc.LinesForeground[index]; - foreach (var point in op.GetPoints()) + var p = new Point(point.X + targetItem.X, point.Y + targetItem.Y); + if (p.X >= 0 && p.Y >= 0 && p.X < nikseBitmap.Width && p.Y < nikseBitmap.Height) { - var p = new Point(point.X + targetItem.X, point.Y + targetItem.Y); - if (p.X >= 0 && p.Y >= 0 && p.X < nikseBitmap.Width && p.Y < nikseBitmap.Height) - { - var c = nikseBitmap.GetPixel(p.X, p.Y); - if (c.A <= 150 || c.R + c.G + c.B <= VobSubOcr.NocrMinColor) - { - ok = false; - break; - } - } - else if (p.X >= 0 && p.Y >= 0) + var c = nikseBitmap.GetPixel(p.X, p.Y); + if (c.A <= 150 || c.R + c.G + c.B <= VobSubOcr.NocrMinColor) { ok = false; break; } } - - index++; + else if (p.X >= 0 && p.Y >= 0) + { + ok = false; + break; + } } - index = 0; - while (index < oc.LinesBackground.Count && ok) + index++; + } + + index = 0; + while (index < oc.LinesBackground.Count && ok) + { + var op = oc.LinesBackground[index]; + foreach (var point in op.GetPoints()) { - var op = oc.LinesBackground[index]; - foreach (var point in op.GetPoints()) + var p = new Point(point.X + targetItem.X, point.Y + targetItem.Y); + if (p.X >= 0 && p.Y >= 0 && p.X < nikseBitmap.Width && p.Y < nikseBitmap.Height) { - var p = new Point(point.X + targetItem.X, point.Y + targetItem.Y); - if (p.X >= 0 && p.Y >= 0 && p.X < nikseBitmap.Width && p.Y < nikseBitmap.Height) - { - var c = nikseBitmap.GetPixel(p.X, p.Y); - if (c.A > 150 && c.R + c.G + c.B > VobSubOcr.NocrMinColor) - { - ok = false; - break; - } - } - else if (p.X >= 0 && p.Y >= 0) + var c = nikseBitmap.GetPixel(p.X, p.Y); + if (c.A > 150 && c.R + c.G + c.B > VobSubOcr.NocrMinColor) { ok = false; break; } } - - index++; + else if (p.X >= 0 && p.Y >= 0) + { + ok = false; + break; + } } - if (ok) + index++; + } + + if (ok) + { + var size = GetTotalSize(listIndex, list, oc.ExpandCount); + if (Math.Abs(size.X - oc.Width) < 3 && Math.Abs(size.Y - oc.Height) < 3) { return oc; } } + } + } - - // double widthPercent = targetItem.NikseBitmap.Height * 100.0 / targetItem.NikseBitmap.Width; - if (true) //Math.Abs(oc.WidthPercent - widthPercent) < 15) + for (var i = 0; i < OcrCharactersExpanded.Count; i++) + { + var oc = OcrCharactersExpanded[i]; + if (oc.ExpandCount > 1 && oc.Width > w && targetItem.X + oc.Width < nikseBitmap.Width) + { + bool ok = true; + var index = 0; + while (index < oc.LinesForeground.Count && ok) { - ok = true; - index = 0; - while (index < oc.LinesForeground.Count && ok) + var op = oc.LinesForeground[index]; + foreach (var point in op.ScaledGetPoints(oc, oc.Width, oc.Height - 1)) { - var op = oc.LinesForeground[index]; - foreach (var point in op.ScaledGetPoints(oc, oc.Width, oc.Height - 1)) + var p = new Point(point.X + targetItem.X, point.Y + targetItem.Y); + if (p.X >= 0 && p.Y >= 0 && p.X < nikseBitmap.Width && p.Y < nikseBitmap.Height) { - var p = new Point(point.X + targetItem.X, point.Y + targetItem.Y); - if (p.X >= 0 && p.Y >= 0 && p.X < nikseBitmap.Width && p.Y < nikseBitmap.Height) - { - var c = nikseBitmap.GetPixel(p.X, p.Y); - if (c.A <= 150 || c.R + c.G + c.B <= VobSubOcr.NocrMinColor) - { - ok = false; - break; - } - } - else if (p.X >= 0 && p.Y >= 0) + var c = nikseBitmap.GetPixel(p.X, p.Y); + if (c.A <= 150 || c.R + c.G + c.B <= VobSubOcr.NocrMinColor) { ok = false; break; } } - - index++; + else if (p.X >= 0 && p.Y >= 0) + { + ok = false; + break; + } } - index = 0; - while (index < oc.LinesBackground.Count && ok) + index++; + } + + index = 0; + while (index < oc.LinesBackground.Count && ok) + { + var op = oc.LinesBackground[index]; + foreach (var point in op.ScaledGetPoints(oc, oc.Width, oc.Height - 1)) { - var op = oc.LinesBackground[index]; - foreach (var point in op.ScaledGetPoints(oc, oc.Width, oc.Height - 1)) + var p = new Point(point.X + targetItem.X, point.Y + targetItem.Y); + if (p.X >= 0 && p.Y >= 0 && p.X < nikseBitmap.Width && p.Y < nikseBitmap.Height) { - var p = new Point(point.X + targetItem.X, point.Y + targetItem.Y); - if (p.X >= 0 && p.Y >= 0 && p.X < nikseBitmap.Width && p.Y < nikseBitmap.Height) - { - var c = nikseBitmap.GetPixel(p.X, p.Y); - if (c.A > 150 && c.R + c.G + c.B > VobSubOcr.NocrMinColor) - { - ok = false; - break; - } - } - else if (p.X >= 0 && p.Y >= 0) + var c = nikseBitmap.GetPixel(p.X, p.Y); + if (c.A > 150 && c.R + c.G + c.B > VobSubOcr.NocrMinColor) { ok = false; break; } } - - index++; + else if (p.X >= 0 && p.Y >= 0) + { + ok = false; + break; + } } - if (ok) + index++; + } + + if (ok) + { + var size = GetTotalSize(listIndex, list, oc.ExpandCount); + var widthPercent = size.Y * 100.0 / size.X; + if (Math.Abs(widthPercent - oc.WidthPercent) < 15) { return oc; } @@ -226,6 +232,45 @@ namespace Nikse.SubtitleEdit.Logic.Ocr return null; } + private static Point GetTotalSize(int listIndex, List items, int count) + { + if (listIndex + count >= items.Count) + { + return new Point(-100, -100); + } + + var minimumX = int.MaxValue; + int maximumX = int.MinValue; + int minimumY = int.MaxValue; + int maximumY = int.MinValue; + + for (var idx = listIndex; idx < listIndex + count; idx++) + { + var item = items[idx]; + if (item.Y < minimumY) + { + minimumY = item.Y; + } + + if (item.Y + item.NikseBitmap.Height > maximumY) + { + maximumY = item.Y + item.NikseBitmap.Height; + } + + if (item.X < minimumX) + { + minimumX = item.X; + } + + if (item.X + item.NikseBitmap.Width > maximumX) + { + maximumX = item.X + item.NikseBitmap.Width; + } + } + + return new Point(maximumX - minimumX, maximumY - minimumY); + } + public NOcrChar GetMatch(NikseBitmap bitmap, int topMargin, bool deepSeek, bool italic, double italicAngle, int maxWrongPixels) { // only very very accurate matches