OCR текстови слоеве, barcode граници и кутии за заличаване на лица се изместват при отрязани PDF страници, когато bitmap пикселите се картират обратно през MediaBox вместо през кутията, която рендерерът действително е растеризирал: CropBox, отрязан до MediaBox (ISO 32000-1 §14.11.2). HotPDF оправи това за ApplyLoadedOCRTextLayer във v2.770.153, а за DecodeLoadedPageBarcodes и DetectLoadedRedactionFindings — във v2.770.154
Докладът за бъга, който обикновено пристига, изглежда така. Архив със сканирани договори минава през OCR, изходът е търсим, а попадението при търсене на номер на клауза се оцветява половин инч под и вляво от отпечатания номер. Повечето файлове в партидата са наред. Развалените всички идват от една сканираща станция, записваща /CropBox, за да отреже полето на плотата. Тая една подробност разделя картината, която OCR engine-ът е видял, от рамката, в която текстовият слой е поставен, а същото разминаване измества barcode граници и, по-сериозно, кутиите за заличаване на лица
Защо OCR текстовият слой се измества от сканираните думи?
Текстовият слой се измества, защото две половини на тръбопровода не се съгласяват кой правоъгълник покрива bitmap-ът. Във v2.766.64 HotPDF промени рендирането, SVG export-а, прегледача и печатането да зачитат 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, така че картирането и растерът съвпадаха, на цената на разпознаване на съдържание, което прегледачите никога не показват. Поправките смениха три неща заедно за всяка функция: трансформацията, оценката на пикселния бюджет и страничната кутия, подавана на персонален engine в записа на заявката
Няколко случая никога не бяха засегнати:
- Страници без
/CropBoxили с CropBox, равен на MediaBox, се картират идентично преди и след поправката DecodeLoadedPageBarcodesсъс зададенHasRegionрендира точно региона, който подавате, и картира през същия регион, така че декодирането с изричен регион беше вярно през цялото време; проверката, че регионът лежи вътре в страницата, все още ползва MediaBox- Находки за заличаване по шаблон (имейли, номера на карти и така нататък) идват от извличане на текст в user space, не от растер, така че само находките от детекция на лица се изместиха
Три координатни системи и кои HotPDF API ползват коя
HotPDF код, който пипа разпознаването, работи с три системи, а повечето бъгове при картирането идват от смесването на две от тях
- Bitmap пиксели: начало горе-вляво, 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-ят, с който строите нови страници (текстови изходи, фигури, barcodes, линкове, полета на формуляри), работи с начало горе-вляво и Y, растящ надолу. Тази система принадлежи на генерирането на документи и няма нищо общо с API-те за заредени документи по-горе, така че никога не подавайте правоъгълник в user space на заредена страница в него непроменен
Записът на OCR думата е нарочно в пиксели: engine докладва каквото е видял в изображението, а ApplyLoadedOCRTextLayer притежава конверсията. Това разделение работи само когато конверсията ползва правилната кутия, което v2.770.153 възстанови
Трансформацията устройство-към-страница зад OCR, barcodes и лица
HotPDF картира bitmap пикселите към страницата с единствена афинна матрица, построена от пет входа: въртенето, мащабът DPI / 72, височината на bitmap-а и Left, Bottom, Right и Top на рендираната кутия. OCR, декодирането на barcodes и детекцията на лица споделят една рутина за това, което е причината един грешен вход на кутия да счупи и трите по един и същ начин. За невъртяна страница матрицата страница-към-устройство [A B C D E F] е:
A = ScaleиD = -Scale, къдетоScale = DPI / 72; отрицателниятDобръща user space (Y нагоре) в bitmap пространство (Y надолу)B = C = 0, защото невъртяна страница няма shear или размяна между оситеE = -Left * Scale, което мести левия ръб на кутията на пикселна колона 0F = BitmapHeight + Bottom * Scale, което картира долния ръб на кутията на y = BitmapHeight, долния ръб на bitmap-а, така че горният ръб каца на ред 0
Пикселите се връщат към страницата през обратната на тази матрица. Request.PageRotation носи /Rotate на страницата, нормализирано до 0, 90, 180 или 270 (всяка стойност, която не е кратно на 90, се третира като 0), а рендерерът върти страницата по часовниковата стрелка, както изисква ISO 32000-1 §7.7.3.3. При въртене осите се разменят и друга двойка ръбове на кутията се закача за началото на bitmap-а. Записано като обратни формули, с S = DPI / 72, x и y в пиксели и H — височината на bitmap-а:
| /Rotate | Page X | Page 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 се изместваха. Въртенето също разменя размерностите на bitmap-а: при 90 и 270 bitmap-ът е (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, а bitmap-ът е 2250 на 3000 пиксела
Да речем, че engine докладва дума с пикселна кутия 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 кутия, и не отрязва, така че видимата кутия е правилното нещо, с което да сравнявате
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 за barcodes и лица, пуснете отново разпознаването на тези страници след надграждането. OCR слой, записан от засегнат билд, остава в запазения файл, а опцията по подразбиране SkipPagesWithText ще прескача тези страници при втори проход, освен ако не я изключите или не махнете стария слой първо
Как трябва персонален IHPDFOCREngine да картира пикселите обратно в PDF пространството?
Персонален IHPDFOCREngine трябва да връща кутиите на думите в bitmap пиксели и да остави HotPDF да върши картирането; конвертирайте към user space само за собствени решения и тогава ползвайте кутията от заявката, никога MediaBox. От v2.770.153 полетата PageLeft, PageBottom, PageRight и PageTop на заявката описват рендираната видима кутия, така че съвпадат с Request.Bitmap точно. Помощникът по-долу е обратната на трансформацията на библиотеката, включително ползването ѝ на действителната височина на bitmap-а за изправени страници, така че съвпада с HotPDF до пиксел
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Bitmap пиксел (начало горе-вляво, Y надолу) към PDF user space
// (начало долу-вляво, Y нагоре), през кутията, от която bitmap-ът е рендиран
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 вътре в engine е правило за зони: фактури, чиито бланки никога не искате търсими, или печатна област, объркваща разпознавача. Engine-ът по-долу, написан с 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;
Подайте engine-а на ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) както с всеки друг engine. Библиотеката валидира това, което се връща, преди да му вярва: дума се изпуска и брои в Info.DroppedWordCount, когато кутията ѝ излезе от bitmap-а, когато Right <= Left или Bottom <= Top, или когато Confidence е извън 0..1 или под MinimumConfidence. Връщането на повече думи от MaxWordsPerPage или бутането на текущия сбор над MaxTotalWords проваля целия разговор с бюджетна грешка, така че зачитайте Request.MaxWords в engine-а. Не конвертирайте кутиите на думите към user space преди да ги върнете; HotPDF би приел точковите стойности за пиксели и слоят би се сринал към началото на bitmap-а
Картиране на изхода на собствен детектор
Същият помощник обслужва домашен тръбопровод, построен върху RenderLoadedPageToBitmap, който рендира видимата кутия и прилага /Rotate точно както правят функциите за разпознаване. Прочетете кутията с GetLoadedPageVisibleBox, нормализирайте въртенето по същия начин като HotPDF и картирайте два противоположни ъгъла на всяка пикселна кутия. Ос Y се обръща и при 90 и 270 градуса осите се разменят, така картираните ъгли излизат без фиксиран ред; вземете минимума и максимума на картираните точки, което е и начинът, по който HotPDF строи barcode границите
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;
Поведението при въртене е разгледано по-задълбочено в изравняването на въртене на страница без чупене на страничните кутии, а тръбопроводът за декодиране на barcodes, ползващ същата трансформация — в декодирането на завъртяни QR кодове от PDF страници. Ако engine-ът ви опакова външен разпознавач, Tesseract OCR adapter-ът за търсим 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са bitmap пиксели с начало горе-вляво;GetLoadedPageBoxиGetLoadedPageVisibleBoxвръщат PDF user space с начало долу-вляво иBottom < Top - Мащабът е
DPI / 72; извеждайте го от DPI, никога от странична кутия, разделена на ширината на bitmap-а - /Rotate решава кои ръбове имат значение: Left и Bottom при 0 и 90, Right и Bottom при 180, Right и Top при 270
- Връщайте OCR думи в пиксели и оставете HotPDF да ги картира; конвертирайте само за собствена филтрираща логика
- Пуснете отново OCR върху отрязани страници, обработени от засегнат билд, и помнете, че
SkipPagesWithTextпрескача страници, които вече носят стария слой
Функциите за разпознаване, запитванията за странични кутии и рендирането на заредени документи, ползвани тук, идват всички в HotPDF компонента за Delphi и C++Builder; лицензиране, trial изтегляния и пълният списък с функции са на страницата на HotPDF Delphi PDF компонент