HotPDF OCR چینی و چندزبانه را در Delphi از طریق آداپتور DLL بومی یعنی RapidOCR انجام میدهد: THPDFRapidOCRDLLOptions.ForLanguage یک تگ زبانی مثل 'zh-CN' و 'zh-TW' و 'ru' یا 'ar' را به یک مدل بازشناسی و dictionary نویسههای منطبق نگاشت میکند و THotPDF.ApplyLoadedOCRTextLayer خطهای بازشناسیشده را به یک لایهٔ متنی Unicode نامرئی و قابلجستوجو روی صفحات PDF اسکنشده تبدیل میکند
راه انداختن یک دمو با خط لاتین بخش آسان است. خرابیهای جالب وقتی شروع میشوند که به چینی سنتی یا روسی سوییچ کنی و خروجی به یک آشفتگی خوشفرم و مطمئن تبدیل شود، یا وقتی هر خط بیسروصدا آخرین نویسهاش را گم میکند، یا وقتی یک صفحهٔ عربی با جعبههای متنیاش در ترتیب اشتباه برمیگردد. هیچکدام از اینها به خودی خود exception بالا نمیدهند. پیشتنظیمهای زبانی اضافهشده در HotPDF v2.775.0 عمدتاً برای بستن همین شکافها هستند و چهار تلهٔ پایین حتی اگر هرگز کد بومی را لمس نکنی ارزش فهمیدن دارند، چون هر کدام علائمی را توضیح میدهد که وگرنه ممکن بود یک روز دنبالش بدوی
ForLanguage چطور یک مدل و dictionary انتخاب میکند؟
THPDFRapidOCRDLLOptions.ForLanguage یک تگ را به یکی از نه پروفایل حل میکند و آپشنهایی برمیگرداند که به <profile>/recognition.onnx و <profile>/dictionary.txt زیر پوشهٔ مدل تو اشاره میکنند، در حالی که detector مشترک و دستهبند زاویهٔ اختیاری و پیشفرضهای thread و پیکسل و timeout از THPDFRapidOCRDLLOptions.Default نگه داشته میشوند. متد تگ را کوچکحرف میکند، زیرخطها را به خط تیره تبدیل میکند و فاصلههای دورش را میتراشد، پس 'zh_TW' و 'ZH-tw' و ' zh-tw ' همه روی یک پروفایل فرود میآیند. نامهای مستعار یک فهرست صریحاند نه تطبیق پیشوند: 'zh-Hant-TW' پذیرفته میشود چون در فهرست است، در حالی که یک واریانت منطقهای دلخواه که در فهرست نیست قبل از load شدن هر مدلی 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 |
آداپتور خودش هرگز چیزی دانلود نمیکند. تو فایلها را یک بار با هلپر همراهشده فراهم میکنی، مثلاً tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (یا -Language All برای هر نه پروفایل)، و هلپر یک detector و دستهبند مشترک در نام فایلهای ریشهای که Default انتظار دارد میگذارد. بعد از آن یک اسکن چینی سادهشده با چند خط قابلجستوجو میشود. لولهکشی موتور همان درز IHPDFOCREngine است که در مقالهٔ DLL درونفرایندی RapidOCR و مرز 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، detector و دستهبند مشترک
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
// یک فهرست صفحهٔ خالی یعنی همهٔ صفحات؛ صفحاتی که از قبل متن دارند skip میشوند
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;
دو جزئیات در آن خروجی لایق یک نکتهاند. خط لولهٔ بومی بهازای هر خط متن تشخیصدادهشده یک نتیجه برمیگرداند نه بهازای هر کلمه، پس AcceptedWordCount اینجا خط میشمارد و MinimumConfidence با میانگین اطمینان نویسههای کل خط مقایسه میشود: خطی با میانگین 0.45 بهعنوان یک واحد انداخته میشود. UniqueScalarCount گزارش میکند لایهٔ متن چند scalar یونیکد متمایز را مجبور شده به فونت و جدول ToUnicode اش نگاشت کند، یک چک عقلانی مفید که متن CJK واقعاً رسیده نه یک مشت fallback لاتین. interface موتور را در طول اسناد زنده نگه دار، چون مقداردهی مدل داخل factory اتفاق میافتد و همان گام پرهزینه است
چرا عوض کردن فقط مدل بازشناسی آشغال تولید میکند؟
یک مدل بازشناسی CTC هرگز نویسه خروجی نمیدهد، فقط اندیس کلاس، و dictionary تنها چیزی است که اندیس 1,204 را به یک گلیف تبدیل میکند. ch/recognition.onnx را با cyrillic/recognition.onnx عوض کن اما dictionary چینی را نگه دار، و مدل با خوشحالی اندیسهای سیریلیک معتبر تولید میکند که dictionary قدیمی به نویسههای هان تصادفی ترجمهشان میکند. نتیجه شبیه متن به نظر میرسد، اعتبارسنجی UTF-8 را رد میکند، و برای دقیقاً هیچ چیز قابلجستوجو است. برای همین ForLanguage همیشه RecognitionModel و CharacterDictionary را با هم ست میکند، و آپشنهای دستساز هرگز نباید یکی را بدون دیگری عوض کنند
چک امنیتی واضح، مقایسهٔ اندازهٔ dictionary با عرض خروجی مدل، لازم است اما کافی نیست. دو dictionary میتوانند تعداد مدخل برابر در ترتیب متفاوت داشته باشند و یک خطای off-by-one در ترتیب هر نویسه را یک code point جابهجا میکند. برای همین HotPDF موقع مقداردهی مدل توسط factory در دو مرحله چک میکند. اول، شمارش کلاس خروجی باید برابر مدخلهای dictionary بهعلاوهٔ دو باشد. دوم، وقتی فایل ONNX یک فهرست متادیتای character جاسازی کرده، هر مدخل dictionary بهترتیب با آن مقایسه میشود و ناهمخوانی مقداردهی را با EInvalidOperation و یک تشخیص بومی شکست میدهد بهجای تولید آشفتگی محتملنما بعداً
«بهعلاوهٔ دو» از چیدمان کلاس میآید. کلاس 0 بلیک CTC است، کلاسهای 1 تا N خطهای dictionary بهترتیب فایلاند، و کلاس آخر یک فاصله است. بعضی dictionaryها مدخل فاصلهٔ خودشان را هم حمل میکنند و آن خط باید دقیقاً همانطور که هست نگه داشته شود. اینجا است که یک Trim خیرخواهانه خرابی واقعی میکند: یک مدخل تکفاصله را به رشتهٔ خالی تبدیل و جدول را جابهجا یا میشکند. تنها نرمالسازی امن حذف یک carriage return انتهایی است، پس dictionary ذخیرهشده با پایان خطهای CRLF درست load میشود، در حالی که یک علامت ترتیب بایت UTF-8 یا یک خط خالی یا مدخلی حاوی tab رد میشود. طرح زیر چیدمان را در Pascal نشان میدهد؛ کد توضیحی است، نه یک API در HotPDF
// فقط برای توضیح: جدول کلاسی که یک بازشناس 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
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;
decode کردن حریصانهٔ CTC در واقع چه میکند؟
decode کردن حریصانهٔ CTC در هر گام زمانی کلاس بالاترین امتیاز را برمیدارد، تکرارهای متوالی را به یک نویسه فرومیریزد و کلاس بلیک را میاندازد؛ بلیک همان چیزی است که اجازه میدهد حروف واقعاً دوتایی جان سالم به در ببرند. یک مدل بازشناسی یک خط متن را بهشکل دنبالهای از برشهای عمودی باریک میبیند و برای هر برش، یا گام زمانی، برای هر کلاس یک احتمال خروجی میدهد. خطی حاوی AA中 ممکن است توالی argmax یعنی A A blank A 中 space تولید کند. فرو ریختن دو گام اول A یک A میدهد، بلیک آن را از A بعدی جدا میکند و نتیجه AA中 میشود با فاصلهٔ انتهایی سالم. بدون قاعدهٔ بلیک، book و bok قابلتفکیک نبودند
چون decoder فقط یک دوجن خط است، مرزها راحت اشتباه میشوند و خرابیها بیصدایند. اگر حلقهٔ داخلی argmax یک کلاس مانده به آخر بایستد، کلاس فاصله هرگز نمیتواند برنده شود و هر خط بدون فاصلهگذاری کلمه برمیگردد که جستوجوی عبارت را روی صفحات انگلیسی و لاتین نابود میکند. اگر حلقهٔ بیرونی یک گام زمانی مانده به آخر بایستد، آخرین نویسهٔ هر خط محو میشود که برای یک خط کوتاه میشود یکسوم متن. و اگر نگهبان تکرار توسط بلیک ریست نشود، نویسههای دوتایی مثل ll یا تکرارهای چینی مثل 谢谢 یکی میشوند. decoder در HotPDF کلاس آخر و گام زمانی آخر را میگنجاند، تکرارهای جدا-شده-با-بلیک را نگه میدارد، و اضافه بر آن امتیازهایی را که متناهی نیستند یا بیرون 0 تا 1 میافتند و هر شمارش کلاسی که با dictionary نخواند رد میکند. همین منطق بهشکل یک توضیح در Pascal
// فقط برای توضیح: decode کردن حریصانهٔ 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 است
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; // بلیک نگهبان تکرار را ریست میکند
end;
end;
decode کردن حریصانه دقیقترین استراتژی CTC موجود نیست؛ beam search با یک مدل زبانی میتواند بعضی برشهای مبهم را درست کند. برای اسناد چاپی در 300 DPI نتیجهٔ حریصانه معمولاً همان چیزی است که مدل ارائه میدهد و decoder جای جبران ضعفهای مدل نیست. مدل لاتین PP-OCRv3 مثلاً حتی روی ورودی تمیز میتواند ñ را n بخواند. HotPDF آن را با جایگزینی نویسهها در پسپردازش پنهان نمیکند، چون یک جدول جایگزینی که اسپانیایی را درست میکند یک چیز دیگر را میشکند، و یک نویسهٔ اشتباه در یک لایهٔ قابلجستوجو بدتر از یک از دست دادن صادقانه است
HotPDF خطهای متنی را چطور مرتب میکند، از جمله عربی راستبهچپ؟
HotPDF جعبههای متنی تشخیصدادهشده را بالا به پایین مرتب میکند، وقتی جعبهها دستکم نصف ارتفاع جعبهٔ کوچکتر عمودی همپوشانی دارند آنها را در یک ردیف گروه میکند، و هر ردیف را چپ به راست یا وقتی RightToLeft فعال است راست به چپ مرتب میکند؛ نویسههای داخل هر خط بازشناسیشده هرگز معکوس نمیشوند. گروهبندی مهم است چون detector اغلب یک خط بصری تکی را به چند جعبه میشکند، مثلاً یک برچسب و یک مقدار که با فاصلهٔ بزرگی جدا شدهاند، و یک مرتبسازی خالص مبتنی بر مختصات بالایی هر وقت tops شان یک یا دو پیکسل فرق داشته باشد آنها را با خط مجاور درهممیبافت
پیشتنظیم عربی RightToLeft := True را ست میکند، که به DLL میگوید جعبههای هر ردیف را با لبهٔ راستشان، از حاشیهٔ راست به داخل، مرتب کند. کل اثر همین است. متنی که مدل برای یک خط برمیگرداند از قبل بهترتیب منطقی یونیکد است، ترتیبی که خوانندهٔ عربی میخواند و تایپ میکند، و همان ترتیبی هم هست که استخراج متن و جستوجو در PDF انتظار دارند. معکوس کردن مکانیکی رشته برای اینکه در debugger «درست به نظر برسد» جستوجو و کپی و paste و صفحهخوانها را میشکند. نمایش دوسویه و شکلدهی گلیف کار viewer است
یک موتور یک پروفایل زبان را سرو میکند. تشخیص خودکار خط وجود ندارد، پس سندی که خطوط را قاطی میکند بهازای هر پروفایل یک موتور میخواهد که روی صفحات استفادهکننده از آن اعمال شود. چون ApplyLoadedOCRTextLayer یک فهرست صفحهٔ صریح میگیرد و هر فراخوانی را بهعنوان تراکنش مستقل همه-یا-هیچ خودش ثبت میکند، این کار سرراست است
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// برای تگ ناشناس EArgumentException بالا میدهد، قبل از load شدن هر مدلی
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 استفاده میکند؛ آداپتور فقط وقتی RightToLeft ست شده باشد آن را لازم دارد، پس یک DLL قدیمیتر همچنان زبانهای چپبهراست را سرو میکند و موقع ساخت موتور با یک EArgumentException که export گمشده را برای عربی نام میبرد شکست میخورد
چرا مدلهای OCR جدیدتر load نمیشوند؟
DLL یعنی RapidOCR در HotPDF یک ONNX Runtime استاتیک یعنی 1.14 را لینک میکند که نمیتواند مدلهای ذخیرهشده با نسخهٔ 10 از ONNX IR را بخواند و exportهای جدیدتر مثل مدلهای PP-OCRv5 میتوانند runtime جدیدتری از آن بخواهند؛ چنین مدلی موقع ساخت موتور با یک تشخیص بومی شکست میخورد. همان محدودیت دلیل این است که بستههای زبانی به جفتهای مشخص بازشناس و dictionary یعنی PP-OCRv3 و PP-OCRv4 پین شدهاند نه «آخرین»، و دلیل مخلوط بودن آن دو نسل در جدول بالا: هر جفت پینشده یکی است که زیر آن runtime load و تأیید میشود
نصبکننده جفت شدن را اعمال میکند. هر فایل در manifest اش یک hash یعنی SHA256 دارد، یک فایل موجود با hash متفاوت بهجای بازنویسی شدن نصب را متوقف میکند و هر دانلود زیر یک نام موقت فرود میآید و فقط بعد از مطابقت hash اش سر جایش میرود. این محافظت در برابر نسخهٔ آرام مشکل dictionary است: کسی با دست یک recognition.onnx جدیدتر داخل یک پوشهٔ پروفایل میگذارد، شمارش کلاسها اتفاقاً میخواند و هیچ چیز شکست نمیخورد تا وقتی مشتری گزارش بدهد جستوجو کلماتی را که واضح میبیند پیدا نمیکند. در زمان اجرا آداپتور آفلاین میماند و هرگز مدلی را که نیست fetch نمیکند. بازشناس شکل مدل را هم موقع load اعتبارسنجی میکند، ورودی NCHW با ارتفاع ثابت 32 یا 48 پیکسل یا ارتفاع پویا را میپذیرد که آن را با 48 اجرا میکند
اگر خطی لازم داری که هیچکدام از نه پروفایل پوشش نمیدهند، همچنان میتوانی RecognitionModel و CharacterDictionary را به فایلهای خودت اشاره بدهی. همان چکها اعمال میشوند، که همین هدف است: یک جفت ناهمخوان موقع مقداردهی شکست میخورد، نه در آرشیو مشتریات. برای صفحاتی که هیچکدام از پروفایلهای RapidOCR مناسب نیستند، آداپتور Tesseract برای PDF قابلجستوجو به همان فراخوانی ApplyLoadedOCRTextLayer وصل میشود و برای فرمهای ASCII چاپ ماشینی موتور OCR تطبیق قالب داخلی اصلاً مدلی نمیخواهد
مرجع سریع: چکلیست چندزبانهٔ RapidOCR
- آپشنها را با
THPDFRapidOCRDLLOptions.ForLanguageبساز و EArgumentExceptionرا یک تگ پشتیبانینشده بگیر نه یک خطای runtime -
RecognitionModelوCharacterDictionaryرا با هم عوض کن، هرگز یکی تنها نه؛ شمارش کلاس برابر ثابت نمیکند ترتیب نویسهها برابر است - dictionaryها را UTF-8 بدون BOM نگه دار، مدخلها را هرگز تراش نده، و انتظار داشته باش مدل N + 2 کلاس داشته باشد: بلیک، N مدخل، فاصله
- یک decoder سفارشی CTC باید کلاس آخر و گام زمانی آخر را پوشش دهد و تکرارهای جدا-شده-با-بلیک را نگه دارد
- بهازای هر پروفایل زبان یک موتور و برای اسناد چندخطی فهرست صفحهٔ صریح بده
RightToLeftفقط ترتیب جعبهها را عوض میکند؛ متن بازشناسیشده بهترتیب منطقی یونیکد میماند- مدلها را با
Install-RapidOCRModels.ps1نصب کن تا پینهای SHA256 جفت مدل و dictionary را نگه دارند؛ اگر با-SkipClassifierنصب کردی UseAngleClassifier := Falseرا ست کن - قبل از اجرای صفحات A3 یا بزرگتر در 300 DPI
MaxPixelsرا از پیشفرض 16,777,216 بالاتر ببر
پیشتنظیمهای زبانی RapidOCR و آداپتور DLL بومی و خط لولهٔ لایهٔ متنی OCR بخشی از کامپوننت HotPDF Delphi PDF برای Delphi و C++Builder و Windows FPC/Lazarus هستند، از v2.775.0 برای پروفایلهای چندزبانه