HotPDF isporučuje THPDFBuiltInOCREngine, ograničeni OCR engine sa poređenjem šablona napisan u celosti u Object Pascal-u: binarizuje renderovanu stranicu Otsu pragom, izdvaja glifove kao povezane komponente i ocenjuje svaki glif prema pokrivenosti sivim tonovima u odnosu na keširane šablone iz više fontova, pa Delphi aplikacija može da izgradi tekstualni sloj za pretragu bez spoljašnje OCR zavisnosti. Engine je morao da bude ponovo izgrađen od nule u v2.731.0, a razlog nije bio matcher. Bili su to pikseli
Stari engine je prolazio svoje testove. Prepoznavao je velika ASCII slova na sintetičkim bitmapama i mesecima je nastavljao da radi na Win32. Zatim je isti kod pokrenut pod Win64 i nije proizveo baš ništa: ni reči, ni dijagnostiku osim „found no high-contrast foreground“, ni pad. Ispostavilo se da su u putanji za čitanje piksela postojale dve nezavisne greške koje su jedna drugu poništavale, a njihovo razvezivanje lepo pokazuje zašto OCR kod ne uspeva glasno, već tiho
Zašto je stari OCR radio samo slučajno?
Stari engine je radio zato što su bitmapе šablona i ciljne bitmapе bile okrenute na isti način, pa je vertikalna inverzija u čitaču piksela bila nevidljiva matcher-u. TBitmap.ScanLine vraća redove obrnutim redosledom u odnosu na DIB konvenciju sa pozitivnim biHeight koju ostatak imaging putanje pretpostavlja. Renderujte M naopako, uporedite ga sa šablonom koji je takođe naopak i L1 razlika biće ista kao kod ispravnog poređenja. Svaki glif je prolazio. Ništa nije bilo ispravno
Ta simetrija upravo čini ovu klasu greške skupom. Svaka jednostrana popravka lomi prepoznavanje: ispravite čitanje cilja, a šablone ostavite kakvi jesu, i prepoznavanje se raspadne u šum; prvo ispravite šablone i dobićete isti pad sa druge strane. Ne postoji postepena putanja popravke. Zato je obnova zamenila celo čitanje pozivom GetDIBits nad eksplicitno deklarisanim BITMAPINFOHEADER, gde pozitivan biHeight po ugovoru znači bottom-up redove, a zatim jednom, namerno, okreće redove pri kopiranju u bafer sivih tonova
Druga greška pojavila se tek na Win64. HDC prosleđen u GetDIBits ne sme da bude sopstveni memory DC bitmapе, jer je bitmapа već izabrana u njemu, a Windows to dokumentuje kao nevažeće. Prosleđivanje Bitmap.Canvas.Handle bilo je tolerisano u Win32 procesu, a dosledno je otkazivalo u Win64 test procesu. Popravka je privremeni screen DC iz GetDC(0), oslobođen u finally bloku, koji nema nikakvu vezu sa bitmapom
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; // pozitivno => bottom-up redovi
Info.bmiHeader.biPlanes := 1;
Info.bmiHeader.biBitCount := 24;
Info.bmiHeader.biCompression := BI_RGB;
DC := GetDC(0); // nikada Work.Canvas.Handle: Work je tamo izabrana
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]; // jedan namerni preokret
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 i povezane komponente: od sivih piksela do okvira glifova
HotPDF prvo binarizuje Otsu metodom, a na lokalni prag u prozoru prelazi samo kada Otsu nije primenljiv. Globalna putanja zahteva stvarno bimodalni histogram: engine računa maksimum varijanse između klasa, a dodatno zahteva da raspon sivih tonova obuhvati najmanje 64 nivoa pre nego što poveruje rezultatu. Isprani sken, stranica sa gradijentnom pozadinom ili bitmapа koja je gotovo u potpunosti mastilo ne prolaze taj test. Fallback zatim poredi svaki piksel sa srednjom vrednošću prozora 31 puta 31 uz bias od 6 nivoa sivog, izračunatom tekućim zbirovima kolona tako da klizni prozor ostane linearan po broju piksela
Ekstrakcija glifova je označavanje 8-povezanih komponenti nad dobijenom maskom, sa eksplicitnim stekom umesto rekurzije, jer maska cele stranice bez problema može da preplavi Delphi thread stack pri dubokom flood fill-u. Dva filtra rade već tokom označavanja: komponente manje od 9 piksela odbacuju se kao šum u vidu tačkica, a svaka komponenta koja zauzima više od tri petine i širine i visine slike odbacuje se kao okvir ili linija, a ne kao glif. Drugi prolaz spaja vertikalno naslagane okvire čije je horizontalno preklapanje najmanje četvrtina užeg okvira, čime se tačka slova i ili j ponovo sjedinjuje sa stabljikom. Sve ovo radi nad rasterom, a raster dolazi iz istog renderera opisanog u tekstu o renderovanju učitane PDF stranice u bitmapu u Delphi-ju, što je važno iz praktičnog razloga: OCR kvalitet je odozgo ograničen kvalitetom rendera, a podrazumevani DPI tekstualnog sloja od 300 predstavlja nameran kompromis, a ne maksimum
Šta čini veliko I i malo l neodlučivim?
U Arial-u se veliko I i malo l rasterizuju u pikselima identične stubove, pa nijedna osobina oblika ne može da ih razdvoji i case mora da dođe iz sasvim drugog izvora. Odgovor engine-a je grupisanje visina na nivou reda. Okviri glifova grupišu se u tekstualne redove prema vertikalnom preklapanju, svaki red se analizira po cap visini i modalnoj baseline-i, a visine unutar reda dele se na nisku i visoku grupu. Stub koji pripada niskoj grupi jeste l; isti stub u visokoj grupi jeste I
Očigledna implementacija tog razdvajanja jeste prag sa fiksnim odnosom, a ona ne radi. Odnos x-height-a i cap-height-a u Arial-u je oko 0,72, tačno između vrednosti 0,70 i 0,75 na koje svi prvo pomisle. Pomak konstante za jednu stotinku u bilo kom smeru preokrene case čitavog korpusa. HotPDF umesto toga radi jednodimenzionalnu podelu k=2 koja minimizuje varijansu: sortira kandidatne visine, isprobava svaku tačku preseka i zadržava onu kod koje je zbir kvadrata odstupanja unutar grupa najmanji. Prag tako postaje osobina stranice, a ne konstanta u izvornom kodu
// ClusterHeights je sortiran rastuće; pronađi k=2 presek sa najmanjom varijansom
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;
// samo odnos srednjih vrednosti grupa odlučuje koja je traka niska
if SmallMean / TallMean <= 0.80 then
SmallGroup := ggSmall // prava x-height traka: oblici malih slova
else
SmallGroup := ggTall; // jedna visinska traka: sve je cap visina
Line.LowercaseContext := (SmallGroup = ggSmall);
Redovi sa samo jednom visinskom trakom u sebi nemaju nikakav dokaz. Naslov samo velikim slovima i natpis samo malim slovima izgledaju isto kada se posmatraju odvojeno. Za njih HotPDF poredi medijanu visine reda sa medijanom x-height-a na nivou stranice, uzetom iz redova koji jesu podeljeni: odnos na ili ispod 1,10 označava kontekst malih slova, odnos na ili iznad 1,18 kontekst velikih slova, a sve između ostaje bez ograničenja. Matcher zatim dodaje mali bonus za preferirani case od 0,03 kandidatu koji se slaže sa tim kontekstom, čime se rešavaju izjednačenja bez preglasavanja jasne razlike u obliku
Zašto je mreža šablona 12x18 mešala c i o?
Mreža šablona proširena je sa 12 puta 18 ćelija na 16 puta 24 zato što je pri manjoj rezoluciji margina pokrivenosti sivim tonovima između c i o pala ispod 0,007, duboko unutar praga neodlučnosti engine-a. Svaki okvir glifa ponovo se uzorkuje u mrežu kao vrednost pokrivenosti od 0 do 255, a ne kao binarna maska, pa ćelija sa jednom trećinom mastila čita približno 85 umesto da se zaokruži na crno ili belo. Na 12 puta 18 otvorena strana slova c zauzima tek nešto više od jedne kolone ćelija, a antialias prosekom ispere otvor. Na 16 puta 24 otvor preživljava resampling i većina lako zamenljivih parova vraća se na bezbednu udaljenost
Ocenjivanje je normalizovana L1 udaljenost između dve mreže pokrivenosti, uvećana za kaznu od 0,30 puta logaritam razlike odnosa stranica i 0,16 puta razliku gustine mastila, uz hard prefilter koji preskače svaki šablon čiji se odnos stranica razlikuje više od faktora 2,6. Šabloni se rasterizuju jednom po procesu iz pet sistemskih fontova (Arial, Times New Roman, Courier New, Tahoma i Segoe UI) kroz alfabet od 62 znaka, keširaju iza kritične sekcije i koriste u svakom narednom pozivu
Poslednja konstanta je zanimljiva. Kada se drugoplasirani znak po oceni nalazi unutar 0,018 od pobednika, HotPDF ograničava pouzdanost glifa na 0,5, što je ispod praga prihvatanja od 0,55, pa se glif jednostavno ne emituje. To je nameran fail-closed rez, a ne artefakt podešavanja: ograničeni engine koji nagađa proizvodi tekstualni sloj za pretragu čiji tekst ne odgovara slici, a pogrešna reč u tekstualnom sloju gora je od nedostajuće jer je nevidljiva osobi koja pregleda sken
Razdvajanje reči bez fiksnog praga razmaka
HotPDF izvodi prag razmaka između reči po redu iz raspodele razmaka između glifova, a ne iz fiksnog umnoška prosečne širine glifa. Klasična heuristika „razmak širi od 0,75 prosečnog advance-a jeste razmak“ lomi se čim red pomeša cifre sa uskim slovima, jer prosečni advance prestaje da opisuje bilo šta stvarno. Engine umesto toga sortira razmake za red i traži najveći skok između uzastopnih sortiranih vrednosti, što je granica između klastera unutar reči i klastera između reči, ako takav klaster postoji. Tri zaštite sprečavaju aktiviranje na šumu: skok mora biti najmanje 0,22 prosečne širine glifa, prvi razmak iznad preseka najmanje 0,32 te širine, a poslednji razmak ispod preseka ne sme biti veći od 0,65. Ako bilo koja zaštita otkaže, prag ostaje MaxInt i ceo red postaje jedna reč. Poslednja zaštita sprečava da jedan neuobičajeno širok kerning par podeli reč na dva dela, što je mnogo štetnija greška od spajanja dve reči, jer spojeni token i dalje sadrži prave znakove pravim redom za pretragu podniski
Upis nevidljivog tekstualnog sloja preko skenirane slike
ApplyLoadedOCRTextLayer pretvara prepoznate reči u sloj za pretragu tako što ih crta u režimu prikaza teksta 3, režimu koji ni ne popunjava ni ne iscrtava, definisanom u ISO 32000-1 §9.3.6, postavljenom preko skenirane slike iz koje su potekle. Content stream počinje sa BT, praćen sa 3 Tr, a svaka reč se postavlja tekstualnom matricom izgrađenom iz prijavljene baseline-e, njene cap visine preračunate iz piksela pri traženom DPI-ju i horizontalnog skaliranja koje sintetički niz glifova rasteže na izmerenu širinu reči. Rezultat se kopira i pretražuje kao tekst, ali ništa ne boji
Postoji overload bez engine-a koji za vas instancira ugrađeni recognizer, i to je ono što bi većina pozivalaca ugrađene putanje trebalo da koristi. Pre nego što se otvori copy-on-write transakcija, završavaju se prepoznavanje, Unicode validacija, računanje budžeta i izgradnja sadržaja, pa otkazivanje, prekoračenje budžeta ili greška engine-a ostavljaju graf objekata i broj verzije netaknutim. Reči se filtriraju dvaput: engine izbacuje sve ispod sopstvenog praga pouzdanosti po glifu od 0,55, a zatim THPDFOCRTextLayerOptions.MinimumConfidence (podrazumevano 0,5) izbacuje cele reči ispod praga pozivaoca
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; // ostavi stranice nastale iz teksta
Options.UseOptionalContentGroup := True;
Options.OptionalContentGroupName := 'OCR Text Layer';
// overload bez engine-a: HotPDF obezbeđuje ugrađeni ograničeni recognizer
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;
Jedno ograničenje treba izgovoriti jasno, a ne otkriti ga kasnije. Nevidljivi sloj koristi deljeni sintetički Type0 font bez ugrađivanja, što je dovoljno za pretragu i kopiranje u svakom viewer-u, ali ne zadovoljava zahtev za ugrađenim fontom iz ISO 19005. Ako izlaz mora biti PDF/A, pozivalac mora zasebno da ugradi usklađen font. OCR tekstualni sloj nosi geometriju, a ne strukturu, pa redosled čitanja dolazi samo iz pozicija glifova; ako vam treba logički redosled sa stranice koja već ima pravi tekst, ekstrakcija teksta po redosledu strukture koju vodi tag tree rešava drugi problem drugim alatom
Gde se ugrađeni engine zaustavlja
Ugrađeni engine je namerno uzak, a poznavanje njegovih granica čuva njegovu korisnost. Cilja visokokontrastni mašinski štampani ASCII iz fontova bliskih njegovih pet šablonskih fontova, a sve izvan toga vraća nula reči umesto nagađanja. Konkretne granice su:
- Slike do 4096 puta 4096 i 4.194.304 piksela, uz rok za prepoznavanje od 2000 ms i kooperativno otkazivanje kroz
THPDFCancellationToken - Alfabet od 62 ASCII slova i cifre; bez interpunkcije, akcentovanih znakova i CJK-a
- Samo tekst poravnat sa osama, pri rotaciji stranice koju je renderer već normalizovao; iskošeni skenovi se ne ispravljaju
- Nejasni parovi glifova ostaju nerešeni, pa stranica može da vrati delimične reči ili dijagnostiku „found no unambiguous ASCII words“
Kada je taj omotač preuzak, IHPDFOCREngine je šav. Implementirajte Recognize u sopstvenom engine-u, predajte ga overload-u ApplyLoadedOCRTextLayer sa tri argumenta i sve nizvodno (mapiranje koordinata, obrada rotacije, Unicode validacija, budžeti i atomsko potvrđivanje) ostaje isto. Bitmapа je pozajmljena za trajanje sinhronog poziva i ne sme da se zadrži. Da potvrdite da je sloj zaista upisan, ponovo učitajte sačuvani fajl i pokrenite običnu tekstualnu putanju opisanu u tekstu o izdvajanju teksta iz učitanog PDF-a u Delphi-ju; ako se reči vrate, sloj je stvaran
Ugrađeni OCR sa poređenjem šablona, nevidljivi tekstualni sloj, renderer stranice koji ih napaja i ekstrakcija teksta iz učitanog dokumenta koja ih proverava isporučuju se u istoj izvornoj VCL komponenti, bez spoljašnjeg OCR runtime-a i bez DLL-a koji bi trebalo isporučiti uz aplikaciju. Ako u Delphi-ju ili C++Builder-u pravite hvatanje dokumenata, arhivu ili pretragu skeniranih PDF-ova, HotPDF Delphi PDF komponenta daje vam ceo pipeline u jednoj zavisnosti