Tehnički članak

HotPDF OCR pomak tekstualnog sloja: mapiranje kroz CropBox

OCR tekstualni slojevi, okviri barkodova i okviri zamračivanja lica odskaču na obrezanim PDF stranicama kad se bitmapni pikseli mapiraju natrag kroz MediaBox umjesto kroz okvir koji je renderer doista rasterizirao: CropBox obrezan na MediaBox (ISO 32000-1 §14.11.2). HotPDF je to popravio za ApplyLoadedOCRTextLayer u v2.770.153, a za DecodeLoadedPageBarcodes i DetectLoadedRedactionFindings u v2.770.154

Prijave bugova koje obično stignu izgledaju ovako. Skenirani arhiv ugovora prolazi kroz OCR, izlaz je pretraživ, i pogodak pretraživanja za brojem stavke istaknut je pola inča ispod i lijevo od tiskanog broja. Većina datoteka u seriji u redu je. Pokvarene sve su došle s jedne skenirajuće stanice koja zapisuje /CropBox da obreže rub stakla. Taj jedan detalj odvaja sliku koju je OCR engine vidio od okvira u koji je tekstualni sloj postavljen, i isto nepoklapanje mijenja okvire barkodova i, ozbiljnije, okvire zamračivanja lica

Zašto se OCR tekstualni sloj odmiče od skeniranih riječi?

Tekstualni se sloj odmiče jer se dvije polovice pipelinea nisu slagale oko toga koji pravokutnik bitmapa pokriva. U v2.766.64 HotPDF je promijenio renderiranje, SVG izvoz, preglednik i ispis da poštuje CropBox: stranica se prikazuje kroz svoj CropBox obrezan na MediaBox, što propisuje ISO 32000-1 §14.11.2, i dodan je GetLoadedPageVisibleBox da vraća taj vidljivi okvir. Prepoznavne značajke nastavile su graditi svoju device-to-page transformaciju iz GetLoadedPageBox(PageIndex, pbMediaBox, ...). Raster sada pokriva vidljivi okvir, transformacija je i dalje pretpostavljala MediaBox, i svaka se prepoznata pozicija vraćala pomaknuta za razmak između njih dvaju

Zahvaćeni je prozor stoga precizan. ApplyLoadedOCRTextLayer je krivo postavljao tekst od v2.766.64 do v2.770.152. Cjelostanični DecodeLoadedPageBarcodes i detekcija lica unutar DetectLoadedRedactionFindings ostali su pogrešni verziju duže, do v2.770.153. Prije v2.766.64 renderer je crtao cijeli MediaBox, pa su se mapiranje i raster poklapali, po cijenu prepoznavanja sadržaja koji preglednici nikad ne prikazuju. Popravci su zajedno promijenili tri stvari za svaku značajku: transformaciju, procjenu pikselnog proračuna i okvir stranice predan prilagođenom engineu u zapisu zahtjeva

Nekoliko slučajeva nikad nije bilo zahvaćeno:

  • Stranice bez /CropBox-a, ili čiji je CropBox jednak MediaBoxu, mapiraju se identično prije i poslije popravka
  • DecodeLoadedPageBarcodes s postavljenim HasRegion renderira točno regiju koju prenesete i mapira kroz tu istu regiju, pa je dekodiranje eksplicitne regije cijelo vrijeme bilo ispravno; provjera da regija leži unutar stranice i dalje koristi MediaBox
  • Nalazi zamračivanja temeljeni na obrascima (e-pošta, brojevi kartica i tako dalje) dolaze iz izdvajanja teksta u user spaceu, a ne iz rastera, pa su se pomaknuli samo nalazi detekcije lica

Tri koordinatna okvira i koje HotPDF API-je svaki koristi

HotPDF kod koji dira prepoznavanje bavi se s tri okvira, i većina bugova mapiranja dolazi od miješanja dva od njih

  • Bitmapni pikseli: ishodište gore lijevo, Y raste prema dolje, jedinice su pikseli na zahtjevani DPI. THPDFOCRWord.Left, Top, Right i Bottom u su tom okviru, kao i neobavezne baseline točke, rezultati koje vraća prilagođeni IHPDFBarcodeDecoder i okviri od prilagođenog IHPDFFaceDetector
  • PDF user space učitane stranice: ishodište dolje lijevo, Y raste prema gore, jedinice su točke, s Bottom < Top. GetLoadedPageBox i GetLoadedPageVisibleBox vraćaju Left, Bottom, Right, Top u tom okviru, kao i polja PageLeft, PageBottom, PageRight i PageTop od THPDFOCRRequest, granice u THPDFDecodedBarcode i pravokutnici u THPDFRedactionFinding
  • HotPDF koordinate crtanja stranica: API kojim gradite nove stranice (tekstovni izlaz, oblici, barkodovi, poveznice, polja obrazaca) radi s ishodištem gore lijevo i Y rastućim prema dolje. Taj okvir pripada generiranju dokumenata i nema nikakve veze s gore navedenim API-jima učitanih dokumenata, pa nikad ne uvodite u njega pravokutnik user spacea učitane stranice nepromijenjen

