Teknisk artikel

HotPDF kinesisk och flerspråkig OCR med RapidOCR i Delphi

HotPDF utför kinesisk och flerspråkig OCR i Delphi genom sin nativa RapidOCR-DLL-adaptor: THPDFRapidOCRDLLOptions.ForLanguage mappar en språktagg som 'zh-CN', 'zh-TW', 'ru' eller 'ar' till en matchad igenkänningsmodell och teckenordbok, och THotPDF.ApplyLoadedOCRTextLayer gör de igenkända raderna till ett osynligt, sökbart Unicode-textskikt på inskannade PDF-sidor

Att få en demo i latinskt skrift att fungera är den lätta delen. De intressanta felen börjar när du växlar till traditionell kinesiska eller ryska och utdatat blir självsäkert, välbildat nonsens, eller när varje rad tyst förlorar sitt sista tecken, eller när en arabisk sida kommer tillbaka med sina textboxar i fel ordning. Inget av detta kastar någon exception av sig självt. De språkprofiler som lades till i HotPDF v2.775.0 finns mest för att stänga de luckorna, och de fyra fällorna nedan är värda att förstå även om du aldrig rör den nativa koden, för var och en förklarar ett symtom du annars kunde lägga en dag på att jaga

Hur väljer ForLanguage en modell och ordbok?

THPDFRapidOCRDLLOptions.ForLanguage löser en tagg till en av nio profiler och returnerar alternativ som pekar på <profile>/recognition.onnx och <profile>/dictionary.txt under din modellkatalog, medan den behåller den delade detektorn, den valfria vinkelklassificeraren och tråd-, pixel- och timeout-standarderna från THPDFRapidOCRDLLOptions.Default. Metoden gemenar taggen, gör om understreck till bindestreck och trimmar omgivande blanksteg, så 'zh_TW', 'ZH-tw' och ' zh-tw ' landar alla på samma profil. Aliaser är en explicit lista i stället för en prefixmatchning: 'zh-Hant-TW' accepteras för att den är listad, medan en godtycklig regional variant som inte är listad kastar EArgumentException innan någon modell laddas

HotPDF ForLanguage-profiluppslagning för THPDFRapidOCRDLLOptions: taggar som zh_TW, ZH-tw och zh-TW normaliseras och matchas mot nio listade profiler, var och en fästande en igenkänningsmodell och ordbok som alltid sätts tillsammans, medan en olistad tagg kastar EArgumentException innan någon modell laddas
en tagg väljer ett fäst modell-ordboks-par; detektorn, klassificeraren och budgeterna förblir delade, och en okänd tagg faller snabbt i stället för att ladda något
ProfilSpråkExempel på taggarFäst modell
chFörenklad kinesiska och engelskazh, zh-CN, zh-Hans, chi_simPP-OCRv4
chinese_chtTraditionell kinesiskazh-TW, zh-HK, zh-Hant, chi_traPP-OCRv3
enEngelskaen, en-US, en-GB, engPP-OCRv4
latinFranska, tyska, spanska, portugisiska, italienska, nederländska, turkiskafr, de, es-419, pt-BR, trPP-OCRv3
japanJapanskaja, ja-JP, jpnPP-OCRv4
koreanKoreanskako, ko-KR, korPP-OCRv4
cyrillicRyska, ukrainska, bulgariska, vitryskaru, ru-RU, uk, bgPP-OCRv3
arabicArabiska, persiska, urduar, ar-SA, fa, urPP-OCRv4
devanagariHindi, marathi, nepalesiskahi, mr, nePP-OCRv4

Adaptorn själv laddar aldrig ner något. Du förser filerna en gång med det medföljande hjälpskriptet, till exempel tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (eller -Language All för alla nio profiler), och hjälparen placerar en delad detektor och klassificerare på rotfilnamnen som Default förväntar. Därefter blir en förenklat kinesisk skanning sökbar med några rader. Motorpärmen är samma IHPDFOCREngine-fog som beskrivs i artikeln om in-process RapidOCR-DLL:en och dess ABI-gräns, så den här stannar fokuserad på språk

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, delad detektor och klassificerare
  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 sidolista betyder varje sida; sidor som redan har text hoppas
    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;

