Τεχνικό Άρθρο

Πυρήνες downsampling και dithering εκτύπωσης στο HotPDF

Το HotPDF εκθέτει τρεις πυρήνες downsampling εικόνων μέσω της ιδιότητας ImageDownsampleKernel και ένα ξεχωριστό πέρασμα Floyd-Steinberg μέσω του RenderOutputDither. Ο πρώτος ελέγχει πώς φαίνονται οι φωτογραφίες αφού τις μαζέψετε για να πετύχετε budget μεγέθους, ο δεύτερος ελέγχει πώς φαίνονται αφού μια σελίδα περιοριστεί σε άσπρο-μαύρο. Κανένας δεν είναι ενεργός εξ ορισμού, και οι δύο είναι opt-in για τον ίδιο λόγο: κοστίζουν πραγματικό χρόνο

Η πίεση που οδηγεί κόσμο εδώ είναι γνώριμη. Ένα σκαναρισμένο συμβόλαιο 60 MB πρέπει να βγει μέσα από email gateway που απορρίπτει οτιδήποτε πάνω από 10 MB, ή μια παρτίδα καταστάσεων πρέπει να καταλήξει σε μονόχρωμη συσκευή τύπου fax που αποδίδει κάθε γκρι pixel είτε ως χαρτί είτε ως toner. Και τα δύο προβλήματα είναι προβλήματα resampling, και τα δύο έχουν μια γρήγορη απάντηση που φαίνεται άσχημη και μια αργή που φαίνεται σωστή

Σε τι διαφέρουν στην πραγματικότητα οι τρεις πυρήνες

Η THPDFResampleKernel έχει τρεις τιμές, και κάθονται σε πραγματικά διαφορετικά σημεία της καμπύλης ταχύτητας και ποιότητας. Η rkHalftone αναθέτει στην ιστορική διαδρομή GDI StretchBlt με τη λειτουργία HALFTONE, που παρά το όνομα είναι φιλτράρισμα κλάσης bilinear: γρήγορο, επαρκές για line art και screenshots, και επιρρεπές στις τραχιές ακμές που αναγνωρίζεις αμέσως σε μαζεμένες φωτογραφίες. Η rkBicubic τρέχει διαχωρίσιμο πυρήνα Catmull-Rom, και η rkLanczos3 τρέχει διαχωρίσιμο windowed sinc με υποστήριξη τριών λοβών

Και οι δύο διαχωρίσιμοι πυρήνες τρέχουν ως δύο περάσματα, οριζόντιο μετά κάθετο, με 6 έως 12 taps ανά pixel προορισμού σε καθαρή Pascal. Είναι κατά προσέγγιση μια τάξη μεγέθους πιο αργό από τη διαδρομή GDI, που είναι ακριβώς ο λόγος που η rkHalftone μένει η προεπιλογή. Σε νυχτερινή παρτίδα χιλιάδων σελίδων η διαφορά είναι απόφαση προγραμματισμού, όχι προτίμηση. Σε ένα έγγραφο που ένας χρήστης περιμένει, το Lanczos3 είναι σχεδόν δωρεάν και ορατά καλύτερο

Καμπύλες βαρύτητας των τριών πυρήνων downsampling του HotPDF: η rkHalftone αναθέτει στη διαδρομή GDI HALFTONE κλάσης bilinear με υποστήριξη ενός, η rkBicubic τρέχει διαχωρίσιμο κυβικό Catmull-Rom με υποστήριξη δύο, και η rkLanczos3 τρέχει windowed sinc με υποστήριξη τριών, ανταλλάσσοντας κατά προσέγγιση μια τάξη μεγέθους σε ταχύτητα για ορατά καλύτερες φωτογραφίες
Οι τρεις τιμές πυρήνα κάθονται σε πραγματικά διαφορετικά σημεία της καμπύλης ταχύτητας και ποιότητας: μια διαδρομή GDI κλάσης bilinear, ένα κυβικό Catmull-Rom και ένα windowed sinc τριών λοβών, και οι διαχωρίσιμοι πυρήνες κανονικοποιούν βάρη ώστε τίποτα να μην χτυπά πέρα από μαύρο ή άσπρο

