Teknik Makale

Tesseract'ı HotPDF OCR Metin Katmanına Takmak (Delphi)

Hiçbir zaman dosya sonunu bildirmeyen bir pipe, bir OCR özelliğinde kovalaması tuhaf bir hatadır. Uygulamanızdaki bir thread, bir yardımcı programın standart çıktısını okur ve ReadFile'in ERROR_BROKEN_PIPE ile başarısız olmasını bekler. Öteki bir thread, tesseract.exe'i bInheritHandles True set edilmiş CreateProcess ile başlatır; çünkü child process'e standart hatası için bir dosyayı böyle verirsiniz. Yardımcının pipe yazma ucu kalıtsaldı; Tesseract artık bir kopyasına sahip ve okuma thread'i, Tesseract sayfasını bitirene kadar askıda kalır — 300 DPI'lık, iki dil modeli yüklenmiş bir taramada bu hızlı değildir

Harici bir tanıyıcı askıdan fazlasını getirir: child ne görebilir, yarım geçerli çıktıyla ne yapılmalı, piksel kutuları aranabilir PDF metnine nasıl çevrilir. Yerleşik şablon eşlemeli OCR engine'i makine basımı ASCII harf ve rakamlarını bilerek kapsar. Aksanlı adlar, Çince sözleşmeler ve çok dilli arşivler eğitilmiş dil modelleri ister ve Tesseract her zamanki adaydır; executable'ınızın yanına kurabileceğiniz bir komut satırı programıdır

Tesseract'ı HotPDF OCR metin katmanına takmak

HotPDF, yerel kurulu bir Tesseract executable'ını HPDFTesseractRecognition unit'indeki HPDFCreateTesseractOCREngine üzerinden bir IHPDFOCREngine olarak sarar. O engine'i THotPDF.ApplyLoadedOCRTextLayer'a geçersiniz; her istenen sayfayı render eder, Tesseract'ı sayfa başına bir kez koşturur, kelimeleri filtreler ve her istenen sayfa için görünmez bir Unicode metin katmanını tek bir işlemde — ya da hiçbirinde — yazar. Koordinat eşleme, dönüş, confidence filtresi ve commit, yerleşik engine'in çoktan kullandığı pipeline'dır; yalnızca tanıma değişir

HotPDF sayfa başına OCR hattı: sayfayı yapılandırılmış DPI'da render et, input.bmpyi özel bir HotPDF-OCR dizinine kaydet, Tesseract alt sürecini tessedit_create_tsv ile başlat, on iki sütunlu TSVyi ayrıştır, kelimeleri güvene göre filtrele ve görünmez metin katmanını istenen tüm sayfalar için ya da hiçbiri için işle
Adaptör yalnızca tanımayı değiştirir: render, ayrıştırma, doğrulama ve ya hep ya hiç commit mevcut metin katmanı hattında kalır; sonraki kod asla değişmez

Factory, hiçbir sayfaya dokunulmadan EArgumentException raise eder. Executable ile tessdata dizini var olmalı, timeout 1 ile 3.600.000 milisaniye arasında olmalı ve dil yalnızca ASCII harfleri, rakamları, _ ile +'i içerebilir; çünkü sonunda bir komut satırına düşer: eng+chi_sim meşrudur, tırnak ya da boşluklu bir değer beklemekte olan bir enjeksiyondur. İkinci bir overload bir THPDFTesseractOptions record'u alır. Onun Default'u --psm 3, --oem 3, sayfa başına 60 saniye ve 16.777.216 piksel tavanı verir; kelime döndürmeyen iki mod olan tpsOSDOnly ile tpsAutoOnly'yi reddeder

uses
  SysUtils, HPDFTypes, HPDFDoc, HPDFTesseractRecognition;

function MakeSearchable(const SourceFile, TargetFile: string;
  Token: THPDFCancellationToken): Boolean;
