Odborný článok

Čínska a multijazyčná OCR s RapidOCR v Delphi (HotPDF)

HotPDF vykonáva čínsku a multijazyčnú OCR v Delphi cez svoj natívny adaptér RapidOCR DLL: THPDFRapidOCRDLLOptions.ForLanguage mapuje jazykovú značku ako 'zh-CN', 'zh-TW', 'ru' alebo 'ar' na zodpovedajúci rozpoznávací model a znakový slovník a THotPDF.ApplyLoadedOCRTextLayer mení rozpoznané línie na neviditeľnú, prehľadávateľnú Unicode textovú vrstvu na naskenovaných PDF stranách

Rozbehať demo v latinkovom písme je tá ľahká časť. Zaujímavé zlyhania začínajú, keď prepnete na tradičnú čínštinu alebo ruštinu a výstup sa zmení na sebaistý, dobre utvorený nesmysel, alebo keď každá línia potichu príde o posledný znak, alebo keď arabská strana sa vráti s textovými boxmi v nesprávnom poradí. Žiadne z toho samo o sebe nespadne výnimkou. Jazykové presety pridané v HotPDF v2.775.0 existujú prevažne na zapchanie týchto dier a štyri pasce nižšie stoja za pochopenie aj vtedy, keď sa natívneho kódu nikdy nedotknete, lebo každá z nich vysvetľuje príznak, po ktorom by ste inak behali deň

Ako ForLanguage vyberie model a slovník?

THPDFRapidOCRDLLOptions.ForLanguage vyrieši značku na jeden z deviatich profilov a vráti options ukazujúce na <profile>/recognition.onnx a <profile>/dictionary.txt pod vaším adresárom modelov, pričom ponechá zdieľaný detektor, voliteľný klasifikátor uhla a predvolené vlákien, pixelov aj termínu z THPDFRapidOCRDLLOptions.Default. Metóda prevedie značku na malé písmená, podčiarkovníky zmení na spojovníky a odstrihne okolité biele miesta, takže 'zh_TW', 'ZH-tw' aj ' zh-tw ' pristanú na tom istom profile. Aliasy sú explicitný zoznam, nie porovnanie prefixu: 'zh-Hant-TW' sa prijme, lebo je vypísaný, zatiaľ čo ľubovoľná regionálna varianta, ktorá vypísaná nie je, vyhodí EArgumentException skôr, než sa načíta akýkoľvek model

HotPDF riešenie profilu ForLanguage pre THPDFRapidOCRDLLOptions: značky ako zh_TW, ZH-tw a zh-TW sa normalizujú a porovnajú proti deviatim vypísaným profilom, z ktorých každý pripne rozpoznávací model a slovník nastavované vždy spolu, zatiaľ čo nevypísaná značka vyhodí EArgumentException skôr, než sa čokoľvek načíta
jedna značka vyberá jeden pripnutý pár model-slovník; detektor, klasifikátor aj rozpočty ostanú zdieľané a neznáma značka zlyhá rýchlo namiesto načítania čohokoľvek
ProfilJazykyPríklady značiekPripnutý model
chZjednodušená čínština a angličtinazh, zh-CN, zh-Hans, chi_simPP-OCRv4
chinese_chtTradičná čínštinazh-TW, zh-HK, zh-Hant, chi_traPP-OCRv3
enAngličtinaen, en-US, en-GB, engPP-OCRv4
latinFrancúzština, nemčina, španielčina, portugalčina, taliančina, holandčina, turečtinafr, de, es-419, pt-BR, trPP-OCRv3
japanJapončinaja, ja-JP, jpnPP-OCRv4
koreanKórejčinako, ko-KR, korPP-OCRv4
cyrillicRuština, ukrajinčina, bulharčina, bieloruštinaru, ru-RU, uk, bgPP-OCRv3
arabicArabčina, perzština, urdčinaar, ar-SA, fa, urPP-OCRv4
devanagariHindčina, maráthčina, nepálčinahi, mr, nePP-OCRv4

Adaptér sám nič nestahuje. Súbory provisionsujete raz s pribaleným pomocníkom, napríklad tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (alebo -Language All pre všetkých deväť profilov) a pomocník položí zdieľaný detektor a klasifikátor na koreňové mená súborov, ktoré očakáva Default. Potom sa nasken zjednodušenej čínštiny stane prehľadávateľným na pár riadkoch. Inštalatérstvo enginu je to isté rozhranie IHPDFOCREngine popísané v článku o in-process RapidOCR DLL a jeho ABI hranici, takže tento ostanú pri jazykoch

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

