Teknisk artikel

HotPDF kinesisk og flersproget OCR med RapidOCR i Delphi

HotPDF udfører kinesisk og flersproget OCR i Delphi gennem sin native RapidOCR-DLL-adapter: THPDFRapidOCRDLLOptions.ForLanguage mapper et sprogtag som 'zh-CN', 'zh-TW', 'ru' eller 'ar' til en matchende recognition-model og tegndictionary, og THotPDF.ApplyLoadedOCRTextLayer omdanner de genkendte linjer til et usynligt, søgbart Unicode-tekstlag på scannede PDF-sider

At få en latin-script-demo til at virke er den lette del. De interessante fejl starter, når du skifter til Traditionelt kinesisk eller russisk, og outputtet bliver til selvsikker, velformet volapyk, eller når hver linje stille mister sit sidste tegn, eller når en arabisk side kommer tilbage med sine tekstboxes i forkert rækkefølge. Ingen af delene rejser en exception af sig selv. Sprogprofilerne, der kom i HotPDF v2.775.0, findes mest for at lukke de huller, og de fire fælder nedenfor er værd at forstå, selv hvis du aldrig rører den native kode, for hver enkelt forklarer et symptom, du ellers kunne bruge en dag på at jage

Hvordan vælger ForLanguage en model og en dictionary?

THPDFRapidOCRDLLOptions.ForLanguage opløser et tag til én af ni profiler og returnerer options, der peger på <profile>/recognition.onnx og <profile>/dictionary.txt under din model-mappe, mens den beholder den delte detector, den valgfrie angle classifier og tråd-, pixel- og timeout-defaults fra THPDFRapidOCRDLLOptions.Default. Metoden lowercaser tagget, omdanner underscores til bindestreger og trimmer omgivende whitespace, så 'zh_TW', 'ZH-tw' og ' zh-tw ' alle lander på samme profil. Aliaser er en eksplicit liste frem for et prefix-match: 'zh-Hant-TW' accepteres, fordi den er listet, mens en vilkårlig regional variant, der ikke er listet, rejser EArgumentException, før nogen model loades

HotPDF'ens ForLanguage-profilopløsning for THPDFRapidOCRDLLOptions: tags som zh_TW, ZH-tw og zh-TW normaliseres og matches mod ni listede profiler, der hver fastlåser en recognition-model og en dictionary, som altid sættes sammen, mens et ikke-listet tag rejser EArgumentException, før nogen model loader
ét tag vælger ét fastlåst model-dictionary-par; detector, classifier og budgetter forbliver delte, og et ukendt tag fejler hurtigt i stedet for at loade noget
ProfilSprogEksempel-tagsFastlåst model
chForenklet kinesisk og engelskzh, zh-CN, zh-Hans, chi_simPP-OCRv4
chinese_chtTraditionelt kinesiskzh-TW, zh-HK, zh-Hant, chi_traPP-OCRv3
enEngelsken, en-US, en-GB, engPP-OCRv4
latinFransk, tysk, spansk, portugisisk, italiensk, hollandsk, tyrkiskfr, de, es-419, pt-BR, trPP-OCRv3
japanJapanskja, ja-JP, jpnPP-OCRv4
koreanKoreanskko, ko-KR, korPP-OCRv4
cyrillicRussisk, ukrainsk, bulgarsk, hviderussiskru, ru-RU, uk, bgPP-OCRv3
arabicArabisk, persisk, urduar, ar-SA, fa, urPP-OCRv4
devanagariHindi, marathi, nepalesiskhi, mr, nePP-OCRv4

Adapteren selv downloader aldrig noget. Du provisionerer filerne én gang med det medfølgende hjælperscript, for eksempel tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (eller -Language All for alle ni profiler), og hjælperen placerer en delt detector og classifier ved de rodfilnavne, Default forventer. Derefter bliver en forenklet kinesisk scanning søgbar med få linjer. Engine-sømmen er samme IHPDFOCREngine, som er beskrevet i artiklen om in-process RapidOCR-DLL'en og dens ABI-grænseflade, så denne forbliver fokuseret på sprog

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, delt detector og 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
    // en tom sideliste betyder alle sider; sider med tekst i forvejen skippes
    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;

