Техническа статия

OCR текстовият слой и CropBox: картиране в HotPDF

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 възстанови

Координатни системи при разпознаване в HotPDF: bitmap пиксели с начало горе-вляво, ползвани от кутиите THPDFOCRWord и персонални декодери, PDF user space с начало долу-вляво, връщано от GetLoadedPageBox и GetLoadedPageVisibleBox, и API-ят за чертане на страница с начало горе-вляво, който никога не бива да получава правоъгълник от заредена страница непроменен
engine-ите докладват пиксели, защото това са видяли, HotPDF ги картира, а смесването на двете системи е начинът, по който слоевете и кутиите за заличаване се изместват

Трансформацията устройство-към-страница зад 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, което мести левия ръб на кутията на пикселна колона 0
  • F = 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-а:

/RotatePage XPage YРъбове на кутията, от които зависи картирането
0Left + x / SBottom + (H - y) / SLeft, Bottom
90Left + y / SBottom + x / SLeft, Bottom
180Right - x / SBottom + y / SRight, Bottom
270Right - y / STop - x / SRight, Top

Последната колона обяснява защо бъгът изглеждаше случаен в продукция. CropBox, отрязващ само горната част на страницата, оставя Left и Bottom недокоснати, така че изправените страници излизаха съвършени и само страници с /Rotate 270 се изместваха. Въртенето също разменя размерностите на bitmap-а: при 90 и 270 bitmap-ът е (Top - Bottom) * S пиксела широк и (Right - Left) * S пиксела висок

Таблица за обратно картиране при въртене на страница в HotPDF: при /Rotate 0 и 90 трансформацията закача ръбовете Left и Bottom на рендираната кутия, при 180 — Right и Bottom, при 270 — Right и Top, което е причината отрязана страница да се измества в различна посока за всяка ориентация в смесен документ
същото отрязване от половин инч изглежда като три различни бъга, щом страниците носят различни стойности /Rotate, защото всяка ориентация закача различна двойка ръбове на кутията

Какво се развалва с 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) точки
Анатомия на изместването от CropBox в HotPDF на страница US Letter с MediaBox 0 0 612 792 и CropBox 36 36 576 756: рендерерът растеризира видимата кутия при 300 DPI, така че картирането на пиксел 450 на дума през GetLoadedPageVisibleBox дава 144.0 и 600.48, докато трансформацията през MediaBox каца на 108.0 и 564.48
растерът покрива CropBox, така че всяка трансформация, построена от MediaBox, измества всяка разпозната дума с точно полето на отрязване

Завъртете същата страница и посоката на грешката се сменя, защото участват различни ръбове. При /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 компонент