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

HotPDF in-process RapidOCR: native DLL OCR в Delphi

HotPDF прави сканирани PDF страници търсими с in-process RapidOCR чрез HPDFCreateRapidOCRDLLOCREngine — фабрика, добавена във v2.774.0, която зарежда HotPDFRapidOCR.dll, държи ONNX моделите за детекция, класификация на ъгъл и разпознаване резидентни в паметта и връща IHPDFOCREngine. Подавате този engine на THotPDF.ApplyLoadedOCRTextLayer, който рендира всяка страница, върши CPU inference без Python и без дочерен процес и записва невидим Unicode текстов слой

Мотивацията е цената на страница. По-рано доставеният process adapter на RapidOCR, HPDFCreateRapidOCREngine, стартира Python worker при всеки разговор към Recognize, а този worker импортира своята среда и зарича ONNX моделите си, преди да прочете и един пиксел. Върху архив от 500 страници този стартов данък се повтаря 500 пъти, а разгръщането значи да доставиш Python среда до Delphi изпълнимия файл. Native DLL-ът зарича моделите веднъж, когато създадете engine-а, а разгръщането се свива до DLL, файловете му с модели и речник с символи. Това, което отдавате в замяна, е възможността да убиете заклещен разпознавач, и по-голямата част от инженерството в този adapter е как да живеете с това честно

Как правите сканиран PDF търсим с RapidOCR DLL-а?

Създаването на търсим PDF с native RapidOCR DLL-а струва едно фабрично извикване и същия разговор ApplyLoadedOCRTextLayer, който всеки HotPDF OCR engine ползва. Фабриката живее в unit-а HPDFRapidOCRRecognition и валидира предварително: DLL-ът и директорията с модели трябва да съществуват, всеки файл с модел и речник трябва да се намери, ABI версията трябва да е 1, а всички задължителни export-и трябва да са налице, преди някой модел да е инициализиран. Грешки в конфигурацията вдигат EArgumentException; модел, който се проваля да се зареди, вдига EInvalidOperation с диагностичния текст, който DLL-ът е написал

Последователност на фабричната валидация в HotPDF RapidOCR DLL за HPDFCreateRapidOCRDLLOCREngine: пътищата и файловете с модели трябва да съществуват, HPDFRapidOCRAbiVersion трябва да върне 1, задължителните export-и трябва да се намерят, а HPDFRapidOCRCreate трябва да инициализира моделите, като EArgumentException или EInvalidOperation се вдигат предварително, преди някое разпознаване да тръгне, а второто носи native диагностичния текст
валидацията е предварителна нарочно: проблеми с конфигурацията вдигат грешка, преди някой модел да се инициализира, така че лош път или ABI никога не стига до краен срок на разпознаване
uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFRapidOCRRecognition;

procedure MakeSearchable(const SourceFile, TargetFile: string);
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  // Моделите се заричат тук, извън всеки краен срок на разпознаване.
  // Относителните имена на модели в THPDFRapidOCRDLLOptions.Default се
  // решават спрямо директорията с модели.
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models');
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    if Doc.LoadFromFile(SourceFile) < 1 then
      raise Exception.Create('Cannot load ' + SourceFile);
    Options := THPDFOCRTextLayerOptions.Default;  // 300 DPI, MinimumConfidence 0.5
    // празен списък страници значи всяка страница; страници с текст вече се прескачат
    if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
      raise Exception.Create(string(Info.Diagnostic));
    Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
      ' lines accepted, ', Info.DroppedWordCount, ' dropped');
    Doc.SaveLoadedDocument(TargetFile);
  finally
    Doc.Free;
  end;
end;