procedure MakeChineseScanSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Models: THPDFRapidOCRDLLOptions;
  Layer: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // ch/recognition.onnx + ch/dictionary.txt, zdieľaný detektor a klasifikátor
  Models := THPDFRapidOCRDLLOptions.ForLanguage('zh-CN');
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    if Doc.LoadFromFile(SourceFile) < 1 then
      raise Exception.Create('Cannot load ' + SourceFile);
    Layer := THPDFOCRTextLayerOptions.Default;  // 300 DPI, MinimumConfidence 0.5
    // prázdny zoznam strán znamená každá strana; strany, ktoré už text majú, sa preskočia
    if not Doc.ApplyLoadedOCRTextLayer([], Engine, Layer, Info) then
      raise Exception.Create(string(Info.Diagnostic));
    Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
      ' lines, ', Info.UniqueScalarCount, ' distinct characters');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

Dva detaily v tom výstupe si zaslúžia poznámku. Natívna pipeline vracia jeden výsledok na detekovanú textovú líniu, nie na slovo, takže AcceptedWordCount tu počíta línie a MinimumConfidence sa porovnáva s priemernou confidence znakov celej línie: línia priemerujúca 0.45 sa zahodí ako celok. UniqueScalarCount hlási, koľko odlišných Unicode scalarov musela textová vrstva zmapovať do svojho fontu a tabuľky ToUnicode — užitočná sanity kontrola, že CJK text naozaj prišiel, a nie hrsť latinských záloha. Rozhranie enginu držte nažive cez dokumenty, lebo inicializácia modelov sa deje vo factory a je to tá drahá etapa

Prečo prepnutie len rozpoznávacieho modelu vyrobí smetie?

CTC rozpoznávací model nikdy nevypúšťa znaky, len indexy tried a slovník je jediná vec, ktorá zmení index 1 204 na glyf. Vymeňte ch/recognition.onnx za cyrillic/recognition.onnx, ale nechajte čínsky slovník a model bude spokojne emitovať platné cyrilské indexy, ktoré starý slovník preloží na náhodné han znaky. Výsledok vyzerá ako text, prejde UTF-8 validáciou a je prehľadávateľný presne na nič. Preto ForLanguage vždy nastavuje RecognitionModel a CharacterDictionary spolu a preto ručne stavané options nikdy nemajú meniť jedno bez druhého

Zjavná bezpečnostná kontrola, porovnanie veľkosti slovníka so šírkou výstupu modelu, je nutná, ale nie postačujúca. Dva slovníky môžu mať rovnaký počet položiek v inom poradí a odchýlka o jedno v poradí posunie každý znak o jeden code point. HotPDF preto kontroluje v dvoch etapách, keď factory inicializuje model. Najprv sa počet výstupných tried musí rovnať položkám slovníka plus dva. Potom, keď ONNX súbor vnára metadata zoznam character, porovná sa každá položka slovníka s ním v poradí a nezrovnalosť prepasuje inicializáciu s EInvalidOperation a natívnou diagnostikou namiesto neskôr vyrobenia verohodného smetia

To „plus dva“ pochádza z rozloženia tried. Trieda 0 je CTC blank, triedy 1 až N sú riadky slovníka v poradí súboru a finálna trieda je medzera. Niektoré slovníky nesú aj vlastnú položku medzery a ten riadok musí ostať presne taký, aký je. Tu dobre mienený Trim narobí reálnu škodu: zmení položku s jedinou medzerou na prázdny reťazec a posunie alebo pokazí tabuľku. Jediná bezpečná normalizácia je odstránenie koncového carriage return, takže slovník uložený s koncami riadkov CRLF sa načíta korektne, zatiaľ čo UTF-8 byte order mark, prázdny riadok alebo položka obsahujúca tab sa odmietnu. Náčrt nižšie ukazuje rozloženie v Pascali; je to vysvetľujúci kód, nie API HotPDF

HotPDF rozloženie tabuľky tried CTC pre slovníky RapidOCR: trieda 0 je blank, triedy 1 až N sú riadky slovníka v poradí súboru s prípadnou samostatnou položkou medzery zachovanou a finálna trieda je medzera, čo dáva N plus 2 výstupných tried, ktoré factory overuje voči modelu, metadata vrátane
slovník je jediná vec meniaca indexy tried na znaky, takže jeho veľkosť, poradie aj položka medzery sa overia skôr, než sa rozpozná jediná strana
// Len ilustrácia: tabuľka tried, ktorú CTC recognizer očakáva
uses
  SysUtils, IOUtils;

