Teknisk artikel

HotPDF OCR-textskikt offset: mappning genom CropBox

OCR-textskikt, streckkodsgränser och ansiktssvartningsboxar driver på beskurna PDF-sidor när bitmappspixlar mappas tillbaka genom MediaBox i stället för genom boxen renderaren faktiskt rastrerade: CropBox klippt mot MediaBox (ISO 32000-1 §14.11.2). HotPDF fixade detta för ApplyLoadedOCRTextLayer i v2.770.153, och för DecodeLoadedPageBarcodes och DetectLoadedRedactionFindings i v2.770.154

Buggrapporten som vanligen anländer ser ut så här. Ett inskannat kontraktsarkiv går genom OCR, utdatat är sökbart, och sökträffen för ett klausulnummer markeras en halv tum under och till vänster om det tryckta numret. De flesta filer i batchen är fina. De trasiga kom alla från en skanningsstation som skriver en /CropBox för att trimna platanmarginalen. Den där ena detaljen skiljer bilden OCR-motorn såg från ramen textskiktet placerades i, och samma missmatchning flyttar streckkodsgränser och, allvarligare, ansiktssvartningsboxar

Varför driver OCR-textskiktet iväg från de inskannade orden?

Textskiktet driver för att två halvor av pipelinen var oense om vilken rektangel bitmappen täcker. I v2.766.64 ändrade HotPDF rendering, SVG-export, visaren och utskrift till att hedra CropBox: en sida visas genom sin CropBox klippt mot sin MediaBox, vilket är vad ISO 32000-1 §14.11.2 föreskriver, och GetLoadedPageVisibleBox lades till för att returnera den synliga boxen. Igenkänningsfunktionerna fortsatte bygga sin device-till-sida-transform från GetLoadedPageBox(PageIndex, pbMediaBox, ...). Rastreringen täckte nu den synliga boxen, transformen antog fortfarande MediaBox, och varje igenkänd position kom tillbaka förskjuten med gapet mellan de två

Det drabbade fönstret är därför precist. ApplyLoadedOCRTextLayer felpacerade text från v2.766.64 till v2.770.152. Hela sidans DecodeLoadedPageBarcodes och ansiktsdetekteringen inuti DetectLoadedRedactionFindings förblev fel ett bygge längre, till v2.770.153. Före v2.766.64 ritade renderaren hela MediaBox, så mappning och rastrering överensstämde, på bekostnad av att igenkänna innehåll visare aldrig visar. Fixarna ändrade tre saker tillsammans för varje funktion: transformen, pixelbudgetuppskattningen, och sidboxen som lämnas till en egen motor i begäranposten

Flera fall drabbades aldrig:

  • Sidor utan /CropBox, eller vars CropBox är lika med MediaBox, mappas identiskt före och efter fixen
  • DecodeLoadedPageBarcodes med HasRegion satt renderar exakt den region du skickar och mappar genom samma region, så explicit regionsavkodning var korrekt hela tiden; kontrollen att regionen ligger inuti sidan använder fortfarande MediaBox
  • Mönsterbaserade svartningsfynd (mejl, kortnummer och så vidare) kommer från textextraktion i user space, inte från en rastrering, så bara ansiktsdetekteringsfynden flyttades

Tre koordinatramar, och vilka HotPDF-API:er som använder vilken

HotPDF-kod som rör igenkänning har att göra med tre ramar, och de flesta mappningsbuggar kommer från att blanda två av dem

  • Bitmappspixlar: ursprung uppe vänster, Y växer nedåt, enheten är pixlar vid begärandets DPI. THPDFOCRWord.Left, Top, Right och Bottom ligger i den ramen, liksom de valfria baslinjepunkterna, resultaten en egen IHPDFBarcodeDecoder returnerar, och boxarna från en egen IHPDFFaceDetector
  • PDF user space för en laddad sida: ursprung nere vänster, Y växer uppåt, enheten är punkter, med Bottom < Top. GetLoadedPageBox och GetLoadedPageVisibleBox returnerar Left, Bottom, Right, Top i den ramen, och det gör också fälten PageLeft, PageBottom, PageRight och PageTop hos THPDFOCRRequest, gränserna i THPDFDecodedBarcode och rektanglarna i THPDFRedactionFinding
  • HotPDF sidritningskoordinater: API:et du använder för att bygga nya sidor (textutdata, former, streckkoder, länkar, formulärfält) arbetar med ursprung uppe vänster och Y växande nedåt. Den ramen tillhör dokumentgenerering och har ingenting med de laddade dokumentens API:er ovan att göra, så mata aldrig en laddad sidas user space-rektangel in i den oförändrad

