From 91ca44b3a80cc128fdc0e315a7f182efba6e2f0e Mon Sep 17 00:00:00 2001 From: Stefan van der Walt Date: Thu, 13 May 2010 15:50:52 +0200 Subject: [PATCH] Add Zachary's freeimage wrapper. Fix Ctypes loader to use NumPy. --- CONTRIBUTORS.txt | 2 +- scikits/image/io/_plugins/freeimage_plugin.py | 380 ++++++++++++++++++ 2 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 scikits/image/io/_plugins/freeimage_plugin.py diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 6c8708d1..590da154 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -26,7 +26,7 @@ Reading of paletted images - Zachary Pincus - Tracing of low cost paths + Tracing of low cost paths, FreeImage I/O plugin - Almar Klein Binary heap class for graph algorithms diff --git a/scikits/image/io/_plugins/freeimage_plugin.py b/scikits/image/io/_plugins/freeimage_plugin.py new file mode 100644 index 00000000..2579936c --- /dev/null +++ b/scikits/image/io/_plugins/freeimage_plugin.py @@ -0,0 +1,380 @@ +import ctypes +import numpy +import sys +import os + +lib_dirs = ('.', + '/lib', + '/usr/lib', + '/usr/local/lib', + '/opt/local/lib', + ) + +_FI = None +for d in lib_dirs: + try: + _FI = numpy.ctypeslib.load_library('libFreeImage', d) + except OSError: + pass + +if not _FI: + raise OSError('Could not find libFreeImage in any of the following ' + 'directories: \'%s\'' % '\', \''.join(lib_dirs)) + +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) + +_FI.FreeImage_SetOutputMessage(_error_handler) + +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_NODATA = -1 + 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 + FIMD_CUSTOM = 10 + +def read(filename, flags=0): + """Read an image to a numpy array of shape (width, height) for greyscale + images, or shape (nchannels, width, height) for RGB or RGBA images.""" + bitmap = _read_bitmap(filename, flags) + try: + return _array_from_bitmap(bitmap) + finally: + _FI.FreeImage_Unload(bitmap) + +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 + (nchannels, width, height) for RGB or RGBA images. + + """ + 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) + if not multibitmap: + raise ValueError('Could not open %s as multi-page image.'%filename) + try: + pages = _FI.FreeImage_GetPageCount(multibitmap) + arrays = [] + for i in range(pages): + bitmap = _FI.FreeImage_LockPage(multibitmap, i) + try: + arrays.append(_array_from_bitmap(bitmap)) + finally: + _FI.FreeImage_UnlockPage(multibitmap, bitmap, False) + return arrays + finally: + _FI.FreeImage_CloseMultiBitmap(multibitmap, 0) + +def _read_bitmap(filename, flags): + """Load a file to a FreeImage bitmap pointer""" + ftype = _FI.FreeImage_GetFileType(str(filename), 0) + if ftype == -1: + raise ValueError('Cannot determine type of file %s'%filename) + bitmap = _FI.FreeImage_Load(ftype, filename, flags) + if not bitmap: + raise ValueError('Could not load file %s'%filename) + return bitmap + +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 + if len(shape) == 3 and _FI.FreeImage_IsLittleEndian() and \ + dtype.type == numpy.uint8: + b = array[0].copy() + array[0] = array[2] + array[2] = b + return array[...,::-1].copy() + +def string_tag(bitmap, key, model=METADATA_MODELS.FIMD_EXIF_MAIN): + """Retrieve the value of a metadata tag with the given string key as a + string.""" + tag = ctypes.c_int() + if not _FI.FreeImage_GetMetadata(model, bitmap, str(key), ctypes.byref(tag)): + return + char_ptr = ctypes.c_char * _FI.FreeImage_GetTagLength(tag) + return char_ptr.from_address(_FI.FreeImage_GetTagValue(tag)).raw() + +def write(array, filename, flags=0): + """Write a (width, height) or (nchannels, width, height) array to + a greyscale, RGB, or RGBA image, with file type deduced from the + filename. + + """ + filename = str(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 (nchannels, width, height) + arrays to a multipage greyscale, RGB, or RGBA image, with file type + deduced from the 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: + bitmap = _array_to_bitmap(array) + _FI.FreeImage_AppendPage(multibitmap, bitmap) + finally: + _FI.FreeImage_CloseMultiBitmap(multibitmap, flags) + +def _array_to_bitmap(array): + """Allocate a FreeImage bitmap and copy a numpy array into it. + + """ + shape = array.shape + dtype = array.dtype + if len(shape) == 2: + n_channels = 1 + 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.') + width, height = shape[-2:] + + itemsize = array.dtype.itemsize + bpp = 8 * itemsize * n_channels + bitmap = _FI.FreeImage_AllocateT(fi_type, width, height, bpp, 0, 0, 0) + if not bitmap: + raise RuntimeError('Could not allocate image for storage') + try: + wrapped_array = _wrap_bitmap_bits_in_array(bitmap, 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] = array[2,:,::-1] + wrapped_array[1] = array[1,:,::-1] + wrapped_array[2] = array[0,:,::-1] + if shape[0] == 4: + wrapped_array[3] = array[3,:,::-1] + else: + wrapped_array[:] = array[...,::-1] + + return bitmap, fi_type + except: + _FI.FreeImage_Unload(bitmap) + raise