kwimage.im_io module

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.

kwimage.im_io.imread(fpath, space='auto', backend='auto', **kw)[source]

Reads image data in a specified format using some backend implementation.

Parameters:
  • fpath (str) – path to the file to be read

  • space (str) – 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. Defaults to ‘auto’.

    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) – 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’, ‘pil’, and ‘cv2’. Defaults to ‘auto’.

  • **kw – backend-specific arguments

    The gdal backend accepts:

    overview, ignore_color_table, nodata_method, band_indices

Returns:

the image data in the specified color space.

Return type:

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.

Note

Some backends will respect EXIF orientation (skimage) and others will not (gdal, cv2).

The scikit-image backend is itself another multi-backend plugin-based image reader/writer.

Raises:
  • IOError - If the image cannot be read

  • ImportError - If trying to read a nitf without gdal

  • NotImplementedError - if trying to read a corner-case image

Example

>>> # xdoctest: +REQUIRES(--network)
>>> import kwimage
>>> import ubelt as ub
>>> # 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 = kwimage.imread(fpath)
>>> # Check that write + read preserves data
>>> dpath = ub.Path.appdir('kwimage/test/imread').ensuredir()
>>> tmp_fpath = dpath / 'tmp0.tif'
>>> kwimage.imwrite(tmp_fpath, img1)
>>> img2 = kwimage.imread(tmp_fpath)
>>> assert np.all(img2 == img1)
>>> tmp_fpath.delete()
>>> # xdoctest: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> kwplot.imshow(img1, pnum=(1, 2, 1), fnum=1, norm=True, title='tif orig')
>>> kwplot.imshow(img2, pnum=(1, 2, 2), fnum=1, norm=True, title='tif io round-trip')

Example

>>> # xdoctest: +REQUIRES(--network)
>>> import kwimage
>>> import ubelt as ub
>>> img1 = kwimage.imread(ub.grabdata(
>>>     'http://i.imgur.com/iXNf4Me.png', fname='ada.png', hasher='sha256',
>>>     hash_prefix='898cf2588c40baf64d6e09b6a93b4c8dcc0db26140639a365b57619e17dd1c77'))
>>> dpath = ub.Path.appdir('kwimage/test/imread').ensuredir()
>>> tmp_tif_fpath = dpath / 'tmp1.tif'
>>> tmp_png_fpath = dpath / 'tmp1.png'
>>> kwimage.imwrite(tmp_tif_fpath, img1)
>>> kwimage.imwrite(tmp_png_fpath, img1)
>>> tif_im = kwimage.imread(tmp_tif_fpath)
>>> png_im = kwimage.imread(tmp_png_fpath)
>>> assert np.all(tif_im == png_im)
>>> tmp_tif_fpath.delete()
>>> tmp_png_fpath.delete()
>>> # xdoctest: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> kwplot.imshow(png_im, pnum=(1, 2, 1), fnum=1, title='tif io')
>>> kwplot.imshow(tif_im, pnum=(1, 2, 2), fnum=1, title='png io')

Example

>>> # xdoctest: +REQUIRES(--network)
>>> import kwimage
>>> import ubelt as ub
>>> # FIXME: Dead link...
>>> tif_fpath = ub.grabdata(
>>>     'https://ghostscript.com/doc/tiff/test/images/rgb-3c-16b.tiff',
>>>     fname='pepper.tif', hasher='sha256',
>>>     hash_prefix='31ff3a1f416cb7281acfbcbb4b56ee8bb94e9f91489602ff2806e5a49abc03c0')
>>> img1 = kwimage.imread(tif_fpath)
>>> dpath = ub.Path.appdir('kwimage/test/imread').ensuredir()
>>> tmp_tif_fpath = dpath / 'tmp2.tif'
>>> tmp_png_fpath = dpath / 'tmp2.png'
>>> kwimage.imwrite(tmp_tif_fpath, img1)
>>> kwimage.imwrite(tmp_png_fpath, img1)
>>> tif_im = kwimage.imread(tmp_tif_fpath)
>>> png_im = kwimage.imread(tmp_png_fpath)
>>> assert np.all(tif_im == png_im)
>>> tmp_tif_fpath.delete()
>>> tmp_png_fpath.delete()
>>> 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)

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
>>> dpath = ub.Path.appdir('kwimage/test/imread').ensuredir()
>>> tmp_fpath = dpath / 'tmp3.mha'
>>> kwimage.imwrite(tmp_fpath, img1_stack)
>>> recon = kwimage.imread(tmp_fpath)
>>> assert not np.may_share_memory(recon, img1_stack)
>>> assert np.all(recon == img1_stack)
>>> tmp_fpath.delete()
>>> # xdoctest: +REQUIRES(--show)
>>> import kwplot
>>> kwplot.autompl()
>>> kwplot.imshow(kwimage.stack_images_grid(recon[0::20]),
>>>               title='kwimage.imread with a .mha file')
>>> kwplot.show_if_requested()

