:py:mod:`kwimage.im_io` ======================= .. py:module:: kwimage.im_io .. autoapi-nested-parse:: This module provides functions ``imread`` and ``imwrite`` which are wrappers around concrete readers/writers provided by other libraries. This allows us to support a wider array of formats than any of individual backends. Module Contents --------------- Functions ~~~~~~~~~ .. autoapisummary:: kwimage.im_io.imread kwimage.im_io.imwrite kwimage.im_io.load_image_shape .. py:function:: imread(fpath, space='auto', backend='auto') Reads image data in a specified format using some backend implementation. :Parameters: * **fpath** (*str*) -- path to the file to be read * **space** (*str, default='auto'*) -- The desired colorspace of the image. Can by any colorspace accepted by `convert_colorspace`, or it can be 'auto', in which case the colorspace of the image is unmodified (except in the case where a color image is read by opencv, in which case we convert BGR to RGB by default). If None, then no modification is made to whatever backend is used to read the image. New in version 0.7.10: when the backend does not resolve to "cv2" the "auto" space resolves to None, thus the image is read as-is. * **backend** (*str, default='auto'*) -- which backend reader to use. By default the file extension is used to determine this, but it can be manually overridden. Valid backends are 'gdal', 'skimage', 'itk', and 'cv2'. :returns: the image data in the specified color space. :rtype: ndarray .. note:: if space is something non-standard like HSV or LAB, then the file must be a normal 8-bit color image, otherwise an error will occur. :raises IOError - If the image cannot be read: :raises ImportError - If trying to read a nitf without gdal: :raises NotImplementedError - if trying to read a corner-case image: .. rubric:: Example >>> # xdoctest: +REQUIRES(--network) >>> from kwimage.im_io import * # NOQA >>> import tempfile >>> from os.path import splitext # NOQA >>> # Test a non-standard image, which encodes a depth map >>> fpath = ub.grabdata( >>> 'http://www.topcoder.com/contest/problem/UrbanMapper3D/JAX_Tile_043_DTM.tif', >>> hasher='sha256', hash_prefix='64522acba6f0fb7060cd4c202ed32c5163c34e63d386afdada4190cce51ff4d4') >>> img1 = imread(fpath) >>> # Check that write + read preserves data >>> tmp = tempfile.NamedTemporaryFile(suffix=splitext(fpath)[1]) >>> imwrite(tmp.name, img1) >>> img2 = imread(tmp.name) >>> assert np.all(img2 == img1) >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> kwplot.imshow(img1, pnum=(1, 2, 1), fnum=1, norm=True) >>> kwplot.imshow(img2, pnum=(1, 2, 2), fnum=1, norm=True) .. rubric:: Example >>> # xdoctest: +REQUIRES(--network) >>> import tempfile >>> img1 = imread(ub.grabdata( >>> 'http://i.imgur.com/iXNf4Me.png', fname='ada.png', hasher='sha256', >>> hash_prefix='898cf2588c40baf64d6e09b6a93b4c8dcc0db26140639a365b57619e17dd1c77')) >>> tmp_tif = tempfile.NamedTemporaryFile(suffix='.tif') >>> tmp_png = tempfile.NamedTemporaryFile(suffix='.png') >>> imwrite(tmp_tif.name, img1) >>> imwrite(tmp_png.name, img1) >>> tif_im = imread(tmp_tif.name) >>> png_im = imread(tmp_png.name) >>> assert np.all(tif_im == png_im) >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> kwplot.imshow(png_im, pnum=(1, 2, 1), fnum=1) >>> kwplot.imshow(tif_im, pnum=(1, 2, 2), fnum=1) .. rubric:: Example >>> # xdoctest: +REQUIRES(--network) >>> import tempfile >>> tif_fpath = ub.grabdata( >>> 'https://ghostscript.com/doc/tiff/test/images/rgb-3c-16b.tiff', >>> fname='pepper.tif', hasher='sha256', >>> hash_prefix='31ff3a1f416cb7281acfbcbb4b56ee8bb94e9f91489602ff2806e5a49abc03c0') >>> img1 = imread(tif_fpath) >>> tmp_tif = tempfile.NamedTemporaryFile(suffix='.tif') >>> tmp_png = tempfile.NamedTemporaryFile(suffix='.png') >>> imwrite(tmp_tif.name, img1) >>> imwrite(tmp_png.name, img1) >>> tif_im = imread(tmp_tif.name) >>> png_im = imread(tmp_png.name) >>> assert np.all(tif_im == png_im) >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> kwplot.imshow(png_im / 2 ** 16, pnum=(1, 2, 1), fnum=1) >>> kwplot.imshow(tif_im / 2 ** 16, pnum=(1, 2, 2), fnum=1) .. rubric:: Example >>> # xdoctest: +REQUIRES(module:itk, --network) >>> import kwimage >>> import ubelt as ub >>> # Grab an image that ITK can read >>> fpath = ub.grabdata( >>> url='https://data.kitware.com/api/v1/file/606754e32fa25629b9476f9e/download', >>> fname='brainweb1e5a10f17Rot20Tx20.mha', >>> hash_prefix='08f0812591691ae24a29788ba8cd1942e91', hasher='sha512') >>> # Read the image (this is actually a DxHxW stack of images) >>> img1_stack = kwimage.imread(fpath) >>> # Check that write + read preserves data >>> import tempfile >>> tmp_file = tempfile.NamedTemporaryFile(suffix='.mha') >>> kwimage.imwrite(tmp_file.name, img1_stack) >>> recon = kwimage.imread(tmp_file.name) >>> assert not np.may_share_memory(recon, img1_stack) >>> assert np.all(recon == img1_stack) >>> # xdoctest: +REQUIRES(--show) >>> import kwplot >>> kwplot.autompl() >>> kwplot.imshow(kwimage.stack_images_grid(recon[0::20])) >>> kwplot.show_if_requested() Benchmark: >>> from kwimage.im_io import * # NOQA >>> import timerit >>> import kwimage >>> import tempfile >>> # >>> dsize = (1920, 1080) >>> img1 = kwimage.grab_test_image('amazon', dsize=dsize) >>> ti = timerit.Timerit(10, bestof=3, verbose=1, unit='us') >>> formats = {} >>> dpath = ub.ensure_app_cache_dir('cache') >>> space = 'auto' >>> formats['png'] = kwimage.imwrite(join(dpath, '.png'), img1, space=space, backend='cv2') >>> formats['jpg'] = kwimage.imwrite(join(dpath, '.jpg'), img1, space=space, backend='cv2') >>> formats['tif_raw'] = kwimage.imwrite(join(dpath, '.raw.tif'), img1, space=space, backend='gdal', compress='RAW') >>> formats['tif_deflate'] = kwimage.imwrite(join(dpath, '.deflate.tif'), img1, space=space, backend='gdal', compress='DEFLATE') >>> formats['tif_lzw'] = kwimage.imwrite(join(dpath, '.lzw.tif'), img1, space=space, backend='gdal', compress='LZW') >>> grid = [ >>> ('cv2', 'png'), >>> ('cv2', 'jpg'), >>> ('gdal', 'jpg'), >>> ('turbojpeg', 'jpg'), >>> ('gdal', 'tif_raw'), >>> ('gdal', 'tif_lzw'), >>> ('gdal', 'tif_deflate'), >>> ('skimage', 'tif_raw'), >>> ] >>> backend, filefmt = 'cv2', 'png' >>> for backend, filefmt in grid: >>> for timer in ti.reset(f'imread-{filefmt}-{backend}'): >>> with timer: >>> kwimage.imread(formats[filefmt], space=space, backend=backend) >>> # Test all formats in auto mode >>> for filefmt in formats.keys(): >>> for timer in ti.reset(f'kwimage.imread-{filefmt}-auto'): >>> with timer: >>> kwimage.imread(formats[filefmt], space=space, backend='auto') >>> ti.measures = ub.map_vals(ub.sorted_vals, ti.measures) >>> import netharn as nh >>> print('ti.measures = {}'.format(nh.util.align(ub.repr2(ti.measures['min'], nl=2), ':'))) Timed best=42891.504 µs, mean=44008.439 ± 1409.2 µs for imread-png-cv2 Timed best=33146.808 µs, mean=34185.172 ± 656.3 µs for imread-jpg-cv2 Timed best=40120.306 µs, mean=41220.927 ± 1010.9 µs for imread-jpg-gdal Timed best=30798.162 µs, mean=31573.070 ± 737.0 µs for imread-jpg-turbojpeg Timed best=6223.170 µs, mean=6370.462 ± 150.7 µs for imread-tif_raw-gdal Timed best=42459.404 µs, mean=46519.940 ± 5664.9 µs for imread-tif_lzw-gdal Timed best=36271.175 µs, mean=37301.108 ± 861.1 µs for imread-tif_deflate-gdal Timed best=5239.503 µs, mean=6566.574 ± 1086.2 µs for imread-tif_raw-skimage ti.measures = { 'imread-tif_raw-skimage' : 0.0052395030070329085, 'imread-tif_raw-gdal' : 0.006223169999429956, 'imread-jpg-turbojpeg' : 0.030798161998973228, 'imread-jpg-cv2' : 0.03314680799667258, 'imread-tif_deflate-gdal': 0.03627117499127053, 'imread-jpg-gdal' : 0.040120305988239124, 'imread-tif_lzw-gdal' : 0.042459404008695856, 'imread-png-cv2' : 0.042891503995633684, } >>> print('ti.measures = {}'.format(nh.util.align(ub.repr2(ti.measures['mean'], nl=2), ':'))) .. py:function:: imwrite(fpath, image, space='auto', backend='auto', **kwargs) Writes image data to disk. :Parameters: * **fpath** (*PathLike*) -- location to save the image * **image** (*ndarray*) -- image data * **space** (*str | None, default='auto'*) -- the colorspace of the image to save. Can by any colorspace accepted by `convert_colorspace`, or it can be 'auto', in which case we assume the input image is either RGB, RGBA or grayscale. If None, then absolutely no color modification is made and whatever backend is used writes the image as-is. New in version 0.7.10: when the backend does not resolve to "cv2", the "auto" space resolves to None, thus the image is saved as-is. * **backend** (*str, default='auto'*) -- which backend writer to use. By default the file extension is used to determine this. Valid backends are 'gdal', 'skimage', 'itk', and 'cv2'. * **\*\*kwargs** -- args passed to the backend writer :returns: path to the written file :rtype: str .. rubric:: Notes The image may be modified to preserve its colorspace depending on which backend is used to write the image. When saving as a jpeg or png, the image must be encoded with the uint8 data type. When saving as a tiff, any data type is allowed. :raises Exception: if the image cannot be written Doctest: >>> # xdoctest: +REQUIRES(--network) >>> # This should be moved to a unit test >>> import tempfile >>> test_image_paths = [ >>> ub.grabdata('https://ghostscript.com/doc/tiff/test/images/rgb-3c-16b.tiff', fname='pepper.tif'), >>> ub.grabdata('http://i.imgur.com/iXNf4Me.png', fname='ada.png'), >>> #ub.grabdata('http://www.topcoder.com/contest/problem/UrbanMapper3D/JAX_Tile_043_DTM.tif'), >>> ub.grabdata('https://upload.wikimedia.org/wikipedia/commons/f/fa/Grayscale_8bits_palette_sample_image.png', fname='parrot.png') >>> ] >>> for fpath in test_image_paths: >>> for space in ['auto', 'rgb', 'bgr', 'gray', 'rgba']: >>> img1 = imread(fpath, space=space) >>> print('Test im-io consistency of fpath = {!r} in {} space, shape={}'.format(fpath, space, img1.shape)) >>> # Write the image in TIF and PNG format >>> tmp_tif = tempfile.NamedTemporaryFile(suffix='.tif') >>> tmp_png = tempfile.NamedTemporaryFile(suffix='.png') >>> imwrite(tmp_tif.name, img1, space=space, backend='skimage') >>> imwrite(tmp_png.name, img1, space=space) >>> tif_im = imread(tmp_tif.name, space=space) >>> png_im = imread(tmp_png.name, space=space) >>> assert np.all(tif_im == png_im), 'im-read/write inconsistency' >>> if _have_gdal: >>> tmp_tif2 = tempfile.NamedTemporaryFile(suffix='.tif') >>> imwrite(tmp_tif2.name, img1, space=space, backend='gdal') >>> tif_im2 = imread(tmp_tif2.name, space=space) >>> assert np.all(tif_im == tif_im2), 'im-read/write inconsistency' >>> if space == 'gray': >>> assert tif_im.ndim == 2 >>> assert png_im.ndim == 2 >>> elif space in ['rgb', 'bgr']: >>> assert tif_im.shape[2] == 3 >>> assert png_im.shape[2] == 3 >>> elif space in ['rgba', 'bgra']: >>> assert tif_im.shape[2] == 4 >>> assert png_im.shape[2] == 4 Benchmark: >>> import timerit >>> import os >>> import kwimage >>> import tempfile >>> # >>> img1 = kwimage.grab_test_image('astro', dsize=(1920, 1080)) >>> space = 'auto' >>> # >>> file_sizes = {} >>> # >>> ti = timerit.Timerit(10, bestof=3, verbose=2) >>> # >>> for timer in ti.reset('imwrite-skimage-tif'): >>> with timer: >>> tmp = tempfile.NamedTemporaryFile(suffix='.tif') >>> kwimage.imwrite(tmp.name, img1, space=space, backend='skimage') >>> file_sizes[ti.label] = os.stat(tmp.name).st_size >>> # >>> for timer in ti.reset('imwrite-cv2-png'): >>> with timer: >>> tmp = tempfile.NamedTemporaryFile(suffix='.png') >>> kwimage.imwrite(tmp.name, img1, space=space, backend='cv2') >>> file_sizes[ti.label] = os.stat(tmp.name).st_size >>> # >>> for timer in ti.reset('imwrite-cv2-jpg'): >>> with timer: >>> tmp = tempfile.NamedTemporaryFile(suffix='.jpg') >>> kwimage.imwrite(tmp.name, img1, space=space, backend='cv2') >>> file_sizes[ti.label] = os.stat(tmp.name).st_size >>> # >>> for timer in ti.reset('imwrite-gdal-raw'): >>> with timer: >>> tmp = tempfile.NamedTemporaryFile(suffix='.tif') >>> kwimage.imwrite(tmp.name, img1, space=space, backend='gdal', compress='RAW') >>> file_sizes[ti.label] = os.stat(tmp.name).st_size >>> # >>> for timer in ti.reset('imwrite-gdal-lzw'): >>> with timer: >>> tmp = tempfile.NamedTemporaryFile(suffix='.tif') >>> kwimage.imwrite(tmp.name, img1, space=space, backend='gdal', compress='LZW') >>> file_sizes[ti.label] = os.stat(tmp.name).st_size >>> # >>> for timer in ti.reset('imwrite-gdal-zstd'): >>> with timer: >>> tmp = tempfile.NamedTemporaryFile(suffix='.tif') >>> kwimage.imwrite(tmp.name, img1, space=space, backend='gdal', compress='ZSTD') >>> file_sizes[ti.label] = os.stat(tmp.name).st_size >>> # >>> for timer in ti.reset('imwrite-gdal-deflate'): >>> with timer: >>> tmp = tempfile.NamedTemporaryFile(suffix='.tif') >>> kwimage.imwrite(tmp.name, img1, space=space, backend='gdal', compress='DEFLATE') >>> file_sizes[ti.label] = os.stat(tmp.name).st_size >>> # >>> for timer in ti.reset('imwrite-gdal-jpeg'): >>> with timer: >>> tmp = tempfile.NamedTemporaryFile(suffix='.tif') >>> kwimage.imwrite(tmp.name, img1, space=space, backend='gdal', compress='JPEG') >>> file_sizes[ti.label] = os.stat(tmp.name).st_size >>> # >>> file_sizes = ub.sorted_vals(file_sizes) >>> import xdev >>> file_sizes_human = ub.map_vals(lambda x: xdev.byte_str(x, 'MB'), file_sizes) >>> print('ti.rankings = {}'.format(ub.repr2(ti.rankings, nl=2))) >>> print('file_sizes = {}'.format(ub.repr2(file_sizes_human, nl=1))) .. rubric:: Example >>> # Test saving a multi-band file >>> import kwimage >>> import tempfile >>> # In this case the backend will not resolve to cv2, so >>> # we should not need to specify space. >>> data = np.random.rand(32, 32, 13).astype(np.float32) >>> temp = tempfile.NamedTemporaryFile(suffix='.tif') >>> fpath = temp.name >>> kwimage.imwrite(fpath, data) >>> recon = kwimage.imread(fpath) >>> assert np.all(recon == data) >>> kwimage.imwrite(fpath, data, backend='skimage') >>> recon = kwimage.imread(fpath) >>> assert np.all(recon == data) >>> import pytest >>> # In this case the backend will resolve to cv2, and thus we expect >>> # a failure >>> temp = tempfile.NamedTemporaryFile(suffix='.png') >>> fpath = temp.name >>> with pytest.raises(NotImplementedError): >>> kwimage.imwrite(fpath, data) .. py:function:: load_image_shape(fpath) Determine the height/width/channels of an image without reading the entire file. :Parameters: **fpath** (*str*) -- path to an image :returns: Tuple - shape of the dataset. Recall this library uses the convention that "shape" is refers to height,width,channels and "size" is width,height ordering. Benchmark: >>> # For large files, PIL is much faster >>> import gdal >>> from PIL import Image >>> # >>> import kwimage >>> fpath = kwimage.grab_test_image_fpath() >>> # >>> ti = ub.Timerit(100, bestof=10, verbose=2) >>> for timer in ti.reset('gdal'): >>> with timer: >>> gdal_dset = gdal.Open(fpath, gdal.GA_ReadOnly) >>> width = gdal_dset.RasterXSize >>> height = gdal_dset.RasterYSize >>> gdal_dset = None >>> # >>> for timer in ti.reset('PIL'): >>> with timer: >>> pil_img = Image.open(fpath) >>> width, height = pil_img.size >>> pil_img.close() Timed gdal for: 100 loops, best of 10 time per loop: best=62.967 µs, mean=63.991 ± 0.8 µs Timed PIL for: 100 loops, best of 10 time per loop: best=46.640 µs, mean=47.314 ± 0.4 µs