Artykuł techniczny

Jądra downsamplingu obrazów i dithering druku w HotPDF

HotPDF wystawia trzy jądra downsamplingu obrazów przez właściwość ImageDownsampleKernel i osobny przebieg Floyd-Steinberg przez RenderOutputDither. Pierwsze kontroluje, jak wyglądają fotografie po tym, jak zmniejszysz je, by zmieścić się w budżecie rozmiaru, drugie — jak wyglądają po zredukowaniu strony do czerni i bieli. Żadne nie jest domyślnie włączone i oba są opt-in z tego samego powodu: kosztują realny czas

Nacisk, który sprowadza tu ludzi, jest znajomy. Zeskanowana umowa o rozmiarze 60 MB musi wyjść przez bramkę poczty odrzucającą cokolwiek ponad 10 MB, albo partia wyciągów ma wylądować na monochromatycznym urządzeniu w stylu faksu, które renderuje każdy szary piksel jako papier albo toner. Oba problemy to problemy resamplingu i oba mają szybką odpowiedź, która wygląda źle, oraz powolną, która wygląda dobrze

Czym faktycznie różnią się trzy jądra

THPDFResampleKernel ma trzy wartości i siedzą one w istotnie różnych punktach krzywej szybkość–jakość. rkHalftone deleguje do historycznej ścieżki GDI StretchBlt z trybem HALFTONE, która mimo nazwy jest filtrowaniem klasy bilinear: szybkie, wystarczające dla line artu i screenshotów, podatne na chrupiące krawędzie, które od razu poznajesz na zeskalowanych w dół fotografiach. rkBicubic odpala separowalne jądro Catmull-Rom, a rkLanczos3 separowalne okienkowane sinc o trzech płatkach wsparcia

Oba jądra separowalne chodzą jako dwa przebiegi, poziomy potem pionowy, z 6 do 12 tapami na piksel docelowy w czystym Pascalu. To z grubsza rząd wielkości wolniej niż ścieżka GDI, i dokładnie dlatego rkHalftone zostaje domyślne. Przy nocnej paczce tysięcy stron różnica to decyzja szeregowania, nie preferencja. Przy pojedynczym dokumencie, na który użytkownik czeka, Lanczos3 jest prawie darmowy i widocznie lepszy

Krzywe wag trzech jąder downsamplingu HotPDF: rkHalftone deleguje do ścieżki GDI HALFTONE klasy bilinear o supporcie jednego, rkBicubic odpala separowalny sześcienny Catmull-Rom o supporcie dwóch, a rkLanczos3 okienkowane sinc o supporcie trzech, wymieniając z grubsza rząd wielkości szybkości na widocznie lepsze fotografie
Trzy wartości jąder siedzą w istotnie różnych punktach krzywej szybkość–jakość: ścieżka GDI klasy bilinear, sześcienny Catmull-Rom i okienkowane sinc o trzech płatkach, a jądra separowalne normalizują wagi, więc nic nie dzwoni ponad czerń ani biel

Dwie własności implementacyjne warto znać, bo determinują, co wynik może, a czego nie może. Brzegi są clampowane przez replikację krawędzi, a nie zawijanie albo wygaszanie, a wagi są normalizowane per piksel docelowy. Raz oznaczają, że wynik nigdy nie dzwoni poniżej czerni ani powyżej bieli, więc klasyczna halo overshootu Lanczosa wokół twardej krawędzie nie pojawia się jako przycięte artefakty w zakodowanym obrazie

var
  Pdf: THotPDF;
  Info: THPDFLoadedResourceOptimizationInfo;
  Changed: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-contract.pdf');
    Pdf.ImageDownsampleKernel := rkLanczos3;   // ustaw przed wywołaniem
    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;