THPDFRapidOCRDLLOptions.Default назова ch_PP-OCRv3_det_infer.onnx, ch_PP-OCRv3_rec_infer.onnx, ch_ppocr_mobile_v2.0_cls_infer.onnx и ppocr_keys_v1.txt, с една CPU нишка, входен лимит от 16,777,216 пиксела и краен срок за разпознаване от 60,000 ms. От v2.775.0 THPDFRapidOCRDLLOptions.ForLanguage сменя със съответстващ модел за разпознаване и речник за традиционен китайски, руски, японски, арабски и други профили; защо моделът и речникът трябва да се сменят заедно е разгледано в многоезичните модели RapidOCR и CTC речниците в HotPDF. Engine-ът се представя като RapidOCR (native DLL) в Info.EngineName, което държи логовете еднозначни до външния process adapter на Tesseract OCR и вградения engine за разпознаване по шаблон

Защо C ABI-ят говори само int32_t и UTF-8 байтове?

ABI-ят на HotPDFRapidOCR.dll ползва само цели числа с фиксирана ширина, сурови указатели и изрични дължини в байтове, защото Delphi, C++Builder и Free Pascal не споделят нищо с MSVC освен C конвенцията за извикване. std::string, std::vector или C++ изключение имат подредба и модел на размотаване, принадлежащи на един компилатор и една runtime библиотека. Оставите едно от тях да пресече границата и провалът е развален стек или heap блок, освободен от грешния алокатор, а не чиста грешка

ABI версия 1 затова следва кратък списък правила. Всеки export е cdecl и връща статус int32_t, където 1 значи успех, а 0 — провал. Всяка функция, която може да провали, приема буфер за диагностика, собственост на извикващия, и капацитета му в байтове; DLL-ът пише NUL-терминирано UTF-8 съобщение, съкратено да се събере, а adapter-ът го декодира с твърд терминатор в последния байт на собствения си буфер от 4,096 байта. Тялото на всеки export е опаковано в try и с catch (const std::exception &), и с catch (...), така че грешка от ONNX Runtime, assertion от OpenCV или невалиден речник става статус 0 плюс текст, никога изключение, избягало в Pascal код

ExportРоляКога adapter-ът го решава
HPDFRapidOCRAbiVersionВръща 1; всяка друга стойност се отхвърляПърво, преди всичко друго
HPDFRapidOCRCreateЗарича моделите за детекция, опционалната класификация, разпознаването и речникаВъв фабриката
HPDFRapidOCRRecognizeПуска един bitmap и излъчва по един callback на текстов редВъв фабриката
HPDFRapidOCRDestroyОсвобождава инстанцията на моделаВъв фабриката
HPDFRapidOCRSetReadingDirectionОпционален ред на редовете отдясно наляво, добавен във v2.775.0Само когато RightToLeft е зададен

Опционалният export се решава мърляво нарочно: DLL от v2.774.0, в който го няма, все още обслужва заявки отляво надясно. DLL-ът се зарича с LoadLibraryEx със search флагове, покриващи собствената му папка плюс директориите по подразбиране за безопасно търсене, така че зависимости от ONNX Runtime или OpenCV, сложени до HotPDFRapidOCR.dll, се намират без пипане по PATH. Пътищата към модели и речник пътуват като UTF-8 и DLL-ът ги конвертира с MultiByteToWideChar в строг режим, преди да отвори файлове през wide-character API, така че директория с модели под китайско или кирилско потребителско име работи, вместо да се разшири байт по байт до безсмислици

Едно правило живее в билда, а не в header-а. DLL-ът статично линква ONNX Runtime и OpenCV, а CMake конфигурацията по подразбиране ползва статичния release CRT (/MT). Статични библиотеки, компилирани срещу /MD, смесени в /MT DLL, дават в най-добрия случай link грешки, а в най-лошия — два независими heap-а, така че доставените библиотеки трябва да съвпадат с CRT режима, който DLL-ът ползва

Какво става между TBitmap и текстов ред?

HotPDF подава на DLL-а независим top-down BGR snapshot на рендираната страница, а DLL-ът връща обратно по един callback на разпознат текстов ред с зает UTF-8 текст, който adapter-ът трябва да копира, преди да се върне

