HotPDF thực hiện OCR tiếng Trung và đa ngôn ngữ trong Delphi qua adapter DLL RapidOCR native của mình: THPDFRapidOCRDLLOptions.ForLanguage ánh xạ một tag ngôn ngữ như 'zh-CN', 'zh-TW', 'ru' hay 'ar' tới một cặp model recognition và từ điển ký tự khớp nhau, còn THotPDF.ApplyLoadedOCRTextLayer biến các dòng được nhận diện thành một text layer Unicode vô hình, có thể tìm kiếm trên các trang PDF đã scan
Đưa một bản demo chữ Latin chạy được là phần dễ. Những thất bại thú vị bắt đầu khi bạn chuyển sang chữ Hán phồn thể hay tiếng Nga và đầu ra biến thành thứ vô nghĩa mượt mà, đầy tự tin, hay khi mọi dòng lặng lẽ mất ký tự cuối, hay khi một trang Ả Rập quay về với các text box sai thứ tự. Chẳng trường hợp nào trong số đó tự nó raise exception. Các preset ngôn ngữ thêm vào ở HotPDF v2.775.0 tồn tại chủ yếu để bịt những khe hở ấy, và bốn cái bẫy dưới đây đáng được hiểu dù bạn có bao giờ đụng tới code native hay không, vì mỗi cái giải thích một triệu chứng mà nếu không biết bạn có thể bỏ cả ngày đi săn
ForLanguage chọn model và từ điển thế nào?
THPDFRapidOCRDLLOptions.ForLanguage phân giải một tag thành một trong chín profile và trả về options trỏ tới <profile>/recognition.onnx cùng <profile>/dictionary.txt dưới thư mục model của bạn, đồng thời giữ lại detector dùng chung, angle classifier tùy chọn, và các mặc định thread, pixel, timeout từ THPDFRapidOCRDLLOptions.Default. Phương thức chữ thường hóa tag, đổi gạch dưới thành gạch nối và cắt khoảng trắng hai đầu, nên 'zh_TW', 'ZH-tw' và ' zh-tw ' cùng đáp vào một profile. Các alias là một danh sách tường minh chứ không phải so khớp tiền tố: 'zh-Hant-TW' được chấp nhận vì nó nằm trong danh sách, còn một biến thể vùng miền tùy ý không được liệt kê sẽ raise EArgumentException trước khi model nào được nạp
| Profile | Ngôn ngữ | Tag ví dụ | Model được ghim |
|---|---|---|---|
ch | Tiếng Trung giản thể và tiếng Anh | zh, zh-CN, zh-Hans, chi_sim | PP-OCRv4 |
chinese_cht | Tiếng Trung phồn thể | zh-TW, zh-HK, zh-Hant, chi_tra | PP-OCRv3 |
en | Tiếng Anh | en, en-US, en-GB, eng | PP-OCRv4 |
latin | Pháp, Đức, Tây Ban Nha, Bồ Đào Nha, Ý, Hà Lan, Thổ Nhĩ Kỳ | fr, de, es-419, pt-BR, tr | PP-OCRv3 |
japan | Tiếng Nhật | ja, ja-JP, jpn | PP-OCRv4 |
korean | Tiếng Hàn | ko, ko-KR, kor | PP-OCRv4 |
cyrillic | Nga, Ukraina, Bulgaria, Belarus | ru, ru-RU, uk, bg | PP-OCRv3 |
arabic | Ả Rập, Ba Tư, Urdu | ar, ar-SA, fa, ur | PP-OCRv4 |
devanagari | Hindi, Marathi, Nepal | hi, mr, ne | PP-OCRv4 |
Bản thân adapter không bao giờ tải xuống bất cứ thứ gì. Bạn cấp các tệp một lần bằng script trợ lý đi kèm, ví dụ tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (hay -Language All cho cả chín profile), và script đặt một detector và classifier dùng chung tại các tên tệp ở gốc mà Default mong đợi. Từ đó, một bản scan tiếng Trung giản thể trở nên tìm kiếm được với vài dòng code. Phần nối engine là đúng mối nối IHPDFOCREngine đã mô tả trong bài viết về DLL RapidOCR in-process và biên ABI của nó, nên bài này giữ tập trung vào phần ngôn ngữ
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 và classifier dùng chung
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
// danh sách trang rỗng nghĩa là mọi trang; trang đã có văn bản bị bỏ qua
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;
Hai chi tiết trong đầu ra ấy đáng một lời ghi chú. Pipeline native trả về một kết quả cho mỗi dòng văn bản được phát hiện, chứ không phải mỗi từ, nên AcceptedWordCount ở đây đếm dòng, và MinimumConfidence được so với confidence trung bình ký tự của cả dòng: một dòng trung bình 0.45 bị bỏ như một khối. UniqueScalarCount báo bao nhiêu Unicode scalar khác biệt mà text layer phải ánh xạ vào font và bảng ToUnicode của nó — một phép kiểm hợp lý rằng văn bản CJK thật sự đã tới thay vì một nắm fallback Latin. Hãy giữ interface engine sống xuyên các tài liệu, vì việc khởi tạo model diễn ra trong factory và là bước tốn kém nhất
Vì sao chỉ thay model recognition lại sinh ra rác?
Một model recognition CTC không bao giờ xuất ký tự, chỉ xuất các chỉ số lớp, và từ điển là thứ duy nhất biến chỉ số 1.204 thành một glyph. Đổi ch/recognition.onnx lấy cyrillic/recognition.onnx mà giữ nguyên từ điển tiếng Trung, model sẽ vui vẻ phát ra các chỉ số Cyrillic hợp lệ mà từ điển cũ dịch thành các chữ Hán ngẫu nhiên. Kết quả trông như văn bản, qua được kiểm tra UTF-8, và tìm kiếm được — nhưng chẳng ra gì. Đó là lý do ForLanguage luôn đặt RecognitionModel và CharacterDictionary cùng nhau, và lý do các options dựng tay không bao giờ được đổi một cái mà thiếu cái kia
Phép kiểm an toàn hiển nhiên — so kích thước từ điển với bề rộng output của model — là cần nhưng chưa đủ. Hai từ điển có thể cùng số entry nhưng theo thứ tự khác nhau, và lệch một bậc trong thứ tự dịch mọi ký tự đi một code point. Vì thế HotPDF kiểm tra theo hai tầng khi factory khởi tạo model. Một, số lớp output phải bằng số entry từ điển cộng hai. Hai, khi tệp ONNX nhúng một danh sách metadata character, từng entry từ điển được so với nó theo thứ tự, và một chỗ lệch sẽ làm khởi tạo thất bại với EInvalidOperation cùng một chẩn đoán native, thay vì sinh ra thứ rác trông hợp lý về sau
Cái "cộng hai" đến từ bố cục lớp. Lớp 0 là CTC blank, các lớp 1 tới N là các dòng từ điển theo thứ tự tệp, và lớp cuối là một dấu cách. Vài từ điển còn mang riêng một entry dấu cách của chính nó, và dòng ấy phải được giữ nguyên trạng. Đây chính là chỗ một Trim thiện chí gây thiệt thật: nó biến một entry chỉ toàn dấu cách thành chuỗi rỗng và dịch hoặc phá vỡ cả bảng. Phép chuẩn hóa duy nhất an toàn là bỏ ký tự carriage return ở đuôi, để một từ điển lưu với kết thúc dòng CRLF nạp đúng, còn một BOM UTF-8, một dòng rỗng, hay một entry chứa tab thì bị từ chối. Đoạn phác dưới đây cho thấy bố cục bằng Pascal; đây là code giải thích, không phải một API của HotPDF
// Chỉ minh họa: bảng lớp mà một recognizer CTC mong đợi
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); // dòng mới ở cuối tệp
SetLength(Result, Last + 3);
Result[0] := ''; // class 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: chỉ bỏ CR
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // đừng bao giờ Trim: ' ' là một lớp
end;
Result[Last + 2] := ' '; // lớp cuối: dấu cách
// Length(Result) phải bằng số lớp output của model
end;
Decode CTC tham lam thực chất làm gì?
Decode CTC tham lam chọn lớp có điểm cao nhất tại mỗi time step, gộp các lần lặp liền kề thành một ký tự, và bỏ lớp blank; chính cái blank cho phép các chữ thật sự bị lặp đôi sống sót. Một model recognition nhìn một dòng văn bản như một chuỗi các lát dọc hẹp, và với mỗi lát — mỗi time step — nó xuất một xác suất cho từng lớp. Một dòng chứa AA中 có thể cho ra chuỗi argmax A A blank A 中 space. Gộp hai step A đầu thành một A, cái blank tách nó khỏi A kế tiếp, và kết quả là AA中 với dấu cách đuôi còn nguyên. Thiếu quy tắc blank, book và bok sẽ không thể phân biệt
Vì decoder chỉ dài chừng ấy dòng, việc sai biên rất dễ xảy ra, và các thất bại đều câm lặng. Nếu vòng argmax trong dừng sớm một lớp, lớp dấu cách không bao giờ thắng được và mọi dòng quay về không có khoảng cách giữa các từ, thứ phá sập tìm kiếm cụm từ trên các trang tiếng Anh và Latin. Nếu vòng ngoài dừng sớm một time step, ký tự cuối của mọi dòng biến mất, và với một dòng ngắn thì đó có thể là một phần ba văn bản. Còn nếu cờ lặp không được blank reset, các ký tự lặp đôi như ll hay các từ láy tiếng Trung như 谢谢 sẽ gộp thành một. Decoder của HotPDF gồm cả lớp cuối và time step cuối, giữ các lần lặp được blank tách, ngoài ra còn từ chối các điểm không hữu hạn hay nằm ngoài 0 tới 1, và mọi số lớp không khớp từ điển. Đây là cùng logic đó dưới dạng một minh họa Pascal
// Chỉ minh họa: decode CTC tham lam với các biên đúng.
// Scores giữ Steps * Classes xác suất, mỗi hàng là một time step
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; // class 0 là CTC blank
for Step := 0 to Steps - 1 do // gồm cả time step cuối
begin
Best := 0;
BestScore := Scores[Step * Classes];
for C := 1 to Classes - 1 do // gồm cả lớp cuối (dấu cách)
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; // một blank reset cờ lặp
end;
end;
Decode tham lam không phải chiến lược CTC chính xác nhất có thể; beam search với một language model có thể sửa vài lát còn mơ hồ. Với tài liệu in ở 300 DPI, kết quả tham lam thường là tất cả những gì model có thể cho, và decoder không phải nơi để bù đắp điểm yếu của model. Chẳng hạn, model Latin PP-OCRv3 có thể đọc ñ thành n ngay cả trên input sạch. HotPDF không bịt chỗ hụt ấy bằng các bảng thay thế ký tự hậu kỳ, vì một bảng thay thế cứu được tiếng Tây Ban Nha thì phá thứ khác, và một ký tự sai trong một lớp có thể tìm kiếm còn tệ hơn một chỗ trượt thẳng thắn
HotPDF sắp thứ tự các dòng văn bản, gồm cả tiếng Ả Rập phải-sang-trái, thế nào?
HotPDF sắp các text box được phát hiện từ trên xuống dưới, gộp các box thành một hàng khi chúng chồng lên nhau theo chiều dọc ít nhất nửa chiều cao của box nhỏ hơn, rồi sắp từng hàng từ trái sang phải, hay từ phải sang trái khi RightToLeft được bật; các ký tự bên trong mỗi dòng được nhận diện không bao giờ bị đảo. Việc gộp hàng quan trọng vì detector thường xẻ một dòng nhìn thấy thành vài box — chẳng hạn một nhãn và một giá trị cách nhau một khoảng trống rộng — và một phép sort thuần theo tọa độ đỉnh sẽ đan xen chúng với dòng bên cạnh mỗi khi hai đỉnh lệch nhau một hai pixel
Preset Ả Rập đặt RightToLeft := True, ra lệnh cho DLL sắp các box trong mỗi hàng theo cạnh phải của chúng, từ lề phải thu vào trong. Toàn bộ hiệu ứng chỉ có vậy. Văn bản mà model trả về cho một dòng vốn đã theo thứ tự logic Unicode — thứ tự mà một người đọc Ả Rập đọc và gõ — và đó cũng là thứ tự mà trích văn bản PDF cùng tìm kiếm mong đợi. Đảo ngược máy móc chuỗi ký tự để nó "trông đúng" trong debugger sẽ phá tìm kiếm, copy paste, và screen reader. Hiển thị hai chiều và định hình glyph là việc của viewer
Một engine phục vụ một profile ngôn ngữ. Không có tự phát hiện kiểu chữ, nên một tài liệu trộn nhiều kiểu chữ cần một engine mỗi profile, áp lên các trang dùng profile đó. Vì ApplyLoadedOCRTextLayer nhận một danh sách trang tường minh và commit mỗi lời gọi như một giao dịch tất-cả-hoặc-không-cái-gì riêng, chuyện này rất gọn gàng
uses
SysUtils, HPDFDoc, HPDFRapidOCRRecognition;
function CreateRapidEngine(const Tag: string): IHPDFOCREngine;
var
Models: THPDFRapidOCRDLLOptions;
begin
// raise EArgumentException với tag lạ, trước khi model nào được nạp
Models := THPDFRapidOCRDLLOptions.ForLanguage(Tag);
Models.MaxPixels := 33554432; // chỗ cho trang 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'); // profile chinese_cht
Arabic := CreateRapidEngine('ar-SA'); // profile 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;
Dòng MaxPixels nằm đó có lý do của nó. Các options DLL mặc định 16.777.216 pixel mỗi yêu cầu — mức phủ thoải mái A4 và US Letter ở 300 DPI — nhưng một trang A3 ở 300 DPI cỡ 3508 nhân 4961 pixel, khoảng 17,4 triệu, và yêu cầu bị từ chối vì vượt ngân sách. Hãy nâng MaxPixels (trần là 67.108.864) hay hạ THPDFOCRTextLayerOptions.DPI cho các khổ lớn. Việc sắp phải-sang-trái dùng export tùy chọn HPDFRapidOCRSetReadingDirection của ABI version 1; adapter chỉ đòi nó khi RightToLeft được đặt, nên một DLL cũ hơn vẫn phục vụ các ngôn ngữ trái-sang-phải và thất bại ngay lúc tạo engine với một EArgumentException nêu tên export còn thiếu cho tiếng Ả Rập
Vì sao các model OCR mới hơn nạp thất bại?
DLL RapidOCR của HotPDF liên kết một ONNX Runtime 1.14 tĩnh, thứ không đọc được các model lưu với ONNX IR version 10, và các bản export mới hơn như các model PP-OCRv5 có thể đòi một runtime mới hơn thế nữa; một model như vậy thất bại ngay lúc tạo engine với một chẩn đoán native. Ràng buộc đó chính là lý do các gói ngôn ngữ được ghim vào các cặp recognizer và từ điển PP-OCRv3 cùng PP-OCRv4 cụ thể thay vì "mới nhất", và lý do bảng ở trên trộn hai thế hệ: mọi cặp được ghim đều là những cặp nạp và kiểm chứng được dưới runtime ấy
Trình cài đặt thực thi việc ghép cặp. Mọi tệp trong manifest của nó mang một hash SHA256, một tệp đã tồn tại với hash khác sẽ chặn việc cài thay vì bị ghi đè, và mỗi lần tải xuống đáp vào một tên tạm và chỉ được dời vào vị trí sau khi hash khớp. Điều đó che chắn trước phiên bản êm thầm của vấn đề từ điển: ai đó thả tay một recognition.onnx mới hơn vào một thư mục profile, số lớp tình cờ khớp, và chẳng gì thất bại cho tới khi một khách hàng báo rằng tìm kiếm không thấy những từ mà mắt thường thấy rõ. Khi chạy, adapter giữ offline và không bao giờ tải một model còn thiếu. Recognizer còn kiểm chứng hình dạng model lúc nạp, nhận input NCHW với chiều cao cố định 32 hay 48 pixel hay chiều cao động, thứ nó chạy ở 48
Nếu bạn cần một kiểu chữ mà chín profile không phủ tới, bạn vẫn có thể trỏ RecognitionModel và CharacterDictionary vào các tệp của riêng mình. Các phép kiểm tương tự áp dụng, và đó chính là ý nghĩa của chúng: một cặp lệch nhau thất bại ngay lúc khởi tạo, chứ không phải trong kho tài liệu của khách. Với những trang mà không profile RapidOCR nào vừa, adapter Tesseract cho PDF có thể tìm kiếm cắm vào đúng lời gọi ApplyLoadedOCRTextLayer ấy, còn với các biểu mẫu ASCII in máy OCR engine khớp mẫu dựng sẵn chẳng cần model nào cả
Tra nhanh: danh mục RapidOCR đa ngôn ngữ
- Tạo options bằng
THPDFRapidOCRDLLOptions.ForLanguagevà coiEArgumentExceptionlà một tag không được hỗ trợ, không phải một lỗi runtime - Đổi
RecognitionModelvàCharacterDictionarycùng nhau, đừng bao giờ đổi một mình; số lớp bằng nhau không chứng minh thứ tự ký tự bằng nhau - Giữ từ điển ở dạng UTF-8 không BOM, đừng bao giờ trim entry, và mong đợi model có N + 2 lớp: blank, N entry, dấu cách
- Một decoder CTC tùy chỉnh phải phủ lớp cuối và time step cuối, và giữ các lần lặp được blank tách
- Dùng một engine mỗi profile ngôn ngữ và truyền danh sách trang tường minh cho các tài liệu trộn kiểu chữ
RightToLeftchỉ đổi thứ tự box; văn bản được nhận diện giữ nguyên thứ tự logic Unicode- Cài model bằng
Install-RapidOCRModels.ps1để các ghim SHA256 giữ vững cặp model-từ điển; đặtUseAngleClassifier := Falsenếu bạn cài với-SkipClassifier - Nâng
MaxPixelstrên mức mặc định 16.777.216 trước khi chạy trang A3 hay lớn hơn ở 300 DPI
Các preset ngôn ngữ RapidOCR, adapter DLL native và pipeline text layer OCR là một phần của HotPDF Delphi PDF Component cho Delphi, C++Builder và Windows FPC/Lazarus, bắt đầu từ v2.775.0 cho các profile đa ngôn ngữ