Technický článek

HotPDF: čínské a vícejazyčné OCR s RapidOCR v Delphi

HotPDF dělá čínské a vícejazyčné OCR v Delphi přes svůj nativní RapidOCR DLL adapter: THPDFRapidOCRDLLOptions.ForLanguage namapuje jazykovou značku jako 'zh-CN', 'zh-TW', 'ru' nebo 'ar' na odpovídající rozpoznávací model a znakový slovník a THotPDF.ApplyLoadedOCRTextLayer z rozpoznaných řádků udělá neviditelnou, prohledávatelnou Unicode textovou vrstvu na naskenovaných PDF stránkách

Rozchodit demo pro latinku je ta snadná část. Zajímavé selhání začínají, když přepnete na tradiční čínštinu nebo ruštinu a výstup se změní v sebejistý, formálně dokonalý nesmysl, když každý řádek potichu ztratí poslední znak, nebo když arabská stránka přijde s textovými boxy ve špatném pořadí. Ani jedno z toho samo od sebe nevyhodí výjimku. Jazykové presety přidané v HotPDF v2.775.0 existují hlavně proto, aby tyhle mezery zavřely, a čtyři pasti dole stojí za pochopení, i kdybyste se nativního kódu nikdy nedotkli, protože každá z nich vysvětluje příznak, po kterém byste jinak mohli den pátrat

Jak ForLanguage vybírá model a slovník?

THPDFRapidOCRDLLOptions.ForLanguage převede značku na jeden z devíti profilů a vrátí nastavení mířící na <profile>/recognition.onnx a <profile>/dictionary.txt pod vaším adresářem modelů, přičemž ponechá sdílený detektor, volitelný angle classifier a defaulty pro vlákna, pixely i timeout z THPDFRapidOCRDLLOptions.Default. Metoda převede značku na malá písmena, podtržítka změní na spojovníky a ořízne okolní bílé znaky, takže 'zh_TW', 'ZH-tw' i ' zh-tw ' skončí u téhož profilu. Aliasy jsou výčtový seznam, ne shoda prefixu: 'zh-Hant-TW' se přijme, protože je na seznamu, zatímco libovolná neuvedená regionální varianta vyhodí EArgumentException, dřív než se kterýkoli model nahraje

Rozlišení profilů ForLanguage v HotPDF pro THPDFRapidOCRDLLOptions: značky jako zh_TW, ZH-tw a zh-TW se normalizují a porovnávají s devíti uvedenými profily, každý s pevně navázaným rozpoznávacím modelem a slovníkem, které se nastavují vždy společně, zatímco neuvedená značka vyhodí EArgumentException, dřív než se kterýkoli model nahraje
jedna značka vybírá jeden pevně navázaný pár model-slovník; detektor, klasifikátor i rozpočty zůstávají sdílené a neznámá značka failne rychle místo načítání čehokoli
ProfilJazykyUkázkové značkyPevný 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
latinFrancouzština, němčina, španělština, portugalština, italština, nizozemština, turečtinafr, de, es-419, pt-BR, trPP-OCRv3
japanJaponštinaja, ja-JP, jpnPP-OCRv4
koreanKorejštinako, ko-KR, korPP-OCRv4
cyrillicRuština, ukrajinština, bulharština, běloruštinaru, ru-RU, uk, bgPP-OCRv3
arabicArabština, perština, urduar, ar-SA, fa, urPP-OCRv4
devanagariHindština, maráthština, nepálštinahi, mr, nePP-OCRv4

Adapter sám o sobě nic nestahuje. Soubory jednou připravíte přibaleným helperem, například tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (nebo -Language All pro všech devět profilů), a helper umístí sdílený detektor a klasifikátor do kořenových názvů souborů, které očekává Default. Potom je sken ve zjednodušené čínštině prohledávatelný za pár řádků. Engine se zapojuje přes totéž rozhraní IHPDFOCREngine, které popisuje článek o in-process RapidOCR DLL a její hranici ABI, takže tenhle zůstává zaměřený na jazyky

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, sdílený 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ázdný seznam stránek znamená všechny stránky; stránky, které už text mají, se přeskakují
    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 tomhle výstupu stojí za poznámku. Nativní pipeline vrací jeden výsledek na detekovaný textový řádek, ne na slovo, takže AcceptedWordCount počítá tady řádky a MinimumConfidence se porovnává s průměrnou confidence znaků celého řádku: řádek s průměrem 0,45 se zahodí jako celek. UniqueScalarCount hlásí, kolik různých Unicode skalárů musela textová vrstva namapovat do svého fontu a tabulky ToUnicode, což je užitečná kontrola, že CJK text skutečně dorazil a ne jen hrstka latinských fallbacků. Interface engine udržujte naživu napříč dokumenty, protože inicializace modelů probíhá v továrně a je to ta nejdražší část

