Techninis straipsnis

HotPDF kinų ir daugiakalbis OCR su RapidOCR Delphi

HotPDF atlieka kinų ir daugiakalbį OCR Delphi per savą natyvų RapidOCR DLL adapterį: THPDFRapidOCRDLLOptions.ForLanguage kalbos žymą, tokią kaip 'zh-CN', 'zh-TW', 'ru' arba 'ar', susieja su atitinkančiu atpažinimo modeliu ir simbolių žodynu, o THotPDF.ApplyLoadedOCRTextLayer atpažintas eilutes paverčia nematomu, ieškomu Unicode teksto sluoksniu nuskenuotuose PDF puslapiuose

Lotyniško rašto demo paleidimas – lengvoji dalis. Įdomios nesėkmės prasideda, kai perjungiate į tradicinę kinų ar rusų kalbą ir išvestis virsta pasitikinčia, taisyklingos formos beprotybe, kai kiekviena eilutė tylioje praranda paskutinįjį simbolį arba kai arabų puslapis grįžta su teksto langeliais netinkama tvarka. Joks iš šių pats nekelia išimties. HotPDF v2.775.0 pridėti kalbų profiliai egzistuoja daugiausia tam, kad užtupestų tuos plyšius, o keturi žemiau esantys spąstai verti supratimo net jei natyvaus kodo neliestumėte niekada, nes kiekvienas paaiškina simptomą, už kurį kitaip galėtumėte gaišti dieną

Kaip ForLanguage renkasi modelį ir žodyną?

THPDFRapidOCRDLLOptions.ForLanguage žymą išspręndžia į vieną iš devynių profilių ir grąžina nustatymus, rodančius į <profile>/recognition.onnx ir <profile>/dictionary.txt žemiau jūsų modelių katalogo, bendrąjį aptikiklį, pasirinktinį kampų klasifikatorių ir gijų, pikselių bei laiko limitų numatytąsias iš THPDFRapidOCRDLLOptions.Default palikdama. Metodas žymą nuleidžia į mažąsias raides, pabraukimus paverčia brūkšneliais ir nukerpa aplinkinius tarpus, tad 'zh_TW', 'ZH-tw' ir ' zh-tw ' visi atkrenta ant to paties profilio. Aliasing yra aiškus sąrašas, o ne priešdėlio atitikimas: 'zh-Hant-TW' priimama, nes įvardyta, o savavališkas regioninis variantas, sąraše nesantis, kelia EArgumentException dar prieš įkeliant bet kurį modelį

HotPDF ForLanguage profilio išsprendimas THPDFRapidOCRDLLOptions: žymos, tokios kaip zh_TW, ZH-tw ir zh-TW, suvienodinamos ir gretinamos su devyniais išvardytais profiliais, iš kurių kiekvienas fiksuoja kartu visada nustatomą atpažinimo modelio ir žodyno porą, o neįvardyta žyma kelia EArgumentException dar prieš įkeliant bet kokį modelį
viena žyma pasirenka vieną fiksuotą modelio-žodyno porą; aptikiklis, klasifikatorius ir biudžetai lieka bendri, o nežinoma žyma žlunga greitai, nieko neįkėlusi
ProfilisKalbosŽymių pavyzdžiaiFiksuotas modelis
chSupaprastintoji kinų ir anglųzh, zh-CN, zh-Hans, chi_simPP-OCRv4
chinese_chtTradicinė kinųzh-TW, zh-HK, zh-Hant, chi_traPP-OCRv3
enAnglųen, en-US, en-GB, engPP-OCRv4
latinPrancūzų, vokiečių, ispanų, portugalų, italų, olandų, turkųfr, de, es-419, pt-BR, trPP-OCRv3
japanJaponųja, ja-JP, jpnPP-OCRv4
koreanKorėjiečiųko, ko-KR, korPP-OCRv4
cyrillicRusų, ukrainiečių, bulgarų, baltarusiųru, ru-RU, uk, bgPP-OCRv3
arabicArabų, persų, urduar, ar-SA, fa, urPP-OCRv4
devanagariHindi, maratų, nepaliečiųhi, mr, nePP-OCRv4

