Teknisk artikkel

HotPDF OCR tekstlagsforskyvning: mapping gjennom CropBox-en

OCR-tekstlag, strekkodebokser og ansiktssvertingsbokser driver på beskårede PDF-sider når bitmappiksler mappes tilbake gjennom MediaBox i stedet for boksen rendereren faktisk rasteriserte: CropBox klippet mot MediaBox (ISO 32000-1 §14.11.2). HotPDF fikset dette for ApplyLoadedOCRTextLayer i v2.770.153, og for DecodeLoadedPageBarcodes og DetectLoadedRedactionFindings i v2.770.154

Bug-rapporten som vanligvis ankommer, ser slik ut. Et skannet kontraktsarkiv går gjennom OCR, utgangen er søkbar, og søketreffet for et paragrafnummer fremheves en halv tomme under og til venstre for det trykte tallet. De fleste filene i bunken er fine. De ødelagte kom alle fra én skannestasjon som skriver en /CropBox for å trimme kanten fra glasset. Den ene detaljen skiller bildet OCR-motoren så fra rammen tekstlaget ble plassert i, og samme mismatch flytter strekkodebokser og, alvorligere, ansiktssvertingsbokser

Hvorfor driver OCR-tekstlaget bort fra de skannede ordene?

Tekstlaget driver fordi to halvdeler av pipelinen var uenige om hvilket rektangel bitmapen dekker. I v2.766.64 endret HotPDF rendering, SVG-eksport, viewer-en og utskrift til å ære CropBox: en side vises gjennom sin CropBox klippet mot sin MediaBox, som er det ISO 32000-1 §14.11.2 forskriver, og GetLoadedPageVisibleBox ble lagt til for å returnere den synlige boksen. Gjenkjenningsfunksjonene fortsatte å bygge sin device-til-side-transform fra GetLoadedPageBox(PageIndex, pbMediaBox, ...). Rasteren dekket nå den synlige boksen, transformen antok fortsatt MediaBox, og hver gjenkjent posisjon kom tilbake forskjøvet med gapet mellom de to

Det berørte vinduet er derfor presist. ApplyLoadedOCRTextLayer plasserte tekst feil fra v2.766.64 gjennom v2.770.152. Hele-sides DecodeLoadedPageBarcodes og ansiktsdeteksjonen inne i DetectLoadedRedactionFindings forble gale én build lenger, gjennom v2.770.153. Før v2.766.64 tegnet rendereren hele MediaBox, så mapping og raster var enige, på bekostning av å gjenkjenne innhold viewere aldri viser. Fiksene endret tre ting sammen for hver funksjon: transformen, pikselbudsjettanslaget, og sideboksen gitt til en custom motor i request-recorden

Flere tilfeller ble aldri berørt:

  • Sider uten en /CropBox, eller hvis CropBox liker MediaBox, mapper identisk før og etter fiksen
  • DecodeLoadedPageBarcodes med HasRegion satt rendrer nøyaktig regionen du sender inn, og mapper gjennom samme region, så eksplisitt region-dekoding var korrekt hele veien; sjekken av at regionen ligger inne i siden bruker fortsatt MediaBox
  • Mønsterbaserte svertingsfunn (e-poster, kortnumre og så videre) kommer fra tekstekstraksjon i user space, ikke fra en raster, så bare ansiktsdeteksjonsfunnene flyttet seg

Tre koordinatrammer, og hvilke HotPDF API-er som bruker hver

HotPDF-kode som rører gjenkjenning forholder seg til tre rammer, og de fleste mappingsbugene kommer fra å blande to av dem

  • Bitmappiksler: origo oppe til venstre, Y vokser nedover, enheter er piksler ved forespørselens DPI. THPDFOCRWord.Left, Top, Right og Bottom er i denne rammen, likeså de valgfrie baseline-punktene, resultatene en custom IHPDFBarcodeDecoder returnerer, og boksene fra en custom IHPDFFaceDetector
  • PDF user space for en lastet side: origo nede til venstre, Y vokser oppover, enheter er punkter, med Bottom < Top. GetLoadedPageBox og GetLoadedPageVisibleBox returnerer Left, Bottom, Right, Top i denne rammen, og det gjør også PageLeft-, PageBottom-, PageRight- og PageTop-feltene i THPDFOCRRequest, grensene i THPDFDecodedBarcode og rektanglene i THPDFRedactionFinding
  • HotPDF side-tegnekoordinater: API-en du bruker til å bygge nye sider (tekstutgang, former, strekkoder, lenker, skjemafelter) virker med origo oppe til venstre og Y voksende nedover. Den rammen tilhører dokumentgenerering og har ingenting med de lastede dokument-API-ene over å gjøre, så mat aldri et user space-rektangel fra en lastet side inn i den uendret