Proč samotná výměna rozpoznávacího modelu produkuje nesmysly?

CTC rozpoznávací model nikdy nevypisuje znaky, jen indexy tříd, a slovník je jediná věc, která z indexu 1 204 udělá glyf. Vyměňte ch/recognition.onnx za cyrillic/recognition.onnx, ale ponechte čínský slovník, a model s nadšením vysílá validní cyrilské indexy, které starý slovník překládá na náhodné znaky Han. Výsledek vypadá jako text, projde validací UTF-8 a vyhledávání v něm nenajde vůbec nic. Proto ForLanguage vždy nastavuje RecognitionModel a CharacterDictionary společně a ručně poskládané nastavení by nikdy nemělo měnit jedno bez druhého

Zjevná bezpečnostní kontrola, srovnání velikosti slovníku se šířkou výstupu modelu, je nutná, ale nestačí. Dva slovníky můžou mít stejný počet položek v jiném pořadí a off-by-one v pořadí posune každý znak o jeden code point. HotPDF proto kontroluje při inicializaci modelu v továrně ve dvou krocích. Za prvé, počet výstupních tříd se musí rovnat položkám slovníku plus dva. Za druhé, když má ONNX soubor v metadatech seznam character, porovná se s ním každá položka slovníku po pořadě a neshoda ukončí inicializaci s EInvalidOperation a nativní diagnostikou místo toho, aby později produkovala věrohodný nesmysl

„Plus dva" pochází z rozložení tříd. Třída 0 je CTC blank, třídy 1 až N jsou řádky slovníku v pořadí podle souboru a poslední třída je mezera. Některé slovníky nesou i vlastní položku mezery a tenhle řádek musí zůstat přesně takový, jaký je. Tady napáchá dobromyslný Trim skutečné škody: z položky s jedinou mezerou udělá prázdný řetězec a posune nebo rozbije tabulku. Jediná bezpečná normalizace je odstranění koncového carriage return, takže slovník uložený s konci řádků CRLF se nahraje správně, zatímco UTF-8 byte order mark, prázdný řádek nebo položka obsahující tabulátor se odmítnou. Náčrt dole ukazuje rozložení v Pascalu; je to vysvětlující kód, ne API HotPDF