Pats adapteris nieko neparsiunčia. Failus parūpinkite kartą su pridėtuoju pagalbininku, pavyzdžiui tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (arba -Language All visiems devyniems profiliams), o pagalbininkas padeda bendrąjį aptikiklį ir klasifikatorių prie šakninių failų vardų, kurių tikisi Default. Po to supaprastintosios kinų skenuotė tampa ieškoma keliomis eilutėmis. Variklio pajungimas – ta pati IHPDFOCREngine siūlė, aprašyta straipsnyje apie RapidOCR DLL procese ir jo ABI ribą, tad šis lieka prie kalbų

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, bendras aptikiklis ir klasifikatorius
  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
    // tuščias puslapių sąrašas reiškia visus puslapius; puslapiai, turintys tekstą, praleidžiami
    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;

Dvi tos išvesties detalės nusipelno pastabos. Natyvus srautas grąžina po vieną rezultatą aptiktai teksto eilutei, o ne žodžiui, tad AcceptedWordCount čia skaičiuoja eilutes, o MinimumConfidence lyginama su visos eilutės vidutine simbolių pasitikėjimo reikšme: eilutė, kurios vidurkis 0.45, numetama kaip vienetas. UniqueScalarCount praneša, kiek atskirų Unicode skaliarų teksto sluoksniui teko susieti su savu šriftu ir ToUnicode lentele – naudinga sąžiningumo patikra, kad CJK tekstas tikrai atkeliavo, o ne saujelė lotyniškų atsarginių. Laikykite variklio sąsają gyvą per dokumentus, nes modelių inicializacija vyksta faktorijoje ir yra brangus žingsnis

Kodėl keičiant tik atpažinimo modelį gaunamas šlamštas?

CTC atpažinimo modelis niekada neišduoda simbolių – tik klasių indeksus, o žodynas yra vienintelis dalykas, paverčiantis 1 204 indeksą į glifą. Pakeiskite ch/recognition.onnx į cyrillic/recognition.onnx, bet palikite kinų žodyną, ir modelis mielai išduos teisėtus kirilicos indeksus, kuriuos senasis žodynas išvers į atsitiktinius Han simbolius. Rezultatas atrodo kaip tekstas, praeina UTF-8 patikrą ir yra ieškomas lygiai nieko. Todėl ForLanguage visada nustato RecognitionModel ir CharacterDictionary kartu, o rankomis sudėti nustatymai niekada neturėtų keisti vieno be kito

Akivaizdi saugumo patikra – žodyno dydžio lyginimas su modelio išvesties pločiu – būtina, bet nepakankama. Du žodynai gali turėti tą patį įrašų skaičių kita tvarka, ir vienetinė paklaida tvarkoje pastumpia kiekvieną simbolį per vieną kodo tašką. HotPDF todėl tikrina dviem etapais, kai faktorija inicializuoja modelį. Pirma, išvesties klasių skaičius privalo būti lygus žodyno įrašams plius du. Antra, kai ONNX failas įterpia character metaduomenų sąrašą, kiekvienas žodyno įrašas su juo lyginamas tvarka, ir nesutapimas žlugdo inicializaciją su EInvalidOperation ir natyvia diagnostika vietoj tikėtino šlamšto vėliau

„Plius du“ atsiranda iš klasių išdėstymo. 0 klasė yra CTC blank, 1–N klasės – žodyno eilutės failo tvarka, o galutinė klasė – tarpas. Kai kurie žodynai dar neša savą tarpų įrašą, ir ta eilutė privalo likti lygiai tokia, kokia yra. Būtent čia geraširdiškas Trim padaro tikrą žalą: vieno tarpo įrašą paverčia tuščia eilute ir pastumpia arba sulaužo lentelę. Vienintelė saugi normalizacija – pabaigos grąžinimo simbolio pašalinimas, tad CRLF eilučių pabaigomis išsaugotas žodynas įsikelia teisingai, o UTF-8 baitų eiliškumo žyma, tuščia eilutė arba įrašas su tabuliatoriumi atmetami. Eskizas žemiau rodo išdėstymą Pascal kalba; tai paaiškinamasis kodas, ne HotPDF API

