Articolo tecnico

OCR cinese e multilingua con RapidOCR in HotPDF e Delphi

HotPDF esegue OCR cinese e multilingua in Delphi attraverso il suo adapter nativo per la DLL RapidOCR: THPDFRapidOCRDLLOptions.ForLanguage mappa un tag di lingua come 'zh-CN', 'zh-TW', 'ru' o 'ar' a un modello di riconoscimento e un dizionario di caratteri abbinati, e THotPDF.ApplyLoadedOCRTextLayer trasforma le righe riconosciute in un livello di testo Unicode invisibile e ricercabile sulle pagine PDF scansionate

Far funzionare una demo in scrittura latina è la parte facile. I fallimenti interessanti cominciano quando passi al cinese tradizionale o al russo e l'output diventa una sciocchezza sicura di sé e ben formata, o quando ogni riga perde silenziosamente il proprio ultimo carattere, o quando una pagina araba torna con le proprie text box nell'ordine sbagliato. Nessuno di questi solleva un'eccezione da solo. I preset linguistici aggiunti in HotPDF v2.775.0 esistono soprattutto per chiudere quei varchi, e le quattro trappole qui sotto meritano di essere capite anche se non tocchi mai il codice nativo, perché ciascuna spiega un sintomo che altrimenti potresti passare una giornata a cacciare

Come sceglie ForLanguage un modello e un dizionario?

THPDFRapidOCRDLLOptions.ForLanguage risolve un tag a uno di nove profili e restituisce opzioni che puntano a <profile>/recognition.onnx e <profile>/dictionary.txt sotto la tua directory dei modelli, conservando il rilevatore condiviso, il classificatore d'angolo opzionale, e i default di thread, pixel e timeout di THPDFRapidOCRDLLOptions.Default. Il metodo mette il tag in minuscolo, trasforma i trattini bassi in trattini e taglia gli spazi ai bordi, così 'zh_TW', 'ZH-tw' e ' zh-tw ' atterrano tutti sullo stesso profilo. Gli alias sono una lista esplicita anziché un confronto per prefisso: 'zh-Hant-TW' viene accettato perché è elencato, mentre una variante regionale arbitraria non elencata solleva EArgumentException prima che qualsiasi modello venga caricato

Risoluzione del profilo ForLanguage in HotPDF per THPDFRapidOCRDLLOptions: tag come zh_TW, ZH-tw e zh-TW vengono normalizzati e confrontati con nove profili elencati, ognuno dei quali fissa un modello di riconoscimento e un dizionario che vengono sempre impostati insieme, mentre un tag non elencato solleva EArgumentException prima che qualsiasi modello carichi
un tag seleziona una coppia modello-dizionario fissata; rilevatore, classificatore e budget restano condivisi, e un tag sconosciuto fallisce in fretta invece di caricare qualsiasi cosa
ProfiloLingueTag di esempioModello fissato
chCinese semplificato e inglesezh, zh-CN, zh-Hans, chi_simPP-OCRv4
chinese_chtCinese tradizionalezh-TW, zh-HK, zh-Hant, chi_traPP-OCRv3
enIngleseen, en-US, en-GB, engPP-OCRv4
latinFrancese, tedesco, spagnolo, portoghese, italiano, olandese, turcofr, de, es-419, pt-BR, trPP-OCRv3
japanGiapponeseja, ja-JP, jpnPP-OCRv4
koreanCoreanoko, ko-KR, korPP-OCRv4
cyrillicRusso, ucraino, bulgaro, bielorussoru, ru-RU, uk, bgPP-OCRv3
arabicArabo, persiano, urduar, ar-SA, fa, urPP-OCRv4
devanagariHindi, marathi, nepalesehi, mr, nePP-OCRv4

L'adapter in sé non scarica mai nulla. Provisioni i file una volta con l'helper incluso, per esempio tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (oppure -Language All per tutti e nove i profili), e l'helper colloca un rilevatore e un classificatore condivisi ai nomi di file alla radice che Default si aspetta. Dopo di che, una scansione in cinese semplificato diventa ricercabile con poche righe. Il cablaggio del motore passa per la stessa cucitura IHPDFOCREngine descritta in l'articolo sulla DLL RapidOCR in-process e il suo confine ABI, quindi questo resta concentrato sulle lingue

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, detector e classificatore condivisi
  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
    // una lista pagine vuota significa tutte; le pagine con testo vengono saltate
    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;