Benchmark

>>> import timerit
>>> import kwimage
>>> import ubelt as ub
>>> #
>>> dsize = (1920, 1080)
>>> img1 = kwimage.grab_test_image('amazon', dsize=dsize)
>>> ti = timerit.Timerit(10, bestof=3, verbose=1, unit='us')
>>> formats = {}
>>> dpath = ub.Path.appdir('kwimage/bench/im_io').ensuredir()
>>> 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)
>>> print('ti.measures = {}'.format(ub.urepr(ti.measures['min'], nl=2, align=':')))
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,
}
kwimage.im_io.imwrite(fpath, image, space='auto', backend='auto', **kwargs)[source]

Writes image data to disk.

Parameters:
  • fpath (PathLike) – location to save the image

  • image (ndarray) – image data

  • space (str | None) – 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) – 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. When the backend is gdal, available options are: compress (str): Common options are auto, DEFLATE, LZW, JPEG. blocksize (int): size of tiled blocks (e.g. 256) overviews (None | str | int | list): Number of overviews. overview_resample (str): Common options NEAREST, CUBIC, LANCZOS options (List[str]): other gdal options. nodata (int): denotes a integer value as nodata. metadata (dict): the metadata for the default empty domain. transform (kwimage.Affine): Transform to CRS from pixel space crs (str): The coordinate reference system for transform. See _imwrite_cloud_optimized_geotiff() for more details each options. When the backend is itk, see itk.imwrite() for options When the backend is skimage, see skimage.io.imsave() for options When the backend is cv2 see cv2.imwrite() for options.

Returns:

path to the written file

Return type:

str

Note

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.

The scikit-image backend is itself another multi-backend plugin-based image reader/writer.

Raises:

Exception – if the image cannot be written

Example

>>> # xdoctest: +REQUIRES(--network)
>>> # This should be moved to a unit test
>>> from kwimage.im_io import _have_gdal  # NOQA
>>> import kwimage
>>> import ubelt as ub
>>> import uuid
>>> dpath = ub.Path.appdir('kwimage/test/imwrite').ensuredir()
>>> 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 = kwimage.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_fpath = dpath / (str(uuid.uuid4()) + '.tif')
>>>         tmp_png_fpath = dpath / (str(uuid.uuid4()) + '.png')
>>>         kwimage.imwrite(tmp_tif_fpath, img1, space=space, backend='skimage')
>>>         kwimage.imwrite(tmp_png_fpath, img1, space=space)
>>>         tif_im = kwimage.imread(tmp_tif_fpath, space=space)
>>>         png_im = kwimage.imread(tmp_png_fpath, space=space)
>>>         assert np.all(tif_im == png_im), 'im-read/write inconsistency'
>>>         if _have_gdal:
>>>             tmp_tif2_fpath = dpath / (str(uuid.uuid4()) + '.tif')
>>>             kwimage.imwrite(tmp_tif2_fpath, img1, space=space, backend='gdal')
>>>             tif_im2 = kwimage.imread(tmp_tif2_fpath, space=space)
>>>             assert np.all(tif_im == tif_im2), 'im-read/write inconsistency'
>>>             tmp_tif2_fpath.delete()
>>>         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
>>>         tmp_tif_fpath.delete()
>>>         tmp_png_fpath.delete()

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.urepr(ti.rankings, nl=2)))
>>> print('file_sizes = {}'.format(ub.urepr(file_sizes_human, nl=1)))

Example

>>> # Test saving a multi-band file
>>> import kwimage
>>> import pytest
>>> import ubelt as ub
>>> dpath = ub.Path.appdir('kwimage/test/imwrite').ensuredir()
>>> # 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)
>>> fpath = dpath / 'tmp1.tif'
>>> kwimage.imwrite(fpath, data)
>>> recon = kwimage.imread(fpath)
>>> assert np.all(recon == data)
>>> kwimage.imwrite(fpath, data, backend='skimage')
>>> recon = kwimage.imread(fpath, backend='skimage')
>>> assert np.all(recon == data)
>>> # xdoctest: +REQUIRES(module:osgeo)
>>> # gdal should error when trying to read an image written by skimage
>>> with pytest.raises(NotImplementedError):
>>>     kwimage.imread(fpath, backend='gdal')
>>> # In this case the backend will resolve to cv2, and thus we expect
>>> # a failure
>>> fpath = dpath / 'tmp1.png'
>>> with pytest.raises(NotImplementedError):
>>>     kwimage.imwrite(fpath, data)

