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, преди някой модел да се зареди
| Профил | Езици | Примерни тагове | Закачен модел |
|---|---|---|---|
ch | Опростен китайски и английски | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | Традиционен китайски | zh-TW, zh-HK, zh-Hant, chi_tra | PP-OCRv3 |
en | Английски | en, en-US, en-GB, eng | PP-OCRv4 |
latin | Френски, немски, испански, португалски, италиански, нидерландски, турски | fr, de, es-419, pt-BR, tr | PP-OCRv3 |
japan | Японски | ja, ja-JP, jpn | PP-OCRv4 |
korean | Корейски | ko, ko-KR, kor | PP-OCRv4 |
cyrillic | Руски, украински, български, беларуски | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Арабски, персийски, урду | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Хинди, маратхи, непалски | hi, mr, ne | PP-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 разпознавач очаква
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 биха били неразличими
Понеже декодерът е само десетина реда, лесно е границите да се объркат, а провалите са мълчаливи. Ако вътрешният 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 за многоезичните профили