"""
easyleed.io
-------------
Import routines for different LEED file formats
"""
import numpy as np
from .qt import QtGui as qtgui
# load regular expression package (for parsing of energy from file name)
import re
import os.path
from . import logger
#### load packages for available file types ####
formats_available = ['IMG']
try:
import pyfits
formats_available.append("FITS")
except:
logger.warning("The pyfits package is not installed.")
# try to import PIL in two possible ways (dependent on PIL version)
try:
from PIL import Image
formats_available.append("PIL")
except:
try:
import Image
formats_available.append("PIL")
except:
logger.warning("The pillow package is not installed.")
[docs]class ImageLoader(object):
""" Abstract base class for a class loading LEED images.
Subclasses need to provide
- get_image(image_path)
Subclasses may override (default: from filename with regex)
- get_energy(image_path)
"""
def __init__(self, image_paths, regex):
# build a dictionary with energy as key and imagePath as value
self.regex = regex
self.files = {}
for image_path in image_paths:
energy = self.get_energy(image_path)
self.files[energy] = image_path
self.energies = sorted(self.files.keys())
self.restart()
def get_energy(self, image_path):
m = re.search(self.regex, image_path)
if m is None:
raise IOError('Invalid filename. Check naming policy.')
return float(m.group())
[docs] def current_energy(self):
""" Get current energy. """
return self.energies[self.index]
def __iter__(self):
return self
[docs] def restart(self):
""" Start at lowest energy again. """
self.index = -1
[docs] def previous(self):
""" Get image at next lower beam energy. """
if self.index == 0:
raise StopIteration("there is no previous image")
else:
self.index -= 1
energy = self.energies[self.index]
return self.get_image(self.files[energy]), energy
def __next__(self):
""" Get image at next higher beam energy. """
if self.index < len(self.energies)-1:
self.index += 1
energy = self.energies[self.index]
return self.get_image(self.files[energy]), energy
else:
raise StopIteration()
next = __next__
[docs] def goto(self, energy):
""" Get image at given beam energy. """
self.index = self.energies.index(energy)
return self.get_image(self.files[energy]), energy
# FIXME: untested
[docs] def custom_iter(self, energies):
""" Returns an iterator to iter over the given energies."""
non_elements = set(energies) - set(self.energies)
if non_elements:
raise Exception("ImageLoader doesn't have the following elements: %s" % (list(non_elements)))
for energy in energies:
yield self.get_image(energy), energy
[docs]class ImgImageLoader(ImageLoader):
""" Load .img image files (HotLeed format). """
extensions = ["img"]
def get_energy(self, image_path):
with open(image_path, "rb") as f:
return self.load_header(f)["Beam Voltage (eV)"]
@staticmethod
def load_header(f):
# find header length
line = f.readline()
while not b"Header length:" in line:
line = f.readline()
header_length = int(line.split(b": ")[1].strip())
# jump back to beginning
f.seek(0)
# read in header
header_raw = f.read(header_length)
## process header ##
# dict containing names of all interesting entrys
header = {b"Beam Voltage (eV)": 0, b"Date": "", b"Comment": "",
b"x1": 0, b"y1": 0, b"x2": 0, b"y2": 0, b"Number of frames": 0,
b'length' : header_length}
headerlines = header_raw.split(b"\n")
for line in headerlines:
parts = line.split(b": ")
if parts[0] in header.keys():
# convert int entrys
if type(header[parts[0]]) == type(1):
header[parts[0]] = int(parts[1])
# convert string entrys
elif type(header[parts[0]]) == type(""):
header[parts[0]] = parts[1].strip()
return header
@staticmethod
def get_image(image_path):
with open(image_path, "rb") as f:
header = ImgImageLoader.load_header(f)
# jump to begin of image
f.seek(header[b'length'])
# read in image
content = f.read()
# make numpy array from image
image = np.frombuffer(content, dtype=np.uint16)
# calculate size of image from header information
size = (header[b"y2"]-header[b"y1"]+1, header[b"x2"]-header[b"x1"]+1)
# reshape image as 2d array
image = image.reshape((size))
return image
[docs]class FitsImageLoader(ImageLoader):
""" Load .fits image files. """
extensions = ["fit", "fits"]
@staticmethod
def get_image(image_path):
hdulist = pyfits.open(image_path)
data = hdulist[0].data
hdulist.close()
return data
[docs]class PILImageLoader(ImageLoader):
""" Load image files supported by Python Imaging Library (PIL). """
extensions = ["tif", "tiff", "png", "jpg", "bmp"]
@staticmethod
def get_image(image_path):
im = Image.open(image_path)
sc = len(im.mode) == 1 or im.mode.find(';') == 1 # is the image single-channel?
return np.asarray(im if sc else im.convert('L'))
""" Dictionary of available ImageFormats. """
IMAGE_FORMATS = [format_ for format_ in \
[ImageFormat("PIL", PILImageLoader),
ImageFormat("FITS", FitsImageLoader),
ImageFormat("IMG", ImgImageLoader)] \
if format_.abbrev in formats_available]
class AllImageLoader(ImageLoader):
@staticmethod
def supported_extensions():
extensions = []
for image_format in IMAGE_FORMATS:
extensions.extend(image_format.extensions_wildcard())
return extensions
def get_image(self, image_path):
extension = os.path.splitext(image_path)[1][1:]
for image_format in IMAGE_FORMATS:
loader = image_format.loader
if extension in loader.extensions:
return loader.get_image(image_path)
raise IOError('The filetype is not supported')
[docs]def normalize255(array):
""" Returns a normalized array of uint8."""
nmin, nmax = array.min(), array.max()
if nmin:
array = array - nmin
scale = 255.0 / (nmax - nmin)
if scale != 1.0:
array = array * scale
return array.astype("uint8")
qtGreyColorTable = [qtgui.qRgb(i, i, i) for i in range(256)]
[docs]def npimage2qimage(npimage):
""" Converts numpy grayscale image to qimage."""
h, w = npimage.shape
npimage = normalize255(npimage)
# second w to avoid problems if image is not 32-bit aligned
# --> indicates bytesPerLine
qimage = qtgui.QImage(npimage.data, w, h, w, qtgui.QImage.Format_Indexed8)
qimage.setColorTable(qtGreyColorTable)
return qimage