Technisch artikel

HotPDF Chinese en meertalige OCR met RapidOCR in Delphi

HotPDF doet Chinese en meertalige OCR in Delphi via zijn native RapidOCR-DLL-adapter: THPDFRapidOCRDLLOptions.ForLanguage mapt een taaltag zoals 'zh-CN', 'zh-TW', 'ru' of 'ar' op een matchend recognitiemodel en karakterwoordenboek, en THotPDF.ApplyLoadedOCRTextLayer maakt van de herkende regels een onzichtbare, doorzoekbare Unicode-tekstlaag op gescande PDF-pagina's

Een demo in het Latijnse schrift aan de praat krijgen is het makkelijke deel. De interessante falingen beginnen zodra u omschakelt naar Traditioneel Chinees of Russisch en de uitvoer verandert in zelfverzekerd, netjes gevormde onzin, of zodra elke regel stilletjes zijn laatste karakter verliest, of zodra een Arabische pagina terugkomt met zijn tekstboxen in de verkeerde volgorde. Geen daarvan geeft uit zichzelf een exception. De taalpresets toegevoegd in HotPDF v2.775.0 bestaan vooral om die gaten te dichten, en de vier valkuilen hieronder zijn het begrijpen waard ook als u de native code nooit aanraakt, want elk verklaart een symptoom waar u anders een dag achteraan kunt jagen

Hoe kiest ForLanguage een model en woordenboek?

THPDFRapidOCRDLLOptions.ForLanguage resolvet een tag naar een van negen profielen en geeft opties terug die wijzen naar <profile>/recognition.onnx en <profile>/dictionary.txt onder uw modelmap, terwijl de gedeelde detector, de optionele hoekclassificator en de thread-, pixel- en timeout-defaults uit THPDFRapidOCRDLLOptions.Default blijven staan. De methode zet de tag om naar kleine letters, verandert underscores in koppeltekens en knipt witruimte aan de randen af, dus 'zh_TW', 'ZH-tw' en ' zh-tw ' komen allemaal op hetzelfde profiel uit. Aliassen zijn een expliciete lijst in plaats van een prefixmatch: 'zh-Hant-TW' wordt geaccepteerd omdat hij op de lijst staat, terwijl een willekeurige regionale variant die er niet op staat een EArgumentException geeft voordat er een model wordt geladen

HotPDF ForLanguage-profielresolutie voor THPDFRapidOCRDLLOptions: tags zoals zh_TW, ZH-tw en zh-TW worden genormaliseerd en gematcht tegen negen opgesomde profielen, die elk een recognitiemodel en woordenboek vastpinnen die altijd samen worden gezet, terwijl een niet-opgesomde tag EArgumentException geeft voordat er een model laadt
één tag selecteert één vastgepind model-woordenboek-paar; de detector, classifier en budgets blijven gedeeld, en een onbekende tag faalt snel in plaats van iets te laden
ProfielTalenVoorbeeldtagsVastgepind model
chVereenvoudigd Chinees en Engelszh, zh-CN, zh-Hans, chi_simPP-OCRv4
chinese_chtTraditioneel Chineeszh-TW, zh-HK, zh-Hant, chi_traPP-OCRv3
enEngelsen, en-US, en-GB, engPP-OCRv4
latinFrans, Duits, Spaans, Portugees, Italiaans, Nederlands, Turksfr, de, es-419, pt-BR, trPP-OCRv3
japanJapansja, ja-JP, jpnPP-OCRv4
koreanKoreaansko, ko-KR, korPP-OCRv4
cyrillicRussisch, Oekraïens, Bulgaars, Wit-Russischru, ru-RU, uk, bgPP-OCRv3
arabicArabisch, Perzisch, Urduar, ar-SA, fa, urPP-OCRv4
devanagariHindi, Marathi, Nepaleeshi, mr, nePP-OCRv4

De adapter zelf downloadt nooit iets. U voorziet de bestanden één keer met de meegeleverde helper, bijvoorbeeld tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (of -Language All voor alle negen profielen), en de helper legt een gedeelde detector en classifier neer op de root-bestandsnamen die Default verwacht. Daarna wordt een scan in Vereenvoudigd Chinees doorzoekbaar met een paar regels code. De engine-bedrading is dezelfde IHPDFOCREngine-naad die wordt beschreven in het artikel over de in-process RapidOCR-DLL en zijn ABI-grens, dus dit artikel blijft bij de talen

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, gedeelde detector en classifier
  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
    // een lege paginalijst betekent elke pagina; pagina's die al tekst hebben worden overgeslagen
    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;

