Τεχνικό Άρθρο

Αποκωδικοποίηση περιστραμμένων QR σε σελίδες PDF με HotPDF

Το HotPDF αποκωδικοποιεί περιστραμμένα QR symbols σε φορτωμένη σελίδα PDF κανονικοποιώντας τη δειγματοληπτική μήτρα modules μέσα από και τους οκτώ προσανατολισμούς D4, στον ίδιο τον decoder. Η εξωτερική επανάληψη περιστροφής που δουλεύει για γραμμικές symbologies δεν μπορεί να δουλέψει για QR, και το να καταλάβετε γιατί σας γλιτώνει μια μέρα κυνηγητού ενός decoder που φαίνεται χαλασμένος και δεν είναι

Το σενάριο είναι αρκετά συνηθισμένο. Σκαναρισμένες αποδείξεις παράδοσης φτάνουν ως PDF, κάθε σελίδα κουβαλά ετικέτα QR, και ο χειριστής του scanner τάισε μια στοίβα φύλλων όποια κατεύθυνση δεχόταν ο δίσκος. Μερικές ετικέτες είναι όρθιες, μερικές απέχουν ένα τέταρτο στρίψιμο, λίγες είναι ανάποδα. Καλείτε τον barcode decoder, οι μισές σελίδες επιλύονται, και οι άλλες μισές γυρίζουν κενές χωρίς κανένα σφάλμα

Γιατί η περιστροφή της μάσκας σάρωσης δεν φτιάχνει ποτέ περιστραμμένο QR;

Γιατί η διάταξη των finder patterns του QR είναι σκόπιμα ασύμμετρη, και μια περιστροφή ολόκληρης εικόνας διατηρεί εκείνη την ασυμμετρία αντί να την αφαιρέσει. Το QR Code τοποθετεί τρία τετράγωνα finder στις γωνίες πάνω-αριστερά, πάνω-δεξιά και κάτω-αριστερά, και αφήνει τη γωνία κάτω-δεξιά κενή (ISO/IEC 18004:2015 §6.3.3). Εκείνη η λείπουσα γωνία είναι το στίγμα προσανατολισμού. Περιστρέψτε το bitmap της σελίδας ενενήντα μοίρες και το κενό απλώς μετακομίζει σε άλλη γωνία. Δεν υπάρχει μη τετριμμένη περιστροφή του επιπέδου που να αντιστοιχίζει μια διάταξη τριών γωνιών πίσω στον εαυτό της, οπότε ένας decoder που δέχεται μόνο την κανονική διάταξη θα απορρίπτει κάθε προσπάθεια με τη σειρά

Αυτό μετράει επειδή η προφανής λύση είναι η λάθος. Το φυσικό ένστικτο είναι να κρεμάσεις την επανάληψη έξω: απόδωσε τη σελίδα, δώσε τη μάσκα στον decoder, και αν αποτύχει, γύρισε τη μάσκα και ξαναδοκίμασε για 90, 180 και 270 μοίρες. Για Code 39 η πολιτική αυτή είναι ακριβώς σωστή, γιατί μια γραμμική symbology έχει μοτίβο start και stop που ο scanner βρίσκει μόλις οι ράβδοι τρέχουν οριζόντια. Για QR είναι τέσσερις εγγυημένες αποτυχίες ακολουθούμενες από αναφορά ότι δεν βρέθηκε τίποτα

Η ομάδα D4, εφαρμοσμένη στη μήτρα modules

Ο σωστός τόπος για την κανονικοποίηση είναι μετά τη δειγματοληψία, στο boolean πλέγμα modules και όχι στη μάσκα pixels. Μόλις ο decoder επιλύσει το symbol σε μήτρα n επί n από σκούρα και ανοιχτά modules, μπορεί να απαριθμήσει τη διεδρική ομάδα του τετραγώνου: τέσσερις περιστροφές επί δύο ανακλάσεις, οκτώ υποψήφιοι προσανατολισμοί συνολικά. Για κάθε υποψήφιο ελέγχει το τρίγωνο των finders, και ο πρώτος υποψήφιος του οποίου τα τρία finders πέφτουν στις θέσεις πάνω-αριστερά, πάνω-δεξιά και κάτω-αριστερά είναι ο πραγματικός προσανατολισμός. Από εκεί και πέρα το υπάρχον pipeline τρέχει αμετάβλητο, γιατί τα bits format information, η zigzag τοποθέτηση δεδομένων και η διόρθωση Reed-Solomon όλα προϋποθέτουν κανονική μήτρα και τώρα παίρνουν μία

