Προγραμματισμός

Επεξεργασία εικόνας με Java 2D

Η επεξεργασία εικόνας είναι η τέχνη και η επιστήμη του χειρισμού ψηφιακών εικόνων. Στέκεται με το ένα πόδι σταθερά στα μαθηματικά και το άλλο στην αισθητική, και είναι ένα κρίσιμο στοιχείο των γραφικών υπολογιστικών συστημάτων. Εάν έχετε ασχοληθεί ποτέ με τη δημιουργία των δικών σας εικόνων για ιστοσελίδες, αναμφίβολα θα εκτιμήσετε τη σημασία των δυνατοτήτων χειρισμού εικόνων του Photoshop για τον καθαρισμό των σαρώσεων και την εκκαθάριση εικόνων λιγότερο από το βέλτιστο.

Εάν κάνατε κάποια εργασία επεξεργασίας εικόνων στο JDK 1.0 ή 1.1, ίσως θυμάστε ότι ήταν λίγο ασαφής. Το παλιό μοντέλο παραγωγών και καταναλωτών δεδομένων εικόνας είναι δύσκολο για την επεξεργασία εικόνας. Πριν από το JDK 1.2, περιλαμβανόταν επεξεργασία εικόνας MemoryImageSourceμικρό, PixelGrabbers, και άλλα τέτοια arcana. Το Java 2D, ωστόσο, παρέχει ένα καθαρότερο, ευκολότερο στη χρήση μοντέλο.

Αυτό το μήνα, θα εξετάσουμε τους αλγόριθμους πίσω από πολλές σημαντικές λειτουργίες επεξεργασίας εικόνας (ops) και να σας δείξουμε πώς μπορούν να εφαρμοστούν χρησιμοποιώντας το Java 2D. Θα σας δείξουμε επίσης πώς χρησιμοποιούνται αυτές οι λειτουργίες για να επηρεάσουν την εμφάνιση της εικόνας.

Επειδή η επεξεργασία εικόνων είναι μια πραγματικά χρήσιμη αυτόνομη εφαρμογή του Java 2D, δημιουργήσαμε το παράδειγμα αυτού του μήνα, το ImageDicer, ώστε να είναι όσο το δυνατόν πιο επαναχρησιμοποιήσιμο για τις δικές σας εφαρμογές. Αυτό το μοναδικό παράδειγμα δείχνει όλες τις τεχνικές επεξεργασίας εικόνας που θα καλύψουμε στη στήλη αυτού του μήνα.

Λάβετε υπόψη ότι λίγο πριν από τη δημοσίευση αυτού του άρθρου, η Sun κυκλοφόρησε το κιτ ανάπτυξης Java 1.2 Beta 4. Το Beta 4 φαίνεται να δίνει καλύτερη απόδοση για το παράδειγμα επεξεργασίας εικόνων, αλλά προσθέτει επίσης μερικά νέα σφάλματα που περιλαμβάνουν οριακό έλεγχο ConvolveOpμικρό. Αυτά τα προβλήματα επηρεάζουν τα παραδείγματα ανίχνευσης και οξύτητας που χρησιμοποιούμε στη συζήτησή μας.

Πιστεύουμε ότι αυτά τα παραδείγματα είναι πολύτιμα, οπότε αντί να τα παραλείψουμε εντελώς, συμβιβαστήκαμε: για να διασφαλίσουμε ότι λειτουργεί, ο κώδικας παραδείγματος αντικατοπτρίζει τις αλλαγές Beta 4, αλλά διατηρήσαμε τα στοιχεία από την εκτέλεση 1.2 Beta 3, ώστε να μπορείτε να δείτε τις λειτουργίες λειτουργεί σωστά.

Ας ελπίσουμε ότι, η Sun θα αντιμετωπίσει αυτά τα σφάλματα πριν από την τελική έκδοση Java 1.2.

Η επεξεργασία εικόνας δεν είναι επιστήμη πυραύλων

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

Το 2D API εισάγει ένα απλό μοντέλο επεξεργασίας εικόνας για να βοηθήσει τους προγραμματιστές να χειριστούν αυτά τα pixel εικόνας. Αυτό το μοντέλο βασίζεται στο java.awt.image.BufferedImage κλάσης και επεξεργασίας εικόνας όπως περιελιγμός και κατώφλι αντιπροσωπεύονται από υλοποιήσεις του java.awt.image.BufferedImageOp διεπαφή.