Twee details in die uitvoer verdienen een notitie. De native pijplijn geeft één resultaat per gedetecteerde tekstregel, niet per woord, dus AcceptedWordCount telt hier regels, en MinimumConfidence wordt vergeleken met de gemiddelde karakterconfidence van de hele regel: een regel met gemiddeld 0.45 wordt als geheel laten vallen. UniqueScalarCount meldt hoeveel verschillende Unicode-scalars de tekstlaag in zijn font en ToUnicode-tabel moest mappen, een nuttige sanity check dat CJK-tekst er echt aankwam in plaats van een handjevol Latijnse fallbacks. Houd de engine-interface in leven over documenten heen, want modelinitialisatie gebeurt in de factory en is de dure stap

Waarom levert alleen het recognitiemodel wisselen rommel op?

Een CTC-recognitiemodel geeft nooit karakters, alleen klasse-indexen, en het woordenboek is het enige dat index 1.204 in een glyph verandert. Wissel ch/recognition.onnx om voor cyrillic/recognition.onnx maar houd het Chinese woordenboek aan, en het model zal vrolijk geldige Cyrilische indexen uitstoten die het oude woordenboek vertaalt in willekeurige Han-karakters. Het resultaat oogt als tekst, haalt UTF-8-validatie en is doorzoekbaar voor precies niets. Daarom zet ForLanguage RecognitionModel en CharacterDictionary altijd samen, en waarom handgebouwde opties er nooit één zonder de ander mogen veranderen

De voor de hand liggende veiligheidscontrole, het woordenboekformaat vergelijken met de breedte van de modeluitvoer, is nodig maar niet voldoende. Twee woordenboeken kunnen evenveel entries hebben in een andere volgorde, en een off-by-one in de volgorde schuift elk karakter één codepunt op. HotPDF controleert daarom in twee stappen zodra de factory het model initialiseert. Ten eerste moet het klasseaantal van de uitvoer gelijk zijn aan de woordenboekentries plus twee. Ten tweede, als het ONNX-bestand een character-metadatalijst insluit, wordt elke woordenboekentry er in volgorde mee vergeleken, en een mismatch laat de initialisatie falen met EInvalidOperation en een native diagnostiek in plaats van later geloofwaardige rommel te produceren

De "plus twee" komt uit de klasselayout. Klasse 0 is de CTC blank, klassen 1 tot N zijn de woordenboekregels in bestandsvolgorde, en de laatste klasse is een spatie. Sommige woordenboeken voeren bovendien hun eigen space-entry, en die regel moet exact blijven zoals hij is. Dit is de plek waar een goedbedoelde Trim echte schade aanricht: hij maakt van een entry met één spatie een lege string en verschuift of breekt de tabel. De enige veilige normalisatie is het verwijderen van een afsluitende carriage return, dus een woordenboek opgeslagen met CRLF-regeleinden laadt correct, terwijl een UTF-8 byte order mark, een lege regel of een entry met een tab wordt geweigerd. De schets hieronder toont de layout in Pascal; het is toelichtende code, geen HotPDF-API