Τέσσερις αποδόσεις της ίδιας μήτρας modules QR του HotPDF κάτω από τις περιστροφές της ομάδας D4 στις 0, 90, 180 και 270 μοίρες, δείχνοντας τα τρία finder patterns να μεταναστεύουν γωνίες ενώ η κενή γωνία κινείται μαζί τους, ώστε μόνο ο κανονικός προσανατολισμός παρουσιάζει finders πάνω-αριστερά, πάνω-δεξιά και κάτω-αριστερά στον decoder
Η περιστροφή της μάσκας pixels δεν μπορεί να αφαιρέσει την ασυμμετρία finders του QR, οπότε το HotPDF απαριθμεί τους προσανατολισμούς D4 πάνω στη δειγματοληπτική μήτρα modules και κρατά τον πρώτο υποψήφιο του οποίου τα finders πέφτουν πάνω-αριστερά, πάνω-δεξιά και κάτω-αριστερά

Δύο ιδιότητες το κάνουν φθηνό. Η μήτρα είναι μικρή σε σύγκριση με το απεικονισμένο bitmap, οπότε οκτώ αντιμεταθέσεις κοστίζουν πολύ λιγότερο από οκτώ απεικονίσεις σελίδας. Και η μήτρα είναι καθαρός boolean πίνακας χτισμένος από τον δειγματολήπτη, οπότε κανένας μετασχηματισμός στην πορεία δεν μπορεί να εισαγάγει τιμές που δεν δειγματοληπήθηκαν ποτέ

Η ανίχνευση έκδοσης είναι αναζήτηση διαιρετότητας, όχι διαίρεση

Το πλήθος modules δεν μπορεί να συναχθεί διαιρώντας το δειγματοληπτικό πλάτος με ένα υποτιθέμενο μέγεθος module, και το λάθος εδώ είναι λεπτή πηγή αποτυχιών αποκωδικοποίησης σε απεικονίσεις υψηλής ανάλυσης. Ένα QR symbol έκδοσης v είναι 4v + 17 modules πλάτος, οπότε η έκδοση 1 είναι 21 modules και η έκδοση 40 είναι 177. Μια μάσκα που μετρά 126 pixels πλάτος είναι εξίσου συνεπής με την έκδοση 1 στα έξι pixels ανά module και με αρκετές υψηλότερες εκδόσεις σε μικρότερα μεγέθη module. Η γραμμική διαίρεση διαλέγει μία από αυτές και συνήθως είναι λάθος

Αυτό που δουλεύει είναι αναζήτηση διαιρετότητας πάνω στις υποψήφιες εκδόσεις. Περπατήστε από την έκδοση 40 προς την 1, κρατήστε τους υποψηφίους των οποίων το πλήθος modules διαιρεί το δειγματοληπτικό πλάτος ακριβώς και αφήνει τουλάχιστον τρία pixels ανά module, και πάρτε τη μικρότερη επιζήσασα έκδοση. Το πάτωμα των τριών pixels είναι όσο εμποδίζει την αναζήτηση να δεχτεί μια γελοία πυκνή ανάγνωση ενός χοντρού symbol, και ο κανόνας της μικρότερης έκδοσης επιλύει την υπόλοιπη αμφισημία υπέρ της ανάγνωσης που θα παρήγε όντως ένας scanner

