Teknisk artikel

HotPDF OCR-tekstlag offset: mapping gennem CropBox

OCR-tekstlag, barcode-grænser og face redaction-boxes driver på beskårne PDF-sider, når bitmappixels mappes tilbage gennem MediaBox i stedet for den box, rendereren faktisk rasteriserede: CropBox'en beskåret til MediaBox (ISO 32000-1 §14.11.2). HotPDF rettede det for ApplyLoadedOCRTextLayer i v2.770.153 og for DecodeLoadedPageBarcodes og DetectLoadedRedactionFindings i v2.770.154

Bug-rapporten, der normalt ankommer, ser sådan ud. Et scannet kontraktarkiv køres gennem OCR, outputtet er søgbart, og søgerammen for et paragrafnummer fremhæves en halv tomme under og til venstre for det printede nummer. De fleste filer i batchen er fine. De ødelagte kom alle fra én scanningsstation, der skriver en /CropBox for at trimme kanten fra glaspladen. Den ene detalje adskiller det billede, OCR-enginen så, fra den ramme, tekstlaget blev placeret i, og samme mismatch flytter barcode-grænser og, mere alvorligt, face redaction-boxes

Hvorfor driver OCR-tekstlaget væk fra de scannede ord?

Tekstlaget driver, fordi pipelineens to halvdele var uenige om, hvilket rektangel bitmap'en dækker. I v2.766.64 ændrede HotPDF rendering, SVG-eksport, viewer og print til at følge CropBox'en: en side vises gennem dens CropBox beskåret til dens MediaBox, hvilket er det, ISO 32000-1 §14.11.2 forskriver, og GetLoadedPageVisibleBox blev tilføjet for at returnere den synlige box. Genkendelsesfunktionerne byggede stadig deres device-to-page-transform af GetLoadedPageBox(PageIndex, pbMediaBox, ...). Rasteren dækkede nu den synlige box, transformen antog stadig MediaBox, og hver genkendt position kom tilbage forskudt med afstanden mellem de to

Det berørte vindue er derfor præcist. ApplyLoadedOCRTextLayer placerede tekst forkert fra v2.766.64 til og med v2.770.152. Helsides DecodeLoadedPageBarcodes og face detectionen inde i DetectLoadedRedactionFindings forblev forkerte én build længere, til og med v2.770.153. Før v2.766.64 tegnede rendereren hele MediaBox, så mapping og raster var enige, på bekostning af at genkende indhold, som viewere aldrig viser. Fixene ændrede tre ting sammen for hver funktion: transformen, pixel-budget-estimatet og den side-box, der gives til en tilpasset engine i request-recorden

Flere tilfælde var aldrig berørt:

  • Sider uden en /CropBox, eller hvis CropBox er lig MediaBox, mapper identisk før og efter fixet
  • DecodeLoadedPageBarcodes med HasRegion sat renderer præcis den region, du giver med, og mapper gennem samme region, så explicit-region-dekodning var korrekt hele vejen; tjekket på, at regionen ligger inde i siden, bruger stadig MediaBox
  • Mønsterbaserede redaction-fund (emails, kortnumre og så videre) kommer fra tekst-ekstraktion i user space, ikke fra en raster, så kun face-detection-funderne flyttede sig

Tre koordinatrammer, og hvilke HotPDF-API'er der bruger hver

HotPDF-kode, der rører genkendelse, arbejder med tre rammer, og de fleste mapping-bugs kommer fra at blande to af dem

  • Bitmap-pixels: origo øverst til venstre, Y vokser nedad, enheden er pixels ved anmodningens DPI. THPDFOCRWord.Left, Top, Right og Bottom er i denne ramme, ligesom de valgfrie baseline-punkter, resultaterne en tilpasset IHPDFBarcodeDecoder returnerer og boxene fra en tilpasset IHPDFFaceDetector
  • PDF user space for en indlæst side: origo nederst til venstre, Y vokser opad, enheden er punkter, med Bottom < Top. GetLoadedPageBox og GetLoadedPageVisibleBox returnerer Left, Bottom, Right, Top i denne ramme, og det gør PageLeft-, PageBottom-, PageRight- og PageTop-felterne i THPDFOCRRequest, grænserne i THPDFDecodedBarcode og rektanglerne i THPDFRedactionFinding også
  • HotPDF side-tegnekoordinater: API'et, du bruger til at bygge nye sider (tekstoutput, figurer, barcodes, links, formfelter), arbejder med origo øverst til venstre og Y voksende nedad. Den ramme tilhører dokumentgenerering og har intet at gøre med de indlæste dokumenters API'er ovenfor, så giv aldrig et indlæst sides user-space-rektangel videre til den uændret

