Tehnički članak

Ugrađeni OCR s predlošcima u Delphiju uz HotPDF

HotPDF isporučuje THPDFBuiltInOCREngine, ograničeni OCR mehanizam s podudaranjem predložaka napisan u cijelosti u Object Pascalu: binarizira iscrtanu stranicu Otsuovim pragom, izdvaja glifove kao povezane komponente i svaki glif boduje prema pokrivenosti sivim tonovima u odnosu na predloške više fontova spremljene u predmemoriji, pa Delphi aplikacija može izgraditi tekstualni sloj koji je moguće pretraživati bez vanjske OCR ovisnosti. Mehanizam je u v2.731.0 morao biti ponovno izgrađen od nule, a razlog nije bio matcher. Bili su to pikseli

Stari je mehanizam prolazio svoje testove. Prepoznavao je velika ASCII slova na sintetičkim bitmapama i na Win32 je to mjesecima nastavljao raditi. Zatim je isti kod pokrenut pod Win64 i nije proizveo baš ništa: ni riječi, ni dijagnostiku osim poruke found no high-contrast foreground, ni rušenje. Ispostavilo se da su dva neovisna propusta u putu čitanja piksela međusobno poništavala jedan drugoga, a njihovo raspetljavanje dobro pokazuje zašto OCR kod često nečujno zakaže umjesto da glasno prijavi problem

Zašto je stari OCR mehanizam radio samo slučajno?

Stari je mehanizam radio jer su njegove bitmapne predloške i ciljne bitmape okretao na isti način, pa vertikalna inverzija u čitaču piksela nije bila vidljiva matcheru. TBitmap.ScanLine vraća redke obrnutim redoslijedom od DIB konvencije s pozitivnim biHeightom koju pretpostavlja ostatak putanje za slike. Iscrtajte M naopako, usporedite ga s predloškom koji je također naopako i L1 razlika jednaka je ispravnoj usporedbi. Svaki se glif podudarao. Ništa nije bilo ispravno

Upravo ta simetrija čini ovu vrstu pogreške skupom. Ispravite samo čitanje cilja, a predloške ostavite, i prepoznavanje se urušava u šum; prvo ispravite predloške i dobit ćete isto urušavanje iz drugog smjera. Ne postoji postupni put popravka. Zato je obnova cijelo čitanje zamijenila s GetDIBits prema izričito deklariranom BITMAPINFOHEADERu, gdje pozitivan biHeight po ugovoru znači redke odozdo prema gore, a zatim jednom, namjerno, okreće redke pri kopiranju u međuspremnik sivih tonova

Druga se pogreška pojavila tek pod Win64. HDC predan funkciji GetDIBits ne smije biti vlastiti memorijski DC bitmapne slike jer je bitmapa već odabrana u njemu, a Windows to dokumentira kao nevažeće. Predavanje Bitmap.Canvas.Handle tolerirao je Win32 proces, ali je dosljedno zakazivalo u Win64 testnom procesu. Popravak je privremeni zaslonski DC iz GetDC(0), oslobođen u bloku finally, koji nema nikakve veze s 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 => redci odozdo prema gore
    Info.bmiHeader.biPlanes := 1;
    Info.bmiHeader.biBitCount := 24;
    Info.bmiHeader.biCompression := BI_RGB;
    DC := GetDC(0);            // nikad Work.Canvas.Handle: Work je ondje odabran
    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 namjerni okret
      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 najprije binarizira Otsuovom metodom i vraća se na prag lokalnog prozora samo kada Otsu nije primjenjiv. Globalni put traži stvarno bimodalni histogram: mehanizam računa maksimum varijance između klasa i dodatno zahtijeva da raspon sivih tonova obuhvaća barem 64 razine prije nego što mu povjeruje. Isprani sken, stranica s gradijentnom pozadinom ili bitmapa gotovo posve ispunjena tintom ne prolaze taj test. Fallback zatim svaki piksel uspoređuje sa srednjom vrijednošću prozora 31 puta 31 uz pomak od 6 razina sivih tonova, računatu tekućim zbrojevima stupaca, pa klizni prozor ostaje linearan po broju piksela

Izdvajanje glifova označavanje je 8-povezanih komponenti nad dobivenom maskom, s izričitim stogom umjesto rekurzije, jer maska cijele stranice rado napuni Delphi stog dretve pri dubokom popunjavanju. Pri označavanju rade dva filtra: komponente manje od 9 piksela odbacuju se kao šum točkica, a svaka komponenta koja se proteže preko više od tri petine širine i visine slike odbacuje se kao okvir ili crta, a ne kao glif. Drugi prolaz spaja okomito naslagane okvire čije je vodoravno preklapanje barem četvrtina užeg okvira, čime se točka slova i ili j ponovno spaja sa stabljikom. Sve ovo radi nad rasterom, a raster dolazi iz istog renderera opisanog u članku o iscrtavanju učitane PDF stranice u bitmapu u Delphiju, što je važno iz praktičnog razloga: kvaliteta OCR-a odozgo je ograničena kvalitetom iscrtavanja, a zadani DPI tekstualnog sloja od 300 namjerna je razmjena, a ne maksimum

Što čini veliko I i malo l neodlučivima?

U Arialu se veliko I i malo l rasteriziraju u pikselima identične trake, pa ih nijedna značajka oblika ne može razdvojiti i veličina slova mora doći iz nekog sasvim drugog izvora. Odgovor mehanizma je klasteriranje visina na razini retka. Okviri glifova grupiraju se u tekstualne retke prema okomitom preklapanju, za svaki se red analiziraju visina velikih slova i modalna osnovna linija, a visine unutar retka dijele se na kratak i visok klaster. Traka u kratkom klasteru je l; ista traka u visokom klasteru je I

Očita implementacija te podjele jest prag s fiksnim omjerom i ne radi. Omjer x-visine i visine velikih slova u Arialu iznosi oko 0,72, što pada točno na vrijednosti 0,70 i 0,75 za kojima svi prvo posegnu. Pomaknite konstantu za stotinku u bilo kojem smjeru i cijeli korpus promijeni veličinu slova. HotPDF umjesto toga radi jednodimenzionalnu podjelu k=2 koja minimizira varijancu: sortira kandidate visina, iskušava svaku točku reza i zadržava rez s najmanjim zbrojem kvadrata odstupanja unutar klastera. Prag postaje svojstvo stranice, a ne konstanta u izvornom kodu

// ClusterHeights je sortiran uzlazno; pronađi k=2 rez s najmanjom varijancom
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 omjer srednjih vrijednosti dvaju klastera odlučuje koja je traka kratka
if SmallMean / TallMean <= 0.80 then
  SmallGroup := ggSmall          // stvarna traka x-visine: oblici malih slova
else
  SmallGroup := ggTall;          // jedna visinska traka: sve je visina velikih slova
Line.LowercaseContext := (SmallGroup = ggSmall);

Retci s jednom visinskom trakom nemaju nikakav unutarnji dokaz. Naslov s velikim slovima i natpis s malim slovima izgledaju jednako u izolaciji. Za njih HotPDF uspoređuje medijan visine retka s medijanom x-visine cijele stranice, uzetim iz redaka koji su se podijelili: omjer od najviše 1,10 označuje kontekst malih slova, omjer od najmanje 1,18 kontekst velikih slova, a sve između ostaje bez ograničenja. Matcher zatim primjenjuje mali bonus preferencije veličine slova od 0,03 prema kandidatu koji se slaže s tim kontekstom, što razrješava izjednačenja bez nadjačavanja jasne razlike u obliku

Zašto je mreža predložaka 12x18 pomiješala c i o?

Mreža predložaka proširena je s 12 puta 18 ćelija na 16 puta 24 jer je pri manjoj razlučivosti razlika pokrivenosti sivim tonovima između c i o pala ispod 0,007, duboko unutar praga dvosmislenosti mehanizma. Svaki se okvir glifa ponovno uzorkuje u mrežu kao vrijednosti pokrivenosti od 0 do 255, a ne kao binarna maska, pa se ćelija koja je jednom trećinom ispunjena tintom očitava približno kao 85 umjesto da se zaokruži na crno ili bijelo. Na 12 puta 18 otvorena strana slova c proteže se tek nešto više od jednog stupca ćelija, a antialiasirani prosjek ispere prazninu. Na 16 puta 24 praznina preživi ponovno uzorkovanje i većina lako zamjenjivih parova vraća se na sigurnu udaljenost

Bodovanje je normalizirana L1 udaljenost između dviju mreža pokrivenosti, uvećana za kaznu od 0,30 puta logaritam razlike omjera stranica i 0,16 puta razliku gustoće tinte, uz tvrdi predfiltar koji preskače predložak čiji se omjer stranica razlikuje više od faktora 2,6. Predlošci se rasteriziraju jednom po procesu iz pet sistemskih fontova (Arial, Times New Roman, Courier New, Tahoma i Segoe UI) preko abecede od 62 znaka, spremaju iza kritičnog odsječka i ponovno koriste pri svakom sljedećem pozivu

Posljednja je konstanta zanimljiva. Kada rezultat drugoplasiranog znaka odstupa najviše 0,018 od pobjednika, HotPDF ograničava pouzdanost glifa na 0,5, što je ispod praga prihvaćanja od 0,55, pa se glif jednostavno ne emitira. To je namjerna odluka da se neuspjeh zatvori, a ne artefakt ugađanja: ograničeni mehanizam koji nagađa proizvodi tekstualni sloj koji se može pretraživati, ali čiji se tekst ne podudara sa slikom, a pogrešna riječ u tekstualnom sloju gora je od one koja nedostaje jer je nevidljiva osobi koja pregledava sken

Razdvajanje riječi bez fiksnog praga razmaka

HotPDF prag razmaka između riječi izvodi po retku iz raspodjele razmaka između glifova, a ne iz fiksnog višekratnika prosječne širine glifa. Klasična heuristika, prema kojoj je razmak širi od 0,75 prosječnog pomaka razmak između riječi, puca čim se u retku pomiješaju znamenke s uskim slovima jer prosječni pomak više ne opisuje ništa stvarno. Mehanizam umjesto toga sortira razmake u retku i traži najveći skok između uzastopnih sortiranih vrijednosti, što je granica između klastera unutar riječi i klastera između riječi ako takva granica postoji. Tri zaštite sprečavaju da se aktivira na šumu: skok mora biti barem 0,22 prosječne širine glifa, prvi razmak iznad reza mora biti barem 0,32 te širine, a posljednji razmak ispod reza ne smije prelaziti 0,65 te širine. Ako bilo koja zaštita ne prođe, prag ostaje MaxInt i cijeli redak postaje jedna riječ. Upravo posljednja zaštita sprječava da jedan neobično širok kerning par podijeli riječ na dva dijela, što je mnogo štetnija pogreška od spajanja dviju riječi jer spojeni token i dalje sadrži ispravne znakove ispravnim redoslijedom za pretraživanje podniza

Pisanje nevidljivog tekstualnog sloja preko skenirane slike

ApplyLoadedOCRTextLayer pretvara prepoznate riječi u sloj koji se može pretraživati tako da ih crta u načinu prikaza teksta 3, načinu ni ispune ni obrisa definiranom u ISO 32000-1 §9.3.6, postavljenom iznad skenirane slike iz koje su došle. Tok sadržaja otvara se s BT, zatim 3 Tr, a svaka se riječ postavlja matricom teksta izgrađenom iz prijavljene osnovne linije, visine velikih slova pretvorene iz piksela pri zadanom DPI-ju i vodoravnog skaliranja koje sintetički niz glifova rasteže na izmjerenu širinu riječi. Rezultat se kopira i pretražuje kao tekst, a ništa ne boji

