OCR textové vrstvy, hranice barcodes a boxy redakcie tvárí driftujú na orezaných PDF stranách, keď sa pixely bitmapy mapujú späť cez MediaBox namiesto boxu, ktorý renderer naozaj rasterizoval: CropBox orezaný na MediaBox (ISO 32000-1 §14.11.2). HotPDF to opravil pre ApplyLoadedOCRTextLayer vo v2.770.153 a pre DecodeLoadedPageBarcodes a DetectLoadedRedactionFindings vo v2.770.154
Bug report, ktorý zvyčajne príde, vyzerá takto. Naskenovaný archív zmlúv prejde OCR, výstup je prehľadávateľný a zásah hľadania čísla odseku je označený pol palca pod a vľavo od vytlačeného čísla. Väčšina súborov v dávke je v poriadku. Pokazené všetky prišli z jednej skenovacej stanice, ktorá zapisuje /CropBox, aby orezala okraj plátena skenera. Ten jeden detail oddeľuje obraz, ktorý OCR engine videl, od rámu, do ktorého sa umiestnila textová vrstva a tá istá nezrovnalosť posúva hranice barcodes a, vážnejšie, boxy redakcie tvárí
Prečo sa OCR textová vrstva posúva preč od naskenovaných slov?
Textová vrstva driftuje, pretože dve polovice pipeline nesúhlasili o tom, ktorý obdĺžnik bitmapa pokrýva. Vo v2.766.64 zmenil HotPDF renderovanie, SVG export, prehliadač aj tlač tak, aby ctili CropBox: strana sa zobrazuje cez svoj CropBox orezaný na MediaBox, čo predpisuje ISO 32000-1 §14.11.2, a GetLoadedPageVisibleBox pribudol, aby ten viditeľný box vracal. Rozpoznávacie funkcie si stále stavali transformáciu device-to-page z GetLoadedPageBox(PageIndex, pbMediaBox, ...). Raster teraz pokrýval viditeľný box, transformácia stále predpokladala MediaBox a každá rozpoznaná pozícia sa vracala posunutá o medzeru medzi nimi
Postihnuté okno je preto presné. ApplyLoadedOCRTextLayer umiestňoval text zle od v2.766.64 po v2.770.152. Celostránkové DecodeLoadedPageBarcodes a detekcia tvárí vo vnútri DetectLoadedRedactionFindings ostali zlé o zostavenie dlhšie, do v2.770.153. Pred v2.766.64 kreslil renderer celý MediaBox, takže mapovanie a raster súhlasili, za cenu rozpoznávania obsahu, ktorý prehliadače nikdy neukazujú. Opravy zmenili pri každej funkcii tri veci spolu: transformáciu, odhad pixelového rozpočtu a page box podávaný vlastnému engine v request zázname
Niekoľko prípadov nebolo nikdy postihnutých:
- Strany bez
/CropBoxalebo strany, ktorých CropBox sa rovná MediaBoxu, sa mapujú identicky pred aj po oprave DecodeLoadedPageBarcodess nastavenýmHasRegionrenderuje presne región, ktorý podáte, a mapuje cez ten istý región, takže dekódovanie explicitného regiónu bolo korektné celý čas; kontrola, že región leží vnútri strany, stále používa MediaBox- Redakčné nálezy na báze patternov (emaily, čísla kariet a podobne) pochádzajú z extrakcie textu v user space, nie z rasteru, takže sa posunuli len nálezy detekcie tvárí
Tri súradnicové rámce a ktoré API HotPDF používajú ktorý
Kód HotPDF, ktorý siaha po rozpoznávaní, pracuje s troma rámcami a väčšina mapovacích bugov pochádza z ich zmiešania dvoch z nich
- Bitmapové pixely: pôvod v ľavom hornom rohu, Y rastie nadol, jednotky sú pixely pri DPI požiadavky.
THPDFOCRWord.Left,Top,RightaBottomsú v tomto ráme, rovnako ako voliteľné baseline body, výsledky, ktoré vracia vlastnýIHPDFBarcodeDecoder, a boxy od vlastnéhoIHPDFFaceDetector - PDF user space načítanej strany: pôvod v ľavom dolnom rohu, Y rastie nahor, jednotky sú body, s
Bottom < Top.GetLoadedPageBoxaGetLoadedPageVisibleBoxvracajú Left, Bottom, Right, Top v tomto ráme a rovnako poliaPageLeft,PageBottom,PageRightaPageTopzTHPDFOCRRequest, hranice vTHPDFDecodedBarcodea obdĺžniky vTHPDFRedactionFinding - Kresliace súradnice strán HotPDF: API, ktoré používate na stavbu nových strán (textový výstup, tvary, barcodes, linky, formulárové polia), pracuje s pôvodom v ľavom hornom rohu a Y rastúcim nadol. Ten rám patrí generovaniu dokumentov a s API načítaných dokumentov vyššie nesúvisí nič, takže doňho nikdy nekŕmte obdĺžnik user space načítanej strany nezmenený
Záznam OCR slova je zámerne pixelový: engine hlási, čo videl v obraze, a konverziu vlastní ApplyLoadedOCRTextLayer. To delenie funguje len vtedy, keď konverzia používa správny box, čo v2.770.153 obnovil
Transformácia device-to-page za OCR, barcodes a tvárami
HotPDF mapuje bitmapové pixely na stranu jedinou afínnou maticou postavenou z piatich vstupov: rotácie, mierky DPI / 72, výšky bitmapy a Left, Bottom, Right a Top renderovaného boxu. OCR, dekódovanie barcodes aj detekcia tvárí zdieľajú jedinú rutinu, prečo jeden zlý boxový vstup pokazil všetky tri rovnakým spôsobom. Pre neotočenú stranu je matica page-to-device [A B C D E F]:
A = ScaleaD = -Scale, kdeScale = DPI / 72; zápornéDprevráti user space (Y nahor) do bitmapového priestoru (Y nadol)B = C = 0, lebo neotočená strana nemá žiadny shear ani zámenu osíE = -Left * Scale, čo presunie ľavú hranu boxu do pixelového stĺpca 0F = BitmapHeight + Bottom * Scale, čo mapuje spodnú hranu boxu na y = BitmapHeight, spodnú hranu bitmapy, takže horná hrana pristane na riadku 0
Pixely sa vracajú na stranu inverziou tej matice. Request.PageRotation nesie /Rotate strany normalizované na 0, 90, 180 alebo 270 (každá hodnota, ktorá nie je násobkom 90, sa berie ako 0) a renderer otáča stranu v smere hodinových ručičiek, ako vyžaduje ISO 32000-1 §7.7.3.3. Pri rotácii sa osi vymenia a na pôvod bitmapy sa pripne iný pár hrán boxu. Zapísané ako inverzné vzorce, s S = DPI / 72, x a y v pixeloch a H ako výškou bitmapy:
| /Rotate | Page X | Page Y | Hrany boxu, na ktorých mapovanie závisí |
|---|---|---|---|
| 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 |
Posledný stĺpec vysvetľuje, prečo bug vyzeral v produkcii náhodný. CropBox, ktorý oreže len hornú časť strany, nechá Left a Bottom nedotknuté, takže vzpriamené strany vyšli dokonale a driftujú len strany nesúce /Rotate 270. Rotácia tiež vymení dimenzie bitmapy: pri 90 a 270 je bitmapa široká (Top - Bottom) * S pixelov a vysoká (Right - Left) * S pixelov
Čo je zlé s MediaBox [0 0 612 792] a CropBox [36 36 576 756]?
S orezom na pol palca z každej strany pristáva textová vrstva neotočenej strany presne 36 bodov vľavo od a 36 bodov pod naskenovanými slovami, keď sa použije MediaBox. Vezmime stranu US Letter, ktorej CropBox oreže 36 bodov (0,5 palca) z každej hrany. Viditeľný box je 540 krát 720 bodov, takže pri predvolenom OCR rozlíšení 300 DPI je mierka 300 / 72 ≈ 4,1667 a bitmapa je 2250 krát 3000 pixelov
Predpokladajme, že engine hlási slovo s pixelovým boxom Left 450, Top 600, Right 900, Bottom 660 a bez baseline. HotPDF potom umiestni baseline 20 percent výšky slova nad spodnú hranu, na pixelový riadok 648, a zmapuje štartovací bod (450, 648):
- Cez viditeľný box: x = 36 + 450 / 4.1667 = 144.0 a y = 36 + (3000 - 648) / 4.1667 = 600.48, čo je miesto, kde je slovo vytlačené
- Cez MediaBox: x = 0 + 108.0 = 108.0 a y = 0 + 564.48 = 564.48, uniformný posun o (-36, -36) bodov
Otočte tú istú stranu a smer chyby sa zmení, lebo hrajú iné hrany. Pri /Rotate 180 používa X člen Right a 612 namiesto 576 tlačí vrstvu 36 bodov doprava, kým Bottom ju stále ťahá 36 bodov nadol. Pri /Rotate 270 sú obe Right aj Top príliš veľké, takže vrstva sa posunie 36 bodov doprava a 36 bodov nahor. Dokument so zmiešanými orientáciami dokáže ukázať drift tromi smermi, spoľahlivý fingerprint tohto bugu. Ručne písaný kód odvodzujúci mierku z boxu, ako Bitmap.Width / (Right - Left), navyše roztiahne každú súradnicu o 612 / 540, zhruba 13 percent, navrch posunu
Ktoré vaše PDF dokumenty sú postihnuté?
PDF dokument je exponovaný, keď má aspoň jedna strana viditeľný box odlišný od MediaBoxu a HotPDF vám to povie v pár riadkoch. Porovnávajte GetLoadedPageBox s pbMediaBox proti GetLoadedPageVisibleBox pre každú stranu a vypisujte popri tom GetLoadedPageRotation, aby ste smer driftu dokázali predpovedať z tabuľky vyššie. THPDFPageBoundary ponúka aj pbCropBox, pbBleedBox, pbTrimBox a pbArtBox, ale GetLoadedPageBox(pbCropBox) prepadne na MediaBox, keď crop box neexistuje a neoreže, takže viditeľný box je správna vec na porovnávanie
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;
// uložené pole môže vypisovať svoje rohy v ľubovoľnom poradí
if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
// už normalizované a orezané 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 detaily GetLoadedPageVisibleBox majú význam pre skripty ako tento. Funkcia nechá svoje out parametre nedotknuté, keď zlyhá, takže prednastavenie predvolenej veľkosti strany pred volaním je bezpečný vzor. A keď deformovaný CropBox nepretína MediaBox vôbec, funkcia vráti MediaBox namiesto prázdneho obdĺžnika. Ak report vypíše strany a vaše nasadené zostavenie je staršie ako v2.770.153 pre OCR, alebo v2.770.154 pre barcodes a tváre, pustite rozpoznávanie na tých stranách znova po upgrade. OCR vrstva commitnutá postihnutým zostavením ostanú v uloženom súbore a predvolená option SkipPagesWithText tie strany pri druhom prechode preskočí, pokiaľ ju nevypnete alebo najprv neodstránite starú vrstvu
Ako má vlastný IHPDFOCREngine mapovať pixely späť do PDF priestoru?
Vlastný IHPDFOCREngine má vraciať word boxy v bitmapových pixeloch a nechať mapovanie na HotPDF; do user space prevádzajte len pre vlastné rozhodnutia a aj vtedy použite box z requestu, nikdy MediaBox. Od v2.770.153 popisujú PageLeft, PageBottom, PageRight a PageTop requestu renderovaný viditeľný box, takže sa presne zhodujú s Request.Bitmap. Pomocník nižšie je inverziou transformácie knižnice, vrátane jej použitia skutočnej výšky bitmapy pre vzpriamené strany, takže súhlasí s HotPDF na pixel
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Bitmapový pixel (pôvod hore vľavo, Y nadol) do PDF user space
// (pôvod dole vľavo, Y nahor) cez box, z ktorého sa bitmapa renderovala
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;
Realistický dôvod, prečo potrebovať user space vo vnútri enginu, je pravidlo zón: faktúry, ktorých letterhead nikdy nechcete prehľadávateľný, alebo oblasť pečiatky, ktorá zmätie recognizer. Engine nižšie, napísaný s TInterfacedObject, takže referenčné počítanie obslúži jeho životnosť, filtruje slová podľa toho, kam padajú ich stredu na strane, a preživších vracia nedotknutých v pixelových súradniciach. RunRecognizer zastupuje vaše vlastné volanie recognizer
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // user space
function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
out Words: THPDFOCRWords): boolean; // váš recognizer, pixelové boxy
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]; // stále pixely: HotPDF si ich zmapuje sám
Inc(Count);
end;
SetLength(Words, Count);
Result := True;
end;
Engine podajte do ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) ako každý iný engine. Knižnica validuje to, čo sa vráti, skôr než mu verí: slovo sa zahodí a započíta do Info.DroppedWordCount, keď jeho box opustí bitmapu, keď je Right <= Left alebo Bottom <= Top, alebo keď Confidence je mimo 0..1 alebo pod MinimumConfidence. Vrátenie viac slov než MaxWordsPerPage alebo prekročenie bežiaceho súčtu cez MaxTotalWords prepasuje celé volanie s chybou rozpočtu, takže rešpektujte Request.MaxWords vo vnútri enginu. Nekonvertujte word boxy do user space pred vrátením; HotPDF by bral bodové hodnoty ako pixely a vrstva by sa zrútila smerom k pôvodu bitmapy
Mapovanie výstupu vlastného detektora
Ten istý pomocník obsluhuje vlastnú pipeline postavenú na RenderLoadedPageToBitmap, ktorá renderuje viditeľný box a aplikuje /Rotate presne tak ako rozpoznávacie funkcie. Prečítajte box cez GetLoadedPageVisibleBox, normalizujte rotáciu rovnako ako HotPDF a zmapujte dva opačné rohy každého pixelového boxu. Os Y sa prevracia a pri 90 a 270 stupňoch sa vymenia osi, takže zmapované rohy vychádzajú v žiadnom fixnom poradí; vezmite minimum a maximum zmapovaných bodov, čo je aj spôsob, akým HotPDF stavia hranice barcodes
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); // váš kód, pixelový 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;
Správanie pri rotácii je pokryté hlbšie v článku o zrovnaní rotácie strany bez rozbitia page boxov a pipeline dekódovania barcodes konzumujúca tú istú transformáciu v článku o dekódovaní otočených QR kódov z PDF strán. Ak váš engine zabalí externý recognizer, adaptér Tesseract OCR pre prehľadávateľné PDF ukazuje procesovú izoláciu a stranu zrušenia toho istého rozhrania
Rýchla referencia: mapovanie súradníc bezpečné voči CropBoxu
- Renderer rasterizuje viditeľný box, CropBox orezaný na MediaBox (ISO 32000-1 §14.11.2); každé mapovanie pixelov na stranu musí použiť ten box, prečítaný cez
GetLoadedPageVisibleBox - HotPDF v2.770.153 opravil
ApplyLoadedOCRTextLayer; v2.770.154 opravil celostránkovéDecodeLoadedPageBarcodesa nálezy tvárí zDetectLoadedRedactionFindings; zostavenia od v2.766.64 po tie verzie sú postihnuté - Boxy
THPDFOCRWordsú bitmapové pixely s pôvodom hore vľavo;GetLoadedPageBoxaGetLoadedPageVisibleBoxvracajú PDF user space s pôvodom dole vľavo aBottom < Top - Mierka je
DPI / 72; odvodzujte ju z DPI, nikdy z page boxu delenej do šírky bitmapy - /Rotate rozhoduje, ktoré hrany hrajú: Left a Bottom pri 0 a 90, Right a Bottom pri 180, Right a Top pri 270
- Vracajte OCR slová v pixeloch a nechajte ich zmapovať HotPDF; konvertujte len pre vlastnú filtrovaciu logiku
- Pustite OCR znova na orezaných stranách spracovaných postihnutým zostavením a pamätajte, že
SkipPagesWithTextpreskočí strany, ktoré už starú vrstvu nesú
Rozpoznávacie funkcie, dotazy na page boxy aj renderovanie načítaných dokumentov použité tu dodáva komponent HotPDF pre Delphi a C++Builder; licencovanie, skúšobné stiahnutia a kompletný zoznam funkcií sú na stránke HotPDF Delphi PDF component