Техническа статия

HotPDF: китайски и многоезичен OCR с RapidOCR в Delphi

HotPDF върши китайски и многоезичен OCR в Delphi през своя native RapidOCR DLL adapter: THPDFRapidOCRDLLOptions.ForLanguage съпоставя езиков таг като 'zh-CN', 'zh-TW', 'ru' или 'ar' на съвпадащ модел за разпознаване и символен речник, а THotPDF.ApplyLoadedOCRTextLayer превръща разпознатите редове в невидим, търсим Unicode текстов слой върху сканираните PDF страници

Да накарате демонстрация на латиница да заработи е лесната част. Интересните провали започват, когато преминете към традиционен китайски или руски и изходът се превърне в уверени, добре оформени безсмислици, или когато всеки ред мълчаливо губи последния си символ, или когато арабска страница се върне с текстовите си кутии в грешен ред. Нито едно от тях не вдига изключение само по себе си. Езиковите presets, добавени в HotPDF v2.775.0, съществуват основно да затворят тези пролуки, а четирите капана по-долу си заслужават да ги разберете, дори никога да не пипате native кода, защото всеки обяснява симптом, който иначе можете да гоните цял ден

Как ForLanguage избира модел и речник?

THPDFRapidOCRDLLOptions.ForLanguage решава таг към един от девет профила и връща опции, сочащи <profile>/recognition.onnx и <profile>/dictionary.txt под вашата директория с модели, като пази споделения детектор, опционалния класификатор на ъгъл и дефолтите за нишки, пиксели и timeout от THPDFRapidOCRDLLOptions.Default. Методът превръща тага в малки букви, сменя подчертавките с тирета и отрязва околните интервали, така че 'zh_TW', 'ZH-tw' и ' zh-tw ' всички кацат на същия профил. Псевдонимите са изричен списък, а не префиксно съвпадение: 'zh-Hant-TW' се приема, защото е изброен, докато произволен регионален вариант, който не е изброен, вдига EArgumentException, преди някой модел да се зареди

Решаване на профил в ForLanguage на HotPDF за THPDFRapidOCRDLLOptions: тагове като zh_TW, ZH-tw и zh-TW се нормализират и съпоставят срещу девет изброени профила, всеки закачащ модел за разпознаване и речник, задавани винаги заедно, докато неизброен таг вдига EArgumentException, преди някой модел да се зареди
един таг избира една закачена двойка модел-речник; детекторът, класификаторът и бюджетите остават споделени, а непознат таг проваля бързо вместо да зарича нещо
ПрофилЕзициПримерни таговеЗакачен модел
chОпростен китайски и английскиzh, zh-CN, zh-Hans, chi_simPP-OCRv4
chinese_chtТрадиционен китайскиzh-TW, zh-HK, zh-Hant, chi_traPP-OCRv3
enАнглийскиen, en-US, en-GB, engPP-OCRv4
latinФренски, немски, испански, португалски, италиански, нидерландски, турскиfr, de, es-419, pt-BR, trPP-OCRv3
japanЯпонскиja, ja-JP, jpnPP-OCRv4
koreanКорейскиko, ko-KR, korPP-OCRv4
cyrillicРуски, украински, български, беларускиru, ru-RU, uk, bgPP-OCRv3
arabicАрабски, персийски, урдуar, ar-SA, fa, urPP-OCRv4
devanagariХинди, маратхи, непалскиhi, mr, nePP-OCRv4

Самият adapter никога не тегли нищо. Осигурявате файловете веднъж с вградения помощник, например tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (или -Language All за всичките девет профила), а помощникът поставя споделен детектор и класификатор на коренните имена на файлове, които Default очаква. След това сканиране на опростен китайски става търсимо с няколко реда. Инсталацията на engine-а е същият шев IHPDFOCREngine, описан в статията за in-process RapidOCR DLL-а и ABI границата му, така че тази остава съсредоточена върху езиците

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, споделен детектор и класификатор
  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
    // празен списък страници значи всяка страница; страници с текст вече се прескачат
    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;