OCR-ordposten är med flit pixelbaserad: en motor rapporterar vad den såg i bilden, och ApplyLoadedOCRTextLayer äger konverteringen. Den divisionen fungerar bara när konverteringen använder rätt box, vilket är vad v2.770.153 återställde

HotPDF igenkänningskoordinatramar: bitmappspixlar med ursprung uppe vänster använda av THPDFOCRWord-boxar och egna avkodare, PDF user space med ursprung nere vänster returnerat av GetLoadedPageBox och GetLoadedPageVisibleBox, samt API:et för sidritning uppe vänster, som aldrig får ta emot en laddad sidas rektangel oförändrad
motorer rapporterar pixlar för det är vad de såg, HotPDF mappar dem, och att blanda de två ramarna är hur skikt och svartningsboxar driver

Device-till-sida-transformen bakom OCR, streckkoder och ansikten

HotPDF mappar bitmappspixlar till sidan med en enda affin matris byggd från fem inmatningar: rotationen, skalan DPI / 72, bitmappshöjden, och Left, Bottom, Right och Top hos den renderade boxen. OCR, streckkodsavkodning och ansiktsdetektering delar alla en rutin för det, vilket är varför en felaktig boxinmatning slogo sönder alla tre på samma sätt. För en oroterad sida är sid-till-device-matrisen [A B C D E F]:

  • A = Scale och D = -Scale, där Scale = DPI / 72; den negativa D vänder user space (Y uppåt) till bitmappsutrymme (Y nedåt)
  • B = C = 0, för en oroterad sida har ingen skjuvning eller byte mellan axlar
  • E = -Left * Scale, som flyttar boxens vänsterkant till pixelkolumn 0
  • F = BitmapHeight + Bottom * Scale, som mappar boxens nederkant till y = BitmapHeight, bitmappens undre kant, så att toppkanten landar på rad 0

Pixlar går tillbaka till sidan genom den matrisens invers. Request.PageRotation bär sidans /Rotate normaliserad till 0, 90, 180 eller 270 (varje värde som inte är en multipel av 90 behandlas som 0), och renderaren vrider sidan medurs som ISO 32000-1 §7.7.3.3 kräver. Under rotation byter axlarna plats och ett annat par boxkanter fästs vid bitmappsorigo. Utskrivet som inversa formler, med S = DPI / 72, x och y i pixlar och H bitmappshöjden:

/RotateSid-XSid-YBoxkanter mappningen beror på
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

Sista kolumnen förklarar varför buggen verkade slumpmässig i produktion. En CropBox som trimnar bara sidans topp lämnar Left och Bottom orörda, så upprätta sidor kom ut perfekt och bara sidor bärande /Rotate 270 drev. Rotation byter också bitmappsdimensioner: vid 90 och 270 är bitmappen (Top - Bottom) * S pixlar bred och (Right - Left) * S pixlar hög

HotPDF tabell över invers mappning för sidrotation: vid /Rotate 0 och 90 fäster transformen Left- och Bottom-kanterna hos den renderade boxen, vid 180 Right och Bottom, vid 270 Right och Top, vilket är varför en beskuren sida driver i olika riktning för varje orientering i ett blandat dokument
samma halvtumsbeskärning ser ut som tre olika buggar när sidor bär olika /Rotate-värden, för varje orientering fäster ett annat par boxkanter

Vad går fel med MediaBox [0 0 612 792] och CropBox [36 36 576 756]?

Med en halvtumsbeskärning på varje sida landar en oroterad sidas textskikt exakt 36 punkter vänster om och 36 punkter under de inskannade orden när MediaBox används. Ta en US Letter-sida vars CropBox trimnar 36 punkter (0,5 tum) från varje kant. Den synliga boxen är 540 gånger 720 punkter, så vid standard-OCR-upplösningen 300 DPI är skalan 300 / 72 ≈ 4,1667 och bitmappen är 2250 gånger 3000 pixlar