OCR-ord-recorden er bevidst pixel-baseret: en engine melder, hvad den så i billedet, og ApplyLoadedOCRTextLayer ejer konverteringen. Den opdeling virker kun, når konverteringen bruger den rigtige box, hvilket er det, v2.770.153 genoprettede

HotPDF'ens genkendelseskoordinatrammer: bitmap-pixels med origo øverst til venstre, brugt af THPDFOCRWord-boxes og tilpassede dekodere, PDF user space med origo nederst til venstre, returneret af GetLoadedPageBox og GetLoadedPageVisibleBox, og det øverst-til-venstre side-tegne-API, som aldrig må modtage et indlæst sides-rektangel uændret
engines melder pixels, for det er det, de så, HotPDF mapper dem, og at blande de to rammer er sådan lag og redaction-boxes driver

Device-to-page-transformen bag OCR, barcodes og faces

HotPDF mapper bitmap-pixels til siden med én enkelt affin matrix bygget af fem inputs: rotationen, skalaen DPI / 72, bitmap-højden og Left, Bottom, Right og Top for den renderede box. OCR, barcode-dekodning og face detection deler alle én rutine til det, hvilket er hvorfor ét forkert box-input brød alle tre på samme måde. For en uroteret side er page-to-device-matrixen [A B C D E F]:

  • A = Scale og D = -Scale, hvor Scale = DPI / 72; den negative D vender user space (Y opad) til bitmap space (Y nedad)
  • B = C = 0, for en uroteret side har ingen shear eller bytning mellem akserne
  • E = -Left * Scale, som flytter boxens venstre kant til pixelkolonne 0
  • F = BitmapHeight + Bottom * Scale, som mapper boxens bundkant til y = BitmapHeight, bitmap'ens nederste kant, så topkanten lander på række 0

Pixels går tilbage til siden gennem den matrices inverse. Request.PageRotation bærer sidens /Rotate normaliseret til 0, 90, 180 eller 270 (enhver værdi, der ikke er multiplum af 90, behandles som 0), og rendereren drejer siden med uret, som ISO 32000-1 §7.7.3.3 kræver. Under rotation bytter akserne plads, og et andet par box-kanter fastgøres til bitmap-origo. Skrevet ud som inverse formler, med S = DPI / 72, x og y i pixels og H bitmap-højden:

/RotateSide-XSide-YBox-kanter, mappingen afhænger af
0Left + x / SBottom + (H - y) / SLeft, Bottom
90Left + y / SBottom + x / SLeft, Bottom
180Right - x / SBottom + y / SRight, Bottom
270Right - y / STop - x / SRight, Top

Sidste kolonne forklarer, hvorfor buggen så tilfældig ud i produktion. En CropBox, der kun trimmer sidens top, lader Left og Bottom stå urørt, så oprejste sider kom perfekt ud, og kun sider med /Rotate 270 drev. Rotation bytter også bitmap-dimensionerne: ved 90 og 270 er bitmap'en (Top - Bottom) * S pixels bred og (Right - Left) * S pixels høj

HotPDF'ens inverse mapping-tabel for siderotation: ved /Rotate 0 og 90 fastgør transformen Left- og Bottom-kanterne af den renderede box, ved 180 Right og Bottom, ved 270 Right og Top, hvilket er hvorfor en beskåret side driver i en anden retning for hver orientering i et blandet dokument
den samme halve tomme crop ligner tre forskellige bugs, så snart sider har forskellige /Rotate-værdier, for hver orientering fastgør et andet par box-kanter

Hvad går galt med MediaBox [0 0 612 792] og CropBox [36 36 576 756]?

Med en halv tomme crop på hver side lander en uroteret sides tekstlag præcis 36 punkter venstre for og 36 punkter under de scannede ord, når MediaBox bruges. Tag en US Letter-side, hvis CropBox trimmer 36 punkter (0,5 tomme) fra hver kant. Den synlige box er 540 gange 720 punkter, så ved default OCR-opløsning på 300 DPI er skalaen 300 / 72 ≈ 4,1667, og bitmap'en er 2250 gange 3000 pixels