Η περιήγηση ανίχνευσης έκδοσης του HotPDF για QR symbol πάνω σε μάσκα 126 pixels, δοκιμάζοντας κάθε υποψήφιο πλήθος modules 4v συν 17 από την έκδοση 40 προς την 1 για ακριβή διαιρετότητα και πάτωμα module τριών pixels προτού κερδίσει η μικρότερη επιζήσασα έκδοση
Το πλήθος modules QR βγαίνει από αναζήτηση διαιρετότητας πάνω σε υποψήφιες εκδόσεις, και όχι διαιρώντας το πλάτος της μάσκας με υποτιθέμενο μέγεθος module, και η μικρότερη επιζήσασα έκδοση επιλύει την αμφισημία
var
  Pdf: THotPDF;
  Options: THPDFBarcodeDecodeOptions;
  Codes: THPDFDecodedBarcodes;
  Info: THPDFBarcodeDecodeInfo;
  I: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('delivery-notes.pdf');
    Options := THPDFBarcodeDecodeOptions.Default;
    Options.DPI := 300;
    Options.RotationPolicy := bdrpFallback;
    Options.MinimumConfidence := 0.5;
    Options.MaxResults := 16;
    if Pdf.DecodeLoadedPageBarcodes(0, Options, Codes, Info) then
      for I := 0 to High(Codes) do
        if Codes[I].Symbology = bsyQRCode then
          Writeln(Codes[I].Text, '  at ',
            Format('%.0f', [Codes[I].OrientationDegrees]), ' degrees');
  finally
    Pdf.Free;
  end;
end;

Η THPDFBarcodeDecodeOptions.Default γυρνά γεμάτο record και όχι μηδενισμένο, που μετράει γιατί DPI μηδέν ή πλαφόν αποτελεσμάτων μηδέν είναι ένας πειστικά φυσιολογικός τρόπος να μη γυρίσει τίποτα. Η RotationPolicy ελέγχει μόνο την εξωτερική επανάληψη: η bdrpNone απεικονίζει μία φορά, η bdrpFallback ξαναδοκιμάζει τους άλλους προσανατολισμούς μετά από αποτυχημένο πρώτο πέρασμα, και η bdrpAll απεικονίζει κάθε προσανατολισμό άνευ όρων. Επειδή η κανονικοποίηση QR γίνεται μέσα στον decoder, οι σελίδες QR επιλύονται στην πρώτη προσπάθεια κάτω από οποιαδήποτε από τις τρεις πολιτικές. Η πολιτική είναι εκεί για τις γραμμικές symbologies που την χρειάζονται πραγματικά

Πώς αποδεικνύετε ότι ένας μετασχηματισμός bitmap δεν εφευρίσκει pixels;

Μετρήστε το μελάνι και από τις δύο πλευρές και απαιτήστε τα σύνολα να ταιριάζουν. Μια περιστροφή είναι αντιμετάθεση pixels, τίποτα παραπάνω, οπότε το πλήθος των μη μηδενικών κελιών στην έξοδο πρέπει να ισούται με εκείνο στην είσοδο. Όταν μια περιστροφή μάσκας στο μονοπάτι της εξωτερικής επανάληψης ανέφερε 4800 ορισμένα κελιά να μπαίνουν και 7439 να βγαίνουν, εκείνη η μία σύγκριση αρκούσε για να καταδικάσει τον μετασχηματισμό χωρίς να διαβάσετε γραμμή από τη γεωμετρία του

Η αιτία ήταν πεζή και αξίζει να την πάρετε ως κανόνα. Ένας δυναμικός πίνακας μεγεθυμένος με SetLength δεν είναι εγγυημένο ότι φτάνει μηδενισμένος όταν είναι αποτέλεσμα συνάρτησης που ταξιδεύει μονοπάτι που το runtime δεν καθαρίζει, και κελιά που η περιστροφή δεν γράφει ποτέ κουβαλάνε μετά ό,τι bytes ήταν εκεί πριν. Μερικά από εκείνα τα ξεπερασμένα bytes είναι μη μηδενικά, και μη μηδενικό σημαίνει μελάνι. Η λύση είναι μία γραμμή, FillChar(Result[0], N, 0) προτού τρέξει ο βρόχος αντιμετάθεσης, και η πειθαρχία που υπονοεί είναι φαρδύτερη: κάθε συνάρτηση που γυρνά μάσκα ή bitmap buffer πρέπει να καθαρίζει την έξοδό της ρητά αντί να στηρίζεται σε σημασιολογία δέσμευσης μνήμης

