䟿利なオヌプン゜ヌスずZxingに別の蚀語を話すように教えた方法

蚘事の1぀で 、SharePoint 2010の受信トレむ機胜を䜿甚しお、スキャンされたクヌポンを含むドキュメントを受信および凊理する方法に぀いお説明したした。 このプロゞェクトを実斜するにあたっお、いく぀かの興味深い問題を解決する必芁がありたした。 そしお今、私たちはより詳现に1぀の点に焊点を圓おたいず思いたす。







そのため、タスクの1぀は、クヌポンスキャンシヌトの数字を認識するこずでした。 いく぀かのクヌポンがある堎合があり、それらが垂盎および氎平の䞡方でシヌトに配眮できるこずに泚意する䟡倀がありたす。



クヌポンスキャンで芋たものは、他のプロゞェクトで既に遭遇したCodabarバヌコヌドず非垞に䌌おいたした。







Codabarは線圢バヌコヌドです。 各文字は、4぀の行ずそれらの間の3぀のスペヌスの7぀の芁玠で゚ンコヌドされたす。 それらの間で、文字は远加のスペヌスで区切られたす。 Codabarは開始文字で始たり、停止文字で終わりたす。 開始蚘号たたは停止蚘号は、原則ずしおABCD蚘号です。 参考0-9、-、$。

したがっお、このバヌコヌドには、各文字が行ずスペヌスの特定のシヌケンスに察応するアルファベットがありたす。





写真は、倀「401」を含むCodabar'aの䟋を瀺しおいたす。



Zxing



.NETでバヌコヌドを䜿甚する堎合、 移怍版のZxingラむブラリを䜿甚したす 。 ラむブラリは、QRコヌド、PDF 417、EAN、UPC、Aztec、デヌタマトリックスなど、あらゆる皮類の1Dおよび2Dバヌコヌドを生成および認識できたす。 そしお最も重芁なこずずしお、圌女はCodabarずの連携方法を知っおいたす。 通垞、Zxingラむブラリを䜿甚しおも問題は発生せず、さたざたなプラットフォヌムで䜿甚したした。 しかし、バヌコヌドZxingはすぐに認識できたせんでした。 すべおがそれほど単玔ではないこずが刀明したした...慎重に調べおみるず、顧客コヌドは、Codabarに非垞に䌌おいたすが、ただ異なっおいるこずがわかりたした。





おそらく、この圢匏も「暙準」ですが、詳现な説明ず情報は芋぀かりたせんでした。 このコヌドの認識を自動化するためのラむブラリ実装があるかもしれたせんが、それらを芋぀けるのは幞運ではありたせんでした...その結果、Zxingでの䜜業を継続し、以䞋を実行するこずにしたした゜ヌスコヌドを取埗し、独自のニヌズに合わせお認識アルゎリズムを倉曎したす。



アルゎリズム



Zxingでは、特定のコヌドCodabarReader.csなどの認識ロゞックを実装する各クラスには、OneDReader.csクラスで宣蚀された独自の抜象decodeRowメ゜ッドの実装がありたす。



override public List<Result> decodeRow(int rowNumber, BitArray row, Hashtable hints)
      
      







入力は、画像の行番号ず、行のピクセル倀を含む実際の配列です暗い-明るい。



次に、setCountersBitArray rowメ゜ッドを䜿甚しお、次のアルゎリズムに埓っおint [] counters配列が開始されたす。暗いピクセルから開始し、行配列で癜いピクセルに遭遇するたで配列の最初の芁玠が増加し始めたす。 その埌、counters配列の2番目の芁玠ぞの遷移が行われ、黒いピクセルが珟れるたで増分されたす。 そしお、行末たで続きたす。 その結果、カりンタヌ配列は次のようになりたす。



15 7 10 3 4 8 16 ...



぀たり、15個の黒ピクセル、7個の癜、10個の黒、3個の癜などです。 この実装の最初の芁玠は黒いピクセルに察応しおいたす。



次に、開始文字に察応するシヌケンスを探したすこの䟋では、文字「A」で、元のCodabarでは文字「A」、「B」、「C」、「D」のいずれかです。 findStartPatternメ゜ッドout int charOffset、int globalOffsetを䜿甚しお怜玢したす。 矛盟が芋぀かるたで、globalOffsetの倀を増やし画像行の珟圚の䜍眮を決定したす、counters配列の次の文字に進みたす。 findStartPatternメ゜ッドは、メ゜ッドを呌び出したす。



 int toNarrowWidePattern(int position, int offset)
      
      







