Technisch artikel

HotPDF OCR-tekstlaag-offset: mappen via de CropBox

OCR-tekstlagen, barcode-grenzen en gezichtsredactie-boxen drijven af op bijgesneden PDF-pagina's zodra bitmappixels via de MediaBox worden terug gemapt in plaats van via de box die de renderer werkelijk heeft gerasterd: de CropBox afgeknipt op de MediaBox (ISO 32000-1 §14.11.2). HotPDF heeft dit voor ApplyLoadedOCRTextLayer opgelost in v2.770.153, en voor DecodeLoadedPageBarcodes en DetectLoadedRedactionFindings in v2.770.154

Het bugrapport dat doorgaans binnenkomt ziet er zo uit. Een gescand contractarchief gaat door OCR, de uitvoer is doorzoekbaar, en de zoektreffer voor een clausulenummer wordt gemarkeerd op een halve inch eronder en links van het gedrukte nummer. De meeste bestanden in de batch zijn in orde. De gebroken exemplaren komen allemaal van één scanstation dat een /CropBox wegschrijft om de platrand bij te snijden. Dat ene detail scheidt het beeld dat de OCR-engine zag van het kader waarin de tekstlaag werd geplaatst, en dezelfde mismatch verplaatst barcode-grenzen en, serieuzer, gezichtsredactie-boxen

Waarom drijft de OCR-tekstlaag weg van de gescande woorden?

De tekstlaag drijft af omdat twee helften van de pijplijn het oneens waren over welke rechthoek de bitmap dekt. In v2.766.64 veranderde HotPDF rendering, SVG-export, de viewer en afdrukken zodat ze de CropBox eren: een pagina wordt getoond via zijn CropBox afgeknipt op zijn MediaBox, wat ISO 32000-1 §14.11.2 voorschrijft, en GetLoadedPageVisibleBox werd toegevoegd om die zichtbare box terug te geven. De herkenningsfuncties bleven hun device-naar-pagina-transformatie bouwen vanuit GetLoadedPageBox(PageIndex, pbMediaBox, ...). De raster dekte nu de zichtbare box, de transformatie ging nog steeds uit van de MediaBox, en elke herkende positie kwam terug verschoven om de afstand tussen de twee

Het getroffen venster is daarmee precies te trekken. ApplyLoadedOCRTextLayer zette tekst verkeerd van v2.766.64 tot en met v2.770.152. Volledige-pagina DecodeLoadedPageBarcodes en de gezichtsdetectie binnen DetectLoadedRedactionFindings bleven één build langer fout, tot en met v2.770.153. Vóór v2.766.64 tekende de renderer de hele MediaBox, dus mapping en raster waren het eens, ten koste van het herkennen van content die viewers nooit tonen. De fixes veranderden drie dingen tegelijk per functie: de transformatie, de pixelbudgetschatting en de paginabox die aan een custom engine in het requestrecord wordt meegegeven

Enkele gevallen zijn nooit getroffen:

  • Pagina's zonder /CropBox, of waarvan de CropBox gelijk is aan de MediaBox, mappen voor en na de fix identiek
  • DecodeLoadedPageBarcodes met HasRegion gezet rendert exact de regio die u doorgeeft en mapt via diezelfde regio, dus expliciete-regio-decodering was de hele tijd correct; de controle dat de regio binnen de pagina ligt, gebruikt nog steeds de MediaBox
  • Op patronen gebaseerde redaction findings (e-mailadressen, kaartnummers enzovoort) komen uit tekstextractie in user space, niet uit een raster, dus alleen de gezichtsdetectie-findings versprongen

Drie coördinaatframes, en welke HotPDF-API's elk gebruiken

HotPDF-code die herkenning aanraakt, werkt met drie frames, en de meeste mappingbugs komen van het door elkaar halen van twee ervan

  • Bitmappixels: origine linksboven, Y groeit omlaag, de eenheden zijn pixels bij de aangevraagde DPI. THPDFOCRWord.Left, Top, Right en Bottom zitten in dit frame, net als de optionele baseline-punten, de resultaten die een custom IHPDFBarcodeDecoder teruggeeft en de boxes van een custom IHPDFFaceDetector
  • PDF user space van een geladen pagina: origine linksonder, Y groeit omhoog, de eenheden zijn punten, met Bottom < Top. GetLoadedPageBox en GetLoadedPageVisibleBox geven Left, Bottom, Right, Top terug in dit frame, en dat geldt ook voor de velden PageLeft, PageBottom, PageRight en PageTop van THPDFOCRRequest, de grenzen in THPDFDecodedBarcode en de rechthoeken in THPDFRedactionFinding
  • HotPDF pagina-tekencoördinaten: de API waarmee u nieuwe pagina's bouwt (tekstuitvoer, vormen, barcodes, links, formuliervelden) werkt met een origine linksboven en Y groeiend omlaag. Dat frame hoort bij documentgeneratie en heeft niets te maken met de hierboven genoemde geladen-document-API's, dus voer er nooit ongewijzigd een user-space-rechthoek van een geladen pagina in