OCR zapis riječi namjerno je pikselno baziran: engine javlja što je vidio u slici, a ApplyLoadedOCRTextLayer posjeduje pretvorbu. Ta podjela radi samo kad pretvorba koristi pravi okvir, što je v2.770.153 vratio

HotPDF prepoznavni koordinatni okviri: bitmapni pikseli s ishodištem gore lijevo koje koriste THPDFOCRWord okviri i prilagođeni dekoderi, PDF user space s ishodištem dolje lijevo koji vraćaju GetLoadedPageBox i GetLoadedPageVisibleBox, i top-left API crtanja stranica, koji nikad ne smije primiti pravokutnik učitane stranice nepromijenjen
enginei javljaju piksele jer to je ono što su vidjeli, HotPDF ih mapira, i miješanje ta dva okvira način je na koji se slojevi i okviri zamračivanja odmiču

Device-to-page transformacija iza OCR-a, barkodova i lica

HotPDF mapira bitmapne piksele na stranicu jednom affinom matricom građenom iz pet ulaza: rotacije, mjerila DPI / 72, visine bitmape te Left, Bottom, Right i Top renderiranog okvira. OCR, dekodiranje barkodova i detekcija lica svi dijele jednu rutinu za to, pa je jedan pogrešan ulaz okvira slomio sve tri na isti način. Za nerotiranu stranicu page-to-device matrica [A B C D E F] jest:

  • A = Scale i D = -Scale, gdje je Scale = DPI / 72; negativan D okreće user space (Y gore) u bitmapni prostor (Y dolje)
  • B = C = 0, jer nerotirana stranica nema smicanja ni zamjene između osi
  • E = -Left * Scale, što pomjera lijevi rub okvira na pikselni stupac 0
  • F = BitmapHeight + Bottom * Scale, što mapira donji rub okvira na y = BitmapHeight, donji rub bitmape, pa gornji rub doskoči na redak 0

Pikseli se vraćaju na stranicu kroz inverz te matrice. Request.PageRotation nosi /Rotate stranice normaliziran na 0, 90, 180 ili 270 (svaka vrijednost koja nije višekratnik broja 90 tretira se kao 0), i renderer okreće stranicu u smjeru kazaljke kako zahtijeva ISO 32000-1 §7.7.3.3. Pod rotacijom osi se mijenjaju i drugi par rubova okvira pribija se na ishodište bitmape. Napisano kao inverzne formule, s S = DPI / 72, x i y u pikselima i H visinom bitmape:

/RotateStranica XStranica YRubovi okvira od kojih mapiranje ovisi
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

Zadnji stupac objašnjava zašto je bug u proizvodnji izgledao nasumičan. CropBox koji reže samo vrh stranice ostavlja Left i Bottom netaknutima, pa su uspravne stranice izašle savršeno i samo su stranice s /Rotate 270 odstupale. Rotacija također mijenja dimenzije bitmape: na 90 i 270 bitmapa je (Top - Bottom) * S piksela široka i (Right - Left) * S piksela visoka

HotPDF tablica inverznog mapiranja za rotaciju stranice: na /Rotate 0 i 90 transformacija pribija Left i Bottom rubove renderiranog okvira, na 180 Right i Bottom, na 270 Right i Top, pa obrezana stranica odstupa u drugom smjeru za svaku orijentaciju u mješovitom dokumentu
isti rez od pola inča izgleda kao tri različita buga jednom kad stranice nose različite /Rotate vrijednosti, jer svaka orijentacija pribija drugi par rubova okvira

Što pođe po zlu s MediaBox [0 0 612 792] i CropBox [36 36 576 756]?

S rezom od pola inča na svakoj strani tekstualni sloj nerotirane stranice doskoči točno 36 točaka lijevo i 36 točaka ispod skeniranih riječi kad se koristi MediaBox. Uzmite US Letter stranicu čiji CropBox reže 36 točaka (0,5 inča) s ruba. Vidljivi okvir je 540 puta 720 točaka, pa je pri zadanoj OCR rezoluciji od 300 DPI mjerilo 300 / 72 ≈ 4.1667, a bitmapa 2250 puta 3000 piksela

