mirror of
https://github.com/wassname/scikit-image.git
synced 2026-06-29 17:53:25 +08:00
532 lines
16 KiB
Python
532 lines
16 KiB
Python
import ctypes
|
|
import numpy
|
|
import sys
|
|
import os
|
|
import os.path
|
|
from numpy.compat import asbytes
|
|
|
|
def _load_library(libname, loader_path):
|
|
""" A small fork of numpy.ctypeslib.load_library
|
|
to support windll.
|
|
"""
|
|
if ctypes.__version__ < '1.0.1':
|
|
import warnings
|
|
warnings.warn("All features of ctypes interface may not work " \
|
|
"with ctypes < 1.0.1")
|
|
|
|
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':
|
|
libname_ext.insert(0, '%s.dll' % libname)
|
|
elif sys.platform == 'darwin':
|
|
libname_ext.insert(0, '%s.dylib' % libname)
|
|
else:
|
|
libname_ext = [libname]
|
|
|
|
loader_path = os.path.abspath(loader_path)
|
|
if not os.path.isdir(loader_path):
|
|
libdir = os.path.dirname(loader_path)
|
|
else:
|
|
libdir = loader_path
|
|
for ln in libname_ext:
|
|
try:
|
|
libpath = os.path.join(libdir, ln)
|
|
if sys.platform == 'win32':
|
|
return ctypes.windll[libpath]
|
|
else:
|
|
return ctypes.cdll[libpath]
|
|
except OSError, e:
|
|
pass
|
|
|
|
raise e
|
|
|
|
lib_dirs = [os.path.dirname(__file__),
|
|
'/lib',
|
|
'/usr/lib',
|
|
'/usr/local/lib',
|
|
'/opt/local/lib',
|
|
]
|
|
|
|
if 'HOME' in os.environ:
|
|
lib_dirs.append(os.path.join(os.environ['HOME'], 'lib'))
|
|
|
|
API = {
|
|
'FreeImage_Load': (ctypes.c_void_p,
|
|
[ctypes.c_int, ctypes.c_char_p, ctypes.c_int]),
|
|
'FreeImage_GetWidth': (ctypes.c_uint,
|
|
[ctypes.c_void_p]),
|
|
'FreeImage_GetHeight': (ctypes.c_uint,
|
|
[ctypes.c_void_p]),
|
|
'FreeImage_GetImageType': (ctypes.c_uint,
|
|
[ctypes.c_void_p]),
|
|
'FreeImage_GetBPP': (ctypes.c_uint,
|
|
[ctypes.c_void_p]),
|
|
'FreeImage_GetPitch': (ctypes.c_uint,
|
|
[ctypes.c_void_p]),
|
|
'FreeImage_GetBits': (ctypes.c_void_p,
|
|
[ctypes.c_void_p]),
|
|
}
|
|
|
|
# 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
|
|
|
|
_FI = None
|
|
for d in lib_dirs:
|
|
for libname in ('freeimage', 'FreeImage',
|
|
'libfreeimage', 'libFreeImage'):
|
|
try:
|
|
_FI = _load_library(libname, d)
|
|
except OSError:
|
|
pass
|
|
else:
|
|
break
|
|
|
|
if _FI is not None:
|
|
break
|
|
|
|
if not _FI:
|
|
raise OSError('Could not find libFreeImage in any of the following '
|
|
'directories: \'%s\'' % '\', \''.join(lib_dirs))
|
|
|
|
register_api(_FI, API)
|
|
|
|
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 (width, height, nchannels) 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.
|
|
|
|
"""
|
|
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)
|
|
if not multibitmap:
|
|
raise ValueError('Could not open %s as multi-page image.' % filename)
|
|
try:
|
|
multibitmap = ctypes.c_void_p(multibitmap)
|
|
pages = _FI.FreeImage_GetPageCount(multibitmap)
|
|
arrays = []
|
|
for i in range(pages):
|
|
bitmap = _FI.FreeImage_LockPage(multibitmap, i)
|
|
bitmap = ctypes.c_void_p(bitmap)
|
|
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"""
|
|
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)
|
|
if not bitmap:
|
|
raise ValueError('Could not load file %s' % filename)
|
|
return ctypes.c_void_p(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
|
|
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 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 (width, height, nchannels) array to
|
|
a 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 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.
|
|
|
|
"""
|
|
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:
|
|
multibitmap = ctypes.c_void_p(multibitmap)
|
|
for array in arrays:
|
|
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)
|
|
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]
|
|
bitmap = ctypes.c_void_p(bitmap)
|
|
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)
|
|
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, as_grey=False):
|
|
"""
|
|
img = imread(filename, as_grey=False)
|
|
|
|
Reads an image from file `filename`
|
|
|
|
Parameters
|
|
----------
|
|
filename : file name
|
|
as_grey : Whether to convert to grey scale image (default: no)
|
|
Returns
|
|
-------
|
|
img : ndarray
|
|
"""
|
|
img = read(filename)
|
|
if as_grey and len(img) == 3:
|
|
# these are the values that wikipedia says are typical
|
|
transform = numpy.array([ 0.30, 0.59, 0.11])
|
|
return numpy.dot(img, transform)
|
|
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)
|