Η εφαρμογή αυτών των λειτουργιών είναι σχετικά απλή. Ας υποθέσουμε, για παράδειγμα, ότι έχετε ήδη την αρχική εικόνα ως BufferedImage που ονομάζεται πηγή. Η εκτέλεση της λειτουργίας που απεικονίζεται στο παραπάνω σχήμα θα χρειαζόταν μόνο μερικές γραμμές κώδικα:

001 σύντομο [] όριο = νέο σύντομο [256]; 002 για (int i = 0; i <256; i ++) όριο 003 [i] = (i <128); (σύντομη) 0: (σύντομη) 255; 004 BufferedImageOp thresholdOp = 005 new LookupOp (νέο ShortLookupTable (0, threshold), null); 006 BufferedImage destination = thresholdOp.filter (πηγή, null); 

Αυτό είναι το μόνο που υπάρχει σε αυτό. Τώρα ας ρίξουμε μια ματιά στα βήματα με περισσότερες λεπτομέρειες:

  1. Εγκαταστήστε τη λειτουργία εικόνας της επιλογής σας (γραμμές 004 και 005). Εδώ χρησιμοποιήσαμε ένα Αναζήτηση, η οποία είναι μία από τις λειτουργίες εικόνας που περιλαμβάνονται στην υλοποίηση Java 2D. Όπως κάθε άλλη λειτουργία εικόνας, εφαρμόζει το BufferedImageOp διεπαφή. Θα μιλήσουμε περισσότερα για αυτήν τη λειτουργία αργότερα.

  2. Καλέστε τη λειτουργία φίλτρο() μέθοδος με την εικόνα προέλευσης (γραμμή 006). Η πηγή επεξεργάζεται και επιστρέφεται η εικόνα προορισμού.

Εάν έχετε ήδη δημιουργήσει ένα BufferedImage που θα κρατήσει την εικόνα προορισμού, μπορείτε να τη μεταβιβάσετε ως τη δεύτερη παράμετρο φίλτρο(). Εάν περάσετε μηδενικό, όπως κάναμε στο παραπάνω παράδειγμα, ένας νέος προορισμός BufferedImage δημιουργειται.

Το 2D API περιλαμβάνει μια χούφτα από αυτές τις ενσωματωμένες λειτουργίες εικόνας. Θα συζητήσουμε τρία σε αυτήν τη στήλη: περιελιγμός,πίνακες αναζήτησης, και κατώφλι. Ανατρέξτε στην τεκμηρίωση Java 2D για πληροφορίες σχετικά με τις υπόλοιπες λειτουργίες που είναι διαθέσιμες στο 2D API (Πόροι).

Περιελιγμός

ΕΝΑ περιελιγμός Η λειτουργία σάς επιτρέπει να συνδυάσετε τα χρώματα ενός εικονοστοιχείου πηγής και των γειτόνων του για να προσδιορίσετε το χρώμα ενός εικονοστοιχείου προορισμού. Αυτός ο συνδυασμός καθορίζεται χρησιμοποιώντας ένα πυρήνας, ένας γραμμικός τελεστής που καθορίζει την αναλογία κάθε χρώματος εικονοστοιχείου πηγής που χρησιμοποιείται για τον υπολογισμό του χρώματος εικονοστοιχείου προορισμού.

Σκεφτείτε τον πυρήνα ως πρότυπο που επικαλύπτεται στην εικόνα για να εκτελέσετε μια συνέλιξη σε ένα pixel κάθε φορά. Καθώς κάθε εικονοστοιχείο είναι περίπλοκο, το πρότυπο μετακινείται στο επόμενο εικονοστοιχείο στην εικόνα προέλευσης και επαναλαμβάνεται η διαδικασία συνέλιξης. Ένα αρχικό αντίγραφο της εικόνας χρησιμοποιείται για τιμές εισόδου για τη συνέλιξη και όλες οι τιμές εξόδου αποθηκεύονται σε ένα αντίγραφο προορισμού της εικόνας. Μόλις ολοκληρωθεί η διαδικασία συνέλιξης, επιστρέφεται η εικόνα προορισμού.

Το κέντρο του πυρήνα μπορεί να θεωρηθεί ότι επικαλύπτει το εικονοστοιχείο πηγής που είναι περίπλοκο. Για παράδειγμα, μια λειτουργία συνέλιξης που χρησιμοποιεί τον ακόλουθο πυρήνα δεν επηρεάζει μια εικόνα: κάθε εικονοστοιχείο προορισμού έχει το ίδιο χρώμα με το αντίστοιχο εικονοστοιχείο προέλευσης.

 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 