Anta att motorn rapporterar ett ord med pixelbox Left 450, Top 600, Right 900, Bottom 660 och ingen baslinje. HotPDF placerar sedan baslinjen 20 procent av ordhöjden över nederkanten, på pixelrad 648, och mappar startpunkten (450, 648):

  • Genom den synliga boxen: x = 36 + 450 / 4,1667 = 144,0 och y = 36 + (3000 - 648) / 4,1667 = 600,48, vilket är där ordet är tryckt
  • Genom MediaBox: x = 0 + 108,0 = 108,0 och y = 0 + 564,48 = 564,48, en enhetlig förskjutning på (-36, -36) punkter
HotPDF CropBox-driftanatomi på en US Letter-sida med MediaBox 0 0 612 792 och CropBox 36 36 576 756: renderaren rastrerar den synliga boxen vid 300 DPI, så att mappning av ordpixeln 450 genom GetLoadedPageVisibleBox ger 144,0 och 600,48 medan MediaBox-transformen landar på 108,0 och 564,48
rastreringen täcker CropBox, så en transform byggd från MediaBox förskjuter varje igenkänd ord med exakt beskärningsmarginalen

Rotera samma sida och felets riktning ändras, för olika kanter är inblandade. Vid /Rotate 180 använder X-termet Right, och 612 i stället för 576 knuffar skiktet 36 punkter åt höger medan Bottom fortfarande drar det 36 punkter ner. Vid /Rotate 270 är både Right och Top för stora, så skiktet flyttar 36 punkter höger och 36 punkter upp. Ett dokument med blandade orienteringar kan visa driften i tre riktningar, ett pålitligt fingeravtryck för denna bugg. Handskriven kod som härleder skalan ur boxen, som Bitmap.Width / (Right - Left), drar dessutom ut varje koordinat med 612 / 540, ungefär 13 procent, utöver offseten

Vilka av dina PDF-dokument är drabbade?

Ett PDF-dokument är utsatt när minst en sida har en synlig box som avviker från sin MediaBox, och HotPDF kan säga dig det på några rader. Jämför GetLoadedPageBox med pbMediaBox mot GetLoadedPageVisibleBox för varje sida, och skriv ut GetLoadedPageRotation bredvid så att du kan förutsäga driftriktningen från tabellen ovan. THPDFPageBoundary erbjuder också pbCropBox, pbBleedBox, pbTrimBox och pbArtBox, men GetLoadedPageBox(pbCropBox) faller tillbaka på MediaBox när ingen crop box finns och klipper inte, så den synliga boxen är det rätta att jämföra mot

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;
      // den lagrade arrayen kan lista sina hörn i vilken ordning som helst
      if MR < ML then begin Tmp := ML; ML := MR; MR := Tmp; end;
      if MT < MB then begin Tmp := MB; MB := MT; MT := Tmp; end;
      // redan normaliserad och klippt mot 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;

Två detaljer hos GetLoadedPageVisibleBox spelar roll för skript som detta. Funktionen lämnar sina out-parametrar orörda när den misslyckas, så att förinställa en standardsidstorlek före anropet är ett säkert mönster. Och när en felformad CropBox inte skär MediaBox alls returnerar funktionen MediaBox i stället för en tom rektangel. Listar rapporten sidor och ditt utrullade bygge är äldre än v2.770.153 för OCR, eller v2.770.154 för streckkoder och ansikten, kör igenkänningen igen på de sidorna efter uppgradering. Ett textskikt committat av ett drabbat bygge stannar i den sparade filen, och standardalternativet SkipPagesWithText hoppar över de sidorna vid andra passet om du inte stänger av det eller tar bort gamla skiktet först

Hur ska en egen IHPDFOCREngine mappa pixlar tillbaka till PDF-utrymmet?

En egen IHPDFOCREngine ska returnera ordboxar i bitmappspixlar och låta HotPDF göra mappningen; konvertera till user space bara för dina egna beslut, och använd då boxen från begärandet, aldrig MediaBox. Sedan v2.770.153 beskriver begärandets PageLeft, PageBottom, PageRight och PageTop den renderade synliga boxen, så de matchar Request.Bitmap exakt. Hjälparen nedan är inversen till bibliotekets transform, inklusive dess användning av den faktiska bitmappshöjden för upprätta sidor, så den överensstämmer med HotPDF till pixeln

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

// Bitmappspixel (ursprung uppe vänster, Y nedåt) till PDF user space
// (ursprung nere vänster, Y uppåt), genom boxen bitmappen renderades från
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;