HotPDF CTC-klassentabellayout voor RapidOCR-woordenboeken: klasse 0 is de blank, klassen 1 tot N zijn de woordenboekregels in bestandsvolgorde met een eventuele losse space-entry gehandhaafd, en de laatste klasse is een spatie, wat N plus 2 uitvoerklassen oplevert die de factory tegen het model verifieert, metadata inbegrepen
het woordenboek is het enige dat klasse-indexen in karakters verandert, dus zijn formaat, volgorde en space-entry worden geverifieerd voordat er ook maar één pagina wordt herkend
// Alleen ter illustratie: de klassentabel die een CTC-recognizer verwacht
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);                                   // regeleinde aan het eind van het bestand
  SetLength(Result, Last + 3);
  Result[0] := '';                               // klasse 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: alleen de CR laten vallen
    if (Entry = '') or (Pos(#9, Entry) > 0) then
      raise EArgumentException.Create('Invalid dictionary entry');
    Result[I + 1] := Entry;                      // nooit Trim: ' ' is een klasse
  end;
  Result[Last + 2] := ' ';                       // laatste klasse: spatie
  // Length(Result) moet gelijk zijn aan het klasseaantal van de modeluitvoer
end;

Wat doet greedy CTC-decodering eigenlijk?

Greedy CTC-decodering pikt bij elke tijdstap de hoogst scorende klasse, klapt opeenvolgende herhalingen samen tot één karakter en laat de blank-klasse vallen; de blank is wat echt verdubbelde letters laat overleven. Een recognitiemodel bekijkt een tekstregel als een reeks smalle verticale plakjes, en voor elk plakje, of tijdstap, geeft het een kans voor elke klasse. Een regel met AA中 zou de argmax-reeks A A blank A 中 space kunnen opleveren. De eerste twee A-stappen samenvouwen geeft één A, de blank scheidt hem van de volgende A, en het resultaat is AA中 met de afsluitende spatie intact. Zonder de blank-regel zouden book en bok ononderscheidbaar zijn

HotPDF GreedyCTCDecode-walkthrough: zes tijdstappen stemmen op argmax-klassen A, A, blank, A, een Han-karakter en spatie, opeenvolgende herhalingen klappen samen, de blank reset de herhalingsvang zodat een echt verdubbeld karakter overleeft, en drie grensbugs laten stilletjes woordspatiëring, het laatste karakter of verdubbelde karakters vallen
de decoder is een dozijn regels en elke grens telt: neem de laatste klasse mee, neem de laatste stap mee, en laat alleen een blank herhalingen scheiden

Omdat de decoder maar een dozijn regels is, gaan de grenzen makkelijk mis, en de falingen zijn stil. Stopt de binnenste argmax-lus één klasse te vroeg, dan kan de spatieklasse nooit winnen en komt elke regel terug zonder woordspatiëring, wat zinszoeken op Engelse en Latijnse pagina's vernielt. Stopt de buitenste lus één tijdstap te vroeg, dan verdwijnt het laatste karakter van elke regel, wat bij een korte regel een derde van de tekst kan zijn. En wordt de herhalingsvang niet door een blank gereset, dan klappen verdubbelde karakters zoals ll of Chinese reduplicaties zoals 谢谢 samen tot één. De HotPDF-decoder neemt de laatste klasse en de laatste tijdstap mee, houdt door blanks gescheiden herhalingen aan, en weigert bovendien scores die niet eindig zijn of buiten 0 tot 1 vallen, en elk klasseaantal dat niet met het woordenboek matcht. Hier is dezelfde logica als Pascal-illustratie

// Alleen ter illustratie: greedy CTC-decodering met correcte grenzen.
// Scores bevat Steps * Classes kansen, één rij per tijdstap
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;                            // klasse 0 is de CTC blank
  for Step := 0 to Steps - 1 do             // neem de laatste tijdstap mee
  begin
    Best := 0;
    BestScore := Scores[Step * Classes];
    for C := 1 to Classes - 1 do            // neem de laatste klasse mee (spatie)
      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;                       // een blank reset de herhalingsvang
  end;
end;

Greedy decodering is niet de meest accurate CTC-strategie die bestaat; beam search met een taalmodel kan enkele twijfelachtigee plakjes redden. Voor gedrukte documenten op 300 DPI is het greedy-resultaat meestal alles wat het model te bieden heeft, en de decoder is niet de plek om modelzwaktes te compenseren. Het Latijnse PP-OCRv3-model kan bijvoorbeeld ñ als n lezen zelfs op nette invoer. HotPDF plakt dat niet dicht met post-processing karaktervervangingen, want een substitutietabel die het Spaans fixt breekt iets anders, en een fout karakter in een doorzoekbare laag is erger dan een eerlijke miss

Hoe ordent HotPDF tekstregels, inclusief rechts-naar-links Arabisch?

HotPDF sorteert gedetecteerde tekstboxen van boven naar beneden, groepeert boxen in een rij zodra ze verticaal met ten minste de helft van de kleinere boxhoogte overlappen, en ordent elke rij van links naar rechts, of van rechts naar links zodra RightToLeft is ingeschakeld; de karakters binnen elke herkende regel worden nooit omgekeerd. De groepering telt omdat een detector vaak één visuele regel in meerdere boxen splitst, bijvoorbeeld een label en een waarde gescheiden door een brede opening, en een pure sortering op bovencoördinaat zou ze met de naburige regel interleaven zodra hun bovenkanten een pixel of twee verschillen

Het Arabische preset zet RightToLeft := True, wat de DLL vertelt de boxen in elke rij op hun rechterrand te ordenen, van de rechtermarge naar binnen. Dat is het hele effect. De tekst die het model voor een regel teruggeeft staat al in Unicode-logische volgorde, de volgorde waarin een Arabische lezer leest en typt, en dat is ook de volgorde waarin PDF-tekstextractie en zoeken verwachten. De string mechanisch omdraaien zodat hij er in een debugger "goed uitziet" breekt zoeken, kopiëren en plakken en screenreaders. Bidirectionele weergave en glyph-vormgeving zijn het werk van de viewer

Eén engine bedient één taalprofiel. Er is geen automatische schriftdetectie, dus een document dat schriften mengt heeft één engine per profiel nodig, toegepast op de pagina's die hem gebruiken. Omdat ApplyLoadedOCRTextLayer een expliciete paginalijst neemt en elke aanroep als eigen alles-of-niets-transactie wegschrijft, is dat eenvoudig

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
  Models: THPDFRapidOCRDLLOptions;
begin
  // geeft EArgumentException bij een onbekende tag, voordat er een model laadt
  Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
  Models.MaxPixels := 33554432;          // ruimte voor A3-pagina's op 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');  // chinese_cht-profiel
  Arabic := CreateRapidEngine('ar-SA');   // arabic-profiel, 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;

De MaxPixels-regel staat er met reden. De DLL-opties defaulten op 16.777.216 pixels per verzoek, wat A4 en US Letter op 300 DPI ruim dekt, maar een A3-pagina op 300 DPI is zo'n 3508 bij 4961 pixels, ruwweg 17,4 miljoen, en het verzoek wordt geweigerd als over budget. Verhoog MaxPixels (het plafond is 67.108.864) of verlaag THPDFOCRTextLayerOptions.DPI voor grote formaten. Rechts-naar-links-ordenen gebruikt de optionele HPDFRapidOCRSetReadingDirection-export van ABI-versie 1; de adapter eist hem alleen zodra RightToLeft is gezet, dus een oudere DLL blijft links-naar-rechts-talen bedienen en faalt bij het aanmaken van de engine met een EArgumentException die de ontbrekende export noemt voor Arabisch

Waarom laden nieuwere OCR-modellen niet?

De HotPDF RapidOCR-DLL linkt een statische ONNX Runtime 1.14, die modellen opgeslagen met ONNX IR-versie 10 niet kan lezen, en nieuwere exports zoals PP-OCRv5-modellen kunnen een nieuwere runtime dan dat vereisen; zo'n model faalt bij het aanmaken van de engine met een native diagnostiek. Die beperking is de reden dat de taalpakketten vastzitten aan specifieke PP-OCRv3- en PP-OCRv4-recognizer- en woordenboekparen in plaats van "latest", en waarom de tabel hierboven de twee generaties mengt: elk vastgepind paar is er een dat onder die runtime laadt en verifieert

De installer handhaaft het paren. Elk bestand in zijn manifest voert een SHA256-hash, een bestaand bestand met een andere hash stopt de installatie in plaats van overschreven te worden, en elke download landt onder een tijdelijke naam en schuift pas op zijn plek zodra zijn hash matcht. Dat beschermt tegen de stille variant van het woordenboekprobleem: iemand legt handmatig een nieuwere recognition.onnx in een profielmap, het klasseaantal blijkt toevallig te matchen, en niets faalt totdat een klant meldt dat zoeken de woorden niet vindt die hij duidelijk ziet. Op runtime blijft de adapter offline en haalt nooit een ontbrekend model op. De recognizer valideert ook de modelvorm bij het laden, en accepteert NCHW-invoer met een vaste hoogte van 32 of 48 pixels of een dynamische hoogte, die hij op 48 draait

Heeft u een schrift nodig dat geen van de negen profielen dekt, dan kunt u RecognitionModel en CharacterDictionary nog steeds naar eigen bestanden wijzen. Dezelfde controles gelden, en dat is het punt: een verkeerd paar faalt bij initialisatie, niet in het archief van uw klant. Voor pagina's waar geen enkel RapidOCR-profiel past, koppelt de Tesseract-adapter voor doorzoekbare PDF in op dezelfde ApplyLoadedOCRTextLayer-aanroep, en voor machinaal bedrukte ASCII-formulieren heeft de ingebouwde template-matching-OCR-engine helemaal geen modellen nodig

Snelnaslag: meertalige RapidOCR-checklist

  • Maak opties met THPDFRapidOCRDLLOptions.ForLanguage en zie EArgumentException als een niet-ondersteunde tag, niet als een runtimefout
  • Verander RecognitionModel en CharacterDictionary samen, nooit één alleen; gelijke klasseaantallen bewijzen geen gelijke karaktervolgorde
  • Houd woordenboeken als UTF-8 zonder BOM, trim nooit entries, en verwacht dat het model N + 2 klassen heeft: blank, N entries, spatie
  • Een custom CTC-decoder moet de laatste klasse en de laatste tijdstap dekken en herhalingen die door een blank gescheiden worden aan te houden
  • Gebruik één engine per taalprofiel en geef expliciete paginalijsten door voor documenten met gemengde schriften
  • RightToLeft verandert alleen de boxvolgorde; herkende tekst blijft in Unicode-logische volgorde
  • Installeer modellen met Install-RapidOCRModels.ps1 zodat SHA256-pins het model-woordenboek-paar vasthouden; zet UseAngleClassifier := False als u met -SkipClassifier hebt geïnstalleerd
  • Verhoog MaxPixels boven de default van 16.777.216 voordat u A3 of groter op 300 DPI draait

De RapidOCR-taalpresets, de native DLL-adapter en de OCR-tekstlaag-pijplijn maken deel uit van de HotPDF Delphi PDF Component voor Delphi, C++Builder en Windows FPC/Lazarus, te beginnen met v2.775.0 voor de meertalige profielen