File Coverage

blib/lib/PDL/IO/ENVI.pm
Criterion Covered Total %
statement 66 121 54.5
branch 25 74 33.7
condition 4 6 66.6
subroutine 7 8 87.5
pod 2 2 100.0
total 104 211 49.2


line stmt bran cond sub pod time code
1             package PDL::IO::ENVI;
2              
3 1     1   85632 use strict;
  1         2  
  1         32  
4 1     1   3 use warnings;
  1         6  
  1         51  
5 1     1   519 use PDL::LiteF;
  1         927  
  1         6  
6 1     1   98034 use PDL::IO::FlexRaw;
  1         81105  
  1         86  
7 1     1   8 use PDL::Exporter;
  1         2  
  1         4  
8 1     1   20 use Config;
  1         2  
  1         1653  
9              
10             our @ISA = qw( PDL::Exporter );
11             our @EXPORT_OK = qw( readenvi readenvi_hdr );
12             our @EXPORT = @EXPORT_OK;
13             our %EXPORT_TAGS = ( Func=>[@EXPORT_OK] );
14              
15             our $VERSION = "2.098";
16             $VERSION = eval $VERSION;
17              
18             our $verbose = 0; # for diagnostics
19              
20             =head1 NAME
21              
22             PDL::IO::ENVI - read ENVI data files into PDL
23              
24             =head1 SYNOPSIS
25              
26             use PDL::IO::ENVI;
27             $pdl = readenvi("file.dat"); # implies there's a file.hdr next to it
28              
29             $hdr = readenvi_hdr("file.hdr"); # available separately, used for testing
30              
31             =head1 DESCRIPTION
32              
33             Allows you to read ENVI data into an ndarray.
34              
35             =head1 FUNCTIONS
36              
37             =head2 readenvi_hdr
38              
39             =for ref
40              
41             Given the name of an ENVI file header, parses the header and returns
42             a hash-ref.
43              
44             TODO
45             (1) verify that all required fields are present
46             (2) parse map_info for pixel geolocation
47             - handle keyword=value inside list
48             (3) check that all sensor keywords are parsed
49             (4) add support for offset/stride/count/reshape
50             (5) implement writeenvi/wenvi routine
51             (6) LATER: add support for complex data input, e.g. [2,S,L,B]
52             (7) LATER: support unsigned long long
53              
54             =cut
55              
56             # This is a hash ref of the known/allowed keywords
57             # in an ENVI header file. While these are the current
58             # values, this implementation allows for new keywords
59             # by parsing according to the following rules:
60             #
61             # (1) keywords are between the start of line and the =
62             # (2) keywords are case insensitive
63             # (3) white space is significant but amount and type is not
64             # (4) string values will have leading and trailing whitespace removed
65             # (5) canonical whitespace is a single ASCII space char
66             # (6) single spaces in hash keywords will be replace by underscore
67             # (7) canonical case for normalized keywords is lowercase
68             # (8) required key-value pairs are always on a single line
69             # (9) brace starting lists must be on same line as keyword =
70             # (10) comment lines begin with ; in the first column
71             #
72             # Initially, we will parse all keyword = values but only fully
73             # process for the required and optional entries needed for the
74             # scissor data files. A hash value of 1 indicates required..
75             #
76             my $envi_keywords = {
77             'band_names' => 0, # optional, CSV str of band names
78             'bands' => 1, # required, num of bands in image file
79             'bbl' => 0, # optional, (tbd)
80             'byte_order' => 1, # required, num 0 or 1 for LSF or MSF order
81             'class_lookup' => 0, # optional, (tbd)
82             'class_names' => 0, # optional, (tbd)
83             'classes' => 0, # optional, num of classes, including unclassified
84             'complex_function' => 0, # optional, (tbd)
85             'coordinate_system string' => 0, # optional, (tbd, for georeferencing)
86             'data_gain_values' => 0, # optional, CSV of gain vals for each band
87             'data_ignore_value' => 0, # optional, value of bad/missing element in data
88             'data_offset_values' => 0, # optional, CSV of offset vals for each band
89             'data_type' => 1, # required, id number in 1-6,9,12-15
90             'default_bands' => 0, # optional, CSV of 1 or 3 band numbers to display
91             'default_stretch' => 0, # optional, str of stretch to use for image display
92             'dem_band' => 0, # optional, (tbd)
93             'dem_file' => 0, # optional, (tbd)
94             'description' => 0, # optional, str describing the image or processing
95             'file_type' => 1, # required, ENVI Standard or from filetype.txt
96             'fwhm' => 0, # optional, CSV of band widths in wavelength units
97             'geo_points' => 0, # optional, CSV of x,y,lat,long of 1-4 image pts
98             'header_offset' => 1, # required, num bytes imbedded hdr in image file
99             'interleave' => 1, # required, str/num of BSQ/0, BIL/1, or BIP/2
100             'lines' => 1, # required, num lines in image
101             'map_info' => 0, # optional, CSV of values, as in
102             # UTM, x0, y0, east0, north0, xpixsize, ypixsize,
103             # UTM zone #, N or S (UTM only), datum,
104             # units=str, rotation=val
105             'pixel_size' => 0, # optional, CSV of x and y pixel size in meters
106             'major_frame_offsets' => 0, # optional, (tbd)
107             'minor_frame_offsets' => 0, # optional, (tbd)
108             'projection_info' => 0, # optional, (tbd)
109             'reflectance_scale_factor' => 0, # optional, (tbd)
110             'rpc_info' => 0, # optional, (tbd)
111             'samples' => 1, # required, num samples per image line each band
112             'sensor_type' => 0, # optional, str Unknown or exact match in sensor.txt
113             'spectra_names' => 0, # optional, (tbd)
114             'wavelength' => 0, # optional, CSV of band center value in image
115             'wavelength_units' => 0, # optional, str with units for wavelength and fwhm
116             'x_start' => 0, # optional, (tbd)
117             'y_start' => 0, # optional, (tbd)
118             'z_plot_average' => 0, # optional, (tbd)
119             'z_plot_range' => 0, # optional, (tbd)
120             'z_plot_titles' => 0, # optional, (tbd)
121             };
122              
123             my $envi_required_keywords = [];
124             foreach (sort keys %$envi_keywords) {
125             push @$envi_required_keywords, $_ if $envi_keywords->{$_};
126             }
127              
128             my $interleave = {
129             'bsq' => [ qw( samples lines bands ) ],
130             'bil' => [ qw( samples bands lines ) ],
131             'bip' => [ qw( bands samples lines ) ],
132             };
133              
134             my $envi_data_types = [];
135             $envi_data_types->[1] = 'byte';
136             $envi_data_types->[2] = 'short';
137             $envi_data_types->[3] = 'long';
138             $envi_data_types->[4] = 'float';
139             $envi_data_types->[5] = 'double';
140             $envi_data_types->[6] = undef; # complex, not supported, [2,shape]
141             $envi_data_types->[9] = undef; # double complex, not supported, [2,shape]
142             $envi_data_types->[12] = 'ushort';
143             $envi_data_types->[13] = 'ulong';
144             $envi_data_types->[14] = 'longlong';
145             $envi_data_types->[15] = undef; # unsigned long64, not supported, longlong?
146              
147             # Takes one arg, an ENVI hdr filename and
148             # returns a hash reference of the header data
149             #
150             sub readenvi_hdr {
151 1     1 1 214348 my $hdrname = $_[0];
152 1         4 my $hdr = {};
153              
154             # an easy progress message
155 1 50       7 if ($verbose>1) {
156 0         0 print STDERR "readenvi_hdr: reading ENVI hdr data from '@_'\n";
157 0         0 print STDERR "readenvi_hdr: required ENVI keywords are:\n";
158 0         0 print STDERR " @{ [sort @$envi_required_keywords] }\n";
  0         0  
159             }
160              
161             # open hdr file
162 1 50       101 open my $hdrfile, '<', $hdrname
163             or barf "readenvi_hdr: couldn't open '$hdrname' for reading: $!";
164 1         13 binmode $hdrfile;
165              
166 1 50       78 if ( eof($hdrfile) ) {
167 0         0 barf "readenvi_hdr: WARNING '$hdrname' is empty, invalid ENVI format"
168             }
169              
170             ITEM:
171 1         6 while (!eof($hdrfile)) {
172             # check for ENVI hdr start word on first line
173 1         6 my $line = <$hdrfile>;
174 1 50       10 if ($line !~ /^ENVI\r?$/) {
175 0         0 barf "readenvi_hdr: '$hdrname' is not in ENVI hdr format"
176             }
177 1         31 $hdr->{ENVI} = 1; # this marks this header as ENVI
178              
179             # collect key=values into a hash
180 1         4 my ($keyword,$val);
181 1         2 my $in_list = 0; # used to track when we re reading a { } list
182             LINE:
183 1         7 while (defined($line = <$hdrfile>)) {
184              
185 18 50       43 next LINE if $line =~ /^;/; # skip comment line (maybe print?)
186              
187 18         108 $line =~ s/\s+$//;
188 18         41 $line =~ s/^\s+//;
189 18 50       46 next LINE if $line =~ /^$/;
190              
191 18         29 chomp $line;
192              
193 18 100       40 if ($in_list>0) {
194             # append to value string
195 2         7 $val .= " $line"; # need to keep whitespace for separation
196 2 50       6 if ($line =~ /{/) {
197 0         0 barf "readenvi_hdr: warning, found nested braces for line '$line'\n";
198             }
199 2 50       9 if ( $val =~ /}$/ ) { # got to end of list
200             # parse $val list
201 2 50       7 print STDERR "readenvi_hdr: got list value = $val\n" if $verbose>1;
202             # clear list parse flag
203 2         6 $in_list--;
204             }
205             } else {
206             # look for next keyword = line
207 16         35 ($keyword,$val) = (undef, undef);
208 16         76 ($keyword,$val) = $line =~ /^\s*([^=]+)=\s*(.*)$/;
209 16 50       37 if (defined $keyword) {
210             # warning exit in case underscores are used in keywords
211 16 50       35 if ($keyword =~ /_/) {
212 0         0 barf "readenvi_hdr: WARNING keyword '$keyword' contains underscore!"
213             }
214             # normalize to lc and single underscore for whitespace
215 16         60 $keyword =~ s/\s+$//;
216 16         82 $keyword =~ s/\s+/_/g;
217 16         33 $keyword = lc $keyword;
218              
219 16         31 $val =~ s/^\s+//;
220 16         31 $val =~ s/\s+$//;
221              
222 16 100 66     76 $in_list++ if $val =~ /^{/ and not $in_list;
223 16 100 66     50 $in_list-- if $val =~ /}$/ and $in_list;
224              
225 16 100       45 next LINE if $in_list>0;
226              
227             # parse ENVI hdr lists and convert to perl array ref
228 14 100       30 if ($val =~ /^{/) { # } vim gets confused
229             # strip off braces
230 2         29 $val =~ s/^{\s*//;
231 2         32 $val =~ s/\s*}$//;
232 2         27 my @listval = split ',\s*', $val;
233 2 50       8 print STDERR "readenvi_hdr: expanded $keyword list value to (@listval)\n" if $verbose;
234 2         35 $val = [@listval];
235             }
236              
237 14 100       43 my $reqoropt = $envi_keywords->{$keyword} ? 'required' : 'optional';
238 14 50       27 print STDERR " got $reqoropt $keyword = $val\n" if $verbose;
239              
240             # replace ignore_value by data_ignore_value
241 14         25 $keyword =~ s/^ignore_value$/data_ignore_value/;
242 14         105 $hdr->{$keyword} = $val;
243              
244             } else {
245              
246 0 0       0 print STDERR " NOT a 'keyword =' line: '$line'\n" if $verbose;
247              
248             }
249             }
250             }
251              
252             }
253             # close hdr file
254 1         16 close $hdrfile;
255 1         11 return $hdr;
256             }
257              
258             =head2 readenvi
259              
260             =for ref
261              
262             reads ENVI standard format image files
263              
264             =for usage
265              
266             $im = readenvi( filename ); # read image data
267             ($im, $hdr) = readenvi( filename ); # read image data and hdr data hashref
268            
269             readenvi will look for an ENVI header file named filename.hdr
270            
271             If that file is not found, it will try with the windows
272             convention of replacing the suffix of the filename by .hdr
273            
274             If valid header data is found, the image will be read and
275             returned, with a ref to a hash of the hdr data in list
276             context.
277            
278             NOTE: This routine only supports raw binary data at this time.
279              
280             =cut
281              
282             sub readenvi {
283 0 0   0 1   barf 'Usage ($x [,$hdr]) = readenvi("filename")' if $#_ > 0;
284 0           my $enviname = $_[0];
285              
286 0           my $envi; # image data to return
287             my $filehdr; # image file header (before ENVI image data)
288 0           my $envihdr; # image hdr to return
289 0           my $flexhdr = [];
290              
291             # an easy progress message
292 0 0         print STDERR "readenvi: reading ENVI data from '@_'\n" if $verbose;
293              
294             # read ENVI header
295 0           my $envihdrname;
296              
297 0           $envihdrname = $enviname . '.hdr';
298 0 0         if (! -f $envihdrname ) {
299 0           $envihdrname = $enviname;
300 0           $envihdrname =~ s/\.\w+$/.hdr/;
301             }
302              
303 0 0         print STDERR "readenvi: ERROR could not find ENVI hdr file\n" unless -r $envihdrname;
304              
305 0           $envihdr = readenvi_hdr($envihdrname);
306              
307             # add read of imbedded_header data if have header_offset non-zero
308 0 0         if ($envihdr->{header_offset}) {
309             push @$flexhdr, { Type => 'byte', NDims => 1, Dims=>$envihdr->{header_offset} }
310 0           }
311              
312             # see if we need to swap
313 0 0         my $byteorder = ($Config{byteorder} =~ /4321$/) ? 1 : 0;
314 0 0         print STDERR "readenvi: Config{byteorder} is $Config{byteorder}\n" if $verbose>1;
315 0 0         if ($byteorder != $envihdr->{byte_order}) {
316 0 0         print STDERR "readenvi: got byteorder of $byteorder, ENVI file has $envihdr->{byte_order}\n" if $verbose;
317 0 0         print STDERR "readenvi: adding { Type => 'swap' } to \$flexhdr\n" if $verbose;
318 0 0         push @$flexhdr, { Type => 'swap' } if $byteorder != $envihdr->{byte_order};
319             }
320              
321             # determine data type for readflex from interleave header value
322 0           my $imagespec = { };
323 0           my $imagetype = $envi_data_types->[$envihdr->{data_type}];
324 0 0         print STDERR "readenvi: setting image { Type => $imagetype }\n" if $verbose;
325 0           $imagespec->{Type} = $imagetype;
326            
327             # construct Dims for readflex
328 0           my @imagedims = ();
329 0           @imagedims = @{$interleave->{lc($envihdr->{interleave})}};
  0            
330 0 0         print STDERR "readenvi: Need Dims => @imagedims\n" if $verbose;
331 0           my $imagedims = [ map { $envihdr->{$_} } @imagedims ];
  0            
332 0 0         print STDERR "readenvi: computed Dims => [", join( ', ', @{$imagedims} ), "]\n" if $verbose;
  0            
333 0           $imagespec->{Dims} = $imagedims;
334 0           $imagespec->{Ndims} = scalar(@$imagedims);
335 0           push @$flexhdr, $imagespec;
336              
337             # read file using readflex
338 0           my (@envidata) = readflex( $enviname, $flexhdr );
339 0 0         if (2==@envidata) {
340 0           ($filehdr,$envi) = @envidata;
341 0           $envihdr->{imbedded_header} = $filehdr;
342             } else {
343 0           ($envi) = @envidata;
344             }
345              
346             # attach ENVI hdr to ndarray
347 0           $envi->sethdr($envihdr);
348              
349             # handle ignore values by mapping to BAD
350 0 0         if ( exists $envihdr->{data_ignore_value} ) {
351 0           $envi->inplace->badflag; # set badflag for image
352 0           $envi->inplace->setvaltobad($envihdr->{data_ignore_value});
353             }
354              
355             # return data and optionally header if requested
356 0 0         return wantarray ? ($envi, $envihdr) : $envi;
357             }
358              
359             =head1 SEE ALSO
360              
361             Sample data: L
362              
363             Header description: L
364              
365             Raster description: L
366              
367             =cut
368              
369             1;