Tehnični članak

Tesseract OCR v iskalni PDF v Delphiju s HotPDF

HotPDF spremeni skenirane strani PDF v iskalni PDF s Tesseractom prek HPDFCreateTesseractOCREngine, tovarne, ki ovije krajevno nameščeno izvršljivo datoteko Tesseract kot IHPDFOCREngine. Ta motor podate ApplyLoadedOCRTextLayer, ki upodobi vsako stran, požene Tesseract enkrat na stran, razčleni njegov izhod TSV na ravni besed in zagosti nevidno plast besedila Unicode za vse zahtevane strani v eni transakciji ali za nobeno

Cevovod OCR HotPDF na stran: upodobi stran pri nastavljenem DPI, shrani input.bmp v zasebno mapo HotPDF-OCR, poženi otroški proces Tesseract s tessedit_create_tsv, razčleni dvanajststolpčni TSV, filtrira besede po zanesljivosti in zagosti nevidno plast besedila za vse zahtevane strani ali za nobeno
Vtičnik zamenja le prepoznavo: upodabljanje, razčlenjevanje, preverjanje in zagotovitev vse-ali-nič ostanejo v obstoječem cevovodu plasti besedila, zato nadaljnja koda nikoli ne spremeni

Razlog, da ta vtičnik obstaja, je obseg. Vgrajen motor OCR z ujemanjem predlog je namenoma ozek: strojno natisnjene črke in števke ASCII, nič drugega. Računi z naglašenimi imeni, kitajske pogodbe in večjezični arhivi potrebujejo pravega prepoznavalnika z učenimi jezikovnimi modeli, Tesseract pa je očitni kandidat, ker je program ukazne vrstice, ki ga lahko namestite poleg svoje aplikacije. Klicanje zunanjega programa iz knjižnice dokumentov zveni trivialno. Ni pa, in večina zanimive kode v vtičniku je o tem, kaj se zgodi, ko se program zmoti, obstane, dobi preklic ali podeduje stvari, ki jih nikoli ne bi smel videti

Kako HotPDF poganja Tesseract iz aplikacije Delphi?

HotPDF poganja Tesseract kot skriti otroški proces na stran, mu daje upodobljeno bitno sliko in bere nazaj datoteko TSV, rezultat pa izpostavi skozi isti šiv IHPDFOCREngine, ki ga uporablja vgrajeni motor. Nič se nadalje ne spremeni: preslikava koordinat, ravnanje z vrtenjem, preverjanje Unicode, filtriranje zanesljivosti in atomična zagotovitev so cevovod plasti besedila, ki ga že imate. Tovarna živi v enoti HPDFTesseractRecognition in preverja vnaprej: izvršljiva datoteka mora obstajati, mapa tessdata mora obstajati, zamuda mora biti med 1 in 3.600.000 milisekundami, identifikator jezika pa sme vsebovati le črke ASCII, števke, _ in +. Zadnji preizkus je pomemben, ker niz jezika pristane na ukazni vrstici, eng+chi_sim je zakonita vrednost Tesseract, karkoli z navedki ali presledki pa ni

uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string;
  Token: THPDFCancellationToken);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // sproži EArgumentException za manjkajočo izvršljivo datoteko, manjkajoč tessdata,
  // napačen identifikator jezika ali zamudo izven 1..3600000 ms
  Engine := HPDFCreateTesseractOCREngine(
    'C:\OCR\Tesseract\tesseract.exe',
    'C:\OCR\Tesseract\tessdata',
    'eng+chi_sim',      // več modelov, spojenih z '+'
    120000);            // omejitev na stran, privzeto je 60000
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    if Doc.LoadFromFile(SourceFile) < 1 then
      raise Exception.Create('Cannot load ' + SourceFile);
    Options := THPDFOCRTextLayerOptions.Default;  // 300 DPI, MinimumConfidence 0.5
    Options.CancellationToken := Token;
    // prazen seznam strani pomeni vsako stran; strani z besedilom se privzeto preskočijo
    if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
    begin
      Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
        ' words accepted, ', Info.DroppedWordCount, ' dropped');
      Doc.SaveLoadedDocument(TargetFile);
    end
    else
      case Info.Status of
        otlsCancelled:      Writeln('Cancelled, document unchanged');
        otlsEngineError:    Writeln('Engine: ', string(Info.Diagnostic));
        otlsBudgetExceeded: Writeln('Budget: ', string(Info.Diagnostic));
      else
        Writeln(string(Info.Diagnostic));
      end;
  finally
    Doc.Free;
  end;
end;

