HotPDF pateikia THPDFBuiltInOCREngine – ribotą šablonų atitikimo OCR variklį, parašytą vien Object Pascal: jis binarizuoja atvaizduotą puslapį Otsu slenksčiavimu, išskiria glifus kaip susietus komponentus ir įvertina kiekvieną glifą pagal pilkio skalės aprėptį, naudodamas podėlyje laikomus kelių šriftų šablonus, todėl Delphi programa gali sukurti ieškomą teksto sluoksnį be išorinės OCR priklausomybės. Variklį v2.731.0 teko perkurti nuo nulio, ir priežastis buvo ne atitikimo algoritmas. Tai buvo pikseliai
Senasis variklis testus išlaikė. Jis atpažino didžiąsias ASCII raides sintetiniuose bitmapuose ir Win32 aplinkoje taip veikė kelis mėnesius. Tada tas pats kodas paleistas Win64 aplinkoje apskritai nieko negrąžino: nei žodžių, nei kitos diagnostikos, išskyrus „found no high-contrast foreground“, nei avarijos. Paaiškėjo, kad pikselių skaitymo kelyje buvo dvi nepriklausomos klaidos, kurios viena kitą slėpė, o jų išnarpliojimas gerai parodo, kodėl OCR kodas tyliai sugenda, o ne garsiai praneša apie klaidą
Kodėl senasis OCR variklis veikė tik atsitiktinai?
Senasis variklis veikė todėl, kad jo šablonų bitmapai ir tiksliniai bitmapai buvo apversti vienodai, todėl vertikalus pikselių skaitytuvo apvertimas atitikikliui buvo nematomas. TBitmap.ScanLine grąžina eilutes priešinga tvarka nei teigiamo biHeight DIB konvencija, kurią daro prielaidą likęs vaizdų kelias. Atvaizduokite M aukštyn kojomis, palyginkite su taip pat apverstu šablonu ir L1 skirtumas bus toks pat kaip teisingo palyginimo. Kiekvienas glifas sutapo. Niekas nebuvo teisinga
Būtent dėl šios simetrijos tokio tipo klaida brangiai kainuoja. Pataisykite tikslinio vaizdo skaitymą, bet palikite šablonus ir atpažinimas subyrės į triukšmą; pirmiausia pataisykite šablonus ir gausite tą patį subyrėjimą iš kitos pusės. Laipsniško taisymo kelio nėra. Todėl perrašant visą skaitymą naudota GetDIBits su aiškiai deklaruotu BITMAPINFOHEADER, kuriame teigiamas biHeight pagal sutartį reiškia bottom-up eilutes, o kopijuojant į pilkio skalės buferį atliekamas vienas sąmoningas apvertimas
Antra klaida išryškėjo tik Win64 aplinkoje. GetDIBits perduodamas HDC negali būti paties bitmapo atminties DC, nes bitmapas jau pasirinktas į jį, o Windows tai dokumentuoja kaip negaliojantį atvejį. Bitmap.Canvas.Handle perdavimą Win32 procesas toleravo, o Win64 testų procese jis nuolat nesėkmingas. Pataisymas – trumpalaikis ekrano DC iš GetDC(0), atlaisvinamas finally bloke ir nesusijęs su jokiu bitmapu
procedure BitmapToGray(Bitmap: TBitmap; out Gray: TBytes);
var
Work: TBitmap;
Info: TBitmapInfo;
Buffer: TBytes;
DC: HDC;
P: PByte;
Stride, X, Y: Integer;
begin
Work := TBitmap.Create;
try
Work.Assign(Bitmap);
Work.PixelFormat := pf24bit;
Stride := ((Work.Width * 24 + 31) div 32) * 4;
SetLength(Buffer, Stride * Work.Height);
FillChar(Info, SizeOf(Info), 0);
Info.bmiHeader.biSize := SizeOf(BITMAPINFOHEADER);
Info.bmiHeader.biWidth := Work.Width;
Info.bmiHeader.biHeight := Work.Height; // teigiamas => bottom-up eilutės
Info.bmiHeader.biPlanes := 1;
Info.bmiHeader.biBitCount := 24;
Info.bmiHeader.biCompression := BI_RGB;
DC := GetDC(0); // niekada Work.Canvas.Handle: Work pasirinktas ten
if DC = 0 then
raise EInvalidOperation.Create('Recognition bitmap pixels could not be read');
try
if GetDIBits(DC, Work.Handle, 0, Work.Height,
@Buffer[0], Info, DIB_RGB_COLORS) <> Work.Height then
raise EInvalidOperation.Create('Recognition bitmap pixels could not be read');
finally
ReleaseDC(0, DC);
end;
SetLength(Gray, Work.Width * Work.Height);
for Y := 0 to Work.Height - 1 do
begin
P := @Buffer[(Work.Height - 1 - Y) * Stride]; // vienas sąmoningas apvertimas
for X := 0 to Work.Width - 1 do
Gray[Y * Work.Width + X] :=
(Integer(P[X * 3]) * 29 + Integer(P[X * 3 + 1]) * 150 +
Integer(P[X * 3 + 2]) * 77) shr 8;
end;
finally
Work.Free;
end;
end;
Binarizacija ir susieti komponentai: nuo pilkų pikselių iki glifų laukelių
HotPDF pirmiausia binarizuoja Otsu metodu ir tik tada, kai Otsu netinka, pereina prie vietinio lango slenksčio. Globaliam keliui reikia tikros bimodalinės histogramos: variklis apskaičiuoja didžiausią tarpklasinės dispersijos reikšmę ir papildomai reikalauja, kad pilkio diapazonas apimtų bent 64 lygius, prieš pasitikėdamas rezultatu. Išblukęs skenavimas, puslapis su gradientiniu fonu arba beveik vien rašalu užpildytas bitmapas šio testo neišlaiko. Tada atsarginis kelias kiekvieną pikselį lygina su 31 x 31 lango vidurkiu, pritaikydamas 6 pilkio lygių poslinkį ir naudodamas slenkančias stulpelių sumas, todėl slenkantis langas išlieka tiesinis pagal pikselių skaičių
Glifai išgaunami taikant 8 jungčių komponentų žymėjimą gautai kaukei, naudojant aiškų steką, o ne rekursiją, nes viso puslapio kaukė gilioje užliejimo paieškoje lengvai perpildytų Delphi gijos steką. Žymėjimo metu taikomi du filtrai: mažesni nei 9 pikselių komponentai atmetami kaip taškinis triukšmas, o komponentas, apimantis daugiau nei tris penktadalius vaizdo pločio ir aukščio, atmetamas kaip rėmelis arba linija, o ne glifas. Antrame etape sujungiami vertikaliai sukrauti laukeliai, kurių horizontalus persidengimas sudaro bent ketvirtadalį siauresnio laukelio, todėl i arba j taškas vėl sujungiamas su stiebu. Visa tai veikia rastrui, o rastras gaunamas iš to paties atvaizduotuvo, aprašyto įkelto PDF puslapio atvaizdavimo bitmapu Delphi aplinkoje, ir tai svarbu dėl praktinės priežasties: OCR kokybę iš viršaus riboja atvaizdavimo kokybė, o numatytasis teksto sluoksnio 300 DPI yra sąmoningas kompromisas, ne maksimumas
Kas daro didžiąją I ir mažąją l nedeterministines?
Arial šriftu didžioji I ir mažoji l rastruojamos į pikseliškai identiškas juostas, todėl joks formos požymis jų neatskiria, o raidžių registras turi ateiti iš visiškai kitos vietos. Variklis naudoja eilučių lygio aukščio klasterizavimą. Glifų laukeliai grupuojami į teksto eilutes pagal vertikalų persidengimą, kiekvienai eilutei apskaičiuojamas didžiųjų raidžių aukštis ir modalinė bazinė linija, o eilutės aukščiai padalijami į trumpą ir aukštą klasterius. Trumpame klasteryje esanti juosta yra l, o ta pati juosta aukštame klasteryje yra I
Akivaizdi tokio padalijimo realizacija yra fiksuotas santykio slenkstis, bet ji neveikia. Arial x-height ir cap-height santykis yra maždaug 0.72, tiksliai tarp 0.70 ir 0.75 reikšmių, kurias pirmiausia renkasi visi. Pakanka konstantą pajudinti viena šimtąja į bet kurią pusę, kad visas korpusas pakeistų raidžių registrą. HotPDF vietoj to atlieka vienmatį k=2 dispersiją mažinantį padalijimą: surikiuoja kandidato aukščius, išbando kiekvieną pjūvio vietą ir pasilieka tą, kurio kvadratinių nuokrypių suma klasteriuose mažiausia. Slenkstis tampa puslapio savybe, o ne šaltinio konstanta
// ClusterHeights surikiuotas didėjimo tvarka; randame mažiausios dispersijos k=2 pjūvį
BestSplit := 1;
BestVariance := 1E18;
for I := 1 to ClusterCount - 1 do
begin
SumA := 0;
for J := 0 to I - 1 do SumA := SumA + ClusterHeights[J];
SumB := 0;
for J := I to ClusterCount - 1 do SumB := SumB + ClusterHeights[J];
MeanA := SumA / I;
MeanB := SumB / (ClusterCount - I);
Variance := 0;
for J := 0 to I - 1 do
Variance := Variance + Sqr(ClusterHeights[J] - MeanA);
for J := I to ClusterCount - 1 do
Variance := Variance + Sqr(ClusterHeights[J] - MeanB);
if Variance < BestVariance then
begin
BestVariance := Variance;
BestSplit := I;
end;
end;
// tik dviejų klasterių vidurkių santykis lemia, kuri juosta yra trumpa
if SmallMean / TallMean <= 0.80 then
SmallGroup := ggSmall // tikra x-height juosta: mažųjų raidžių formos
else
SmallGroup := ggTall; // viena aukščio juosta: viskas cap-height
Line.LowercaseContext := (SmallGroup = ggSmall);
Eilutės, turinčios tik vieną aukščio juostą, neturi jokių vidinių įrodymų. Vien didžiosiomis raidėmis parašyta antraštė ir vien mažosiomis parašytas parašas atskirai atrodo taip pat. Tokiais atvejais HotPDF lygina eilutės medianinį aukštį su puslapio x-height mediana, paimta iš eilučių, kurios buvo padalytos: santykis iki 1.10 eilutę pažymi kaip mažųjų raidžių kontekstą, santykis nuo 1.18 – kaip didžiųjų raidžių kontekstą, o tarpinės reikšmės lieka nevaržomos. Tada atitikimas prideda nedidelę 0.03 raidžių registro pirmenybės premiją kandidatui, kuris sutampa su kontekstu, todėl lygūs rezultatai pastumiami, bet aiškus formos skirtumas niekada nepaisomas
Kodėl 12x18 šablonų tinklelis supainiojo c ir o?
Šablonų tinklelis išplėstas nuo 12 x 18 langelių iki 16 x 24, nes mažesnėje raiškoje pilkio aprėpties skirtumas tarp c ir o nukrito žemiau 0.007, gerokai į saugos ribos, kurią variklis laiko dviprasmiška, vidų. Kiekvienas glifo laukelis į tinklelį perimėlinamas kaip aprėpties reikšmės nuo 0 iki 255, o ne kaip dvejetainė kaukė, todėl trečdaliu rašalo užpildytas langelis tampa maždaug 85, o ne suapvalinamas iki juodos arba baltos spalvos. 12 x 18 tinklelyje atvira c pusė užima vos daugiau nei vieną stulpelį, o anti-aliasing vidurkis nuplauna tarpą. 16 x 24 tinklelyje tarpas išlieka, ir dauguma lengvai supainiojamų porų vėl nutolsta iki saugaus atstumo
Įvertis yra normalizuotas L1 atstumas tarp dviejų aprėpties tinklelių, prie jo pridedama 0.30 logaritminio kraštinių santykio skirtumo bauda ir 0.16 rašalo tankio skirtumo bauda, o kietasis pirminis filtras praleidžia tik tuos šablonus, kurių kraštinių santykis skiriasi ne daugiau kaip 2.6 karto. Šablonai vieną kartą per procesą rastruojami iš penkių sistemos šriftų (Arial, Times New Roman, Courier New, Tahoma ir Segoe UI) 62 simbolių abėcėlei, podėliuojami už kritinės sekcijos ir pakartotinai naudojami kiekviename vėlesniame iškvietime
Įdomiausia yra paskutinė konstanta. Kai antroje vietoje likusio simbolio balas nuo nugalėtojo skiriasi ne daugiau kaip 0.018, HotPDF glifo pasitikėjimą apriboja iki 0.5, o tai mažiau nei 0.55 priėmimo riba, todėl glifas išvis neišvedamas. Tai sąmoningas fail-closed sprendimas, o ne derinimo šalutinis efektas: ribotas variklis, kuris spėja, sukuria ieškomą sluoksnį, kurio tekstas neatitinka vaizdo, o klaidingas žodis teksto sluoksnyje blogiau už trūkstamą, nes jį peržiūrintis žmogus jo nemato
Žodžių skaidymas be fiksuoto tarpo slenksčio
HotPDF žodžio tarpo slenkstį kiekvienai eilutei išveda iš tarpglifinių tarpų pasiskirstymo, o ne iš fiksuoto vidutinio glifo pločio daugiklio. Klasikinė euristika „tarpu platesniu nei 0.75 vidutinio poslinkio laikome tarpą“ sugenda, kai eilutėje susimaišo skaitmenys ir siauros raidės, nes vidutinis poslinkis nustoja ką nors realiai apibūdinti. Vietoj to variklis surikiuoja eilutės tarpus ir ieško didžiausio šuolio tarp gretimų surikiuotų reikšmių, kuris, jei toks klasteris egzistuoja, yra riba tarp tarpų žodžio viduje ir tarpų tarp žodžių. Trys saugikliai neleidžia tam suveikti dėl triukšmo: šuolis turi sudaryti bent 0.22 vidutinio glifo pločio, pirmas tarpas virš pjūvio turi būti bent 0.32 jo, o paskutinis tarpas žemiau pjūvio negali viršyti 0.65. Jei bent vienas saugiklis nesuveikia, slenkstis lieka MaxInt ir visa eilutė tampa vienu žodžiu. Būtent paskutinis saugiklis neleidžia vienai neįprastai plačiai kerningo porai perskirti žodžio į dvi dalis, o tai daug žalingesnė klaida nei dviejų žodžių suliejimas, nes sulietame žetone vis tiek lieka teisingi simboliai teisinga tvarka paieškai pagal poeilutę
Nematomo teksto sluoksnio rašymas virš skenuoto vaizdo
ApplyLoadedOCRTextLayer atpažintus žodžius paverčia ieškomu sluoksniu, nupiešdama juos teksto rodymo režimu 3 – nei užpildymo, nei kontūro režimu, apibrėžtu ISO 32000-1 §9.3.6 – virš skenuoto vaizdo, iš kurio jie gauti. Turinio srautas prasideda BT, po kurio eina 3 Tr, o kiekvienas žodis išdėstomas teksto matrica, sudaryta iš praneštos bazinės linijos, iš pikselių pagal prašomą DPI perskaičiuoto didžiųjų raidžių aukščio ir horizontalios skalės, kuri sintetinės glifų sekos plotį ištempia iki išmatuoto žodžio pločio. Rezultatą galima kopijuoti ir jame ieškoti, bet jis nieko nenudažo
Yra overload be variklio, kuris pats sukuria integruotą atpažintuvą, ir jį turėtų naudoti dauguma integruoto kelio iškvietėjų. Atpažinimas, Unicode tikrinimas, biudžeto apskaita ir turinio konstravimas baigiami prieš atveriant copy-on-write transakciją, todėl atšaukimas, biudžeto viršijimas ar variklio klaida palieka objektų grafiką ir versijos numerį nepakeistus. Žodžiai filtruojami du kartus: variklis atmeta viską, kas nesiekia jo paties 0.55 glifo pasitikėjimo ribos, o tada THPDFOCRTextLayerOptions.MinimumConfidence (numatytoji reikšmė 0.5) atmeta ištisus žodžius, žemesnius už iškvietėjo ribą
var
Doc: THotPDF;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile('scan.pdf') < 1 then
Exit;
Options := THPDFOCRTextLayerOptions.Default; // DPI 300, MinimumConfidence 0.5
Options.SkipPagesWithText := True; // palikti skaitmeninius puslapius nepakeistus
Options.UseOptionalContentGroup := True;
Options.OptionalContentGroupName := 'OCR Text Layer';
// overload be variklio: HotPDF pateikia integruotą ribotą atpažintuvą
if Doc.ApplyLoadedOCRTextLayer([0], Options, Info) then
begin
Writeln(Info.AcceptedWordCount, ' words accepted by ',
string(Info.EngineName));
Doc.SaveLoadedDocument('scan-searchable.pdf');
end
else
Writeln('No text layer written: ', string(Info.Diagnostic));
finally
Doc.Free;
end;
end;
Vieną ribą verta įvardyti aiškiai, o ne atrasti vėliau. Nematomas sluoksnis naudoja bendrą sintetinį neįterptą Type0 šriftą, kurio pakanka paieškai ir kopijavimui kiekvienoje peržiūros programoje, bet jis neatitinka ISO 19005 šrifto įterpimo reikalavimo. Jei išvestis turi būti PDF/A, iškvietėjas turi atskirai įterpti reikalavimus atitinkantį šriftą. OCR teksto sluoksnis taip pat turi geometriją, o ne struktūrą, todėl skaitymo tvarka gaunama vien iš glifų pozicijų; jei reikia loginės tvarkos puslapyje, kuriame jau yra tikras tekstas, struktūros tvarka paremtas teksto išgavimas iš žymų medžio yra kitas įrankis kitai problemai
Kur integruotas variklis sustoja
Integruotas variklis sąmoningai siauras, o jo ribų žinojimas padeda jį išlaikyti naudingu. Jis taikomas didelio kontrasto mašininiu būdu spausdintam ASCII iš šriftų, panašių į penkis jo šriftų šablonus, o viskas už šio intervalo grąžina ne spėjimą, o jokio žodžio. Konkrečios ribos:
- Vaizdai iki 4096 x 4096 ir 4 194 304 pikselių, su 2000 ms atpažinimo terminu ir kooperatyviu atšaukimu per
THPDFCancellationToken - 62 simbolių ASCII raidžių ir skaitmenų abėcėlė; be skyrybos ženklų, diakritinių simbolių ir CJK
- Tik ašinis tekstas, tokio puslapio pasukimo, kokį jau normalizavo atvaizduotuvas; kreivi skenai neatverčiami
- Dviprasmiškos glifų poros lieka neišspręstos, todėl puslapis gali grąžinti dalinius žodžius arba diagnostiką „found no unambiguous ASCII words“
Kai šio voko nepakanka, jungties vieta yra IHPDFOCREngine. Įgyvendinkite Recognize savo variklyje, perduokite jį trijų argumentų ApplyLoadedOCRTextLayer overload ir visa tolesnė dalis – koordinačių susiejimas, pasukimo tvarkymas, Unicode tikrinimas, biudžetai ir atominis commit – lieka tokia pati. Bitmapas pasiskolinamas sinchroninio iškvietimo trukmei ir jo laikyti negalima. Norėdami patvirtinti, kad sluoksnis įrašytas teisingai, iš naujo įkelkite išsaugotą failą ir paleiskite įprastą teksto kelią, aprašytą teksto išgavime iš įkelto PDF Delphi aplinkoje; jei žodžiai grįžta, sluoksnis tikras
Integruotas šablonų atitikimo OCR, nematomas teksto sluoksnis, juos maitinantis puslapio atvaizduotuvas ir įkelto dokumento teksto išgavimas, kuriuo jie patikrinami, pateikiami tame pačiame vietiniame VCL komponente, be išorinės OCR vykdymo aplinkos ir be DLL, kurią reikėtų diegti kartu su programa. Jei Delphi ar C++Builder kuriate dokumentų fiksavimą, archyvavimą arba skenavimo PDF paiešką, HotPDF Delphi PDF komponentas suteikia visą konvejerį vienoje priklausomybėje