Αυτό που έκανε το ελάττωμα να επιβιώσει τρεις εκδόσεις είναι πιο ενδιαφέρον από το ελάττωμα. Μόλις το QR μετέφερε τον χειρισμό προσανατολισμού του μέσα στον decoder, το QR σταμάτησε να ασκεί καθόλου την εξωτερική περιστροφή μάσκας, και ο μόνος απομένον καταναλωτής εκείνου του μονοπατιού κώδικα ήταν ο Code 39. Η κοινή υποδομή κρύβει bugs σαν αυτό συνεχώς: η κάλυψη από ένα χαρακτηριστικό κάνει ένα μονοπάτι να φαίνεται τεσταμένο ενώ το χαρακτηριστικό που πραγματικά εξαρτάται από αυτό δεν έχει δικό του καθόλου. Κάθε μονοπάτι που σταματά να χρησιμοποιεί ένα νέο χαρακτηριστικό θέλει ένα test που το χρησιμοποιεί ακόμα

Ανάγνωση των αποτελεσμάτων πίσω σε συντεταγμένες σελίδας

Κάθε γεωμετρική τιμή που παράγει ο decoder εκφράζεται στο σύστημα συντεταγμένων του bitmap της προσπάθειας, και ο caller τη θέλει σε PDF user space. Εκείνη η μετατροπή τρέχει σε δύο στάδια: αναιρείται το τέταρτο στρίψιμο που εφάρμοσε η επανάληψη, μετά αναιρείται ο μετασχηματισμός απόδοσης που αντιστοίχισε τον user space στο bitmap. Αυτό που φτάνει στο THPDFDecodedBarcode είναι ένα bounding box ευθυγραμμισμένο με άξονες σε user space, με Left, Bottom, Right και Top κατά τη σύμβαση PDF ότι το Y μεγαλώνει προς τα πάνω, συν ένα OrientationDegrees αντίθετα ωρολογιακά

Το barcode pipeline του HotPDF από απεικονισμένο bitmap σελίδας μέσα από δειγματοληψία σε boolean μήτρα modules, κανονικοποίηση D4, ανίχνευση έκδοσης με διαιρετότητα και αποκωδικοποίηση Reed-Solomon, μετά η μετατροπή συντεταγμένων δύο σταδίων που αναιρεί το τέταρτο στρίψιμο της επανάληψης και τον μετασχηματισμό απόδοσης προτού το THPDFDecodedBarcode δημοσιεύσει Left, Bottom, Right, Top και OrientationDegrees σε user space
Η κανονικοποίηση QR μέσα στον decoder αφήνει τις σελίδες να επιλύονται στην πρώτη προσπάθεια, ενώ η μετατροπή συντεταγμένων δύο σταδίων γυρνά αποτελέσματα του bitmap προσπάθειας σε boxes user space ευθυγραμμισμένα με άξονες

Πάρτε λάθος την κατεύθυνση εκείνης της δεύτερης μετατροπής και το σύμπτωμα είναι άσχημο: το κείμενο αποκωδικοποιείται τέλεια, αλλά το box που σχεδιάζετε για overlay αναθεώρησης προσγειώνεται στον καθρέφτη της σωστής θέσης. Όποιος χτίζει interface αναθεώρησης πάνω στον decoder πρέπει να κάνει assert πάνω σε γνωστό fixture, με symbol τοποθετημένο σκόπιμα κοντά σε μια γωνία σελίδας ώστε ένας ανεστραμμένος άξονας Y να φαίνεται με μια ματιά. Η ίδια λογική ισχύει για κάθε συντεταγμένη που διασχίζει το όριο της απόδοσης, που είναι ο λόγος που το rendering σελίδας PDF σε bitmap σε Delphi αξίζει να το καταλάβετε προτού χτίσετε πάνω στον decoder

Τι κάνει και τι δεν κάνει ο ενσωματωμένος decoder

Ο ενσωματωμένος decoder είναι μια οριοθετημένη υλοποίηση χωρίς εξαρτήσεις, και είναι ειλικρινής για τα όριά του αντί να υποβαθμίζεται σιωπηλά. Αναγνωρίζει Code 39 και QR, επικυρώνει τα προστατευμένα με BCH bits format και το μοτίβο μάσκας προτού δημοσιεύσει οποιαδήποτε δεδομένα, και δεν επιχειρεί ανάκτηση σφάλματος πάνω σε κατεστραμμένα symbols. Αν η είσοδός σας είναι φωτογραφία καμπύλης ετικέτας κάτω από ανομοιόμορφο φως, αυτή είναι άλλη κατηγορία προβλήματος και θέλει εξειδικευμένη μηχανή