Het OCR-wordrecord is bewust pixelgebaseerd: een engine meldt wat hij in de afbeelding zag, en ApplyLoadedOCRTextLayer bezit de conversie. Die scheiding werkt alleen als de conversie de juiste box gebruikt, en dat is wat v2.770.153 herstelde

HotPDF herkenningscoördinaatframes: bitmappixels met origine linksboven gebruikt door THPDFOCRWord-boxen en custom decoders, PDF user space met origine linksonder teruggegeven door GetLoadedPageBox en GetLoadedPageVisibleBox, en de top-left pagina-teken-API, die nooit ongewijzigd een rechthoek van een geladen pagina mag ontvangen
engines melden pixels omdat dat is wat ze zagen, HotPDF mapt ze, en de twee frames door elkaar halen is hoe lagen en redactie-boxen afdrijven

De device-naar-pagina-transformatie achter OCR, barcodes en gezichten

HotPDF mapt bitmappixels naar de pagina met één affiene matrix gebouwd uit vijf invoeren: de rotatie, de schaal DPI / 72, de bitmaphoogte en de Left, Bottom, Right en Top van de gerenderde box. OCR, barcodedecodering en gezichtsdetectie delen één routine daarvoor, en daarom brak één verkeerde box-invoer alle drie op dezelfde manier. Voor een ongedraaide pagina is de pagina-naar-device-matrix [A B C D E F]:

  • A = Scale en D = -Scale, waarbij Scale = DPI / 72; de negatieve D klapt user space (Y omhoog) om naar bitmapruimte (Y omlaag)
  • B = C = 0, want een ongedraaide pagina heeft geen shear of assenwissel
  • E = -Left * Scale, wat de linkerboxrand naar pixelkolom 0 verplaatst
  • F = BitmapHeight + Bottom * Scale, wat de onderrand van de box op y = BitmapHeight mapt, de onderrand van de bitmap, zodat de bovenrand op rij 0 uitkomt

Pixels gaan via de inverse van die matrix terug naar de pagina. Request.PageRotation voert de /Rotate van de pagina genormaliseerd naar 0, 90, 180 of 270 (elke waarde die geen veelvoud van 90 is, geldt als 0), en de renderer draait de pagina met de klok mee zoals ISO 32000-1 §7.7.3.3 vereist. Onder rotatie wisselen de assen en zit een ander paar boxranden vast aan de bitmaporigine. Uitgeschreven als inverse formules, met S = DPI / 72, x en y in pixels en H de bitmaphoogte:

/RotatePagina-XPagina-YBoxranden waar de mapping van afhangt
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

De laatste kolom verklaart waarom de bug in productie willekeurig leek. Een CropBox die alleen de bovenkant van de pagina bijsnijdt laat Left en Bottom onaangeroerd, dus rechtopstaande pagina's kwamen er perfect uit en alleen pagina's met /Rotate 270 dreven af. Rotatie wisselt bovendien de bitmapdimensies: bij 90 en 270 is de bitmap (Top - Bottom) * S pixels breed en (Right - Left) * S pixels hoog

HotPDF inverse mappingtabel voor paginarotatie: bij /Rotate 0 en 90 zet de transformatie de Left- en Bottom-randen van de gerenderde box vast, bij 180 Right en Bottom, bij 270 Right en Top, en daarom drijft een bijgesneden pagina voor elke oriëntatie in een gemengd document in een andere richting
dezelfde half-inch crop oogt als drie verschillende bugs zodra pagina's verschillende /Rotate-waarden voeren, want elke oriëntatie zet een ander paar boxranden vast

Wat gaat er mis met MediaBox [0 0 612 792] en CropBox [36 36 576 756]?

Met een half-inch crop aan elke kant landt de tekstlaag van een ongedraaide pagina precies 36 punten links van en 36 punten onder de gescande woorden zodra de MediaBox wordt gebruikt. Neem een US Letter-pagina waarvan de CropBox 36 punten (0,5 inch) van elke rand afsnijdt. De zichtbare box is 540 bij 720 punten, dus bij de default OCR-resolutie van 300 DPI is de schaal 300 / 72 ≈ 4.1667 en is de bitmap 2250 bij 3000 pixels