Due dettagli in quell'output meritano una nota. La pipeline nativa restituisce un risultato per riga di testo rilevata, non per parola, quindi AcceptedWordCount conta qui righe, e MinimumConfidence viene confrontato con la confidenza media dei caratteri dell'intera riga: una riga con media 0,45 viene scartata come unità. UniqueScalarCount riporta quanti scalari Unicode distinti il livello di testo ha dovuto mappare nel proprio font e nella tabella ToUnicode, un utile controllo di sanità che il testo CJK sia davvero arrivato invece di una manciata di ripieghi latini. Tieni viva l'interfaccia del motore tra i documenti, perché l'inizializzazione dei modelli avviene nella factory ed è il passo costoso

Perché cambiare solo il modello di riconoscimento produce spazzatura?

Un modello di riconoscimento CTC non produce mai caratteri, solo indici di classe, e il dizionario è l'unica cosa che trasforma l'indice 1.204 in un glifo. Scambia ch/recognition.onnx con cyrillic/recognition.onnx ma tieni il dizionario cinese, e il modello emetterà allegramente indici cirillici validi che il vecchio dizionario traduce in caratteri Han casuali. Il risultato sembra testo, passa la validazione UTF-8, ed è ricercabile per esattamente niente. Ecco perché ForLanguage imposta sempre RecognitionModel e CharacterDictionary insieme, e perché le opzioni costruite a mano non dovrebbero mai cambiare uno senza l'altro

L'ovvio controllo di sicurezza, confrontare la dimensione del dizionario con la larghezza di output del modello, è necessario ma non sufficiente. Due dizionari possono avere lo stesso numero di voci in ordine diverso, e uno scarto di uno nell'ordine sposta ogni carattere di un code point. HotPDF quindi controlla in due stadi quando la factory inizializza il modello. Primo, il conteggio di classi di output deve eguagliare le voci del dizionario più due. Secondo, quando il file ONNX incorpora una lista di metadati character, ogni voce del dizionario viene confrontata con essa in ordine, e una discrepanza fa fallire l'inizializzazione con EInvalidOperation e una diagnostica nativa invece di produrre spazzatura plausibile più tardi

Il "più due" viene dal layout delle classi. La classe 0 è il blank CTC, le classi da 1 a N sono le righe del dizionario in ordine di file, e la classe finale è uno spazio. Alcuni dizionari portano anche una propria voce spazio, e quella riga va conservata esattamente com'è. È qui che un Trim ben intenzionato fa danno vero: trasforma una voce di spazio singolo in una stringa vuota e sposta o rompe la tabella. L'unica normalizzazione sicura è togliere un carriage return finale, così un dizionario salvato con finali di riga CRLF si carica correttamente, mentre un byte order mark UTF-8, una riga vuota, o una voce contenente una tab viene rifiutata. Lo schizzo qui sotto mostra il layout in Pascal; è codice esplicativo, non un'API HotPDF

