Unverified Commit 66bff139 authored by wuwencheng's avatar wuwencheng Committed by GitHub
Browse files

[Feature] Add multi file backends to imread/imwrite. (#1527)

* Add file client to image io

* Fix petrel_client imwrite error

* Add examples to the docstring and delete the file check of imread

* modify docstring v1.3.19->v1.4.1

* Deprecate auto_mkdir parameter and complete test_io.py

* Fix error caused by deleting the mock package in test_io.py

* Add annotation to imencode

* modify imread input assert and delete the judgement of file client 'put' method

* Delete try except in imwrite.

* Add a error file extension unit test.
parent 81f032ed
# Copyright (c) OpenMMLab. All rights reserved. # Copyright (c) OpenMMLab. All rights reserved.
import io import io
import os.path as osp import os.path as osp
import warnings
from pathlib import Path from pathlib import Path
import cv2 import cv2
...@@ -8,7 +9,8 @@ import numpy as np ...@@ -8,7 +9,8 @@ import numpy as np
from cv2 import (IMREAD_COLOR, IMREAD_GRAYSCALE, IMREAD_IGNORE_ORIENTATION, from cv2 import (IMREAD_COLOR, IMREAD_GRAYSCALE, IMREAD_IGNORE_ORIENTATION,
IMREAD_UNCHANGED) IMREAD_UNCHANGED)
from mmcv.utils import check_file_exist, is_str, mkdir_or_exist from mmcv.fileio import FileClient
from mmcv.utils import is_filepath, is_str
try: try:
from turbojpeg import TJCS_RGB, TJPF_BGR, TJPF_GRAY, TurboJPEG from turbojpeg import TJCS_RGB, TJPF_BGR, TJPF_GRAY, TurboJPEG
...@@ -137,9 +139,16 @@ def _pillow2array(img, flag='color', channel_order='bgr'): ...@@ -137,9 +139,16 @@ def _pillow2array(img, flag='color', channel_order='bgr'):
return array return array
def imread(img_or_path, flag='color', channel_order='bgr', backend=None): def imread(img_or_path,
flag='color',
channel_order='bgr',
backend=None,
file_client_args=None):
"""Read an image. """Read an image.
Note:
In v1.4.1 and later, add `file_client_args` parameters.
Args: Args:
img_or_path (ndarray or str or Path): Either a numpy array or str or img_or_path (ndarray or str or Path): Either a numpy array or str or
pathlib.Path. If it is a numpy array (loaded image), then pathlib.Path. If it is a numpy array (loaded image), then
...@@ -157,44 +166,42 @@ def imread(img_or_path, flag='color', channel_order='bgr', backend=None): ...@@ -157,44 +166,42 @@ def imread(img_or_path, flag='color', channel_order='bgr', backend=None):
`cv2`, `pillow`, `turbojpeg`, `tifffile`, `None`. `cv2`, `pillow`, `turbojpeg`, `tifffile`, `None`.
If backend is None, the global imread_backend specified by If backend is None, the global imread_backend specified by
``mmcv.use_backend()`` will be used. Default: None. ``mmcv.use_backend()`` will be used. Default: None.
file_client_args (dict | None): Arguments to instantiate a
FileClient. See :class:`mmcv.fileio.FileClient` for details.
Default: None.
Returns: Returns:
ndarray: Loaded image array. ndarray: Loaded image array.
Examples:
>>> import mmcv
>>> img_path = '/path/to/img.jpg'
>>> img = mmcv.imread(img_path)
>>> img = mmcv.imread(img_path, flag='color', channel_order='rgb',
... backend='cv2')
>>> img = mmcv.imread(img_path, flag='color', channel_order='bgr',
... backend='pillow')
>>> s3_img_path = 's3://bucket/img.jpg'
>>> # infer the file backend by the prefix s3
>>> img = mmcv.imread(s3_img_path)
>>> # manually set the file backend petrel
>>> img = mmcv.imread(s3_img_path, file_client_args={
... 'backend': 'petrel'})
>>> http_img_path = 'http://path/to/img.jpg'
>>> img = mmcv.imread(http_img_path)
>>> img = mmcv.imread(http_img_path, file_client_args={
... 'backend': 'http'})
""" """
if backend is None:
backend = imread_backend
if backend not in supported_backends:
raise ValueError(f'backend: {backend} is not supported. Supported '
"backends are 'cv2', 'turbojpeg', 'pillow'")
if isinstance(img_or_path, Path): if isinstance(img_or_path, Path):
img_or_path = str(img_or_path) img_or_path = str(img_or_path)
if isinstance(img_or_path, np.ndarray): if isinstance(img_or_path, np.ndarray):
return img_or_path return img_or_path
elif is_str(img_or_path): elif is_str(img_or_path):
check_file_exist(img_or_path, file_client = FileClient.infer_client(file_client_args, img_or_path)
f'img file does not exist: {img_or_path}') img_bytes = file_client.get(img_or_path)
if backend == 'turbojpeg': return imfrombytes(img_bytes, flag, channel_order, backend)
with open(img_or_path, 'rb') as in_file:
img = jpeg.decode(in_file.read(),
_jpegflag(flag, channel_order))
if img.shape[-1] == 1:
img = img[:, :, 0]
return img
elif backend == 'pillow':
img = Image.open(img_or_path)
img = _pillow2array(img, flag, channel_order)
return img
elif backend == 'tifffile':
img = tifffile.imread(img_or_path)
return img
else:
flag = imread_flags[flag] if is_str(flag) else flag
img = cv2.imread(img_or_path, flag)
if flag == IMREAD_COLOR and channel_order == 'rgb':
cv2.cvtColor(img, cv2.COLOR_BGR2RGB, img)
return img
else: else:
raise TypeError('"img" must be a numpy array or a str or ' raise TypeError('"img" must be a numpy array or a str or '
'a pathlib.Path object') 'a pathlib.Path object')
...@@ -207,28 +214,42 @@ def imfrombytes(content, flag='color', channel_order='bgr', backend=None): ...@@ -207,28 +214,42 @@ def imfrombytes(content, flag='color', channel_order='bgr', backend=None):
content (bytes): Image bytes got from files or other streams. content (bytes): Image bytes got from files or other streams.
flag (str): Same as :func:`imread`. flag (str): Same as :func:`imread`.
backend (str | None): The image decoding backend type. Options are backend (str | None): The image decoding backend type. Options are
`cv2`, `pillow`, `turbojpeg`, `None`. If backend is None, the `cv2`, `pillow`, `turbojpeg`, `tifffile`, `None`. If backend is
global imread_backend specified by ``mmcv.use_backend()`` will be None, the global imread_backend specified by ``mmcv.use_backend()``
used. Default: None. will be used. Default: None.
Returns: Returns:
ndarray: Loaded image array. ndarray: Loaded image array.
Examples:
>>> img_path = '/path/to/img.jpg'
>>> with open(img_path, 'rb') as f:
>>> img_buff = f.read()
>>> img = mmcv.imfrombytes(img_buff)
>>> img = mmcv.imfrombytes(img_buff, flag='color', channel_order='rgb')
>>> img = mmcv.imfrombytes(img_buff, backend='pillow')
>>> img = mmcv.imfrombytes(img_buff, backend='cv2')
""" """
if backend is None: if backend is None:
backend = imread_backend backend = imread_backend
if backend not in supported_backends: if backend not in supported_backends:
raise ValueError(f'backend: {backend} is not supported. Supported ' raise ValueError(
"backends are 'cv2', 'turbojpeg', 'pillow'") f'backend: {backend} is not supported. Supported '
"backends are 'cv2', 'turbojpeg', 'pillow', 'tifffile'")
if backend == 'turbojpeg': if backend == 'turbojpeg':
img = jpeg.decode(content, _jpegflag(flag, channel_order)) img = jpeg.decode(content, _jpegflag(flag, channel_order))
if img.shape[-1] == 1: if img.shape[-1] == 1:
img = img[:, :, 0] img = img[:, :, 0]
return img return img
elif backend == 'pillow': elif backend == 'pillow':
buff = io.BytesIO(content) with io.BytesIO(content) as buff:
img = Image.open(buff) img = Image.open(buff)
img = _pillow2array(img, flag, channel_order) img = _pillow2array(img, flag, channel_order)
return img
elif backend == 'tifffile':
with io.BytesIO(content) as buff:
img = tifffile.imread(buff)
return img return img
else: else:
img_np = np.frombuffer(content, np.uint8) img_np = np.frombuffer(content, np.uint8)
...@@ -239,20 +260,53 @@ def imfrombytes(content, flag='color', channel_order='bgr', backend=None): ...@@ -239,20 +260,53 @@ def imfrombytes(content, flag='color', channel_order='bgr', backend=None):
return img return img
def imwrite(img, file_path, params=None, auto_mkdir=True): def imwrite(img,
file_path,
params=None,
auto_mkdir=None,
file_client_args=None):
"""Write image to file. """Write image to file.
Note:
In v1.4.1 and later, add `file_client_args` parameters.
Warning:
The parameter `auto_mkdir` will be deprecated in the future and every
file clients will make directory automatically.
Args: Args:
img (ndarray): Image array to be written. img (ndarray): Image array to be written.
file_path (str): Image file path. file_path (str): Image file path.
params (None or list): Same as opencv :func:`imwrite` interface. params (None or list): Same as opencv :func:`imwrite` interface.
auto_mkdir (bool): If the parent folder of `file_path` does not exist, auto_mkdir (bool): If the parent folder of `file_path` does not exist,
whether to create it automatically. whether to create it automatically. It will be deprecated.
file_client_args (dict | None): Arguments to instantiate a
FileClient. See :class:`mmcv.fileio.FileClient` for details.
Default: None.
Returns: Returns:
bool: Successful or not. bool: Successful or not.
Examples:
>>> # write to hard disk client
>>> ret = mmcv.imwrite(img, '/path/to/img.jpg')
>>> # infer the file backend by the prefix s3
>>> ret = mmcv.imwrite(img, 's3://bucket/img.jpg')
>>> # manually set the file backend petrel
>>> ret = mmcv.imwrite(img, 's3://bucket/img.jpg', file_client_args={
... 'backend': 'petrel'})
""" """
if auto_mkdir: assert is_filepath(file_path)
dir_name = osp.abspath(osp.dirname(file_path)) file_path = str(file_path)
mkdir_or_exist(dir_name) if auto_mkdir is not None:
return cv2.imwrite(file_path, img, params) warnings.warn(
'The parameter `auto_mkdir` will be deprecated in the future and '
'every file clients will make directory automatically.')
file_client = FileClient.infer_client(file_client_args, file_path)
img_ext = osp.splitext(file_path)[-1]
# Encode image according to image suffix.
# For example, if image path is '/path/your/img.jpg', the encode
# format is '.jpg'.
flag, img_buff = cv2.imencode(img_ext, img, params)
file_client.put(img_buff.tobytes(), file_path)
return flag
# Copyright (c) OpenMMLab. All rights reserved. # Copyright (c) OpenMMLab. All rights reserved.
import os import os
import os.path as osp import os.path as osp
import sys
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import MagicMock, patch
import cv2 import cv2
import numpy as np import numpy as np
...@@ -11,6 +12,7 @@ import pytest ...@@ -11,6 +12,7 @@ import pytest
from numpy.testing import assert_allclose, assert_array_equal from numpy.testing import assert_allclose, assert_array_equal
import mmcv import mmcv
from mmcv.fileio.file_client import HTTPBackend, PetrelBackend
class TestIO: class TestIO:
...@@ -29,6 +31,18 @@ class TestIO: ...@@ -29,6 +31,18 @@ class TestIO:
cls.exif_img_path = osp.join(cls.data_dir, 'color_exif.jpg') cls.exif_img_path = osp.join(cls.data_dir, 'color_exif.jpg')
cls.img = cv2.imread(cls.img_path) cls.img = cv2.imread(cls.img_path)
cls.tiff_path = osp.join(cls.data_dir, 'uint16-5channel.tif') cls.tiff_path = osp.join(cls.data_dir, 'uint16-5channel.tif')
# petrel s3 path
cls.s3_path = 's3://path/of/your/file.jpg'
# http path
cls.http_path = 'http://path/of/your/file.jpg'
# add mock package
sys.modules['petrel_client'] = MagicMock()
sys.modules['petrel_client.client'] = MagicMock()
@classmethod
def teardown_class(cls):
# clean instances avoid to influence other unittest
mmcv.FileClient._instances = {}
def assert_img_equal(self, img, ref_img, ratio_thr=0.999): def assert_img_equal(self, img, ref_img, ratio_thr=0.999):
assert img.shape == ref_img.shape assert img.shape == ref_img.shape
...@@ -41,6 +55,7 @@ class TestIO: ...@@ -41,6 +55,7 @@ class TestIO:
# backend cv2 # backend cv2
mmcv.use_backend('cv2') mmcv.use_backend('cv2')
# HardDiskBackend
img_cv2_color_bgr = mmcv.imread(self.img_path) img_cv2_color_bgr = mmcv.imread(self.img_path)
assert img_cv2_color_bgr.shape == (300, 400, 3) assert img_cv2_color_bgr.shape == (300, 400, 3)
img_cv2_color_rgb = mmcv.imread(self.img_path, channel_order='rgb') img_cv2_color_rgb = mmcv.imread(self.img_path, channel_order='rgb')
...@@ -69,6 +84,37 @@ class TestIO: ...@@ -69,6 +84,37 @@ class TestIO:
with pytest.raises(TypeError): with pytest.raises(TypeError):
mmcv.imread(1) mmcv.imread(1)
# PetrelBackend
img_cv2_color_bgr = mmcv.imread(self.img_path)
with patch.object(
PetrelBackend, 'get',
return_value=img_cv2_color_bgr) as mock_method:
img_cv2_color_bgr_petrel = mmcv.imread(self.s3_path, backend='cv2')
img_cv2_color_bgr_petrel_with_args = mmcv.imread(
self.s3_path,
backend='cv2',
file_client_args={'backend': 'petrel'})
mock_method.assert_called()
assert_array_equal(img_cv2_color_bgr_petrel,
img_cv2_color_bgr_petrel_with_args)
# HTTPBackend
img_cv2_color_bgr = mmcv.imread(self.img_path)
with patch.object(
HTTPBackend, 'get',
return_value=img_cv2_color_bgr) as mock_method:
img_cv2_color_bgr_http = mmcv.imread(self.http_path, backend='cv2')
img_cv2_color_bgr_http_with_args = mmcv.imread(
self.http_path,
backend='cv2',
file_client_args={'backend': 'http'})
mock_method.assert_called()
assert_array_equal(img_cv2_color_bgr_http,
img_cv2_color_bgr_http_with_args)
with pytest.raises(FileNotFoundError):
mmcv.imread('/not/exists/' + self.img_path)
# test arg backend pillow # test arg backend pillow
img_pil_gray_alpha = mmcv.imread( img_pil_gray_alpha = mmcv.imread(
self.gray_alpha_img_path, 'grayscale', backend='pillow') self.gray_alpha_img_path, 'grayscale', backend='pillow')
...@@ -311,9 +357,18 @@ class TestIO: ...@@ -311,9 +357,18 @@ class TestIO:
os.remove(out_file) os.remove(out_file)
self.assert_img_equal(img, rewrite_img) self.assert_img_equal(img, rewrite_img)
ret = mmcv.imwrite( # test petrel client
img, './non_exist_path/mmcv_test.jpg', auto_mkdir=False) with patch.object(
assert ret is False PetrelBackend, 'put', return_value=None) as mock_method:
ret = mmcv.imwrite(img, self.s3_path)
ret_with_args = mmcv.imwrite(
img, self.s3_path, file_client_args={'backend': 'petrel'})
assert ret
assert ret_with_args
mock_method.assert_called()
with pytest.raises(cv2.error):
mmcv.imwrite(img, 'error_file.jppg')
@patch('mmcv.image.io.TurboJPEG', None) @patch('mmcv.image.io.TurboJPEG', None)
def test_no_turbojpeg(self): def test_no_turbojpeg(self):
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment