HotPDF sisältää THPDFBuiltInOCREngine-moottorin, kokonaan Object Pascalilla kirjoitetun rajatun mallinsovitus-OCR:n: se binarisoi renderöidyn sivun Otsu-kynnystyksellä, poimii glyfit yhdistettyinä komponentteina ja pisteyttää jokaisen glyfin harmaasävypeiton perusteella välimuistissa olevia monifonttisia malleja vasten, joten Delphi-sovellus voi rakentaa haettavan tekstikerroksen ilman ulkoista OCR-riippuvuutta. Moottori täytyi rakentaa alusta v2.731.0:ssa, eikä syy ollut sovittimessa. Syy olivat pikselit
Vanha moottori läpäisi testinsä. Se tunnisti synteettisten bittikarttojen isot ASCII-kirjaimet, ja Win32:ssa se jatkoi niin kuukausien ajan. Sitten sama koodi ajettiin Win64:ssa ja tuloksena ei ollut mitään: ei sanoja, ei muuta diagnostiikkaa kuin "found no high-contrast foreground", ei kaatumista. Ongelmaksi osoittautui kaksi toisistaan riippumatonta virhettä pikselien lukupolussa, jotka olivat kumonneet toisensa, ja niiden purkaminen näyttää hyvin miksi OCR-koodi epäonnistuu hiljaisesti eikä näyttävästi
Miksi vanha OCR-moottori toimi vain sattumalta?
Vanha moottori toimi, koska sen mallibittikartat ja kohdebittikartat käännettiin samalla tavalla, joten pikselinlukijan pystysuuntainen käännös jäi sovittimelta piiloon. TBitmap.ScanLine palauttaa rivit päinvastaisessa järjestyksessä kuin positiivisen biHeight-arvon DIB-sopimus, johon muu kuvankäsittelypolku luottaa. Renderöi M ylösalaisin, vertaa sitä myös ylösalaisin olevaan malliin ja L1-ero on sama kuin oikeassa vertailussa. Jokainen glyfi sopi. Mikään ei ollut oikein
Juuri tämä symmetria tekee tämän luokan virheistä kalliita. Yksipuolinen korjaus rikkoo sovituksen: korjaa kohteen lukeminen ja jätä mallit ennalleen, niin tunnistus muuttuu kohinaksi; korjaa mallit ensin ja saat saman romahduksen toisesta suunnasta. Vähittäistä korjauspolkua ei ole. Uudelleenrakennus korvasi siksi koko luvun GetDIBits-kutsulla, joka käyttää eksplisiittisesti ilmoitettua BITMAPINFOHEADER-rakennetta, jossa positiivinen biHeight tarkoittaa bottom-up-rivejä sopimuksen eikä VCL:n konvention perusteella, ja kääntää suunnan yhden kerran tarkoituksella kopioidessaan harmaasävydataan
Toinen virhe tuli näkyviin vasta Win64:ssa. GetDIBits-kutsulle annettu HDC ei saa olla bittikartan oma muistilaite, koska bittikartta on jo valittu siihen ja Windows dokumentoi sen virheelliseksi. Bitmap.Canvas.Handle-arvon antaminen toimi Win32-prosessissa siedetysti mutta epäonnistui johdonmukaisesti Win64-testiprosessissa. Korjaus on GetDC(0)-kutsulla saatava väliaikainen näyttölaite, joka vapautetaan finally-lohkossa ja jolla ei ole riippuvuutta mistään bittikartasta
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; // positiivinen => bottom-up-rivit
Info.bmiHeader.biPlanes := 1;
Info.bmiHeader.biBitCount := 24;
Info.bmiHeader.biCompression := BI_RGB;
DC := GetDC(0); // älä koskaan käytä Work.Canvas.Handle: Work on valittu siihen
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]; // yksi tarkoituksellinen käännös
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;
Binarisointi ja yhdistetyt komponentit: harmaasävyistä glyfilaatikoihin
HotPDF binarisoi ensin Otsun menetelmällä ja käyttää paikallisen ikkunan kynnystä vain, kun Otsu ei sovellu. Globaali polku vaatii aidosti bimodaalisen histogrammin: moottori laskee luokkien välisen varianssin maksimin ja vaatii lisäksi harmaa-alueen kattavan vähintään 64 tasoa ennen kuin se luottaa tulokseen. Haalistunut skannaus, liukuvärinen tausta tai lähes kokonaan mustetta oleva bittikartta epäonnistuu tässä testissä. Varapolku vertaa sitten jokaista pikseliä 31 × 31 -ikkunan keskiarvoon kuuden harmaatasoon suuruisella vinoumalla, joka lasketaan juoksevilla sarakesummilla jotta liukuva ikkuna pysyy pikselimäärään nähden lineaarisena
Glyfien poiminta on 8-yhdistettyä komponenttimerkintää syntyneen maskin yli ja käyttää eksplisiittistä pinoa rekursion sijaan, koska koko sivun maski voi helposti täyttää Delphin säikeen pinon syvässä flood fill -ajossa. Merkintävaiheessa ajetaan kaksi suodatinta: alle 9 pikselin komponentit pudotetaan pilkkukohinana, ja komponentti joka kattaa yli kolme viidesosaa sekä kuvan leveydestä että korkeudesta pudotetaan kehikkona tai viivana eikä glyfinä. Toinen vaihe yhdistää pystysuoraan pinotut laatikot, joiden vaakasuuntainen limitys on vähintään neljännes kapeamman laatikon leveydestä, mikä palauttaa i- tai j-merkin pisteen yhteen varren kanssa. Kaikki tämä toimii rasterilla, ja rasteri tulee samasta renderöijästä kuin ladatun PDF-sivun renderöinti bittikartaksi Delphissä, millä on käytännön merkitys: OCR:n laatu ei voi ylittää renderöinnin laatua, ja tekstikerroksen oletus-DPI 300 on harkittu vaihtokauppa eikä maksimi
Mikä tekee isosta I:stä ja pienestä l:stä erottamattomat?
Arialissa iso I ja pieni l rasteroituvat pikselintarkoiksi palkeiksi, joten mikään muotopiirre ei voi erottaa niitä ja kirjainkoon täytyy tulla kokonaan muualta. Moottorin vastaus on rivitason korkeusklusterointi. Glyfilaatikot ryhmitellään tekstiriveiksi pystysuuntaisen limityksen perusteella, jokaisesta rivistä analysoidaan versaalikorkeus ja tyypillinen baseline, ja rivin korkeudet jaetaan lyhyeen ja pitkään klusteriin. Lyhyeen klusteriin kuuluva palkki on l; samainen palkki pitkässä klusterissa on I
Jaon ilmeinen toteutus on kiinteä suhdekynnys, eikä se toimi. Arialin x-korkeuden ja versaalikorkeuden suhde on noin 0,72, mikä osuu suoraan arvoihin 0,70 ja 0,75 joita kaikki kokeilevat ensin. Siirrä vakiota sadasosan kumpaan tahansa suuntaan ja kokonainen aineisto vaihtaa kirjainkoon. HotPDF käyttää sen sijaan yksiulotteista k=2-varianssia minimoivaa jakoa: lajittele ehdokaskorkeudet, kokeile jokaista katkaisukohtaa ja pidä katkaisukohta jonka sisäinen neliöpoikkeamien summa on pienin. Kynnys muuttuu sivun ominaisuudeksi lähdekoodin vakion sijaan
// ClusterHeights on lajiteltu nousevasti; etsi pienimmän varianssin k=2-jako
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;
// vain klusterien keskiarvojen suhde päättää kumpi kaista on lyhyt
if SmallMean / TallMean <= 0.80 then
SmallGroup := ggSmall // todellinen x-korkeuskaista: pienaakkosmuodot
else
SmallGroup := ggTall; // yksi korkeuskaista: kaikki ovat versaalia
Line.LowercaseContext := (SmallGroup = ggSmall);
Rivit joilla on vain yksi korkeuskaista eivät sisällä lainkaan sisäistä näyttöä. Pelkillä versaaleilla kirjoitettu otsikko ja kokonaan pienaakkosinen kuvateksti näyttävät irrallaan samoilta. Niissä HotPDF vertaa rivin mediaanikorkeutta sivutason mediaaniseen x-korkeuteen, joka saadaan jakautuneilta riveiltä: enintään 1,10 oleva suhde merkitsee riviä pienaakkoskontekstiksi, vähintään 1,18 oleva suhde versaali-kontekstiksi ja niiden väliin jäävä alue pysyy rajoittamattomana. Sovitus lisää sitten pienen 0,03:n kirjainkokubonuksen kontekstin kanssa yhtenevälle ehdokkaalle, mikä ratkaisee tasatilanteita mutta ei koskaan ohita selvää muotoeroa
Miksi 12x18-malliruudukko sekoitti c:n ja o:n?
Malliruudukko kasvatettiin koosta 12 × 18 kokoon 16 × 24, koska pienemmässä tarkkuudessa harmaasävypeiton ero c:n ja o:n välillä putosi alle 0,007:n, selvästi moottorin epäselvyyskynnyksen sisälle. Jokainen glyfilaatikko näytteistetään ruudukkoon peittoarvoina väliltä 0–255 binäärisen maskin sijaan, joten kolmanneksen mustetta sisältävä solu saa arvoksi noin 85 eikä pyöristy mustaksi tai valkoiseksi. Koossa 12 × 18 c:n avoin puoli kattaa juuri ja juuri yhden solusarakkeen ja antialiasoitu keskiarvo pesee aukon pois. Koossa 16 × 24 aukko säilyy näytteistyksessä ja useimmat helposti sekoittuvat parit palaavat turvallisen etäisyyden päähän toisistaan
Pisteytys on kahden peittoruudukon normalisoitu L1-etäisyys, johon lisätään 0,30 kertaa sivusuhteen eron logaritmi ja 0,16 kertaa musteen tiheyden ero, sekä kova esisuodatus joka ohittaa mallin jos sen sivusuhde eroaa yli kertoimella 2,6. Mallit rasteroidaan kerran prosessia kohti viidestä järjestelmäfontista (Arial, Times New Roman, Courier New, Tahoma ja Segoe UI) 62 merkin aakkoston yli, tallennetaan kriittisen osion taakse ja käytetään uudelleen jokaisessa myöhemmässä kutsussa
Viimeinen vakio on kiinnostava. Kun toiseksi paras merkki saa tuloksen joka on enintään 0,018 voittajan tulosta suurempi, HotPDF rajoittaa glyfin luottamuksen arvoon 0,5, joka jää alle 0,55:n hyväksymisrajan, joten glyfiä ei yksinkertaisesti tuoteta. Tämä on tarkoituksellinen fail-closed-raja eikä säätöjäännös: rajattu moottori joka arvaa tuottaa haettavan tekstikerroksen jonka teksti ei vastaa kuvaa, ja väärä sana tekstikerroksessa on pahempi kuin puuttuva sana koska skannausta tarkasteleva henkilö ei huomaa sitä
Sanojen jakaminen ilman kiinteää välirajaa
HotPDF johtaa sanavälin kynnyksen rivikohtaisesti glyfivälien jakaumasta eikä keskimääräisen glyfileveyden kiinteästä kertoimesta. Klassinen heuristiikka "keskiarvon etenemästä yli 0,75:n suuruinen väli on välilyönti" rikkoutuu heti kun rivillä on numeroita ja kapeita kirjaimia sekaisin, koska keskimääräinen etenemä lakkaa kuvaamasta mitään todellista. Moottori lajittelee rivin välit ja etsii suurimman hypyn peräkkäisten lajiteltujen arvojen välillä, mikä on sisäisen sanaklusterin ja sanojen välisen klusterin raja jos sellainen on olemassa. Kolme suojaa estää laukaisun kohinasta: hypyn on oltava vähintään 0,22 keskimääräisestä glyfileveydestä, jaon jälkeisen ensimmäisen välin vähintään 0,32 siitä ja jaon alapuolinen viimeinen väli ei saa ylittää arvoa 0,65 siitä. Jos yksikin suoja pettää, kynnys pysyy arvossa MaxInt ja koko rivistä tulee yksi sana. Viimeinen suoja estää yksittäistä epätavallisen leveää kernausparia jakamasta sanaa kahtia, mikä on paljon haitallisempi virhe kuin kahden sanan yhdistäminen, koska yhdistetty token sisältää silti oikeat merkit oikeassa järjestyksessä alimerkkijonohakua varten
Näkymättömän tekstikerroksen kirjoittaminen skannatun kuvan päälle
ApplyLoadedOCRTextLayer muuttaa tunnistetut sanat haettavaksi kerrokseksi piirtämällä ne tekstin renderöintitilassa 3, ISO 32000-1 §9.3.6:ssa määritellyssä tilassa joka ei täytä eikä piirrä reunaa, niiden lähteenä olevan skannatun kuvan päälle. Sisältöstream alkaa BT-operaattorilla jota seuraa 3 Tr, ja jokainen sana sijoitetaan tekstimatriisilla joka rakennetaan ilmoitetun baselinen, pyynnön DPI:stä pikseleistä muunnetun versaalikorkeuden ja synteettisen glyfiajon mitattuun sanaleveyteen venyttävän vaakasuuntaisen skaalan avulla. Tulos kopioituu ja on haettavissa tekstinä mutta ei maalaa mitään
Moottoriton overload luo sisäänrakennetun tunnistimen puolestasi, ja sitä useimpien tämän polun kutsujien tulisi käyttää. Tunnistus, Unicoden validointi, budjetin laskenta ja sisällön rakentaminen valmistuvat kaikki ennen copy-on-write-transaktion avaamista, joten peruutus, budjetin ylitys tai moottorivirhe jättää objektigraafin ja versionumeron ennalleen. Sanat suodatetaan kahdesti: moottori pudottaa kaiken oman 0,55:n glyfikohtaisen luottamusrajansa alapuolelta, minkä jälkeen THPDFOCRTextLayerOptions.MinimumConfidence (oletus 0,5) pudottaa kutsujan rajaa heikommat kokonaiset sanat
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; // jätä digitaalisina syntyneet sivut ennalleen
Options.UseOptionalContentGroup := True;
Options.OptionalContentGroupName := 'OCR Text Layer';
// moottoriton overload: HotPDF toimittaa sisäänrakennetun rajatun tunnistimen
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;
Yksi rajoitus kannattaa sanoa suoraan eikä löytää vasta myöhemmin. Näkymätön kerros käyttää jaettua synteettistä upottamatonta Type0-fonttia, joka riittää hakuun ja kopiointiin jokaisessa katseluohjelmassa mutta ei täytä ISO 19005:n fontin upotusvaatimusta. Jos tulosteen täytyy olla PDF/A, kutsujan täytyy upottaa vaatimukset täyttävä fontti erikseen. OCR-tekstikerros sisältää myös geometrian eikä rakennetta, joten lukujärjestys tulee pelkistä glyfipositioista; jos tarvitset loogisen järjestyksen sivulta jolla on jo oikeaa tekstiä, tagipuusta ohjattu rakenteen mukainen tekstinpoiminta on eri työkalu eri ongelmaan
Mihin sisäänrakennettu moottori pysähtyy
Sisäänrakennettu moottori on tarkoituksella kapea, ja sen rajojen tunteminen pitää sen hyödyllisenä. Se kohdistuu korkean kontrastin koneella painettuun ASCII-tekstiin, joka tulee lähelle sen viittä mallifonttia, ja kaikki rajojen ulkopuolinen palauttaa sanattoman tuloksen arvauksen sijaan. Konkreettiset rajat ovat:
- Kuvat enintään koolla 4096 × 4096 ja 4 194 304 pikseliä, 2000 ms:n tunnistusmääräajalla ja yhteistyöhön perustuvalla peruutuksella
THPDFCancellationToken-tokenin kautta - 62 merkin ASCII-kirjain- ja numeromerkkien aakkosto; ei välimerkkejä, aksenttimerkkejä eikä CJK-merkkejä
- Vain akselien suuntainen teksti siinä sivukierrossa johon renderöijä on jo normalisoinut sen; vinoutuneita skannauksia ei suoristeta
- Epäselvät glyfiparit jätetään ratkaisematta, joten sivu voi palauttaa osittaisia sanoja tai diagnostiikan "found no unambiguous ASCII words"
Kun tämä kehys on liian pieni, IHPDFOCREngine on saumakohta. Toteuta Recognize oman moottorisi päälle, anna se kolmeargumenttiselle ApplyLoadedOCRTextLayer-overloadille ja kaikki alavirran toiminta (koordinaattien kartoitus, kiertojen käsittely, Unicoden validointi, budjetit ja atominen commit) pysyy samana. Bittikartta lainataan synkronisen kutsun ajaksi eikä sitä saa säilyttää. Varmista kerroksen oikea asettuminen lataamalla tallennettu tiedosto uudelleen ja ajamalla tavallinen tekstipolku joka on kuvattu artikkelissa tekstin poiminta ladatusta PDF:stä Delphissä; jos sanat palaavat, kerros on todellinen
Sisäänrakennettu mallinsovitus-OCR, näkymätön tekstikerros, niitä syöttävä sivurenderöijä ja niitä varmistava ladatun dokumentin tekstinpoiminta toimitetaan kaikki samassa natiivissa VCL-komponentissa ilman ulkoista OCR-ajonaikaista riippuvuutta ja ilman sovelluksen rinnalle toimitettavaa DLL:ää. Jos rakennat dokumenttien kaappausta, arkistointia tai skannausten hakua Delphillä tai C++Builderilla, HotPDF Delphi PDF -komponentti antaa koko putken yhtenä riippuvuutena