Example

>>> import ubelt as ub
>>> import kwimage
>>> dpath = ub.Path.appdir('kwimage/badwrite').ensuredir()
>>> dpath.delete().ensuredir()
>>> imdata = kwimage.ensure_uint255(kwimage.grab_test_image())[:, :, 0]
>>> import pytest
>>> fpath = dpath / 'does-not-exist/img.jpg'
>>> with pytest.raises(IOError):
...     kwimage.imwrite(fpath, imdata, backend='cv2')
>>> with pytest.raises(IOError):
...     kwimage.imwrite(fpath, imdata, backend='skimage')
>>> # xdoctest: +SKIP
>>> # TODO: run tests conditionally
>>> with pytest.raises(IOError):
...     kwimage.imwrite(fpath, imdata, backend='gdal')
>>> with pytest.raises((IOError, RuntimeError)):
...     kwimage.imwrite(fpath, imdata, backend='itk')
kwimage.im_io.load_image_shape(fpath, backend='auto', include_channels=True)[source]

Determine the height/width/channels of an image without reading the entire file.

Parameters:
  • fpath (str) – path to an image

  • backend (str) – can be “auto”, “pil”, or “gdal”.

  • include_channels (bool) – if False, only reads the height, width.

Returns:

Tuple[int, int, int] - shape of the image

Recall this library uses the convention that “shape” is refers to height,width,channels array-style ordering and “size” is width,height cv2-style ordering.

Example

>>> # xdoctest: +REQUIRES(module:osgeo)
>>> # Test the loading the shape works the same as loading the image and
>>> # testing the shape
>>> import kwimage
>>> temp_dpath = ub.Path.appdir('kwimage/tests/load_image_shape').ensuredir()
>>> data = kwimage.grab_test_image()
>>> datas = {
>>>     'rgb255': kwimage.ensure_uint255(data),
>>>     'rgb01': kwimage.ensure_float01(data),
>>>     'rgba01': kwimage.ensure_alpha_channel(data),
>>> }
>>> results = {}
>>> # These should be consistent
>>> # The was a problem where CV2_IMREAD_UNCHANGED read the alpha band,
>>> # but PIL did not, but maybe this is fixed now?
>>> for key, imdata in datas.items():
>>>     fpath = temp_dpath / f'{key}.png'
>>>     kwimage.imwrite(fpath, imdata)
>>>     shapes = {}
>>>     shapes['pil_load_shape'] = kwimage.load_image_shape(fpath, backend='pil')
>>>     shapes['gdal_load_shape'] = kwimage.load_image_shape(fpath, backend='gdal')
>>>     shapes['auto_load_shape'] = kwimage.load_image_shape(fpath, backend='auto')
>>>     shapes['pil'] = kwimage.imread(fpath, backend='pil').shape
>>>     shapes['cv2'] = kwimage.imread(fpath, backend='cv2').shape
>>>     shapes['gdal'] = kwimage.imread(fpath, backend='gdal').shape
>>>     shapes['skimage'] = kwimage.imread(fpath, backend='skimage').shape
>>>     results[key] = shapes
>>> print('results = {}'.format(ub.urepr(results, nl=2, align=':', sort=0)))
>>> for shapes in results.values():
>>>     assert ub.allsame(shapes.values())
>>> temp_dpath.delete()

Benchmark

>>> # For large files, PIL is much faster
>>> # xdoctest: +REQUIRES(module:osgeo)
>>> from osgeo import gdal
>>> from PIL import Image
>>> import timerit
>>> #
>>> import kwimage
>>> fpath = kwimage.grab_test_image_fpath()
>>> #
>>> ti = timerit.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()
>>> # xdoctest: +REQUIRES(module:imagesize)
>>> # The imagesize module is quite fast
>>> import imagesize
>>> for timer in ti.reset('imagesize'):
>>>     with timer:
>>>         width, height = imagesize.get(fpath)
Timed gdal for: 100 loops, best of 10
    time per loop: best=54.423 µs, mean=72.761 ± 15.9 µs
Timed PIL for: 100 loops, best of 10
    time per loop: best=25.986 µs, mean=26.791 ± 1.2 µs
Timed imagesize for: 100 loops, best of 10
    time per loop: best=5.092 µs, mean=5.195 ± 0.1 µs

Example

>>> # xdoctest: +REQUIRES(module:osgeo)
>>> import ubelt as ub
>>> import kwimage
>>> dpath = ub.Path.appdir('kwimage/tests', type='cache').ensuredir()
>>> fpath = dpath / 'foo.tif'
>>> kwimage.imwrite(fpath, np.random.rand(64, 64, 3))
>>> shape = kwimage.load_image_shape(fpath)
>>> assert shape == (64, 64, 3)