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 DecodeLoadedPageBarcodesmedHasRegionsatt 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,RightogBottomer i denne rammen, likeså de valgfrie baseline-punktene, resultatene en customIHPDFBarcodeDecoderreturnerer, og boksene fra en customIHPDFFaceDetector - PDF user space for en lastet side: origo nede til venstre, Y vokser oppover, enheter er punkter, med
Bottom < Top.GetLoadedPageBoxogGetLoadedPageVisibleBoxreturnerer Left, Bottom, Right, Top i denne rammen, og det gjør ogsåPageLeft-,PageBottom-,PageRight- ogPageTop-feltene iTHPDFOCRRequest, grensene iTHPDFDecodedBarcodeog rektanglene iTHPDFRedactionFinding - 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
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 = ScaleogD = -Scale, derScale = DPI / 72; den negativeDvender user space (Y opp) om til bitmap space (Y ned)B = C = 0, for en urotet side har ingen shear eller bytte mellom akserE = -Left * Scale, som flytter boksens venstre kant til pikselkolonne 0F = 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:
| /Rotate | Side X | Side Y | Boks-kanter mappingen er avhengig av |
|---|---|---|---|
| 0 | Left + x / S | Bottom + (H - y) / S | Left, Bottom |
| 90 | Left + y / S | Bottom + x / S | Left, Bottom |
| 180 | Right - x / S | Bottom + y / S | Right, Bottom |
| 270 | Right - y / S | Top - x / S | Right, 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
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
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-sidesDecodeLoadedPageBarcodesog ansiktsfunn fraDetectLoadedRedactionFindings; builds fra v2.766.64 opp til de versjonene er berørt THPDFOCRWord-bokser er bitmappiksler med origo oppe til venstre;GetLoadedPageBoxogGetLoadedPageVisibleBoxreturnerer PDF user space med origo nede til venstre ogBottom < 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
SkipPagesWithTexthopper 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