function BuildCTCClassTable(const FileName: string): TArray<string>;
var
  Text, Entry: string;
  Lines: TArray<string>;
  I, Last: Integer;
begin
  Text := TEncoding.UTF8.GetString(TFile.ReadAllBytes(FileName));
  if (Text <> '') and (Text[1] = #$FEFF) then
    raise EArgumentException.Create('Dictionary must be UTF-8 without a BOM');
  Lines := Text.Split([#10]);
  Last := High(Lines);
  if (Last >= 0) and (Lines[Last] = '') then
    Dec(Last);                                   // nový riadok na konci súboru
  SetLength(Result, Last + 3);
  Result[0] := '';                               // trieda 0: CTC blank
  for I := 0 to Last do
  begin
    Entry := Lines[I];
    if (Entry <> '') and (Entry[Length(Entry)] = #13) then
      SetLength(Entry, Length(Entry) - 1);       // CRLF: zhoď len CR
    if (Entry = '') or (Pos(#9, Entry) > 0) then
      raise EArgumentException.Create('Invalid dictionary entry');
    Result[I + 1] := Entry;                      // nikdy Trim: ' ' je trieda
  end;
  Result[Last + 2] := ' ';                       // finálna trieda: medzera
  // Length(Result) sa musí rovnať počtu tried výstupu modelu
end;

Čo vlastne robí greedy CTC dekódovanie?

Greedy CTC dekódovanie vyberá najlepšie skórovanú triedu v každom časovom kroku, zvinie po sebe idúce opakovania do jedného znaku a zahodí triedu blank; blank je to, čo dovoľuje skutočne zdvojeným písmenám prežiť. Rozpoznávací model sa pozerá na textovú líniu ako na sekvenciu úzkych vertikálnych rezov a pre každý rez, čiže časový krok, vypúšťa pravdepodobnosť pre každú triedu. Línia obsahujúca AA中 môže vyprodukovať sekvenciu argmax A A blank A 中 space. Zvinutie prvých dvoch krokov A dá jedno A, blank ho oddelí od ďalšieho A a výsledkom je AA中 s koncovou medzerou intact. Bez pravidla blank by book a bok boli nerozoznateľné

HotPDF priebeh GreedyCTCDecode: šesť časových krokov hlasuje argmax triedy A, A, blank, A, han znak a medzera, po sebe idúce opakovania sa zvinú, blank reštartuje strážcu opakovaní, takže skutočne zdvojené písmeno prežije, a tri hraničné bugy potichu zahodia medzery slov, posledný znak alebo zdvojené znaky
dekodér je tucet riadkov a každá hranica má význam: zahrňte poslednú triedu, zahrňte posledný krok a nechajte opakovania oddeľovať len blank

Keďže dekodér má len tucet riadkov, ľahko sa pokazia hranice a zlyhania sú tiché. Ak vnútorná slučka argmax skončí jednu triedu skôr, trieda medzery nemôže nikdy vyhrať a každá línia sa vráti bez medzier slov, čo rozmláti frázové hľadanie na anglických a latinských stranách. Ak vonkajšia slučka skončí jeden časový krok skôr, posledný znak každej línie zmizne, čo môže byť pri krátkej línii tretina textu. A ak strážca opakovaní nie je reštartovaný blankom, zdvojené znaky ako ll alebo čínske reduplikácie ako 谢谢 sa zvinú do jedného. Dekodér HotPDF zahrňa poslednú triedu aj posledný časový krok, drží opakovania oddelené blankom a navyše odmietne skóre, ktoré nie sú konečné alebo padajú mimo 0 až 1, a akýkoľvek počet tried nesúhlasiaci so slovníkom. Tu je tá istá logika ako Pascal ilustrácia

// Len ilustrácia: greedy CTC dekódovanie s korektnými hranicami.
// Scores drží Steps * Classes pravdepodobností, jeden riadok na časový krok
function GreedyCTCDecode(const Scores: array of Single;
  Steps, Classes: Integer; const Characters: array of string): string;
var
  Step, C, Best, Previous: Integer;
  BestScore: Single;
begin
  if (Classes < 3) or (Length(Characters) <> Classes) or
    (Length(Scores) <> Steps * Classes) then
    raise EArgumentException.Create('Model output does not match the dictionary');
  Result := '';
  Previous := 0;                            // trieda 0 je CTC blank
  for Step := 0 to Steps - 1 do             // zahrňte posledný časový krok
  begin
    Best := 0;
    BestScore := Scores[Step * Classes];
    for C := 1 to Classes - 1 do            // zahrňte poslednú triedu (medzeru)
      if Scores[Step * Classes + C] > BestScore then
      begin
        Best := C;
        BestScore := Scores[Step * Classes + C];
      end;
    if (Best <> 0) and (Best <> Previous) then
      Result := Result + Characters[Best];
    Previous := Best;                       // blank reštartuje strážcu opakovaní
  end;
end;

Greedy dekódovanie nie je najpresnejšia dostupná CTC stratégia; beam search s jazykovým modelom dokáže opraviť niektoré nejednoznačné rezy. Pre tlačené dokumenty pri 300 DPI je greedy výsledok zvyčajne to, čo model ponúknuť môže, a dekodér nie je miesto, kde sa kompenzujú slabosti modelu. Latinský model PP-OCRv3 dokáže napríklad prečítať ñ ako n dokonca aj na čistom vstupe. HotPDF to nezametá post-processingovými náhradami znakov, lebo substitučná tabuľka, ktorá opraví španielčinu, niečo iné pokazí a zlý znak v prehľadávateľnej vrstve je horší než poctivá minule

Ako HotPDF usporadúva textové línie, vrátane sprava-doľava arabských?

HotPDF zotriedi detekované textové boxy zhora nadol, zoskupí boxy do riadka, keď sa vertikálne prekrývajú najmenej o polovicu výšky menšieho boxu a usporiada každý riadok zľava doprava, alebo sprava doľava, keď je zapnuté RightToLeft; znaky vo vnútri každej rozpoznanej línie sa nikdy neprevracajú. Zoskupovanie má význam, lebo detektor často rozdelí jeden vizuálny riadok na viacero boxov, napríklad nálepka a hodnota oddelené širokou medzerou a čisté triedenie podľa hornej súradnice by ich prelúdalo so susedným riadkom zakaždým, keď sa ich horné hrany líšia o pixel či dva

Arabský preset nastaví RightToLeft := True, čo prikáže DLL usporiadať boxy v každom riadku podľa ich pravej hrany, od pravého okraja dovnútra. To je celý efekt. Text, ktorý model vráti pre líniu, je už v Unicode logickom poradí, poradí, v akom ho arabský čitateľ číta a píše, a to je aj poradie, ktoré očakáva extrakcia aj hľadanie PDF textu. Mechanické prevrátenie reťazca, aby „vyzeral správne“ v debugeri, by rozbilo hľadanie, kopírovanie, vkladanie aj screen readery. Obojsmerné zobrazenie a tvarovanie glyfov je práca prehliadača

Jeden engine obsluhuje jeden jazykový profil. Automatická detekcia písma neexistuje, takže dokument miešajúci písma potrebuje jeden engine na profil, aplikovaný na strany, ktoré ho používajú. Keďže ApplyLoadedOCRTextLayer berie explicitný zoznam strán a commitne každé volanie ako vlastnú all-or-nothing transakciu, je to priamočiary

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
  Models: THPDFRapidOCRDLLOptions;
begin
  // vyhodí EArgumentException pre neznámu značku, skôr než sa čokoľvek načíta
  Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
  Models.MaxPixels := 33554432;          // priestor pre strany A3 pri 300 DPI
  Result := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
end;

procedure OCRMixedArchive(Doc: THotPDF);
var
  Chinese, Arabic: IHPDFOCREngine;
  Layer: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  Chinese := CreateRapidEngine('zh-TW');  // profil chinese_cht
  Arabic := CreateRapidEngine('ar-SA');   // profil arabic, RightToLeft = True
  Layer := THPDFOCRTextLayerOptions.Default;
  if not Doc.ApplyLoadedOCRTextLayer([0, 1, 2], Chinese, Layer, Info) then
    raise Exception.Create(string(Info.Diagnostic));
  if not Doc.ApplyLoadedOCRTextLayer([3], Arabic, Layer, Info) then
    raise Exception.Create(string(Info.Diagnostic));
end;

Riadok MaxPixels tam je z dôvodu. Options DLL sa predvolene berú na 16 777 216 pixelov na požiadavku, čo pokrýva A4 aj US Letter pri 300 DPI s rezervou, ale strana A3 pri 300 DPI je zhruba 3508 krát 4961 pixelov, asi 17,4 milióna, a požiadavka sa odmietne ako nad rozpočet. Zvýšte MaxPixels (strop je 67 108 864) alebo znížte THPDFOCRTextLayerOptions.DPI pre veľké formáty. Usporiadanie sprava doľava používa voliteľný export HPDFRapidOCRSetReadingDirection verzie ABI 1; adaptér ho vyžaduje len keď je nastavené RightToLeft, takže staršie DLL naďalej obsluhuje jazyky zľava doprava a zlyhá pri vytvorení enginu s EArgumentException menujúcou chýbajúci export pre arabčinu

Prečo sa novšie OCR modely nevedia načítať?

DLL RapidOCR v HotPDF linkuje statický ONNX Runtime 1.14, ktorý nedokáže čítať modely uložené s ONNX IR verziou 10 a novšie exporty ako modely PP-OCRv5 môžu vyžadovať novší runtime; takýto model zlyhá pri vytvorení enginu s natívnou diagnostikou. To obmedzenie je dôvod, prečo sú jazykové balíčky pripnuté na konkrétne páry recognizer a slovník PP-OCRv3 a PP-OCRv4 namiesto „najnovšieho“ a prečo tabuľka vyššie mieša obe generácie: každý pripnutý pár je taký, čo sa pod tým runtime načíta a overí

Inštalátor pairing vynucuje. Každý súbor v jeho manifeste nesie heš SHA256, existujúci súbor s iným hešom zastaví inštaláciu namiesto prepísania a každé stiahnutie pristane pod dočasným menom a na miesto sa presunie až potom, čo heš sedí. To chráni proti tichej verzii problému so slovníkom: niekto ručne vhodí novšie recognition.onnx do priečinka profilu, počet tried náhodou sedí a nič nezlyhá, kým zákazník nenahlási, že hľadanie nenachádza slová, ktoré zjavne vidí. Počas behu ostanú adaptér offline a nikdy nestiahne chýbajúci model. Recognizer navyše overuje tvar modelu pri načítaní, prijíma NCHW vstup s fixnou výškou 32 alebo 48 pixelov alebo dynamickou výškou, ktorú beží pri 48

Ak potrebujete písmo, ktoré žiadny z deviatich profilov nepokrýva, stále môžete namieriť RecognitionModel a CharacterDictionary na vlastné súbory. Platia tie isté kontroly, v tom je zmysel: nezhodný pár zlyhá pri inicializácii, nie v archíve vášho zákazníka. Pre strany, kde nesedí žiadny profil RapidOCR, adaptér Tesseract pre prehľadávateľné PDF sa zapojí do rovnakého volania ApplyLoadedOCRTextLayer a pre strojovo tlačené ASCII formuláre vstavaný OCR engine s template matchingom nepotrebuje žiadne modely

Rýchla referencia: multijazyčný checklist RapidOCR

  • Vytvárajte options cez THPDFRapidOCRDLLOptions.ForLanguage a berte EArgumentException ako nepodporovanú značku, nie runtime poruchu
  • Meniť RecognitionModel a CharacterDictionary spolu, nikdy jedno samotné; rovnaké počty tried nedokazujú rovnaké poradie znakov
  • Držte slovníky ako UTF-8 bez BOM, nikdy nestrihajte položky a očakávajte, že model má N + 2 tried: blank, N položiek, medzera
  • Vlastný CTC dekodér musí pokryť poslednú triedu aj posledný časový krok a držať opakovania oddelené blankom
  • Používajte jeden engine na jazykový profil a podávajte explicitné zoznamy strán pre dokumenty miešajúce písma
  • RightToLeft mení len poradie boxov; rozpoznaný text ostanú v Unicode logickom poradí
  • Inštalujte modely cez Install-RapidOCRModels.ps1, aby piny SHA256 držali pairing modelu a slovníka; nastavte UseAngleClassifier := False, ak ste inštalovali s -SkipClassifier
  • Zvýšte MaxPixels nad predvolené 16 777 216 skôr, než pustíte strany A3 a väčšie pri 300 DPI

Jazykové presety RapidOCR, natívny DLL adaptér aj pipeline OCR textovej vrstvy sú súčasťou HotPDF Delphi PDF Component pre Delphi, C++Builder a Windows FPC/Lazarus, od v2.775.0 pre multijazyčné profily