OCR-ordrecorden er med vilje pikselbasert: en motor rapporterer hva den så i bildet, og ApplyLoadedOCRTextLayer eier konverteringen. Den delingen virker bare når konverteringen bruker riktig boks, noe v2.770.153 gjenopprettet

HotPDF gjenkjenningskoordinatrammer: bitmappiksler med origo oppe til venstre brukt av THPDFOCRWord-bokser og custom dekodere, PDF user space med origo nede til venstre returnert av GetLoadedPageBox og GetLoadedPageVisibleBox, og side-tegne-API-et med origo oppe til venstre, som aldri må motta et rektangel fra en lastet side uendret
Motorer rapporterer piksler fordi det er det de så, HotPDF mapper dem, og å blande de to rammene er slik lag og svertingsbokser driver

Device-til-side-transformen bak OCR, strekkoder og ansikter

HotPDF mapper bitmappiksler til siden med én enkelt affin matrise bygget fra fem innganger: rotasjonen, skalaen DPI / 72, bitmap-høyden, og Left, Bottom, Right og Top til den renderte boksen. OCR, strekkodedekoding og ansiktsdeteksjon deler én rutine for det, noe som er grunnen til at én gal boks-inngang knuste alle tre på samme måte. For en urotet side er side-til-device-matrisen [A B C D E F]:

  • A = Scale og D = -Scale, der Scale = DPI / 72; den negative D vender user space (Y opp) om til bitmap space (Y ned)
  • B = C = 0, for en urotet side har ingen shear eller bytte mellom akser
  • E = -Left * Scale, som flytter boksens venstre kant til pikselkolonne 0
  • F = BitmapHeight + Bottom * Scale, som mapper boksens bunnkant til y = BitmapHeight, den nedre kanten av bitmapen, så toppkanten lander på rad 0

Piksler går tilbake til siden gjennom inversen av den matrisen. Request.PageRotation bærer sidens /Rotate normalisert til 0, 90, 180 eller 270 (enhver verdi som ikke er et multiplum av 90 behandles som 0), og rendereren snur siden med klokken slik ISO 32000-1 §7.7.3.3 krever. Under rotasjon bytter aksene plass, og et annet par boks-kanter festes til bitmap-origo. Skrevet ut som inverse formler, med S = DPI / 72, x og y i piksler og H bitmap-høyden:

/RotateSide XSide YBoks-kanter mappingen er avhengig av
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

Siste kolonne forklarer hvorfor bugen så tilfeldig ut i produksjon. En CropBox som trimmer bare toppen av siden lar Left og Bottom være urørt, så opprettede sider kom ut perfekt, og bare sider med /Rotate 270 drev. Rotasjon bytter også bitmap-dimensjonene: ved 90 og 270 er bitmapen (Top - Bottom) * S piksler bred og (Right - Left) * S piksler høy

HotPDF invers-mappingtabell for siderotasjon: ved /Rotate 0 og 90 fester transformen Left- og Bottom-kantene til den renderte boksen, ved 180 Right og Bottom, ved 270 Right og Top, noe som er grunnen til at en beskåret side driver i en annen retning for hver orientering i et blandet dokument
Samme halvtommers beskjæring ser ut som tre forskjellige buger så snart sider bærer ulike /Rotate-verdier, for hver orientering fester et annet par boks-kanter

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

Med en halvtommers beskjæring på hver side lander en urotet sides tekstlag nøyaktig 36 punkter til venstre for og 36 punkter under de skannede ordene når MediaBox brukes. Ta en US Letter-side hvis CropBox trimmer 36 punkter (0.5 tomme) fra hver kant. Den synlige boksen er 540 ganger 720 punkter, så ved standard OCR-oppløsning på 300 DPI er skalaen 300 / 72 ≈ 4.1667 og bitmapen 2250 ganger 3000 piksler