Argument MinimumSavingsBytes, wyżej 4096, to straż trzymająca operację w ryzach uczciwości. Rekodowanie obrazu już sprawnie skompresowanego może dać strumień większy niż oryginał, a downsampler ślepo zastępujący każdy obraz od czasu do czasu powiększy plik, który miał zmniejszyć. Próg mówi: zatwierdź zastąpienie tylko, gdy oszczędza przynajmniej tyle bajtów. PreservedCalibratedImageCount raportuje drugą konserwatywną decyzję, obrazy pozostawione nietknięte, bo niosą skalibrowaną przestrzeń kolorów, którą resampling by skompromitował

Dlaczego zły współczynnik wielomianu tak trudno wychwycić?

Bo zepsute jądro interpolacji nie wywala się i nie rzuca wyjątkiem, po prostu produkuje obraz, który wygląda subtelnie źle w sposób, którego nikt nie umie przypisać. Jądro Catmull-Rom jest sześcienne odcinkami, a jego zewnętrzna gałąź w zagnieżdżonej postaci Hornera to ((-0.5t + 2.5)t - 4)t + 2. Zapisz ten środkowy współczynnik jako -5 zamiast -4, a funkcja nadal się liczy, nadal zwraca liczby w wiarygodnym zakresie i nadal produkuje obraz

Szkoda wychodzi jako W(1) liczące się do -1 tam, gdzie musi być 0. Ujemne wagi się kumulują, suma clipuje się do zera, a widoczny objaw to gradient czerniejący lewym końcem i krawędź skokowa tracąca tony pośrednie. Nic w tej awarii nie wskazuje na wielomian. Kontrola, która łapie to w sekundy, jest arytmetyczna, nie wizualna: jądro interpolujące musi spełniać W(0) = 1 oraz W(±1) = W(±2) = 0, a każde jądro, które mija te trzy punkty, ma błąd współczynnika i kropka. Zasertuj te trzy wartości w teście jednostkowym, a cała klasa defektów od literówek znika

Wykres zewnętrznej gałęzi sześciennej Catmull-Rom w HotPDF pokazujący, dlaczego zły współczynnik się kryje: literówka wpisująca -5 zamiast -4 w zagnieżdżonej postaci Hornera nadal się liczy i zostawia W(1) na -1 oraz W(2) na -2 tam, gdzie wymagane jest zero, więc asercja W(0) równa 1 plus dwa warunki zerowe łapie to w sekundy
Zepsute jądro nigdy się nie wywala, po prostu zwraca liczby wyglądające wiarygodnie, dlatego oko nie wyłapie literówki we współczynniku. W(0) = 1 z zerami na plus i minus jeden oraz dwa to trójlinijkowy test jednostkowy

Dithering Floyd-Steinberg i jego miejsce w potoku

Przebieg ditheringu to inny problem niż resampling i mieszka w innym punkcie potoku. RenderOutputDither nakłada dyfuzję błędu Floyd-Steinberg po kompozycji strony, co jest jedynym sensownym miejscem dla monochromatycznego podglądu wydruku albo eksportu w stylu faksu: ta operacja dotyczy redukcji skończonego rastra do jednego bitu na piksel, a nie tego, jak pojedyncze obrazy były skalowane na wejściu

Sam algorytm jest krótki. Luminancja jest progowana na 50 procentach, a błąd kwantyzacji jest dyfundowany do czterech sąsiadów z klasycznymi wagami 7/16, 3/16, 5/16 i 1/16, w prawo, w lewo w dół, w dół i w prawo w dół. Piksel wyjściowy to 0 albo 255 w każdym kanale. Naiwna alternatywa daje zamiast tego twardy próg bez dyfuzji, co zamienia fotografię w sylwetkę i gubi każdy ton średni, który niósł zawartość