これは、counters配列の珟圚の芁玠の数ず文字の長さ開始文字たたは停止文字の堎合は3、残りの文字の堎合は9を取りたす。 文字が芋぀からない堎合は-1を返したす。 文字が芋぀かった堎合、CHARACTER_ENCODINGS配列のこの䜍眮を返したす。



アルファベット



コヌドのアルファベットは、次のフィヌルドによっお決定されたす。



CHARACTER_ENCODINGS配列に保存されおいる倀ず、䞀般的にCodabarがどのように゚ンコヌドされるかに぀いおのいく぀かの蚀葉。 たずえば、数倀「0」は、次の䞀連のストラむプずスペヌスによっお゚ンコヌドされたす。







これは次のように蚘述されたす101010011バヌコヌド゚ンコヌディング。 単䞀の0/1は短いスペヌス/ストリップを゚ンコヌドし、ダブル00/11は長いスペヌス/ストリップを゚ンコヌドしたす。 さらに、このシヌケンスはコヌド0000011幅の゚ンコヌド、たたは16進圢匏0x03に倉換されたす。 ぀たり 単䞀の文字はれロで、二重の文字は1で曞き蟌たれたす。 この堎合、各文字は7文字ではなく9文字で゚ンコヌドされたすが、デゞタルコヌドを䜜成するロゞックは同じです。