Två detaljer i det utdatat förtjänar en notis. Den nativa pipelinen returnerar ett resultat per detekterad textrad, inte per ord, så AcceptedWordCount räknar rader här, och MinimumConfidence jämförs mot radens hela medelteckenkonfidens: en rad som medelvärdesätter 0,45 kastas som en enhet. UniqueScalarCount rapporterar hur många distinkta Unicode-skalarer textskiktet var tvunget att mappa in i sin font och ToUnicode-tabell, en nyttig sundhetskontroll att CJK-text faktiskt anlände i stället för en handfull latinska fallbacks. Håll motorinterfacet vid liv över dokument, för modellinitieringen händer i fabriken och är det dyra steget

Varför framställer att bara byta igenkänningsmodell skräp?

En CTC-igenkänningsmodell matar aldrig ut tecken, bara klassindex, och ordboken är den enda sak som gör om index 1 204 till en glyf. Byt ch/recognition.onnx mot cyrillic/recognition.onnx men behåll den kinesiska ordboken, och modellen kommer glatt att emittera giltiga kyrilliska index som gamla ordboken översätter till slumpmässiga han-tecken. Resultatet ser ut som text, passerar UTF-8-validering, och är sökbart för exakt ingenting. Det är därför ForLanguage alltid sätter RecognitionModel och CharacterDictionary tillsammans, och varför handbyggda alternativ aldrig ska ändra en utan den andra

Den uppenbara säkerhetskontrollen, att jämföra ordboksstorleken med modellens utdatabredd, är nödvändig men inte tillräcklig. Två ordböcker kan ha samma antal poster i en annan ordning, och ett av-väga-ett i ordningen förskjuter varje tecken med en code point. HotPDF kontrollerar därför i två steg när fabriken initierar modellen. Först måste utdataklassantalet vara lika med ordboksposterna plus två. Sedan, när ONNX-filen bäddar in en character-metadatalista, jämförs varje ordbokspost med den i ordning, och en missmatchning fäller initieringen med EInvalidOperation och en nativ diagnostext i stället för att framställa trolig skräp senare

"Plus två" kommer från klasslayouten. Klass 0 är CTC-blanken, klass 1 till N är ordboksraderna i filordning, och sista klassen är ett mellanslag. Vissa ordböcker bär också sin egen mellanslagspost, och den raden måste behållas exakt som den är. Det är här en välvillig Trim gör verklig skada: den gör en mellanslagspost till en tom sträng och förskjuter eller bryter tabellen. Den enda normalisering som är säker är att ta bort en avslutande vagnretur, så att en ordbok sparad med CRLF-radslut laddas korrekt, medan ett UTF-8-byte order-märke, en tom rad, eller en post som innehåller en tab avvisas. Skissen nedan visar layouten i Pascal; den är förklarande kod, inte ett HotPDF-API