Pozycja ditheringu Floyd-Steinberg HotPDF w potoku renderowania: RenderOutputDither chodzi po kompozycji strony na skończonym rastrze 24-bitowym, progując luminancję na 50 procentach i dyfundując każdy błąd kwantyzacji w prawo i w dół wagami 7/16, 3/16, 5/16 i 1/16 przez bufor wiersza, który musi akumulować, produkując monochromatyczne wyjście jednobitowe
Dithering należy po kompozycji, bo redukuje skończony raster do jednego bitu, a nie ze względu na to, jak obrazy były skalowane. Wagi dyfuzji sumują się do jedności, a bufor wiersza musi akumulować, nie nadpisywać
// Dithering w czasie renderowania dla monochromatycznego urządzenia podglądu
Pdf.RenderOutputDither := True;

// Albo nałóż ten sam przebieg na bitmapę, którą już masz. Bitmapa musi
// być pf24bit; funkcja zwraca False, zamiast zgadywać
if not HPDFFloydSteinbergDitherBitmap(Preview) then
  raise Exception.Create('dither expects a 24-bit bitmap');

// Bezpośredni dostęp do jądra, gdy resamplujesz poza potokiem dokumentu
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
  Small.SaveToFile('thumb.bmp');
finally
  Small.Free;
end;

Jeden detal implementacyjny dyfuzji błędu kąsa każdego raz. Bufor błędu między wierszami musi akumulować. Każdy piksel w następnym wierszu dostaje wkłady od trzech różnych pikseli bieżącego wiersza, tapy 3/16, 5/16 i 1/16, i jeśli kod przypisuje zamiast dodawać, każdy zapis wyrzuca poprzedni wkład i przeżywa tylko ostatni tap. Obraz nadal wygląda na ditherowany, co utrudnia zauważenie, ale tekstura jest zła i reprodukcja tonalna dryfuje. Test, który to łapie, jest ilościowy: zditheruj jednolite pole średniej szarości i wymagaj, by pokrycie wnętrza wylądowało między 40 a 60 procentami

Której kombinacji powinien użyć potok redukcji rozmiaru?

Dopasuj jądro do tego, czym obrazy faktycznie są, a dithering traktuj jako sprawę urządzenia, nie kompresji. Dla skanów fotograficznych, które muszą zmieścić się w budżecie rozmiaru, rkLanczos3 przy 150 albo 200 DPI trzyma detal, który ludzie zauważają, ścinając liczbę pikseli czterokrotnie i bardziej. Dla screenshotów, diagramów i line artu rkHalftone jest naprawdę w porządku i znacznie szybszy, bo te obrazy mają niewiele gradientów tonalnych do zachowania. Dla mieszanej paczki, w której nie da się przejrzeć każdego obrazu, rkBicubic to rozsądny środek: lepszy niż bilinear, z grubsza połowa liczby tapów Lanczos3

Downsampling to jedna dźwignia z kilku i nie zawsze największa. Skany bilevel zwykle reagują znacznie lepiej na enkoder opisany w artykule natywna kompresja JBIG2 bilevel w Delphi, gdzie zysk bierze się ze słowników symboli, a nie z liczby pikseli. Zanim zdecydujesz, pomaga wiedzieć, co faktycznie jest w pliku, do tego służy wydobywanie obrazów i ich filtrów decode: inwentaryzacja obiektów obrazów i ich istniejącej kompresji mówi ci, czy resampling ma czym zyskać

Jeśli budujesz powierzchnię podglądu pokazującą wynik, ta sama ścieżka renderowania udokumentowana w artykule renderowanie strony PDF do bitmapy to miejsce, gdzie działa RenderOutputDither, więc zditherowany podgląd i zditherowane wyjście pochodzą z jednej ścieżki kodu, a nie z dwóch implementacji, które się rozjeżdżają

Szeroka zasada za obiema funkcjami: ustawienia jakości powinny być jawne i odwracalne. HotPDF trzyma historyczne zachowanie jako domyślne, więc istniejąca aplikacja upgraduje się bez zaskakującej zmiany w wyjściu albo czasie, a ładniejsze, wolniejsze ścieżki stawia jedno przypisanie właściwości dalej. Obie są częścią komponentu PDF HotPDF dla Delphi, razem z maszynerią optymalizacji zasobów i renderowania, na której budują