Ο βασικός κανόνας για τη δημιουργία πυρήνων είναι ότι όλα τα στοιχεία πρέπει να προσθέσουν έως και 1 εάν θέλετε να διατηρήσετε τη φωτεινότητα της εικόνας.

Στο 2D API, μια συνέλιξη αντιπροσωπεύεται από ένα java.awt.image.ConvolveOp. Μπορείτε να δημιουργήσετε ένα ConvolveOp χρησιμοποιώντας έναν πυρήνα, ο οποίος αντιπροσωπεύεται από μια παρουσία του java.awt.image.Kernel. Ο ακόλουθος κώδικας κατασκευάζει ένα ConvolveOp χρησιμοποιώντας τον πυρήνα που παρουσιάζεται παραπάνω.

001 float [] ταυτότητα πυρήνα = {002 0,0f, 0,0f, 0,0f, 003 0,0f, 1,0f, 0,0f, 004 0,0f, 0,0f, 0,0f 005}; 006 BufferedImageOp ταυτότητα = 007 νέο ConvolveOp (νέο πυρήνα (3, 3, ταυτότητα πυρήνα)); 

Η λειτουργία συνέλιξης είναι χρήσιμη για την εκτέλεση πολλών κοινών λειτουργιών σε εικόνες, τις οποίες θα αναλύσουμε σε λίγο. Διαφορετικοί πυρήνες παράγουν ριζικά διαφορετικά αποτελέσματα.

Τώρα είμαστε έτοιμοι να παρουσιάσουμε μερικούς πυρήνες επεξεργασίας εικόνας και τα αποτελέσματά τους. Η μη τροποποιημένη εικόνα μας είναι Lady Agnew της Lochnaw, ζωγραφισμένο από τον John Singer Sargent το 1892 και το 1893.

Ο παρακάτω κώδικας δημιουργεί ένα ConvolveOp που συνδυάζει ίσες ποσότητες κάθε εικονοστοιχείου πηγής και των γειτόνων του. Αυτή η τεχνική έχει ως αποτέλεσμα ένα θόλωμα.

001 float ένατο = 1.0f / 9.0f; 002 float [] blurKernel = {003 ένατο, ένατο, ένατο, 004 ένατο, ένατο, ένατο, 005 ένατο, ένατο, ένατο 006}; 007 BufferedImageOp blur = νέο ConvolveOp (νέο πυρήνα (3, 3, blurKernel)); 

Ένας άλλος κοινός πυρήνας συνέλιξης δίνει έμφαση στα άκρα της εικόνας. Αυτή η λειτουργία ονομάζεται συνήθως ανίχνευση άκρων. Σε αντίθεση με τους άλλους πυρήνες που παρουσιάζονται εδώ, οι συντελεστές αυτού του πυρήνα δεν προσθέτουν έως 1.

001 float [] edgeKernel = {002 0.0f, -1.0f, 0.0f, 003 -1.0f, 4.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005}; 006 BufferedImageOp edge = νέο ConvolveOp (νέο πυρήνα (3, 3, edgeKernel)); 

Μπορείτε να δείτε τι κάνει αυτός ο πυρήνας κοιτάζοντας τους συντελεστές στον πυρήνα (γραμμές 002-004). Σκεφτείτε για λίγο πώς χρησιμοποιείται ο πυρήνας ανίχνευσης άκρων σε μια περιοχή που είναι εντελώς ένα χρώμα. Κάθε εικονοστοιχείο θα καταλήξει χωρίς χρώμα (μαύρο) επειδή το χρώμα των γύρω εικονοστοιχείων ακυρώνει το χρώμα του εικονοστοιχείου προέλευσης. Τα φωτεινά pixel που περιβάλλονται από σκούρα pixel θα παραμείνουν φωτεινά.

Παρατηρήστε πόσο πιο σκοτεινή είναι η επεξεργασμένη εικόνα σε σύγκριση με την αρχική. Αυτό συμβαίνει επειδή τα στοιχεία του πυρήνα ανίχνευσης άκρων δεν προσθέτουν έως 1.