Layout della tabella di classi CTC in HotPDF per i dizionari RapidOCR: la classe 0 è il blank, le classi da 1 a N sono le righe del dizionario in ordine di file con eventuali voci di spazio singolo conservate, e la classe finale è uno spazio, dando N più 2 classi di output che la factory verifica contro il modello, metadati inclusi
il dizionario è l'unica cosa che trasforma gli indici di classe in caratteri, quindi dimensione, ordine e voce spazio vengono verificati prima che una sola pagina venga riconosciuta
// Solo illustrazione: la tabella di classi che un recognizer CTC si aspetta
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);                                   // newline a fine file
  SetLength(Result, Last + 3);
  Result[0] := '';                               // classe 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: togli solo il CR
    if (Entry = '') or (Pos(#9, Entry) > 0) then
      raise EArgumentException.Create('Invalid dictionary entry');
    Result[I + 1] := Entry;                      // mai Trim: ' ' è una classe
  end;
  Result[Last + 2] := ' ';                       // classe finale: spazio
  // Length(Result) deve eguagliare il conteggio classi dell'output del modello
end;

Che cosa fa davvero la decodifica CTC greedy?

La decodifica CTC greedy sceglie la classe col punteggio più alto a ogni time step, collassa i ripetuti consecutivi in un solo carattere, e scarta la classe blank; il blank è ciò che permette alle lettere davvero raddoppiate di sopravvivere. Un modello di riconoscimento guarda una riga di testo come una sequenza di strette fette verticali, e per ogni fetta, o time step, produce una probabilità per ogni classe. Una riga contenente AA中 potrebbe produrre la sequenza di argmax A A blank A 中 spazio. Collassare i primi due step A dà una A, il blank la separa dalla successiva A, e il risultato è AA中 con lo spazio finale intatto. Senza la regola del blank, book e bok sarebbero indistinguibili

Percorso GreedyCTCDecode di HotPDF: sei time step votano classi argmax A, A, blank, A, un carattere Han e spazio, i ripetuti consecutivi collassano, il blank azzera la guardia dei ripetuti così una lettera davvero raddoppiata sopravvive, e tre bug ai confini scartano silenziosamente la spaziatura delle parole, l'ultimo carattere, o i caratteri raddoppiati
il decoder è una dozzina di righe e ogni confine conta: includi l'ultima classe, includi l'ultimo step, e lascia che solo un blank separi i ripetuti

Poiché il decoder è solo una dozzina di righe, è facile sbagliare i confini, e i fallimenti sono silenziosi. Se il ciclo argmax interno si ferma una classe prima, la classe spazio non può mai vincere e ogni riga torna senza spaziatura delle parole, il che distrugge la ricerca di frasi sulle pagine inglesi e latine. Se il ciclo esterno si ferma un time step prima, l'ultimo carattere di ogni riga sparisce, che per una riga corta può essere un terzo del testo. E se la guardia dei ripetuti non viene azzerata da un blank, caratteri raddoppiati come ll o reduplicazioni cinesi come 谢谢 collassano in uno. Il decoder di HotPDF include l'ultima classe e l'ultimo time step, conserva i ripetuti separati da blank, e inoltre rifiuta punteggi che non sono finiti o stanno fuori da 0 a 1, e qualsiasi conteggio di classi che non corrisponda al dizionario. Ecco la stessa logica come illustrazione Pascal

// Solo illustrazione: decodifica CTC greedy con confini corretti.
// Scores tiene Steps * Classes probabilità, una riga per 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;                            // la classe 0 è il CTC blank
  for Step := 0 to Steps - 1 do             // includi l'ultimo time step
  begin
    Best := 0;
    BestScore := Scores[Step * Classes];
    for C := 1 to Classes - 1 do            // includi l'ultima classe (spazio)
      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;                       // un blank azzera la guardia dei ripetuti
  end;
end;

La decodifica greedy non è la strategia CTC più precisa disponibile; la beam search con un modello linguistico può sistemare qualche fetta ambigua. Per documenti stampati a 300 DPI il risultato greedy di solito è ciò che il modello ha da offrire, e il decoder non è il posto dove compensare le debolezze del modello. Il modello latino PP-OCRv3, per esempio, può leggere ñ come n anche su input pulito. HotPDF non copre quel difetto con sostituzioni di caratteri in post-processing, perché una tabella di sostituzione che sistema lo spagnolo rompe qualcos'altro, e un carattere sbagliato in un livello ricercabile è peggio di un mancato onesto

Come ordina HotPDF le righe di testo, incluso l'arabo da destra a sinistra?

HotPDF ordina le text box rilevate dall'alto al basso, raggruppa le box in una riga quando si sovrappongono verticalmente per almeno metà dell'altezza della box più piccola, e ordina ogni riga da sinistra a destra, o da destra a sinistra quando RightToLeft è attivo; i caratteri dentro ogni riga riconosciuta non vengono mai invertiti. Il raggruppamento conta perché un rilevatore spesso spezza una riga visiva in più box, per esempio un'etichetta e un valore separati da un ampio varco, e un puro ordinamento per coordinata alta li intreccerebbe con la riga vicina ogni volta che i loro alti differiscono di un pixel o due

Il preset arabo imposta RightToLeft := True, che dice alla DLL di ordinare le box di ogni riga per il proprio bordo destro, dal margine destro verso l'interno. È tutto l'effetto. Il testo che il modello restituisce per una riga è già in ordine logico Unicode, l'ordine in cui un lettore arabo legge e digita, ed è anche l'ordine che l'estrazione di testo PDF e la ricerca si aspettano. Invertire meccanicamente la stringa per farla "sembrare giusta" in un debugger romperebbe ricerca, copia e incolla, e screen reader. La visualizzazione bidirezionale e la forma dei glifi sono compito del viewer

Un motore serve un profilo lingua. Non c'è rilevamento automatico della scrittura, quindi un documento che mescola scritture ha bisogno di un motore per profilo, applicato alle pagine che la usano. Poiché ApplyLoadedOCRTextLayer prende una lista pagine esplicita e scrive ogni chiamata come propria transazione tutto-o-nulla, è semplice

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
  Models: THPDFRapidOCRDLLOptions;
begin
  // solleva EArgumentException per un tag sconosciuto, prima che qualsiasi modello carichi
  Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
  Models.MaxPixels := 33554432;          // spazio per pagine A3 a 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');  // profilo chinese_cht
  Arabic := CreateRapidEngine('ar-SA');   // profilo 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;

La riga MaxPixels c'è per un motivo. Le opzioni della DLL hanno per default 16.777.216 pixel per richiesta, che coprono comodamente A4 e US Letter a 300 DPI, ma una pagina A3 a 300 DPI è circa 3508 per 4961 pixel, circa 17,4 milioni, e la richiesta viene rifiutata come fuori budget. Alza MaxPixels (il tetto è 67.108.864) o abbassa THPDFOCRTextLayerOptions.DPI per i formati grandi. L'ordinamento da destra a sinistra usa l'export opzionale HPDFRapidOCRSetReadingDirection della versione ABI 1; l'adapter lo richiede solo quando RightToLeft è impostato, così una DLL più vecchia continua a servire le lingue da sinistra a destra e fallisce alla creazione del motore con una EArgumentException che nomina l'export mancante per l'arabo

Perché i modelli OCR più recenti non si caricano?

La DLL RapidOCR di HotPDF collega una ONNX Runtime 1.14 statica, che non può leggere modelli salvati con ONNX IR versione 10, e export più recenti come i modelli PP-OCRv5 possono richiedere un runtime più nuovo di quello; un tale modello fallisce alla creazione del motore con una diagnostica nativa. Quel vincolo è la ragione per cui i language pack sono fissati a specifiche coppie recognizer e dizionario PP-OCRv3 e PP-OCRv4 invece che a "l'ultimo", e perché la tabella qui sopra mescola le due generazioni: ogni coppia fissata è una che si carica e verifica sotto quel runtime

L'installatore impone l'abbinamento. Ogni file nel proprio manifest porta un hash SHA256, un file esistente con hash diverso ferma l'installazione invece di venire sovrascritto, e ogni download atterra sotto un nome temporaneo e si sposta sul posto solo dopo che il suo hash torna. Questo protegge dalla versione silenziosa del problema del dizionario: qualcuno lascia cadere a mano un recognition.onnx più recente in una cartella di profilo, il conteggio di classi per caso torna, e nulla fallisce finché un cliente non segnala che la ricerca non trova parole che si vedono benissimo. A runtime l'adapter resta offline e non va mai a prendere un modello mancante. Il recognizer valida anche la forma del modello al caricamento, accettando ingresso NCHW con altezza fissa di 32 o 48 pixel o altezza dinamica, che esegue a 48

Se ti serve una scrittura che nessuno dei nove profili copre, puoi comunque puntare RecognitionModel e CharacterDictionary ai tuoi file. Gli stessi controlli si applicano, che è il punto: una coppia disabbinata fallisce all'inizializzazione, non nell'archivio del tuo cliente. Per le pagine dove nessun profilo RapidOCR sta, l'adapter Tesseract per PDF ricercabile si innesta nella stessa chiamata ApplyLoadedOCRTextLayer, e per moduli ASCII stampati il motore OCR integrato a corrispondenza di template non ha bisogno di modelli affatto

Riferimento rapido: checklist RapidOCR multilingua

  • Crea le opzioni con THPDFRapidOCRDLLOptions.ForLanguage e tratta EArgumentException come un tag non supportato, non come un guasto di runtime
  • Cambia RecognitionModel e CharacterDictionary insieme, mai uno da solo; conteggi di classi uguali non provano un ordine di caratteri uguale
  • Tieni i dizionari come UTF-8 senza BOM, non fare mai trim delle voci, e aspettati che il modello abbia N + 2 classi: blank, N voci, spazio
  • Un decoder CTC personalizzato deve coprire l'ultima classe e l'ultimo time step e conservare i ripetuti separati da un blank
  • Usa un motore per profilo lingua e passa liste pagine esplicite per i documenti con scritture miste
  • RightToLeft cambia solo l'ordine delle box; il testo riconosciuto resta in ordine logico Unicode
  • Installa i modelli con Install-RapidOCRModels.ps1 così i pin SHA256 tengono l'abbinamento modello-dizionario; imposta UseAngleClassifier := False se hai installato con -SkipClassifier
  • Alza MaxPixels sopra il default di 16.777.216 prima di girare pagine A3 o più grandi a 300 DPI

I preset linguistici RapidOCR, l'adapter della DLL nativa e la pipeline del livello di testo OCR fanno parte del HotPDF Delphi PDF Component per Delphi, C++Builder e Windows FPC/Lazarus, a partire dalla v2.775.0 per i profili multilingua