Anta at motoren rapporterer et ord med pikselboks Left 450, Top 600, Right 900, Bottom 660 og ingen baseline. HotPDF plasserer da baselinjen 20 prosent av ordhøyden over bunnkanten, ved pikselrad 648, og mapper startpunktet (450, 648):

  • Gjennom den synlige boksen: x = 36 + 450 / 4.1667 = 144.0 og y = 36 + (3000 - 648) / 4.1667 = 600.48, som er der ordet er trykt
  • Gjennom MediaBox: x = 0 + 108.0 = 108.0 og y = 0 + 564.48 = 564.48, en uniform forskyvning på (-36, -36) punkter
HotPDF CropBox-drift-anatomi på en US Letter-side med MediaBox 0 0 612 792 og CropBox 36 36 576 756: rendereren rasteriserer den synlige boksen ved 300 DPI, så mapping av ordpiksel 450 gjennom GetLoadedPageVisibleBox gir 144.0 og 600.48, mens MediaBox-transformen lander på 108.0 og 564.48
Rasteren dekker CropBox, så enhver transform bygget fra MediaBox forskjøver hvert gjenkjent ord med nøyaktig beskjæringsmarginen

Rotér samme side, og feilens retning endres, fordi ulike kanter er involvert. Ved /Rotate 180 bruker X-leddet Right, og 612 i stedet for 576 skyver laget 36 punkter til høyre mens Bottom fortsatt trekker det 36 punkter ned. Ved /Rotate 270 er både Right og Top for store, så laget flytter 36 punkter til høyre og 36 punkter opp. Et dokument med blandede orienteringer kan vise driften i tre retninger, et pålitelig fingeravtrykk for denne bugen. Håndskrevet kode som utleder skalaen fra boksen, som Bitmap.Width / (Right - Left), strekker også hver koordinat med 612 / 540, rundt 13 prosent, oppå forskyvningen

Hvilke av dine PDF-dokumenter er berørt?

Et PDF-dokument er utsatt når minst én side har en synlig boks som avviker fra sin MediaBox, og HotPDF kan fortelle deg det på noen få linjer. Sammenlign GetLoadedPageBox med pbMediaBox mot GetLoadedPageVisibleBox for hver side, og skriv ut GetLoadedPageRotation ved siden av så du kan forutsi driftsretningen fra tabellen over. THPDFPageBoundary tilbyr også pbCropBox, pbBleedBox, pbTrimBox og pbArtBox, men GetLoadedPageBox(pbCropBox) faller tilbake på MediaBox når ingen crop-boks finnes, og klipper ikke, så den synlige boksen er den riktige å sammenligne mot

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 lagrede arrayet kan liste hjørnene sine i vilkårlig rekkefø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 normalisert og beskåret mot 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 betyr noe for skript som dette. Funksjonen lar sine out-parametere være urørt når den feiler, så å forhåndsstille en standard sidestørrelse før kallet er et trygt mønster. Og når en misdannet CropBox ikke skjærer MediaBox i det hele tatt, returnerer funksjonen MediaBox i stedet for et tomt rektangel. Lister rapporten sider, og din utrulledede build er eldre enn v2.770.153 for OCR, eller v2.770.154 for strekkoder og ansikter, kjør gjenkjenning på de sidene på nytt etter oppgradering. Et OCR-lag committet av en berørt build blir i den lagrede filen, og standardvalget SkipPagesWithText vil hoppe over de sidene i et andre pass med mindre du slår det av eller fjerner det gamle laget først

Hvordan bør en custom IHPDFOCREngine mappe piksler tilbake til PDF space?

En custom IHPDFOCREngine bør returnere ordentbokser i bitmappiksler og la HotPDF gjøre mappingen; konverter til user space bare for dine egne beslutninger, og bruk da boksen fra forespørselen, aldri MediaBox. Siden v2.770.153 beskriver forespørselens PageLeft, PageBottom, PageRight og PageTop den renderte synlige boksen, så de matcher Request.Bitmap nøyaktig. Hjelperen nedenfor er inversen av bibliotekets transform, inkludert dets bruk av faktisk bitmap-høyde for opprettede sider, så den er enig med HotPDF til pikselen

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

