Bài viết kỹ thuật

Kernel downsampling ảnh và dithering in ấn trong HotPDF

HotPDF mở ba kernel downsampling ảnh qua property ImageDownsampleKernel và một pass Floyd-Steinberg riêng qua RenderOutputDither. Cái đầu quyết định ảnh trông ra sao sau khi bạn thu nhỏ để chạm một giới hạn dung lượng, cái sau quyết định chúng trông ra sao sau khi page bị giảm về đen trắng. Không cái nào bật sẵn, và cả hai đều là opt-in vì cùng một lý do: chúng trả giá bằng thời gian thật

Áp lực đẩy người ta tới đây thì quen thuộc. Một hợp đồng scan 60 MB phải đi qua một email gateway từ chối mọi thứ trên 10 MB, hoặc cả lô statement phải đáp xuống một thiết bị mono kiểu fax mà render mọi pixel xám thành giấy hoặc mực. Cả hai đều là bài toán resampling, và cả hai đều có một câu trả lời nhanh mà xấu và một câu trả lời chậm mà đúng

Ba kernel thực sự khác nhau ở điểm nào

THPDFResampleKernel có ba giá trị, và chúng nằm ở những điểm khác biệt thật sự trên đường cong speed–quality. rkHalftone ủy thác cho đường GDI StretchBlt lịch sử với mode HALFTONE, mà dù mang cái tên đó vẫn là filtering hạng bilinear: nhanh, đủ dùng cho line art và screenshot, và hay cho ra những cạnh giòn tan bạn nhìn ảnh thu nhỏ là nhận ra ngay. rkBicubic chạy một kernel Catmull-Rom separable, còn rkLanczos3 chạy một windowed sinc separable với support ba lobe

Cả hai kernel separable chạy thành hai pass, ngang rồi dọc, với 6 tới 12 taps cho mỗi destination pixel bằng Pascal thuần. Chậm hơn đường GDI cỡ một bậc độ lớn, và đó chính là lý do rkHalftone giữ vị trí default. Với một batch đêm hàng nghìn page, sự khác biệt là một quyết định xếp lịch chứ không phải sở thích. Với một document đơn lẻ mà người dùng đang chờ, Lanczos3 gần như miễn phí và tốt hơn thấy rõ

Đường cong weight của ba kernel downsampling HotPDF: rkHalftone ủy thác cho đường GDI HALFTONE hạng bilinear với support bằng một, rkBicubic chạy cubic Catmull-Rom separable với support bằng hai, rkLanczos3 chạy windowed sinc với support bằng ba, đánh đổi cỡ một bậc độ lớn về speed lấy ảnh tốt hơn thấy rõ
Ba giá trị kernel nằm ở những điểm khác thật sự trên đường cong speed–quality: một đường GDI hạng bilinear, một cubic Catmull-Rom và một windowed sinc ba lobe, và các kernel separable normalize weight nên chẳng gì ringing quá đen hay quá trắng

Hai tính chất triển khai đáng nhớ vì chúng quyết định output làm được gì và không làm được gì. Border clamp bằng edge replication chứ không wrap hay mờ dần, và weight được normalize theo từng destination pixel. Hai điều đó cộng lại nghĩa là kết quả không bao giờ ringing xuống dưới đen hay lên trên trắng, nên cái halo overshoot Lanczos kinh điển quanh một cạnh cứng không xuất hiện thành artifact bị cắt trong ảnh đã encode

