Artykuł techniczny

Dekodowanie obróconych kodów QR w stronach PDF z HotPDF

HotPDF dekoduje obrócone symbole QR na załadowanej stronie PDF, normalizując próbkowaną macierz modułów przez wszystkie osiem orientacji D4 wewnątrz samego dekodera. Zewnętrzna próba rotacji, która działa dla symbologii liniowych, nie może zadziałać dla QR, a zrozumienie dlaczego oszczędza ci dnia gonienia za dekoderem, który wygląda na zepsuty, a nie jest

Scenariusz jest dość zwyczajny. Zeskanowane listy przewozowe przychodzą jako PDF-y, każda strona niesie etykietę QR, a operator skanera wsadził stos arkuszy w taką orientację, jaką przyjęła podajka. Część etykiet jest prosto, część odchylona o ćwierć obrotu, kilka do góry nogami. Wołasz dekoder kodów, połowa stron się rozwiązuje, a druga wraca pusta, bez żadnego błędu

Dlaczego obracanie maski skanu nigdy nie naprawia obróconego QR?

Bo layout wzorców finder QR jest świadomie asymetryczny, a obrót całego obrazu zachowuje tę asymetrię zamiast jej usunąć. QR Code kładzie trzy kwadraty finder w narożnikach lewym górnym, prawym górnym i lewym dolnym, a prawy dolny zostawia pusty (ISO/IEC 18004:2015 §6.3.3). Ten brakujący narożnik jest wskazówką orientacji. Obróć bitmapę strony o dziewięćdziesiąt stopni, a luka po prostu przeniesie się do innego narożnika. Nie istnieje nietrywialny obrót płaszczyzny, który odwzorowałby layout trójnarożnikowy na sam siebie, więc dekoder akceptujący wyłącznie kanoniczne ułożenie odrzuci kolejno każdą próbę

Ma to znaczenie, bo oczywista naprawa jest zła. Naturalny instynkt każe powiesić retry na zewnątrz: wyrenderuj stronę, podaj maskę dekoderowi, a jak polegnie, obróć maskę i spróbuj ponownie dla 90, 180 i 270 stopni. Dla Code 39 ta polityka jest dokładnie trafna, bo symbologia liniowa ma wzorzec start i stop, który skaner znajdzie, gdy tylko paski pobiegną poziomo. Dla QR to cztery zagwarantowane porażki, po których następuje raport nic nie znaleziono

Grupa D4 nałożona na macierz modułów

Właściwe miejsce normalizacji jest po próbkowaniu, na booleanowej siatce modułów, a nie na masce pikselowej. Gdy dekoder rozwiąże symbol do macierzy n na n modułów ciemnych i jasnych, może wyliczyć grupę diedralną kwadratu: cztery obroty razy dwa odbicia, osiem kandydackich orientacji łącznie. Dla każdego kandydata sprawdza trójkąt finderów, a pierwszy kandydat, którego trzy findery wylądują w pozycjach lewy górny, prawy górny i lewy dolny, jest prawdziwą orientacją. Stąd istniejący potok biegnie bez zmian, bo bity informacji o formacie, zygzakowe rozmieszczenie danych i korekcja Reed-Solomon wszystkie zakładają kanoniczną macierz i teraz ją dostają

Cztery renderowania tej samej macierzy modułów QR HotPDF pod obrotami grupy D4 o 0, 90, 180 i 270 stopni, pokazujące trzy wzorce finder wędrujące po narożnikach, podczas gdy pusty narożnik wędruje z nimi, więc tylko orientacja kanoniczna podaje dekoderowi findery w lewym górnym, prawym górnym i lewym dolnym
Obrót maski pikselowej nie usunie asymetrii finderów QR, więc HotPDF wylicza orientacje D4 na próbkowanej macierzy modułów i trzyma pierwszego kandydata, którego findery wylądują lewy górny, prawy górny i lewy dolny

Dwie własności robią z tego tanie rozwiązanie. Macierz jest mała w porównaniu z wyrenderowaną bitmapą, więc osiem transpozycji kosztuje znacznie mniej niż osiem renderowań strony. A macierz to czysta tablica booleanowa zbudowana przez sampler, więc żadna transformacja po drodze nie wniesie wartości, które nigdy nie były próbkowane

Detekcja wersji to przeszukiwanie podzielności, nie dzielenie

Liczby modułów nie da się wyprowadzić dzieląc próbkowaną szerokość przez założony rozmiar modułu, a pomyłka tutaj to subtelne źródło niepowodzeń dekodowania przy renderach wysokiej rozdzielczości. Symbol QR wersji v ma 4v + 17 modułów w poprzek, więc wersja 1 to 21 modułów, a wersja 40 to 177. Maska mierząca 126 pikseli szerokości jest równie zgodna z wersją 1 przy sześciu pikselach na moduł, co z kilkoma wyższymi wersjami przy mniejszych rozmiarach modułu. Liniowe dzielenie wybiera jedno z nich i zwykle jest w błędzie