Antag, at enginen melder et ord med pixel-box Left 450, Top 600, Right 900, Bottom 660 og ingen baseline. HotPDF placerer så baseline 20 procent af ordhøjden over bundkanten, ved pixelrække 648, og mapper startpunktet (450, 648):

  • Gennem den synlige box: x = 36 + 450 / 4,1667 = 144,0 og y = 36 + (3000 - 648) / 4,1667 = 600,48, hvilket er dér, hvor ordet er printet
  • Gennem MediaBox: x = 0 + 108,0 = 108,0 og y = 0 + 564,48 = 564,48, en ensartet forskydning på (-36, -36) punkter
HotPDF'ens CropBox-drift-anatomi på en US Letter-side med MediaBox 0 0 612 792 og CropBox 36 36 576 756: rendereren rasteriserer den synlige box ved 300 DPI, så mapping af ordpixel 450 gennem GetLoadedPageVisibleBox giver 144,0 og 600,48, mens MediaBox-transformen lander på 108,0 og 564,48
rasteren dækker CropBox'en, så enhver transform bygget af MediaBox flytter hvert genkendt ord med præcis crop-margenen

Rotér samme side, og fejlens retning ændrer sig, for forskellige kanter er involveret. Ved /Rotate 180 bruger X-ledet Right, og 612 i stedet for 576 skubber laget 36 punkter til højre, mens Bottom stadig trækker det 36 punkter ned. Ved /Rotate 270 er både Right og Top for store, så laget flytter 36 punkter til højre og 36 punkter op. Et dokument med blandede orienteringer kan vise drift i tre retninger, et sikkert fingeraftryk for denne bug. Håndskrevet kode, der udleder skalaen fra boxen, som Bitmap.Width / (Right - Left), strækker desuden hver koordinat med 612 / 540, cirka 13 procent, oven på offseten

Hvilke af dine PDF-dokumenter er berørt?

Et PDF-dokument er udsat, når mindst én side har en synlig box, der afviger fra dens MediaBox, og HotPDF kan fortælle dig det på få linjer. Sammenlign GetLoadedPageBox med pbMediaBox mod GetLoadedPageVisibleBox for hver side, og print GetLoadedPageRotation ved siden af, så du kan forudsige drift-retningen ud fra tabellen ovenfor. THPDFPageBoundary tilbyder også pbCropBox, pbBleedBox, pbTrimBox og pbArtBox, men GetLoadedPageBox(pbCropBox) falder tilbage til MediaBox, når ingen crop box findes, og beskærer ikke, så den synlige box er den rigtige at sammenligne med

uses
  System.SysUtils, HPDFDoc;

procedure ReportCroppedPages(const FileName: string);
var
  Pdf: THotPDF;
  I: Integer;
  ML, MB, MR, MT, VL, VB, VR, VT, Tmp: Single;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile(FileName) < 1 then
      raise Exception.Create('Cannot load ' + FileName);
    for I := 0 to Pdf.LoadedPageCount - 1 do
    begin
      if not Pdf.GetLoadedPageBox(I, pbMediaBox, ML, MB, MR, MT) then
        Continue;
      // det gemte array kan liste sine hjørner i vilkårlig rækkefølge
      if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
      if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
      // allerede normaliseret og beskåret til MediaBox
      if not Pdf.GetLoadedPageVisibleBox(I, VL, VB, VR, VT) then
        Continue;
      if (Abs(VL - ML) > 0.01) or (Abs(VB - MB) > 0.01) or
         (Abs(VR - MR) > 0.01) or (Abs(VT - MT) > 0.01) then
        Writeln(Format('Page %d  MediaBox [%g %g %g %g]  visible [%g %g %g %g]  /Rotate %d',
          [I + 1, ML, MB, MR, MT, VL, VB, VR, VT,
           Pdf.GetLoadedPageRotation(I)]));
    end;
  finally
    Pdf.Free;
  end;
end;

To detaljer ved GetLoadedPageVisibleBox betyder noget for scripts som dette. Funktionen lader sine out-parametre stå urørt, når den fejler, så at forudindstille en default sidestørrelse før kaldet er et sikkert mønster. Og når en misdannet CropBox slet ikke skærer MediaBox, returnerer funktionen MediaBox frem for et tomt rektangel. Lister rapporten sider, og din deployerede build er ældre end v2.770.153 for OCR, eller v2.770.154 for barcodes og faces, så genkør genkendelsen på de sider efter opgradering. Et OCR-lag committeret af en berørt build bliver i den gemte fil, og default-optionen SkipPagesWithText springer de sider over i anden gennemkørsel, medmindre du slår den fra eller fjerner det gamle lag først