Δύο ιδιότητες υλοποίησης αξίζει να ξέρετε γιατί καθορίζουν τι μπορεί και τι δεν μπορεί να κάνει η έξοδος. Τα περιθώρια σφίγγονται με αναπαραγωγή ακμής αντί για αναδίπλωση ή ξεθώριασμα, και τα βάρη κανονικοποιούνται ανά pixel προορισμού. Μαζί, αυτά τα δύο σημαίνουν ότι το αποτέλεσμα δεν χτυπά ποτέ κάτω από μαύρο ή πάνω από άσπρο, οπότε το κλασικό overshoot φωτοστέφανο του Lanczos γύρω από σκληρή ακμή δεν εμφανίζεται ως κομμένα artifacts στην κωδικοποιημένη εικόνα

var
  Pdf: THotPDF;
  Info: THPDFLoadedResourceOptimizationInfo;
  Changed: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('scanned-contract.pdf');
    Pdf.ImageDownsampleKernel := rkLanczos3;   // ορίστε πριν την κλήση
    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;

Το όρισμα MinimumSavingsBytes, 4096 παραπάνω, είναι ο φρουρός που κρατά την πράξη τίμια. Η επαν-κωδικοποίηση μιας εικόνας που ήταν ήδη αποδοτικά συμπιεσμένη μπορεί να παράγει μεγαλύτερο stream από το πρωτότυπο, και ένας downsampler που αντικαθιστά τυφλά κάθε εικόνα θα μεγαλώσει καμιά φορά το αρχείο που του ζητήθηκε να μαζέψει. Το κατώφλι λέει: υποβάλε την αντικατάσταση μόνο όταν εξοικονομεί τουλάχιστον τόσα bytes. Η PreservedCalibratedImageCount αναφέρει την άλλη συντηρητική απόφαση, εικόνες που αφήνονται ανέγγιχτες επειδή κουβαλούν calibrated color space που το resampling θα υπονόμευε

Γιατί ένας λάθος συντελεστής πολυωνύμου είναι τόσο δύσκολο να εντοπιστεί;

Γιατί ένας χαλασμένος πυρήνας παρεμβολής δεν κρασάρει ούτε σηκώνει exception, απλώς παράγει μια εικόνα που φαίνεται λεπτά λάθος με τρόπο που κανείς δεν μπορεί να αποδώσει. Ο πυρήνας Catmull-Rom είναι τεμαδιαία κυβικός, και ο εξωτερικός του κλάδος σε εμφωλευμένη μορφή Horner είναι ((-0.5t + 2.5)t - 4)t + 2. Γράψτε εκείνον τον μεσαίο συντελεστή -5 αντί για -4 και η συνάρτηση ακόμα υπολογίζεται, ακόμα επιστρέφει αριθμούς σε πιθανό εύρος, και ακόμα παράγει εικόνα

Η ζημιά εμφανίζεται ως W(1) που υπολογίζεται σε -1 ενώ πρέπει να είναι 0. Αρνητικά βάρη συσσωρεύονται, το άθροισμα σφίγγεται στο μηδέν, και το ορατό σύμπτωμα είναι μια διαβάθμιση της οποίας το αριστερό άκρο μαυρίζει και μια ακμή βήματος που χάνει τους ενδιάμεσους τόνους της. Τίποτα στην αποτυχία δεν δείχνει πολυώνυμο. Ο έλεγχος που το πιάνει σε δευτερόλεπτα είναι αριθμητικός και όχι οπτικός: ένας πυρήνας παρεμβολής πρέπει να ικανοποιεί W(0) = 1 και W(±1) = W(±2) = 0, και κάθε πυρήνας που χάνει αυτά τα τρία σημεία έχει σφάλμα συντελεστή, τέλος. Κάντε assert σε αυτές τις τρεις τιμές σε unit test και όλη η κατηγορία τυπογραφικών ελαττωμάτων εξαφανίζεται