Działa przeszukiwanie podzielności po kandydujących wersjach. Idź od wersji 40 w dół do wersji 1, zatrzymaj kandydatów, których liczba modułów dzieli próbkowaną szerokość bez reszty i zostawia co najmniej trzy piksele na moduł, i weź najmniejszą przetrwałą wersję. Podłoga trzech pikseli pilnuje, by wyszukiwanie nie przyjęło absurdalnie gęstego odczytu grubego symbolu, a reguła najmniejszej wersji rozstrzyga pozostałą dwuznaczność na korzyść odczytu, jaki faktycznie wyprodukowałby skaner

Przebieg detekcji wersji HotPDF dla symbolu QR na próbkowanej masce 126 pikseli, testujący każdą kandydującą liczbę modułów 4v plus 17 od wersji 40 w dół do wersji 1 pod kątem podzielności bez reszty i podłogi trzech pikseli na moduł, zanim wygra najmniejsza przetrwała wersja
Liczba modułów QR bierze się z przeszukania podzielności po kandydujących wersjach, a nie z dzielenia szerokości maski przez założony rozmiar modułu, a najmniejsza przetrwała wersja rozstrzyga dwuznaczność
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 oddaje wypełniony rekord, a nie wyzerowany, co ma znaczenie, bo DPI równe zero albo limit wyników zero to wyglądający poprawnie sposób, by nie dostać nic. RotationPolicy steruje wyłącznie zewnętrznym retrym: bdrpNone renderuje raz, bdrpFallback powtarza pozostałe orientacje po nieudanym pierwszym przebiegu, a bdrpAll renderuje każdą orientację bezwarunkowo. Ponieważ normalizacja QR dzieje się wewnątrz dekodera, strony QR rozwiążą się za pierwszym podejściem przy każdej z trzech polityk. Polityka jest tam dla symbologii liniowych, które naprawdę jej potrzebują

Jak udowodnić, że transformacja bitmapy nie wymyśla pikseli?

Policz atrament po obu stronach i wymagaj, by sumy się zgadzały. Obrót to permutacja pikseli i nic więcej, więc liczba niezerowych komórek w wyjściu musi równać się liczbie w wejściu. Gdy obrót maski w ścieżce zewnętrznego retrya zgłosił 4800 ustawionych komórek na wejściu i 7439 na wyjściu, to pojedyncze porównanie wystarczyło, by skazać transformację bez czytania ani linii jej geometrii

Przyczyna była przyziemna i warta zapamiętania jako reguła. Tablica dynamiczna wymiarowana SetLength nie ma gwarancji, że przyjdzie wyzerowana, gdy jest wynikiem funkcji podróżującym ścieżką, którą runtime nie czyści, i komórki, których obrót nigdy nie zapisze, niosą wtedy cokolwiek, co tam było przedtem. Część tych nieświeżych bajtów jest niezerowa, a niezerowe znaczy atrament. Naprawa to jedna linia, FillChar(Result[0], N, 0) przed startem pętli permutacji, a dyscyplina, którą implikuje, jest szersza: każda funkcja zwracająca maskę albo bufor bitmapy powinna jawnie wyczyścić swoje wyjście, zamiast polegać na semantyce alokacji

To, co pozwoliło defektowi przetrwać trzy wydania, jest ciekawsze niż defekt. Gdy QR przeniósł swoją obsługę orientacji do dekodera, QR przestał całkowicie ćwiczyć zewnętrzny obrót maski, a jedynym pozostającym konsumentem tej ścieżki kodu został Code 39. Współdzielona infrastruktura chowa takie błędy non stop: pokrycie z jednej funkcji sprawia, że ścieżka wygląda na przetestowaną, podczas gdy funkcja faktycznie od niej zależna nie ma nic własnego. Każda ścieżka, której nowa funkcja przestaje używać, potrzebuje testu, który nadal jej używa

Odczyt wyników we współrzędnych strony

Każda wartość geometryczna produkowana przez dekoder jest wyrażona w układzie współrzędnych bitmapy próby, a wołający potrzebuje jej w przestrzeni użytkownika PDF-a. Ta konwersja biegnie w dwóch etapach: cofnij ćwierć obrotu nałożony przez retry, potem cofnij transformację renderowania, która odwzorowała przestrzeń użytkownika na bitmapę. To, co przychodzi w THPDFDecodedBarcode, to prostokąt otaczający wyrównany do osi w przestrzeni użytkownika, z Left, Bottom, Right i Top według konwencji PDF, że Y rośnie w górę, plus liczący się przeciwnie do wskazówek zegara OrientationDegrees