var
  Doc: THotPDF;
  Engine: IHPDFOCREngine;
  TessOptions: THPDFTesseractOptions;
  Options: THPDFOCRTextLayerOptions;
  Info: THPDFOCRTextLayerInfo;
begin
  TessOptions := THPDFTesseractOptions.Default; // psm 3, oem 3, 60 sn, 16.777.216 px
  TessOptions.TimeoutMilliseconds := 120000;    // her sayfaya uygulanır
  // eksik executable ya da tessdata klasörü, kötü bir dil tanımlayıcısı ya da
  // aralık dışı optionlar için EArgumentException raise eder
  Engine := HPDFCreateTesseractOCREngine(
    'C:\OCR\Tesseract\tesseract.exe',
    'C:\OCR\Tesseract\tessdata',
    'eng+chi_sim',                              // '+' ile birleştirilmiş modeller
    TessOptions);
  Doc := THotPDF.Create(nil);
  try
    Doc.AutoLaunch := False;
    if Doc.LoadFromFile(SourceFile) < 1 then
      raise Exception.Create('Cannot load ' + SourceFile);
    Options := THPDFOCRTextLayerOptions.Default; // 300 DPI, MinimumConfidence 0.5
    Options.CancellationToken := Token;
    // boş sayfa listesi her sayfa demektir; çoktan metni olan sayfalar atlanır
    Result := Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info);
    if Result then
    begin
      Writeln(string(Info.EngineName), ': ', Info.AcceptedWordCount,
        ' words kept, ', Info.DroppedWordCount, ' below the threshold');
      Doc.SaveLoadedDocument(TargetFile);
    end
    else
      case Info.Status of
        otlsCancelled:      Writeln('Cancelled, document unchanged');
        otlsEngineError:    Writeln('Engine: ', string(Info.Diagnostic));
        otlsBudgetExceeded: Writeln('Budget: ', string(Info.Diagnostic));
      else
        Writeln(string(Info.Diagnostic));
      end;
  finally
    Doc.Free;
  end;
end;

Piksel aritmetiğini bir kez yapın. 300 DPI'da bir A4 sayfası kabaca 2.481'e 3.508 piksel, yaklaşık 8,7 milyondur ve sığar. Aynı çözünürlükte bir A3 sayfası kabaca 3.508'e 4.961, 17,4 milyona yakındır; piksel bütçesi teşhisiyle başarısız olur ve koşu otlsEngineError olarak biter. Yalnızca bir timeout alan overload hiçbir piksel sınırı uygulamaz. Dışarıdan dosya kabul eden bir servis için options overload'unu tutun ve büyük formatlar bekleniyorsa MaxPixels'i (67.108.864'e dek) yükseltin ya da THPDFOCRTextLayerOptions.DPI'yi bilerek düşürün

Sayfa başına engine, temp yolu altında özel bir HotPDF-OCR-{GUID} dizini kurar, bitmap'i input.bmp olarak kaydeder ve tesseract input.bmp output --tessdata-dir … -l … --dpi N --psm 3 -c tessedit_create_tsv=1 koşturur; bir engine modu set edildiğinde --oem ekler. Yollar, backslash ile gömülü tırnaklar için Windows kurallarıyla tırnaklanır ve --dpi render çözünürlüğünü taşır; Tesseract onu hiç tahmin etmez. Tesseract ile .traineddata dosyaları paketlenmez; sağlamak uygulamanın işidir. Kendi Linux test makinelerimizde tanıyıcı, binary'nin, paylaşılan kütüphane kapanışının ve tessdata klasörünün kullanıcı düzeyi bir demetinden koşar; /usr/bin/tesseract'e bel bağlamak yalnızca birilerinin sistem paketini kurduğu yerlerde çalışır

Tek bozuk TSV satırı bütün sayfayı neden boşaltır?