Γράφημα του εξωτερικού κλάδου bicubic Catmull-Rom του HotPDF που δείχνει γιατί κρύβεται ένας λάθος συντελεστής: το τυπογραφικό που γράφει -5 αντί για -4 στην εμφωλευμένη μορφή Horner ακόμα υπολογίζεται και αφήνει το W(1) σε -1 και το W(2) σε -2 όπου απαιτείται μηδέν, οπότε assert ότι W(0) ισούται με 1 συν οι δύο μηδενικοί περιορισμοί το πιάνει σε δευτερόλεπτα
Ένας χαλασμένος πυρήνας δεν κρασάρει ποτέ, απλώς επιστρέφει αριθμούς που φαίνονται πιθανοί, γι αυτό το μάτι δεν πιάνει τυπογραφικό σε συντελεστή. W(0) = 1 με μηδενικά στο συν και πλην ένα και δύο είναι unit test τριών γραμμών

Floyd-Steinberg dithering, και πού ανήκει στο pipeline

Το πέρασμα dither είναι διαφορετικό πρόβλημα από το resampling και κατοικεί σε διαφορετικό σημείο του pipeline. Το RenderOutputDither εφαρμόζει διάχυση σφάλματος Floyd-Steinberg μετά τη σύνθεση σελίδας, που είναι η μόνη τοποθέτηση που βγάζει νόημα για μονόχρωμη προεπισκόπηση εκτύπωσης ή εξαγωγή τύπου fax: η πράξη αφορά τη μείωση ενός ολοκληρωμένου raster σε ένα bit ανά pixel, όχι το πώς κλιμακώθηκαν οι επιμέρους εικόνες στην πορεία

Ο αλγόριθμος ίδιος είναι σύντομος. Η φωτεινότητα κατωφλιώνεται στο 50 τοις εκατό, και το σφάλμα κβαντοποίησης διαχέεται σε τέσσερις γείτονες με τα κλασικά βάρη 7/16, 3/16, 5/16 και 1/16, προς τα δεξιά, κάτω-αριστερά, κάτω και κάτω-δεξιά. Το pixel εξόδου είναι 0 ή 255 σε κάθε κανάλι. Όσο σας δίνει η ναϊβική εναλλακτική, ένα σκληρό κατώφλι χωρίς διάχυση, μετατρέπει φωτογραφία σε σιλουέτα και χάνει κάθε ενδιάμεσο τόνο που κουβαλούσε το περιεχόμενο

Τοποθέτηση στο render pipeline του Floyd-Steinberg dithering του HotPDF: το RenderOutputDither τρέχει μετά τη σύνθεση σελίδας πάνω στο ολοκληρωμένο raster 24-bit, κατωφλιώνει τη φωτεινότητα στο 50 τοις εκατό και διαχέει κάθε σφάλμα κβαντοποίησης δεξιά και κάτω με βάρη 7/16, 3/16, 5/16 και 1/16 μέσα από buffer γραμμής που πρέπει να αθροίζει, παράγοντας μονόχρωμη έξοδο ενός bit
Το dithering ανήκει μετά τη σύνθεση γιατί μειώνει ένα ολοκληρωμένο raster σε ένα bit, όχι λόγω του πώς κλιμακώθηκαν οι εικόνες. Τα βάρη διάχυσης αθροίζονται σε ένα, και το buffer γραμμής πρέπει να αθροίζει και όχι να ξεγράφει
// Dithering την ώρα της απόδοσης για μονόχρωμη συσκευή προεπισκόπησης
Pdf.RenderOutputDither := True;

// Ή εφαρμόστε το ίδιο πέρασμα σε bitmap που έχετε ήδη. Το bitmap πρέπει
// να είναι pf24bit· η συνάρτηση επιστρέφει False αντί να μαντέψει
if not HPDFFloydSteinbergDitherBitmap(Preview) then
  raise Exception.Create('dither expects a 24-bit bitmap');

// Άμεση πρόσβαση πυρήνα όταν κάνετε resampling εκτός του pipeline εγγράφου
Small := HPDFResizeBitmapKernel(Large, 640, 480, rkLanczos3);
try
  Small.SaveToFile('thumb.bmp');
finally
  Small.Free;
end;