To detaljer i det output fortjener en note. Den native pipeline returnerer ét resultat pr. detekteret tekstlinje, ikke pr. ord, så AcceptedWordCount tæller linjer her, og MinimumConfidence sammenlignes med den gennemsnitlige tegn-confidence for hele linjen: en linje, der i gennemsnit ligger på 0.45, droppes som én enhed. UniqueScalarCount melder, hvor mange distinkte Unicode-scalarer tekstlaget måtte mappe ind i sin font og ToUnicode-tabel, et nyttigt sanity-tjek på, at CJK-tekst faktisk ankom i stedet for en håndfuld latin-fallbacks. Hold engine-interfacet i live på tværs af dokumenter, for model-initialisering sker i factoryen og er det dyre trin

Hvorfor giver det volapyk kun at skifte recognition-modellen?

En CTC-recognition-model udsender aldrig tegn, kun klasseindeks, og dictionary'en er det eneste, der omsætter indeks 1.204 til et glyph. Byt ch/recognition.onnx ud med cyrillic/recognition.onnx, men behold den kinesiske dictionary, og modellen vil gladelig udsende gyldige kyrilliske indeks, som den gamle dictionary oversætter til tilfældige Han-tegn. Resultatet ligner tekst, består UTF-8-validering og er søgbart for præcis ingenting. Det er derfor, ForLanguage altid sætter RecognitionModel og CharacterDictionary sammen, og hvorfor håndbyggede options aldrig må ændre den ene uden den anden

Det oplagte sikkerhedstjek, at sammenligne dictionary-størrelsen med model-outputtets bredde, er nødvendigt men ikke tilstrækkeligt. To dictionaries kan have samme antal poster i en anden rækkefølge, og en off-by-one i rækkefølgen flytter hvert tegn ét kodepunkt. HotPDF tjekker derfor i to trin, når factoryen initialiserer modellen. Først skal output-klasseantallet svare til dictionary-posterne plus to. Dernæst, når ONNX-filen indlejrer en character-metadataliste, sammenlignes hver dictionary-post med den i rækkefølge, og et mismatch fejler initialiseringen med EInvalidOperation og en native diagnostik i stedet for senere at producere plausibelt volapyk

"Plus to" kommer fra klasse-layoutet. Klasse 0 er CTC-blanken, klasserne 1 til N er dictionary-linjerne i filrækkefølge, og den sidste klasse er et mellemrum. Nogle dictionaries bærer også deres egen mellemrums-post, og den linje skal bevares præcis som den er. Det er dér, en velmenende Trim gør rigtig skade: den omdanner en mellemrums-post til en tom streng og flytter eller knækker tabellen. Den eneste normalisering, der er sikker, er at fjerne et afsluttende carriage return, så en dictionary gemt med CRLF-linjeslutninger loader korrekt, mens en UTF-8 byte order mark, en tom linje eller en post, der indeholder et tab, afvises. Skitsen nedenfor viser layoutet i Pascal; det er forklarende kode, ikke et HotPDF-API