HotPDF CTC klasių lentelės išdėstymas RapidOCR žodynams: 0 klasė yra blank, 1–N klasės – žodyno eilutės failo tvarka, išlaikant bet kokį vieną tarpų įrašą, o galutinė klasė – tarpas; iš viso N plius 2 išvesties klasės, kurias faktorija patvirtina pagal modelį, įskaitant metaduomenis
žodynas – vienintelis dalykas, verčiantis klasių indeksus į simbolius, tad jo dydis, tvarka ir tarpų įrašas patvirtinami dar prieš atpažįstant pirmąjį puslapį
// Tik iliustracija: klasės lentelė, kurios tikisi CTC atpažintojas
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);                                   // nauja eilutė failo gale
  SetLength(Result, Last + 3);
  Result[0] := '';                               // klasė 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: nupjauname tik CR
    if (Entry = '') or (Pos(#9, Entry) > 0) then
      raise EArgumentException.Create('Invalid dictionary entry');
    Result[I + 1] := Entry;                      // niekada Trim: ' ' yra klasė
  end;
  Result[Last + 2] := ' ';                       // galutinė klasė: tarpas
  // Length(Result) privalo būti lygus modelio išvesties klasių skaičiui
end;

Ką godus CTC dekodavimas iš tikrųjų daro?

Godus CTC dekodavimas kiekvienu laiko žingsniu renkasi aukščiausiai įvertintą klasę, suspaudžia iš eilės einančius kartojimus į vieną simbolį ir numeta blank klasę; blank yra tai, kas leidžia tikroms dviguboms raidėms išgyventi. Atpažinimo modelis į teksto eilutę žiūri kaip į siaurų vertikalių pjūvių seką, ir kiekvienam pjūviui – laiko žingsniui – išduoda tikimybę kiekvienai klasei. Eilutė su AA中 gali pagimdyti argmax seką A A blank A 中 space. Pirmų dviejų A žingsnių suspaudimas duoda vieną A, blank jį atskiria nuo kito A, o rezultatas – AA中 su nepaliesta galine tarpine. Be blank taisyklės book ir bok būtų neatskiriami

HotPDF GreedyCTCDecode apėjimas: šeši laiko žingsniai balsuoja argmax klases A, A, blank, A, Han simbolį ir tarpą, iš eilės einantys kartojimai suspaudžiami, blank atstato kartojimo apsaugą, tad tikra dviguba raidė išgyvena, o trys ribų klaidos tylioje numeta žodžių tarpus, paskutinįjį simbolį arba dvigubas raides
dekoduotojas – keliolika eilučių, ir kiekviena riba svarbi: įtraukite paskutinę klasę, įtraukite paskutinį žingsnį ir leiskite vien tik blank atskirti kartojimus

Kadangi dekoduotojas – vos keliolika eilučių, ribas lengva padaryti neteisingai, o nesėkmės tylia. Jei vidinis argmax ciklas sustoja viena klasę ankščiau, tarpo klasė niekada nelaimi, ir kiekviena eilutė grįžta be žodžių tarpų – kas suardo frazių paiešką anglų ir lotynų puslapiuose. Jei išorinis ciklas sustoja vienu laiko žingsniu ankščiau, kiekvienos eilutės paskutinis simbolis dingsta, o trumpai eilutei tai gali būti trečdalis teksto. O jei kartojimo apsaugos neatstato blank, dvigubi simboliai, tokie kaip ll, ar kinų kartojimai, tokie kaip 谢谢, suspaudžiami į vieną. HotPDF dekoduotojas įtraukia paskutinę klasę ir paskutinį laiko žingsnį, išlaiko blank atskirtus kartojimus, be to atmeta įverčius, kurie nėra baigtiniai arba krenta už 0–1 ribų, ir bet kokį klasių skaičių, neatitinkantį žodyno. Štai ta pati logika kaip Pascal iliustracija

// Tik iliustracija: godus CTC dekodavimas su teisingomis ribomis.
// Scores laiko Steps * Classes tikimybių, po eilutę laiko žingsniui
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;                            // klasė 0 yra CTC blank
  for Step := 0 to Steps - 1 do             // įtraukite paskutinį laiko žingsnį
  begin
    Best := 0;
    BestScore := Scores[Step * Classes];
    for C := 1 to Classes - 1 do            // įtraukite paskutinę klasę (tarpas)
      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 atstato kartojimo apsaugą
  end;
end;

Godus dekodavimas nėra tiksliausia iš turimų CTC strategijų; spindulio paieška su kalbos modeliu gali ištaisyti kai kuriuos dviprasmiškus pjūvius. Spausdintiems dokumentams ties 300 DPI godus rezultatas paprastai ir yra tai, ką modelis turi, o dekoduotojas – ne vieta kompensuoti modelio silpnybes. Lotynų PP-OCRv3 modelis, pavyzdžiui, gali skaityti ñ kaip n net ant švaraus įvedimo. HotPDF to neplombuoja po-apdorojimo simbolių pakeitimais, nes pakaitų lentelė, ištaisanti ispanų, sulaužo ką nors kita, o netinkamas simbolis ieškomame sluoksnyje blogesnis už sąžiningą praleidimą

Kaip HotPDF rikiuoja teksto eilutes, įskaitant iš dešinės į kairę rašomą arabų?

HotPDF aptiktus teksto langelius rikiuoja iš viršaus į apačią, langelius sugrupuoja į eilutę, kai jie vertikaliai persidengia bent per pusę mažesniojo langelio aukščio, ir kiekvieną eilutę tvarko iš kairės į dešinę arba iš dešinės į kairę, kai įjungta RightToLeft; simboliai kiekvienos atpažintos eilutės viduje niekada neapverčiami. Grupavimas svarbus, nes aptikiklis dažnai vieną vizualinę eilutę skelia į kelis langelius – pavyzdžiui, etiketę ir reikšmę, atskirtas plačiu tarpu – o grynas viršutinės koordinatės rikiavimas juos supintų su gretima eilute, kai tik jų viršūnės skiriasi pikseliu ar dviem

Arabų profilis nustato RightToLeft := True, kas liepia DLL kiekvienos eilutės langelius tvarkyti pagal jų dešinę kraštinę, nuo dešinės paraštės į vidų. Tai visas efektas. Tekstas, kurį modelis grąžina eilutei, jau yra Unicode loginėje tvarkoje – tokioje, kokia arabų skaitytojas jį skaito ir renkasi, – ir būtent tos tvarkos tikisi PDF teksto ištrauka ir paieška. Mechaniškai apversti eilutę, kad debugeryje „atrodytų teisingai“, reikštų sulaužyti paiešką, kopijavimą ir įklijavimą bei ekrano skaitykles. Dvikryptis rodymas ir glifų formavimas – peržiūros programos darbas

Vienas variklis aptarnauja vieną kalbos profilį. Automatinio rašto aptikimo nėra, tad dokumentas, maišantis raštus, reikalauja po variklį profilui, taikomo puslapiams, kurie jį naudoja. Kadangi ApplyLoadedOCRTextLayer priima aiškų puslapių sąrašą ir kiekvieną kvietimą įrašo kaip atskirą viskas-arba-nieko transakciją, tai paprasta

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
  Models: THPDFRapidOCRDLLOptions;
begin
  // kelia EArgumentException dėl nežinomos žymos, dar prieš įkeliant modelius
  Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
  Models.MaxPixels := 33554432;          // vietos A3 puslapiams ties 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 profilis
  Arabic := CreateRapidEngine('ar-SA');   // arabic profilis, 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 eilutė ten ne šiaip. DLL nustatymų numatytoji – 16 777 216 pikselių užklausai, ko užtenka A4 ir US Letter ties 300 DPI su atsarga, bet A3 puslapis ties 300 DPI yra apie 3508 į 4961 pikselius, maždaug 17,4 milijono, ir užklausa atmetama kaip viršijanti biudžetą. Kelkite MaxPixels (lubos – 67 108 864) arba žeminkite THPDFOCRTextLayerOptions.DPI dideliems formatams. Iš dešinės į kairę tvarka naudoja pasirinktinį ABI versijos 1 eksportą HPDFRapidOCRSetReadingDirection; adapteris jo reikalauja tik nustačius RightToLeft, tad senesnė DLL tebeaptarnauja iš kairės į dešinę kalbas, o kuriant variklį žlunga su EArgumentException, įvardijančia dingusį eksportą arabų kalbai

Kodėl naujesni OCR modeliai neįsikelia?

HotPDF RapidOCR DLL susieja statinį ONNX Runtime 1.14, kuris negali skaityti modelių, išsaugotų su ONNX IR versija 10, o naujesni eksportai, kaip PP-OCRv5 modeliai, gali reikalauti naujesnio runtime; toks modelis žlunga kuriant variklį su natyvia diagnostika. Tas apribojimas yra priežastis, kodėl kalbų paketai fiksuoti prie konkrečių PP-OCRv3 ir PP-OCRv4 atpažintojo bei žodyno porų vietoj „naujausios“, ir kodėl aukščiau esanti lentelė maišo abi kartas: kiekviena fiksuotoji pora – tokia, kuri įsikelia ir patvirtinama to runtime sąlygomis

Diegimo įrankis prižiūri poravimą. Kiekvienas manifeste esantis failas neša SHA256 hashą, esamas failas su kitoku hashu sustabdo diegimą vietoj perrašymo, o kiekvienas parsisiuntimas atsirenka po laikinu vardu ir į vietą keliauja tik jam hashui sutapus. Tai saugo nuo tyliosios žodyno problemos versijos: kas nors rankomis įmeta naujesnį recognition.onnx į profilio aplanką, klasių skaičius atsitiktinai sutampa, ir niekas nesugriūva, kol klientas nepraneša, kad paieška neranda žodžių, kuriuos jis aiškiai mato. Veikimo metu adapteris lieka atsijungęs ir dingusio modelio niekada neparsiunčia. Atpažintojas dar patvirtina modelio formą įkeliant – priima NCHW įvedimą su fiksuotu 32 arba 48 pikselių aukščiu arba dinamišku aukščiu, kurį paleidžia ties 48

Jeigu jums reikia rašto, kurio nė vienas iš devynių profilių nedengia, vis tiek galite nukreipti RecognitionModel ir CharacterDictionary į savuosius failus. Tos pačios patikros galioja – ir tai yra esmė: nesutampanti pora žlunga inicializacijos metu, o ne kliento archyve. Puslapiams, kuriems netinka nė vienas RapidOCR profilis, Tesseract adapteris ieškomam PDF įsijungia į tą patį ApplyLoadedOCRTextLayer kvietimą, o mašininio spaudimo ASCII formoms vidinis šablonų atitikimo OCR variklis iš viso apsieina be modelių

Trumpa atmintinė: daugiakalbio RapidOCR sąrašas

  • Kurkite nustatymus su THPDFRapidOCRDLLOptions.ForLanguage ir EArgumentException traktuokite kaip nepalaikomą žymą, o ne runtime gedimą
  • Keiskite RecognitionModel ir CharacterDictionary kartu, niekada vieno atskirai; lygūs klasių skaičiai neįrodo lygios simbolių tvarkos
  • Laikykite žodynus UTF-8 be BOM, niekada nekirpkite įrašų ir tikėkitės modelio su N + 2 klasėmis: blank, N įrašų, tarpas
  • Pasirinktinis CTC dekoduotojas privalo dengti paskutinę klasę ir paskutinį laiko žingsnį ir išlaikyti kartojimus, atskirtus blank
  • Naudokite po variklį kalbos profiliui ir perduokite aiškius puslapių sąrašus kelių raštų dokumentams
  • RightToLeft keičia tik langelių tvarką; atpažintas tekstas lieka Unicode loginėje tvarkoje
  • Įdiekite modelius su Install-RapidOCRModels.ps1, kad SHA256 fiksacijos laikytų modelio ir žodyno poravimą; nustatykite UseAngleClassifier := False, jei diegėte su -SkipClassifier
  • Pakelkite MaxPixels virš 16 777 216 numatytosios prieš paleisdami A3 ar didesnius puslapius ties 300 DPI

RapidOCR kalbų profiliai, natyvus DLL adapteris ir OCR teksto sluoksnio srautas – HotPDF Delphi PDF Component dalis, skirto Delphi, C++Builder ir Windows FPC/Lazarus, nuo v2.775.0 su daugiakalbiais profiliais