Rozložení tabulky tříd CTC v HotPDF pro slovníky RapidOCR: třída 0 je blank, třídy 1 až N jsou řádky slovníku v pořadí podle souboru s ponechanou samostatnou položkou mezery a poslední třída je mezera, což dává N plus 2 výstupních tříd, které továrna ověří proti modelu, metadata včetně
slovník je jediná věc, která převádí indexy tříd na znaky, takže jeho velikost, pořadí i položka mezery se ověří, dřív než se rozpozná jediná stránka
// Jen ilustrace: tabulka tříd, kterou CTC recognizer očekává
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);                                   // konec řádku na konci souboru
  SetLength(Result, Last + 3);
  Result[0] := '';                               // třída 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: ubere se jen CR
    if (Entry = '') or (Pos(#9, Entry) > 0) then
      raise EArgumentException.Create('Invalid dictionary entry');
    Result[I + 1] := Entry;                      // nikdy Trim: ' ' je třída
  end;
  Result[Last + 2] := ' ';                       // poslední třída: mezera
  // Length(Result) se musí rovnat počtu výstupních tříd modelu
end;

Co vlastně dělá greedy CTC dekódování?

Greedy CTC dekódování v každém časovém kroku vybere třídu s nejvyšším skóre, sloučí po sobě jdoucí opakování do jednoho znaku a blank třídu zahodí; právě blank totiž umožňuje, aby opravdu zdvojené písmeno přežilo. Rozpoznávací model se na textový řádek dívá jako na sekvenci úzkých svislých řezů a pro každý řez, tedy časový krok, vypíše pravděpodobnost pro každou třídu. Řádek obsahující AA中 může produkovat argmax sekvenci A A blank A 中 space. Sloučení prvních dvou kroků A dá jedno A, blank ho oddělí od dalšího A a výsledkem je AA中 s koncovou mezerou zachovanou. Bez pravidla blanku by book a bok byly nerozlišitelné

Průchod GreedyCTCDecode v HotPDF: šest časových kroků hlasuje argmax třídy A, A, blank, A, znak Han a mezera, po sobě jdoucí opakování se sloučí, blank resetuje hlídač opakování, takže opravdu zdvojené písmeno přežije, a tři hraniční bugy potichu ztratí mezery mezi slovy, poslední znak nebo zdvojené znaky
dekodér je tucet řádků a na každé hranici záleží: zahrňte poslední třídu, zahrňte poslední krok a opakování ať odděluje jen blank

Když má dekodér jen tucet řádků, snadno se pokazí hranice a selhání jsou tichá. Skončí-li vnitřní argmax smyčka o třídu dřív, třída mezera nemůže nikdy vyhrát a každý řádek se vrátí bez mezer mezi slovy, což rozbije vyhledávání frází na anglických a latinských stránkách. Skončí-li vnější smyčka o časový krok dřív, zmizí poslední znak každého řádku, což u krátkého řádku může být třetina textu. A pokud hlídač opakování nerestuje blank, sloučí se zdvojené znaky jako ll nebo čínské reduplikace jako 谢谢 do jednoho. Dekodér HotPDF zahrnuje poslední třídu i poslední časový krok, zachovává opakování oddělená blankem a navíc odmítá skóre, které není konečné nebo leží mimo rozsah 0 až 1, a jakýkoli počet tříd, který neodpovídá slovníku. Tady je tatáž logika jako ilustrace v Pascalu

// Jen ilustrace: greedy CTC dekódování se správnými hranicemi.
// Scores drží Steps * Classes pravděpodobností, jeden řádek 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;                            // třída 0 je CTC blank
  for Step := 0 to Steps - 1 do             // zahrň i poslední časový krok
  begin
    Best := 0;
    BestScore := Scores[Step * Classes];
    for C := 1 to Classes - 1 do            // zahrň i poslední třídu (mezera)
      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 resetuje hlídač opakování
  end;
end;

Greedy dekódování není nejpřesnější dostupná CTC strategie; beam search s jazykovým modelem zachrání pár nejednoznačných řezů. U tištěných dokumentů v 300 DPI je greedy výsledek obvykle to nejlepší, co model nabízí, a dekodér není místo, kde by se kompenzovaly slabosti modelu. Latinský model PP-OCRv3 umí třeba přečíst ñ jako n, i na čistém vstupu. HotPDF to nepřetahuje post-processingem s náhradami znaků, protože substituční tabulka, která spraví španělštinu, rozbije něco jiného a špatný znak v prohledávatelné vrstvě je horší než čestné selhání

Jak HotPDF řadí textové řádky, včetně arabského zápisu zprava doleva?

HotPDF řadí detekované textové boxy shora dolů, sdružuje boxy do řádku, když se svisle překrývají alespoň o polovinu výšky menšího boxu, a každý řádek seřadí zleva doprava, případně zprava doleva, když je zapnuté RightToLeft; znaky uvnitř každého rozpoznaného řádku se nikdy neobracejí. Na sdružování záleží, protože detektor často rozdělí jeden vizuální řádek do několika boxů, třeba jmenovku a hodnotu oddělené širokou mezerou, a čisté řazení podle horní souřadnice by je propletlo se sousedním řádkem pokaždé, když se jejich horní hrany liší o pixel či dva

Arabský preset nastaví RightToLeft := True, čímž řekne DLL, aby boxy v každém řádku srovnala podle pravé hrany, od pravého okraje dovnitř. To je celý efekt. Text, který model pro řádek vrátí, už je v logickém pořadí Unicode, v pořadí, v jakém ho arabský čtenář čte a píše, a přesně toto pořadí očekává extrakce textu z PDF i hledání. Mechanické obrácení řetězce, aby v debuggeru „vypadal správně", rozbije hledání, kopírování, vkládání i screen readery. Obousměrné zobrazení a tvarování glyfů je práce vieweru

Jeden engine obsluhuje jeden jazykový profil. Automatická detekce písma neexistuje, takže dokument míchající skripty potřebuje jeden engine na profil, aplikovaný na stránky, které ho používají. Protože ApplyLoadedOCRTextLayer bere explicitní seznam stránek a každé volání commituje jako vlastní all-or-nothing transakci, je to přímočaré

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
  Models: THPDFRapidOCRDLLOptions;
begin
  // vyhodí EArgumentException pro neznámou značku, dřív než se cokoli nahraje
  Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
  Models.MaxPixels := 33554432;          // prostor pro A3 stránky v 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;

Řádek MaxPixels tam není náhodou. Nastavení DLL defaultuje na 16 777 216 pixelů na požadavek, což A4 i US Letter v 300 DPI pokryje s rezervou, ale A3 stránka v 300 DPI má zhruba 3 508 × 4 961 pixelů, kolem 17,4 milionu, a požadavek se odmítne jako přes rozpočet. Zvyšte MaxPixels (strop je 67 108 864) nebo pro velké formáty snižte THPDFOCRTextLayerOptions.DPI. Řazení zprava doleva používá volitelný export HPDFRapidOCRSetReadingDirection z ABI verze 1; adapter ho vyžaduje jen tehdy, když je nastavené RightToLeft, takže starší DLL dál obsluhuje jazyky zleva doprava a u vytváření engine selže s EArgumentException, která jmenuje chybějící export pro arabštinu

Proč se novější OCR modely nedaří nahrát?

HotPDF RapidOCR DLL linkuje statický ONNX Runtime 1.14, který neumí číst modely uložené s ONNX IR verzí 10, a novější exporty jako modely PP-OCRv5 můžou vyžadovat novější runtime; takový model selže už při vytváření engine s nativní diagnostikou. Právě tohle omezení je důvod, proč jsou jazykové balíčky přichycené ke konkrétním párům recognizer a slovník PP-OCRv3 a PP-OCRv4 místo „latest", a proč tabulka výše míchá obě generace: každý přichycený pár je takový, který se pod tímto runtimem nahraje a ověří

Instalátor pár vynucuje. Každý soubor v manifestu má SHA256 hash, existující soubor s jiným hashem instalaci zastaví místo aby se přepsal a každé stažení se nejdřív uloží pod dočasným názvem a na své místo se přesune, až hash sedne. Tím se chrání před tichou verzí problému se slovníkem: někdo ručně odloží novější recognition.onnx do složky profilu, počet tříd náhodou sedí a nic neselže, dokud zákazník nenahlásí, že hledání nenajde slova, která vidí na vlastní oči. Za běhu zůstává adapter offline a chybějící model nikdy nestáhne. Recognizer také validuje tvar modelu při nahrání, přijímá NCHW vstup s pevnou výškou 32 nebo 48 pixelů nebo dynamickou výškou, kterou provozuje na 48

Pokud potřebujete písmo, které žádný z devíti profilů nepokrývá, můžete RecognitionModel a CharacterDictionary i tak nasměrovat na vlastní soubory. Platí tytéž kontroly, a o to jde: nespárovaný pár selže při inicializaci, ne v archivu vašeho zákazníka. Pro stránky, kde se nehodí žádný profil RapidOCR, se do téhož volání ApplyLoadedOCRTextLayer zapojí Tesseract adapter pro prohledávatelné PDF a pro strojově tištěné ASCII formuláře vystačí vestavěný template-matching OCR engine, který nepotřebuje žádné modely

Rychlá reference: checklist vícejazyčného RapidOCR

  • Vytvářejte nastavení přes THPDFRapidOCRDLLOptions.ForLanguage a berte EArgumentException jako nepodporovanou značku, ne jako runtime chybu
  • Měňte RecognitionModel a CharacterDictionary společně, nikdy jedno bez druhého; stejné počty tříd nedokazují stejné pořadí znaků
  • Slovníky uchovávejte jako UTF-8 bez BOM, položky nikdy neorezávejte a počítejte s tím, že model má N + 2 třídy: blank, N položek, mezera
  • Vlastní CTC dekodér musí pokrýt poslední třídu i poslední časový krok a nechávat opakování oddělená blankem
  • Používejte jeden engine na jazykový profil a pro dokumenty míchající skripty předávejte explicitní seznamy stránek
  • RightToLeft mění jen pořadí boxů; rozpoznaný text zůstává v logickém pořadí Unicode
  • Instalujte modely přes Install-RapidOCRModels.ps1, aby SHA256 piny držely pár model-slovník; nastavte UseAngleClassifier := False, pokud jste instalovali s -SkipClassifier
  • Zvyšte MaxPixels nad defaultních 16 777 216, než spustíte A3 nebo větší stránky v 300 DPI

Jazykové presety RapidOCR, nativní DLL adapter i pipeline OCR textové vrstvy jsou součástí HotPDF Delphi PDF Component pro Delphi, C++Builder a Windows FPC/Lazarus, vícejazyčné profily od v2.775.0