Μια απλή παραλλαγή στην ανίχνευση άκρων είναι η ακονίζοντας πυρήνας. Σε αυτήν την περίπτωση, η εικόνα προέλευσης προστίθεται σε έναν πυρήνα ανίχνευσης άκρων ως εξής:

 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 -1.0 4.0 -1.0 + 0.0 1.0 0.0 = -1.0 5.0 -1.0 0.0 -1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 

Ο πυρήνας ακονίσματος είναι στην πραγματικότητα μόνο ένας πιθανός πυρήνας που ακονίζει τις εικόνες.

Η επιλογή ενός πυρήνα 3 x 3 είναι κάπως αυθαίρετη. Μπορείτε να ορίσετε πυρήνες οποιουδήποτε μεγέθους και πιθανώς δεν χρειάζεται καν να είναι τετράγωνο. Στο JDK 1.2 Beta 3 και 4, ωστόσο, ένας μη τετραγωνικός πυρήνας δημιούργησε σφάλμα εφαρμογής και ένας πυρήνας 5 x 5 μασούρισε τα δεδομένα εικόνας με τον πιο περίεργο τρόπο. Εκτός αν έχετε έναν επιτακτικό λόγο να απομακρυνθείτε από 3 πυρήνες, δεν το συνιστούμε.

Μπορεί επίσης να αναρωτιέστε τι συμβαίνει στην άκρη της εικόνας. Όπως γνωρίζετε, η λειτουργία συνέλιξης λαμβάνει υπόψη τους γείτονες ενός εικονοστοιχείου προέλευσης, αλλά τα εικονοστοιχεία προέλευσης στις άκρες της εικόνας δεν έχουν γείτονες στη μία πλευρά. ο ConvolveOp Η τάξη περιλαμβάνει σταθερές που καθορίζουν ποια συμπεριφορά πρέπει να είναι στα άκρα. ο EDGE_ZERO_FILL Η σταθερά καθορίζει ότι οι άκρες της εικόνας προορισμού έχουν οριστεί σε 0 EDGE_NO_OP Η σταθερά καθορίζει ότι τα εικονοστοιχεία προέλευσης κατά μήκος της άκρης της εικόνας αντιγράφονται στον προορισμό χωρίς να τροποποιηθούν. Εάν δεν καθορίσετε μια συμπεριφορά άκρων κατά την κατασκευή ενός ConvolveOp, EDGE_ZERO_FILL χρησιμοποιείται.

Το ακόλουθο παράδειγμα δείχνει πώς θα μπορούσατε να δημιουργήσετε έναν τελεστή ακονίσματος που χρησιμοποιεί το EDGE_NO_OP κανόνας (ΟΧΙ_ΟΠ περνά ως ConvolveOp παράμετρος στη γραμμή 008):

001 float [] sharpKernel = {002 0.0f, -1.0f, 0.0f, 003 -1.0f, 5.0f, -1.0f, 004 0.0f, -1.0f, 0.0f 005}; 006 BufferedImageOp sharpen = νέο ConvolveOp (007 νέο πυρήνα (3, 3, sharpKernel), 008 ConvolveOp.EDGE_NO_OP, null); 

Πίνακες αναζήτησης

Μια άλλη ευέλικτη λειτουργία εικόνας περιλαμβάνει τη χρήση ενός πίνακας αναζήτησης. Για αυτήν τη λειτουργία, τα χρώματα pixel πηγής μεταφράζονται σε χρώματα pixel προορισμού μέσω της χρήσης ενός πίνακα. Ένα χρώμα, θυμηθείτε, αποτελείται από κόκκινα, πράσινα και μπλε στοιχεία. Κάθε στοιχείο έχει τιμή από 0 έως 255. Τρεις πίνακες με 256 καταχωρήσεις επαρκούν για τη μετάφραση οποιουδήποτε χρώματος πηγής σε χρώμα προορισμού.

ο java.awt.image.LookupOp και java.awt.image.LookupTable Πίνακας τάξεις ενσωματώνουν αυτήν τη λειτουργία. Μπορείτε να ορίσετε ξεχωριστούς πίνακες για κάθε στοιχείο χρώματος ή να χρησιμοποιήσετε έναν πίνακα και για τα τρία. Ας δούμε ένα απλό παράδειγμα που αντιστρέφει τα χρώματα κάθε συστατικού. Το μόνο που πρέπει να κάνουμε είναι να δημιουργήσουμε έναν πίνακα που αντιπροσωπεύει τον πίνακα (γραμμές 001-003). Στη συνέχεια δημιουργούμε ένα Πίνακας αναζήτησης από τον πίνακα και ένα Αναζήτηση από το Πίνακας αναζήτησης (γραμμές 004-005).