HotPDF CTC-klasstabellayout för RapidOCR-ordböcker: klass 0 är blanken, klass 1 till N är ordboksraderna i filordning med en ensam mellanslagspost behållen, och sista klassen är ett mellanslag, vilket ger N plus 2 utdataklasser som fabriken verifierar mot modellen, metadata inkluderat
ordboken är den enda sak som gör klassindex till tecken, så dess storlek, ordning och mellanslagspost verifieras innan en enda sida igenkänns
// Endast illustration: klasstabellen en CTC-igenkännare förväntar
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);                                   // radbrytning vid filslut
  SetLength(Result, Last + 3);
  Result[0] := '';                               // klass 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: kasta bara CR
    if (Entry = '') or (Pos(#9, Entry) > 0) then
      raise EArgumentException.Create('Invalid dictionary entry');
    Result[I + 1] := Entry;                      // aldrig Trim: ' ' är en klass
  end;
  Result[Last + 2] := ' ';                       // sista klass: mellanslag
  // Length(Result) måste vara lika med modellens utdataklassantal
end;

Vad gör greedy CTC-avkodning egentligen?

Greedy CTC-avkodning plockar den högst poängsatta klassen vid varje tidssteg, kollapsar påföljande upprepningar till ett tecken, och kastar blankklassen; blanken är det som låter äkta dubblerade bokstäver överleva. En igenkänningsmodell tittar på en textrad som en sekvens av smala vertikala skivor, och för varje skiva, eller tidssteg, matar den ut en sannolikhet för varje klass. En rad som innehåller AA中 kan producera argmax-sekvensen A A blank A 中 space. Att kollapsa de två första A-stegen ger ett A, blanken skiljer det från nästa A, och resultatet är AA中 med den avslutande mellanslaget intakt. Utan blankregeln vore book och bok oskiljbara

HotPDF GreedyCTCDecode-genomgång: sex tidssteg röstar fram argmax-klasserna A, A, blank, A, ett han-tecken och mellanslag, påföljande upprepningar kollapsar, blanken återställer upprepningsskyddet så att en äkta dubblerad bokstav överlever, och tre gränsbuggar tyst tappar ordmellanslag, sista tecknet, eller dubblerade tecken
avkodaren är ett dussin rader och varje gräns spelar roll: inkludera sista klassen, inkludera sista steget, och låt bara en blank skilja upprepningar

Eftersom avkodaren bara är ett dussin rader är det lätt att få gränserna fel, och misslyckandena är tysta. Stannar inre argmax-loopen en klass för kort kan mellanslagsklassen aldrig vinna och varje rad kommer tillbaka utan ordmellanslag, vilket förstör frasökning på engelska och latinska sidor. Stannar yttre loopen ett tidssteg för kort försvinner sista tecknet i varje rad, vilket för en kort rad kan vara en tredjedel av texten. Och återställs inte upprepningsskyddet av en blank kollapsar dubblerade tecken som ll eller kinesiska reduplikationer som 谢谢 till ett. HotPDF-avkodaren inkluderar sista klassen och sista tidssteget, behåller blankavgränsade upprepningar, och avvisar dessutom poäng som inte är ändliga eller faller utanför 0 till 1, och varje klassantal som inte matchar ordboken. Här är samma logik som en Pascal-illustration

// Endast illustration: greedy CTC-avkodning med korrekta gränser.
// Scores håller Steps * Classes sannolikheter, en rad per tidssteg
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;                            // klass 0 är CTC-blanken
  for Step := 0 to Steps - 1 do             // inkludera sista tidssteget
  begin
    Best := 0;
    BestScore := Scores[Step * Classes];
    for C := 1 to Classes - 1 do            // inkludera sista klassen (mellanslag)
      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 återställer upprepningsskyddet
  end;
end;

Greedy-avkodning är inte den mest träffsäkra CTC-strategi som finns; beam search med en språkmodell kan fixa vissa tvetydiga skivor. För tryckta dokument vid 300 DPI är greedy-resultatet vanligen det modellen har att erbjuda, och avkodaren är inte platsen att kompensera för modellsvagheter. Den latinska PP-OCRv3-modellen kan till exempel läsa ñ som n även på ren indata. HotPDF klistrar inte över det med efterbehandlade teckenutbyten, för en substitutionstabell som fixar spanskan bryter något annat, och ett fel tecken i ett sökbart skikt är värre än en ärlig miss

Hur ordnar HotPDF textrader, inklusive höger-till-vänster-arabiska?

HotPDF sorterar detekterade textboxar uppifrån och ner, grupperar boxar i en rad när de överlappar vertikalt med minst hälften av den mindre boxhöjden, och ordnar varje rad vänster till höger, eller höger till vänster när RightToLeft är aktiverat; tecknen inuti varje igenkänd rad vänds aldrig. Grupperingen spelar roll för en detektor ofta delar en visuell rad i flera boxar, till exempel en etikett och ett värde skilda av ett brett gap, och en ren toppkoordinatsortering skulle fläta in dem med den närliggande raden närhelst deras toppar skiljer en pixel eller två

Den arabiska profilen sätter RightToLeft := True, vilket talar om för DLL:en att ordna boxarna i varje rad efter sin högerkant, från högermarginalen inåt. Det är hela effekten. Texten modellen returnerar för en rad är redan i Unicode logisk ordning, den ordning en arabisk läsare läser och skriver den i, och det är också den ordning PDF-textextraktion och sökning förväntar. Att mekaniskt vända strängen för att få den att "se rätt ut" i en debugger skulle bryta sökning, kopiera och klistra in, och skärmläsare. Bidirektionell visning och glyfformning är visarens jobb

En motor betjänar en språkprofil. Det finns ingen automatisk skriftdetektering, så ett dokument som blandar skrifter behöver en motor per profil, tillämpad på de sidor som använder den. Eftersom ApplyLoadedOCRTextLayer tar en explicit sidolista och committar varje anrop som sin egen allt-eller-inget-transaktion är det rakt på sak

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
  Models: THPDFRapidOCRDLLOptions;
begin
  // kastar EArgumentException för en okänd tagg, före någon modell laddas
  Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
  Models.MaxPixels := 33554432;          // utrymme för A3-sidor vid 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-profil
  Arabic := CreateRapidEngine('ar-SA');   // arabic-profil, 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-raden finns av en anledning. DLL-alternativen har som standard 16 777 216 pixlar per begäran, vilket täcker A4 och US Letter vid 300 DPI gott, men en A3-sida vid 300 DPI är ungefär 3508 gånger 4961 pixlar, ungefär 17,4 miljoner, och begärandet avvisas som överbudget. Höj MaxPixels (taket är 67 108 864) eller sänk THPDFOCRTextLayerOptions.DPI för stora format. Höger-till-vänster-ordningen använder den valfria exporten HPDFRapidOCRSetReadingDirection av ABI version 1; adaptorn kräver den bara när RightToLeft är satt, så en äldre DLL fortsätter betjäna vänster-till-höger-språk och faller vid motorskapandet med en EArgumentException som namnger den saknade exporten för arabiska

Varför misslyckas nyare OCR-modeller att ladda?

HotPDF RapidOCR-DLL:en länkar en statisk ONNX Runtime 1.14, som inte kan läsa modeller sparade med ONNX IR version 10, och nyare exporter som PP-OCRv5-modeller kan kräva en nyare runtime än så; en sådan modell faller vid motorskapandet med en nativ diagnostext. Den begränsningen är anledningen till att språkpaketen är fästade vid specifika PP-OCRv3- och PP-OCRv4-igenkännar- och ordbokspar i stället för "senaste", och varför tabellen ovan blandar de två generationerna: varje fäst par är ett som laddas och verifieras under den runtime:n

Installationsprogrammet upprätthåller paraformen. Varje fil i dess manifest bär en SHA256-hash, en existerande fil med en annan hash stoppar installationen i stället för att skrivas över, och varje nerladdning landar under ett tillfälligt namn och flyttas bara på plats efter att dess hash matchar. Det skyddar mot den tysta versionen av ordboksproblemet: någon släpper en nyare recognition.onnx i en profilmapp för hand, klassantalet råkar matcha, och inget faller förrän en kund rapporterar att sökningen inte hittar ord de tydligt kan se. Vid runtime stannar adaptorn offline och hämtar aldrig en saknad modell. Igenkännaren validerar också modellformen vid laddning, och accepterar NCHW-indata med en fast höjd på 32 eller 48 pixlar eller en dynamisk höjd, som den kör vid 48

Behöver du en skrift som ingen av de nio profilerna täcker kan du fortfarande peka RecognitionModel och CharacterDictionary mot dina egna filer. Samma kontroller gäller, vilket är poängen: ett felmatchat par faller vid initieringen, inte i din kunds arkiv. För sidor där ingen RapidOCR-profil passar kopplar Tesseract-adaptorn för sökbar PDF in i samma ApplyLoadedOCRTextLayer-anrop, och för maskintryckta ASCII-formulär behöver den inbyggda mallmatchnings-OCR-motorn inga modeller alls

Snabbreferens: flerspråkig RapidOCR-checklista

  • Skapa alternativ med THPDFRapidOCRDLLOptions.ForLanguage och behandla EArgumentException som en tagg utan stöd, inte ett runtime-fel
  • Ändra RecognitionModel och CharacterDictionary tillsammans, aldrig en ensam; lika klassantal bevisar inte lika teckenordning
  • Behåll ordböcker som UTF-8 utan BOM, trimma aldrig poster, och förvänta dig att modellen har N + 2 klasser: blank, N poster, mellanslag
  • En egen CTC-avkodare måste täcka sista klassen och sista tidssteget och behålla upprepningar skilda av en blank
  • Använd en motor per språkprofil och skicka explicita sidolistor för dokument med blandade skrifter
  • RightToLeft ändrar bara boxordning; igenkänd text stannar i Unicode logisk ordning
  • Installera modeller med Install-RapidOCRModels.ps1 så att SHA256-fästningarna håller modell- och ordboksparet; sätt UseAngleClassifier := False om du installerade med -SkipClassifier
  • Höj MaxPixels över standardvärdet 16 777 216 innan A3 eller större sidor körs vid 300 DPI

RapidOCR-språkprofilerna, den nativa DLL-adaptorn och OCR-textskiktspipelinen är del av HotPDF Delphi PDF Component för Delphi, C++Builder och Windows FPC/Lazarus, med början i v2.775.0 för de flerspråkiga profilerna