Za vsako stran Recognize ustvari zasebno mapo pod potjo temp z imenom HotPDF-OCR-{GUID}, shrani upodobljeno bitno sliko kot input.bmp in požene tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1, kjer je vsak argument poti v navedkih po pravilih ubežanja ukazne vrstice Windows za poševnice nazaj in vgrajene navedke. Vrednost --dpi je upodobitveni DPI iz THPDFOCRTextLayerOptions.DPI, tako da Tesseract nikoli ne ugiba ločljivosti iz metapodatkov slike, --psm 3 pa zahteva povsem samodejno segmentacijo strani. Motor se prijavi kot Tesseract (local CLI), kar pristane v Info.EngineName. Tesseract in njegovi jezikovni modeli niso zapakirani s HotPDF; namestitev je delo aplikacije

Zakaj je razčlenjevalnik TSV tako strog?

Razčlenjevalnik TSV v HotPDF odpove celo stran ob kateri koli deformirani vrstici, ker delno razčlenjen seznam besed daje plast besedila, ki tiho ne ustreza sliki. Izhod TSV Tesseract ima fiksno glavo dvanajstih stolpcev, od level do text, HotPDF pa primerja prvo vrstico s to točno glavo, potem ko odstrani neobvezno oznako vrstnega reda bajtov. Vsaka naslednja vrstica se mora razdeliti točno na dvanajst polj, delitev pa se ustavi po enajstem tabulatorju, tako da tabulator znotraj prepoznanega besedila ostane del besede in ne ustvari trinajstega stolpca. Le vrstice ravni 5 so besede; ravni od 1 do 4 opisujejo strani, bloke, odstavke in vrstice, te pa se preskočijo. Vrstice ravni 5, katerih besedilo je prazno ali čisti presledek, se prav tako preskočijo, ker prazna beseda ima škatlo, a nič za locirati ali iskati. Vse ostalo se trdo preveri: celoštevilčna geometrija, zanesljivost, razčlenjena z nespremenljivim formatom en-US, tako da nemška okolica ne prebere 93.5 kot smeti, škatla, ki leži povsem znotraj bitne slike, in zanesljivost med 0 in 100. Ena sama odpoved sproži izjemo, motor vrne False in polje besed se počisti. Regresijski preizkusi vključujejo točno ta primer: ena veljavna beseda, ki ji sledi pokvarjena vrstica, mora dati nič besed in ne eno

Šest zapor, ki jih vsaka vrstica TSV Tesseract prestane v HotPDF: točna glava dvanajstih stolpcev, točno dvanajst polj, le raven 5, neprazno besedilo, škatla znotraj bitne slike in zanesljivost od 0 do 100, razčlenjena nespremenljivo, kjer ena pokvarjena vrstica odpove celo stran do nič besed
Delno razčlenjen seznam besed bi tiho ne ustrezel sliki, zato razčlenjevalnik odkloni celotno stran ob prvi deformirani vrstici, namesto da bi obdržal besede, ki jih je že prebral
// strnjeno iz zanke ravni 5 v HPDFLocalTSVRecognition
if (Fields.Count <> 12) or not TryStrToInt(Fields[0], Level) then
  raise EConvertError.Create('Invalid Local OCR TSV row');
if Level <> 5 then Continue;                 // vrstice strani/bloka/odstavka/vrstice
WordText := Fields[11];
if Trim(WordText) = '' then Continue;        // besede presledkov nimajo položaja
if not TryStrToInt(Fields[6], X) or not TryStrToInt(Fields[7], Y) or
  not TryStrToInt(Fields[8], W) or not TryStrToInt(Fields[9], H) or
  not TryStrToFloat(Fields[10], Confidence, Settings) then
  raise EConvertError.Create('Invalid Local OCR word geometry');
if (X < 0) or (Y < 0) or (W <= 0) or (H <= 0) or
  (Int64(X) + W > Request.Bitmap.Width) or
  (Int64(Y) + H > Request.Bitmap.Height) or
  not ((Confidence >= 0) and (Confidence <= 100)) then
  raise EConvertError.Create('Local OCR word is outside the image');
Words[Count].Confidence := Confidence / 100;  // cevovod pričakuje 0..1

Zadnja vrstica sodeluje s privzeto vrednostjo, ki je morda ne pričakujete. Zanesljivost Tesseract teče od 0 do 100, cevovod dela v 0 do 1, THPDFOCRTextLayerOptions.MinimumConfidence pa privzame 0.5, zato se vsaka beseda Tesseract pod 50 šteje v Info.DroppedWordCount in nikoli ne pride do strani. Na čistem skenu 300 DPI je to razumen prag. Na hrupnem faksu lahko odvrne presenetljiv del strani, prava poteza pa je pogledati števec odvrženih, preden znižate prag, ker so nizko zanesljive besede točno tiste, ki so najbolj verjetno napačne