Tesseract adaptörü ilk bozuk satırda bütün sayfayı düşürür; çünkü kısmen ayrıştırılmış bir kelime listesi, görüntüyle uyuşmayan bir metin katmanı üretir ve kaydedilen PDF'te hangi kelimelerin eksik gittiğini söyleyen hiçbir şey yoktur. Çıktı, gömülü NUL'suz katı UTF-8 olarak decode edilebilmelidir. İlk satır, isteğe bağlı bir byte order mark kırıldıktan sonra Tesseract'ın on iki sütunlu başlığına — level'dan text'e — eşit olmalıdır. Her satır tam olarak on iki alana bölünmeli ve bölme, on birinci tabdan sonra durur; böylece tanınan metnin içindeki bir tab, on üçüncü sütun kurmak yerine metin alanına düşer ve aşağıdaki kontrol karakteri kontrolü onu orada reddeder

Yalnızca level 5 satırları kelimedir; sayfa, blok, paragraf ve satır satırları atlanır; boş ya da saf boşluk olan kelimeler de öyle. Kontrol karakteri (U+0020 altı ya da U+007F ile U+009F arası) içeren bir kelime sayfayı düşürür. Gerisi tamsayı geometri, Alman locale'li bir uygulamanın hâlâ 93.5 okuması için değişmez biçimlendirmeye ayrıştırılmış bir confidence, bitmap'in içinde tam duran bir kutu ve 0 ile 100 arası bir confidence ister. Bir geçerli kelimeyi bir bozuk satır izlerse sıfır kelime çıkar

HotPDF'de her Tesseract TSV satırının geçtiği altı kapı: tam on iki sütunlu başlık, tam olarak on iki alan, yalnızca seviye 5, boş olmayan metin, bitmapin içinde bir kutu ve değişmez biçimde ayrıştırılmış 0 ile 100 arası güven; tek bir bozuk satır tüm sayfayı sıfır kelimeye kadar batırır
Kısmen ayrıştırılmış bir kelime listesi görüntüyle sessizce çelişirdi; parser, zaten okuduğu kelimeleri tutmak yerine ilk bozuk satırda tüm sayfayı reddeder

Tek tuhaf bir satır bu yüzden bütün koşuyu düşürür ve her sayfaya dokunmaz. Doğru takas budur. Yeniden koşmak ucuzdur; fatura numarası aramasının işte o önemli sayfayı bulamadığı bir aranabilir PDF, aylar sonra başkalarının elinde yüzeye çıkar

Son adım ayrıca beklemeyebileceğiniz bir varsayılanla karşılaşır. Tesseract confidence'ı 0'dan 100'e bildirir, pipeline 100'e böler ve THPDFOCRTextLayerOptions.MinimumConfidence varsayılanı 0,5'tir; 50'nin altındaki her kelime sayfaya değil Info.DroppedWordCount'a düşer. Temiz bir taramada o taban mantıklıdır. Gürültülü bir faksta metnin büyük bir hissesini silebilir; düşürmeden önce Info.ReceivedWordCount ile Info.AcceptedWordCount'u kıyaslayın

Çoktan metni olan bir dosyada OCR'ı yeniden koşturmak

SkipPagesWithText varsayılan olarak True'dur ve ikinci turda insanları yakalar. Bir toplu işin eng ile işlendiğini ve şimdi eng+deu gerektiğini düşünün: her sayfa görünmez metin taşıyor; Info.SkippedPageCount sayfa sayısına eşit. HotPDF yazdığı content stream'i kendi OCR katmanı olarak işaretler ve ReplaceExistingOCRTextLayer, o işareti kullanarak o sayfaları yeniden tanır; eski katmanı düşürür, orijinal sayfa içeriğini tutar

Options := THPDFOCRTextLayerOptions.Default;
Options.ReplaceExistingOCRTextLayer := True;  // önceki katmanımızı takas et
Options.UseOptionalContentGroup := True;      // 'OCR Text Layer' adlı katman
Options.CancellationToken := Token;
if Doc.ApplyLoadedOCRTextLayer([], Engine, Options, Info) then
  Writeln(Info.ReplacedPageCount, ' replaced, ',
    Info.SkippedPageCount, ' skipped, ',
    Info.RecognizedPageCount, ' recognized')
