HotPDF, tamamen Object Pascal ile yazılmış, sınırları belirlenmiş bir şablon eşlemeli OCR motoru olan THPDFBuiltInOCREngine'i sunar: render edilmiş sayfayı Otsu eşiklemesiyle ikilileştirir, glyph'leri connected component olarak çıkarır ve her glyph'ü önbelleğe alınmış çoklu font şablonlarına karşı grayscale coverage ile puanlar; böylece bir Delphi uygulaması harici OCR bağımlılığı olmadan aranabilir bir metin katmanı oluşturabilir. Motorun v2.731.0'da baştan kurulması gerekti ve neden matcher değildi. Piksellerdi
Eski motor testlerini geçiyordu. Sentetik bitmap'lerde büyük ASCII harflerini tanıyor ve Win32'de bunu aylarca sürdürüyordu. Sonra aynı kod Win64 altında çalıştırıldı ve hiçbir şey üretmedi: kelime yoktu, "found no high-contrast foreground" dışında tanı yoktu, çökme yoktu. Hatanın, birbirini tesadüfen dengeleyen pixel-reading yolundaki iki bağımsız yanlış olduğu ortaya çıktı; bunları ayırmak OCR kodunun neden yüksek sesle değil sessizce başarısız olduğuna iyi bir örnektir
Eski OCR motoru neden yalnızca tesadüfen çalışıyordu?
Eski motor çalışıyordu çünkü template bitmap'leri ve hedef bitmap'leri aynı şekilde ters çevrilmişti; bu yüzden pixel reader'daki dikey terslik matcher tarafından görünmüyordu. TBitmap.ScanLine, görüntü yolunun geri kalanının varsaydığı pozitif biHeight DIB kuralının tersi sırada satırlar verir. Bir M'yi baş aşağı render edip yine baş aşağı bir template ile karşılaştırırsanız L1 farkı doğru karşılaştırmayla aynıdır. Her glyph eşleşti. Doğru olan hiçbir şey yoktu
Bu simetri, bu hata sınıfını pahalı yapan şeydir. Tek taraflı bir düzeltme eşlemeyi bozar: hedef okumasını düzeltip şablonları bırakırsanız tanıma gürültüye çöker; önce şablonları düzeltirseniz aynı çökme diğer yönden gelir. Aşamalı bir onarım yolu yoktur. Bu nedenle yeniden kurulum, bütün okumayı açıkça bildirilmiş bir BITMAPINFOHEADER karşısında GetDIBits kullanacak şekilde değiştirdi; burada pozitif biHeight sözleşme gereği bottom-up satırlar demektir, VCL kuralı gereği değil, ve grayscale buffer'a kopyalarken bir kez bilerek ters çevrilir
İkinci hata yalnızca Win64'ün ortaya çıkardığıdır. GetDIBits'e geçirilen HDC, bitmap'in kendi memory DC'si olmamalıdır; bitmap zaten ona seçili olduğundan Windows bunu geçersiz olarak belgeler. Bitmap.Canvas.Handle geçirmek Win32 süreci tarafından tolere edildi ve Win64 test sürecinde tutarlı biçimde başarısız oldu. Düzeltme, bitmap'e borcu olmayan ve finally bloğunda serbest bırakılan GetDC(0) ile elde edilen atıl bir screen DC'dir
procedure BitmapToGray(Bitmap: TBitmap; out Gray: TBytes);
var
Work: TBitmap;
Info: TBitmapInfo;
Buffer: TBytes;
DC: HDC;
P: PByte;
Stride, X, Y: Integer;
begin
Work := TBitmap.Create;
try
Work.Assign(Bitmap);
Work.PixelFormat := pf24bit;
Stride := ((Work.Width * 24 + 31) div 32) * 4;
SetLength(Buffer, Stride * Work.Height);
FillChar(Info, SizeOf(Info), 0);
Info.bmiHeader.biSize := SizeOf(BITMAPINFOHEADER);
Info.bmiHeader.biWidth := Work.Width;
Info.bmiHeader.biHeight := Work.Height; // pozitif => bottom-up satırlar
Info.bmiHeader.biPlanes := 1;
Info.bmiHeader.biBitCount := 24;
Info.bmiHeader.biCompression := BI_RGB;
DC := GetDC(0); // asla Work.Canvas.Handle değil: Work orada seçili
if DC = 0 then
raise EInvalidOperation.Create('Recognition bitmap pixels could not be read');
try
if GetDIBits(DC, Work.Handle, 0, Work.Height,
@Buffer[0], Info, DIB_RGB_COLORS) <> Work.Height then
raise EInvalidOperation.Create('Recognition bitmap pixels could not be read');
finally
ReleaseDC(0, DC);
end;
SetLength(Gray, Work.Width * Work.Height);
for Y := 0 to Work.Height - 1 do
begin
P := @Buffer[(Work.Height - 1 - Y) * Stride]; // tek bilinçli ters çevirme
for X := 0 to Work.Width - 1 do
Gray[Y * Work.Width + X] :=
(Integer(P[X * 3]) * 29 + Integer(P[X * 3 + 1]) * 150 +
Integer(P[X * 3 + 2]) * 77) shr 8;
end;
finally
Work.Free;
end;
end;
İri piksellerden glyph kutularına: ikilileştirme ve connected component'ler
HotPDF önce Otsu yöntemiyle ikilileştirir ve yalnızca Otsu uygulanabilir olmadığında yerel pencere eşiğine döner. Global yol gerçek bir bimodal histogram ister: motor sınıflar arası varyans maksimumunu hesaplar ve sonuca güvenmeden önce gri aralığın en az 64 seviye kapsamasını da şart koşar. Soluk bir tarama, gradyan arka planlı bir sayfa veya neredeyse tamamen mürekkep olan bir bitmap bu testi geçemez. Ardından fallback, kayan pencereyi piksel sayısında lineer tutmak için running column sum'larla hesaplanan 31'e 31'lik bir pencerenin ortalamasını 6 gri seviyelik bias ile her piksele karşılaştırır
Glyph çıkarma, oluşan maskede 8-connected component labeling'dir ve recursion yerine açık bir stack kullanır; çünkü tam sayfa maskesi derin bir flood fill'de Delphi thread stack'ini kolayca taşırabilir. Labeling sırasında iki filtre çalışır: 9 pikselden küçük bileşenler speckle noise olarak atılır ve görüntünün hem genişliğinin hem yüksekliğinin üçte beşinden fazlasına yayılan her bileşen glyph yerine frame veya rule olarak atılır. İkinci geçişte, yatay örtüşmesi daha dar kutunun en az dörtte biri olan dikey istiflenmiş kutular birleştirilir; i veya j'nin noktasını gövdesiyle yeniden buluşturan şey budur. Bunların tamamı bir raster üzerinde çalışır ve raster, Delphi'de yüklenmiş PDF sayfasını bitmap'e render etme açıklamasındaki renderer'dan gelir; bu pratik bir nedenle önemlidir: OCR kalitesi render kalitesinin üstüne çıkamaz ve varsayılan 300 DPI text-layer değeri bir maksimum değil, bilinçli bir ödündür
Büyük I ile küçük l'yi kararsız yapan nedir?
Arial'de büyük I ve küçük l piksel olarak özdeş çubuklara rasterize olur; hiçbir şekil özelliği onları ayıramaz ve büyük-küçük harf bilgisi tamamen başka bir yerden gelmelidir. Motorun cevabı satır düzeyinde yükseklik kümelemesidir. Glyph kutuları dikey örtüşmeye göre text line'lara gruplanır, her satırın cap height ve modal baseline değeri analiz edilir ve satır içindeki yükseklikler kısa bir cluster ile uzun bir cluster'a bölünür. Kısa cluster'da duran çubuk l, uzun cluster'da duran aynı çubuk I'dir
Bu bölmenin bariz uygulaması sabit oran eşiğidir ve çalışmaz. Arial'in x-height ile cap-height oranı yaklaşık 0.72'dir; bu da herkesin önce denediği 0.70 ve 0.75 değerlerinin tam ortasına düşer. Sabiti her iki yönde yüzde bir oynatın, bütün corpus harf durumunu değiştirir. HotPDF bunun yerine tek boyutlu k=2 varyans-minimize eden bir bölme yapar: aday yükseklikleri sıralar, bütün kesme noktalarını dener ve cluster içi kare sapmalar toplamının en küçük olduğu kesmeyi tutar. Eşik artık source içindeki bir sabit değil, sayfanın özelliğidir
// ClusterHeights artan sırada sıralı; varyansı en düşük k=2 bölmesini bul
BestSplit := 1;
BestVariance := 1E18;
for I := 1 to ClusterCount - 1 do
begin
SumA := 0;
for J := 0 to I - 1 do SumA := SumA + ClusterHeights[J];
SumB := 0;
for J := I to ClusterCount - 1 do SumB := SumB + ClusterHeights[J];
MeanA := SumA / I;
MeanB := SumB / (ClusterCount - I);
Variance := 0;
for J := 0 to I - 1 do
Variance := Variance + Sqr(ClusterHeights[J] - MeanA);
for J := I to ClusterCount - 1 do
Variance := Variance + Sqr(ClusterHeights[J] - MeanB);
if Variance < BestVariance then
begin
BestVariance := Variance;
BestSplit := I;
end;
end;
// kısa bandı yalnızca iki cluster ortalaması arasındaki oran belirler
if SmallMean / TallMean <= 0.80 then
SmallGroup := ggSmall // gerçek bir x-height bandı: küçük harf şekilleri
else
SmallGroup := ggTall; // tek yükseklik bandı: her şey büyük harf yüksekliğinde
Line.LowercaseContext := (SmallGroup = ggSmall);
Tek bir yükseklik bandına sahip satırlarda içeriden hiç kanıt yoktur. Tamamı büyük harf bir başlık ile tamamı küçük harf bir caption tek başına aynı görünür. Bu satırlar için HotPDF, satırın median yüksekliğini bölünmüş satırlardan alınan sayfa düzeyi median x-height ile karşılaştırır: 1.10 veya altındaki oran satırı küçük harf bağlamı olarak, 1.18 veya üstündeki oran büyük harf bağlamı olarak işaretler; aradaki her şey kısıtsız kalır. Matching ardından bu bağlama uyan adaya doğru 0.03'lük küçük bir case-preference bonusu uygular; bu, açık bir şekil farkını asla geçersiz kılmadan eşitlikleri iter
12x18 template grid neden c ile o'yu karıştırıyordu?
Template grid, daha küçük çözünürlükte c ile o arasındaki grayscale coverage farkı 0.007'nin altına, motorun ambiguity threshold değerinin hayli içine düştüğü için 12'ye 18 hücreden 16'ya 24'e genişletildi. Her glyph kutusu binary stencil olarak değil, 0 ile 255 arasında coverage değerleri olarak grid'e yeniden örneklenir; bu yüzden üçte biri mürekkep olan bir hücre siyaha veya beyaza yuvarlanmak yerine yaklaşık 85 olarak okunur. 12'ye 18'de bir c'nin açık tarafı bir hücre sütunundan biraz fazla yer kaplar ve antialiased ortalama boşluğu yıkar. 16'ya 24'te boşluk yeniden örneklemeden sağ çıkar ve kolay karışan çiftlerin çoğu güvenli mesafeye döner
Puanlama, iki coverage grid'i arasındaki normalize edilmiş L1 mesafesine, aspect-ratio farkının logaritmasının 0.30 katı ve ink-density farkının 0.16 katı ceza eklenmesiyle yapılır; aspect ratio'su 2.6 katından fazla farklı olan template'leri atlayan hard prefilter da vardır. Template'ler işlem başına beş sistem fontundan (Arial, Times New Roman, Courier New, Tahoma ve Segoe UI) 62 karakterlik alphabet boyunca bir kez rasterize edilir, bir critical section arkasında önbelleğe alınır ve sonraki her çağrıda yeniden kullanılır
Son sabit ilginç olandır. İkinci en iyi karakterin puanı kazananın 0.018'i içinde olduğunda HotPDF glyph confidence değerini 0.5'e sıkıştırır; bu 0.55 kabul kapısının altında olduğu için glyph hiç yayımlanmaz. Bu bir tuning artifaktı değil, bilerek fail-closed kesimdir: sınırlandırılmış bir motor tahmin ederse metni görüntüyle eşleşmeyen aranabilir bir katman üretir ve text layer'daki yanlış bir kelime, taramayı inceleyen kişiye görünmez olduğu için eksik bir kelimeden daha kötüdür
Sabit boşluk eşiği olmadan kelimeleri bölmek
HotPDF kelime boşluğu eşiğini ortalama glyph genişliğinin sabit bir katı yerine satır içi glyph aralıklarının dağılımından türetir. Klasik buluşsal yöntem olan "ortalama advance'in 0.75'inden geniş boşluk space'tir" yaklaşımı, bir satır rakamları dar harflerle karıştırdığı anda bozulur; çünkü ortalama advance artık gerçek hiçbir şeyi anlatmaz. Bunun yerine motor satırın aralıklarını sıralar ve art arda sıralanmış değerler arasındaki en büyük sıçramayı arar; bir tane varsa bu, kelime içi cluster ile kelimeler arası cluster arasındaki sınırdır. Bunun gürültüde tetiklenmesini üç koruma önler: sıçrama ortalama glyph genişliğinin en az 0.22'si olmalı, bölmenin üzerindeki ilk aralık en az 0.32'si olmalı ve bölmenin altındaki son aralık 0.65'i aşmamalıdır. Herhangi bir koruma başarısız olursa eşik MaxInt olarak kalır ve bütün satır tek kelime olur. Son koruma, tek bir olağan dışı geniş kerning çiftinin kelimeyi ikiye bölmesini önler; iki kelimeyi birleştirmek çok daha az zararlıdır, çünkü birleşmiş token substring aramasında doğru karakterleri doğru sırayla hâlâ taşır
Taranmış görüntünün üzerine görünmez text layer yazmak
ApplyLoadedOCRTextLayer, tanınan kelimeleri taranmış görüntünün üzerine yerleştirilmiş aranabilir bir layer'a çevirir; bunu ISO 32000-1 §9.3.6'da tanımlanan, ne fill ne de stroke olan text rendering mode 3 ile çizer. Content stream BT ve ardından 3 Tr ile açılır; her kelime raporlanan baseline'ından, istek DPI'sından dönüştürülen cap height değerinden ve sentetik glyph run'ını ölçülen kelime genişliğine uzatan bir yatay scale'den oluşturulan text matrix ile yerleştirilir. Sonuç metin gibi kopyalanır ve aranır, ama hiçbir şey boyamaz
Çoğu built-in yolu çağıranın kullanması gereken, yerleşik recognizer'ı sizin için örnekleyen engine-free overload da vardır. Recognition, Unicode validation, budget accounting ve content construction, copy-on-write transaction açılmadan önce tamamlanır; bu nedenle cancellation, budget overrun veya engine failure object graph'ı ve version number'ı değişmeden bırakır. Kelimeler iki kez filtrelenir: motor kendi 0.55 per-glyph confidence kapısının altındakileri atar, ardından THPDFOCRTextLayerOptions.MinimumConfidence (varsayılan 0.5) çağıranın eşiğinin altındaki bütün kelimeleri atar
var
Doc: THotPDF;
Options: THPDFOCRTextLayerOptions;
Info: THPDFOCRTextLayerInfo;
begin
Doc := THotPDF.Create(nil);
try
Doc.AutoLaunch := False;
if Doc.LoadFromFile('scan.pdf') < 1 then
Exit;
Options := THPDFOCRTextLayerOptions.Default; // DPI 300, MinimumConfidence 0.5
Options.SkipPagesWithText := True; // dijital doğmuş sayfaları olduğu gibi bırak
Options.UseOptionalContentGroup := True;
Options.OptionalContentGroupName := 'OCR Text Layer';
// engine-free overload: HotPDF yerleşik sınırlı recognizer'ı sağlar
if Doc.ApplyLoadedOCRTextLayer([0], Options, Info) then
begin
Writeln(Info.AcceptedWordCount, ' words accepted by ',
string(Info.EngineName));
Doc.SaveLoadedDocument('scan-searchable.pdf');
end
else
Writeln('No text layer written: ', string(Info.Diagnostic));
finally
Doc.Free;
end;
end;
Bir sınırın sonradan keşfedilmek yerine baştan açıkça söylenmesi gerekir. Görünmez layer, bütün viewer'larda arama ve kopyalama için yeterli olan ortak sentetik, embed edilmemiş bir Type0 font kullanır; ancak ISO 19005'in font embedding şartını karşılamaz. Çıktının PDF/A olması gerekiyorsa çağıran ayrıca uyumlu bir font gömmelidir. Ayrıca OCR text layer geometri taşır, yapı değil; okuma sırası yalnızca glyph konumlarından gelir. Zaten gerçek metni olan bir sayfadan mantıksal sıra gerekiyorsa tag tree tarafından yönlendirilen structure-order text extraction farklı bir sorun için farklı bir araçtır
Yerleşik motorun durduğu yer
Yerleşik motor bilerek dardır ve sınırlarını bilmek onu kullanışlı tutar. Beş template yüzüne yakın fontlardan gelen yüksek kontrastlı, makine basımı ASCII'yi hedefler; bunun dışındaki her şey tahmin yerine hiçbir kelime döndürmez. Somut sınırlar şunlardır:
THPDFCancellationTokenüzerinden 2000 ms recognition deadline ve işbirlikçi iptal ile 4096'ya 4096'ya kadar ve 4.194.304 piksele kadar görüntüler- ASCII harfleri ve rakamlardan oluşan 62 karakterlik alphabet; noktalama, aksanlı karakter ve CJK yoktur
- Yalnızca eksenlere hizalı metin; renderer'ın zaten normalize ettiği sayfa dönüşünde çalışır, eğik taramalar deskew edilmez
- Belirsiz glyph çiftleri çözülmeden kalır; bu yüzden bir sayfa kısmi kelimeler veya "found no unambiguous ASCII words" tanısını döndürebilir
Bu zarf çok küçük kaldığında dikiş noktası IHPDFOCREngine'dir. Kendi motorunuza göre Recognize uygulayın, onu üç argümanlı ApplyLoadedOCRTextLayer overload'una verin; aşağı akıştaki her şey (coordinate mapping, rotation handling, Unicode validation, budgets, atomic commit) aynı kalır. Bitmap synchronous call süresince ödünç alınır ve saklanmamalıdır. Layer'ın doğru indiğini doğrulamak için kaydedilen dosyayı yeniden yükleyin ve Delphi'de yüklenmiş PDF'den metin çıkarma ile anlatılan sıradan text path'i çalıştırın; kelimeler geri geliyorsa layer gerçektir
Yerleşik template-matching OCR, görünmez text layer, bunları besleyen page renderer ve sonucu doğrulayan loaded-document text extraction aynı native VCL component içinde, harici OCR runtime ve uygulamanızın yanında dağıtılacak DLL olmadan sunulur. Delphi veya C++Builder'da taranmış PDF'ler üzerinde document capture, arşivleme ya da arama kuruyorsanız HotPDF Delphi PDF component bütün pipeline'ı tek bağımlılıkta verir