// Bitmap-piksel (origo oppe til venstre, Y nedover) til PDF user space
// (origo nede til venstre, Y oppover), gjennom boksen bitmapen ble rendret 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 grunn til å trenge user space inne i en motor er en sone-regel: fakturaer hvis brevhode du aldri vil ha søkbart, eller et stempelområde som forvirrer gjenkjenneren. Motoren nedenfor, skrevet med TInterfacedObject slik at referansetelling håndterer levetiden dens, filtrerer ord etter hvor sentrene deres faller på siden, og returnerer så de overlevende urørt i pikselkoordinater. RunRecognizer står inn for ditt eget gjenkjenningskall

type
  TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
  private
    FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single;  // user space
    function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
      out Words: THPDFOCRWords): boolean;  // din gjenkjenner, pikselbokser
  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];  // fortsatt piksler: HotPDF mapper dem selv
    Inc(Count);
  end;
  SetLength(Words, Count);
  Result := True;
end;

Gi motoren til ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) som med enhver annen motor. Biblioteket validerer det som kommer tilbake før det stoler på det: et ord slippes og telles i Info.DroppedWordCount når boksen dets forlater bitmapen, når Right <= Left eller Bottom <= Top, eller når Confidence er utenfor 0..1 eller under MinimumConfidence. Å returnere flere ord enn MaxWordsPerPage, eller å skyve den løpende totalen forbi MaxTotalWords, feiler hele kallet med en budsjettfeil, så etterlev Request.MaxWords i motoren. Ikke konverter ordentbokser til user space før du returnerer dem; HotPDF ville behandlet punktverdiene som piksler, og laget ville kollapse mot bitmap-origo

Mappe din egen detektorutgang

Samme hjelper tjener en hjemmelaget pipeline bygget på RenderLoadedPageToBitmap, som rendrer den synlige boksen og anvender /Rotate akkurat som gjenkjenningsfunksjonene. Les boksen med GetLoadedPageVisibleBox, normaliser rotasjonen på samme måte som HotPDF, og map to motstående hjørner av hver pikselboks. Y-aksen vender, og ved 90 og 270 grader bytter aksene plass, så de mappede hjørnene kommer ut i ingen fast rekkefølge; ta minimum og maksimum av de mappede punktene, noe som også er slik HotPDF bygger strekkodegrenser

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, pikselboks
      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;

Rotasjonsatferden dekkes mer i dybden i å flate ut siderotasjon uten å ødelegge sideboksene, og strekkodedekodings-pipelinen som forbruker samme transform i å dekode roterte QR-koder fra PDF-sider. Pakker motoren din inn en ekstern gjenkjenner, viser Tesseract OCR-adapteren for søkbar PDF prosessisoleringen og kanselsiden av samme grensesnitt

Hurtigreferanse: CropBox-sikker koordinatmapping

  • Rendereren rasteriserer den synlige boksen, CropBox klippet mot MediaBox (ISO 32000-1 §14.11.2); hver piksel-til-side-mapping må bruke den boksen, lest med GetLoadedPageVisibleBox
  • HotPDF v2.770.153 fikset ApplyLoadedOCRTextLayer; v2.770.154 fikset hele-sides DecodeLoadedPageBarcodes og ansiktsfunn fra DetectLoadedRedactionFindings; builds fra v2.766.64 opp til de versjonene er berørt
  • THPDFOCRWord-bokser er bitmappiksler med origo oppe til venstre; GetLoadedPageBox og GetLoadedPageVisibleBox returnerer PDF user space med origo nede til venstre og Bottom < Top
  • Skalaen er DPI / 72; utled den fra DPI-en, aldri fra en sideboks delt inn i bitmap-bredden
  • /Rotate avgjør hvilke kanter som betyr noe: Left og Bottom ved 0 og 90, Right og Bottom ved 180, Right og Top ved 270
  • Returner OCR-ord i piksler og la HotPDF mappe dem; konverter bare for din egen filtreringslogikk
  • Kjør OCR på nytt på beskårede sider behandlet av en berørt build, og husk at SkipPagesWithText hopper over sider som allerede bærer det gamle laget

Gjenkjenningsfunksjonene, sideboks-oppslagene og rendering av lastede dokumenter brukt her skiper alle i HotPDF-komponenten for Delphi og C++Builder; lisensiering, prøvenedlastinger og full funksjonsliste ligger på HotPDF Delphi PDF-komponentsiden