else
  Writeln(string(Info.Diagnostic));

Bir scanner driver'ın OCR'ladığı bir dosya o işareti taşımaz; sayfaları hâlâ metinli sayfa sayılır. SkipPagesWithText'i kapatmak ilk katmanın üstüne ikinci bir görünmez katman dizer ve her arama isabeti iki kez görünür. O dosyalara dokunmayın

Tesseract process'i hangi tutamaçları kalıt alır?

Tesseract child process'i HotPDF'den tam olarak iki tutamaç kalıt alır: biri standart girdi ile çıktı için NUL üzerinde açılan, öteki özel dizindeki stderr.txt için. Yukarıdaki pipe askısı CreateProcess'in kendisinden gelir: standart tutamaçları geçirmek için bInheritHandles = True gerekir ve kendi başına, ev sahibindeki her kalıtsal tutamacı — alakasız kodun açtığı pipe'lar, dosyalar ve olaylar dahil — geçirir. HotPDF, PROC_THREAD_ATTRIBUTE_HANDLE_LIST taşıyan bir attribute listesiyle STARTUPINFOEX ve EXTENDED_STARTUPINFO_PRESENT bayrağını kullanır. bInheritHandles True kalır; yalnızca listelenen tutamaçlar geçer

HotPDF'de Tesseract alt süreç tutamaç mirası: bInheritHandles ile düz bir CreateProcess her kalıtsal dosya, pipe ve olay tutamacını çocuğa geçirir; PROC_THREAD_ATTRIBUTE_HANDLE_LIST ile STARTUPINFOEX ise kümeyi stdin ve stdout için bir NUL tutamacı artı stderr dosya tutamacıyla sınırlar
Öznitelik listesi olmadan alt süreç, çıkana dek alakasız nesneleri canlı tutar; dosyaları kilitler, pipe'ları aç bırakır. Onunla yalnızca listelenen iki tutamaç sınırı geçer

Aynı desen, uygulamanızın başlattığı her öteki child process'e ait olur; çünkü her şeyi kalıt alan tek bir başlatıcı, process'teki her pipe okuyucusunu askıya almak için yeterlidir:

// Kendi CreateProcess çağrılarınız için desen (parça: NullHandle, ErrorHandle,
// Exe, CommandLine, WorkDir ve ProcessInfo başka yerde bildirilidir).
// Eski Delphi RTL'leri attribute listesi fonksiyonlarını bildirmez; derleyici
// bulamazsa kernel32'den kendi external bildirimlerinizi ekleyin
// Üretim kodunda her BOOL sonucunu kontrol edin
type
  TStartupInfoExRec = record
    StartupInfo: TStartupInfo;
    AttributeList: Pointer;
  end;
const
  HANDLE_LIST_ATTRIBUTE = $00020002;   // PROC_THREAD_ATTRIBUTE_HANDLE_LIST
  EXTENDED_STARTUP_FLAG = $00080000;   // EXTENDED_STARTUPINFO_PRESENT
var
  Startup: TStartupInfoExRec;
  HandleList: array[0..1] of THandle;
  Size: NativeUInt;
