Text layer OCR, biên barcode và khối che mặt (redaction) bị lệch trên các trang PDF đã crop khi pixel bitmap được ánh xạ ngược qua MediaBox thay vì box mà renderer thực sự đã rasterize: CropBox được cắt theo MediaBox (ISO 32000-1 §14.11.2). HotPDF sửa điều này cho ApplyLoadedOCRTextLayer ở v2.770.153, và cho DecodeLoadedPageBarcodes cùng DetectLoadedRedactionFindings ở v2.770.154
Báo cáo lỗi thường tới theo kiểu này. Một kho hợp đồng scan chạy qua OCR, kết quả tìm được, và phần highlight trúng một số điều khoản lại sáng lên nửa inch bên dưới và bên trái con số in. Đa số tệp trong lô đều ổn. Những tệp hỏng đều đến từ một trạm scan duy nhất có ghi /CropBox để cắt mép bàn kính. Chính chi tiết ấy tách bức ảnh mà OCR engine nhìn thấy khỏi khung mà text layer được đặt vào, và cùng sự lệch đó kéo theo biên barcode và, nghiêm trọng hơn, các khối che mặt
Vì sao text layer OCR trôi ra xa các chữ đã scan?
Text layer trôi vì hai nửa của pipeline bất đồng về hình chữ nhật mà bitmap phủ. Ở v2.766.64, HotPDF đổi rendering, export SVG, viewer và in ấn để tôn trọng CropBox: một trang được hiển thị qua CropBox của nó cắt theo MediaBox, đúng điều ISO 32000-1 §14.11.2 quy định, và GetLoadedPageVisibleBox được thêm vào để trả về box nhìn thấy được ấy. Các tính năng nhận diện vẫn dựng phép biến đổi device-to-page của mình từ GetLoadedPageBox(PageIndex, pbMediaBox, ...). Raster giờ phủ box nhìn thấy được, phép biến đổi vẫn giả định MediaBox, và mọi vị trí được nhận diện quay về lệch đúng khoảng cách giữa hai cái
Cửa sổ bị ảnh hưởng vì thế rất rõ ràng. ApplyLoadedOCRTextLayer đặt sai chỗ văn bản từ v2.766.64 tới v2.770.152. DecodeLoadedPageBarcodes cả trang và phần phát hiện mặt trong DetectLoadedRedactionFindings sai lâu hơn một bản build, tới hết v2.770.153. Trước v2.766.64, renderer vẽ trọn MediaBox, nên phép ánh xạ và raster khớp nhau, đổi lấy việc nhận diện cả nội dung mà viewer chẳng bao giờ hiển thị. Các bản sửa đổi ba thứ cùng lúc cho từng tính năng: phép biến đổi, ước lượng ngân sách pixel, và page box trao cho một engine tùy chỉnh trong record yêu cầu
Vài trường hợp chưa bao giờ bị ảnh hưởng:
- Các trang không có
/CropBox, hay có CropBox trùng MediaBox, ánh xạ như nhau trước và sau bản sửa DecodeLoadedPageBarcodesvớiHasRegionđược đặt render đúng region bạn truyền và ánh xạ qua chính region ấy, nên việc decode theo region tường minh luôn đúng; phép kiểm region nằm trong trang vẫn dùng MediaBox- Các phát hiện redaction theo mẫu (email, số thẻ và tương tự) đến từ trích văn bản trong user space, không từ raster, nên chỉ các phát hiện từ face detection bị xê dịch
Ba hệ tọa độ, và API HotPDF nào dùng hệ nào
Code HotPDF chạm tới phần nhận diện phải đối phó với ba hệ, và phần lớn bug ánh xạ đến từ việc trộn lẫn hai trong số đó
- Pixel bitmap: gốc trên-trái, Y hướng xuống, đơn vị là pixel theo DPI của yêu cầu.
THPDFOCRWord.Left,Top,RightvàBottomnằm trong hệ này, cùng các điểm baseline tùy chọn, kết quả mà mộtIHPDFBarcodeDecodertùy chỉnh trả về, và các box từ mộtIHPDFFaceDetectortùy chỉnh - PDF user space của một trang đã nạp: gốc dưới-trái, Y hướng lên, đơn vị là point, với
Bottom < Top.GetLoadedPageBoxvàGetLoadedPageVisibleBoxtrả về Left, Bottom, Right, Top trong hệ này, và các trườngPageLeft,PageBottom,PageRight,PageTopcủaTHPDFOCRRequest, các biên trongTHPDFDecodedBarcodecùng các hình chữ nhật trongTHPDFRedactionFindingcũng vậy - Tọa độ vẽ trang HotPDF: API bạn dùng để dựng các trang mới (xuất văn bản, hình khối, barcode, link, trường biểu mẫu) làm việc với gốc trên-trái và Y hướng xuống. Hệ đó thuộc về phần sinh tài liệu và chẳng liên quan gì tới các API tài liệu đã nạp ở trên, nên đừng bao giờ nạp thẳng một hình chữ nhật user space của trang đã nạp vào đó
Record từ OCR được cố ý dựa trên pixel: engine báo lại những gì nó nhìn thấy trong ảnh, còn ApplyLoadedOCRTextLayer nắm phần chuyển đổi. Sự phân công đó chỉ hoạt động khi phép chuyển đổi dùng đúng box, và đó là điều v2.770.153 khôi phục lại
Phép biến đổi device-to-page đứng sau OCR, barcode và nhận diện mặt
HotPDF ánh xạ pixel bitmap sang trang bằng một ma trận affine duy nhất dựng từ năm input: góc xoay, tỉ lệ DPI / 72, chiều cao bitmap, và Left, Bottom, Right, Top của box đã render. OCR, decode barcode và nhận diện mặt dùng chung một thủ tục cho việc này, nên một input box sai đã làm hỏng cả ba theo cùng một kiểu. Với trang không xoay, ma trận page-to-device [A B C D E F] là:
A = ScalevàD = -Scale, vớiScale = DPI / 72; giá trịDâm lật user space (Y hướng lên) thành bitmap space (Y hướng xuống)B = C = 0, vì một trang không xoay chẳng có shear hay trao đổi trục nàoE = -Left * Scale, đưa cạnh trái của box về cột pixel 0F = BitmapHeight + Bottom * Scale, ánh xạ cạnh dưới của box tới y = BitmapHeight — cạnh dưới của bitmap — để cạnh trên đáp vào hàng 0
Pixel quay ngược về trang qua ma trận nghịch đảo của phép trên. Request.PageRotation mang /Rotate của trang đã chuẩn hóa về 0, 90, 180 hay 270 (mọi giá trị không phải bội của 90 được coi là 0), và renderer xoay trang theo chiều kim đồng hồ như ISO 32000-1 §7.7.3.3 yêu cầu. Khi có xoay, các trục trao chỗ cho nhau và một cặp cạnh box khác bị ghim vào gốc bitmap. Viết ra thành các công thức nghịch đảo, với S = DPI / 72, x và y theo pixel và H là chiều cao bitmap:
| /Rotate | Trang X | Trang Y | Các cạnh box mà phép ánh xạ dựa vào |
|---|---|---|---|
| 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 |
Cột cuối giải thích vì sao bug nhìn như ngẫu nhiên trên môi trường thật. Một CropBox chỉ cắt phần đỉnh trang thì không đụng tới Left và Bottom, nên các trang đứng thẳng ra hoàn hảo và chỉ các trang mang /Rotate 270 mới lệch. Xoay còn tráo cả kích thước bitmap: ở 90 và 270, bitmap rộng (Top - Bottom) * S pixel và cao (Right - Left) * S pixel
Chuyện gì sai với MediaBox [0 0 612 792] và CropBox [36 36 576 756]?
Với một vết cắt nửa inch mỗi phía, text layer của một trang không xoay đáp lệch đúng 36 point sang trái và 36 point xuống dưới so với các chữ đã scan nếu MediaBox được dùng. Lấy một trang US Letter có CropBox cắt 36 point (0.5 inch) mỗi cạnh. Box nhìn thấy được là 540 nhân 720 point, nên ở độ phân giải OCR mặc định 300 DPI, tỉ lệ là 300 / 72 ≈ 4.1667 và bitmap là 2250 nhân 3000 pixel
Giả sử engine báo một từ với box pixel Left 450, Top 600, Right 900, Bottom 660 và không có baseline. HotPDF khi đó đặt baseline cách cạnh dưới 20 phần trăm chiều cao của từ, tại hàng pixel 648, và ánh xạ điểm bắt đầu (450, 648):
- Qua box nhìn thấy được: x = 36 + 450 / 4.1667 = 144.0 và y = 36 + (3000 - 648) / 4.1667 = 600.48, đúng chỗ chữ được in
- Qua MediaBox: x = 0 + 108.0 = 108.0 và y = 0 + 564.48 = 564.48, một dịch chuyển đều (-36, -36) point
Xoay cùng trang đó thì hướng của lỗi đổi, vì những cạnh khác bị dính vào. Ở /Rotate 180, số hạng X dùng Right, và 612 thay vì 576 đẩy layer sang phải 36 point trong khi Bottom vẫn kéo nó xuống 36 point. Ở /Rotate 270, cả Right lẫn Top đều quá lớn, nên layer dịch 36 point sang phải và 36 point lên trên. Một tài liệu với các hướng trộn lẫn có thể hiện sự trôi theo ba hướng — một dấu vân tay đáng tin cho bug này. Code tự viết suy tỉ lệ từ box, kiểu Bitmap.Width / (Right - Left), còn kéo giãn mọi tọa độ thêm 612 / 540, khoảng 13 phần trăm, chồng lên phần lệch
Tài liệu PDF nào của bạn bị ảnh hưởng?
Một tài liệu PDF bị phơi nhiễm khi ít nhất một trang có box nhìn thấy được khác MediaBox của nó, và HotPDF có thể báo cho bạn điều đó trong vài dòng. So GetLoadedPageBox với pbMediaBox với GetLoadedPageVisibleBox cho từng trang, và in kèm GetLoadedPageRotation để đoán trước hướng trôi từ bảng ở trên. THPDFPageBoundary còn có pbCropBox, pbBleedBox, pbTrimBox và pbArtBox, nhưng GetLoadedPageBox(pbCropBox) lùi về MediaBox khi không có crop box nào và không cắt, nên box nhìn thấy được mới là thứ đúng để so
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;
// mảng lưu có thể liệt kê các góc theo thứ tự bất kỳ
if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
// đã chuẩn hóa và cắt theo 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;
Hai chi tiết của GetLoadedPageVisibleBox quan trọng với các script kiểu này. Hàm không đụng tới các tham số out của nó khi thất bại, nên đặt sẵn một cỡ trang mặc định trước lời gọi là một mẫu an toàn. Và khi một CropBox dị dạng chẳng giao với MediaBox chút nào, hàm trả về MediaBox thay vì một hình chữ nhật rỗng. Nếu báo cáo liệt kê các trang và bản build bạn triển khai cũ hơn v2.770.153 cho OCR, hay v2.770.154 cho barcode và mặt, hãy chạy lại nhận diện trên các trang ấy sau khi nâng cấp. Một OCR layer được ghi bởi bản build bị ảnh hưởng vẫn nằm trong tệp đã lưu, và option mặc định SkipPagesWithText sẽ bỏ qua những trang đó ở lượt hai trừ khi bạn tắt nó đi hay xóa lớp cũ trước
Một IHPDFOCREngine tùy chỉnh nên ánh xạ pixel ngược về PDF space thế nào?
Một IHPDFOCREngine tùy chỉnh nên trả về các word box theo pixel bitmap và để HotPDF lo ánh xạ; chỉ chuyển sang user space cho các quyết định của riêng bạn, và khi đó dùng box trong yêu cầu, đừng bao giờ dùng MediaBox. Kể từ v2.770.153, các trường PageLeft, PageBottom, PageRight và PageTop của yêu cầu mô tả box nhìn thấy được đã render, nên chúng khớp Request.Bitmap chính xác. Helper dưới đây là nghịch đảo của phép biến đổi của thư viện, gồm cả việc dùng chiều cao bitmap thực cho các trang đứng thẳng, nên nó khớp HotPDF tới từng pixel
uses
System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;
// Pixel bitmap (gốc trên-trái, Y hướng xuống) sang PDF user space
// (gốc dưới-trái, Y hướng lên), qua box mà bitmap được render từ đó
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;
Một lý do thực tế khiến bạn cần user space bên trong engine là quy tắc theo vùng: những tờ hóa đơn mà phần letterhead bạn không bao giờ muốn tìm kiếm được, hay một vùng con dấu làm recognizer bối rối. Engine dưới đây, viết bằng TInterfacedObject để đếm tham chiếu lo vòng đời, lọc các từ theo vị trí tâm của chúng rơi trên trang, rồi trả lại những từ sống sót nguyên trạng theo tọa độ pixel. RunRecognizer đứng thay cho lời gọi recognizer của bạn
type
TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
private
FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single; // user space
function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
out Words: THPDFOCRWords): boolean; // recognizer của bạn, box pixel
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]; // vẫn là pixel: HotPDF tự ánh xạ chúng
Inc(Count);
end;
SetLength(Words, Count);
Result := True;
end;
Trao engine cho ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) như với bất kỳ engine nào khác. Thư viện kiểm chứng những gì quay về trước khi tin: một từ bị bỏ và đếm vào Info.DroppedWordCount khi box của nó lọt ra ngoài bitmap, khi Right <= Left hay Bottom <= Top, hay khi Confidence ngoài 0..1 hoặc dưới MinimumConfidence. Trả về nhiều hơn MaxWordsPerPage từ, hay đẩy tổng cộng dồn vượt MaxTotalWords, sẽ làm cả lời gọi thất bại với một lỗi ngân sách, nên hãy tôn trọng Request.MaxWords trong engine. Đừng chuyển các word box sang user space trước khi trả về; HotPDF sẽ coi các giá trị point đó là pixel và layer sẽ sụp về gốc bitmap
Ánh xạ đầu ra detector tự viết của bạn
Cùng helper ấy phục vụ một pipeline tự dựng trên RenderLoadedPageToBitmap, thứ render box nhìn thấy được và áp /Rotate y như các tính năng nhận diện. Đọc box bằng GetLoadedPageVisibleBox, chuẩn hóa góc xoay cùng kiểu HotPDF làm, và ánh xạ hai góc đối diện của mỗi box pixel. Trục Y lật, và ở 90 cùng 270 độ, các trục tráo chỗ, nên các góc đã ánh xạ ra theo thứ tự không cố định; hãy lấy min và max của các điểm đã ánh xạ, cũng chính là cách HotPDF dựng biên 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); // code của bạn, box pixel
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;
Hành vi xoay được bàn sâu hơn trong làm phẳng góc xoay trang mà không phá các page box, còn pipeline decode barcode tiêu thụ cùng phép biến đổi đó nằm trong decode mã QR xoay từ các trang PDF. Nếu engine của bạn bọc một recognizer ngoài, adapter OCR Tesseract cho PDF có thể tìm kiếm cho thấy phía cô lập tiến trình và hủy của cùng interface đó
Tra nhanh: ánh xạ tọa độ an toàn với CropBox
- Renderer rasterize box nhìn thấy được, tức CropBox cắt theo MediaBox (ISO 32000-1 §14.11.2); mọi phép ánh xạ pixel-sang-trang phải dùng box đó, đọc bằng
GetLoadedPageVisibleBox - HotPDF v2.770.153 sửa
ApplyLoadedOCRTextLayer; v2.770.154 sửaDecodeLoadedPageBarcodescả trang và các phát hiện mặt từDetectLoadedRedactionFindings; các bản build từ v2.766.64 tới trước các phiên bản đó đều bị ảnh hưởng - Các box
THPDFOCRWordlà pixel bitmap với gốc trên-trái;GetLoadedPageBoxvàGetLoadedPageVisibleBoxtrả về PDF user space với gốc dưới-trái vàBottom < Top - Tỉ lệ là
DPI / 72; hãy suy nó từ DPI, đừng bao giờ từ một page box chia vào bề rộng bitmap - /Rotate quyết định những cạnh nào quan trọng: Left và Bottom ở 0 và 90, Right và Bottom ở 180, Right và Top ở 270
- Trả các từ OCR theo pixel và để HotPDF ánh xạ; chỉ chuyển đổi cho logic lọc của riêng bạn
- Chạy lại OCR trên các trang đã crop từng đi qua một bản build bị ảnh hưởng, và nhớ rằng
SkipPagesWithTextbỏ qua các trang đang mang lớp cũ
Các tính năng nhận diện, các truy vấn page box và rendering tài liệu đã nạp dùng ở đây đều đi kèm HotPDF component cho Delphi và C++Builder; giấy phép, bản dùng thử và danh sách tính năng đầy đủ nằm trên trang HotPDF Delphi PDF component