Hvordan bør en tilpasset IHPDFOCREngine mappe pixels tilbage til PDF space?

En tilpasset IHPDFOCREngine bør returnere ord-boxes i bitmap-pixels og lade HotPDF gøre mappingen; konvertér kun til user space til dine egne beslutninger, og brug så boxen fra requesten, aldrig MediaBox. Siden v2.770.153 beskriver requestens PageLeft, PageBottom, PageRight og PageTop den renderede synlige box, så de matcher Request.Bitmap præcist. Hjælperen nedenfor er inversen af bibliotekets transform, inklusive dets brug af den faktiske bitmap-højde for oprejste sider, så den er enig med HotPDF til pixel

uses
  System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;

// Bitmap-pixel (top-left origo, Y nedad) til PDF user space
// (bottom-left origo, Y opad), gennem den box, bitmap'en blev renderet fra
procedure HotPixelToPage(Rotation, DPI, BitmapHeight: Integer;
  Left, Bottom, Right, Top: Single; X, Y: Double;
  out PageX, PageY: Double);
var
  S: Double;
begin
  S := DPI / 72.0;
  case Rotation of
    90:  begin PageX := Left + Y / S;  PageY := Bottom + X / S; end;
    180: begin PageX := Right - X / S; PageY := Bottom + Y / S; end;
    270: begin PageX := Right - Y / S; PageY := Top - X / S; end;
  else
    PageX := Left + X / S;
    PageY := Bottom + (BitmapHeight - Y) / S;
  end;
end;

En realistisk grund til at behøve user space inde i en engine er en zoneregel: fakturaer, hvis brevhoved du aldrig vil have søgbart, eller et stempeleje, der forvirrer recognizeren. Enginen nedenfor, skrevet med TInterfacedObject, så reference counting håndterer dens levetid, filtrerer ord efter, hvor deres centre lander på siden, og returnerer derefter de overlevende urørt i pixelkoordinater. RunRecognizer står i stedet for dit eget recognizer-kald

type
  TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
  private
    FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single;  // user space
    function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
      out Words: THPDFOCRWords): boolean;  // din recognizer, pixel-boxes
  public
    constructor Create(SkipLeft, SkipBottom, SkipRight, SkipTop: Single);
    function GetName: AnsiString;
    function Recognize(const Request: THPDFOCRRequest;
      out Words: THPDFOCRWords; out Diagnostic: AnsiString): boolean;
  end;

function TZoneFilterOCREngine.Recognize(const Request: THPDFOCRRequest;
  out Words: THPDFOCRWords; out Diagnostic: AnsiString): boolean;
var
  Raw: THPDFOCRWords;
  I, Count: Integer;
  CX, CY: Double;
begin
  Diagnostic := '';
  SetLength(Words, 0);
  if not RunRecognizer(Request.Bitmap, Request.MaxWords, Raw) then
  begin
    Diagnostic := 'recognizer failed';
    Exit(False);
  end;
  SetLength(Words, Length(Raw));
  Count := 0;
  for I := 0 to High(Raw) do
  begin
    HotPixelToPage(Request.PageRotation, Request.DPI,
      Request.Bitmap.Height, Request.PageLeft, Request.PageBottom,
      Request.PageRight, Request.PageTop,
      (Raw[I].Left + Raw[I].Right) / 2, (Raw[I].Top + Raw[I].Bottom) / 2,
      CX, CY);
    if (CX >= FSkipLeft) and (CX <= FSkipRight) and
       (CY >= FSkipBottom) and (CY <= FSkipTop) then
      Continue;
    Words[Count] := Raw[I];  // stadig pixels: HotPDF mapper dem selv
    Inc(Count);
  end;
  SetLength(Words, Count);
  Result := True;
end;

Giv enginen videre til ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) som med enhver anden engine. Biblioteket validerer, hvad der kommer tilbage, før det stoler på det: et ord droppes og tælles i Info.DroppedWordCount, når dets box forlader bitmap'en, når Right <= Left eller Bottom <= Top, eller når Confidence er uden for 0..1 eller under MinimumConfidence. At returnere flere ord end MaxWordsPerPage, eller skubbe det løbende total forbi MaxTotalWords, fejler hele kaldet med en budgetfejl, så hold Request.MaxWords i enginen. Konvertér ikke ord-boxes til user space, før du returnerer dem; HotPDF ville behandle punktværdierne som pixels, og laget ville kollapse mod bitmap-origo