クヌポンの䟋を勉匷するのに時間を費やす必芁がありたした。 バヌコヌドを泚意深く芋お、特定の文字に察応するシヌケンスを曞きたした。 結果は私たち自身のアルファベットです



 private const String ALPHABET_STRING = "0123456789AE"; static int[] CHARACTER_ENCODINGS = { 0x014, 0x101, 0x041, 0x140, 0x011, 0x110, 0x050, 0x005, 0x104, 0x044, // 0-9 0x000, 0x004, // AE};
      
      







したがっお、コヌドを凊理するプロセスは次のずおりです。開始文字が芋぀かるずすぐに、同じtoNarrowWidePatternメ゜ッドを䜿甚しお情報を探したす。 シヌケンスの長さは固定されおいたす。 特定のステップで、シンボルが停止シンボルかどうかを確認する必芁がありたす。 はいの堎合、結果を生成し、counters配列の次の芁玠に移動しお、文字列内のバヌコヌドの怜玢を続けたす。



その結果、1行をスキャンするず、1぀以䞊のコヌドがありたすたたはありたせん。これらのコヌドは、結果のグロヌバル配列に栌玍されたす。 そしお、画像の次の行に進みたす。



4぀の䜍眮すべおにコヌドが存圚するかどうかドキュメントをチェックする必芁がある堎合は、画像を時蚈回りに90床回転する機胜も远加されたした。 Zxingラむブラリでは、凊理される画像はBinaryBitmapクラスに含たれおおり、このクラスにはrotateCounterClockwiseメ゜ッドがありたす。 画像の回転は簡単です。



したがっお、少し考えお䜜業した埌、新しいコヌド圢匏に合わせおラむブラリを倉曎するこずができたした。 気にする人、コヌドはここにありたす



非衚瀺のテキスト
 using System; using System.Collections; using System.Collections.Generic; using System.Text; using BitArray = ETR.REBT.BarcodeReader.common.BitArray; namespace ETR.REBT.BarcodeReader.oned { public sealed class MyCodeReader : OneDReader { // These values are critical for determining how permissive the decoding // will be. All stripe sizes must be within the window these define, as // compared to the average stripe size. private static readonly int MAX_ACCEPTABLE = (int)(PATTERN_MATCH_RESULT_SCALE_FACTOR * 2.0f); private static readonly int PADDING = (int)(PATTERN_MATCH_RESULT_SCALE_FACTOR * 1.5f); private static readonly int STARTEND_LENGTH = 3; private static readonly int SYMBOL_LENGTH = 9; private static readonly int DATA_LENGTH = 15; // 15 symbols + 2 start/stop symbols private static readonly int All_LENGHT = (16 + DATA_LENGTH * SYMBOL_LENGTH + 2 * STARTEND_LENGTH); private const String ALPHABET_STRING = "0123456789AE"; internal static readonly char[] ALPHABET = ALPHABET_STRING.ToCharArray(); /** * These represent the encodings of characters, as patterns of wide and narrow bars. The 7 least-significant bits of * each int correspond to the pattern of wide and narrow, with 1s representing "wide" and 0s representing narrow. */ internal static int[] CHARACTER_ENCODINGS = { 0x014, 0x101, 0x041, 0x140, 0x011, 0x110, 0x050, 0x005, 0x104, 0x044, // 0-9 0x000, 0x004, // AE }; // minimal number of characters that should be present (inclusing start and stop characters) // under normal circumstances this should be set to 3, but can be set higher // as a last-ditch attempt to reduce false positives. private const int MIN_CHARACTER_LENGTH = 3; // Start and end patterns private static readonly char[] START_ENCODING = { 'A' }; private static readonly char[] END_ENCODING = { 'E' }; private static readonly char[] DATA_ENCODING = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }; // some codabar generator allow the codabar string to be closed by every // character. This will cause lots of false positives! // some industries use a checksum standard but this is not part of the original codabar standard // for more information see : http://www.mecsw.com/specs/codabar.html // Keep some instance variables to avoid reallocations private readonly StringBuilder decodeRowResult; private int[] counters; private int counterLength; public MyCodeReader() { decodeRowResult = new StringBuilder(40); counters = new int[500]; counterLength = 0; } override public List<Result> decodeRow(int rowNumber, BitArray row, Hashtable hints) { List<Result> returnList = null; if (!setCounters(row)) return null; int globalOffset = 0; while (globalOffset < counterLength) { int startSymbolPos = -1; int startOffset = findStartPattern(out startSymbolPos, globalOffset); if (startOffset < 0) return returnList; // we can't find start char in the whole row -> so, exit decodeRowResult.Length = 0; decodeRowResult.Append((char)startSymbolPos); int nextStart = startOffset; nextStart += (STARTEND_LENGTH + 1/*space between symbols*/); bool findNextStart = false; do { int charOffset = toNarrowWidePattern(nextStart, SYMBOL_LENGTH); if (charOffset == -1 || !arrayContains(DATA_ENCODING, ALPHABET[charOffset])) { findNextStart = true; break; } decodeRowResult.Append((char)charOffset); nextStart += (SYMBOL_LENGTH + 1); // Stop as soon as length of data symbols equals to corresponding number if (decodeRowResult.Length == DATA_LENGTH + 1/*start symbol*/) { int endOffset = toNarrowWidePattern(nextStart, STARTEND_LENGTH); if (endOffset == -1 || !arrayContains(END_ENCODING, ALPHABET[endOffset])) { findNextStart = true; break; } globalOffset = nextStart + STARTEND_LENGTH; decodeRowResult.Append((char)endOffset); break; } } while (nextStart < counterLength); // no fixed end pattern so keep on reading while data is available if (findNextStart) { globalOffset = ++startOffset; continue; } if (!validatePattern()) { globalOffset = ++startOffset; continue; } // remove stop/start characters character decodeRowResult.Remove(decodeRowResult.Length - 1, 1); decodeRowResult.Remove(0, 1); int runningCount = 0; for (int i = 0; i < startOffset; i++) { runningCount += counters[i]; } float left = (float)runningCount; for (int i = startOffset; i < nextStart - 1; i++) { runningCount += counters[i]; } float right = (float)runningCount; Result result = new Result( decodeRowResult.ToString(), null, new ResultPoint[] { new ResultPoint(left, (float) rowNumber), new ResultPoint(right, (float) rowNumber) }, BarcodeFormat.CODABAR); if (returnList == null) returnList = new List<Result>(); returnList.Add(result); } return returnList; } private bool validatePattern() { if (decodeRowResult.Length != DATA_LENGTH + 2) { return false; } // Translate character table offsets to actual characters. for (int i = 0; i < decodeRowResult.Length; i++) { decodeRowResult[i] = ALPHABET[decodeRowResult[i]]; } // Ensure a valid start character char startchar = decodeRowResult[0]; if (!arrayContains(START_ENCODING, startchar)) { return false; } // Ensure a valid end character char endchar = decodeRowResult[decodeRowResult.Length - 1]; if (!arrayContains(END_ENCODING, endchar)) { return false; } // Ensure a valid data symbols for (int i = 1; i < decodeRowResult.Length - 1; i++) { if (!arrayContains(DATA_ENCODING, decodeRowResult[i])) { return false; } } return true; } /// <summary> /// Records the size of all runs of white and black pixels, starting with white. /// This is just like recordPattern, except it records all the counters, and /// uses our builtin "counters" member for storage. /// </summary> /// <param name="row">row to count from</param> private bool setCounters(BitArray row) { counterLength = 0; // Start from the first white bit. int i = row.getNextUnset(0); int end = row.Size; if (i >= end) { return false; } bool isWhite = true; int count = 0; for (; i < end; i++) { if (row[i] ^ isWhite) { // that is, exactly one is true count++; } else { counterAppend(count); count = 1; isWhite = !isWhite; } } counterAppend(count); return true; } private void counterAppend(int e) { counters[counterLength] = e; counterLength++; if (counterLength >= counters.Length) { int[] temp = new int[counterLength * 2]; Array.Copy(counters, 0, temp, 0, counterLength); counters = temp; } } private int findStartPattern(out int charOffset, int globalOffset) { charOffset = -1; // // Assume that first (i = 0) set of pixels is white, // so we start find symbols from second set (i = 1). // And next we step over white set ('i += 2'). // for (int i = 1 + globalOffset; i < counterLength; i += 2) { if (counters[i - 1] < counters[i] * 5) // before start char must be a long space continue; charOffset = toNarrowWidePattern(i, 3); if (charOffset != -1 && arrayContains(START_ENCODING, ALPHABET[charOffset])) { return i; } } return -1; } internal static bool arrayContains(char[] array, char key) { if (array != null) { foreach (char c in array) { if (c == key) { return true; } } } return false; } // Assumes that counters[position] is a bar. private int toNarrowWidePattern(int position, int offset) { int end = position + offset; if (end >= counterLength) return -1; // First element is for bars, second is for spaces. int[] maxes = { 0, 0 }; int[] mins = { Int32.MaxValue, Int32.MaxValue }; int[] thresholds = { 0, 0 }; for (int i = 0; i < 2; i++) { for (int j = position + i; j < end; j += 2) { if (counters[j] < mins[i]) { mins[i] = counters[j]; } if (counters[j] > maxes[i]) { maxes[i] = counters[j]; } } double tr = ((double)mins[i] + (double)maxes[i]) / 2; thresholds[i] = (int)Math.Ceiling(tr); } // There are no big spaces in the barcode -> only small spaces thresholds[1] = Int32.MaxValue; // For start and end symbols defined empirically threshold equals to 5 if (offset == STARTEND_LENGTH) thresholds[0] = 5; int bitmask = 1 << offset; int pattern = 0; for (int i = 0; i < offset; i++) { int barOrSpace = i & 1; bitmask >>= 1; if (counters[position + i] >= thresholds[barOrSpace]) { pattern |= bitmask; } } for (int i = 0; i < CHARACTER_ENCODINGS.Length; i++) { if (CHARACTER_ENCODINGS[i] == pattern) { return i; } } return -1; } } }
      
      









「最適化」Zxing



そのため、ペヌゞ䞊の1぀以䞊のコヌドを認識できたした。 しかし、私たちの問題はそこで終わりたせんでした。 条件に応じお、耇数のコヌドを䜿甚できるほか、シヌトの4぀の異なる䜍眮をスキャンする必芁があるため、アルゎリズムは倧幅に「遅く」なりたした。 さらに掘り䞋げお、次の機胜が発芋されたした。

Zxingは、画像に基づいおRGBLuminanceSourceクラスのむンスタンスを䜜成したす。 元の画像の各ピクセルの明るさに関する情報を含むバむトの配列がありたす。 次に、この情報ずしきい倀に基づいお、ビットマップが取埗されたす。



以䞋は、RGBLuminanceSourceクラスのコンストラクタヌのコヌド郚分の䟋です。



  Color c; for (int y = 0; y < height; y++) { int offset = y * width; for (int x = 0; x < width; x++) { c = bitmap.GetPixel(x, y); var r = ColorUtility.GetRValue(c); var g = ColorUtility.GetGValue(c); var b = ColorUtility.GetBValue(c); luminances[offset + x] = (byte)(0.3 * r + 0.59 * g + 0.11 * b + 0.01); } }
      
      





぀たり、サむクルでは、遅いビットマップ.GetPixelx、yが画像の各ピクセルに䜿甚されたす 解像床が200x300ピクセルたたはそれに近いの小さな画像の堎合、このアプロヌチは非垞に適切であり、遅延は発生したせん原則ずしお1぀のコヌドしか認識されない堎合。 ただし、この堎合、画像の解像床は高く最倧3000 x 5000ピクセル、これに方向オプションの数を乗算し、倚くのペヌゞの凊理を乗算する必芁がありたす。 これはすべお、蚱容できない遅延に぀ながりたす。 たずえば、䞊蚘の解像床の1ペヌゞでは、RGBLuminanceSourceクラスのオブゞェクトが8秒で䜜成されたしたが、これはもちろん非垞に長いです。



このコヌドをさらに倉曎し、GetPixelを忘れお、スキャンに取り掛かる必芁がありたした。



  bmp = bitmap.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadOnly, bitmap.PixelFormat); for (var y = 0; y < bmp.Height; y++) { var row = (byte*)bmp.Scan0 + (y * bmp.Stride); int offset = y * width; for (var x = 0; x < bmp.Width; x++) { var b = row[(x * pixelSize)]; var g = row[(x * pixelSize) + 1]; var r = row[(x * pixelSize) + 2]; luminances[offset + x] = (byte)(0.3 * r + 0.59 * g + 0.11 * b + 0.01); } }
      
      





このステップにより、アルゎリズムが倧幅に加速され、蚱容可胜な凊理時間を埗るこずができたした。



PDFで䜜業する



前述のように、クヌポンスキャンは画像ファむルの圢匏たたはPDFドキュメントで行うこずができたす。 pdfペヌゞを画像に倉換するために、itextsharpラむブラリを䜿甚したした 。



このラむブラリを操䜜するためのメむンクラスはPdfReaderです。 このクラスのむンスタンスは、たずえば次のようにしお取埗できたす。



ネタバレの䞋でコヌドを探したす。

非衚瀺のテキスト
 var reader = new PdfReader(filePath)
      
      







その埌、コヌドで䜿甚できたす。

 for (var pageNumber = 1; pageNumber <= reader.NumberOfPages; pageNumber++) { var page = reader.GetPageN(pageNumber); List<ImageRenderInfo> images; try { images = FindImageInPDFDictionary(page); } catch (Exception) { //     PDF  continue; } finally { reader.ReleasePage(pageNumber); } foreach (var img in images) { var image = RenderImage(img); var result = ImageDecoder.Decode(image, allRotations); if (result != null && result.Count > 0) { //  ,     } } }
      
      







この関数を䜿甚しお、PDFドキュメントペヌゞで画像を怜玢したす



 private static List<ImageRenderInfo> FindImageInPDFDictionary(PdfDictionary pg) { var result = new List<ImageRenderInfo>(); var res = (PdfDictionary)PdfReader.GetPdfObject(pg.Get(PdfName.RESOURCES)); var xobj = (PdfDictionary)PdfReader.GetPdfObject(res.Get(PdfName.XOBJECT)); if (xobj == null) return null; foreach (var name in xobj.Keys) { var obj = xobj.Get(name); if (!obj.IsIndirect()) continue; var tg = (PdfDictionary)PdfReader.GetPdfObject(obj); var type = (PdfName)PdfReader.GetPdfObject(tg.Get(PdfName.SUBTYPE)); if (PdfName.IMAGE.Equals(type)) { var width = float.Parse(tg.Get(PdfName.WIDTH).ToString()); var height = float.Parse(tg.Get(PdfName.HEIGHT).ToString()); if (width > ImageDecoder.MinimalSideResolution || height >= ImageDecoder.MinimalSideResolution) { var imgRi = ImageRenderInfo.CreateForXObject(new Matrix(width, height), (PRIndirectReference)obj, tg); result.Add(imgRi); } } if (PdfName.FORM.Equals(type)) { result.AddRange(FindImageInPDFDictionary(tg)); } if (PdfName.GROUP.Equals(type)) { result.AddRange(FindImageInPDFDictionary(tg)); } } return result; }
      
      







ImageRenderInfoクラスのオブゞェクトからBitmap型のオブゞェクトを取埗したす



 private static Bitmap RenderImage(ImageRenderInfo renderInfo) { try { var image = renderInfo.GetImage(); using (var dotnetImg = image.GetDrawingImage()) { if (dotnetImg != null) { using (var ms = new MemoryStream()) { dotnetImg.Save(ms, ImageFormat.Png); return new Bitmap(dotnetImg); } } } } catch (Exception) { } return null; }
      
      







ImageDecoder.Decodeメ゜ッドは、画像内のコヌドを怜玢するロゞックを実装したす。




䞖界には今、倚くの皮類のバヌコヌドがありたす。 それらのほずんどの認識ず生成は、開発者が利甚できるラむブラリに実装されおいたす。 ただし、元のタむプのバヌコヌドに぀たずくず、すぐに認識できなくなるこずがありたす。



そしお、慎重にピアリングし、適切に蚭蚈されたオヌプン゜ヌスラむブラリを䜿甚する方法は、すぐに結果を埗るのに圹立ちたす。



All Articles