begin
  FillChar(Startup, SizeOf(Startup), 0);
  HandleList[0] := NullHandle;         // bInheritHandle = True ile oluşturulmuş
  HandleList[1] := ErrorHandle;        // aynı şekilde
  Size := 0;
  InitializeProcThreadAttributeList(nil, 1, 0, Size);
  GetMem(Startup.AttributeList, Size);
  InitializeProcThreadAttributeList(Startup.AttributeList, 1, 0, Size);
  UpdateProcThreadAttribute(Startup.AttributeList, 0, HANDLE_LIST_ATTRIBUTE,
    @HandleList[0], SizeOf(HandleList), nil, nil);
  Startup.StartupInfo.cb := SizeOf(Startup);
  // ... dwFlags ile hStdInput / hStdOutput / hStdError'i her zamanki gibi ayarlayın ...
  CreateProcess(PChar(Exe), PChar(CommandLine), nil, nil,
    True,                              // tutamaç listesiyle hâlâ gerekli
    CREATE_NO_WINDOW or EXTENDED_STARTUP_FLAG,
    nil, PChar(WorkDir), Startup.StartupInfo, ProcessInfo);
  // child çıktıktan sonra: DeleteProcThreadAttributeList, FreeMem, CloseHandle
end;

Köprü script'leri bir Windows ayrıntısı daha ekler. Bir Python launcher ikinci bir interpreter process'i başlatabilir ve oluşturduğunuz process'i sonlandırmak onu ayakta bırakır. Engine'e, aşağıdaki RapidOCR adaptöründe olduğu gibi bir script yolu verildiğinde HotPDF child'i askıda başlatır, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE taşıyan bir job object'ine atar ve sonra sürdürür; job'ı kapatmak bütün ağacı bitirir. Aynı sınırlama fikri PDF görüntü codec'lerinin worker process'lerinde yalıtılmasını sürer; orada child güvenilmezdir, burada güvenilirdir ama process'inizde tutamaç tutan tek kod sizin değilsiniz

İptal, zaman aşımları ve ya hep ya hiç commit

IHPDFOCREngine.Recognize tek bir Boolean döndürür ve False, hem "Tesseract başarısız oldu"yu hem "kullanıcı İptal'e bastı"yı kapsar. Adaptör, Tesseract koşarken token ile kendi timeout'unu her 25 milisaniyede sorgular ve iptalde temizleyip False döndürür. Yüzeyde okununca bu otlsEngineError gibi görünür; o yüzden ApplyLoadedOCRTextLayer, Recognize False döndürdüğünde önce token'a bakar. Tanıma ile content kurulumu, belge işlemi açılmadan her istenen sayfa için tamamlanır; 50 sayfanın 40.'sındaki bir iptal otlsCancelled bildirir ve 50 sayfayı da yüklendiği gibi bırakır. Öteki sınırlar aynı stili izler:

  • Timeout her Recognize çağrısının başından itibaren işler; 60.000 ms'lik varsayılan sayfa başına bir sınırdır
  • Timeout'ta ya da iptalde hâlâ koşan bir child sonlandırılır ve 5 saniyeye dek beklenir; özel dizin bir finally bloğunda kaldırılır ve silme, bir saniyeye dek yeniden denenir; çünkü çıktı dosyaları process bittikten kısa bir süre kilitli kalabilir
  • output.tsv 64 MiB'ye, stderr.txt 1 MiB'ye sınırlıdır; child koşarken ve çıktıktan sonra kontrol edilir
  • Sayfa başına kelimeler ile UTF-16 code unit'ler, MaxWordsPerPage, MaxTotalWords ile MaxTextCodeUnits'in gerisiyle sınırlıdır; aşmak listeyi kırpmak yerine koşuyu düşürür
  • Sıfır olmayan bir çıkış kodu, stderr.txt'in 4.096 karakterine dek taşınarak döner; eksik bir .traineddata dosyasını öğrenmenin en hızlı yoludur

Görünmez metin katmanı sayfanın içinde neye benzer?

HotPDF kelimeleri metin gösterim modu 3 ile yazar (ne doldurma ne stroke, ISO 32000-1 §9.3.6); tarama görünür kalır, arama ile kopya kelimeler üzerinde çalışır. Content stream BT'yi 3 Tr ile açar; her kelime taban çizgisinde bir Tm matrisi, piksel cinsinden kutu yüksekliğinin 72 çarpı render DPI'sına bölünmüş bir font boyutu ve koşuyu kutu genişliğine geren bir Tz ölçeği alır. O germe, arama vurgusunu görüntüdeki kelimenin üzerinde tutar

