import ctypes import numpy import sys import os import os.path from numpy.compat import asbytes def _load_library(libname, libdir): """Try to load a libray with the base name 'libname' from the directory 'libdir', checking for various common shared-lib file extensions. Uses windll to load libaries on windows machines, and cdll to load libraries on other platforms. Returns (library, errors), where library may or may not be None, and errors is a dict, potentially empty, mapping filenames to the ctypes errors raised trying to load those filenames. Non-extant paths are NOT added to the error dict.""" ext = os.path.splitext(libname)[1] if not ext: # Try to load library with platform-specific name, otherwise # default to libname.[so|pyd]. Sometimes, these files are built # erroneously on non-linux platforms. libname_ext = ['%s.so' % libname, '%s.pyd' % libname] if sys.platform == 'win32': # 'win32' is returned even on 64-bit win libname_ext.insert(0, '%s.dll' % libname) elif sys.platform == 'darwin': libname_ext.insert(0, '%s.dylib' % libname) else: libname_ext = [libname] library = None errors = {} lib_paths = [os.path.join(libdir, ln) for ln in libname_ext] lib_paths = [lp for lp in lib_paths if os.path.exists(lp)] if sys.platform == 'win32': loader = ctypes.windll else: loader = ctypes.cdll for lp in lib_paths: try: library = loader[lp] except Exception, e: errors[lp] = e return library, errors def load_freeimage(): lib_dirs = [os.path.dirname(__file__), '/lib', '/usr/lib', '/usr/local/lib', '/opt/local/lib', os.path.join(sys.prefix, 'lib'), os.path.join(sys.prefix, 'DLLs') ] if 'HOME' in os.environ: lib_dirs.append(os.path.join(os.environ['HOME'], 'lib')) lib_dirs = [ld for ld in lib_dirs if os.path.exists(ld)] freeimage = None errors = {} for d in lib_dirs: for libname in ('freeimage', 'FreeImage', 'libfreeimage', 'libFreeImage'): freeimage, new_errors = _load_library(libname, d) if freeimage: break errors.update(new_errors) if freeimage: break if freeimage: if sys.platform == 'win32': functype = ctypes.WINFUNCTYPE else: functype = ctypes.CFUNCTYPE @functype(None, ctypes.c_int, ctypes.c_char_p) def error_handler(fif, message): raise RuntimeError('FreeImage error: %s' % message) freeimage.FreeImage_SetOutputMessage(error_handler) elif errors: # No freeimage library found, but load-errors reported err_txt = ['%s:\n%s'%(pl, err.message) for pl, err in errors.items()] raise OSError('One or more FreeImage libraries were found, but could ' 'not be loaded due to the following errors:\n'+ '\n\n'.join(err_txt)) else: # No potential libraries found raise OSError('Could not find a FreeImage library in any of:\n'+ '\n'.join(lib_dirs)) return freeimage _FI = load_freeimage() API = { # All we're doing here is telling ctypes that some of the FreeImage # functions return pointers instead of integers. (On 64-bit systems, # without this information the pointers get truncated and crashes result). # There's no need to list functions that return ints, or the types of the # parameters to these or other functions -- that's fine to do implicitly. # Note that the ctypes immediately converts the returned void_p back to a # python int again! This is really not helpful, because then passing it # back to another library call will cause truncation-to-32-bits on 64-bit # systems. Thanks, ctypes! So after these calls one must immediately # re-wrap the int as a c_void_p if it is to be passed back into FreeImage. 'FreeImage_AllocateT': (ctypes.c_void_p, None), 'FreeImage_FindFirstMetadata': (ctypes.c_void_p, None), 'FreeImage_GetBits': (ctypes.c_void_p, None), 'FreeImage_GetPalette': (ctypes.c_void_p, None), 'FreeImage_GetTagKey': (ctypes.c_char_p, None), 'FreeImage_GetTagValue': (ctypes.c_void_p, None), 'FreeImage_Load': (ctypes.c_void_p, None), 'FreeImage_LockPage': (ctypes.c_void_p, None), 'FreeImage_OpenMultiBitmap': (ctypes.c_void_p, None) } # Albert's ctypes pattern def register_api(lib,api): for f, (restype, argtypes) in api.items(): func = getattr(lib, f) func.restype = restype func.argtypes = argtypes register_api(_FI, API) class FI_TYPES(object): FIT_UNKNOWN = 0 FIT_BITMAP = 1 FIT_UINT16 = 2 FIT_INT16 = 3 FIT_UINT32 = 4 FIT_INT32 = 5 FIT_FLOAT = 6 FIT_DOUBLE = 7 FIT_COMPLEX = 8 FIT_RGB16 = 9 FIT_RGBA16 = 10 FIT_RGBF = 11 FIT_RGBAF = 12 dtypes = { FIT_BITMAP: numpy.uint8, FIT_UINT16: numpy.uint16, FIT_INT16: numpy.int16, FIT_UINT32: numpy.uint32, FIT_INT32: numpy.int32, FIT_FLOAT: numpy.float32, FIT_DOUBLE: numpy.float64, FIT_COMPLEX: numpy.complex128, FIT_RGB16: numpy.uint16, FIT_RGBA16: numpy.uint16, FIT_RGBF: numpy.float32, FIT_RGBAF: numpy.float32 } fi_types = { (numpy.uint8, 1): FIT_BITMAP, (numpy.uint8, 3): FIT_BITMAP, (numpy.uint8, 4): FIT_BITMAP, (numpy.uint16, 1): FIT_UINT16, (numpy.int16, 1): FIT_INT16, (numpy.uint32, 1): FIT_UINT32, (numpy.int32, 1): FIT_INT32, (numpy.float32, 1): FIT_FLOAT, (numpy.float64, 1): FIT_DOUBLE, (numpy.complex128, 1): FIT_COMPLEX, (numpy.uint16, 3): FIT_RGB16, (numpy.uint16, 4): FIT_RGBA16, (numpy.float32, 3): FIT_RGBF, (numpy.float32, 4): FIT_RGBAF } extra_dims = { FIT_UINT16: [], FIT_INT16: [], FIT_UINT32: [], FIT_INT32: [], FIT_FLOAT: [], FIT_DOUBLE: [], FIT_COMPLEX: [], FIT_RGB16: [3], FIT_RGBA16: [4], FIT_RGBF: [3], FIT_RGBAF: [4] } @classmethod def get_type_and_shape(cls, bitmap): w = _FI.FreeImage_GetWidth(bitmap) h = _FI.FreeImage_GetHeight(bitmap) fi_type = _FI.FreeImage_GetImageType(bitmap) if not fi_type: raise ValueError('Unknown image pixel type') dtype = cls.dtypes[fi_type] if fi_type == cls.FIT_BITMAP: bpp = _FI.FreeImage_GetBPP(bitmap) if bpp == 8: extra_dims = [] elif bpp == 24: extra_dims = [3] elif bpp == 32: extra_dims = [4] else: raise ValueError('Cannot convert %d BPP bitmap' % bpp) else: extra_dims = cls.extra_dims[fi_type] return numpy.dtype(dtype), extra_dims + [w, h] class IO_FLAGS(object): #Bmp BMP_DEFAULT = 0 BMP_SAVE_RLE = 1 #Png PNG_DEFAULT = 0 PNG_IGNOREGAMMA = 1 #Gif GIF_DEFAULT = 0 GIF_LOAD256 = 1 GIF_PLAYBACK = 2 #Ico ICO_DEFAULT = 0 ICO_MAKEALPHA = 1 #Tiff TIFF_DEFAULT = 0 TIFF_CMYK = 0x0001 TIFF_NONE = 0x0800 TIFF_PACKBITS = 0x0100 TIFF_DEFLATE = 0x0200 TIFF_ADOBE_DEFLATE = 0x0400 TIFF_CCITTFAX3 = 0x1000 TIFF_CCITTFAX4 = 0x2000 TIFF_LZW = 0x4000 TIFF_JPEG = 0x8000 #Jpeg JPEG_DEFAULT = 0 JPEG_FAST = 1 JPEG_ACCURATE = 2 JPEG_QUALITYSUPERB = 0x80 JPEG_QUALITYGOOD = 0x100 JPEG_QUALITYNORMAL = 0x200 JPEG_QUALITYAVERAGE = 0x400 JPEG_QUALITYBAD = 0x800 JPEG_CMYK = 0x1000 JPEG_PROGRESSIVE = 0x2000 #Others... CUT_DEFAULT = 0 DDS_DEFAULT = 0 HDR_DEFAULT = 0 IFF_DEFAULT = 0 KOALA_DEFAULT = 0 LBM_DEFAULT = 0 MNG_DEFAULT = 0 PCD_DEFAULT = 0 PCD_BASE = 1 PCD_BASEDIV4 = 2 PCD_BASEDIV16 = 3 PCX_DEFAULT = 0 PNM_DEFAULT = 0 PNM_SAVE_RAW = 0 PNM_SAVE_ASCII = 1 PSD_DEFAULT = 0 RAS_DEFAULT = 0 TARGA_DEFAULT = 0 TARGA_LOAD_RGB888 = 1 WBMP_DEFAULT = 0 XBM_DEFAULT = 0 class METADATA_MODELS(object): FIMD_COMMENTS = 0 FIMD_EXIF_MAIN = 1 FIMD_EXIF_EXIF = 2 FIMD_EXIF_GPS = 3 FIMD_EXIF_MAKERNOTE = 4 FIMD_EXIF_INTEROP = 5 FIMD_IPTC = 6 FIMD_XMP = 7 FIMD_GEOTIFF = 8 FIMD_ANIMATION = 9 class METADATA_DATATYPE(object): FIDT_BYTE = 1 # 8-bit unsigned integer FIDT_ASCII = 2 # 8-bit bytes w/ last byte null FIDT_SHORT = 3 # 16-bit unsigned integer FIDT_LONG = 4 # 32-bit unsigned integer FIDT_RATIONAL = 5 # 64-bit unsigned fraction FIDT_SBYTE = 6 # 8-bit signed integer FIDT_UNDEFINED = 7 # 8-bit untyped data FIDT_SSHORT = 8 # 16-bit signed integer FIDT_SLONG = 9 # 32-bit signed integer FIDT_SRATIONAL = 10 # 64-bit signed fraction FIDT_FLOAT = 11 # 32-bit IEEE floating point FIDT_DOUBLE = 12 # 64-bit IEEE floating point FIDT_IFD = 13 # 32-bit unsigned integer (offset) FIDT_PALETTE = 14 # 32-bit RGBQUAD dtypes = { FIDT_BYTE: numpy.uint8, FIDT_SHORT: numpy.uint16, FIDT_LONG: numpy.uint32, FIDT_RATIONAL: [('numerator', numpy.uint32), ('denominator', numpy.uint32)], FIDT_SBYTE: numpy.int8, FIDT_UNDEFINED: numpy.uint8, FIDT_SSHORT: numpy.int16, FIDT_SLONG: numpy.int32, FIDT_SRATIONAL: [('numerator', numpy.int32), ('denominator', numpy.int32)], FIDT_FLOAT: numpy.float32, FIDT_DOUBLE: numpy.float64, FIDT_IFD: numpy.uint32, FIDT_PALETTE: [('R', numpy.uint8), ('G', numpy.uint8), ('B', numpy.uint8), ('A', numpy.uint8)] } def _process_bitmap(filename, flags, process_func): filename = asbytes(filename) ftype = _FI.FreeImage_GetFileType(filename, 0) if ftype == -1: raise ValueError('Cannot determine type of file %s' % filename) bitmap = _FI.FreeImage_Load(ftype, filename, flags) bitmap = ctypes.c_void_p(bitmap) if not bitmap: raise ValueError('Could not load file %s' % filename) try: return process_func(bitmap) finally: _FI.FreeImage_Unload(bitmap) def read(filename, flags=0): """Read an image to a numpy array of shape (width, height) for greyscale images, or shape (width, height, nchannels) for RGB or RGBA images. """ return _process_bitmap(filename, flags, _array_from_bitmap) def read_metadata(filename, flags=0): """Return a dict containing all image metadata. Returned dict maps (metadata_model, tag_name) keys to tag values, where metadata_model is a string name based on the FreeImage "metadata models" defined in the class METADATA_MODELS. """ return _process_bitmap(filename, flags, _read_metadata) def _process_multipage(filename, flags, process_func): filename = asbytes(filename) ftype = _FI.FreeImage_GetFileType(filename, 0) if ftype == -1: raise ValueError('Cannot determine type of file %s' % filename) create_new = False read_only = True keep_cache_in_memory = True multibitmap = _FI.FreeImage_OpenMultiBitmap(ftype, filename, create_new, read_only, keep_cache_in_memory, flags) multibitmap = ctypes.c_void_p(multibitmap) if not multibitmap: raise ValueError('Could not open %s as multi-page image.' % filename) try: pages = _FI.FreeImage_GetPageCount(multibitmap) out = [] for i in range(pages): bitmap = _FI.FreeImage_LockPage(multibitmap, i) bitmap = ctypes.c_void_p(bitmap) if not bitmap: raise ValueError('Could not open %s as a multi-page image.' % filename) try: out.append(process_func(bitmap)) finally: _FI.FreeImage_UnlockPage(multibitmap, bitmap, False) return out finally: _FI.FreeImage_CloseMultiBitmap(multibitmap, 0) def read_multipage(filename, flags=0): """Read a multipage image to a list of numpy arrays, where each array is of shape (width, height) for greyscale images, or shape (width, height, nchannels) for RGB or RGBA images. """ return _process_multipage(filename, flags, _array_from_bitmap) def read_multipage_metadata(filename, flags=0): """Read a multipage image to a list of metadata dicts, one dict for each page. The dict format is as in read_metadata(). """ return _process_multipage(filename, flags, _read_metadata) def _wrap_bitmap_bits_in_array(bitmap, shape, dtype): """Return an ndarray view on the data in a FreeImage bitmap. Only valid for as long as the bitmap is loaded (if single page) / locked in memory (if multipage). """ pitch = _FI.FreeImage_GetPitch(bitmap) height = shape[-1] byte_size = height * pitch itemsize = dtype.itemsize if len(shape) == 3: strides = (itemsize, shape[0]*itemsize, pitch) else: strides = (itemsize, pitch) bits = _FI.FreeImage_GetBits(bitmap) array = numpy.ndarray(shape, dtype=dtype, buffer=(ctypes.c_char*byte_size).from_address(bits), strides=strides) return array def _array_from_bitmap(bitmap): """Convert a FreeImage bitmap pointer to a numpy array. """ dtype, shape = FI_TYPES.get_type_and_shape(bitmap) array = _wrap_bitmap_bits_in_array(bitmap, shape, dtype) # swizzle the color components and flip the scanlines to go from # FreeImage's BGR[A] and upside-down internal memory format to something # more normal def n(arr): return arr[..., ::-1].T if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ dtype.type == numpy.uint8: b = n(array[0]) g = n(array[1]) r = n(array[2]) if shape[0] == 3: return numpy.dstack( (r, g, b) ) elif shape[0] == 4: a = n(array[3]) return numpy.dstack( (r, g, b, a) ) else: raise ValueError('Cannot handle images of shape %s' % shape) # We need to copy because array does *not* own its memory # after bitmap is freed. return n(array).copy() def _read_metadata(bitmap): metadata = {} models = [(name[5:], number) for name, number in METADATA_MODELS.__dict__.items() if name.startswith('FIMD_')] tag = ctypes.c_void_p() for model_name, number in models: mdhandle = _FI.FreeImage_FindFirstMetadata(number, bitmap, ctypes.byref(tag)) mdhandle = ctypes.c_void_p(mdhandle) if mdhandle: more = True while more: tag_name = str(_FI.FreeImage_GetTagKey(tag)) tag_type = _FI.FreeImage_GetTagType(tag) byte_size = _FI.FreeImage_GetTagLength(tag) char_ptr = ctypes.c_char * byte_size tag_str = char_ptr.from_address(_FI.FreeImage_GetTagValue(tag)) if tag_type == METADATA_DATATYPE.FIDT_ASCII: tag_val = str(tag_str.value) else: tag_val = numpy.fromstring(tag_str, dtype=METADATA_DATATYPE.dtypes[tag_type]) if len(tag_val) == 1: tag_val = tag_val[0] metadata[(model_name, tag_name)] = tag_val more = _FI.FreeImage_FindNextMetadata(mdhandle, ctypes.byref(tag)) _FI.FreeImage_FindCloseMetadata(mdhandle) return metadata def write(array, filename, flags=0): """Write a (width, height) or (width, height, nchannels) array to a greyscale, RGB, or RGBA image, with file type deduced from the filename. """ array = numpy.asarray(array) filename = asbytes(filename) ftype = _FI.FreeImage_GetFIFFromFilename(filename) if ftype == -1: raise ValueError('Cannot determine type for %s' % filename) bitmap, fi_type = _array_to_bitmap(array) try: if fi_type == FI_TYPES.FIT_BITMAP: can_write = _FI.FreeImage_FIFSupportsExportBPP(ftype, _FI.FreeImage_GetBPP(bitmap)) else: can_write = _FI.FreeImage_FIFSupportsExportType(ftype, fi_type) if not can_write: raise TypeError('Cannot save image of this format ' 'to this file type') res = _FI.FreeImage_Save(ftype, bitmap, filename, flags) if not res: raise RuntimeError('Could not save image properly.') finally: _FI.FreeImage_Unload(bitmap) def write_multipage(arrays, filename, flags=0): """Write a list of (width, height) or (width, height, nchannels) arrays to a multipage greyscale, RGB, or RGBA image, with file type deduced from the filename. """ filename = asbytes(filename) ftype = _FI.FreeImage_GetFIFFromFilename(filename) if ftype == -1: raise ValueError('Cannot determine type of file %s' % filename) create_new = True read_only = False keep_cache_in_memory = True multibitmap = _FI.FreeImage_OpenMultiBitmap(ftype, filename, create_new, read_only, keep_cache_in_memory, 0) if not multibitmap: raise ValueError('Could not open %s for writing multi-page image.' % filename) try: for array in arrays: array = numpy.asarray(array) bitmap, fi_type = _array_to_bitmap(array) _FI.FreeImage_AppendPage(multibitmap, bitmap) finally: _FI.FreeImage_CloseMultiBitmap(multibitmap, flags) # 4-byte quads of 0,v,v,v from 0,0,0,0 to 0,255,255,255 _GREY_PALETTE = numpy.arange(0, 0x01000000, 0x00010101, dtype=numpy.uint32) def _array_to_bitmap(array): """Allocate a FreeImage bitmap and copy a numpy array into it. """ shape = array.shape dtype = array.dtype r,c = shape[:2] if len(shape) == 2: n_channels = 1 w_shape = (c,r) elif len(shape) == 3: n_channels = shape[2] w_shape = (n_channels,c,r) else: n_channels = shape[0] try: fi_type = FI_TYPES.fi_types[(dtype.type, n_channels)] except KeyError: raise ValueError('Cannot write arrays of given type and shape.') itemsize = array.dtype.itemsize bpp = 8 * itemsize * n_channels bitmap = _FI.FreeImage_AllocateT(fi_type, c, r, bpp, 0, 0, 0) bitmap = ctypes.c_void_p(bitmap) if not bitmap: raise RuntimeError('Could not allocate image for storage') try: def n(arr): # normalise to freeimage's in-memory format return arr.T[:,::-1] wrapped_array = _wrap_bitmap_bits_in_array(bitmap, w_shape, dtype) # swizzle the color components and flip the scanlines to go to # FreeImage's BGR[A] and upside-down internal memory format if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ dtype.type == numpy.uint8: wrapped_array[0] = n(array[:,:,2]) wrapped_array[1] = n(array[:,:,1]) wrapped_array[2] = n(array[:,:,0]) if shape[2] == 4: wrapped_array[3] = n(array[:,:,3]) else: wrapped_array[:] = n(array) if len(shape) == 2 and dtype.type == numpy.uint8: palette = _FI.FreeImage_GetPalette(bitmap) palette = ctypes.c_void_p(palette) if not palette: raise RuntimeError('Could not get image palette') ctypes.memmove(palette, _GREY_PALETTE.ctypes.data, 1024) return bitmap, fi_type except: _FI.FreeImage_Unload(bitmap) raise def imread(filename): """ img = imread(filename) Reads an image from file `filename` Parameters ---------- filename : file name Returns ------- img : ndarray """ img = read(filename) return img def imsave(filename, img): ''' imsave(filename, img) Save image to disk Image type is inferred from filename Parameters ---------- filename : file name img : image to be saved as nd array ''' write(img, filename)