Στο wiki αυτό θα μάθουμε να διαχειριζόμαστε και γενικότερα επεξεργαζόμαστε πολυκάναλες εικόνες με την γλώσσα προγραμματισμού C. Θα ξεκινήσουμε από απλές λειτουργίες όπως ανάγνωση εικόνας και αριθμητική δεικτών και θα προχωρήσουμε σε πιο σύνθετα θέματα
Μια εικόνα ανάλογα το πρότυπο στο οποίο έχει αποθηκευτεί, αποθηκεύει τα στοιχεία της με διαφορετικό τρόπο και περιέχει διαφορετικές πληροφορίες μέσα σε αυτό. Έτσι λοιπόν, η ανάγνωση μιας εικόνας στην γλώσσα C, η οποία είναι χαμηλού επιπέδου, δεν είναι πάρα πολύ εύκολη και εξαρτάται από το πρότυπο στο οποίο βρίσκεται η εικόνα. Σε αυτό το wiki δεν θα κάνουμε χρήση πρόσθετων βιβλιοθηκών μέσω των οποίων παρέχεται η υποστήριξη στα σύνθετα πρότυπα (πχ libtiff για το πρότυπο tif κ.α.).
Έτσι, προκειμένου η διαδικασία να γίνει πιο εύκολη, αρκεί να δημιουργηθεί ένα αρχείο που περιέχει μόνο τις τιμές των εικονοστοιχείων και όχι άλλες πληροφορίες που αφορούν το μέγεθος της εικόνας, τον τύπο της κ.τ.λ. Ένα τέτοιο πρότυπο είναι το πρότυπο BIL/BIP/BSQ (το οποίο έχουμε μάθει ως .ers) το οποίο μετατρέπει την εικόνα σε δύο αρχεία. Το πρώτο που έχει κατάληξη .ers περιέχει όλες τις πληροφορίες της εικόνας (γραμμές, στήλες, κανάλια, τύπος δεδομένων) και το δεύτερο την εικόνα με τις τιμές της σε όλα τα κανάλια. Έτσι αρκεί, να μετατρέψουμε σε πρώτη φάση την εικόνα μας σε .ers. Η μετατροπή μπορεί να γίνει με πολλούς τρόπους, αλλά ένας από τους πιο εύκολους είναι μέσω της βιβλιοθήκης gdal. Η gdal είναι μια βιβλιοθήκη ελεύθερου λογισμικού που χρησιμοποιείται για γεωχωρικά δεδομένα και υποστηρίζει όλα τα πρότυπα στα οποία μπορεί να έχει αποθηκευτεί μια εικόνα.
Έχοντας λοιπόν την εικόνα μας την μεταφορτώνουμε με την εντολή:
wget https://upload.wikimedia.org/wikipedia/en/2/24/Lenna.png
και μέσω της gdal την μετατρέπουμε σε ers πρότυπο
gdal_translate -of ERS Lenna.png Lenna.ers
Ένα σημαντικό κομμάτι στην επεξεργασία εικόνων είναι ο τρόπος με τον οποίο θα τις επεξεργαστούμε. Έτσι λοιπόν στο απλό πρότυπο που χρησιμοποιούμε, υπάρχουν τρείς διαφορετικές κύριες μέθοδοι αποθήκευσης της εικόνας οι BIL, BIP και BSQ. Στο πλαίσιο του συγκεκριμένου παραδείγματος χρησιμοποιείται η μορφή BIL στην οποία τα κανάλια διαπλέκονται ανά γραμμές (band interleaved by line). Ως πρώτο παράδειγμα θα ξεκινήσουμε με την ανάγνωση της εικόνας. Θα θεωρήσουμε ότι απαιτείται δυναμική καταχώρηση της μνήμης την ώρα της εκτέλεσης και όλη την εικόνα αντεγραμμένη από το αρχείο της στο δίσκο σε αυτήν. Τα παραδείγματά μας λαμβάνουν τα ορίσματά τους στην γραμμή εντολών κατά το σχήμα:
$./a.out Lenna 512 512 3
όπου a.out το όνομα του εκτελέσιμου μετά την μεταγλώττιση πχ με gcc ex1.c, Lenna το όνομα του αρχείου με την εικόνα (και όχι τα στοιχεία/πληροφορίες της που είναι στο ers), 512 οι γραμμές, 512 οι στήλες και 3 τα κανάλια της. Τα τελευταία μπορούμε να τα δούμε στο Lenna.ers (είναι αρχείο κειμένου).
Ξεκινάμε με την 1η έκδοση:
#include <stdio.h> /* FILE, fopen(), printf(), fread(), fclose() */ #include <stdlib.h> /* exit(), malloc(), atoi(), free() */ int main(int argc, char **argv){ FILE *fpin; int rows, cols, bands; unsigned char *image; register i, j, k; if (argc!=5){ printf("Usage: $rLenna InputImageFile Rows Columns Bands\n"); exit(1); } if ((fpin=fopen(argv[1],"rb"))==NULL){ printf("Error: Bad InputImageFile: <%s>\n",argv[1]); exit(1); } rows=atoi(argv[2]); cols=atoi(argv[3]); bands=atoi(argv[4]); if (rows<1){ printf("Error: Bad rows value: <%s>\n",argv[2]); exit(1); } if (cols<1){ printf("Error: Bad columns value: <%s>\n",argv[3]); exit(1); } if (bands<1){ printf("Error: Bad bands value: <%s>\n",argv[4]); exit(1); } if ((image=malloc(rows*cols*bands*sizeof(char)))==NULL){ printf("Error: Not enough memory available\n"); exit(1); } if (fread(image,sizeof(char),rows*cols*bands,fpin)!=rows*cols*bands){ printf("Error: Input file size does not match image dimensions\n"); exit(1); } if (fclose(fpin)==EOF){ printf("Error: Bad file closing\n"); exit(1); } for (i=10;i<12;i++){ printf("Line %3d: ",i+1); for (j=0;j<bands;j++){ printf("Band %3d: ",j+1); for (k=20;k<30;k++){ printf("Column %3d: %3d\n",k+1, (int)image [ ( (i*bands) + j ) * cols + k ] ); } } } free(image); return 0; }
Τα κύρια σημεία στο παραπάνω πρόγραμμα είναι:
→ Η main λαμβάνει τα ορίσματά της από την γραμμή εντολών στις argc και argv ( http://www.cprogramming.com/tutorial/c/lesson14.html ). Κάθε όρισμα αφού μετατραπεί και αντιγραφεί σε κατάλληλη μεταβλητή ελέγχεται για την ορθή τιμή του.
→ Χρησιμοποιούμε την δομή FILE και την συνάρτηση fopen για να ανοίξουμε το αρχείο για ανάγνωση ως δυαδικό (http://www.cprogramming.com/tutorial/cfileio.html)
→ Χρησιμοποιούμε την συνάρτηση malloc για να δεσμεύσουμε την απαραίτητη μνήμη. Η εικόνα μας έχει μέγεθος 512x512x3x1 bytes.
→ Διαβάζουμε την εικόνα μας με μία εντολή fread (http://www.cprogramming.com/tutorial/cfileio.html)
→ Εμφανίζουμε ένα απόσπασμα της εικόνας στην οθόνη ως ψηφιακές τιμές. Προσοχή στην απαραίτητη αριθμητική δεικτών (http://cslibrary.stanford.edu/104/)
Συνεχίζουμε με την 2η έκδοση:
#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv){ FILE *fpin; int rows, cols, bands; unsigned char ***image; register i, j, k; if (argc!=5){ printf("Usage: $rLenna InputImageFile Rows Columns Bands\n"); exit(1); } if ((fpin=fopen(argv[1],"rb"))==NULL){ printf("Error: Bad InputImageFile: <%s>\n",argv[1]); exit(1); } rows=atoi(argv[2]); cols=atoi(argv[3]); bands=atoi(argv[4]); if (rows<1){ printf("Error: Bad rows value: <%s>\n",argv[2]); exit(1); } if (cols<1){ printf("Error: Bad columns value: <%s>\n",argv[3]); exit(1); } if (bands<1){ printf("Error: Bad bands value: <%s>\n",argv[4]); exit(1); } if ((image=malloc(rows*sizeof(char**)))==NULL){ printf("Error: Not enough memory available A\n"); exit(1); } for (i=0;i<rows;i++){ if ((image[i]=malloc(bands*sizeof(char*)))==NULL){ printf("Error: Not enough memory available B\n"); exit(1); } for (j=0;j<bands;j++){ if ((image[i][j]=malloc(cols*sizeof(char)))==NULL){ printf("Error: Not enough memory available C\n"); exit(1); } } } for (i=0;i<rows;i++){ for (j=0;j<bands;j++){ if (fread(image[i][j],sizeof(char),cols,fpin)!=cols){ printf("Error: Input file size does not match image dimensions\n"); exit(1); } } } if (fclose(fpin)==EOF){ printf("Error: Bad file closing\n"); exit(1); } for (i=10;i<12;i++){ printf("Line %3d: ",i+1); for (j=0;j<bands;j++){ printf("Band %3d: ",j+1); for (k=20;k<30;k++){ printf("Column %3d: %3d\n",k+1, (int)image [ i ] [ j ] [ k ] ); } } } for (i=0;i<rows;i++) for (j=0;j<bands;j++) free(image[i][j]); free(image[i]); free(image); return 0; }
Εδώ οι αλλαγές με πριν είναι σημαντικές:
→ Η δήλωση της εικόνας είναι δείκτης σε δείκτη σε δείκτη σε χαρακτήρα.
→ Η κατάληψη μνήμης γίνεται σε 3 επίπεδα. Πρώτα 512 δείκτες σε γραμμές. Για κάθε έναν από αυτούς 3 δείκτες σε στήλες. Για κάθε έναν νέο δείκτη μνήμη 512 χαρακτήρων. ΠΡΟΣΟΧΗ: η μνήμη που έχει καταληφθεί συνολικά για την εικόνα ΔΕΝ είναι συνεχής και ενιαία. Κατά συνέπεια εδώ δεν μπορούμε να την γεμίσουμε με μία εντολή fread
→ Ο συνδυασμός των παραπάνω τεχνικών δήλωσης και κατάληψης μνήμης επιτρέπει την άμεση διευθυνσιοδότηση χωρίς αριθμητική δεικτών.
* Ερ.: Από την Lenna.png μετατρέψαμε την Lenna.ers όπου δημιούργησε δύο αρχεία, την Lenna και την Lenna.ers. Η Lenna.ers περιέχει τα δεδομένα της εικόνας και η Lenna τις τιμές της εικόνας. Επίσης χρησιμοποιήσαμε το GHex για την απεικόνισή τους ώστε να δούμε τις ψηφιακές τιμές. Γιατί δεν μπορούσαμε να χρησιμοποιήσουμε την Lenna.png εξαρχής, αφού ο χρήστης εισάγει τις γραμμές και τις στήλες της εικόνας έτσι και αλλιώς; * Απ.: Η τυποποίηση (format) png είναι σχετικά σύνθετη. Για να την χρησιμοποιήσουμε σε κώδικα θα πρέπει είτε να διαβάσουμε την περιγραφή της και να την προγραμματίσουμε είτε να καλέσουμε κάποια βιβλιοθήκη η οποία να το κάνει. Εδώ μετατρέψαμε με την gdal στο επιθυμητό απλό format bil. Περισότερα για την gdal: http://www.gdal.org και http://www.gdal.org/gdal_utilities.html . Περισσότερα για τις τυποποιήσεις και ειδικότερα για bil,bip,bsq http://users.ntua.gr/chiossif/Free_As_Freedom_Software/BIL_BIP_BSQ.pdf
* Ερ.: rLenna.c 7η γραμμή: γιατί unsigned char? * Απ.: Στο συνοδευτικό αρχείο ers βλέπουμε ότι τα δεδομένα της εικόνας είναι CellType = Unsigned8BitInteger. Ο τύπος αυτός στην C είναι ο unsigned char.
* Ερ.: rLenna.c 14η γραμμή: το r είναι για read, το b γιατί είναι? * Απ.: Στην αναφορά της fopen ( http://www.cplusplus.com/reference/cstdio/fopen/?kw=fopen ) αναφέρει ότι: “In order to open a file as a binary file, a “b” character has to be included in the mode string.”
* Eρ.: Γιατί binary? Δεν έχει 0 και 1 μόνο αλλά τιμές από 0, 255. * Απ.: Αυτή η τυποποίηση και η χρήση της θεωρείται δυαδική (binary). Αν επρόκειτο για αρχείο κειμένου (ASCII) δηλαδή για αρχείο με εμφανίσιμους όλους τους χαρακτήρες και αλλαγές γραμμής και στηλοθέτες (tabs) κ.τ.λ. τότε θα το ανοίγαμε χωρίς το b και θα το διαβάζαμε με getchar και την παρέα της gets scanf κ.α.
* Ερ.: rLenna.c 52η γραμμή: Τώρα κάνετε int τον δείκτη στην εικόνα?? Γιατί δεν τον δηλώσατε εξ αρχής int? * Απ.: Η κατάληψη μνήμης γίνεται με τον πραγματικό τύπο και άρα πρέπει να είναι δηλωμένο char. Η εμφάνιση μέσω της printf εφόσον γίνεται ως integer, θα πρέπει να αλλάξει τύπο και άρα έχουμε αλλαγή τύπου. Έτσι θα εμφανιστεί όπως δηλώνει και ο περιγραφέας πεδίου σαν ακέραιος αριθμός
* Ερ.: Γιατι δεν εμφανίζεται σαν char τότε?? * Απ.: Η εμφάνιση char περνάει μέσα από τον πίνακα ascii %c. Δεν θέλαμε κάτι τέτοιο, σωστά;
* Eρ.: Στην 2η έκδοση δεν κατάλαβα την χρήση του malloc. * Απ.: Στην 1η έκδοση έπιασα μία περιοχή μνήμης για όλη την εικόνα και περπατούσα μέσα της με πράξεις (βλ. image [ ( (i*bands) + j ) * cols + k ] ). Έτσι δεν χρειάζεται δείκτης σε δείκτη και άλλα τέτοια κόλπα. Στην 2η έκδοση επέλεξα έναν πίνακα δεικτών σε δείκτες καναλιών με πλήθος όσο οι γραμμές, ενώ οι πίνακες καναλιών δείχνουν σε δείκτες με στοιχεία όσα οι στήλες. Έτσι έχω διαδοχικά malloc (όχι κατ'ανάγκη σε συνεχόμενη μνήμη) και μέσω αναλυτικών δεικτών (βλ. image [ i ] [ j ] [ k ] ).
* Ερ.: Εσείς ποια από τις δύο εκδόσεις θα χρησιμοποιούσατε; * Απ.: Γενικά προτιμώ την 1η διότι έχει συνεχή μνήμη και μπορώ να την διαχειρίζομαι όπως θέλω. Για να αποφεύγω τα λάθη στην αριθμητική δεικτών φτιάχνω μια μακροεντολή όπως
IMAGE_PIXEL(x,y,z) (image [(((x)*bands)+(y))*cols+(z)])
και την χρησιμοποιώ έτσι:
IMAGE_PIXEL(i,j,k)
Παράδειγμα μακροεντολής:
#define IMAGE_PIXEL(x,y,z) (image [(((x)*bands)+(y))*cols+(z)])
και παράδειγμα συνάρτησης:
char image_pixel(int x, int y, int z){ return (image [(((x)*bands)+(y))*cols+(z)]); }
Προσοχή θέλουν εδώ τα ονόματα των εμπλεκομένων μεταβλητών καθώς και η ορατότητά τους. Την 2η έκδοση την χρησιμοποιώ όταν φτιάχνω ένα λογισμικό με πολλές αναφορές στην εικόνα και θέλω να είναι ευανάγνωστο. Δυστυχώς εδώ θέλει πολύ προσοχή η σωστή δήλωση των μεταβλητών και η κατάληψη της μνήμης. Τέλος, και για να δώσω την σωστή/αληθινή απάντηση, καμιά από τις δύο. Διότι σε αυτές η εικόνα πρέπει να έρθει όλη στην μνήμη και άρα η μνήμη του υπολογιστή είναι ένα όριο για το μέγεθος της εικόνας. Ακολουθεί καλύτερο παράδειγμα με άλγεβρα εικόνων χωρίς αυτό τον περιορισμό
Να τροποποιήσετε όποια από τις δύο εκδόσεις επιθυμείτε ώστε να:
* υπολογίζετε στατιστικά σε μία εικόνα
* αποκόπτετε σε νέο αρχείο ένα κομμάτι αυτής της εικόνας. Δώστε στην γραμμή εντολών την άνω αριστερή γωνία και το μέγεθος του αποσπάσματος σε πίξελ
* στρίβετε την εικόνα 90 ή 180 μοίρες αριστερά ή δεξιά.
Για να δούμε μια πραγματική εφαρμογή άλγεβρας εικόνας:
έχουμε μία πολυφασματική εικόνα με rows γραμμές, cols στήλες και bands κανάλια (εδώ την Lenna με 512 512 και 3 αντίστοιχα). Θέλουμε να φτιάξουμε μία μονοχρωματική/τόνων_του_γκρι (greyscale) εικόνα έστω Lenna_MONO με συντελεστές 0.33333 0.41667 0.25000 για κάθε κανάλι αντίστοιχα.
Δείτε και μελετήστε το πρόγραμμα που κάνει αυτή την δουλειά:
#include <stdio.h> #include <stdlib.h> int main(int argc, char **argv){ FILE *fpin, *fpout; int rows, cols, bands; unsigned char *image_in, *image_out; double *factors, tmpd; register i, j, k; if (argc<8){ printf("Usage: $Image_algebra InputImageFile Rows Columns Bands OutputImageFile factorsperband\n"); exit(1); } if ((fpin=fopen(argv[1],"rb"))==NULL){ printf("Error: Bad InputImageFile: <%s>\n",argv[1]); exit(1); } rows=atoi(argv[2]); cols=atoi(argv[3]); bands=atoi(argv[4]); if (rows<1){ printf("Error: Bad rows value: <%s>\n",argv[2]); exit(1); } if (cols<1){ printf("Error: Bad columns value: <%s>\n",argv[3]); exit(1); } if (bands<1){ printf("Error: Bad bands value: <%s>\n",argv[4]); exit(1); } if ((argc-6)!=bands){ printf("Error: Wrong number of factors per band: <%d>\n",argc-6); exit(1); } if ((fpout=fopen(argv[5],"wb"))==NULL){ printf("Error: Bad OutputImageFile: <%s>\n",argv[5]); exit(1); } if ((factors=malloc(bands*sizeof(double)))==NULL){ printf("Error: Not enough memory available for factors vector\n"); exit(1); } for (i=0;i<bands;i++){ factors[i]=atof(argv[i+6]); } if ((image_in=malloc(cols*bands*sizeof(char)))==NULL){ printf("Error: Not enough memory available\n"); exit(1); } if ((image_out=malloc(cols*sizeof(char)))==NULL){ printf("Error: Not enough memory available\n"); exit(1); } for(i=0;i<rows;i++){ if (fread(image_in,sizeof(char),cols*bands,fpin)!=cols*bands){ printf("Error: Input file size does not match image dimensions\n"); exit(1); } for (k=0;k<cols;k++){ tmpd=0.0; for (j=0;j<bands;j++) tmpd+=factors[j]*image_in[j*cols+k]; if (tmpd<0.5) image_out[k]=0; else if (tmpd>254.4) image_out[k]=255; else image_out[k]=(unsigned char)tmpd; } if (fwrite(image_out,sizeof(char),cols,fpout)!=cols){ printf("Error: Disk is full\n"); exit(1); } } if (fclose(fpin)==EOF){ printf("Error: Bad input file closing\n"); exit(1); } if (fclose(fpout)==EOF){ printf("Error: Bad output file closing\n"); exit(1); } free(image_in); free(image_out); free(factors); return 0; }
Για να δούμε την εικόνα φτιάχνουμε το αρχείο Lenna_MONO.ers με περιεχόμενα:
DatasetHeader Begin Version = "6.0" Name = "Lenna_MONO.ers" DataSetType = ERStorage DataType = Raster ByteOrder = LSBFirst RasterInfo Begin CellType = Unsigned8BitInteger NrOfLines = 512 NrOfCellsPerLine = 512 NrOfBands = 1 RasterInfo End DatasetHeader End
και με τις εντολές:
gcc Image_algebra.c ./a.out Lenna 512 512 3 Lenna_MONO 0.33333 0.41667 0.25000 gdal_translate -of PNG Lenna_MONO.ers Lenna_MONO.png
μεταγλωττίζουμε τον πηγαίο κώδικα, εκτελούμε το πρόγραμμα και μετατρέπουμε την νέα εικόνα σε πρότυπο png ώστε να την δούμε σε ένα λογισμικό απεικόνισης εικόνων.
Την είδατε; Μπράβο
Πάμε τώρα στο πρόγραμμα να δούμε τι διαφορές και προσθήκες έχουμε από τα προηγούμενα.
→ Εδώ εφαρμόσαμε μια νέα τεχνική διαχείρισης εικόνας. Φορτώνουμε στην μνήμη μόνο το πλήθος των γραμμών που χρειαζόμαστε (δηλαδή μία) και γράφουμε το αποτέλεσμα ανάλογα. Με αυτό τον τρόπο όριο στο μέγεθος των εικόνων τίθεται μόνο κατά στήλες και κανάλια και όχι κατά γραμμές. Δηλαδή αν το πλήθος γραμμών μια εικόνας χωράει στον δίσκο μας και το γινόμενο στήλες επί κανάλια στην μνήμη εμείς μπορούμε να την διαχειριστούμε.
→ Οι απαιτούμενες πράξεις γίνονται με πραγματικούς αριθμούς διπλής ακρίβειας. Υπερβολή; Ναι, αλλά τι μας νοιάζει; με το χέρι τις κάνουμε;
→ Προσέξτε πως ελέγχουμε την απόδοση τιμής στο αποτέλεσμα. Μην ξεχνάμε πως προδιαγραφή (και άρα περιορισμός) είναι η εικόνα αποτέλεσμα να είναι 8bit όπως και η εικόνα εισόδου. Αυτό το λογισμικό αντιμετωπίζει μόνο εικόνες 8bit και έτσι πρέπει να έχει τιμές στο εύρος [0,255] (unsigned char).
* Ερ.: Έχουμε δύο εικόνες με ένα κανάλι και ίδιες διαστάσεις από το ίδιο αντικείμενο/περιοχή και θέλουμε να δούμε οπτικά την ταύτισή τους με την μορφή σκακιέρας: στα άσπρα τετράγωνα η μία στα μαύρα η άλλη. Δεδομένα μας είναι τα ονόματα των αρχείων των εικόνων σε ERS φορμάτ άρα και οι γραμμές και οι στήλες τους, το όνομα της εικόνας σκακιέρας και φυσικά το μέγεθος του τετραγώνου της σκακιέρας. Υποδ. Ξεκινήστε αυτό το πρόγραμμα με χρήση του παραπάνω κώδικα. Στην απάντηση τεκμηριώστε τις κύριες αλλαγές ενώ μέσα στον κώδικα προσθέστε σχόλια
* Απ.: Χρησιμοποιώντας τον παραπάνω κώδικα έγιναν δύο βασικές αλλαγές. Η πρώτη αφορά στα δεδομένα τα οποία χρειάζονται και πρέπει να διαβαστούν και η δεύτερη στη συνθήκη η οποία θα υλοποιήσει το επιθυμητό αποτέλεσμα. Συγκεκριμένα, σε αυτήν την περίπτωση έχουμε εικόνες με ένα κανάλι και όχι rgb, οπότε απλοποιείται η διαδικασία του διαβάσματος. Επίσης, έπρεπε να προστεθεί μια μεταβλητή για το μέγεθος του τετραγώνου της σκακιέρας η ssize.
Το πρόγραμμα διαβάζει τις εικόνες σε τυποποίηση ERS. Για τη μετατροπή αυτή έχει χρησιμοποιηθεί η ακόλουθη εντολή από τερματικό :
gdal_translate -of ers -ot byte -strict -scale -b 1 input.tif input.ers
σε σύστημα με εγκατεστημένη την βιβλιοθήκη gdal.
Την υλοποίηση θα την βρείτε στον σύνδεσμο αυτό και είναι ανάλογη με το παράδειγμα παραπάνω. Η κύρια λειτουργία εκτελείται στην γραμμή:
image_out[k]=((i/ssize-k/ssize)%2)?image_in1[k]:image_in2[k];
όπου με τον τελεστή ()?: αποφασίζεται και αποθηκεύεται στην εικόνα αποτέλεσμα image_out στην θέση k (πρόκειται για ενδιάμεση μνήμη αποθήκευσης μιας γραμμής) είτε η image_in1 είτε η image_in2. Η απόφαση λαμβάνεται από το λογικό (i/ssize-k/ssize)%2 όπου i η τρέχουσα γραμμή η οποία περιέχεται στις ενδιάμεσες μνήμες των εικόνων, k η τρέχουσα στήλη (ως στοιχείο των ενδιάμεσων μνημών) και ssize το μέγεθος του τετράγωνου της σκακιέρας όπως προαναφέρθηκε.
Με βάση το παραπάνω πρόγραμμα φτιάξτε παραλλαγές οι οποίες:
* να υπολογίζουν λόγο δύο καναλιών. Αντί για συντελεστές, δώστε τους αριθμούς των καναλιών που θα συμμετέχουν στον λόγο πχ 2 1 για τον (2 / 1). Προσέξτε το αποτέλεσμα να είναι και πάλι στον ίδιο τύπο δεδομένων όπως η αρχική εικόνα.
* να υπολογίζουν τον κανονικοποιημένο δείκτη. Εδώ δώστε τους αριθμούς των καναλιών που θα συμμετέχουν στον δείκτη πχ 4 3 για τον NDVI ( (4-3)/(4+3) ). Προσέξτε το αποτέλεσμα να είναι και πάλι στον ίδιο τύπο δεδομένων όπως η αρχική.
* να υπολογίζουν οτιδήποτε από τα παραπάνω σε εικόνες 16bit. Φροντίστε και πάλι για την σωστή καταχώρηση των αποτελεσμάτων στη νέα εικόνα.
Σε κάθε περίπτωση φτιάξτε αρχεία ers (τροποποιώντας τα αρχικά) και με την gdal μετατρέψτε την εικόνα σας σε ένα προσφιλές για το σύστημά σας, πρότυπο για απεικόνιση.
Στην προηγούμενη εφαρμογή φτιάξαμε μία μονοκάναλη εικόνα με εφαρμογή πράξεων σε πολυκάναλη (άλγεβρα εικόνων). Μια και την έχουμε λοιπόν ας κάνουμε ένα απλό φίλτρο με την τεχνική της συνέληξης.
#include <stdio.h> /* FILE, printf, fopen, fscanf, fclose, fread, fwrite */ #include <stdlib.h> /* exit, malloc, free, atoi */ #include <string.h> /* memset, memcpy */ int main(int argc, char **argv){ FILE *fpin, *fpout; int rows, cols, frows, fcols, **filter; int frows_2, fcols_2, tmpi; unsigned char *image_in, *image_out; double fdiv, tmpd; register int i, j, k, l; if (argc!=6){ printf("Usage: $Filter InputImageFile Rows Columns OutputImageFile FilterFile\n"); exit(1); } if ((fpin=fopen(argv[5],"r"))==NULL){ printf("Error: Bad FilterFile: <%s>\n",argv[5]); exit(1); } if ((fscanf(fpin,"%d %d",&frows,&fcols))!=2){ printf("Error: Bad format (rows, columns) in filter file: <%s>\n",argv[5]); exit(1); } if ((filter=malloc(frows*sizeof(int*)))==NULL){ printf("Error: Not enough memory available A\n"); exit(1); } for (i=0;i<frows;i++){ if ((filter[i]=malloc(fcols*sizeof(int)))==NULL){ printf("Error: Not enough memory available B\n"); exit(1); } for (j=0;j<fcols;j++){ if ((fscanf(fpin,"%d",&tmpi))!=1){ printf("Error: Bad format (filter) in filter file: <%s>\n",argv[5]); exit(1); } filter[i][j]=tmpi; } } if ((fscanf(fpin,"%lf",&fdiv))!=1){ printf("Error: Bad format (divisor) in filter file: <%s>\n",argv[5]); exit(1); } if (fclose(fpin)==EOF){ printf("Error: Bad filter file closing\n"); exit(1); } frows_2=frows/2; fcols_2=fcols/2; if ((fpin=fopen(argv[1],"rb"))==NULL){ printf("Error: Bad InputImageFile: <%s>\n",argv[1]); exit(1); } rows=atoi(argv[2]); cols=atoi(argv[3]); if (rows<1){ printf("Error: Bad rows value: <%s>\n",argv[2]); exit(1); } if (cols<1){ printf("Error: Bad columns value: <%s>\n",argv[3]); exit(1); } if ((fpout=fopen(argv[4],"wb"))==NULL){ printf("Error: Bad OutputImageFile: <%s>\n",argv[4]); exit(1); } if ((image_in=malloc(frows*cols*sizeof(char)))==NULL){ printf("Error: Not enough memory available C\n"); exit(1); } if ((image_out=malloc(cols*sizeof(char)))==NULL){ printf("Error: Not enough memory available D\n"); exit(1); } if (fread(image_in,sizeof(char),(frows-1)*cols,fpin)!=(frows-1)*cols){ printf("Error: Input file size does not match image dimensions\n"); exit(1); } memset(image_out, 0, cols); for (i=0;i<frows_2;i++){ if (fwrite(image_out,sizeof(char),cols,fpout)!=cols){ printf("Error: Disk is full\n"); exit(1); } } for(i=frows_2;i<rows-frows_2;i++){ if (fread(&image_in[(frows-1)*cols],sizeof(char),cols,fpin)!=cols){ printf("Error: Input file size does not match image dimensions\n"); exit(1); } for (j=fcols_2;j<cols-fcols_2;j++){ tmpd=0.0; for (k=0;k<frows;k++){ for (l=0;l<fcols;l++){ tmpd+=(double)filter[k][l]*image_in[k*cols+ (j+l-fcols_2)]; } } tmpd/=fdiv; if (tmpd<0.5) image_out[j]=0; else if (tmpd>254.4) image_out[j]=255; else image_out[j]=(unsigned char)tmpd; } for (j=0;j<fcols_2;j++){ image_out[j]=0; image_out[cols-j-1]=0; } if (fwrite(image_out,sizeof(char),cols,fpout)!=cols){ printf("Error: Disk is full\n"); exit(1); } memcpy(image_in,&image_in[cols],(frows-1)*cols); } memset(image_out, 0, cols); for (i=0;i<frows_2;i++){ if (fwrite(image_out,sizeof(char),cols,fpout)!=cols){ printf("Error: Disk is full\n"); exit(1); } } if (fclose(fpin)==EOF){ printf("Error: Bad input file closing\n"); exit(1); } if (fclose(fpout)==EOF){ printf("Error: Bad output file closing\n"); exit(1); } free(image_in); free(image_out); for (i=0;i<frows;i++) free(filter[i]); free(filter); return 0; }
Μόλις είδαμε μια εφαρμογή για φιλτράρισμα εικόνας με κυλιόμενη ενδιάμεση μνήμη προσωρινής αποθήκευσης. Ας το δούμε πιο προσεκτικά (αν και όχι τόσο αναλυτικά - μελετήστε…):
→ Χρησιμοποίησα πίνακα δεικτών για να αποθηκεύσω το φίλτρο στην μνήμη αλλά για την εικόνα εισόδου και εξόδου έναν πίνακα για κάθε μία και αριθμητική δεικτών για την προσπέλαση.
→ Δεν διαβάζω όλη την εικόνα στην μνήμη αλλά αντίθετα κρατάω μόνο το πλήθος των γραμμών που είναι απαραίτητες για να εκτελεστεί το φίλτρο. Έτσι υπολογίζω μία γραμμή στην εικόνα εξόδου κάθε φορά και αυτήν αποθηκεύω γραμμή γραμμή. Με αυτό τον τρόπο πετυχαίνω οικονομία μνήμης και παράλληλα εξασφαλίζω ότι και υπολογιστές με μικρή σχετικά μνήμη θα είναι ικανοί να εκτελέσουν αυτήν την επεξεργασία με πολύ μεγάλες εικόνες (αυτό το πρόγραμμα έχει δοκιμαστεί σε εικόνα 4096×4096 pixels ενώ δεν δημιουργείται θέμα και με πολλαπλάσια μεγέθη).
→ Προσέξτε τον τρόπο με τον οποίο «κυλάω» τις παλαιότερες γραμμές στην μνήμη προς τα «πάνω» και διαβάζω μία νέα γραμμή στην θέση της τελευταίας. Έτσι και ενώ σαρώνω γραμμή γραμμή το αρχείο εισόδου ελαχιστοποιώ την επαφή με τον δίσκο για μέγιστη ταχύτητα.
Ομολογουμένως έχει μια πολυπλοκότητα αλλά είναι τόσα τα πλεονεκτήματα χρήσης που πραγματικά αξίζει τον κόπο. Τέτοιες τεχνικές, οι οποίες εξασφαλίζουν προγράμματα ταχύτατα και με ελάχιστες απαιτήσεις στην μνήμη, προγραμματίζονται σήμερα σε όλο και πιο πολύπλοκες εφαρμογές και με όλο και πιο αυξημένες απαιτήσεις επεξεργαστικής ισχύος. Όσοι προγραμματίζουν ανάλογες εφαρμογές σε γλώσσα προγραμματισμού C, θα πρέπει να έχουν αυτές τις τεχνικές στην προγραμματιστική τους ατζέντα
Το αρχείο φίλτρου με το οποίο δοκιμάσαμε τον παραπάνω κώδικα έχει σαν περιεχόμενα:
3 3 0 -1 0 -1 5 -1 0 -1 0 1.0
όπου 3 3 είναι οι διαστάσεις του φίλτρου, ακολουθεί ο πίνακας συνέλιξης και τέλος ο διαιρέτης του φίλτρου.
Η εκτέλεση έγινε ανάλογα με την προηγούμενη εφαρμογή της άλγεβρας εικόνας με τις ακόλουθες εντολές:
gcc Filter.c ./a.out Lenna_MONO 512 512 Lenna_HIGH filter.txt gdal_translate -of PNG Lenna_HIGH.ers Lenna_HIGH.png
Μελετήστε τον κώδικα και αναμένουμε τις ερωτήσεις σας
Με βάση την εφαρμογή φιλτραρίσματος εικόνας σας προτείνουμε να γράψετε / μετατρέψετε το παραπάνω ώστε να:
* εκτελεί φίλτρα ανίχνευσης ακμών σαν το Sobel, Prewitt και Kirsch
* λειτουργεί με εικόνες 16bit
* λειτουργεί για ένα ή περισσότερα κανάλια σε πολυφασματική εικόνα
Θυμίζουμε ξανά την χρήση του προτύπου ers και της gdal για την μετατροπή των εικόνων.
Μαθαίνουμε επεξεργασία εικόνας σε C και είδαμε μέχρι εδώ πως να δουλεύουμε με τεράστιες εικόνες διαβάζοντας μόνο ότι είναι απαραίτητο σε κάθε ανακύκλωση. Αν όμως η εικόνα χωράει στην μνήμη; Πως μπορούμε να κάνουμε επεξεργασίες με την μεγαλύτερη δυνατή ταχύτητα; Την απάντηση εδώ μας την δίνει ο παράλληλος προγραμματισμός.
Οι σύγχρονοι επεξεργαστές περιέχουν πολλούς πυρήνες και έτσι έχουν την δυνατότητα να εκτελέσουν πολλές διεργασίες ταυτόχρονα αρκεί η μία να μην εξαρτάται από την άλλη. Στην περίπτωση των εικόνων αυτό είναι εφικτό με την τμηματική εκτέλεση μιας εικόνας. Αν για παράδειγμα έχουμε τετραπύρηνο επεξεργαστή μπορούμε να κόψουμε μια εικόνα στα τέσσερα (σταυρό) και να δώσουμε κάθε κομμάτι σε κάθε πυρήνα. Φυσικά ο προγραμματισμός σε αυτή την περίπτωση θα ήταν περίπλοκος.
Σήμερα χάρη στην OpenMP μπορούμε με ελάχιστες αλλαγές να προγραμματίσουμε παράλληλα. Δείτε το ακόλουθο πρόγραμμα το οποίο εφαρμόζει το φίλτρο Kirsch σε μία μονοχρωμματική εικόνα:
#include <stdio.h> /* FILE, EOF, printf(), fread(), fwrite(), fclose() */ #include <stdlib.h> /* exit(), malloc(), atoi(), free() */ #include <string.h> /* memcpy(), atoi() */ #include <omp.h> /* #pragma omp parallel */ int main(int argc, char **argv){ FILE *fp; int rows, cols, rows_1, cols_1, kirsch, a[15], t; unsigned char *image_in, *image_out; register i, j, k; if (argc!=5){ printf("Usage: $Kirsch InputImageFile Rows Columns OutputImageFile\n"); exit(1); } if ((fp=fopen(argv[1],"rb"))==NULL){ printf("Error: Bad InputImageFile: <%s>\n",argv[1]); exit(1); } rows=atoi(argv[2]); cols=atoi(argv[3]); if (rows<1){ printf("Error: Bad rows value: <%s>\n",argv[2]); exit(1); } if (cols<1){ printf("Error: Bad columns value: <%s>\n",argv[3]); exit(1); } if ((image_in=malloc(rows*cols*sizeof(char)))==NULL){ printf("Error: Not enough memory available\n"); exit(1); } if ((image_out=malloc(rows*cols*sizeof(char)))==NULL){ printf("Error: Not enough memory available\n"); exit(1); } if ((image_in=malloc(rows*cols*sizeof(char)))==NULL){ printf("Error: Not enough memory available\n"); exit(1); } if (fread(image_in,sizeof(char),rows*cols,fp)!=rows*cols){ printf("Error: Input file size does not match image dimensions\n"); exit(1); } if (fclose(fp)==EOF){ printf("Error: Bad file closing\n"); exit(1); } rows_1=rows-1; cols_1=cols-1; { // START parallel #pragma omp parallel for private(i, j, k, a, t, kirsch) for (i=1;i<rows_1;i++){ for (j=1;j<cols_1;j++){ a[0]=image_in[ (i-1) * cols + j-1 ]; a[1]=image_in[ (i-1) * cols + j ]; a[2]=image_in[ (i-1) * cols + j+1 ]; a[3]=image_in[ i * cols + j+1 ]; a[4]=image_in[ (i+1) * cols + j+1 ]; a[5]=image_in[ (i+1) * cols + j ]; a[6]=image_in[ (i+1) * cols + j-1 ]; a[7]=image_in[ i * cols + j-1 ]; a[8] =a[0]; a[9] =a[1]; a[10]=a[2]; a[11]=a[3]; a[12]=a[4]; a[13]=a[5]; a[14]=a[6]; kirsch=0; for (k=0;k<8;k++){ t=(5.0*(a[k]+a[k+1]+a[k+2])-3.0*(a[k+3]+a[k+4]+a[k+5]+a[k+6]+a[k+7]))*256.0/3826.0; /* 255*3*5=3825 */ if (t>kirsch) kirsch=t; } image_out [ i * cols + j ] = kirsch; } image_out[i*cols+0]=image_out[i*cols+1]; image_out[i*cols+cols_1]=image_out[i*cols+cols-2]; } } // END parallel memcpy(&image_out[0], &image_out[cols], cols); memcpy(&image_out[rows_1*cols], &image_out[(rows-2)*cols], cols); if ((fp=fopen(argv[4],"wb"))==NULL){ printf("Error: Bad OutputImageFile: <%s>\n",argv[4]); exit(1); } if (fwrite(image_out,sizeof(char),rows*cols,fp)!=rows*cols){ printf("Error: Bad write to OutputImageFile\n"); exit(1); } if (fclose(fp)==EOF){ printf("Error: Bad file closing\n"); exit(1); } free(image_in); free(image_out); return 0; }
Για το φίλτρο Kirsch δεν θα γράψουμε τίποτε. Μελετήστε τον ορισμό του και την υλοποίησή του Αλλά ούτε και για την παραλληλοποίηση της εκτέλεσης του φίλτρου θα γράψουμε κάτι. Απλά δείτε: τι κάνει το πρόγραμμά μας παράλληλο; Οι ακόλουθες γραμμές:
... #include <omp.h> /* #pragma omp parallel */ ... { // START parallel #pragma omp parallel for private(i, j, k, a, t, kirsch) ... } // END parallel ...
και φυσικά μεταγλώττιση με την παράμετρο -fopenmp στην gcc Δοκιμάστε το στην Lenna_Grey.ers με την εντολή:
./a.out Lenna_Grey 512 512 Lenna_Kirsch
Στιγμιαίο έτσι; Όπως θα ήταν και χωρίς τις παραπάνω παράλληλες ρυθμίσεις. Ναι αλλά η Lenna_Grey είναι μικρή. Για δοκιμάστε με μία εικόνα 5120×5120 ή μεγαλύτερη και τα συζητάμε
Περισσότερα για παράλληλο προγραμματισμό θα βρείτε στον ιστοχώρο της OpenMP και γενικότερα εδώ. Πάντως εδώ δεν θα συνεχίσουμε άλλο…
Ελπίζω η σπίθα να άναψε
Καλή παραλληλοποίηση
Μετά από τόσο κώδικα ο επίλογος είναι περιττός. Απλά θα πούμε ότι οι εφαρμογές δεν τελειώνουν και είστε όλοι ευπρόσδεκτοι να προσθέσετε κώδικα και σχόλια. Δοκιμάστε με γεωμετρικές διορθώσεις και αναδόμηση, σύνθετες λειτουργίες επεξεργασίας εικόνας/σήματος (FFT κ.α.), κατάτμηση, ταξινόμηση εικόνων και τόσες και τόσες άλλες.
Απλά ακολουθήστε τις ίδιες προδιαγραφές:
* Κώδικα σε ANSI C χωρίς επικλήσεις βιβλιοθηκών ή πρόσθετων εσωτερικά.
* Πρότυπο ers (BIL) και χρήση της gdal για μετατροπές μεταξύ.
* Πάντα λέφτερα
Και μην ξεχνάμε:
αφού «ο καλύτερος τρόπος να πούμε “ευχαριστώ” είναι μεταδίδοντας τις γνώσεις μας σε όλους» σας περιμένουμε να μπείτε και να μας πείτε τα ευχαριστώ σας