В Delphi adapter-ът задава bitmap-а на страницата на частен TBitmap, принуждава pf24bit и чете редовете с GetDIBits при отрицателен biHeight, което дава top-down редове, подплатени до подравняване на четири байта; този stride се подава изрично. Във FPC чете през CreateIntfImage, защото scanline записите на LCL могат да обновят суровото изображение, без да обновят GDI манипулатора. Bitmap-ът на извикващия никога не се модифицира, а пикселният бюджет (MaxPixels, 16,777,216 по подразбиране и конфигурируем до 67,108,864) и лимитът от 32,767 пиксела на размерност се проверяват, преди буферът за snapshot да бъде заделен

Тръбопровод на HotPDF RapidOCR DLL от bitmap до текстов слой: adapter-ът снима страницата като top-down pf24bit BGR, DLL-ът подплатя, детектира, подрежда и разпознава изрязъци, доставя по един callback на ред със зает UTF-8 текст, кутия и увереност, а adapter-ът валидира всеки ред преди записването на текстовия слой
пикселите пресичат ABI-я веднъж като snapshot, редовете се връщат по един callback наведнъж, и нищо не стига търсимия слой, докато всяка проверка не мине

Вътре в DLL-а snapshot-ът се подплатя с 50 бели пиксела, текстовите области се детектират с максимум страна от 1,024 пиксела, кутиите се подреждат в хоризонтални редове, а всеки изрязък се върти опционално от класификатора на ъгъл преди разпознаването. Всеки текстов ред после минава през callback, получаващ const char*, брой байтове, целочислена кутия в пиксели на оригиналното изображение и средна увереност на символите. Указателят към текста е валиден само по време на callback, така че adapter-ът го копира веднага и е строг към това какво приема:

  • UTF-8 се декодира с MB_ERR_INVALID_CHARS; развала последователност проваля страницата вместо да произведе заменящи символи в търсим слой
  • C0 и C1 контролни символи се отхвърлят, а редове само с интервали се прескачат
  • Кутията трябва да лежи вътре в bitmap-а, а увереността да е крайна стойност от 0 до 1
  • Текстът се брои спрямо MaxTextCodeUnits на заявката с твърд таван от 1,048,576 UTF-16 единици на разговор, а символи от допълнителната равнина струват две единици
  • Всяко Pascal изключение вътре в callback се хваща там, съхранява и се превръща в връщане на 0, което кара DLL-а да спре и да докладва провал; съхраненото съобщение после става диагностиката

Две последици имат значение за настройката. Първо, единицата изход е ред, не дума: всеки ред изразходва един слот MaxWords, Info.AcceptedWordCount и Info.DroppedWordCount броят редове, а оцветяването при търсене обхваща кутията на реда. Второ, MinimumConfidence (0.5 по подразбиране) се сравнява със средната увереност на символите на реда, така че ред с един нечетлив символ сред двадесет чисти обикновено оцелява. DLL-ът не доставя базова линия, така че тръбопроводът на текстовия слой я оценява от кутията. Празна страница успява с нула реда, а всеки провал изчиства частичните резултати, така че записът на много страници остава всичко-или-нищо

Собственост на моделите и нишкова безопасност

Всеки RapidOCR DLL engine притежава точно една инстанция на модела през целия си живот, а разговорите към Recognize върху този engine се сериализират от critical section. Държането на интерфейса IHPDFOCREngine е това, което държи моделите топли, така че верният образец за партидна работа е да създадете engine-а веднъж и да го преизползвате между документи

procedure OcrBatch(const Files: TStrings; const OutputDir: string);
var
  Models: THPDFRapidOCRDLLOptions;
  Engine: IHPDFOCREngine;
  Doc: THotPDF;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
  I: Integer;
begin
  Models := THPDFRapidOCRDLLOptions.Default;
  Models.UseAngleClassifier := False;    // прави сканирания: без модел-класификатор
  Models.Threads := 4;                   // 1..64, ограничено до броя логически процесори
  Models.TimeoutMilliseconds := 120000;  // на разговор Recognize, кооперативно
  Engine := HPDFCreateRapidOCRDLLOCREngine(
    'C:\OCR\Win64\HotPDFRapidOCR.dll', 'C:\OCR\models', Models);
  Options := THPDFOCRTextLayerOptions.Default;
  for I := 0 to Files.Count - 1 do
  begin
    Doc := THotPDF.Create(nil);
    try
      Doc.AutoLaunch := False;
      if (Doc.LoadFromFile(Files[I]) > 0) and
        Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
        Doc.SaveLoadedDocument(IncludeTrailingPathDelimiter(OutputDir) +
          ExtractFileName(Files[I]))
      else
        Writeln(Files[I], ': ', string(Info.Diagnostic));
    finally
      Doc.Free;
    end;
  end;