TSV kutuları taşır ama taban çizgisi taşımaz; her taban çizgisi alt kenarın bir beşte biri yüksekliğine konur. Latin ya da Kiril gibi boşlukla ayrılan yazı sistemlerinde soldan sağa bir satırda komşu kelimeler arasında gerilmiş bir boşluk, araya boşluk koyar; kopyalanan metin kelime kırpmalarını korur. Çince hiçbirini almaz. Glyph'ler, Identity-H kodlamalı paylaşımlı gömülü olmayan bir Type0 font ile üretilmiş bir ToUnicode CMap'ten geçer; her farklı Unicode skalerine bir CID — Çince, aksanlı Latin ve yardımcı düzlem karakterleri böylece kopyayı ve aramayı yaşatır. Tek bir koşu 65.535 farklı skalere dek tutar; ISO 19005 gömülü font istediği için PDF/A çıktısı, ayrıca gömülmüş uyumlu bir font ister. Sonucu kontrol etmek için kaydedin, yeniden yükleyin ve yüklenmiş bir PDF'den metin çıkarmayı anlatan sıradan yolu koşturun; beklenen kelimeler beklenen sayfalardan dönüyorsa katman gerçektir

Aynı sözleşmede RapidOCR ve native DLL

Basitleştirilmiş Çince taramalar için HPDFCreateRapidOCREngine(PythonExecutable, BridgeScript, ModelDirectory, TimeoutMilliseconds) aynı process koşturucusunu ve TSV parser'ını yeniden kullanır. Köprü script yolu Python executable'ını izler ve dil chi_sim'e sabitlenir. HotPDF köprüyü tools/OCR/rapidocr_tsv.py olarak taşır; rapidocr ile onnxruntime paketlerini, yerel ONNX modellerini ve bir font dosyasını ister, otomatik indirmeleri reddeder ve Tesseract biçimli TSV yazar; bir THPDFRapidOCROptions overload'u model dosyalarını, thread sayılarını ve piksel tavanını set eder. İki adaptörün arkasındaki genel fabrika — HPDFLocalTSVRecognition içindeki HPDFCreateLocalTSVOCREngine — Tesseract tarzı argümanları alıp on iki sütunlu TSV yazan her programı kabul eder ve tutamaç listesini, timeout'u, iptali ile bütçeleri kalıt alır

HPDFCreateTesseractDLLOCREngine(LibraryPath, TessDataDirectory, Language) Tesseract C API'sini süreç içinde yükler, Tesseract (native DLL) bildirir, istenen her .traineddata dosyasının var olduğunu baştan kontrol eder ve iptali ile timeout'u Tesseract'ın kendi progress monitor'üne haritalar. Sayfa başına bir process başlatması ile bir BMP gidiş dönüşü kurtarır; executable, tanıyıcının çökmelerini ile bellek büyümesini adres alanınızın dışında tutar. Kullanıcının kendi taramalarıyla uğraşan bir masaüstü aracı için DLL uygundur. Yabancılardan belge yutan bir servis için komut satırı engine'i daha güvenli seçimdir. Her iki durumda da tanıma, Windows'ta, bir seferde bir sayfa, render edilmiş bitmap üzerinde koşar; eğik taramaları düzeltin ve temizleyin, engine'e ulaşmadan

OCR adaptörleri, metin katmanı yazıcısı, onları besleyen renderer ve çıktıyı kontrol eden metin çıkarma, Delphi ile C++Builder için tek bir native VCL component'inde yaşar. Yakalama ya da arşiv uygulamanız aranabilir PDF çıktısı gerektiriyorsa HotPDF Delphi PDF component, kendi kuracağınız OCR engine'inin dışındaki her şeyi kapsar