Две подробности в този изход заслужават бележка. Native тръбопроводът връща по един резултат на детектиран текстов ред, не на дума, така че AcceptedWordCount брои редове тук, а MinimumConfidence се сравнява със средната увереност на символите на целия ред: ред, средящ 0.45, се изпуска като единица. UniqueScalarCount докладва колко различни Unicode скалара е трябвало текстовият слой да съпостави в своя шрифт и таблица ToUnicode — полезна sanity проверка, че CJK текст наистина е пристигнал вместо шепа Latin резервни варианти. Дръжте интерфейса на engine-а жив между документите, защото инициализацията на моделите става във фабриката и е скъпата стъпка

Защо сменянето само на модела за разпознаване произвежда боклук?

CTC модел за разпознаване никога не извежда символи, а само класови индекси, а речникът е единственото нещо, превръщащо индекс 1,204 в глиф. Разменете ch/recognition.onnx с cyrillic/recognition.onnx, но задръжте китайския речник, и моделът ще излъчи с удоволствие валидни кирилски индекси, които старият речник превежда на случайни Han символи. Резултатът изглежда като текст, минава UTF-8 валидация и е търсим точно за нищо. Затова ForLanguage винаги задава RecognitionModel и CharacterDictionary заедно, и затова ръчно построените опции никога не бива да сменят едното без другото

Очевидната безопасна проверка — сравнението на размера на речника с ширината на изхода на модела — е необходима, но недостатъчна. Два речника могат да имат еднакъв брой записа в различен ред, а разминаване с едно в реда измества всеки символ с една кодова точка. Затова HotPDF проверява на два етапа, когато фабриката инициализира модела. Първо, броят изходни класове трябва да е равен на записите в речника плюс две. Второ, когато ONNX файлът вгражда метаданнов списък character, всеки запис в речника се сравнява с него по ред, а разминаване проваля инициализацията с EInvalidOperation и native диагностика вместо да произведе правдоподобен боклук по-късно

„Плюс две“ идва от подредбата на класовете. Клас 0 е CTC blank, класовете 1 до N са редовете на речника във файловия ред, а финалният клас е интервал. Някои речници носят и собствен запис за интервал, и този ред трябва да се задържи точно какъвто е. Тук добронамереният Trim върши истинска вреда: превръща запис от един интервал в празен низ и измества или чупи таблицата. Единствената безопасна нормализация е махането на завършващ carriage return, така че речник, записан с CRLF краища на редове, се зарежда коректно, докато UTF-8 byte order mark, празен ред или запис, съдържащ табулация, се отхвърля. Скицата по-долу показва подредбата в Pascal; това е обяснителен код, не HotPDF API