// Βάλτε τη δική σας μηχανή: υλοποιήστε IHPDFBarcodeDecoder και περάστε τη
// στο overload που καταλαβαίνει decoders. Το HotPDF κρατά την απόδοση σελίδας,
// τα budgets, την αντιστοίχιση συντεταγμένων και την αφαίρεση διπλότυπων
if not Pdf.DecodeLoadedPageBarcodes(PageIndex, MyDecoder, Options,
     Codes, Info) then
  case Info.Status of
    bdsBudgetExceeded:
      Log('raise MaxPixels or lower DPI: ' + string(Info.Diagnostic));
    bdsRenderError:
      Log('page did not render: ' + string(Info.Diagnostic));
    bdsDecoderError:
      Log(string(Info.DecoderName) + ' failed: ' + string(Info.Diagnostic));
  end;

Η THPDFBarcodeDecodeInfo είναι εκεί όπου ένα pipeline παραγωγής εξασφαλίζει το ψωμί του. Τα RotationAttemptCount και DecoderCallCount σας λένε αν η εξωτερική επανάληψη τρέξε καθόλου, το ReceivedResultCount απέναντι στο AcceptedResultCount διαχωρίζει decoder που δεν βρήκε τίποτα από κατώφλι εμπιστοσύνης που απέρριψε όλα όσα βρήκε, και τα RenderedPixels με PeakWorkingBytes είναι όσα ζωγραφίζετε όταν μια δουλειά παρτίδας αρχίζει να παλεύει με τη μνήμη. Ένα κενό σύνολο αποτελεσμάτων μαζί με bdsSucceeded σημαίνει ότι η σελίδα όντως δεν έχει αναγνώσιμο symbol, που είναι διαφορετικό λειτουργικό δεδομένο από το bdsBudgetExceeded

Τα πεδία budget αξίζουν συνειδητή απόφαση και όχι προεπιλογή. Τα MaxPixels και MaxWorkingBytes υπάρχουν επειδή το DPI πολλαπλασιάζεται τετραγωνικά: μετάβαση από 300 σε 600 DPI σε σελίδα A4 τετραπλασιάζει και το κόστος απόδοσης και την κορυφαία δέσμευση μνήμης, και μια μη έμπιστη είσοδος που δηλώνει τεράστιο page box μπορεί να γυρίσει μια δουλειά σάρωσης σε επεισόδιο έλλειψης μνήμης. Ορίστε τα πλαφόν όσο θέλει το χειρότερο νόμιμο έγγραφό σας, και μετά αφήστε το bdsBudgetExceeded να οδηγεί τις εξαιρέσεις σε πιο αργό, απομονωμένο μονοπάτι

Αν τα έγγραφά σας αναμειγνύουν ετικέτες αναγνώσιμες από μηχανή με τυπωμένο κείμενο που σκοπεύετε να ευρετηριάσετε, ο barcode decoder ταιριάζει φυσικά με τη μηχανή αναγνώρισης που καλύπτει το template-matching OCR μέσα στο HotPDF, και η πλευρά παραγωγής της ίδιας ιστορίας είναι στο ζωγράφισμα barcodes σε PDF με HotPDF. Και τα δύο τρέχουν πάνω στην ίδια υποδομή απόδοσης και budgets, οπότε ένα pipeline που ήδη ορίζει λογικά όρια για το ένα παίρνει το άλλο σχεδόν δωρεάν

Η ανοχή περιστροφής είναι από εκείνα τα χαρακτηριστικά που είναι αόρατα όταν δουλεύουν και εκνευριστικά όταν δεν δουλεύουν, και το μηχανικό δίδαγμα γενικεύει πέρα από το QR: κανονικοποιήστε όσο πιο κοντά στη σημασιολογική αναπαράσταση γίνεται, όχι στο στρώμα pixels όπου τα δεδομένα κουβαλάνε ακόμα κάθε τυχαιότητα του πώς καταγράφηκαν. Το HotPDF το παραδίδει ως μέρος του HotPDF Delphi PDF component, μαζί με τα κομμάτια απόδοσης, OCR και ανάλυσης σελίδων που τα ίδια intake pipelines χρειάζονται συνήθως