end;  // последната отправка освободена: моделите унищожени, после DLL-ът се разтоварва

Стойността Threads задава и броя intra-op, и броя inter-op нишки на всеки ONNX session, а DLL-ът я притиска към броя активни процесори. Две нишки, споделящи един engine, не вървят паралелно; втората чака заключването. Това чакане не е сляпо EnterCriticalSection: adapter-ът вика TryEnterCriticalSection на всеки 25 ms и проверява токена за отмяна и крайния срок между опитите, така че заявка в опашка пак може да бъде отменена или да изтече. Ако ви трябва истински паралелизъм, създайте по един engine на работник и приемете, че всеки engine държи собствено копие на моделите в паметта

Редът на разглобяване е фиксиран от деструктора на engine-а: HPDFRapidOCRDestroy освобождава първо инстанцията на модела, после FreeLibrary разтоварва DLL-а. От native страната инициализацията на модела е също толкова внимателна; когато моделът за разпознаване се провали след като session-ите на детектора и класификатора вече са построени, тези session-и се освобождават, преди грешката да бъде докладвана, а броят класове в речника се проверява спрямо изхода на модела при инициализация, а не на първата страница

Защо native OCR разговор не може да бъде убит по средата на inference?

Native RapidOCR разговор не може да бъде убит по средата на inference, защото върви на вашата нишка, във вашия процес, по средата на ONNX Runtime session, който не приема прекъсване. Отмяната в DLL adapter-а на HotPDF затова е кооперативна: DLL-ът вика abort callback преди и след детекцията, след класификацията и след всеки разпознат ред и спира на първата контролна точка, където callback върне 0. Единично ONNX Run, който е тръгнал, ще си докара края първо

Алтернативите са по-лоши от чакането. TerminateThread би оставил заключването на CRT heap-а, нишковия пул на ONNX Runtime и всяко състояние на OpenCV в каквото и да е били в момента, отравяйки останалата част от процеса. FreeLibrary, докато разговор все още се изпълнява, разтоварва код, който е на стека. Нито едното не може да бъде направено безопасно, така че adapter-ът никога не опитва. Крайният срок в TimeoutMilliseconds е затова кооперативен краен срок, и изтекъл срок излиза като грешка на engine с диагностика за просрочие, а отменен токен излиза като otlsCancelled:

// Токенът се създава от извикващия и се споделя с UI нишката,
// която вика Token.Cancel, когато потребителят натисне Стоп
Options := THPDFOCRTextLayerOptions.Default;
Options.CancellationToken := Token;
if not Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
  case Info.Status of
    otlsCancelled:
      // връща се на следващата граница на етап или ред; документът непроменен
      Writeln('Cancelled');
    otlsEngineError:
      // включва изтекъл кооперативен краен срок и native диагностиката
      Writeln('Engine: ', string(Info.Diagnostic));
    otlsBudgetExceeded:
      Writeln('Budget: ', string(Info.Diagnostic));
  else
    Writeln(string(Info.Diagnostic));
  end;

Това е основният компромис между process adapter-ите на HotPDF и in-process DLL-а, и нито една страна не печели на всеки ред:

Компромиси на OCR adapter-ите в HotPDF: process adapter-ите стартират работник и заричат модели на всяка страница, но могат да бъдат убити и съдържат сривове, докато in-process RapidOCR DLL зарича моделите веднъж, спира само на кооперативни контролни точки, споделя адресното пространство и се разгръща като DLL с моделите и речника си
избирайте според натоварването: настолно приложение страница по страница печели от топлия DLL, а сървър, поглъщащ недоверени сканирания, трябва да плати за стената на процеса
  • Стартова цена: Tesseract и Python RapidOCR adapter-ите пускат процес и заричат модели за всяка страница; DLL-ът зарича моделите веднъж на engine
  • Сприряване: дочерен процес може да бъде прекратен изцяло, а Python worker-ът върви в kill-on-close Job Object, така че цялото му процесно дърво тръгва с него; DLL-ът може да спре само на граници на етап и ред
  • Ограничаване на повреди: срив в tesseract.exe проваля една страница; access violation вътре в DLL-а сваля вашия процес
  • Разгръщане: process adapter-ите се нуждаят от инсталирана програма или Python среда; DLL-ът се нуждае от себе си, моделите и речника си, съвпадащи с битовостта на приложението
  • Памет: process adapter-ите освобождават всичко, когато детето излезе; DLL engine пази моделите резидентни, докато последната отправка към интерфейса не бъде освободена

За интерактивно настолно приложение, което прави OCR страница по страница, отзивчивостта на DLL-а обикновено печели. За сървър, поглъщащ недоверени сканирания денонощно, границата на процеса си струва стартовата си цена

Билдване и разгръщане на HotPDFRapidOCR.dll

HotPDFRapidOCR.dll се билдва от C++ източниците в Native/RapidOCR с MSVC, C++17, Windows SDK и CMake 3.20 или по-нова, чрез помощен скрипт, приемащ директориите с native network източници, ONNX Runtime и OpenCV плюс платформа Win32 или Win64. Билдвайте и двете, ако доставяте и двете, защото 32-битово Delphi приложение не може да зарича 64-битов DLL, а статичните библиотеки, които доставяте, трябва да съвпадат с целевата архитектура и с CRT режима

Страната на моделите има собствени граници на съвместимост. Детекторът е DB text detector; разпознавачът приема CTC модели в NCHW подредба с фиксирана входна височина 32 или 48 и ползва 48 за модели с динамична височина. Вграденият статичен ONNX Runtime не може да зарича модели, записани с по-нова IR версия, така че скорошни export-и на PP-OCRv5 провалят инициализацията с диагностика вместо да се заричат частично. Речникът трябва да е UTF-8 без BOM, в точно символната подредба на модела, а броят му класове трябва да съвпада с изхода на модела; CRLF краища на редове се приемат. Разпознаването е офлайн: DLL-ът никога не тегли липсващ модел

Бърза справка

  • Фабрика: HPDFCreateRapidOCRDLLOCREngine(LibraryPath, ModelDirectory[, Options]) в HPDFRapidOCRRecognition, налична от v2.774.0 в Delphi, C++Builder и Windows FPC/Lazarus билдове
  • Дръжте върнатия IHPDFOCREngine жив между страници и документи; освобождаването му унищожава моделите и разтоварва DLL-а
  • Един engine върши едно разпознаване наведнъж; създайте няколко engine-а за паралелни работници и разчетете памет за всяко копие на моделите
  • Изходът е по един запис на текстов ред със средна увереност на символите, филтриран по THPDFOCRTextLayerOptions.MinimumConfidence
  • Отмяната и TimeoutMilliseconds са кооперативни; започнало ONNX изпълнение винаги се докарва до край
  • Съобразете битовостта на DLL-а с приложението и CRT режима на статичните библиотеки ONNX Runtime и OpenCV с DLL-а
  • Изберете езиков профил на engine с THPDFRapidOCRDLLOptions.ForLanguage (v2.775.0); един engine не разпознава езици сам

Native RapidOCR adapter-ът, OCR adapter-ите на базата на процеси, рендерерът на страници, който ги храни, и писачката на невидим Unicode текстов слой идват заедно в HotPDF — native VCL PDF компонент за Delphi и C++Builder. Ако приложението ви за улавяне или архивиране на документи се нуждае от търсим изход без Python среда на целевата машина, HotPDF Delphi PDF компонент доставя целия тръбопровод, като остава да разгърнете само DLL-а и моделите му