var
  Pdf: THotPDF;
  Info: THPDFLoadedResourceOptimizationInfo;
  Changed: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-contract.pdf');
    Pdf.ImageDownsampleKernel := rkLanczos3;   // set trước khi gọi
    Changed := Pdf.DownsampleLoadedImages(150, 82, 4096, Info);
    if Changed > 0 then
    begin
      Writeln('resampled images: ', Info.DownsampledImageCount);
      Writeln('kept calibrated : ', Info.PreservedCalibratedImageCount);
      Pdf.SaveToFile('scanned-contract-150dpi.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

Tham số MinimumSavingsBytes — 4096 ở trên — là cái chốt giữ cho phép toán trung thực. Encode lại một ảnh vốn đã được nén hiệu quả có thể cho ra stream to hơn bản gốc, và một downsampler thay mù quáng mọi ảnh sẽ thỉnh thoảng làm phình đúng cái file nó được nhờ thu nhỏ. Ngưỡng này nói rằng: chỉ commit phép thay thế khi nó tiết kiệm được ít nhất bấy nhiêu byte. PreservedCalibratedImageCount báo quyết định bảo thủ còn lại: những ảnh được bỏ yên vì mang một calibrated color space mà resampling sẽ làm tổn hại

Vì sao một hệ số polynomial sai lại khó phát hiện đến thế?

Vì một interpolation kernel hỏng không crash cũng không throw, nó chỉ cho ra một ảnh sai một cách tinh vi theo cách chẳng ai quy trách nhiệm được. Kernel Catmull-Rom là piecewise cubic, và nhánh ngoài của nó ở dạng Horner lồng là ((-0.5t + 2.5)t - 4)t + 2. Viết hệ số giữa thành -5 thay vì -4 và hàm vẫn tính được, vẫn trả về những con số trong khoảng hợp lý, và vẫn tạo ra một tấm ảnh

Tổn hại lộ diện ở chỗ W(1) tính ra -1 trong khi lẽ ra phải là 0. Weight âm cộng dồn, tổng bị clip về 0, và triệu chứng nhìn thấy được là một gradient có đầu trái đen thui và một step edge mất các tone trung gian. Không gì trong cái failure đó chỉ tay vào một polynomial. Phép check bắt được nó trong vài giây là arithmetic chứ không phải thị giác: một interpolating kernel phải thỏa W(0) = 1 và W(±1) = W(±2) = 0, và bất kỳ kernel nào trượt cả ba điểm đó là có lỗi hệ số, chấm hết. Assert trên ba giá trị đó trong một unit test và cả lớp lỗi typo biến mất

Đồ thị nhánh ngoài bicubic Catmull-Rom của HotPDF cho thấy vì sao một hệ số sai trốn tốt: typo viết -5 thay vì -4 trong dạng Horner lồng vẫn tính được và để W(1) ở -1 cùng W(2) ở -2 trong khi điểm đó đòi bằng 0, nên assert W(0) bằng 1 cộng hai ràng buộc 0 bắt được nó trong vài giây
Một kernel hỏng chưa bao giờ crash, nó chỉ trả về những con số trông hợp lý, vì vậy mắt thường không bắt được một typo hệ số. W(0) = 1 với các điểm 0 tại cộng trừ một và hai là một unit test ba dòng

Dithering Floyd-Steinberg, và nó nằm ở đâu trong pipeline

Pass dither là một bài toán khác với resampling và nằm ở một điểm khác trong pipeline. RenderOutputDither áp error diffusion Floyd-Steinberg sau khi compose page xong, và đó là vị trí duy nhất hợp lý cho một print preview monochrome hay một export kiểu fax: phép toán này là về việc giảm một raster đã hoàn thành xuống một bit mỗi pixel, chứ không phải về cách từng ảnh đã được scale lúc vào

Thuật toán thì ngắn. Luminance được threshold ở 50 phần trăm, và quantization error được diffusion sang bốn hàng xóm với các weight kinh điển 7/16, 3/16, 5/16 và 1/16, lần lượt sang phải, dưới-trái, dưới và dưới-phải. Output pixel là 0 hoặc 255 ở mọi channel. Phương án ngây thơ thay thế cho bạn cái gì — một hard threshold không diffusion — biến một tấm ảnh thành bóng đen và đánh mất mọi mid-tone đang mang content

Vị trí trong render pipeline của dithering Floyd-Steinberg trong HotPDF: RenderOutputDither chạy sau khi compose page trên raster 24-bit đã hoàn thành, threshold luminance ở 50 phần trăm và diffusion từng quantization error sang phải và xuống dưới với các weight 7/16, 3/16, 5/16 và 1/16 qua một row buffer bắt buộc phải tích lũy, tạo ra output monochrome một bit
Dithering thuộc về sau composition vì nó giảm một raster đã hoàn thành xuống một bit, chứ không phải vì các ảnh đã được scale thế nào. Các diffusion weight cộng đúng một, và row buffer phải tích lũy chứ không ghi đè
// Dithering lúc render cho một thiết bị preview monochrome
Pdf.RenderOutputDither := True;

// Hoặc áp cùng pass đó lên một bitmap bạn đã có sẵn. Bitmap phải là
// pf24bit; hàm trả False thay vì đoán mò
if not HPDFFloydSteinbergDitherBitmap(Preview) then
  raise Exception.Create('dither expects a 24-bit bitmap');

// Truy cập kernel trực tiếp khi bạn resample ngoài document pipeline
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
  Small.SaveToFile('thumb.bmp');
finally
  Small.Free;
end;

Có đúng một chi tiết triển khai trong error diffusion mà ai cũng bị cắn một lần. Error buffer giữa các row phải tích lũy. Mỗi pixel ở row kế nhận đóng góp từ ba pixel khác nhau ở row hiện tại — các tap 3/16, 5/16 và 1/16 — và nếu code assign thay vì cộng, mỗi lần ghi vứt bỏ đóng góp trước đó và chỉ tap cuối sống sót. Ảnh vẫn trông có dither, chính vì thế mà khó nhận ra, nhưng texture sai và khả năng tái tạo tone trôi dạt. Test bắt được nó là định lượng: dither một vùng xám giữa đồng nhất và yêu cầu coverage phần lòng nằm giữa 40 và 60 phần trăm

Pipeline giảm dung lượng nên dùng tổ hợp nào?

Hãy khớp kernel với ảnh thực sự là gì, và coi dithering là chuyện của thiết bị chứ không phải chuyện nén. Với scan chụp ảnh phải sống qua một giới hạn dung lượng, rkLanczos3 ở 150 hay 200 DPI giữ được phần chi tiết người ta để ý trong khi cắt pixel count đi bốn lần hoặc hơn. Với screenshot, diagram và line art, rkHalftone thật sự ổn và nhanh hơn nhiều, vì những ảnh đó có ít tonal gradient phải giữ. Với một batch lẫn lộn mà bạn không thể soi từng ảnh, rkBicubic là điểm giữa hợp lý: hơn bilinear, số tap chỉ bằng khoảng nửa Lanczos3

Downsampling là một trong vài cần gạt, và không phải lúc nào cũng to nhất. Scan bilevel thường ăn tiền hơn hẳn với encoder nói trong nén bilevel JBIG2 native trong Delphi, nơi mối lợi đến từ symbol dictionary chứ không phải số pixel. Trước khi quyết, biết file bên trong thực sự có gì sẽ giúp nhiều, và đó là việc của extract ảnh cùng decode filter của chúng: một bản kê khai image object và compression hiện có cho biết resampling còn gì để gặt

Nếu bạn đang dựng bề mặt preview hiển thị kết quả, cùng đường render được tài liệu hóa trong render một page PDF ra bitmap chính là nơi RenderOutputDither phát huy tác dụng, để preview có dither và output có dither đi ra từ một code path thay vì hai bản triển khai trôi dạt nhau theo thời gian

Nguyên tắc rộng đứng sau cả hai tính năng là các setting chất lượng nên tường minh và đảo ngược được. HotPDF giữ hành vi lịch sử làm default để application hiện có nâng cấp mà không gặp một thay đổi bất ngờ về output hay thời gian, và để những đường đẹp hơn, chậm hơn cách đúng một phép gán property. Cả hai là một phần của HotPDF Delphi PDF component, cạnh phần máy móc resource optimization và rendering mà chúng dựng trên