Υπάρχει μια λεπτομέρεια υλοποίησης στη διάχυση σφάλματος που δαγκώνει τον καθένα μία φορά. Το buffer σφάλματος από γραμμή σε γραμμή πρέπει να αθροίζει. Κάθε pixel της επόμενης γραμμής λαμβάνει συνεισφορές από τρία διαφορετικά pixels της τρέχουσας γραμμής, τα taps 3/16, 5/16 και 1/16, και αν ο κώδικας αναθέτει αντί να προσθέτει, κάθε εγγραφή απορρίπτει την προηγούμενη συνεισφορά και μόνο το τελευταίο tap επιβιώνει. Η εικόνα εξακολουθεί να φαίνεται dithered, που είναι όσο την κάνει δύσκολη να προσέξει κανείς, αλλά η υφή είναι λάθος και η τονική αναπαραγωγή παρασύρεται. Το test που το πιάνει είναι ποσοτικό: κάντε dither σε ομοιόμορφο πεδίο μεσο-γκρι και απαιτήστε η κάλυψη του εσωτερικού να πέσει μεταξύ 40 και 60 τοις εκατό

Ποιον συνδυασμό να χρησιμοποιήσει ένα pipeline μείωσης μεγέθους;

Ταίριαξτε τον πυρήνα με ό,τι είναι στην πραγματικότητα οι εικόνες, και μεταχειριστείτε το dithering ως ζήτημα συσκευής και όχι συμπίεσης. Για φωτογραφικές σαρώσεις που πρέπει να επιβιώσουν από budget μεγέθους, rkLanczos3 στα 150 ή 200 DPI κρατά τη λεπτομέρεια που παρατηρεί ο κόσμος ενώ κόβει το πλήθος pixels κατά συντελεστή τέσσερα ή περισσότερο. Για screenshots, διαγράμματα και line art, το rkHalftone είναι πραγματικά καλό και πολύ πιο γρήγορο, γιατί εκείνες οι εικόνες έχουν λίγες τονικές διαβαθμίσεις να διαφυλάξουν. Για μικτή παρτίδα όπου δεν μπορείτε να εξετάσετε κάθε εικόνα, το rkBicubic είναι το λογικό μέσο: καλύτερο από bilinear, περίπου μισό πλήθος taps του Lanczos3

Το downsampling είναι ένας μοχλός ανάμεσα σε αρκετούς, και δεν είναι πάντα ο μεγαλύτερος. Οι bilevel σαρώσεις συνήθως ανταποκρίνονται πολύ καλύτερα στον encoder που καλύπτει η εγγενής συμπίεση JBIG2 bilevel σε Delphi, όπου το κέρδος έρχεται από λεξικά symbols και όχι από πλήθη pixels. Πριν αποφασίσετε, βοηθά να ξέρετε τι είναι πραγματικά στο αρχείο, που είναι ο σκοπός της εξαγωγής εικόνων και των decode filters τους: μια απογραφή αντικειμένων εικόνων και της υπάρχουσας συμπίεσής τους σας λέει αν το resampling έχει κάτι να κερδίσει

Αν χτίζετε την επιφάνεια προεπισκόπησης που δείχνει το αποτέλεσμα, η ίδια διαδρομή απόδοσης που τεκμηριώνεται στο rendering σελίδας PDF σε bitmap είναι εκεί όπου δρα το RenderOutputDither, ώστε η dithered προεπισκόπηση και η dithered έξοδος να βγαίνουν από ένα μονοπάτι κώδικα και όχι από δύο υλοποιήσεις που αποκλίνουν

Η φαρδιά αρχή πίσω από και τις δύο δυνατότητες είναι ότι οι ρυθμίσεις ποιότητας πρέπει να είναι ρητές και αναστρέψιμες. Το HotPDF κρατά την ιστορική συμπεριφορά ως προεπιλογή ώστε μια υπάρχουσα εφαρμογή να αναβαθμίζει χωρίς απροσδόκητη αλλαγή σε έξοδο ή χρονισμό, και βάζει τις ομορφότερες, πιο αργές διαδρομές σε απόσταση ανάθεσης μίας ιδιότητας. Και τα δύο είναι μέρος του HotPDF Delphi PDF component, μαζί με τα μηχανήματα βελτιστοποίησης πόρων και απόδοσης πάνω στα οποία χτίζουν