Potok kodów HotPDF od wyrenderowanej bitmapy strony przez próbkowanie do booleanowej macierzy modułów, normalizację D4, detekcję wersji przez podzielność i dekodowanie Reed-Solomon, a potem dwuetapową konwersję współrzędnych cofającą ćwierć obrotu retrya i transformację renderowania, zanim THPDFDecodedBarcode opublikuje Left, Bottom, Right, Top i OrientationDegrees w przestrzeni użytkownika
Normalizacja QR wewnątrz dekodera pozwala stronom rozwiązywać się za pierwszym podejściem, a dwuetapowa konwersja współrzędnych zamienia wyniki z bitmapy próby na proste wyrównane do osi w przestrzeni użytkownika

Pomyłka w kierunku tej drugiej konwersji daje paskudny objaw: tekst dekoduje się doskonale, ale box, który rysujesz jako warstwę przeglądu, ląduje na lustrzanym odbiciu właściwej pozycji. Każdy budujący interfejs przeglądu na dekoderze powinien asertować przeciw znanej fixture, z symbolem umieszczonym celowo blisko jednego narożnika strony, żeby odwrócona oś Y była widoczna na pierwszy rzut oka. To samo rozumowanie dotyczy każdej współrzędnej przekraczającej granicę renderowania, dlatego renderowanie strony PDF do bitmapy w Delphi warto zrozumieć, zanim zaczniesz budować na dekoderze

Co wbudowany dekoder zrobi, a czego nie

Wbudowany dekoder to ograniczona implementacja bez zależności i jest szczera co do swoich limitów, zamiast cicho się degradować. Rozpoznaje Code 39 i QR, waliduje bity formatu chronione BCH oraz wzorzec maski, zanim opublikuje jakiekolwiek dane, i nie próbuje odzyskiwać błędów na uszkodzonych symbolach. Jeśli twoje wejście to fotografia zakrzywionej etykiety pod nierównym światłem, to inna klasa problemu i chce sobie wyspecjalizowany silnik

// Podmień na własny silnik: zaimplementuj IHPDFBarcodeDecoder i przekaż go
// do przeciążenia świadomego dekoderów. HotPDF nadal włada renderowaniem
// stron, budżetami, mapowaniem współrzędnych i deduplikacją
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 to miejsce, gdzie produkcyjny potok zarabia na swoje utrzymanie. RotationAttemptCount i DecoderCallCount mówią ci, czy zewnętrzny retry w ogóle chodził, ReceivedResultCount na tle AcceptedResultCount oddziela dekoder, który nic nie znalazł, od progu pewności odrzucającego wszystko, co znalazł, a RenderedPixels z PeakWorkingBytes to to, co wykreślasz, gdy zadanie wsadowe zaczyna thrashować. Pusty zbiór wyników plus bdsSucceeded znaczy, że strona naprawdę nie ma czytelnego symbolu, co jest innym faktem operacyjnym niż bdsBudgetExceeded

Pola budżetu zasługują na świadomą decyzję, nie na domyślne. MaxPixels i MaxWorkingBytes istnieją, bo DPI mnoży się kwadratowo: przejście z 300 na 600 DPI na stronie A4 czterokrotnie zwiększa i koszt renderowania, i szczytową alokację, a niezaufane wejście deklarujące gigantyczny box strony potrafi zamienić zadanie skanowania w incydent braku pamięci. Ustaw capa na to, czego potrzebuje twój najgorszy legalny dokument, a potem pozwól, by bdsBudgetExceeded kierował odstających na wolniejszą, izolowaną ścieżkę

Jeśli twoje dokumenty mieszają etykiety czytelne maszynowo z drukowanym tekstem, który planujesz indeksować, dekoder kodów naturalnie paruje się z silnikiem rozpoznawania opisanym w artykule OCR przez dopasowanie szablonów wewnątrz HotPDF, a strona generująca tej samej historii jest w rysowaniu kodów kreskowych do PDF-a z HotPDF. Oba chodzą na tej samej infrastrukturze renderowania i budżetów, więc potok, który już ustawił rozsądne limity dla jednego, dostaje drugiego prawie gratis

Tolerancja obrotu to jedna z tych funkcji, które są niewidoczne, gdy działają, i wkurzające, gdy nie, a lekcja inżynierska uogólnia się poza QR: normalizuj tak blisko reprezentacji semantycznej, jak się da, nie na warstwie pikselowej, gdzie dane wciąż niosą każdy wypadek tego, jak zostały przechwycone. HotPDF dostarcza to w ramach komponentu PDF HotPDF dla Delphi, razem z kawałkami renderowania, OCR i analizy stron, których te same potoki przyjęcia zwykle potrzebują