At mappe din egen detector-output

Samme hjælper tjener en egen pipeline bygget på RenderLoadedPageToBitmap, som renderer den synlige box og anvender /Rotate ligesom genkendelsesfunktionerne. Læs boxen med GetLoadedPageVisibleBox, normalisér rotationen på samme måde som HotPDF, og map to modstående hjørner af hver pixel-box. Y-aksen vender, og ved 90 og 270 grader bytter akserne plads, så de mappede hjørner kommer ud i ingen fast rækkefølge; tag minimum og maksimum af de mappede punkter, hvilket også er sådan HotPDF bygger barcode-grænser

const
  DPI = 200;
var
  Pdf: THotPDF;
  Bmp: TBitmap;
  VL, VB, VR, VT: Single;
  Rotation: Integer;
  PxL, PxT, PxR, PxB, X1, Y1, X2, Y2: Double;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-ids.pdf');
    if not Pdf.GetLoadedPageVisibleBox(0, VL, VB, VR, VT) then Exit;
    Rotation := Pdf.GetLoadedPageRotation(0) mod 360;
    if Rotation < 0 then Inc(Rotation, 360);
    if (Rotation <> 90) and (Rotation <> 180) and (Rotation <> 270) then
      Rotation := 0;
    Bmp := Pdf.RenderLoadedPageToBitmap(0, DPI);
    if Bmp = nil then Exit;
    try
      MyDetector(Bmp, PxL, PxT, PxR, PxB);  // din kode, pixel-box
      HotPixelToPage(Rotation, DPI, Bmp.Height, VL, VB, VR, VT,
        PxL, PxT, X1, Y1);
      HotPixelToPage(Rotation, DPI, Bmp.Height, VL, VB, VR, VT,
        PxR, PxB, X2, Y2);
      Writeln(Format('User-space box [%.2f %.2f %.2f %.2f]',
        [Min(X1, X2), Min(Y1, Y2), Max(X1, X2), Max(Y1, Y2)]));
    finally
      Bmp.Free;
    end;
  finally
    Pdf.Free;
  end;
end;

Rotationsadfærden er dækket mere i dybden i at fladtrykke siderotation uden at ødelægge sideboxes, og barcode-dekodningspipelinen, der bruger samme transform, i dekodning af roterede QR-koder fra PDF-sider. Wrapper din engine en ekstern recognizer, viser Tesseract OCR-adapteren til søgbar PDF procesisolering og cancellation-siden af samme interface

Hurtig reference: CropBox-sikker koordinatmapping

  • Rendereren rasteriserer den synlige box, CropBox'en beskåret til MediaBox (ISO 32000-1 §14.11.2); enhver pixel-til-side-mapping skal bruge den box, læst med GetLoadedPageVisibleBox
  • HotPDF v2.770.153 rettede ApplyLoadedOCRTextLayer; v2.770.154 rettede helsides DecodeLoadedPageBarcodes og face-fund fra DetectLoadedRedactionFindings; builds fra v2.766.64 op til de versioner er berørt
  • THPDFOCRWord-boxes er bitmap-pixels med origo øverst til venstre; GetLoadedPageBox og GetLoadedPageVisibleBox returnerer PDF user space med origo nederst til venstre og Bottom < Top
  • Skalaen er DPI / 72; udled den fra DPI'en, aldrig fra en side-box divideret ind i bitmap-bredden
  • /Rotate afgør, hvilke kanter der betyder noget: Left og Bottom ved 0 og 90, Right og Bottom ved 180, Right og Top ved 270
  • Returnér OCR-ord i pixels, og lad HotPDF mappe dem; konvertér kun til din egen filtreringslogik
  • Genkør OCR på beskårne sider behandlet af en berørt build, og husk, at SkipPagesWithText springer sider over, der allerede bærer det gamle lag

Genkendelsesfunktionerne, side-box-forespørgslerne og indlæst dokument-rendering, der bruges her, skiber alle i HotPDF-komponenten til Delphi og C++Builder; licensering, trial-downloads og den fulde funktionsliste er på HotPDF Delphi PDF-komponentsiden