Kaj otroški proces Tesseract podeduje?

Otroški proces Tesseract podeduje točno dva ročaja od HotPDF: ročaj NUL za standardni vhod in izhod ter ročaj datoteke za standardno napako. Ta natančnost je poanta. CreateProcess z bInheritHandles = True je način, kako standardne ročaje podate otroku, a sam preda vsak podedljiv ročaj v gostujočem procesu, vključno z datotekami, cevmi in dogodki, odprtimi s strani nesorodne kode v vaši aplikaciji. Otrok nato te objekte obdrži žive, dokler ne izstopi, zato datoteka ostane zaklenjena ali cev nikoli ne vidi svojega konca, medtem ko Tesseract melje skozi stran. HotPDF zapre to vrzel z razširjenim zapisom zagona: STARTUPINFOEX, seznam lastnosti, ki nosi PROC_THREAD_ATTRIBUTE_HANDLE_LIST, in zastavica ustvarjanja EXTENDED_STARTUPINFO_PRESENT. Ko je seznam ročajev na mestu, mora biti bInheritHandles še vedno True, a le naštetih ročajev prečka mejo. Isto razmišljanje o zadrževanju poganja izolacijo kodekov slik PDF v delovnih procesih, kjer je otrok nezaupanja vredna koda; tu je otrok zaupanja vreden, a gost ni edini lastnik svoje tabele ročajev

Podedovanje ročajev otroškega procesa Tesseract v HotPDF: goli CreateProcess z bInheritHandles podre vsak podedljiv ročaj datoteke, cevi in dogodka otroku, STARTUPINFOEX s PROC_THREAD_ATTRIBUTE_HANDLE_LIST pa omeji množico na ročaj NUL za stdin in stdout ter ročaj datoteke stderr
Brez seznama lastnosti otrok obdrži nesorodne objekte žive, dokler ne izstopi, zaklene datoteke in strada cevi; z njim prečka mejo le dva našteta ročaja
// konstante prikazane po imenu; vir podaja njihove številske vrednosti
// oba ročaja sta ustvarjena z bInheritHandle = True
InheritedHandles[0] := NullHandle;    // stdin in stdout
InheritedHandles[1] := ErrorHandle;   // stderr.txt v zasebni mapi
InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, AttributeBytes);
UpdateProcThreadAttribute(Startup.AttributeList, 0,
  PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
  @InheritedHandles[0], SizeOf(InheritedHandles), nil, nil);
CreateProcess(PChar(Executable), PChar(Command), nil, nil,
  True,                                        // zahtevano s strani seznama ročajev
  CREATE_NO_WINDOW or EXTENDED_STARTUPINFO_PRESENT,
  nil, PChar(DirectoryName), Startup.StartupInfo, ProcessInfo);

Zakaj lahko prekinjen tek OCR izgleda kot odpoved motorja?

Prekinjen tek OCR izgleda kot odpoved motorja, ker IHPDFOCREngine.Recognize vrne en sam Boolean, False pa pomeni tako »Tesseract je odpovedal« kot »uporabnik je pritisnil Prekliči«. Vtičnik vsakih 25 milisekund vzorči žeton preklica in zamudo, medtem ko otrok teče, in ko žeton sproži, sproži izjemo znotraj Recognize, ujame svojo izjemo, počisti za sabo in vrne False z diagnostiko. Če bi cevovod to obravnaval kot napako motorja, bi klicatelj videl otlsEngineError za delo, ki ga je uporabnik namerno ustavil. ApplyLoadedOCRTextLayer zato preveri žeton najprej, kadarkoli Recognize vrne False, rezultat pa spremeni v napako motorja le, kadar žeton ni bil nastavljen. Ta vrstni red ohrani pogodbo več strani: prepoznavanje, preverjanje, obračunavanje proračuna in gradnja vsebine tečejo za vsako zahtevano stran, preden se odpre transakcija grafa, zato preklic na strani 40 od 50 poroča otlsCancelled in pusti dokument, vključno s prvimi 39 stranmi, nedotaknjen. Ni datoteke, delno iskalne, ki bi jo moral kasneje razlagati, preostalo ravnanje z odpovedmi pa sledi istemu omejenemu slogu:

  • Zamuda je na klic Recognize, merjena od njenega začetka, tako da privzeta 60.000 ms velja za vsako stran in ne za celoten dokument
  • Otrok, ki še teče ob zamudi ali preklicu, je ukinjen, počakano je nanj do 5 sekund, njegova zasebna mapa pa je izbrisana v bloku finally
  • output.tsv je omejen na 64 MiB in stderr.txt na 1 MiB, preverjeno medtem, ko otrok teče, in tudi potem, ko izstopi
  • Število besed in enote kode UTF-16 so omejeni na stran s preostalimi proračuni MaxWordsPerPage, MaxTotalWords in MaxTextCodeUnits, njihovo preseganje pa odpove tek, namesto da bi obrezalo seznam besed
  • Standardni izhod gre na NUL, ker Tesseract zapiše output.tsv, standardna napaka pa gre v datoteko, tako da je izstopna koda, različna od nič, poročana z do 4.096 znaki lastnega pritoževanja motorja, navadno najhitrejši način, da izveste, da manjka datoteka .traineddata