Pretpostavimo da engine javlja riječ s pikselnim okvirom Left 450, Top 600, Right 900, Bottom 660 i bez baselinea. HotPDF tada postavlja baseline 20 posto visine riječi iznad donjeg ruba, na pikselnom retku 648, i mapira početnu točku (450, 648):

  • Kroz vidljivi okvir: x = 36 + 450 / 4.1667 = 144.0 i y = 36 + (3000 - 648) / 4.1667 = 600.48, što je mjesto gdje je riječ tiskana
  • Kroz MediaBox: x = 0 + 108.0 = 108.0 i y = 0 + 564.48 = 564.48, jednolik pomak od (-36, -36) točaka
HotPDF CropBox anatomija odstupanja na US Letter stranici s MediaBox 0 0 612 792 i CropBox 36 36 576 756: renderer rasterizira vidljivi okvir na 300 DPI, pa mapiranje piksela riječi 450 kroz GetLoadedPageVisibleBox daje 144.0 i 600.48 dok transformacija MediaBoxom doskoči na 108.0 i 564.48
raster pokriva CropBox, pa svaka transformacija građena iz MediaBoxa pomjera svaku prepoznatu riječ točno za marginu reza

Okrenite istu stranicu i smjer pogreške se mijenja, jer su umiješani različiti rubovi. Na /Rotate 180 X član koristi Right, i 612 umjesto 576 tjera sloj 36 točaka udesno dok ga Bottom i dalje vuče 36 točaka dolje. Na /Rotate 270 i Right i Top preveliki su, pa se sloj pomjera 36 točaka udesno i 36 točaka gore. Dokument s mješovitim orijentacijama može pokazati odstupanje u tri smjera, pouzdani otisak ovog buga. Ručno pisani kod koji izvodi mjerilo iz okvira, poput Bitmap.Width / (Right - Left), također rasteže svaku koordinatu s 612 / 540, otprilike 13 posto, na vrhu pomaka

Koji su vaši PDF dokumenti zahvaćeni?

PDF dokument je izložen kad barem jedna stranica ima vidljivi okvir koji se razlikuje od njezina MediaBoxa, i HotPDF vam to može reći u par linija. Usporedite GetLoadedPageBox s pbMediaBox protiv GetLoadedPageVisibleBox za svaku stranicu, i ispišite GetLoadedPageRotation uz to da iz tablice gore možete predvidjeti smjer odstupanja. THPDFPageBoundary nudi i pbCropBox, pbBleedBox, pbTrimBox i pbArtBox, ali GetLoadedPageBox(pbCropBox) vraća se na MediaBox kad crop box ne postoji i ne obreže, pa je vidljivi okvir prava stvar za usporedbu

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;
      // spremljeno polje može izlistati svoje uglove u bilo kojem redoslijedu
      if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
      if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
      // već normalizirano i obrezano na 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;

Dva detalja GetLoadedPageVisibleBoxa važna su za skripte poput ove. Funkcija ostavlja svoje out parametre netaknutima kad padne, pa je predpostavljanje zadane veličine stranice prije poziva siguran obrazac. A kad neispravan CropBox uopće ne siječe MediaBox, funkcija vraća MediaBox umjesto praznog pravokutnika. Ako izvještaj izlista stranice, a vaša raspoređena verzija je starija od v2.770.153 za OCR, ili v2.770.154 za barkodove i lica, ponovno pokrenite prepoznavanje na tim stranicama nakon nadogradnje. OCR sloj izvršen od zahvaćene verzije ostaje u spremljenoj datoteci, i zadana opcija SkipPagesWithText preskočit će te stranice u drugom prolazu osim ako je isključite ili prvo uklonite stari sloj

Kako bi prilagođeni IHPDFOCREngine trebao mapirati piksele natrag u PDF prostor?

Prilagođeni IHPDFOCREngine trebao bi vraćati okvire riječi u bitmapnim pikselima i prepustiti HotPDF-u mapiranje; pretvarajte u user space samo za vlastite odluke, i tada koristite okvir iz zahtjeva, nikad MediaBox. Od v2.770.153 PageLeft, PageBottom, PageRight i PageTop zahtjeva opisuju renderirani vidljivi okvir, pa se poklapaju s Request.Bitmap točno. Pomoćnik dolje inverz je transformacije biblioteke, uključujući njezinu upotrebu stvarne visine bitmape za uspravne stranice, pa se s HotPDF-om poklapa do piksela

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

// Bitmapni piksel (ishodište gore lijevo, Y dolje) u PDF user space
// (ishodište dolje lijevo, Y gore), kroz okvir iz kojeg je bitmapa renderirana
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;

