HotPDF מבצע OCR של סינית ושל שפות רבות ב-Delphi דרך מתאם ה-DLL המקורי של RapidOCR שלו: THPDFRapidOCRDLLOptions.ForLanguage ממפה תג שפה כמו 'zh-CN', 'zh-TW', 'ru' או 'ar' אל מודל זיהוי ומילון תווים תואמים, ו-THotPDF.ApplyLoadedOCRTextLayer הופך את השורות שזוהו לשכבת טקסט Unicode בלתי נראית וניתנת לחיפוש על עמודי PDF סרוקים
לגרום לדמו בכתב לטיני לעבוד הוא החלק הקל. הכישלונות המעניינים מתחילים כשעוברים לסינית מסורתית או רוסית והפלט הופך לגיבוב משכנע ותקין-צורה, או כשכל שורה מאבדת בשקט את התו האחרון שלה, או כשעמוד ערבי חוזר עם תיבות הטקסט שלו בסדר הלא נכון. אף אחד מאלה לא מעלה חריגה מעצמו. פרופילי השפה שנוספו ב-HotPDF v2.775.0 קיימים בעיקר כדי לסגור את הפערים האלה, וארבע המלכודות שלהלן שוות היכרות גם אם לעולם לא נוגעים בקוד המקורי, כי כל אחת מסבירה סימפטום שאפשר לבזבז עליו יום במרדף אחרת
איך ForLanguage בוחר מודל ומילון?
THPDFRapidOCRDLLOptions.ForLanguage פותר תג אל אחד מתוך תשעה פרופילים ומחזיר אפשרויות שמצביעות על <profile>/recognition.onnx ו-<profile>/dictionary.txt מתחת לספריית המודלים שלכם, תוך שמירה על ה-detector המשותף, מסווג הזוויות האופציונלי, וברירות המחדל של thread, פיקסלים ופסק-זמן מתוך 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 |
המתאם עצמו לעולם לא מוריד דבר. את הקבצים מספקים פעם אחת עם ה-helper המצורף, למשל tools/Install-RapidOCRModels.ps1 -Destination C:/OCR/models -Language ch,chinese_cht,cyrillic (או -Language All עבור כל תשעת הפרופילים), וה-helper מציב detector ו-classifier משותפים בשמות הקבצים בשורש ש-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 ו-classifier משותפים
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;
שני פרטים בפלט הזה ראויים להערה. הצינור המקורי מחזיר תוצאה אחת לכל שורת טקסט שזוהתה, לא לכל מילה, ולכן AcceptedWordCount סופר שורות כאן, ו-MinimumConfidence מושווה מול ממוצע ביטחון התווים של השורה כולה: שורה שממוצעה 0.45 מושמטת בשלמותה. UniqueScalarCount מדווח כמה scalars נבדלים של Unicode נאלצה שכבת הטקסט למפות אל הגופן שלה ואל טבלת ה-ToUnicode, בדיקת היגיון שימושית שטקסט CJK באמת הגיע במקום חופן fallbacks לטיניים. משאירים את ממשק המנוע חי על פני מסמכים, כי אתחול המודלים קורה במפעל והוא הצעד היקר
למה החלפה של מודל הזיהוי בלבד מייצרת גיבוב?
מודל זיהוי CTC לעולם לא פולט תווים, רק אינדקסי מחלקות, והמילון הוא הדבר היחיד שהופך אינדקס 1,204 ל-glyph. מחליפים את ch/recognition.onnx ב-cyrillic/recognition.onnx אבל משאירים את המילון הסיני, והמודל יפלוט בהנאה אינדקסי קירילית תקינים שהמילון הישן מתרגם לתווי Han אקראיים. התוצאה נראית כמו טקסט, עוברת אימות UTF-8, וניתנת לחיפוש עבור בדיוק כלום. לכן ForLanguage תמיד מגדיר RecognitionModel ו-CharacterDictionary יחד, ולכן אפשרויות שנבנות ביד לעולם לא אמורות לשנות את האחד בלי השני
בדיקת הבטיחות המובנת מאליה, השוואת גודל המילון מול רוחב הפלט של המודל, נחוצה אך אינה מספקת. שני מילונים יכולים להחזיק את אותו מספר רשומות בסדר אחר, וסטייה של אחד בסדר מזיזה כל תו בנקודת קוד אחת. לכן HotPDF בודק בשני שלבים כשהמפעל מאתחל את המודל. ראשון, מניין המחלקות בפלט חייב להיות שווה לרשומות המילון בתוספת שתיים. שני, כשקובץ ה-ONNX משבץ רשימת metadata של character, כל רשומת מילון מושווית איתו בסדר, ואי-התאמה מכשילה את האתחול עם EInvalidOperation ודיאגנוסטיקה מקורית במקום לייצר גיבוב סביר מאוחר יותר
הביטוי "ועוד שתיים" מגיע מפריסת המחלקות. מחלקה 0 היא ה-blank של ה-CTC, המחלקות 1 עד N הן שורות המילון בסדר הקובץ, והמחלקה האחרונה היא רווח. חלק מהמילונים נושאים גם רשומת רווח משלהם, ואת השורה הזאת חייבים לשמור בדיוק כפי שהיא. כאן Trim מתוך כוונה טובה עושה נזק אמיתי: הוא הופך רשומת רווח-בודד למחרוזת ריקה ומזיז או שובר את הטבלה. הנרמול היחיד שבטוח הוא השמטת carriage return בסוף שורה, כך שמילון שנשמר עם סיומות שורה של CRLF נטען נכון, בזמן שסימן סדר בייטים של UTF-8, שורה ריקה, או רשומה שמכילה טאב נדחים. השרטוט להלן מציג את הפריסה בפסקל; זהו קוד הסברתי, לא 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] := ''; // 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: משמיטים רק את ה-CR
if (Entry = '') or (Pos(#9, Entry) > 0) then
raise EArgumentException.Create('Invalid dictionary entry');
Result[I + 1] := Entry; // לעולם לא Trim: ' ' הוא מחלקה
end;
Result[Last + 2] := ' '; // final class: space
// Length(Result) חייב להיות שווה למניין המחלקות בפלט המודל
end;
מה פענוח CTC החמדני באמת עושה?
פענוח CTC חמדני בוחר במחלקה בעלת הציון הגבוה ביותר בכל time step, מכווץ חזרות עוקבות לתו אחד, ומשמיט את מחלקת ה-blank; ה-blank הוא מה שמאפשר לאותיות שהוכפלו באמת לשרוד. מודל זיהוי מביט בשורת טקסט בתור רצף של פרוסות אנכיות צרות, ועבור כל פרוסה, או time step, הוא פולט הסתברות עבור כל מחלקה. שורה שמכילה AA中 עשויה לייצר רצף argmax של A A blank A 中 space. כיווץ שני ה-A הראשונים נותן A אחד, ה-blank מפריד בינו לבין ה-A הבא, והתוצאה היא AA中 עם הרווח הנגרר שלם. בלי כלל ה-blank, book ו-bok היו בלתי נבדלים
מכיוון שהמפענח הוא רק כעשור שורות, קל לטעות בגבולות, והכישלונות שקטים. אם לולאת ה-argmax הפנימית עוצרת מחלקה אחת לפני הסוף, מחלקת הרווח לעולם לא יכולה לנצח וכל שורה חוזרת בלי ריווח מילים, מה שהורס חיפוש ביטויים בעמודי אנגלית ולטינית. אם הלולאה החיצונית עוצרת time step אחד לפני הסוף, התו האחרון של כל שורה נעלם, מה שעבור שורה קצרה יכול להיות שליש מהטקסט. ואם שומר החזרות לא מאופס על ידי blank, תווים מוכפלים כמו ll או הכפלות סיניות כמו 谢谢 מתכווצים לאחד. המפענח של HotPDF כולל את המחלקה האחרונה ואת ה-time step האחרון, משמר חזרות מופרדות-blank, ודוחה בנוסף ציונים שאינם סופיים או שחורגים מ-0 עד 1, וכל מניין מחלקות שאינו תואם את המילון. הנה אותה לוגיקה בתור המחשה בפסקל
// המחשה בלבד: פענוח CTC חמדני עם גבולות נכונים.
// Scores מחזיק Steps * Classes הסתברויות, שורה אחת לכל 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; // מחלקה 0 היא ה-blank של ה-CTC
for Step := 0 to Steps - 1 do // כוללים את ה-time step האחרון
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;
פענוח חמדני אינו אסטרטגיית ה-CTC המדויקת ביותר הקיימת; beam search עם מודל שפה יכול לתקן חלק מהפרוסות הדו-משמעיות. עבור מסמכים מודפסים ב-300 DPI התוצאה החמדנית היא בדרך כלל מה שיש למודל להציע, והמפענח אינו המקום לפצות על חולשות המודל. מודל ה-PP-OCRv3 הלטיני, למשל, יכול לקרוא ñ בתור n גם על קלט נקי. HotPDF לא מכסה על זה בהחלפות תווים בפוסט-עיבוד, כי טבלת החלפות שמתקנת ספרדית שוברת משהו אחר, ותו שגוי בשכבה ניתנת לחיפוש גרוע מהחטאה כנה
איך HotPDF מסדר שורות טקסט, כולל ערבית מימין לשמאל?
HotPDF ממיין תיבות טקסט שזוהו מלמעלה למטה, מקבץ תיבות לשורה כשהן חופפות אנכית בלפחות חצי מגובה התיבה הקטנה מביניהן, ומסדר כל שורה משמאל לימין, או מימין לשמאל כש-RightToLeft מופעל; את התווים בתוך כל שורה שזוהתה לעולם לא הופכים. הקיבוץ חשוב כי detector לעיתים קרובות מפצל שורה ויזואלית אחת לכמה תיבות, למשל תווית וערך שמופרדים בפער רחב, ומיון טהור לפי קואורדינטה עליונה היה שוזר אותן עם השורה השכנה בכל פעם שהראשים שלהן שונים בפיקסל או שניים
פריסת הערבית מגדירה RightToLeft := True, מה שאומר ל-DLL לסדר את התיבות בכל שורה לפי הקצה הימני שלהן, מהשוליים הימניים פנימה. זהו האפקט כולו. הטקסט שהמודל מחזיר עבור שורה כבר נמצא בסדר הלוגי של Unicode, הסדר שבו קורא ערבית קורא ומקליד אותו, והוא גם הסדר שחילוץ הטקסט והחיפוש של PDF מצפים לו. היפוך מכני של המחרוזת כדי שת"יראה נכון" ב-debugger היה שובר חיפוש, העתקה והדבקה, וקוראי מסך. תצוגה דו-כיוונית ועיצוב גליפים הם עבודת הצופן
מנוע אחד משרת פרופיל שפה אחד. אין detection אוטומטי של כתב, ולכן מסמך שמערבב כתבים זקוק למנוע אחד לכל פרופיל, שמיושם על העמודים שמשתמשים בו. מכיוון ש-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 profile
Arabic := CreateRapidEngine('ar-SA'); // arabic profile, 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 חדשים נכשלים בטעינה?
ה-DLL של RapidOCR של HotPDF מקשר ONNX Runtime סטטי בגרסה 1.14, שאינו יכול לקרוא מודלים שנשמרו עם גרסת ONNX IR 10, וexports חדשים יותר כמו מודלי PP-OCRv5 יכולים לדרוש ראנטיים חדש יותר מזה; מודל כזה נכשל ביצירת המנוע עם דיאגנוסטיקה מקורית. האילוץ הזה הוא הסיבה שחבילות השפה קבועות על זוגות ספציפיים של מזהה ומילון של PP-OCRv3 ו-PP-OCRv4 במקום על "החדש ביותר", ולכן הטבלה שלמעלה מערבבת את שני הדורות: כל זוג קבוע הוא כזה שנטען ונאמת תחת אותו ראנטיים
המתקין אוכף את הזיווג. כל קובץ ב-manifest שלו נושא hash של SHA256, קובץ קיים עם hash אחר עוצר את ההתקנה במקום להידרס, וכל הורדה נוחתת תחת שם זמני ועוברת למקומה רק אחרי שה-hash שלה מתאים. זה מגן מפני הגרסה השקטה של בעיית המילון: מישהו משליך recognition.onnx חדש יותר ביד לתוך תיקיית פרופיל, המניין המחלקות במקרה תואם, ושום דבר לא נכשל עד שלקוח מדווח שהחיפוש לא מוצא מילים שהוא רואה בבירור. בזמן ריצה המתאם נשאר אופליין ולעולם לא מושיט יד אחר מודל חסר. המזהה גם מאמת את צורת המודל בטעינה, מקבל קלט NCHW בגובה קבוע של 32 או 48 פיקסלים או בגובה דינמי, שאותו הוא מריץ ב-48
אם זקוקים לכתב שאף אחד מתשעת הפרופילים לא מכסה, אפשר עדיין לכוון את RecognitionModel ואת CharacterDictionary אל הקבצים שלכם. אותן בדיקות חלות, וזו המטרה: זוג לא תואם נכשל באתחול, ולא בארכיון של הלקוח שלכם. עבור עמודים שאף פרופיל RapidOCR לא מתאים להם, מתאם ה-Tesseract עבור PDF ניתן לחיפוש מתחבר לאותה קריאת ApplyLoadedOCRTextLayer, ועבור טפסי ASCII מודפסי-מכונה מנוע ה-OCR מבוסס התאמת התבניות המובנה לא זקוק למודלים כלל
עזר זריז: צ'קליסט RapidOCR רב-לשוני
- יוצרים אפשרויות עם
THPDFRapidOCRDLLOptions.ForLanguageומתייחסים ל-EArgumentExceptionבתור תג לא נתמך, לא תקלת ראנטיים - משנים
RecognitionModelו-CharacterDictionaryיחד, לעולם לא אחד לבדו; מנייני מחלקות שווים אינם מוכיחים סדר תווים זהה - משאירים מילונים בתור UTF-8 ללא BOM, לעולם לא גוזמים רשומות, ומצפים שלמודל יהיו N + 2 מחלקות: blank, N רשומות, רווח
- מפענח CTC מותאם חייב לכסות את המחלקה האחרונה ואת ה-time step האחרון ולהשאיר חזרות מופרדות ב-blank
- משתמשים במנוע אחד לכל פרופיל שפה ומעבירים רשימות עמודים מפורשות עבור מסמכים מעורבי-כתב
RightToLeftמשנה רק את סדר התיבות; הטקסט שזוהה נשאר בסדר הלוגי של Unicode- מתקינים מודלים עם
Install-RapidOCRModels.ps1כדי שקיבועי ה-SHA256 יחזיקו את זיווג המודל והמילון; מגדיריםUseAngleClassifier := Falseאם התקנתם עם-SkipClassifier - מרימים את
MaxPixelsמעל ברירת המחדל של 16,777,216 לפני הרצת עמודי A3 ומעלה ב-300 DPI
פרופילי השפה של RapidOCR, מתאם ה-DLL המקורי וצינור שכבת הטקסט של ה-OCR הם חלק מ-HotPDF Delphi PDF Component עבור Delphi, C++Builder ו-FPC/Lazarus ל-Windows, החל מ-v2.775.0 עבור הפרופילים הרב-לשוניים