001 short [] invert = νέο short [256]; 002 για (int i = 0; i <256; i ++) 003 invert [i] = (κοντό) (255 - i); 004 BufferedImageOp invertOp = νέο LookupOp (005 νέο ShortLookupTable (0, invert), null); 

Πίνακας αναζήτησης έχει δύο υποκατηγορίες, Πίνακας ByteLookup και ShortLookupTable Πίνακας, που ενθυλακώνονται ψηφιόλεξη και μικρός συστοιχίες. Εάν δημιουργήσετε ένα Πίνακας αναζήτησης που δεν έχει καταχώριση για καμία τιμή εισαγωγής, θα εξαχθεί μια εξαίρεση.

Αυτή η λειτουργία δημιουργεί ένα εφέ που μοιάζει με αρνητικό χρώμα σε συμβατική μεμβράνη. Σημειώστε επίσης ότι η εφαρμογή αυτής της λειτουργίας δύο φορές θα επαναφέρει την αρχική εικόνα. βασικά παίρνετε ένα αρνητικό από το αρνητικό.

Τι γίνεται αν θέλετε να επηρεάσετε μόνο ένα από τα χρωματικά στοιχεία; Ανετα. Κατασκευάζετε ένα Πίνακας αναζήτησης με ξεχωριστούς πίνακες για καθένα από τα κόκκινα, πράσινα και μπλε στοιχεία. Το παρακάτω παράδειγμα δείχνει πώς να δημιουργήσετε ένα Αναζήτηση που αντιστρέφει μόνο το μπλε στοιχείο του χρώματος. Όπως και με τον προηγούμενο τελεστή αναστροφής, η εφαρμογή αυτού του τελεστή επαναφέρει δύο φορές την αρχική εικόνα.

001 short [] invert = νέο short [256]; 002 κοντό [] ευθύ = νέο κοντό [256]; 003 για (int i = 0; i <256; i ++) {004 invert [i] = (short) (255 - i); 005 ευθεία [i] = (κοντή) i; 006} 007 κοντό [] [] blueInvert = νέο κοντό [] [] {ευθεία, ευθεία, αναστροφή}; 008 BufferedImageOp blueInvertOp = 009 νέο LookupOp (νέο ShortLookupTable (0, blueInvert), null); 

Αφίσα είναι ένα άλλο ωραίο αποτέλεσμα που μπορείτε να εφαρμόσετε χρησιμοποιώντας ένα Αναζήτηση. Η αφίσωση περιλαμβάνει τη μείωση του αριθμού των χρωμάτων που χρησιμοποιούνται για την εμφάνιση μιας εικόνας.

ΕΝΑ Αναζήτηση μπορεί να επιτύχει αυτό το αποτέλεσμα χρησιμοποιώντας έναν πίνακα που αντιστοιχίζει τις τιμές εισόδου σε ένα μικρό σύνολο τιμών εξόδου. Το παρακάτω παράδειγμα δείχνει πώς μπορούν να αντιστοιχιστούν οι τιμές εισόδου σε οκτώ συγκεκριμένες τιμές.

001 σύντομο [] posterize = νέο σύντομο [256]; 002 για (int i = 0; i <256; i ++) 003 postize [i] = (κοντό) (i - (i% 32)); 004 BufferedImageOp posterizeOp = 005 νέο LookupOp (νέο ShortLookupTable (0, posterize), null); 

Κατώφλι

Η τελευταία λειτουργία εικόνας που θα εξετάσουμε είναι κατώφλι. Το κατώφλι κάνει τις αλλαγές χρώματος σε ένα "όριο" ή ένα όριο που καθορίζεται από τον προγραμματιστή, πιο εμφανές (παρόμοιο με το πώς οι γραμμές περιγράμματος σε έναν χάρτη καθιστούν τα όρια υψομέτρου πιο εμφανή). Αυτή η τεχνική χρησιμοποιεί μια καθορισμένη τιμή κατωφλίου, την ελάχιστη τιμή και τη μέγιστη τιμή για τον έλεγχο των τιμών των στοιχείων χρώματος για κάθε εικονοστοιχείο μιας εικόνας. Οι τιμές χρώματος κάτω από το όριο αντιστοιχίζονται στην ελάχιστη τιμή. Οι τιμές πάνω από το όριο αντιστοιχίζονται στη μέγιστη τιμή.