Realan razlog da user space trebate unutar enginea jest pravilo zone: fakture čije zaglavlje nikad ne želite pretraživim, ili područje pečata koje zbuni prepoznavatelj. Engine dolje, napisan s TInterfacedObjectom da brojanje referenci rukuje njegovim životnim vijekom, filtrira riječi prema tome gdje im centri padaju na stranici, pa vraća preživjele netaknute u pikselnim koordinatama. RunRecognizer zamjenjuje vaš vlastiti poziv prepoznavatelja

type
  TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
  private
    FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single;  // user space
    function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
      out Words: THPDFOCRWords): boolean;  // vaš prepoznavatelj, pikselni okviri
  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];  // još uvijek pikseli: HotPDF ih sam mapira
    Inc(Count);
  end;
  SetLength(Words, Count);
  Result := True;
end;

Predajte engine ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) kao i bilo koji drugi engine. Biblioteka validira ono što se vrati prije nego mu vjeruje: riječ se ispada i broji u Info.DroppedWordCount kad njezin okvir napusti bitmapu, kad je Right <= Left ili Bottom <= Top, ili kad je Confidence izvan 0..1 ili ispod MinimumConfidence. Vraćanje više riječi od MaxWordsPerPage, ili guranje tekućeg zbroja preko MaxTotalWords, obara cijeli poziv s proračunskom pogreškom, pa poštujte Request.MaxWords u engineu. Ne pretvarajte okvire riječi u user space prije vraćanja; HotPDF bi vrijednosti točaka tretirao kao piksele i sloj bi se urušio prema ishodištu bitmape

Mapiranje izlaza vlastitog detektora

Isti pomoćnik poslužuje vlastiti pipeline građen na RenderLoadedPageToBitmap, koji renderira vidljivi okvir i primjenjuje /Rotate baš kao prepoznavne značajke. Pročitajte okvir s GetLoadedPageVisibleBox, normalizirajte rotaciju na isti način kao HotPDF, i mapirajte dva suprotna ugla svakog pikselnog okvira. Y se os okreće i, na 90 i 270 stupnjeva, osi se zamjenjuju, pa mapirani uglovi izlaze bez fiksnog redoslijeda; uzmite minimum i maksimum mapiranih točaka, što je i način na koji HotPDF gradi granice barkodova

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);  // vaš kod, pikselni okvir
      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;

Ponašanje rotacije obrađeno je dublje u izravnavanju rotacije stranice bez lomljenja okvira stranica, a pipeline dekodiranja barkodova koji troši istu transformaciju u dekodiranju rotiranih QR kodova s PDF stranica. Ako vaš engine omota vanjskog prepoznavatelja, Tesseract OCR adapter za pretraživi PDF pokazuje stranu izolacije procesa i otkazivanja istog sučelja

Brza referenca: CropBox-sigurno koordinatno mapiranje

  • Renderer rasterizira vidljivi okvir, CropBox obrezan na MediaBox (ISO 32000-1 §14.11.2); svako mapiranje piksel-u-stranicu mora koristiti taj okvir, pročitan s GetLoadedPageVisibleBox
  • HotPDF v2.770.153 popravio je ApplyLoadedOCRTextLayer; v2.770.154 popravio je cjelostanični DecodeLoadedPageBarcodes i nalaze lica iz DetectLoadedRedactionFindings; verzije od v2.766.64 do tih verzija zahvaćene su
  • THPDFOCRWord okviri bitmapni su pikseli s ishodištem gore lijevo; GetLoadedPageBox i GetLoadedPageVisibleBox vraćaju PDF user space s ishodištem dolje lijevo i Bottom < Top
  • Mjerilo je DPI / 72; izvodite ga iz DPI-ja, nikad iz okvira stranice podijeljenog u širinu bitmape
  • /Rotate odlučuje koji rubovi važe: Left i Bottom na 0 i 90, Right i Bottom na 180, Right i Top na 270
  • Vraćajte OCR riječi u pikselima i prepustite HotPDF-u mapiranje; pretvarajte samo za vlastitu logiku filtriranja
  • Ponovno pokrenite OCR na obrezanim stranicama obrađenim zahvaćenom verzijom, i sjetite se da SkipPagesWithText preskače stranice koje već nose stari sloj

Prepoznavne značajke, upiti okvira stranica i renderiranje učitanih dokumenata korišteni ovdje svi se isporučuju u HotPDF komponenti za Delphi i C++Builder; licenciranje, probna preuzimanja i potpuni popis značajki nalaze se na stranici HotPDF Delphi PDF komponente