Подредба на класовата таблица CTC в HotPDF за RapidOCR речници: клас 0 е blank, класовете 1 до N са редовете на речника във файловия ред с всеки самотен запис за интервал запазен, а финалният клас е интервал, давайки N плюс 2 изходни класа, които фабриката проверява срещу модела, включително метаданните
речникът е единственото, превръщащо класови индекси в символи, затова размерът му, редът и записът за интервал се проверяват, преди една-единствена страница да бъде разпозната
// Само илюстрация: класовата таблица, която CTC разпознавач очаква
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);                                   // нов ред в края на файла
  SetLength(Result, Last + 3);
  Result[0] := '';                               // клас 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: маха само CR
    if (Entry = '') or (Pos(#9, Entry) > 0) then
      raise EArgumentException.Create('Invalid dictionary entry');
    Result[I + 1] := Entry;                      // никога Trim: ' ' е клас
  end;
  Result[Last + 2] := ' ';                       // финален клас: интервал
  // Length(Result) трябва да е равен на броя класове в изхода на модела
end;

Какво всъщност прави greedy CTC декодирането?

Greedy CTC декодирането избира класа с най-висока оценка на всяка времева стъпка, свива последователните повторения в един символ и пуска blank класа; blank-ът е това, което позволява на истински удвоени букви да оцелеят. Модел за разпознаване гледа текстов ред като последователност от тесни вертикални резки и за всяка резка, тоест времева стъпка, извежда вероятност за всеки клас. Ред, съдържащ AA中, може да произведе argmax последователност A A blank A 中 space. Свиването на първите две стъпки A дава една A, blank-ът я отделя от следващата A, а резултатът е AA中 със запазен завършващ интервал. Без правилото за blank, book и bok биха били неразличими

Разходка из GreedyCTCDecode в HotPDF: шест времеви стъпки гласуват argmax класове A, A, blank, A, Han символ и интервал, последователните повторения се свиват, blank-ът нулира пазача на повторения така че истински удвоена буква оцелява, а три гранични бъга мълчаливо губят интервала между думите, последния символ или удвоените символи
декодерът е десетина реда и всяка граница има значение: включете последния клас, включете последната стъпка и оставете само blank да отделя повторения

Понеже декодерът е само десетина реда, лесно е границите да се объркат, а провалите са мълчаливи. Ако вътрешният argmax цикъл спре един клас по-рано, класът интервал никога не може да спечели и всеки ред се връща без интервали между думите, което разбива търсенето на фрази на английски и латински страници. Ако външният цикъл спре една времева стъпка по-рано, последният символ на всеки ред изчезва, което за кратък ред може да е третина от текста. А ако пазачът на повторения не се нулира от blank, удвоени символи като ll или китайски редупликации като 谢谢 се свиват в един. Декодерът на HotPDF включва последния клас и последната времева стъпка, пази повторения, разделени от blank, и допълнително отхвърля оценки, които не са крайни или падат извън 0 до 1, и всеки брой класове, който не съвпада с речника. Ето същата логика като Pascal илюстрация

// Само илюстрация: greedy CTC декодиране с верни граници.
// Scores държи Steps * Classes вероятности, по един ред на времева стъпка
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;                            // клас 0 е CTC blank
  for Step := 0 to Steps - 1 do             // включете последната времева стъпка
  begin
    Best := 0;
    BestScore := Scores[Step * Classes];
    for C := 1 to Classes - 1 do            // включете последния клас (интервал)
      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 нулира пазача на повторения
  end;
end;

Greedy декодирането не е най-точната налична CTC стратегия; beam search с езиков модел може да оправи някои двусмислени резки. За печатни документи при 300 DPI greedy резултатът обикновено е всичко, което моделът има да предложи, а декодерът не е мястото да компенсира слабостите на модела. Latin PP-OCRv3 моделът например може да прочете ñ като n дори на чист вход. HotPDF не замазва това с пост-обработващи замени на символи, защото таблица за замяна, оправяща испанския, чупи нещо друго, а грешен символ в търсим слой е по-лош от честен пропуск

Как HotPDF подрежда текстови редове, включително арабски отдясно наляво?

HotPDF сортира детектираните текстови кутии отгоре надолу, групира кутии в ред, когато се застъпват вертикално с поне половината от височината на по-малката кутия, и подрежда всеки ред отляво надясно, или отдясно наляво, когато RightToLeft е включен; символите вътре във всеки разпознат ред никога не се обръщат. Групирането има значение, защото детектор често разпъква една визуална линия на няколко кутии, например етикет и стойност, разделени с широка пролука, а чисто сортиране по горна координата би ги преплело със съседната линия, щом горните им координати се разминават с пиксел или два

Арабският preset задава RightToLeft := True, което казва на DLL-а да подреди кутиите във всеки ред по десния им ръб, от десното поле навътре. Това е целият ефект. Текстът, който моделът връща за ред, вече е в Unicode логически ред — реда, в който арабски читател го чете и пише — и това е и редът, който извличането на PDF текст и търсенето очакват. Механично обръщане на низа, за да изглежда „правилно“ в дебъгер, би счупило търсенето, копирането и поставянето и екранните четци. Двупосочното показване и оформянето на глифи са работа на прегледача

Един engine обслужва един езиков профил. Няма автоматична детекция на писменост, така че документ, смесващ писмености, се нуждае от по един engine на профил, прилаган на страниците, които го ползват. Понеже ApplyLoadedOCRTextLayer приема изричен списък страници и записва всеки разговор като собствена транзакция всичко-или-нищо, това се прави направо

uses
  SysUtils, HPDFDoc, HPDFRapidOCRRecognition;

function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
  Models: THPDFRapidOCRDLLOptions;
begin
  // вдига EArgumentException за непознат таг, преди някой модел да се зареди
  Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
  Models.MaxPixels := 33554432;          // място за A3 страници при 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
  Arabic := CreateRapidEngine('ar-SA');   // профил 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;

Редът MaxPixels е там с причина. Опциите на DLL-а са по подразбиране 16,777,216 пиксела на заявка, което покрива A4 и US Letter при 300 DPI спокойно, но A3 страница при 300 DPI е около 3508 на 4961 пиксела, около 17.4 милиона, и заявката се отказва като над бюджета. Вдигнете MaxPixels (таванът е 67,108,864) или снизете THPDFOCRTextLayerOptions.DPI за големи формати. Подредбата отдясно наляво ползва опционалния export HPDFRapidOCRSetReadingDirection на ABI версия 1; adapter-ът го изисква само когато RightToLeft е зададен, така че по-стар DLL продължава да обслужва езици отляво надясно и проваля създаването на engine с EArgumentException, назоваващ липсващия export за арабски

Защо по-новите OCR модели провалят зареждането?

HotPDF RapidOCR DLL-ът линква статичен ONNX Runtime 1.14, който не може да чете модели, записани с ONNX IR версия 10, а по-нови export-и като моделите PP-OCRv5 могат да изискват по-нова среда от тази; такъв модел проваля създаването на engine с native диагностика. Това ограничение е причината езиковите пакети да са закачени за конкретни двойки разпознавач и речник PP-OCRv3 и PP-OCRv4 вместо за „най-новото“, и защо таблицата по-горе смесва двете поколения: всяка закачена двойка е такава, която се зарежда и проверява под тази среда

Инсталаторът налага двойката. Всеки файл в манифеста му носи SHA256 hash, съществуващ файл с различен hash спира инсталацията вместо да бъде презаписан, а всяко теглене каца под временно име и се мести на място само след като hash-ът му съвпадне. Това предпазва от тихата версия на проблема с речника: някой хвърля по-нов recognition.onnx в папка на профил на ръка, броят класове случайно съвпада, и нищо не се проваля, докато клиент не докладва, че търсенето не намира думи, които вижда съвсем ясно. По време на работа adapter-ът остава офлайн и никога не тегли липсващ модел. Разпознавачът също валидира формата на модела при зареждане, приемайки NCHW вход с фиксирана височина 32 или 48 пиксела или динамична височина, която пуска при 48

Ако ви трябва писменост, която нито един от деветте профила не покрива, пак можете да насочите RecognitionModel и CharacterDictionary към собствени файлове. Същите проверки важат, което е смисълът: размината двойка проваля при инициализация, не в архива на вашия клиент. За страници, където нито един RapidOCR профил не пасва, Tesseract adapter-ът за търсим PDF се включва в същия разговор ApplyLoadedOCRTextLayer, а за машиноотпечатани ASCII формуляри вграденият engine за разпознаване по шаблон не се нуждае изобщо от модели

Бърза справка: многоезичен контрольен списък за RapidOCR

  • Създавайте опции с THPDFRapidOCRDLLOptions.ForLanguage и третирайте EArgumentException като неподдържан таг, не като runtime повреда
  • Сменяйте RecognitionModel и CharacterDictionary заедно, никога само едното; равни бройки класове не доказват равен символен ред
  • Дръжте речниците като UTF-8 без BOM, никога не отрязвайте записа и очаквайте моделът да има N + 2 класа: blank, N записа, интервал
  • Персонален CTC декодер трябва да покрива последния клас и последната времева стъпка и да пази повторения, отделени от blank
  • Ползвайте по един engine на езиков профил и подавайте изрични списъци страници за документи със смесени писмености
  • RightToLeft сменя само реда на кутиите; разпознатият текст остава в Unicode логически ред
  • Инсталирайте моделите с Install-RapidOCRModels.ps1, така че SHA256 закачките да пазят двойката модел-речник; задайте UseAngleClassifier := False, ако сте инсталирали с -SkipClassifier
  • Вдигнете MaxPixels над дефолта 16,777,216, преди да пуснете A3 или по-големи страници при 300 DPI

Езиковите presets на RapidOCR, native DLL adapter-ът и тръбопроводът на OCR текстовия слой са част от HotPDF Delphi PDF Component за Delphi, C++Builder и Windows FPC/Lazarus, започвайки с v2.775.0 за многоезичните профили