Source code for auromat.util.image

# Copyright European Space Agency, 2013

from __future__ import absolute_import, division

import warnings
import os
from datetime import datetime

import numpy as np
import numpy.ma as ma
import skimage.io
import skimage.color
import exifread

from PIL import Image

[docs]def loadImage(imagePath): """ Return RGB image in native color range (e.g. [0,255] for uint8). Ignores the alpha channel. Note that this function is not meant for reading RAW files. Use `rawpy <https://pypi.python.org/pypi/rawpy>`_ with desired postprocessing settings instead. :param imagePath: :rtype: rgb array of shape (height,width,3) """ rgb = skimage.io.imread(imagePath) if rgb.ndim == 2: rgb = skimage.color.gray2rgb(rgb) # ignore alpha if available rgb = rgb[:,:,:3] assert rgb.ndim == 3 and rgb.shape[2] == 3,\ imagePath + '; wrong shape: ' + str(rgb.shape) return rgb
[docs]def saveImage(imagePath, im, **kw): """ :param imagePath: output path :param im: the image Additional optional keywords for JPEG: :param quality: 1 to 95 :param subsampling: '4:4:4', '4:2:2', or '4:1:1' """ if os.path.splitext(imagePath)[1].lower() in ['.jpeg', '.jpg']: # for jpg we use Pillow directly, as skimage curently doesn't allow # to set the JPEG quality im = Image.fromarray(im) im.save(imagePath, optimize=True, **kw) else: skimage.io.imsave(imagePath, im)
[docs]def croppedImage(im, divisible_by=16): """ Return image cropped to the next smaller width/height divisable by the given factor. Cropping is done on each side and will fail if the amount to crop cannot be divided by 2. """ cropped_height = int(im.shape[0]/divisible_by)*divisible_by cropped_width = int(im.shape[1]/divisible_by)*divisible_by assert (im.shape[0]-cropped_height) % 2 == 0 assert (im.shape[1]-cropped_width) % 2 == 0 vcrop = (im.shape[0]-cropped_height)//2 hcrop = (im.shape[1]-cropped_width)//2 im = im[vcrop:vcrop+cropped_height,hcrop:hcrop+cropped_width] return im
def _normalizeImage(rgb): """ Converts (masked) image to float [0,255] color range. If it is a masked image, then it is converted to a non-masked image with invalid entries replaced by nan. """ if rgb.dtype == np.uint8: pass elif rgb.dtype == np.uint16: rgb = rgb * (255/65535) else: raise NotImplementedError('Image format ' + str(rgb.dtype) + ' not supported') rgb = np.require(rgb, np.float) if ma.isMaskedArray(rgb): rgb = rgb.filled(np.nan) return rgb
[docs]def image2mpl(rgb): """ Converts image to [0,1] range suitable for matplotlib. """ rgb = _normalizeImage(rgb) return rgb / 255
[docs]def image2cv(rgb): """ Converts RGB image to BGR uint8 image suitable for use within OpenCV. """ import cv2 as cv rgb = _normalizeImage(rgb) rgb = np.require(rgb, np.uint8) bgr = cv.cvtColor(rgb, cv.COLOR_RGB2BGR) return bgr
[docs]def readExifTime(imagePath): """ Reads the date/time an image was taken from its EXIF header. If available, sub second time is used as the fractional part of the seconds. :type imagePath: string :rtype: datetime """ with open(imagePath, 'rb') as p: tags = exifread.process_file(p, details=False) timeTakenExif = tags['EXIF DateTimeOriginal'] subsec = tags.get('EXIF SubSecTimeOriginal', '0') return convertExifDate(str(timeTakenExif), str(subsec))
[docs]def convertExifDate(dateStr, subsecStr = None): """ Converts an EXIF date/time string to a datetime object. :param dateStr: date of format %Y:%m:%d %H:%M:%S :param subsecStr: float part of seconds as string, e.g. "05" :rtype: datetime.datetime """ if subsecStr is None: subsecStr = '0' else: # If there are no leading zeros, then exiftool returns an int # instead of a string. To support this case, we convert whatever # we get to a string. subsecStr = str(subsecStr) return datetime.strptime(dateStr + '.' + subsecStr, '%Y:%m:%d %H:%M:%S.%f')
[docs]def readFocalLength35mm(imagePath): """ Reads the focal length corresponding to 35mm film from the EXIF header of an image. :type imagePath: string :rtype: int|None """ with open(imagePath, 'rb') as p: tags = exifread.process_file(p, details=False, stop_tag='FocalLengthIn35mmFilm') try: # Note: FocalLength is a ratio while FocalLengthIn35mmFilm is an integer focalLength35mm = tags['EXIF FocalLengthIn35mmFilm'].values[0] except KeyError: return None return focalLength35mm
[docs]def readExposureTime(imagePath): """ Reads the exposure time in seconds from the EXIF header of an image. :type imagePath: string :rtype: float|None """ with open(imagePath, 'rb') as p: tags = exifread.process_file(p, details=False, stop_tag='ExposureTime') try: exposureTimeRatio = tags['EXIF ExposureTime'].values[0] exposureTime = exposureTimeRatio.num / exposureTimeRatio.den except KeyError: return None return exposureTime