Kako prepoznane besede postanejo nevidna plast besedila

HotPDF zapiše besede Tesseract kot nevidno besedilo z načinom upodabljanja besedila 3, načinom ne polni ne potegne, definiranem v ISO 32000-1 §9.3.6, tako da stran še vedno kaže skenirano sliko, medtem ko iskanje in kopiranje delujeta na prepoznanih besedah. Tok vsebine odpre BT s 3 Tr, vsaka beseda dobi matriko Tm pri svoji osnovni črti, velikost pisave, izpeljano iz višine škatle v pikslih pri upodobitvenem DPI, in horizontalno merilo Tz, ki raztegne tek glif do izmerjene širine škatle, zato poudarek iskanja pristane na besedi v sliki in ne drifta čeznjo

TSV Tesseract ima škatle, a brez osnovnih črt, zato vtičnik poroča vsako besedo brez nje, cevovod pa oceni osnovno črto na petino višine škatle nad spodnjim robom. Besedilo samo gre skozi skupno nevgrajeno pisavo Type0 s kodiranjem Identity-H in generirano CMap ToUnicode, en CID na ločeno skalarno vrednost Unicode čez celoten tek, kar je način, kako kitajščina, naglašena latinščina in znaki dopolnilne ravnine vsi preživijo kopiranje in iskanje. Ta zasnova ima dve omejitvi, vredni izrekanja vnaprej: en tek lahko nosi največ 65.535 ločenih skalarnih vrednosti, nevgrajena pisava pa ne zadostuje zahtevi po vgradnji pisav ISO 19005, zato izhod PDF/A potrebuje posebej vgrajeno skladno pisavo. Preverjanje rezultata je preprosto in vredno samodejnega izvajanja: shranite, znova naložite in poženite navadno pot besedila naloženega dokumenta iz izvleče besedila iz naloženega PDF v Delphiju; če se besede vrnejo na pričakovanih straneh, je plast resnična

RapidOCR in drugi motorji na istem protokolu TSV

HotPDF ponovno uporabi istega poganjalca procesov in razčlenjevalca TSV za RapidOCR prek HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds), kar je uporabnejša izbira za skene v poenostavljeno kitajščini. Ukazna vrstica je identična, le da je pot skripta mostu vstavljena za izvršljivo datoteko Python, jezik pa je fiksiran na chi_sim. HotPDF dobavlja most kot tools/OCR/rapidocr_tsv.py; pričakuje paketa rapidocr in onnxruntime plus tri krajevne modele ONNX, onemogoči samodejne prenose modelov in zapiše TSV v obliki Tesseract, tako da stran Delphi ne potrebuje drugega razčlenjevalca. Ime motorja, poročano v Info.EngineName, je RapidOCR (local ONNX). Ta oblika nakazuje splošen recept: vsak prepoznavalnik, ki ga lahko ovijete v majhen skript, ki sprejme seznam argumentov v stilu Tesseract in odda dvanajststolpčni TSV, podeduje izolacijo ročajev, zamudo, preklic, proračune izhoda in zagotovitev vse-ali-nič zastonj. Vtičnika sta le za Windows, tečeta eno stran naenkrat usklajeno in ne izravnata naklona ali predobdelata slike čez to, kar proizvede upodobitelj, zato kakovost slike na vhodu še vedno daje strop tistemu, kar pride ven

Vtičnika Tesseract in RapidOCR, zapisovalec nevidne plasti besedila, upodobitelj strani, ki ju napaja, in izvleče besedila, ki preveri rezultat, vsi prihajajo v isto izvorno komponento VCL za Delphi in C++Builder. Če dodajate OCR aplikaciji za zajemanje dokumentov ali arhiviranje, vam komponenta HotPDF Delphi PDF da cevovod z le samim motorjem OCR, ki ga je treba še namestiti