Stel dat de engine een woord meldt met pixelbox Left 450, Top 600, Right 900, Bottom 660 en geen baseline. HotPDF zet de baseline dan 20 procent van de woordhoogte boven de onderrand, op pixelrij 648, en mapt het startpunt (450, 648):

  • Via de zichtbare box: x = 36 + 450 / 4.1667 = 144.0 en y = 36 + (3000 - 648) / 4.1667 = 600.48, en dat is waar het woord gedrukt staat
  • Via de MediaBox: x = 0 + 108.0 = 108.0 en y = 0 + 564.48 = 564.48, een uniforme verschuiving van (-36, -36) punten
HotPDF CropBox-drift-anatomie op een US Letter-pagina met MediaBox 0 0 612 792 en CropBox 36 36 576 756: de renderer rasteriseert de zichtbare box op 300 DPI, dus woordpixel 450 mappen via GetLoadedPageVisibleBox geeft 144.0 en 600.48 terwijl de MediaBox-transformatie op 108.0 en 564.48 uitkomt
de raster dekt de CropBox, dus elke transformatie gebouwd vanuit de MediaBox verschuift elk herkend woord met exact de cropmarge

Draai dezelfde pagina en de richting van de fout verandert, omdat andere randen meespelen. Bij /Rotate 180 gebruikt de X-term Right, en 612 in plaats van 576 duwt de laag 36 punten naar rechts terwijl Bottom haar nog steeds 36 punten omlaag trekt. Bij /Rotate 270 zijn zowel Right als Top te groot, dus de laag schuift 36 punten rechts en 36 punten omhoog. Een document met gemengde oriëntaties kan de drift in drie richtingen tonen, een betrouwbaar vingerafdruk voor deze bug. Handgeschreven code die de schaal uit de box afleidt, zoals Bitmap.Width / (Right - Left), rekt bovendien elke coördinaat met 612 / 540 uit, ruwweg 13 procent, bovenop de verschuiving

Welke van uw PDF-documenten zijn getroffen?

Een PDF-document is blootgesteld zodra ten minste één pagina een zichtbare box heeft die verschilt van zijn MediaBox, en HotPDF kan dat in een paar regels voor u vaststellen. Vergelijk GetLoadedPageBox met pbMediaBox tegen GetLoadedPageVisibleBox voor elke pagina, en print GetLoadedPageRotation ernaast zodat u de driftrichting uit de tabel hierboven kunt voorspellen. THPDFPageBoundary biedt ook pbCropBox, pbBleedBox, pbTrimBox en pbArtBox, maar GetLoadedPageBox(pbCropBox) valt terug op de MediaBox als er geen crop box bestaat en knipt niet af, dus de zichtbare box is het juiste vergelijkingspunt

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;
      // de opgeslagen array mag zijn hoeken in willekeurige volgorde noemen
      if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
      if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
      // al genormaliseerd en tegen de MediaBox afgeknipt
      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;

Twee details van GetLoadedPageVisibleBox tellen voor scripts als dit. De functie laat zijn out-parameters onaangeroerd als ze faalt, dus vooraf een default paginaformaat zetten vóór de aanroep is een veilig patroon. En als een misvormde CropBox de MediaBox helemaal niet snijdt, geeft de functie de MediaBox terug in plaats van een lege rechthoek. Staat er een rapport met pagina's en is uw uitgerolde build ouder dan v2.770.153 voor OCR, of v2.770.154 voor barcodes en gezichten, dan draait u de herkenning op die pagina's opnieuw na de upgrade. Een OCR-laag weggeschreven door een getroffen build blijft in het opgeslagen bestand zitten, en de default SkipPagesWithText-optie zal die pagina's bij een tweede passe overslaan tenzij u haar uitzet of de oude laag eerst verwijdert

Hoe mapt een custom IHPDFOCREngine pixels terug naar PDF-ruimte?

Een custom IHPDFOCREngine geeft woordboxen in bitmappixels terug en laat HotPDF het mappen doen; converteer alleen voor uw eigen beslissingen naar user space, en gebruik dan de box uit het request, nooit de MediaBox. Sinds v2.770.153 beschrijven PageLeft, PageBottom, PageRight en PageTop van het request de gerenderde zichtbare box, dus ze matchen Request.Bitmap exact. De helper hieronder is de inverse van de transformatie van de library, inclusief haar gebruik van de werkelijke bitmaphoogte voor rechtopstaande pagina's, dus hij is het tot op de pixel met HotPDF eens

uses
  System.SysUtils, System.Math, Vcl.Graphics, HPDFDoc;