HotPDF'ens CTC-klassestabel-layout til RapidOCR-dictionaries: klasse 0 er blanken, klasserne 1 til N er dictionary-linjerne i filrækkefølge med enhver enlig mellemrums-post bevaret, og sidste klasse er et mellemrum, hvilket giver N plus 2 output-klasser, som factoryen verificerer mod modellen, metadata inkluderet
dictionary'en er det eneste, der omsætter klasseindeks til tegn, så dens størrelse, rækkefølge og mellemrums-post verificeres, før en eneste side genkendes
// Kun illustration: den klasstabel, en CTC-recognizer forventer
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);                                   // linjeskift ved filens slutning
  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: drop kun CR'en
    if (Entry = '') or (Pos(#9, Entry) > 0) then
      raise EArgumentException.Create('Invalid dictionary entry');
    Result[I + 1] := Entry;                      // aldrig Trim: ' ' er en klasse
  end;
  Result[Last + 2] := ' ';                       // sidste klasse: mellemrum
  // Length(Result) skal svare til model-outputtets klasseantal
end;

Hvad gør greedy CTC-dekodning egentlig?

Greedy CTC-dekodning vælger den højest scorende klasse ved hvert time step, kollapser sammenhængende gentagelser til ét tegn og dropper blank-klassen; blanken er dét, der gør det muligt for ægte fordoblede bogstaver at overleve. En recognition-model kigger på en tekstlinje som en sekvens af smalle lodrette skiver, og for hver skive, hvert time step, udsender den en sandsynlighed for hver klasse. En linje med AA中 kan producere argmax-sekvensen A A blank A 中 space. At kollapse de to første A-steps giver ét A, blanken adskiller det fra næste A, og resultatet er AA中 med det afsluttende mellemrum intakt. Uden blank-reglen ville book og bok være uadskillelige

HotPDF'ens GreedyCTCDecode-gennemgang: seks time steps stemmer argmax-klasserne A, A, blank, A, et Han-tegn og mellemrum, sammenhængende gentagelser kollapser, blanken nulstiller gentagelsesvagten, så et ægte fordoblet bogstav overlever, og tre grænse-bugs dropper stille ordmellemrum, sidste tegn eller fordoblede tegn
dekoderen er et dusin linjer, og hver grænse betyder noget: medtag sidste klasse, medtag sidste step, og lad kun en blank adskille gentagelser

Eftersom dekoderen kun er et dusin linjer, er det let at ramme grænserne forkert, og fejlene er stille. Stopper det indre argmax-loop én klasse for tidligt, kan mellemrumsklassen aldrig vinde, og hver linje kommer tilbage uden ordmellemrum, hvilket ødelægger sætningssøgning på engelske og latinske sider. Stopper det ydre loop ét time step for tidligt, forsvinder sidste tegn i hver linje, hvilket for en kort linje kan være en tredjedel af teksten. Og nulstilles gentagelsesvagten ikke af en blank, kollapser fordoblede tegn som ll eller kinesiske reduplikationer som 谢谢 til ét. HotPDF's dekoder medtager sidste klasse og sidste time step, beholder blank-adskilte gentagelser og afviser desuden scores, der ikke er endelige eller falder uden for 0 til 1, og ethvert klasseantal, der ikke matcher dictionary'en. Her er samme logik som en Pascal-illustration

// Kun illustration: greedy CTC-dekodning med korrekte grænser.
// Scores holder Steps * Classes sandsynligheder, én række pr. time step
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 er CTC-blanken
  for Step := 0 to Steps - 1 do             // medtag sidste time step
  begin
    Best := 0;
    BestScore := Scores[Step * Classes];
    for C := 1 to Classes - 1 do            // medtag sidste klasse (mellemrum)
      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;                       // en blank nulstiller repeat-vagten
  end;
end;

Greedy-dekodning er ikke den mest præcise CTC-strategi, der findes; beam search med en sprogmodel kan redde nogle tvetydige skiver. For printede dokumenter ved 300 DPI er greedy-resultatet normalt det, modellen har at byde på, og dekoderen er ikke stedet at kompensere for modelsvagheder. Den latinske PP-OCRv3-model kan for eksempel læse ñ som n selv på rent input. HotPDF papirer det ikke over med post-processing-tegnerstatninger, for en substitutionstabel, der fikser spansk, knækker noget andet, og et forkert tegn i et søgbart lag er værre end et ærligt miss

Hvordan sorterer HotPDF tekstlinjer, inklusive højre-til-venstre arabisk?

HotPDF sorterer detekterede tekstboxes fra top til bund, grupperer boxes i en række, når de overlapper lodret med mindst halvdelen af den mindste box-højde, og sorterer hver række venstre mod højre, eller højre mod venstre, når RightToLeft er slået til; tegnene inde i hver genkendt linje vendes aldrig. Grupperingen betyder noget, for en detector deler ofte én visuel linje i flere boxes, for eksempel en label og en værdi adskilt af et bredt mellemrum, og en ren top-koordinat-sortering ville flette dem ind med nabolinjen, hver gang deres tops afviger med en pixel eller to

Den arabiske profil sætter RightToLeft := True, hvilket beder DLL'en om at sortere boxene i hver række efter deres højre kant, fra højre margen og ind. Det er hele effekten. Teksten, modellen returnerer for en linje, er allerede i Unicode logisk rækkefølge, den rækkefølge, en arabisk læser læser og taster den i, og det er også den rækkefølge, PDF-tekst-ekstraktion og søgning forventer. Mekanisk at vende strengen for at få den til at "se rigtig ud" i en debugger ville knække søgning, kopier og indsæt og screen readers. Bidirektional visning og glyph shaping er viewerens job

Én engine serverer én sprogprofil. Der er ingen automatisk script-detektion, så et dokument, der blander scripts, behøver én engine pr. profil, anvendt på de sider, der bruger den. Eftersom ApplyLoadedOCRTextLayer tager en eksplicit sideliste og committer hvert kald som sin egen alt-eller-intet-transaktion, er det ligetil

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
  Models: THPDFRapidOCRDLLOptions;
begin
  // rejser EArgumentException ved ukendt tag, før nogen model loades
  Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
  Models.MaxPixels := 33554432;          // plads til A3-sider ved 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-profilen
  Arabic := CreateRapidEngine('ar-SA');   // arabic-profilen, 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;

MaxPixels-linjen er der af en grund. DLL-options har 16.777.216 pixels pr. request som default, hvilket dækker A4 og US Letter ved 300 DPI fint, men en A3-side ved 300 DPI er omkring 3508 gange 4961 pixels, cirka 17,4 millioner, og requesten afvises som over budget. Hæv MaxPixels (loftet er 67.108.864) eller sænk THPDFOCRTextLayerOptions.DPI for store formater. Højre-til-venstre-sortering bruger den valgfrie HPDFRapidOCRSetReadingDirection-export af ABI-version 1; adapteren kræver den kun, når RightToLeft er sat, så en ældre DLL bliver ved med at servere venstre-til-højre-sprog og fejler ved engine-oprettelse med en EArgumentException, der navngiver den manglende export for arabisk

Hvorfor fejler nyere OCR-modeller at loade?

HotPDF's RapidOCR-DLL linker en statisk ONNX Runtime 1.14, som ikke kan læse modeller gemt med ONNX IR-version 10, og nyere eksporter såsom PP-OCRv5-modeller kan behøve et nyere runtime end det; en sådan model fejler ved engine-oprettelse med en native diagnostik. Den begrænsning er grunden til, at sprogpakkerne er fastlåst til specifikke PP-OCRv3- og PP-OCRv4-recognizer- og dictionary-par i stedet for "seneste", og til, at tabellen ovenfor blander de to generationer: hvert fastlåst par er ét, der loader og verificerer under det runtime

Installeren håndhæver parringen. Hver fil i dens manifest bærer en SHA256-hash, en eksisterende fil med en anden hash stopper installationen i stedet for at blive overskrevet, og hver download lander under et midlertidigt navn og flyttes først på plads, efter dens hash matcher. Det beskytter mod den stille udgave af dictionary-problemet: nogen smider en nyere recognition.onnx i hånden ned i en profilmappe, klasseantallet passer tilfældigvis, og intet fejler, før en kunde melder, at søgningen ikke finder ord, de åbenlyst kan se. Ved runtime forbliver adapteren offline og henter aldrig en manglende model. Recognizeren validerer også modelformen ved load-tid og accepterer NCHW-input med en fast højde på 32 eller 48 pixels eller en dynamisk højde, som den kører ved 48

Behøver du et script, ingen af de ni profiler dækker, kan du stadig pege RecognitionModel og CharacterDictionary på dine egne filer. Samme tjek gælder, og det er pointen: et mismatchet par fejler ved initialisering, ikke i din kundes arkiv. Til sider, hvor ingen RapidOCR-profil passer, kobler Tesseract-adapteren til søgbar PDF sig på samme ApplyLoadedOCRTextLayer-kald, og til maskinprintede ASCII-formularer behøver den indbyggede template-matching OCR-engine slet ingen modeller

Hurtig reference: flersproget RapidOCR-tjekliste

  • Opret options med THPDFRapidOCRDLLOptions.ForLanguage og behandl EArgumentException som et ikke-understøttet tag, ikke en runtime-fejl
  • Skift RecognitionModel og CharacterDictionary sammen, aldrig én alene; lige klasseantal beviser ikke lige tegnrækkefølge
  • Hold dictionaries som UTF-8 uden BOM, trim aldrig poster, og forvent, at modellen har N + 2 klasser: blank, N poster, mellemrum
  • En tilpasset CTC-dekoder skal dække sidste klasse og sidste time step og holde gentagelser adskilt af en blank
  • Brug én engine pr. sprogprofil og giv eksplicitte sidelister med videre for dokumenter med blandede scripts
  • RightToLeft ændrer kun box-rækkefølge; genkendt tekst forbliver i Unicode logisk rækkefølge
  • Installér modeller med Install-RapidOCRModels.ps1, så SHA256-pins holder model- og dictionary-parringen; sæt UseAngleClassifier := False, hvis du installerede med -SkipClassifier
  • Hæv MaxPixels over 16.777.216-defaulten, før du kører A3 eller større sider ved 300 DPI

RapidOCR-sprogprofilerne, den native DLL-adapter og OCR-tekstlags-pipelinen er en del af HotPDF Delphi PDF Component til Delphi, C++Builder og Windows FPC/Lazarus, fra v2.775.0 for de flersprogede profiler