OCR teksto sluoksniai, barkodų ribos ir veidų uždengimo langeliai pasislenka apkarptuose PDF puslapiuose, kai bitmapo pikseliai atgal susiejami per MediaBox vietoj dėžės, kurią atvaizduoklis iš tikrųjų pavertė rastru – CropBox, apkirptą pagal MediaBox (ISO 32000-1 §14.11.2). HotPDF tai pataisė ApplyLoadedOCRTextLayer v2.770.153, o DecodeLoadedPageBarcodes ir DetectLoadedRedactionFindings – v2.770.154
Pranešimas apie klaidą, kuris paprastai atkeliuja, atrodo taip. Skenuotų sutarčių archyvas praeina pro OCR, išvestis ieškoma, o paieškos atitikmuo straipsnio numeriui paryškinamas pusės colio žemiau ir kairiau atspausdinto numerio. Dauguma partijos failų tvarkingi. Sugedusieji visi atkeliavo iš vienos skenavimo stoties, rašančios /CropBox, kad nukirptų stalo kraštinę. Ši viena detalė atskiria atvaizdą, kurį matė OCR variklis, nuo rėmo, kuriame padėtas teksto sluoksnis, ir tas pats nesutapimas paslenka barkodų ribas bei, dar rimčiau, veidų uždengimo langelius
Kodėl OCR teksto sluoksnis pasislenka nuo nuskenuotų žodžių?
Teksto sluoksnis pasislenka todėl, kad dvi srauto pusės nesutarė, kokį stačiakampį dengia bitmapas. v2.766.64 HotPDF pakeitė atvaizdavimą, SVG eksportą, peržiūrą ir spausdinimą, kad pagerbtų CropBox: puslapis rodomas per jo CropBox, apkirptą pagal jo MediaBox – kaip ir nurodo ISO 32000-1 §14.11.2 – o GetLoadedPageVisibleBox pridėta, kad grąžintų tą matomąją dėzę. Atpažinimo funkcijos toliau savo įrenginio-į-puslapį transformaciją statė iš GetLoadedPageBox(PageIndex, pbMediaBox, ...). Rastras dabar dengė matomąją dėzę, transformacija vis dar manė MediaBox, ir kiekviena atpažinta pozicija grįždavo pasislinkusi per tarpą tarp jų
Paveiktas langas todėl tikslus. ApplyLoadedOCRTextLayer tekstą dėstė netinkamai nuo v2.766.64 iki v2.770.152. Visą puslapį apimantis DecodeLoadedPageBarcodes ir veidų aptikimas DetectLoadedRedactionFindings viduje klydo dar vienu dariniu ilgiau – iki v2.770.153. Iki v2.766.64 atvaizduoklis piešdavo visą MediaBox, tad susiejimas ir rastras sutapdavo – kaina tai, kad atpažįstamas turinys, kurio peržiūros niekada nerodo. Taisymai kiekvienai funkcijai kartu pakeitė tris dalykus: transformaciją, pikselių biudžeto įvertinimą ir puslapio dėzę, atiduodamą pasirinktiniam varikliui užklausos įraše
Keli atvejai niekada nepaveikti:
- Puslapiai be
/CropBoxarba kurių CropBox lygus MediaBox, susiejami identiškai iki ir po taisymo DecodeLoadedPageBarcodessu nustatytaHasRegionatvaizduoja lygiai tą sritį, kurią perduodate, ir susieja per tą pačią sritį, tad aiškios srities dekodavimas visą laiką buvo teisingas; patikra, kad sritis slypi puslapyje, vis dar naudoja MediaBox- Rašto pagrįsti uždengimo radiniai (el. laiškai, kortelių numeriai ir t. t.) ateina iš teksto ištraukimo naudotojo erdvėje, o ne iš rastro, tad pasislinko tik veidų aptikimo radiniai
Trys koordinčių kadrai ir kurie HotPDF API naudoja kurį
HotPDF kodas, liečiantis atpažinimą, turi reikalų su trimis kadrais, ir dauguma susiejimo klaidų atsiranda sumaišius du iš jų
- Bitmapo pikseliai: kilmė viršuje kairėje, Y auga žemyn, vienetai – pikseliai užklausos DPI. Šiame kadre yra
THPDFOCRWord.Left,Top,RightirBottom, taip pat pasirinktiniai bazinės linijos taškai, pasirinktinioIHPDFBarcodeDecodergrąžinami rezultatai ir langeliai iš pasirinktinioIHPDFFaceDetector - Įkelto puslapio PDF naudotojo erdvė: kilmė apačioje kairėje, Y auga aukštyn, vienetai – taškai, su
Bottom < Top.GetLoadedPageBoxirGetLoadedPageVisibleBoxgrąžina Left, Bottom, Right, Top šiame kadre, kaip irPageLeft,PageBottom,PageRightbeiPageToplaukaiTHPDFOCRRequestviduje, ribosTHPDFDecodedBarcodeir stačiakampiaiTHPDFRedactionFinding - HotPDF puslapių piešimo koordinatės: API, kuria statote naujus puslapius (teksto išvestis, figūros, barkodai, nuorodos, formos laukai), dirba su kilme viršuje kairėje ir Y, augančiu žemyn. Tas kadras priklauso dokumento generavimui ir su aukščiau esančiais įkelto dokumento API nieko bendra neturi, tad niekada neleiskite į jį įeiti įkelto puslapio naudotojo erdvės stačiakampiui nepakeistam
OCR žodžio įrašas sąmoningai pikselinis: variklis praneša, ką matė atvaizde, o konversiją valdo ApplyLoadedOCRTextLayer. Tas padalijimas veikia tik tada, kai konversija naudoja teisingą dėzę – būtent tai v2.770.153 atstatė
Įrenginio-į-puslapį transformacija už OCR, barkodais ir veidais
HotPDF bitmapo pikselius susieja su puslapiu viena afinine matrica, pastatyta iš penkių įvedimų: pasukimo, mastelio DPI / 72, bitmapo aukščio ir atvaizduotos dėžės Left, Bottom, Right ir Top. OCR, barkodų dekodavimas ir veidų aptikimas visi dalijasi viena tai darančia rutina, todėl vienas netinkamas dėžės įvedimas visus tris sulaužė vienodai. Nesuktam puslapiui puslapio-į-įrenginį matrica [A B C D E F] yra:
A = ScaleirD = -Scale, kurScale = DPI / 72; neigiamasisDapverčia naudotojo erdvę (Y aukštyn) į bitmapo erdvę (Y žemyn)B = C = 0, nes nesuktas puslapis neturi šlyties ir ašių susikeitimoE = -Left * Scale, kas perkelia dėžės kairę kraštinę į 0 pikselių stulpelįF = BitmapHeight + Bottom * Scale, kas susieja dėžės apatinę kraštinę su y = BitmapHeight – bitmapo apatine kraštine, tad viršutinė kraštinė atkrenta ant 0 eilutės
Pikseliai grįžta į puslapį per tos matricos atvirkštinę. Request.PageRotation neša puslapio /Rotate, suvienodintą iki 0, 90, 180 arba 270 (bet kuri reikšmė, ne 90 kartotinis, traktuojama kaip 0), o atvaizduoklis puslapį suka pagal laikrodžio rodyklę, kaip reikalauja ISO 32000-1 §7.7.3.3. Sukus ašys susikeičia, ir prie bitmapo kilmės prikalama kita dėžės kraštinių pora. Atvirkštinėmis formulėmis, su S = DPI / 72, x ir y pikseliais bei H – bitmapo aukščiu:
| /Rotate | Puslapio X | Puslapio Y | Dėžės kraštinės, nuo kurių priklauso susiejimas |
|---|---|---|---|
| 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 |
Paskutinis stulpelis paaiškina, kodėl klaida produkcijoje atrodė atsitiktinė. CropBox, kirpantis tik puslapio viršų, Left ir Bottom palieka nepaliestus, tad statmeni puslapiai išeidavo puikiai, o pasislinkdavo tik puslapiai su /Rotate 270. Pasukimas dar ir susukeičia bitmapo matmenis: ties 90 ir 270 bitmapas yra (Top - Bottom) * S pikselių pločio ir (Right - Left) * S pikselių aukščio
Kas klysta su MediaBox [0 0 612 792] ir CropBox [36 36 576 756]?
Su puses colio kirpimu iš kiekvienos pusės nesukto puslapio teksto sluoksnis nusileidžia lygiai 36 taškus į kairę ir 36 taškus žemiau nuskenuotų žodžių, kai naudojamas MediaBox. Imkime US Letter puslapį, kurio CropBox nukerpa po 36 taškus (0,5 colio) nuo kiekvienos kraštinės. Matomoji dėzė – 540 į 720 taškų, tad prie numatytosios OCR 300 DPI raiškos mastelis yra 300 / 72 ≈ 4.1667, o bitmapas – 2250 į 3000 pikselių
Tarkime, variklis praneša žodį su pikselių langeliu Left 450, Top 600, Right 900, Bottom 660 ir be bazinės linijos. HotPDF tada bazinę liniją padeda 20 procentų žodžio aukščio virš apatinės kraštinės – 648 pikselių eilutėje – ir susieja pradžios tašką (450, 648):
- Per matomąją dėzę: x = 36 + 450 / 4.1667 = 144.0, o y = 36 + (3000 - 648) / 4.1667 = 600.48 – būtent ten, kur žodis atspausdintas
- Per MediaBox: x = 0 + 108.0 = 108.0, o y = 0 + 564.48 = 564.48 – tolygus (-36, -36) taškų poslinkis
Pasukite tą patį puslapį, ir klaidos kryptis keičiasi, nes dalyvauja kitos kraštinės. Ties /Rotate 180 X narys naudoja Right, ir 612 vietoj 576 stumia sluoksnį 36 taškus į dešinę, kol Bottom vis dar traukia 36 taškus žemyn. Ties /Rotate 270 ir Right, ir Top per dideli, tad sluoksnis juda 36 taškus į dešinę ir 36 aukštyn. Dokumentas su maišytomis orientacijomis gali parodyti dreifą trimis kryptimis – patikimas šios klaidos piršto atspaudas. Ranka rašytas kodas, išvedantis mastelį iš dėžės, kaip Bitmap.Width / (Right - Left), dar ir ištempia kiekvieną koordinatę 612 / 540 – maždaug 13 procentų – virš poslinkio
Kurie jūsų PDF dokumentai paveikti?
PDF dokumentas veikiamas, kai bent vienas puslapis turi matomąją dėzę, skirtingą nuo jo MediaBox, ir HotPDF gali tai pasakyti keliomis eilutėmis. Palyginkite GetLoadedPageBox su pbMediaBox su GetLoadedPageVisibleBox kiekvienam puslapiui, ir šalia spausdinkite GetLoadedPageRotation, kad galėtumėte numatyti dreifo kryptį pagal aukščiau esančią lentelę. THPDFPageBoundary dar siūlo pbCropBox, pbBleedBox, pbTrimBox ir pbArtBox, bet GetLoadedPageBox(pbCropBox), kai crop dėzės nėra, grįžta prie MediaBox ir nekirpsta, tad matomoji dėzė – teisingas dalykas lyginimui
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;
// saugomas masyvas gali išvardyti savo kampus bet kuria tvarka
if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
// jau suvienodinta ir apkirpta pagal 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;
Dvi GetLoadedPageVisibleBox detalės svarbios tokiems scenarijams. Funkcija, nesėkmingai pasibaigusi, savų out parametrų neliečia, tad numatytojo puslapio dydžio uždėjimas prieš kvietimą – saugus raštas. O kai sugadintas CropBox visai nesikerta su MediaBox, funkcija grąžina MediaBox, o ne tuščią stačiakampį. Jeigu ataskaita išvardija puslapius, o jūsų paskelbtasis darinis OCR atveju senesnis už v2.770.153, o barkodams ir veidams – už v2.770.154, po atnaujinimo tuos puslapius paleiskite atpažinimui iš naujo. Paveikto darinio įrašytas OCR sluoksnis lieka išsaugotame faile, o numatytoji SkipPagesWithText parinktis antrame ėjime tuos puslapius praleis, nebent jos atsisakysite arba pirmiausia pašalinsite senąjį sluoksnį
Kaip pasirinktinis IHPDFOCREngine turėtų susieti pikselius atgal į PDF erdvę?
Pasirinktinis IHPDFOCREngine turėtų grąžinti žodžių langelius bitmapo pikseliais ir leisti HotPDF atlikti susiejimą; į naudotojo erdvę konvertuokite tik saviesiems sprendimams, ir tada naudokite dėzę iš užklausos – niekada MediaBox. Nuo v2.770.153 užklausos PageLeft, PageBottom, PageRight ir PageTop aprašo atvaizduotąją matomąją dėzę, tad jie sutampa su Request.Bitmap tiksliai. Žemiau esantis pagalbininkas – bibliotekos transformacijos atvirkštinis, įskaitant tikro bitmapo aukščio naudojimą statmeniems puslapiams, tad su HotPDF sutampa iki pikselio
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Bitmapo pikselis (kilmė viršuje kairėje, Y žemyn) į PDF naudotojo erdvę
// (kilmė apačioje kairėje, Y aukštyn), per dėzę, iš kurios bitmapas atvaizduotas
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;
Realistiška priežastis, kodėl variklio viduje gali prireikti naudotojo erdvės, – zonų taisyklė: sąskaitos, kurių antraštė niekada neturėtų būti ieškoma, arba antspaudo sritis, klaidinanti atpažintoją. Žemiau esantis variklis, parašytas su TInterfacedObject, kad nuorodų skaičiavimas tvarkytų jo gyvenimą, filtruoja žodžius pagal tai, kur jų centrai nukrenta puslapyje, tada grąžina išgyvenusiuosius nepaliestus pikselių koordinatėse. RunRecognizer stoja už jūsų pačių atpažintojo kvietimo
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // naudotojo erdvė
function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
out Words: THPDFOCRWords): boolean; // jūsų atpažintojas, pikselių langeliai
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]; // vis dar pikseliai: HotPDF juos susieja pats
Inc(Count);
end;
SetLength(Words, Count);
Result := True;
end;
Atiduokite variklį ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) kaip ir bet kuriam kitam varikliui. Biblioteka patvirtina, kas grįžta, dar prieš pasitikėdama: žodis numetamas ir suskaičiuojamas Info.DroppedWordCount, kai jo langelis išeina už bitmapo, kai Right <= Left arba Bottom <= Top, arba kai Confidence už 0..1 ribų ar žemiau MinimumConfidence. Daugiau žodžių nei MaxWordsPerPage grąžinimas arba einamosios sumos pastūmimas už MaxTotalWords žlugdo visą kvietimą su biudžeto klaida, tad gerbkite Request.MaxWords variklyje. Neverskite žodžių langelių į naudotojo erdvę prieš grąžindami; HotPDF taškines reikšmes laikytų pikseliais, ir sluoksnis susitrauktų link bitmapo kilmės
Savo detektoriaus išvesties susiejimas
Tas pats pagalbininkas aptarnauja savą srautą, pastatytą ant RenderLoadedPageToBitmap, kuris atvaizduoja matomąją dėzę ir taiko /Rotate lygiai kaip atpažinimo funkcijos. Perskaitykite dėzę su GetLoadedPageVisibleBox, suvienodinkite pasukimą taip pat, kaip HotPDF, ir susiekite du priešingus kiekvieno pikselių langelio kampus. Y ašis apsiverčia, o ties 90 ir 270 laipsnių ašys susikeičia, tad susieti kampai išeina be jokios fiksuotos tvarkos; imkite susietų taškų minimumą ir maksimumą – taip pat daro HotPDF, statydamas barkodų ribas
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); // jūsų kodas, pikselių langelis
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;
Pasukimo elgsena giliau aprašyta puslapio pasukimo išlyginime nesugadinus puslapių dėzių, o barkodų dekodavimo srautas, vartojantis tą pačią transformaciją – pasuktų QR kodų dekodavime iš PDF puslapių. Jeigu jūsų variklis apvynioja išorinį atpažintoją, Tesseract OCR adapteris ieškomam PDF rodo tos pačios sąsajos procesų izoliacijos ir atšaukimo pusę
Trumpa atmintinė: CropBox saugus koordinačių susiejimas
- Atvaizduoklis rastrina matomąją dėzę – CropBox, apkirptą pagal MediaBox (ISO 32000-1 §14.11.2); kiekvienas pikselių-į-puslapį susiejimas privalo naudoti tą dėzę, perskaitytą su
GetLoadedPageVisibleBox - HotPDF v2.770.153 pataisė
ApplyLoadedOCRTextLayer; v2.770.154 pataisė visą puslapį apimantįDecodeLoadedPageBarcodesir veidų radinius išDetectLoadedRedactionFindings; dariniai nuo v2.766.64 iki tų versijų paveikti THPDFOCRWordlangeliai – bitmapo pikseliai su kilme viršuje kairėje;GetLoadedPageBoxirGetLoadedPageVisibleBoxgrąžina PDF naudotojo erdvę su kilme apačioje kairėje irBottom < Top- Mastelis –
DPI / 72; išveskite jį iš DPI, niekada iš puslapio dėzės, padalintos į bitmapo plotį - /Rotate sprendžia, kurios kraštinės svarbios: Left ir Bottom ties 0 ir 90, Right ir Bottom ties 180, Right ir Top ties 270
- Grąžinkite OCR žodžius pikseliais ir leiskite HotPDF juos susieti; konvertuokite tik savajai filtravimo logikai
- Paveikto darinio apdorotuose apkarptuose puslapiuose paleiskite OCR iš naujo, ir nepamirškite, kad
SkipPagesWithTextpraleidžia puslapius, kuriuose jau yra senasis sluoksnis
Čia naudotos atpažinimo funkcijos, puslapių dėzių užklausos ir įkelto dokumento atvaizdavimas visi keliauja HotPDF komponente Delphi ir C++Builder; licencijavimui, bandomiesiems parsisiuntimams ir pilnai funkcijų sąrašui – HotPDF Delphi PDF komponento puslapyje