// Bitmappixel (origine linksboven, Y omlaag) naar PDF user space
// (origine linksonder, Y omhoog), via de box waaruit de bitmap is gerenderd
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;

Een realistische reden om user space binnen een engine nodig te hebben is een zoneregel: facturen waarvan u het briefhoofd nooit doorzoekbaar wilt hebben, of een stempelvlak dat de recognizer in de war stuurt. De engine hieronder, geschreven met TInterfacedObject zodat referentietelling de levensduur regelt, filtert woorden op waar hun middens op de pagina vallen, en geeft de overlevenden ongewijzigd in pixelcoördinaten terug. RunRecognizer staat model voor uw eigen recognizeraanroep

type
  TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
  private
    FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single;  // user space
    function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
      out Words: THPDFOCRWords): boolean;  // uw recognizer, pixelboxen
  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];  // nog steeds pixels: HotPDF mapt ze zelf
    Inc(Count);
  end;
  SetLength(Words, Count);
  Result := True;
end;

Geef de engine aan ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) zoals bij elke andere engine. De library valideert wat er terugkomt voordat ze het vertrouwt: een woord wordt laten vallen en geteld in Info.DroppedWordCount wanneer zijn box buiten de bitmap valt, wanneer Right <= Left of Bottom <= Top, of wanneer Confidence buiten 0..1 ligt of onder MinimumConfidence zit. Meer woorden teruggeven dan MaxWordsPerPage, of het lopende totaal voorbij MaxTotalWords duwen, laat de hele aanroep falen met een budgetfout, dus eer Request.MaxWords in de engine. Converteer woordboxen niet naar user space voordat u ze teruggeeft; HotPDF zou de puntwaarden als pixels behandelen en de laag zou naar de bitmaporigine inklappen

De uitvoer van uw eigen detector mappen

Dezelfde helper dient een eigen pijplijn gebouwd op RenderLoadedPageToBitmap, die de zichtbare box rendert en /Rotate toepast precies zoals de herkenningsfuncties doen. Lees de box met GetLoadedPageVisibleBox, normaliseer de rotatie op dezelfde manier als HotPDF, en map twee tegenovergestelde hoeken van elke pixelbox. De Y-as klapt om en, bij 90 en 270 graden, wisselen de assen, dus de gemapte hoeken komen in geen vaste volgorde naar buiten; neem het minimum en maximum van de gemapte punten, en dat is ook hoe HotPDF barcode-grenzen bouwt

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);  // uw code, pixelbox
      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;

Het rotatiegedrag wordt dieper behandeld in paginarotatie afvlakken zonder de pagina-boxen te breken, en de barcodedecodeerpijplijn die dezelfde transformatie verbruikt in gedraaide QR-codes uit PDF-pagina's decoderen. Wrapt uw engine een externe recognizer, dan toont de Tesseract OCR-adapter voor doorzoekbare PDF de procesisolatie- en annuleringskant van dezelfde interface

Snelnaslag: CropBox-veilige coördinaatmapping

  • De renderer rasteriseert de zichtbare box, de CropBox afgeknipt op de MediaBox (ISO 32000-1 §14.11.2); elke pixel-naar-pagina-mapping moet die box gebruiken, gelezen met GetLoadedPageVisibleBox
  • HotPDF v2.770.153 fixte ApplyLoadedOCRTextLayer; v2.770.154 fixte volledige-pagina DecodeLoadedPageBarcodes en gezichtsfindings uit DetectLoadedRedactionFindings; builds van v2.766.64 tot en met die versies zijn getroffen
  • THPDFOCRWord-boxen zijn bitmappixels met origine linksboven; GetLoadedPageBox en GetLoadedPageVisibleBox geven PDF user space terug met origine linksonder en Bottom < Top
  • De schaal is DPI / 72; leid haar af uit de DPI, nooit uit een paginabox gedeeld door de bitmapbreedte
  • /Rotate bepaalt welke randen tellen: Left en Bottom bij 0 en 90, Right en Bottom bij 180, Right en Top bij 270
  • Geef OCR-woorden in pixels terug en laat HotPDF ze mappen; converteer alleen voor uw eigen filterlogica
  • Draai OCR opnieuw op bijgesneden pagina's die een getroffen build heeft verwerkt, en bedenk dat SkipPagesWithText pagina's overslaat die al de oude laag voeren

De herkenningsfuncties, pagina-box-queries en geladen-document-rendering die hier worden gebruikt, worden alle geleverd in de HotPDF-component voor Delphi en C++Builder; licenties, proefdownloads en de volledige functielijst staan op de HotPDF Delphi PDF-componentpagina