##########################################################################################
# vicar/vicarimage.py
##########################################################################################
"""Class to support accessing, reading, and modifying VICAR image files."""
import numpy as np
import sys
import vax
import warnings
from filecache import FCPath
try:
from _version import __version__
except ImportError:
__version__ = 'Version unspecified'
from vicar.vicarlabel import VicarLabel, VicarError
from vicar._DEFINITIONS import _DTYPE_FROM_FORMAT, _FORMAT_FROM_DTYPE, _HOST, _IMMUTABLE
[docs]
class VicarImage():
"""Constructor for a VicarImage.
This class defines the contents of a VICAR data file. It supports methods for reading
and writing files and for accessing header information.
**Properties**:
* `array`: The 3-D data array converted to native format.
* `array3d`: Same as above.
* `array2d`: Same as above, but with leading dimension (typically, bands)
stripped.
* `prefix`: The array prefix bytes as a 3-D array of unsigned bytes.
* `prefix3d`: Same as above.
* `prefix2d`: Same as above, but with the leading dimension stripped.
* `binheader`: The binary header as a bytes object; use vic.binheader_array() to
extract information.
* `label`: The internal object that manages the VICAR label information, available
if direct access to it is needed.
**Core Methods:**
* `arg`: The index of a parameter name within this label.
* `args`: Iterator over the numeric indices of the parameters in a label.
* `binheader_array`: Interpret the content of a binary header.
* `copy`: A copy of this VicarImage.
* `deepcopy`: An independent (deep) copy of this VicarImage.
* `from_array`: Construct a VicarImage object for a NumPy array.
* `from_file`: Construct a VicarImage object from the content of a VICAR data
file.
* `get`: Retrieve a label parameter value or return a default.
* `items`: Iterator over the (name, value) tuples in the label.
* `keys`: Iterator over the parameter names in the label as unique keys.
* `names`: Iterator over the parameter names in the label.
* `values`: Iterator over the parameter values in the label.
* `write_file`: Write this object as a VICAR data file.
**Python Syntax Support Methods:**
* `__contains__`: Enables "`name in image`" syntax for checking a name in the
label.
* `__delitem__`: Enables "`del image[name]`" syntax to remove a label parameter
name.
* `__eq__`: Enables "`a == b`', the test of whether two image objects are equal.
* `__getitem__`: Enables "`image[name]`" dictionary-like syntax to get the value
of a label parameter.
* `__iter__`: Enables "`for key in image:`" syntax to iterate over the label
parameter keys.
* `__len__`: Enables "`len(image)`", the number of parameters in the image's
label.
* `__repr__`: Enables "`repr(image)`", similar to the "`str(label)`", but with the
class name included.
* `__setitem__`: Enables "`image[name] = value`" dictionary-like syntax to set the
value of a label parameter.
* `__str__`: Enables "`str(image)`", returning a string representing the content
of a label.
**Notes About Dictionary Keys:**
When using dictionary-like syntax to reference a parameter in a VICAR label, a
rich set of options are available. For example, if `image` is a VicarImage object,
then:
* `image[n]` where `n` is an integer refers to the "nth" parameter in the
label. `n` can be positive or negative.
* `image[name]` where `name` is a string refers to the first occurrence in
the label of a parameter with this name.
* `image[name,n]` refers to the "nth" occurrence in the label of a parameter
with this name. `n` can be positive or negative.
* `image[name, after]` where both items are strings refers to the first
occurrence of parameter `name` after the first occurrence of parameter
`after` and before the second occurrence of `after`.
* `image[name, after, value]` refers to the first occurrence of parameter
`name` after the first location where `after` equals `value` and before the
next occurrence of `after`.
The last two options make it easy to reference a VICAR label parameter that is
repeated. For example, "`image['DAT_TIM', 'TASK', 'COPY']`" uniquely identifies
the occurrence of `DAT_TIM` applicable to `TASK='COPY'` when there might be other
`TASK` sections of the image label containing other values of `DAT_TIM`.
Append a "+" to `name` to expand upon the function's behavior. With "get"
operations, a list is returned identifying all of the occurrences of the selected
name rather than just the first or "nth". With "set" operations, a new occurrence
of `name` is inserted into the label even if the a parameter of the given name is
already present.
"""
[docs]
def __init__(self, source=None, array=None, *, prefix=None, binheader=None,
strict=True):
"""Constructor for a VicarImage object.
Parameters:
source (FCPath, Path, str, or VicarLabel, optional):
Source for the VicarImage based on a file path or VicarLabel; if not
specified (equvalent to source=None), a minimal label is created.
array (array-like, optional):
Optional data array for this object. If the source is a file path,
this array will override that in the file.
prefix (array-like, optional):
Optional prefix bytes for this object. If the source is a file
path, this value will override that in the file. To remove the
prefix array found in the file, use prefix=[].
binheader (array-like or bytes, optional):
Optional binary header for this data file. If the source is a
file path, this value will override that in the file. To remove
the binheader found in the file, use binheader=b''.
strict (bool, optional):
True (the default) to require strict conformance to the VICAR standard;
False for a looser version of the standard. If `source` is a VicarLabel,
this input is ignored; the value already assigned to the `source` is used
instead.
"""
self._filepath = None
if not source:
self._label = VicarLabel([], strict=strict)
elif isinstance(source, VicarLabel):
self._label = source
else:
self._filepath = FCPath(source)
info = VicarImage._read_file(self.filepath, extraneous='ignore',
strict=strict)
(self._label, array1, prefix1, binheader1) = info
if array is None:
array = array1
if prefix is None:
prefix = prefix1
if binheader is None:
binheader = binheader1
# Validate array, prefix, and binheader using setters
self._array = None
self._prefix = None
self._binheader = None
self.array = array
self.prefix = prefix
self.binheader = binheader
######################################################################################
# Properties
######################################################################################
@property
def filepath(self):
"""The path to the associated file.
Returns:
FCPath or None: The path to the associated file, if any.
"""
return self._filepath
@filepath.setter
def filepath(self, value):
"""Set the path to the associated file.
Parameters:
value (FCPath, Path, str, or None):
The path to the associated file; None to remove a file association.
"""
if value is None:
self._filepath = None
else:
self._filepath = FCPath(value)
@property
def label(self):
"""The VicarLabel object.
Returns:
VicarLabel: The VicarLabel object.
"""
return self._label
@property
def array2d(self):
"""The data array as 2-D.
Returns:
np.ndarray or None:
The data array as 2-dimensional if it is present; otherwise, None.
Raises:
VicarError: If the array has more than two dimensions.
"""
if self._array is None:
return None
if self._array.shape[0] != 1:
raise VicarError(f'VICAR array shape {self._array.shape} is not 2-D')
return self._array[0]
@property
def data_2d(self):
"""The data array as 2-D; DEPRECATED name.
Returns:
np.ndarray or None:
The data array as 2-dimensional if it is present; otherwise, None.
Raises:
VicarError: If the array has more than two dimensions.
"""
return self.array2d
@property
def array3d(self):
"""The data array as 3-D.
Returns:
np.ndarray or None:
The data array as 3-dimensional if it is present; otherwise, None.
"""
return self._array
@property
def data_3d(self):
"""The data array as 3-D; DEPRECATED name.
Returns:
np.ndarray or None:
The data array as 3-dimensional if it is present; otherwise, None.
"""
return self._array
@property
def array(self):
"""The data array.
Returns:
np.ndarray or None: The data array if it is present; otherwise, None.
"""
return self._array
@array.setter
def array(self, value):
"""Set the data array.
Parameters:
value (array-like or None):
The prefix array or None to remove the data array.
Raises:
VicarError:
If the array shape is incompatible with an existing prefix or binary
header.
"""
if value is None:
self._array = None
return
value = np.asarray(value)
if value.ndim != 3:
value = value.reshape((3-value.ndim) * (1,) + value.shape) # reshape to 3-D
VicarImage._check_array_vs_prefix(value, self._prefix)
recsize = self._label['NBB'] + value.shape[-1] * value.itemsize
nlb = self._label['NLB']
if self._binheader is not None:
width = len(bytes(self._binheader))
if width % recsize != 0:
raise VicarError(f'array shape {value.shape} is incompatible with binary '
f'header width {width}')
nlb = width // recsize
# Tests passed
self._array = value
self._label['HOST'] = _HOST
self._label['TYPE'] = 'IMAGE'
(self._label['FORMAT'], isint) = VicarImage._format_isint(value)
if isint:
if value.itemsize > 1:
self._label['INTFMT'] = VicarImage._intfmt(value)
else:
self._label['REALFMT'] = VicarImage._realfmt(value)
self._label['RECSIZE'] = recsize
self._label['NLB'] = nlb
self._label._set_n321(*value.shape)
@property
def prefix2d(self):
"""The image prefix as a 2-D array.
Returns:
np.ndarray: The image prefix as a 2-dimensional array.
Raises:
VicarError: If the prefix array has more than two dimensions.
"""
if self._prefix is None:
return None
if self._prefix.shape[0] != 1:
raise VicarError(f'prefix bytes shape {self._prefix.shape} is not 2-D')
return self._prefix[0]
@property
def prefix_2d(self):
"""The image prefix as a 2-D array; DEPRECATED name.
Returns:
np.ndarray: The image prefix as a 2-dimensional array.
Raises:
VicarError: If the prefix array has more than two dimensions.
"""
return self.prefix2d
@property
def prefix3d(self):
"""The image prefix as a 3-D array.
Returns:
np.ndarray: The image prefix as a 3-dimensional array.
"""
return self._prefix
@property
def prefix_3d(self):
"""The image prefix as a 3-D array; DEPRECATED name.
Returns:
np.ndarray: The image prefix as a 2-dimensional array.
"""
return self._prefix
@property
def prefix(self):
"""The image prefix object.
Returns:
np.ndarray: The image prefix as a 2-dimensional array.
"""
return self._prefix
@prefix.setter
def prefix(self, value):
"""Set the image prefix array.
Parameters:
value (array-like or None):
The prefix array or None to remove the prefix array.
Raises:
VicarError:
If the array shape is incompatible with an existing data array or binary
header.
"""
if value is None:
nbb = 0
else:
value = np.asarray(value)
if value.size == 0:
nbb = 0
value = None
else:
if value.ndim != 3:
value = value.reshape((3-value.ndim) * (1,) + value.shape)
VicarImage._check_array_vs_prefix(self._array, value)
nbb = value.shape[-1] * value.itemsize
recsize = self['RECSIZE']
nlb = self['NLB']
if self._array is not None:
recsize = self._array.shape[-1] * self._array.itemsize + nbb
if self._binheader is not None:
width = len(bytes(self._binheader))
if width % recsize != 0:
raise VicarError(f'new RECSIZE={recsize} is incompatible with binary '
f'header length {width}')
nlb = width // recsize
# Tests passed
self._prefix = value
self._label['NBB'] = nbb
self._label['NLB'] = nlb
self._label['RECSIZE'] = recsize
if value is not None:
self._label['HOST'] = _HOST
(fmt, isint) = VicarImage._format_isint(value)
# Prefix defines attributes left undefined by array
if self._array is None:
self._label['FORMAT'] = fmt
if isint:
if self._array is None or self._array.dtype.kind in 'fc':
if value.itemsize > 1:
self._label['INTFMT'] = VicarImage._intfmt(value)
else:
if self._array is None or self._array.dtype.kind in 'ui':
self._label['REALFMT'] = VicarImage._realfmt(value)
@property
def binheader(self):
"""The binary header as an array or bytes object.
Returns:
np.ndarray, bytes, or None: The binary header if present; otherwise, None.
"""
return self._binheader
@binheader.setter
def binheader(self, value):
"""Set the binary header.
Parameters:
value (bytes, np.ndarray, or None):
The binary header or None to remove the binary header.
Raises:
VicarError:
If the header length is incompatible with the existing RECSIZE.
"""
if value is None:
self._binheader = None
self._label['NLB'] = 0
return
nlb = self._label['NLB']
width = len(bytes(value))
if self._array is not None:
recsize = self._label['RECSIZE']
if width % recsize != 0:
raise VicarError(f'binary header length {width} is incompatible with '
f'RECSIZE={recsize}')
nlb = width // recsize
# Tests passed
self._binheader = None if width == 0 else value
self._label['BHOST'] = _HOST
self._label['NLB'] = nlb
if isinstance(value, np.ndarray):
if value.dtype.kind in 'cf':
self._label['BREALFMT'] = VicarImage._realfmt(value)
elif value.dtype.kind in 'ui' and value.dtype.itemsize > 1:
self._label['BINTFMT'] = VicarImage._intfmt(value)
######################################################################################
# Support for the Standard Python API
######################################################################################
[docs]
def copy(self):
"""A copy of this VicarImage.
The copied label is independent of the original, but the data and prefix array
are shared.
Returns:
VicarImage: The copy.
"""
return VicarImage(source=self.label.copy(), array=self._array,
prefix=self._prefix, binheader=self._binheader)
[docs]
def deepcopy(self):
"""An independent (deep) copy of this VicarImage.
Returns:
VicarImage: The deep copy.
"""
vim = self.copy()
vim._array = None if self._array is None else self._array.copy()
vim._prefix = None if self._prefix is None else self._prefix.copy()
return vim
[docs]
def __eq__(self, other):
"""True if this VicarImage is equal to the given object.
VicarImages are equal if the label content, data array, prefix array, binary
header are all equal. Label formatting hints need not be the same.
Parameters:
other (VicarImage): Second VicarImage to compare with this.
Returns:
bool: True if the objects are equal.
"""
if not isinstance(other, VicarImage):
return False
prefix1 = b'' if self._prefix is None else bytes(self._prefix)
prefix2 = b'' if other._prefix is None else bytes(other._prefix)
binheader1 = b'' if self._binheader is None else bytes(self._binheader)
binheader2 = b'' if other._binheader is None else bytes(other._binheader)
return (self._label == other._label
and np.all(self._array == other._array)
and prefix1 == prefix2
and binheader1 == binheader2)
[docs]
def __str__(self):
return str(self._label)
[docs]
def __repr__(self):
return 'VicarImage("""' + self._label.as_string(sep='\n\n') + '""")'
######################################################################################
# Public API
######################################################################################
[docs]
@staticmethod
def from_file(filepath, extraneous='ignore', strict=True):
"""VicarImage object from an existing VICAR image file.
Parameters:
filepath (FCPath, Path, or str): Path to an existing VICAR data file.
extraneous (str, optional):
How to handle the presence of extraneous bytes at the end of the file, one
of:
* "error" to raise VicarError;
* "warn" to raise a UserWarning;
* "print" to print a message;
* "ignore" to ignore;
* "include" to include the extraneous bytes as part of the return.
strict (bool, optional):
True (the default) to require strict conformance to the VICAR standard;
False for a looser version of the standard.
Returns:
VicarImage or (VicarImage, bytes or None): A VicarImage or a tuple containing:
* VicarImage: A new object containing the content of the specified file.
* bytes or None: Any extraneous bytes from the end of the file, included if
`extraneous` equals "include".
"""
info = VicarImage._read_file(filepath, extraneous=extraneous, strict=strict)
(label, data, prefix, binheader) = info[:4]
vim = VicarImage(source=label, array=data, prefix=prefix, binheader=binheader)
if extraneous == 'include':
return (vim, info[4])
return vim
[docs]
@staticmethod
def from_array(array, strict=True):
"""Construct a VicarImage object for an array.
Parameters:
array (array-like): The data array to use in this VicarImage object.
strict (bool, optional):
True (the default) to require strict conformance to the VICAR standard;
False for a looser version of the standard.
Returns:
VicarImage: A new VicarImage object containing this data array.
"""
return VicarImage(source=[], array=array, strict=strict)
[docs]
def write_file(self, filepath=None):
"""Write the VicarImage object into a file.
Parameters:
filepath (FCPath, Path, or str, optional):
Optional the path of the file to write. If not specified but this object's
filepath attribute is defined, it will write to this file.
"""
filepath = filepath or self.filepath
if self._array is None:
raise VicarError('Image array is missing for ' + str(filepath))
self.filepath = filepath
# Open the file for binary write
with self._filepath.open('wb') as f:
labels = self._label.export(resize=True)
f.write(labels[0].encode('latin1'))
if self._binheader is not None:
if isinstance(self._binheader, np.ndarray):
f.write(self._binheader.data)
else:
f.write(self._binheader)
if self._prefix is None and self._array is not None:
f.write(self._array.data)
else:
n2 = self._label['N2']
n3 = self._label['N3']
nbb = self._label['NBB']
recsize = self._label['RECSIZE']
array = np.empty((n3,n2,recsize), dtype='uint8')
array[:,:,:nbb] = self._prefix.view(dtype='uint8')
array[:,:,nbb:] = self._array.view(dtype='uint8')
f.write(array.data)
f.write(labels[1].encode('latin1'))
######################################################################################
# File I/O
######################################################################################
@staticmethod
def _read_file(filepath, *, extraneous='ignore', strict=True):
"""The VICAR data array, binary header, and prefix bytes from the specified
data file.
Parameters:
filepath (FCPath, Path, or str): Path to an existing VICAR data file.
extraneous (str, optional):
How to handle the presence of extraneous bytes at the end of the file, one
of:
* "error" to raise VicarError;
* "warn" to raise a UserWarning;
* "print" to print a message;
* "ignore" to ignore;
* "include" to include the extraneous bytes as part of the return.
strict (bool, optional): True (the default) to require strict conformance to
the VICAR standard; False for a looser version of the standard.
Returns:
(VicarLabel, np.ndarray, np.ndarray or None, bytes[, bytes]):
A tuple containing:
* VicarLabel: The label.
* np.ndarray: The data as a 3D array converted to native format.
* np.ndarray or None: The prefix array as a 3D array of unsigned bytes if
present; otherwise, None.
* bytes: The binary header if present; otherwise, None.
* bytes, optional: Any extraneous bytes at the end of the file, included if
`extraneous` equals "include".
Raises:
OSError: If the referenced file could not be read.
ValueError: If `extraneous` does not have a valid value.
VicarError: If the referenced file does not conform to the VICAR standard.
"""
if extraneous not in ('ignore', 'print', 'warn', 'error', 'include'):
raise ValueError('invalid input value for extraneous: ' + repr(extraneous))
filepath = FCPath(filepath)
with filepath.open('rb') as f:
# Get the label
(label, extra) = VicarLabel.read_label(f, _extra=True)
# Handle extraneous bytes
if extra:
if all(c == 0 for c in extra):
message = (f'{filepath} has {len(extra)} zero-valued trailing bytes')
else: # pragma: no cover
message = (f'{filepath} has {len(extra)} trailing bytes')
if extraneous == 'print':
print(message)
elif extraneous == 'warn':
warnings.warn(message)
elif extraneous == 'error':
raise VicarError(message)
# Extract key label parameters
ldict = VicarLabel(label, strict=strict)
lblsize = ldict['LBLSIZE'] # bytes in header
recsize = ldict['RECSIZE'] # bytes per record
nlb = ldict['NLB'] # records of binary header
nbb = ldict['NBB'] # number of binary prefix bytes
intfmt = ldict.get('INTFMT', 'LOW') # LOW or HIGH
realfmt = ldict.get('REALFMT', 'VAX') # IEEE, RIEEE, or VAX
format_ = ldict['FORMAT'] # BYTE, HALF, FULL, REAL, etc.
# Read the binary header
f.seek(lblsize)
if nlb:
binheader = f.read(nlb * recsize)
else:
binheader = None
# Read the data and prefix bytes
ldict._n123_from_nbls() # Sometimes N1, N2, N3 are wrong
n2 = ldict['N2']
n3 = ldict['N3']
if n2 and n3:
data = np.frombuffer(f.read(n3 * n2 * recsize), dtype='uint8')
data = data.reshape(n3, n2, recsize)
else:
data = None
# Separate the prefix bytes from the data
if nbb and data is not None:
array = data[:,:,nbb:].copy()
prefix = data[:,:,:nbb].copy()
else:
array = data
prefix = None
# Convert the array to native format
if array is not None:
dtype = _DTYPE_FROM_FORMAT[format_]
if dtype[0] in 'ui':
dtype = ('>' if intfmt == 'HIGH' else '<') + dtype
else: # "fc"
if realfmt == 'VAX': # pragma: no cover
if dtype[-1] == '8':
array = vax.from_vax64(array)
else:
array = vax.from_vax32(array)
dtype = '<' + dtype
else:
dtype = ('>' if realfmt == 'IEEE' else '<') + dtype
array = array.view(dtype=dtype) # define actual format
array = np.asarray(array, dtype='=' + dtype[1:]) # convert to native format
if extraneous == 'include':
return (ldict, array, prefix, binheader, extra)
return (ldict, array, prefix, binheader)
######################################################################################
# Public methods inherited from the VicarLabel object
######################################################################################
[docs]
def __len__(self):
"""The number of keywords in the label of this VicarImage.
Returns:
int: Number of parameters in the VICAR label
"""
return len(self._label)
[docs]
def __getitem__(self, key):
"""Retrieve the value or values of the VICAR parameter defined by key, using
various indexing options.
Parameters:
key (int, name, or tuple): The indexing key, interpreted as follows:
* *int* = `n`: The "nth" parameter in the label. `n` can be positive or
negative.
* *str* = `name`: The first occurrence in the label of a parameter with
this name.
* (*str*, *int*) = (`name`, `n`): The "nth" occurrence in the label of a
parameter with this name. `n` can be positive or negative.
* (*str*, *str*) = (`name`, `after`): The first occurrence of parameter
`name` after the first occurrence of parameter `after` and before the
second occurrence of `after`.
* (*str*, *str*, *any*) = (`name`, `after`, `value`): The first occurrence
of parameter `name` after the first location where `after` equals
`value` and before the next occurrence of `after`.
The last two options make it easy to reference a VICAR label parameter
that is repeated. For example, if `image` is a VicarImage, then
"`image['DAT_TIM', 'TASK', 'COPY']`" uniquely identifies the occurrence of
`DAT_TIM` applicable to `TASK='COPY'` when there might be other `TASK`
sections of the label containing other values of `DAT_TIM`.
Append a "+" to the name to return a list of all values where the
constraints are satisfied, starting with the first or "nth".
Returns:
int, float, string, or list:
The value of the indexed parameter if there is no name ending in "+";
otherwise, a list of all the parameter values starting with the "nth".
Raises:
IndexError: If any numeric component of the key is out of range.
KeyError: If the parameter name is not present in the label (or the section of
the label defined by `after_name` and `after_value`), or if the key format
is unrecognized.
TypeError: If the key is not a recognized type or contains a component that is
not of a recognized type.
ValueError: If no identified parameter equals `value` or if no occurrence of
`after_name` equals `after_value`.
"""
return self._label.__getitem__(key)
[docs]
def get(self, key, default):
"""Retrieve the value of the VICAR parameter defined by the given key, using
various indexing options.
If the key is not found, return a specified default value.
Parameters:
key (int, str, or tuple): The indexing key, interpreted as follows:
* *int* = `n`: The "nth" parameter in the label. `n` can be positive or
negative.
* *str* = `name`: The first occurrence in the label of a parameter with
this name.
* (*str*, *int*) = (`name`, `n`): The "nth" occurrence in the label of a
parameter with this name. `n` can be positive or negative.
* (*str*, *str*) = (`name`, `after`): The first occurrence of parameter
`name` after the first occurrence of parameter `after` and before the
second occurrence of `after`.
* (*str*, *str*, *any*) = (`name`, `after`, `value`): The first occurrence
of parameter `name` after the first location where `after` equals
`value` and before the next occurrence of `after`.
The last two options make it easy to reference a VICAR label parameter
that is repeated. For example, if `image` is a VicarImage, then
"`image['DAT_TIM', 'TASK', 'COPY']`" uniquely identifies the occurrence of
`DAT_TIM` applicable to `TASK='COPY'` when there might be other `TASK`
sections of the label containing other values of `DAT_TIM`.
Append a "+" to the name to return a list of all values where the
constraints are satisfied, starting with the first or "nth".
default (int, float, str, or list): The value to return if the key is not
found.
Returns:
int, float, str, or list:
If a name is provided that ends in a plus, the returned value will be the
list of all values of the selected key, or else `[default]` if the list
would be empty or the key would raise an error.
Otherwise, the returned value is that of the key if present, or `default`
if it is not.
Raises:
IndexError: If any numeric component of the key is out of range.
KeyError: If the parameter name is not present in the label (or the section of
the label defined by `after_name` and `after_value`), or if the key format
is unrecognized.
TypeError: If the key is not a recognized type or contains a component that is
not of a recognized type.
ValueError: If no identified parameter equals `value` or if no occurrence of
`after_name` equals `after_value`.
"""
return self._label.get(key, default)
[docs]
def __setitem__(self, key, value):
"""Set the value of the VICAR parameter defined by the given key; define a new
parameter name and value if necessary.
Parameters:
key (int, str, or tuple): The indexing key, interpreted as follows:
* *int* = `n`: The "nth" parameter in the label. `n` can be positive or
negative.
* *str* = `name`: The first occurrence in the label of a parameter with
this name.
* (*str*, *int*) = (`name`, `n`): The "nth" occurrence in the label of a
parameter with this name. `n` can be positive or negative.
* (*str*, *str*) = (`name`, `after`): The first occurrence of parameter
`name` after the first occurrence of parameter `after` and before the
second occurrence of `after`.
* (*str*, *str*, *any*) = (`name`, `after`, `value`): The first occurrence
of parameter `name` after the first location where `after` equals
`value` and before the next occurrence of `after`.
The last two options make it easy to reference a VICAR label parameter
that is repeated. For example, if `image` is a VicarImage, then
"`image['DAT_TIM', 'TASK', 'COPY']`" uniquely identifies the occurrence of
`DAT_TIM` applicable to `TASK='COPY'` when there might be other `TASK`
sections of the label containing other values of `DAT_TIM`.
Append a "+" to the name to force a new occurrence of the key to be
inserted, even if the key already exists.
value (int, float, string, list, or tuple): Value to assign to the indexed
entry in the label.
Optional formatting can be included if a user wants additional control
over how this value will be formatted in the label string will be
formatted, by replacing the value with a tuple (value, hints...). See
``Notes`` for details about formatting hints.
Raises:
IndexError: If any numeric component of the key is out of range.
KeyError: If the parameter name is not present in the label (or the section of
the label defined by `after_name` and `after_value`), or if the key format
is unrecognized.
TypeError: If the key is not a recognized type or contains a component that is
not of a recognized type.
ValueError: If no identified parameter equals `value` or if no occurrence of
`after_name` equals `after_value`.
Notes:
Formatting hints can be included wherever a VICAR parameter value is
specified; simply replace the value by a tuple (value, hints...).
Hints can be specified using up to four items:
([`format`][[[, `name_blanks`], `val_blanks`], `sep_blanks`])
where:
* `format` is a format string, e.g., "%+7d" or "%7.3f".
* `name_blanks` is the number of blank characters after the name and
before the equal sign; zero by default.
* `val_blanks` is the number of blank characters after the equal sign and
before the value; zero by default.
* `sep_blanks` is the number of blanks after the value and before the next
label parameter or the label's end; two by default.
Note the use of square brackets in the tuple expression above. If the first
hint value is a string, it is interpreted as `format`; otherwise, the `format`
is unspecified. After the optional format, values are interpreted as numbers
of blanks. If only one int is provided, it defines `sep_blanks`, with
`val_blanks` and `name_blanks` set to zero. If two trailing ints are provided,
they define `val_blanks` and `sep_blanks`, with `name_blanks` set to zero.
For example, if the name is "TEXP" and the value is 1.5, this is how hint
values are interpreted::
<no hints> = ("", 0, 0, 0) -> "TEXP=1.5 "
"%.3f" = ("%.3f", 0, 0, 0) -> "TEXP=1.500 "
("%.3f", 4) = ("%.3f", 0, 0, 4) -> "TEXP=1.500 "
("%.3f", 1, 4) = ("%.3f", 0, 1, 4) -> "TEXP= 1.500 "
("%.3f", 2, 1, 4) = ("%.3f", 2, 1, 4) -> "TEXP = 1.500 "
4 = ("", 0, 0, 4) -> "TEXP=1.5 "
(1, 4) = ("", 0, 1, 4) -> "TEXP= 1.5 "
(2, 1, 4) = ("", 2, 1, 4) -> "TEXP = 1.5 "
When the parameter value is a list, it is also possible to embed formatting
information on an item by item basis. Replace any item value by a tuple:
(`item` [, `format`][[, `blanks_before`], `blanks_after`])
where:
* `format` is a format string, e.g., "%+07d", "%12.3e" or "%.4f".
* `blanks_before` is the number of blanks before the value, after the left
parenthesis or comma; zero is the default.
* `blanks_after` is the number of blanks after the value and before the
next comma or the right parenthesis; zero is the default.
Here are some examples of a list with embedded formatting for a parameter
named "XY" with a value [7,6]::
[(7, "%+02d"), 6] = [(7, "%+02d", 0, 0), 6] -> "XY=[+07, 6] "
[(7, 2), 6] = [(7, "", 0, 2), 6] -> "XY=[7 , 6] "
[(7, 1, 2), 6]) = [(7, "", 1, 2), 6] -> "XY=[ 7 , 6] "
[(7, "%02d", 2), 6]) = [(7, "%02d", 0, 2), 6] -> "XY=[07 , 6] "
[(7, "%02d", 1, 2), 6]) = [(7, "%02d", 1, 2), 6] -> "XY=[ 07 , 6] "
"""
name = self._label._get_name(key)
has_plus = VicarLabel._has_plus(key)
if not has_plus and name in _IMMUTABLE:
indx = self._label.arg(key)
if indx == self._label._key_index[name][0]: # if first occurrence
raise VicarError(f'VICAR parameter {name} cannot be modified')
self._label.__setitem__(key, value)
[docs]
def __delitem__(self, key):
"""Delete the value of the VICAR parameter identified by the given key.
Parameters:
key (int, str, or tuple): The indexing key, interpreted as follows:
* *int* = `n`: The "nth" parameter in the label. `n` can be positive or
negative.
* *str* = `name`: The first occurrence in the label of a parameter with
this name.
* (*str*, *int*) = (`name`, `n`): The "nth" occurrence in the label of a
parameter with this name. `n` can be positive or negative.
* (*str*, *str*) = (`name`, `after`): The first occurrence of parameter
`name` after the first occurrence of parameter `after` and before the
second occurrence of `after`.
* (*str*, *str*, *any*) = (`name`, `after`, `value`): The first occurrence
of parameter `name` after the first location where `after` equals
`value` and before the next occurrence of `after`.
The last two options make it easy to reference a VICAR label parameter
that is repeated. For example, if `image` is a VicarImage, then
"`image['DAT_TIM', 'TASK', 'COPY']`" uniquely identifies the occurrence of
`DAT_TIM` applicable to `TASK='COPY'` when there might be other `TASK`
sections of the label containing other values of `DAT_TIM`.
Append a "+" to `name` to delete all of the label parameters whose names
match the constraints, starting with the first or "nth".
Raises:
IndexError: If any numeric component of the key is out of range.
KeyError: If the parameter name is not present in the label (or the section
of the label defined by `after_name` and `after_value`), or if the key
format is unrecognized.
TypeError: If the key is not a recognized type or contains a component that is
not of a recognized type.
ValueError: If no identified parameter equals `value` or if no occurrence of
`after_name` equals `after_value`.
"""
name = self._label._get_name(key)
if name in _IMMUTABLE:
indx = self._label.arg(VicarLabel._remove_plus(key))
if indx == self.label._key_index[name][0]: # if first occurrence
raise VicarError(f'VICAR parameter {name} cannot be deleted')
self._label.__delitem__(key)
[docs]
def __contains__(self, key):
"""True if the given key can is found in the label of this VicarImage.
Parameters:
key (int, str, or tuple): The key identifying the label parameter to check.
* *int* = `n`: The "nth" parameter in the label. `n` can be positive or
negative.
* *str* = `name`: The first occurrence in the label of a parameter with
this name.
* (*str*, *int*) = (`name`, `n`): The "nth" occurrence in the label of a
parameter with this name. `n` can be positive or negative.
* (*str*, *str*) = (`name`, `after`): The first occurrence of parameter
`name` after the first occurrence of parameter `after` and before the
second occurrence of `after`.
* (*str*, *str*, *any*) = (`name`, `after`, `value`): The first occurrence
of parameter `name` after the first location where `after` equals
`value` and before the next occurrence of `after`.
The last two options make it easy to reference a VICAR label parameter
that is repeated. For example, if `image` is a VicarImage, then
"`image['DAT_TIM', 'TASK', 'COPY']`" uniquely identifies the occurrence of
`DAT_TIM` applicable to `TASK='COPY'` when there might be other `TASK`
sections of the label containing other values of `DAT_TIM`.
Returns:
bool: True if the key is found within the label.
"""
return self._label.__contains__(key)
[docs]
def arg(self, key):
"""The index or indices of the keyed item in the label of this VicarImage.
Parameters:
key (int, name, or tuple): The indexing key, interpreted as follows:
* *int* = `n`: The "nth" parameter in the label. `n` can be positive or
negative.
* *str* = `name`: The first occurrence in the label of a parameter with
this name.
* (*str*, *int*) = (`name`, `n`): The "nth" occurrence in the label of a
parameter with this name. `n` can be positive or negative.
* (*str*, *str*) = (`name`, `after`): The first occurrence of parameter
`name` after the first occurrence of parameter `after` and before the
second occurrence of `after`.
* (*str*, *str*, *any*) = (`name`, `after`, `value`): The first occurrence
of parameter `name` after the first location where `after` equals
`value` and before the next occurrence of `after`.
The last two options make it easy to reference a VICAR label parameter
that is repeated. For example, if `image` is a VicarImage, then
"`image['DAT_TIM', 'TASK', 'COPY']`" uniquely identifies the occurrence of
`DAT_TIM` applicable to `TASK='COPY'` when there might be other `TASK`
sections of the label containing other values of `DAT_TIM`.
Append a "+" to the name to return a list of all indices where the
constraints are satisfied, starting with the first or "nth".
value (int, float, or string, optional):
If provided, the identified parameter must equal this value. For an
integer key, if the indexed parameter does not have this value, ValueError
is raised. For any key involving a name, values of the named parameter
that do not match `value` are skipped over until the one(s) with the
correct value are found.
Returns:
int or list[int]:
If "+" is appended to `name`, the list of all positive indices that
identify matching parameters, starting with the "nth". Otherwise, the
single positive index into the label identifying the parameter.
Raises:
IndexError: If any numeric component of the key is out of range.
KeyError: If the parameter name is not present in the label (or the section of
the label defined by `after_name` and `after_value`), or if the key format
is unrecognized.
TypeError: If the key is not a recognized type or contains a component that is
not of a recognized type.
ValueError: If no identified parameter equals `value` or if no occurrence of
`after_name` equals `after_value`.
"""
return self._label.arg(key)
[docs]
def __iter__(self):
"""Iterator over the unique names or (name, occurrence) pairs in the label of
this VicarImage.
Returns:
iterator:
The parameter keys within this label, in order. The key is the parameter
name if it is unique or (name, occurrence number) otherwise.
"""
return self._label.__iter__()
[docs]
def names(self, pattern=None):
"""Iterator for the VICAR parameter name strings in the label of this VicarImage.
Parameters:
pattern (str or re.Pattern, optional):
Regular expression that can be used to filter the label parameter names.
Returns:
list: The matching parameter names within this label, in order.
"""
return self._label.names(pattern=pattern)
[docs]
def keys(self, pattern=None):
"""Iterator over the keys in the label of this VicarImage. The key is the
parameter name if it is unique or (name, occurrence number) otherwise.
Parameters:
pattern (str or re.Pattern, optional):
Regular expression that can be used to filter the label parameter names.
Returns:
list:
The parameter keys within this label, in order. Each key is the parameter
name if it is unique or (name, occurrence number) otherwise.
"""
return self._label.keys(pattern=pattern)
[docs]
def values(self, pattern=None):
"""Iterator over the values in the label of this VicarImage.
Parameters:
pattern (str or re.Pattern, optional):
Regular expression that can be used to filter the label parameter names.
Returns:
iterator: The values of the matching parameters within this label, in order.
"""
return self._label.values(pattern=pattern)
[docs]
def items(self, pattern=None, unique=True):
"""Iterator over the (key, value) pairs in the label of this VicarImage.
Parameters:
pattern (str or re.Pattern, optional):
Regular expression that can be used to filter the label parameter names.
unique (bool, optional):
True to return unique keys, in which non-unique names are replaced by
tuples (name, occurrence). If False, all keys are name strings, and a name
may appear multiple times.
Returns:
iterator:
Tuples (name, value) of the matching parameter names within this label, in
order.
"""
return self._label.items(pattern=pattern, unique=unique)
[docs]
def args(self, pattern=None):
"""Iterator over the numerical indices of the keywords in the label of this
VicarImage.
Parameters:
pattern (str or re.Pattern, optional):
Regular expression that can be used to filter the label parameter names.
Returns:
iterator:
The indices of the matching parameter names within this label, in order.
"""
return self._label.args(pattern=pattern)
[docs]
def as_dict(self):
"""The VicarLabel object.
DEPRECATED, provided primarily for backward compatibility. Use the `label`
property instead, because it behaves as a dictionary.
Returns:
VicarLabel: The VicarLabel object associated with this VicarImage.
"""
return self._label
######################################################################################
# Utilities
######################################################################################
def _intfmt(x):
"""Determine the INTFMT based on the dtype of an array."""
if isinstance(x, np.ndarray):
if x.dtype.byteorder == '<': # pragma: no cover
return 'LOW'
if x.dtype.byteorder == '>': # pragma: no cover
return 'HIGH'
return 'LOW' if sys.byteorder == 'little' else 'HIGH'
def _realfmt(x):
"""Determine the REALFMT value based on the dtype of an array."""
if isinstance(x, np.ndarray):
if x.dtype.byteorder == '>': # pragma: no cover
return 'IEEE'
if x.dtype.byteorder == '<': # pragma: no cover
return 'RIEEE'
return 'RIEEE' if sys.byteorder == 'little' else 'IEEE'
def _format_isint(x):
"""True if this array contains integer values."""
try:
key = x.dtype.kind + str(x.itemsize)
return _FORMAT_FROM_DTYPE[key]
except KeyError:
raise VicarError(f'array dtype "{x.dtype}" is not supported by VICAR')
def _check_array_vs_prefix(array, prefix):
"""Raise an exception if the given image array and prefix byte array are not
compatible.
Parameters:
array (np.ndarray or None): A data array.
prefix (np.ndarray or None): A prefix array.
Raises:
VicarError: If the arrays are invalid or inconsistent with one another.
"""
if array is not None and array.ndim != 3:
raise VicarError(f'data array shape {array.shape} is not 2-D or 3-D')
if prefix is not None and prefix.ndim != 3:
raise VicarError(f'prefix array shape {prefix.shape} is not 2-D or 3-D')
if array is None or prefix is None:
return
if array.shape[:-1] != prefix.shape[:-1]:
raise VicarError('data and prefix arrays have incompatible shapes: '
f'{array.shape}, {prefix.shape}')
(format1, isint1) = VicarImage._format_isint(array)
if isint1:
intfmt1 = VicarImage._intfmt(array)
else:
realfmt1 = VicarImage._realfmt(array)
(format2, isint2) = VicarImage._format_isint(prefix)
if isint2:
intfmt2 = VicarImage._intfmt(prefix)
else:
realfmt2 = VicarImage._realfmt(prefix)
if isint1 and isint2:
if format1 != 'BYTE' and format2 != 'BYTE' and intfmt1 != intfmt2:
raise VicarError('data and prefix array formats are incompatible: '
f'{intfmt1}, {intfmt2}')
if not isint1 and not isint2:
if realfmt1 != realfmt2:
raise VicarError('data and prefix array formats are incompatible: '
f'{realfmt1}, {realfmt2}')
##########################################################################################