En realistisk anledning att behöva user space inuti en motor är en zonregel: fakturor vars brevhuvud du aldrig vill ha sökbart, eller ett stämpelområde som förvirrar igenkännaren. Motorn nedan, skriven med TInterfacedObject så att referensräkning hanterar dess livstid, filtrerar ord efter var deras centrum landar på sidan, och returnerar sedan de överlevande orörda i pixelkoordinater. RunRecognizer står in för ditt eget igenkännaranrop

type
  TZoneFilterOCREngine = class(TInterfacedObject, IHPDFOCREngine)
  private
    FSkipLeft, FSkipBottom, FSkipRight, FSkipTop: Single;  // user space
    function RunRecognizer(Bitmap: TBitmap; MaxWords: Integer;
      out Words: THPDFOCRWords): boolean;  // din igenkännare, pixelboxar
  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];  // fortfarande pixlar: HotPDF mappar dem själv
    Inc(Count);
  end;
  SetLength(Words, Count);
  Result := True;
end;

Lämna motorn till ApplyLoadedOCRTextLayer(PageIndices, Engine, Options, Info) som med vilken annan motor som helst. Biblioteket validerar det som kommer tillbaka innan det litar på det: ett ord kastas och räknas i Info.DroppedWordCount när dess box lämnar bitmappen, när Right <= Left eller Bottom <= Top, eller när Confidence är utanför 0..1 eller under MinimumConfidence. Att returnera fler ord än MaxWordsPerPage, eller knuffa löpande totalen förbi MaxTotalWords, fäller hela anropet med ett budgetfel, så hedra Request.MaxWords i motorn. Konvertera inte ordboxar till user space innan du returnerar dem; HotPDF skulle behandla punktvärdena som pixlar och skiktet skulle kollapsa mot bitmappsorigo

Mappa din egen detektorutdata

Samma hjälpare tjänar en egenpipel byggd på RenderLoadedPageToBitmap, som renderar den synliga boxen och tillämpar /Rotate precis som igenkänningsfunktionerna gör. Läs boxen med GetLoadedPageVisibleBox, normalisera rotationen på samma sätt HotPDF gör, och mappa två motstående hörn av varje pixelbox. Y-axeln vänder och, vid 90 och 270 grader, byter axlarna plats, så de mappade hörnen kommer ut i ingen fast ordning; ta minimum och maximum av de mappade punkterna, vilket också är hur HotPDF bygger streckkodsgränser

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);  // din kod, 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;

Rotationsbeteendet tas upp mer i djupet i att flattena sidrotation utan att bryta sidboxarna, och streckkodsavkodningspipelinen som konsumerar samma transform i att avkoda roterade QR-koder från PDF-sidor. Wrappar din motor en extern igenkännare visar Tesseract OCR-adaptorn för sökbar PDF processisolering- och annulleringssidan av samma gränssnitt

Snabbreferens: CropBox-säker koordinatmappning

  • Renderaren rastrerar den synliga boxen, CropBox klippt mot MediaBox (ISO 32000-1 §14.11.2); varje pixel-till-sida-mappning måste använda den boxen, läst med GetLoadedPageVisibleBox
  • HotPDF v2.770.153 fixade ApplyLoadedOCRTextLayer; v2.770.154 fixade hela sidans DecodeLoadedPageBarcodes och ansiktsfynd från DetectLoadedRedactionFindings; byggen från v2.766.64 upp till de versionerna är drabbade
  • THPDFOCRWord-boxar är bitmappspixlar med ursprung uppe vänster; GetLoadedPageBox och GetLoadedPageVisibleBox returnerar PDF user space med ursprung nere vänster och Bottom < Top
  • Skalan är DPI / 72; härled den från DPI:n, aldrig från en sidbox delad i bitmappsbredden
  • /Rotate avgör vilka kanter som spelar roll: Left och Bottom vid 0 och 90, Right och Bottom vid 180, Right och Top vid 270
  • Returnera OCR-ord i pixlar och låt HotPDF mappa dem; konvertera bara för din egen filtreringslogik
  • Kör OCR igen på beskurna sidor processade av ett drabbat bygge, och kom ihåg att SkipPagesWithText hoppar över sidor som redan bär gamla skiktet

Igenkänningsfunktionerna, sidboxfrågorna och renderingen av laddade dokument som används här skeppas alla i HotPDF-komponenten för Delphi och C++Builder; licensiering, testnedladdningar och den fullständiga funktionslistan finns på HotPDF Delphi PDF component-sidan