Шари тексту OCR, межі штрихкодів і бокси redaction облич зсуваються на обрізаних сторінках PDF, коли пікселі бітмапа мапляться назад через MediaBox замість бокса, який рендерер фактично растеризував: CropBox, притиснутий до MediaBox (ISO 32000-1 §14.11.2). HotPDF полагодив це для ApplyLoadedOCRTextLayer у v2.770.153, а для DecodeLoadedPageBarcodes і DetectLoadedRedactionFindings — у v2.770.154
Баг-репорт, який зазвичай приходить, виглядає так. Архів відсканованих контрактів проходить OCR, вихід пошуковий, і збіг пошуку за номером пункту підсвічується на пів дюйма нижче та лівіше надрукованого числа. Більшість файлів у партії в порядку. Зламані всі прийшли з однієї сканувальної станції, яка пише /CropBox, щоб обрізати поле планшета. Та одна деталь відділяє картину, яку бачив OCR-рушій, від рамки, у яку покладено текстовий шар, і та сама невідповідність зсуває межі штрихкодів і, серйозніше, бокси redaction облич
Чому шар тексту OCR відпливає від відсканованих слів?
Шар тексту відпливає тому, що дві половини конвеєра розходилися в думках про те, який прямокутник покриває бітмап. У v2.766.64 HotPDF змінив рендеринг, SVG-експорт, переглядач і друк, щоб шанувати CropBox: сторінка показується крізь свій CropBox, притиснутий до MediaBox, — саме те, що приписує ISO 32000-1 §14.11.2, — і було додано GetLoadedPageVisibleBox, щоб повертати той видимий бокс. Функції розпізнавання продовжували будувати перетворення пристрій-сторінка з GetLoadedPageBox(PageIndex, pbMediaBox, ...). Растр тепер покривав видимий бокс, перетворення досі припускав MediaBox, і кожна розпізнана позиція поверталася зі зсувом на проміжок між ними
Постраждале вікно відтак точне. ApplyLoadedOCRTextLayer клав текст не туди з v2.766.64 по v2.770.152. Загальносторінкові DecodeLoadedPageBarcodes і детекція облич усередині DetectLoadedRedactionFindings лишалися неправильними на одну збірку довше, по v2.770.153. До v2.766.64 рендерер малював увесь MediaBox, тож мапінг і растр сходилися — ціною розпізнавання вмісту, якого переглядачі ніколи не показують. Виправлення змінювали разом три речі для кожної функції: перетворення, оцінку піксельного бюджету та бокс сторінки, що вручається власному рушію в записі запиту
Кілька випадків не постраждали ніколи:
- Сторінки без
/CropBoxчи з CropBox, рівним MediaBox, мапляться ідентично до і після виправлення DecodeLoadedPageBarcodesзі встановленимHasRegionрендерить точно той регіон, який ви передаєте, і мапить крізь той самий регіон, тож декодування за явним регіоном було правильним увесь час; перевірка, що регіон лежить усередині сторінки, досі користується MediaBox- Результати redaction за патернами (електронні пошти, номери карток тощо) походять із екстракції тексту в user space, а не з растру, тож зсунулися лише результати детекції облич
Три системи координат і те, які HotPDF API яку вживають
Код HotPDF, що торкається розпізнавання, має діло з трьома системами, і більшість багів мапінгу походить зі змішування двох із них
- Пікселі бітмапа: початок згори ліворуч, Y зростає вниз, одиниці — пікселі при запитаному DPI.
THPDFOCRWord.Left,Top,RightіBottom— у цій системі, як і необов'язкові точки базової лінії, результати, які повертає власнийIHPDFBarcodeDecoder, і бокси від власногоIHPDFFaceDetector - PDF user space завантаженої сторінки: початок знизу ліворуч, Y зростає вгору, одиниці — пункти, з
Bottom < Top.GetLoadedPageBoxіGetLoadedPageVisibleBoxповертають Left, Bottom, Right, Top у цій системі, як і поляPageLeft,PageBottom,PageRightіPageTopуTHPDFOCRRequest, межі вTHPDFDecodedBarcodeта прямокутники вTHPDFRedactionFinding - Координати малювання сторінок HotPDF: API, яким ви будуєте нові сторінки (текстовий вихід, фігури, штрихкоди, посилання, поля форм), працює з початком згори ліворуч і Y, що зростає вниз. Та система належить генерації документів і не має нічого спільного з описаними вище API завантаженого документа, тож ніколи не годуйте в неї прямокутник у user space завантаженої сторінки без змін
Запис слова OCR свідомо піксельний: рушій звітує те, що він бачив на зображенні, а конверсією володіє ApplyLoadedOCRTextLayer. Той поділ працює лише тоді, коли конверсія користується правильним боксом — саме те, що повернув собі v2.770.153
Перетворення пристрій-сторінка за OCR, штрихкодами та обличчями
HotPDF мапить пікселі бітмапа на сторінку однією афінною матрицею, збудованою з п'яти входів: ротація, масштаб DPI / 72, висота бітмапа і Left, Bottom, Right, Top відрендереного бокса. OCR, декодування штрихкодів і детекція облич ділять одну підпрограму для того, саме тому один неправильний бокс-вхід зламав усі три однаково. Для неповерненої сторінки матриця сторінка-пристрій [A B C D E F] така:
A = ScaleіD = -Scale, деScale = DPI / 72; від'ємнийDперевертає user space (Y вгору) у простір бітмапа (Y вниз)B = C = 0, бо неповернена сторінка не має ні зсуву, ні перестановки осейE = -Left * Scale, що пересуває лівий край бокса в піксельну колонку 0F = BitmapHeight + Bottom * Scale, що мапить нижній край бокса в y = BitmapHeight — нижній край бітмапа, тож верхній край приземляється на рядок 0
Пікселі повертаються на сторінку через обернену матрицю. Request.PageRotation несе /Rotate сторінки, нормалізований до 0, 90, 180 чи 270 (будь-яке значення, що не кратне 90, трактується як 0), а рендерер повертає сторінку за годинниковою стрілкою, як вимагає ISO 32000-1 §7.7.3.3. Під ротацією осі міняються місцями, і до початку координат бітмапа пришивається інша пара країв бокса. Записано оберненими формулами, з S = DPI / 72, x і y у пікселях і H — висотою бітмапа:
| /Rotate | X сторінки | Y сторінки | Краї бокса, від яких залежить мапінг |
|---|---|---|---|
| 0 | Left + x / S | Bottom + (H - y) / S | Left, Bottom |
| 90 | Left + y / S | Bottom + x / S | Left, Bottom |
| 180 | Right - x / S | Bottom + y / S | Right, Bottom |
| 270 | Right - y / S | Top - x / S | Right, Top |
Остання колонка пояснює, чому баг здавався випадковим у проді. CropBox, що обрізає лише верх сторінки, лишає Left і Bottom недоторканими, тож прямі сторінки виходили ідеально, і лише сторінки з /Rotate 270 відпливали. Ротація ще й міняє місцями розміри бітмапа: при 90 і 270 бітмап має (Top - Bottom) * S пікселів завширшки і (Right - Left) * S заввишки
Що ламається з MediaBox [0 0 612 792] і CropBox [36 36 576 756]?
З півдюймовим обрізом з кожного боку шар тексту неповерненої сторінки приземлюється рівно на 36 пунктів лівіше та на 36 пунктів нижче відсканованих слів, коли користується MediaBox. Візьміть сторінку US Letter, чий CropBox обрізає 36 пунктів (0,5 дюйма) з кожного краю. Видимий бокс — 540 на 720 пунктів, тож при усталеній OCR-роздільності 300 DPI масштаб становить 300 / 72 ≈ 4,1667, а бітмап — 2250 на 3000 пікселів
Припустімо, рушій звітує слово з піксельним боксом Left 450, Top 600, Right 900, Bottom 660 і без базової лінії. HotPDF тоді кладе базову лінію на 20 відсотків висоти слова вище нижнього краю, в піксельному рядку 648, і мапить стартову точку (450, 648):
- Крізь видимий бокс: x = 36 + 450 / 4.1667 = 144.0 і y = 36 + (3000 - 648) / 4.1667 = 600.48 — там, де слово надруковане
- Крізь MediaBox: x = 0 + 108.0 = 108.0 і y = 0 + 564.48 = 564.48 — рівномірний зсув на (-36, -36) пункту
Поверніть ту саму сторінку — і напрямок помилки зміниться, бо залучені різні краї. При /Rotate 180 X-член користується Right, і 612 замість 576 штовхає шар на 36 пунктів праворуч, поки Bottom досі тягне його на 36 пунктів униз. При /Rotate 270 і Right, і Top завеликі, тож шар зсувається на 36 пунктів праворуч і на 36 пунктів угору. Документ зі змішаними орієнтаціями може показувати зсув у трьох напрямках — надійний відбиток цього бага. Рукописний код, що виводить масштаб із бокса, як-от Bitmap.Width / (Right - Left), ще й розтягує кожну координату на 612 / 540, приблизно 13 відсотків, зверху зсуву
Які з ваших PDF-документів постраждали?
Документ PDF під загрозою, коли щонайменше одна сторінка має видимий бокс, що відрізняється від MediaBox, і HotPDF може сказати вам це кількома рядками. Порівнюйте GetLoadedPageBox з pbMediaBox проти GetLoadedPageVisibleBox для кожної сторінки і друкуйте поруч GetLoadedPageRotation, щоб передбачити напрямок зсуву з таблиці вище. THPDFPageBoundary пропонує ще й pbCropBox, pbBleedBox, pbTrimBox і pbArtBox, але GetLoadedPageBox(pbCropBox) падає назад до MediaBox, коли crop box не існує, і не притискає, тож видимий бокс — правильна річ для порівняння
uses
System.SysUtils, HPDFDoc;
procedure ReportCroppedPages(const FileName: string);
var
Pdf: THotPDF;
I: Integer;
ML, MB, MR, MT, VL, VB, VR, VT, Tmp: Single;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) < 1 then
raise Exception.Create('Cannot load ' + FileName);
for I := 0 to Pdf.LoadedPageCount - 1 do
begin
if not Pdf.GetLoadedPageBox(I, pbMediaBox, ML, MB, MR, MT) then
Continue;
// збережений масив може перелічувати свої кути в будь-якому порядку
if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
// уже нормалізовано і притиснуто до MediaBox
if not Pdf.GetLoadedPageVisibleBox(I, VL, VB, VR, VT) then
Continue;
if (Abs(VL - ML) > 0.01) or (Abs(VB - MB) > 0.01) or
(Abs(VR - MR) > 0.01) or (Abs(VT - MT) > 0.01) then
Writeln(Format('Page %d MediaBox [%g %g %g %g] visible [%g %g %g %g] /Rotate %d',
[I + 1, ML, MB, MR, MT, VL, VB, VR, VT,
Pdf.GetLoadedPageRotation(I)]));
end;
finally
Pdf.Free;
end;
end;
Дві деталі GetLoadedPageVisibleBox важать для скриптів на кшталт цього. Функція лишає свої out-параметри недоторканими при невдачі, тож передустановлення усталеного розміру сторінки перед викликом — безпечний шаблон. А коли деформований CropBox узагалі не перетинає MediaBox, функція повертає MediaBox, а не порожній прямокутник. Якщо звіт перелічує сторінки, а ваша розгорнута збірка старіша за v2.770.153 для OCR чи v2.770.154 для штрихкодів і облич, переганяйте розпізнавання на тих сторінках після оновлення. Шар OCR, закомічений постраждалою збіркою, лишається у збереженому файлі, і усталена опція SkipPagesWithText пропустить ті сторінки на другому прогоні, якщо ви її не вимкнете чи не приберете старий шар спершу
Як власний IHPDFOCREngine мусить мапити пікселі назад у простір PDF?
Власний IHPDFOCREngine має повертати бокси слів у пікселях бітмапа і дозволяти HotPDF робити мапінг; конвертуйте в user space лише для власних рішень, і тоді користуйтеся боксом із запиту, ніколи — MediaBox. З v2.770.153 поля PageLeft, PageBottom, PageRight і PageTop запиту описують відрендерений видимий бокс, тож вони збігаються з Request.Bitmap точно. Хелпер нижче — обернена бібліотечного перетворення, з його використанням фактичної висоти бітмапа для прямих сторінок, тож він сходиться з HotPDF до пікселя
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Піксель бітмапа (початок згори ліворуч, Y вниз) у PDF user space
// (початок знизу ліворуч, Y вгору), крізь бокс, з якого рендерився бітмап
procedure HotPixelToPage(Rotation, DPI, BitmapHeight: Integer;
Left, Bottom, Right, Top: Single; X, Y: Double;
out PageX, PageY: Double);
var
S: Double;
begin
S := DPI / 72.0;
case Rotation of
90: begin PageX := Left + Y / S; PageY := Bottom + X / S; end;
180: begin PageX := Right - X / S; PageY := Bottom + Y / S; end;
270: begin PageX := Right - Y / S; PageY := Top - X / S; end;
else
PageX := Left + X / S;
PageY := Bottom + (BitmapHeight - Y) / S;
end;
end;
Реалістична причина потребувати user space всередині рушія — зональне правило: накладні, чию шапку ви ніколи не хочете пошуковою, чи штампова зона, що бентежить розпізнавач. Рушій нижче, написаний на TInterfacedObject, щоб лічильник посилань керував його життям, фільтрує слова за тим, куда падають їхні центри на сторінці, а потім повертає виживших недоторканими в піксельних координатах. RunRecognizer стоїть замість вашого власного виклику розпізнавача
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // user space
function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
out Words: THPDFOCRWords): boolean; // ваш розпізнавач, піксельні бокси
public
constructor Create(SkipLeft, SkipBottom, SkipRight, SkipTop: Single);
function GetName: AnsiString;
function Recognize(const Request: THPDFOCRRequest;
out Words: THPDFOCRWords; out Diagnostic: AnsiString): boolean;
end;
function TZoneFilterOCREngine.Recognize(const Request: THPDFOCRRequest;
out Words: THPDFOCRWords; out Diagnostic: AnsiString): boolean;
var
Raw: THPDFOCRWords;
I, Count: Integer;
CX, CY: Double;
begin
Diagnostic := '';
SetLength(Words, 0);
if not RunRecognizer(Request.Bitmap, Request.MaxWords, Raw) then
begin
Diagnostic := 'recognizer failed';
Exit(False);
end;
SetLength(Words, Length(Raw));
Count := 0;
for I := 0 to High(Raw) do
begin
HotPixelToPage(Request.PageRotation, Request.DPI,
Request.Bitmap.Height, Request.PageLeft, Request.PageBottom,
Request.PageRight, Request.PageTop,
(Raw[I].Left + Raw[I].Right) / 2, (Raw[I].Top + Raw[I].Bottom) / 2,
CX, CY);
if (CX >= FSkipLeft) and (CX <= FSkipRight) and
(CY >= FSkipBottom) and (CY <= FSkipTop) then
Continue;
Words[Count] := Raw[I]; // досі пікселі: HotPDF мапить їх сам
Inc(Count);
end;
SetLength(Words, Count);
Result := True;
end;
Вручіть рушій ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info), як і будь-якому іншому рушієві. Бібліотека валідує те, що повертається, перш ніж довіряти: слово скидається і лічиться в Info.DroppedWordCount, коли його бокс виходить з бітмапа, коли Right <= Left чи Bottom <= Top, або коли Confidence поза 0..1 чи нижче MinimumConfidence. Повернення більше слів, ніж MaxWordsPerPage, чи виштовхування сумарного рахунка за MaxTotalWords валить увесь виклик із бюджетною помилкою, тож шануйте Request.MaxWords у рушії. Не конвертуйте бокси слів у user space перед поверненням; HotPDF трактував би ті точкові значення як пікселі, і шар стиснувся б до початку координат бітмапа
Мапінг виходу власного детектора
Той самий хелпер обслуговує власний конвеєр на базі RenderLoadedPageToBitmap, який рендерить видимий бокс і застосовує /Rotate так само, як і функції розпізнавання. Читайте бокс через GetLoadedPageVisibleBox, нормалізуйте ротацію так само, як HotPDF, і мапте два протилежні кути кожного піксельного бокса. Вісь Y перевертається, а при 90 і 270 градусах осі міняються місцями, тож змаплені кути виходять у ніякому фіксованому порядку; беріть мінімум і максимум змаплених точок — саме так HotPDF будує межі штрихкодів
const
DPI = 200;
var
Pdf: THotPDF;
Bmp: TBitmap;
VL, VB, VR, VT: Single;
Rotation: Integer;
PxL, PxT, PxR, PxB, X1, Y1, X2, Y2: Double;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.LoadFromFile('scanned-ids.pdf');
if not Pdf.GetLoadedPageVisibleBox(0, VL, VB, VR, VT) then Exit;
Rotation := Pdf.GetLoadedPageRotation(0) mod 360;
if Rotation < 0 then Inc(Rotation, 360);
if (Rotation <> 90) and (Rotation <> 180) and (Rotation <> 270) then
Rotation := 0;
Bmp := Pdf.RenderLoadedPageToBitmap(0, DPI);
if Bmp = nil then Exit;
try
MyDetector(Bmp, PxL, PxT, PxR, PxB); // ваш код, піксельний бокс
HotPixelToPage(Rotation, DPI, Bmp.Height, VL, VB, VR, VT,
PxL, PxT, X1, Y1);
HotPixelToPage(Rotation, DPI, Bmp.Height, VL, VB, VR, VT,
PxR, PxB, X2, Y2);
Writeln(Format('User-space box [%.2f %.2f %.2f %.2f]',
[Min(X1, X2), Min(Y1, Y2), Max(X1, X2), Max(Y1, Y2)]));
finally
Bmp.Free;
end;
finally
Pdf.Free;
end;
end;
Поведінка ротації висвітлена глибше в розплющуванні ротації сторінки без ламання боксів сторінки, а конвеєр декодування штрихкодів, який споживає те саме перетворення, — в декодуванні повернених QR-кодів зі сторінок PDF. Якщо ваш рушій загортає зовнішній розпізнавач, адаптер Tesseract OCR для пошукового PDF показує бік ізоляції процесу та скасування того самого інтерфейсу
Шпаргалка: мапінг координат, безпечний щодо CropBox
- Рендерер растеризує видимий бокс — CropBox, притиснутий до MediaBox (ISO 32000-1 §14.11.2); кожен мапінг піксель-сторінка мусить користуватися тим боксом, прочитаним через
GetLoadedPageVisibleBox - HotPDF v2.770.153 полагодив
ApplyLoadedOCRTextLayer; v2.770.154 полагодив загальносторінковіDecodeLoadedPageBarcodesі результати облич ізDetectLoadedRedactionFindings; постраждалі збірки від v2.766.64 до тих версій - Бокси
THPDFOCRWord— це пікселі бітмапа з початком згори ліворуч;GetLoadedPageBoxіGetLoadedPageVisibleBoxповертають PDF user space з початком знизу ліворуч іBottom < Top - Масштаб — це
DPI / 72; виводьте його з DPI, ніколи не з бокса сторінки, поділеного на ширину бітмапа - /Rotate вирішує, які краї важать: Left і Bottom при 0 і 90, Right і Bottom при 180, Right і Top при 270
- Поверніть слова OCR у пікселях і дозвольте HotPDF мапити їх; конвертуйте лише для власної фільтрувальної логіки
- Переганяйте OCR на обрізаних сторінках, оброблених постраждалою збіркою, і пам'ятайте, що
SkipPagesWithTextпропускає сторінки, які вже несуть старий шар
Функції розпізнавання, запити боксів сторінок і рендеринг завантаженого документа, ужиті тут, постачаються в компоненті HotPDF для Delphi і C++Builder; ліцензування, пробні завантаження і повний список можливостей — на сторінці компонента HotPDF Delphi PDF