Postoji overload bez mehanizma koji za vas instancira ugrađeni prepoznavač, i to bi većina pozivatelja ugrađenog puta trebala koristiti. Prepoznavanje, provjera Unicodea, obračun proračuna i izgradnja sadržaja dovršavaju se prije otvaranja transakcije copy-on-write, pa otkazivanje, prekoračenje proračuna ili kvar mehanizma ostavljaju graf objekata i broj verzije netaknutima. Riječi se filtriraju dvaput: mehanizam odbacuje sve ispod vlastitog praga pouzdanosti po glifu od 0,55, a zatim THPDFOCRTextLayerOptions.MinimumConfidence (zadano 0,5) odbacuje cijele riječi ispod granice pozivatelja

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 digitalno rođene stranice na miru
    Options.UseOptionalContentGroup := True;
    Options.OptionalContentGroupName := 'OCR Text Layer';
    // overload bez mehanizma: HotPDF isporučuje ugrađeni ograničeni prepoznavač
    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 vrijedi izreći jasno, umjesto da ga otkrijete kasnije. Nevidljivi sloj koristi zajednički sintetički neugrađeni Type0 font, što je dovoljno za pretraživanje i kopiranje u svakom pregledniku, ali ne zadovoljava zahtjev ugradnje fonta iz ISO 19005. Ako izlaz mora biti PDF/A, pozivatelj mora zasebno ugraditi sukladan font. OCR tekstualni sloj usto nosi geometriju, a ne strukturu, pa redoslijed čitanja dolazi samo iz položaja glifova; ako trebate logički redoslijed sa stranice koja već ima pravi tekst, izdvajanje teksta prema redoslijedu strukture vođeno stablom oznaka drugi je alat za drugi problem

Gdje ugrađeni mehanizam staje

Ugrađeni mehanizam namjerno je uzak, a poznavanje njegovih rubova čini ga korisnim. Cilja strojno tiskani ASCII s visokim kontrastom iz fontova bliskih njegovih pet predložaka, a sve izvan toga vraća nula riječi umjesto pogađanja. Konkretne su granice:

  • Slike do 4096 puta 4096 i 4.194.304 piksela, s rokom za prepoznavanje od 2000 ms i suradničkim otkazivanjem kroz THPDFCancellationToken
  • Abeceda od 62 ASCII slova i znamenke; bez interpunkcije, bez naglašenih znakova i bez CJK-a
  • Samo vodoravno poravnati tekst, pri rotaciji stranice koju je renderer već normalizirao; ukošeni skenovi ne ispravljaju nagib
  • Dvosmisleni parovi glifova ostaju neriješeni, pa stranica može vratiti djelomične riječi ili dijagnostiku found no unambiguous ASCII words

Kada je taj okvir premalen, IHPDFOCREngine je mjesto za priključivanje. Implementirajte Recognize prema vlastitom mehanizmu, predajte ga overloadu ApplyLoadedOCRTextLayer s tri argumenta i sve nizvodno (mapiranje koordinata, obrada rotacije, provjera Unicodea, proračuni, atomska potvrda) ostaje isto. Bitmapa je posuđena tijekom sinkronog poziva i ne smije se zadržati. Kako biste potvrdili da je sloj ispravno sjeo, ponovno učitajte spremljenu datoteku i pokrenite običan put teksta opisan u članku o izdvajanju teksta iz učitanog PDF-a u Delphiju; ako se riječi vrate, sloj je stvaran

Ugrađeni OCR s podudaranjem predložaka, nevidljivi tekstualni sloj, renderer stranice koji ih hrani i izdvajanje teksta iz učitanog dokumenta koje ih provjerava isporučuju se u istoj izvornoj VCL komponenti, bez vanjskog OCR runtimea i bez DLL-a koji biste morali isporučiti uz aplikaciju. Ako u Delphiju ili C++Builderu gradite dohvat dokumenata, arhiviranje ili pretraživanje skeniranih PDF-ova, HotPDF Delphi PDF component daje vam cijeli cjevovod u jednoj ovisnosti