Commit e5c2cab5 authored by Yixiao Fang's avatar Yixiao Fang Committed by zhouzaida
Browse files

[Refactor] Add pillow backend for ColorJitter related functions (#2127)

* add 'backend' for ColorJitter related functions

* add unittest

* fix unittest

* add bgr transpose and revise ut

* update unittest

* revise docstring
parent 08bc4498
# Copyright (c) OpenMMLab. All rights reserved. # Copyright (c) OpenMMLab. All rights reserved.
import warnings
from typing import Optional
import cv2 import cv2
import numpy as np import numpy as np
from PIL import Image, ImageEnhance
from ..utils import is_tuple_of from ..utils import is_tuple_of
from .colorspace import bgr2gray, gray2bgr from .colorspace import bgr2gray, gray2bgr
from .io import imread_backend
def imnormalize(img, mean, std, to_rgb=True): def imnormalize(img, mean, std, to_rgb=True):
...@@ -97,7 +102,7 @@ def posterize(img, bits): ...@@ -97,7 +102,7 @@ def posterize(img, bits):
return img return img
def adjust_color(img, alpha=1, beta=None, gamma=0): def adjust_color(img, alpha=1, beta=None, gamma=0, backend=None):
r"""It blends the source image and its gray image: r"""It blends the source image and its gray image:
.. math:: .. math::
...@@ -110,10 +115,29 @@ def adjust_color(img, alpha=1, beta=None, gamma=0): ...@@ -110,10 +115,29 @@ def adjust_color(img, alpha=1, beta=None, gamma=0):
If None, it's assigned the value (1 - `alpha`). If None, it's assigned the value (1 - `alpha`).
gamma (int | float): Scalar added to each sum. gamma (int | float): Scalar added to each sum.
Same as :func:`cv2.addWeighted`. Default 0. Same as :func:`cv2.addWeighted`. Default 0.
backend (str | None): The image processing backend type. Options are
`cv2`, `pillow`, `None`. If backend is None, the global
``imread_backend`` specified by ``mmcv.use_backend()`` will be
used. Defaults to None.
Returns: Returns:
ndarray: Colored image which has the same size and dtype as input. ndarray: Colored image which has the same size and dtype as input.
""" """
if backend is None:
backend = imread_backend
if backend not in ['cv2', 'pillow']:
raise ValueError(f'backend: {backend} is not supported.'
f"Supported backends are 'cv2', 'pillow'")
if backend == 'pillow':
assert img.dtype == np.uint8, 'Pillow backend only support uint8 type'
warnings.warn("Only use 'alpha' for pillow backend.")
# Image.fromarray defaultly supports RGB, not BGR.
pil_image = Image.fromarray(img[..., ::-1], mode='RGB')
enhancer = ImageEnhance.Color(pil_image)
pil_image = enhancer.enhance(alpha)
return np.array(pil_image, dtype=img.dtype)[..., ::-1]
else:
gray_img = bgr2gray(img) gray_img = bgr2gray(img)
gray_img = np.tile(gray_img[..., None], [1, 1, 3]) gray_img = np.tile(gray_img[..., None], [1, 1, 3])
if beta is None: if beta is None:
...@@ -125,7 +149,7 @@ def adjust_color(img, alpha=1, beta=None, gamma=0): ...@@ -125,7 +149,7 @@ def adjust_color(img, alpha=1, beta=None, gamma=0):
# is not guaranteed to be in range [0, 255], so here clip # is not guaranteed to be in range [0, 255], so here clip
# is needed. # is needed.
colored_img = np.clip(colored_img, 0, 255) colored_img = np.clip(colored_img, 0, 255)
return colored_img return colored_img.astype(img.dtype)
def imequalize(img): def imequalize(img):
...@@ -173,7 +197,7 @@ def imequalize(img): ...@@ -173,7 +197,7 @@ def imequalize(img):
return equalized_img.astype(img.dtype) return equalized_img.astype(img.dtype)
def adjust_brightness(img, factor=1.): def adjust_brightness(img, factor=1., backend=None):
"""Adjust image brightness. """Adjust image brightness.
This function controls the brightness of an image. An This function controls the brightness of an image. An
...@@ -190,10 +214,28 @@ def adjust_brightness(img, factor=1.): ...@@ -190,10 +214,28 @@ def adjust_brightness(img, factor=1.):
Factor 1.0 returns the original image, lower Factor 1.0 returns the original image, lower
factors mean less color (brightness, contrast, factors mean less color (brightness, contrast,
etc), and higher values more. Default 1. etc), and higher values more. Default 1.
backend (str | None): The image processing backend type. Options are
`cv2`, `pillow`, `None`. If backend is None, the global
``imread_backend`` specified by ``mmcv.use_backend()`` will be
used. Defaults to None.
Returns: Returns:
ndarray: The brightened image. ndarray: The brightened image.
""" """
if backend is None:
backend = imread_backend
if backend not in ['cv2', 'pillow']:
raise ValueError(f'backend: {backend} is not supported.'
f"Supported backends are 'cv2', 'pillow'")
if backend == 'pillow':
assert img.dtype == np.uint8, 'Pillow backend only support uint8 type'
# Image.fromarray defaultly supports RGB, not BGR.
pil_image = Image.fromarray(img[..., ::-1], mode='RGB')
enhancer = ImageEnhance.Brightness(pil_image)
pil_image = enhancer.enhance(factor)
return np.array(pil_image, dtype=img.dtype)[..., ::-1]
else:
degenerated = np.zeros_like(img) degenerated = np.zeros_like(img)
# Note manually convert the dtype to np.float32, to # Note manually convert the dtype to np.float32, to
# achieve as close results as PIL.ImageEnhance.Brightness. # achieve as close results as PIL.ImageEnhance.Brightness.
...@@ -205,7 +247,7 @@ def adjust_brightness(img, factor=1.): ...@@ -205,7 +247,7 @@ def adjust_brightness(img, factor=1.):
return brightened_img.astype(img.dtype) return brightened_img.astype(img.dtype)
def adjust_contrast(img, factor=1.): def adjust_contrast(img, factor=1., backend=None):
"""Adjust image contrast. """Adjust image contrast.
This function controls the contrast of an image. An This function controls the contrast of an image. An
...@@ -219,10 +261,28 @@ def adjust_contrast(img, factor=1.): ...@@ -219,10 +261,28 @@ def adjust_contrast(img, factor=1.):
Args: Args:
img (ndarray): Image to be contrasted. BGR order. img (ndarray): Image to be contrasted. BGR order.
factor (float): Same as :func:`mmcv.adjust_brightness`. factor (float): Same as :func:`mmcv.adjust_brightness`.
backend (str | None): The image processing backend type. Options are
`cv2`, `pillow`, `None`. If backend is None, the global
``imread_backend`` specified by ``mmcv.use_backend()`` will be
used. Defaults to None.
Returns: Returns:
ndarray: The contrasted image. ndarray: The contrasted image.
""" """
if backend is None:
backend = imread_backend
if backend not in ['cv2', 'pillow']:
raise ValueError(f'backend: {backend} is not supported.'
f"Supported backends are 'cv2', 'pillow'")
if backend == 'pillow':
assert img.dtype == np.uint8, 'Pillow backend only support uint8 type'
# Image.fromarray defaultly supports RGB, not BGR.
pil_image = Image.fromarray(img[..., ::-1], mode='RGB')
enhancer = ImageEnhance.Contrast(pil_image)
pil_image = enhancer.enhance(factor)
return np.array(pil_image, dtype=img.dtype)[..., ::-1]
else:
gray_img = bgr2gray(img) gray_img = bgr2gray(img)
hist = np.histogram(gray_img, 256, (0, 255))[0] hist = np.histogram(gray_img, 256, (0, 255))[0]
mean = round(np.sum(gray_img) / np.sum(hist)) mean = round(np.sum(gray_img) / np.sum(hist))
...@@ -428,7 +488,9 @@ def clahe(img, clip_limit=40.0, tile_grid_size=(8, 8)): ...@@ -428,7 +488,9 @@ def clahe(img, clip_limit=40.0, tile_grid_size=(8, 8)):
return clahe.apply(np.array(img, dtype=np.uint8)) return clahe.apply(np.array(img, dtype=np.uint8))
def adjust_hue(img: np.ndarray, hue_factor: float) -> np.ndarray: def adjust_hue(img: np.ndarray,
hue_factor: float,
backend: Optional[str] = None) -> np.ndarray:
"""Adjust hue of an image. """Adjust hue of an image.
The image hue is adjusted by converting the image to HSV and cyclically The image hue is adjusted by converting the image to HSV and cyclically
...@@ -449,23 +511,51 @@ def adjust_hue(img: np.ndarray, hue_factor: float) -> np.ndarray: ...@@ -449,23 +511,51 @@ def adjust_hue(img: np.ndarray, hue_factor: float) -> np.ndarray:
HSV space in positive and negative direction respectively. HSV space in positive and negative direction respectively.
0 means no shift. Therefore, both -0.5 and 0.5 will give an image 0 means no shift. Therefore, both -0.5 and 0.5 will give an image
with complementary colors while 0 gives the original image. with complementary colors while 0 gives the original image.
backend (str | None): The image processing backend type. Options are
`cv2`, `pillow`, `None`. If backend is None, the global
``imread_backend`` specified by ``mmcv.use_backend()`` will be
used. Defaults to None.
Returns: Returns:
ndarray: Hue adjusted image. ndarray: Hue adjusted image.
""" """
if backend is None:
backend = imread_backend
if backend not in ['cv2', 'pillow']:
raise ValueError(f'backend: {backend} is not supported.'
f"Supported backends are 'cv2', 'pillow'")
if not (-0.5 <= hue_factor <= 0.5): if not (-0.5 <= hue_factor <= 0.5):
raise ValueError(f'hue_factor:{hue_factor} is not in [-0.5, 0.5].') raise ValueError(f'hue_factor:{hue_factor} is not in [-0.5, 0.5].')
if not (isinstance(img, np.ndarray) and (img.ndim in {2, 3})): if not (isinstance(img, np.ndarray) and (img.ndim in {2, 3})):
raise TypeError('img should be ndarray with dim=[2 or 3].') raise TypeError('img should be ndarray with dim=[2 or 3].')
if backend == 'pillow':
assert img.dtype == np.uint8, 'Pillow backend only support uint8 type'
# Image.fromarray defaultly supports RGB, not BGR.
pil_image = Image.fromarray(img[..., ::-1], mode='RGB')
input_mode = pil_image.mode
if input_mode in {'L', '1', 'I', 'F'}:
return pil_image
h, s, v = pil_image.convert('HSV').split()
np_h = np.array(h, dtype=np.uint8)
# uint8 addition take cares of rotation across boundaries
with np.errstate(over='ignore'):
np_h += np.uint8(hue_factor * 255)
h = Image.fromarray(np_h, 'L')
pil_image = Image.merge('HSV', (h, s, v)).convert(input_mode)
return np.array(pil_image, dtype=img.dtype)[..., ::-1]
else:
dtype = img.dtype dtype = img.dtype
img = img.astype(np.uint8) img = img.astype(np.uint8)
hsv_img = cv2.cvtColor(img, cv2.COLOR_RGB2HSV_FULL) hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV_FULL)
h, s, v = cv2.split(hsv_img) h, s, v = cv2.split(hsv_img)
h = h.astype(np.uint8) h = h.astype(np.uint8)
# uint8 addition take cares of rotation across boundaries # uint8 addition take cares of rotation across boundaries
with np.errstate(over='ignore'): with np.errstate(over='ignore'):
h += np.uint8(hue_factor * 255) h += np.uint8(hue_factor * 255)
hsv_img = cv2.merge([h, s, v]) hsv_img = cv2.merge([h, s, v])
return cv2.cvtColor(hsv_img, cv2.COLOR_HSV2RGB_FULL).astype(dtype) return cv2.cvtColor(hsv_img, cv2.COLOR_HSV2BGR_FULL).astype(dtype)
...@@ -77,7 +77,7 @@ class TestPhotometric: ...@@ -77,7 +77,7 @@ class TestPhotometric:
dtype=np.uint8) dtype=np.uint8)
assert_array_equal(mmcv.posterize(img, 3), img_r) assert_array_equal(mmcv.posterize(img, 3), img_r)
def test_adjust_color(self): def test_adjust_color(self, nb_rand_test=100):
img = np.array([[0, 128, 255], [1, 127, 254], [2, 129, 253]], img = np.array([[0, 128, 255], [1, 127, 254], [2, 129, 253]],
dtype=np.uint8) dtype=np.uint8)
img = np.stack([img, img, img], axis=-1) img = np.stack([img, img, img], axis=-1)
...@@ -108,6 +108,23 @@ class TestPhotometric: ...@@ -108,6 +108,23 @@ class TestPhotometric:
np.round(mmcv.adjust_color(img, 0.8, -0.6, gamma=-0.6)), np.round(mmcv.adjust_color(img, 0.8, -0.6, gamma=-0.6)),
np.round(np.clip(img * 0.8 - 0.6 * img_r - 0.6, 0, 255))) np.round(np.clip(img * 0.8 - 0.6 * img_r - 0.6, 0, 255)))
# test equalize with randomly sampled image.
for _ in range(nb_rand_test):
img = np.clip(np.random.normal(0, 1, (256, 256, 3)) * 260, 0,
255).astype(np.uint8)
factor = np.random.uniform()
cv2_img = mmcv.adjust_color(img, alpha=factor)
pil_img = mmcv.adjust_color(img, alpha=factor, backend='pillow')
np.testing.assert_allclose(cv2_img, pil_img, rtol=0, atol=2)
# the input type must be uint8 for pillow backend
with pytest.raises(AssertionError):
mmcv.adjust_color(img.astype(np.float32), backend='pillow')
# backend must be 'cv2' or 'pillow'
with pytest.raises(ValueError):
mmcv.adjust_color(img.astype(np.uint8), backend='not support')
def test_imequalize(self, nb_rand_test=100): def test_imequalize(self, nb_rand_test=100):
def _imequalize(img): def _imequalize(img):
...@@ -138,15 +155,6 @@ class TestPhotometric: ...@@ -138,15 +155,6 @@ class TestPhotometric:
def test_adjust_brightness(self, nb_rand_test=100): def test_adjust_brightness(self, nb_rand_test=100):
def _adjust_brightness(img, factor):
# adjust the brightness of image using
# PIL.ImageEnhance.Brightness
from PIL import Image
from PIL.ImageEnhance import Brightness
img = Image.fromarray(img)
brightened_img = Brightness(img).enhance(factor)
return np.asarray(brightened_img)
img = np.array([[0, 128, 255], [1, 127, 254], [2, 129, 253]], img = np.array([[0, 128, 255], [1, 127, 254], [2, 129, 253]],
dtype=np.uint8) dtype=np.uint8)
img = np.stack([img, img, img], axis=-1) img = np.stack([img, img, img], axis=-1)
...@@ -162,22 +170,20 @@ class TestPhotometric: ...@@ -162,22 +170,20 @@ class TestPhotometric:
factor = np.random.uniform() + np.random.choice([0, 1]) factor = np.random.uniform() + np.random.choice([0, 1])
np.testing.assert_allclose( np.testing.assert_allclose(
mmcv.adjust_brightness(img, factor).astype(np.int32), mmcv.adjust_brightness(img, factor).astype(np.int32),
_adjust_brightness(img, factor).astype(np.int32), mmcv.adjust_brightness(img, factor,
backend='pillow').astype(np.int32),
rtol=0, rtol=0,
atol=1) atol=1)
def test_adjust_contrast(self, nb_rand_test=100): # the input type must be uint8 for pillow backend
with pytest.raises(AssertionError):
mmcv.adjust_brightness(img.astype(np.float32), backend='pillow')
def _adjust_contrast(img, factor): # backend must be 'cv2' or 'pillow'
from PIL import Image with pytest.raises(ValueError):
from PIL.ImageEnhance import Contrast mmcv.adjust_brightness(img.astype(np.uint8), backend='not support')
# Image.fromarray defaultly supports RGB, not BGR. def test_adjust_contrast(self, nb_rand_test=100):
# convert from BGR to RGB
img = Image.fromarray(img[..., ::-1], mode='RGB')
contrasted_img = Contrast(img).enhance(factor)
# convert from RGB to BGR
return np.asarray(contrasted_img)[..., ::-1]
img = np.array([[0, 128, 255], [1, 127, 254], [2, 129, 253]], img = np.array([[0, 128, 255], [1, 127, 254], [2, 129, 253]],
dtype=np.uint8) dtype=np.uint8)
...@@ -186,7 +192,8 @@ class TestPhotometric: ...@@ -186,7 +192,8 @@ class TestPhotometric:
assert_array_equal(mmcv.adjust_contrast(img, 1.), img) assert_array_equal(mmcv.adjust_contrast(img, 1.), img)
# test case with factor 0.0 # test case with factor 0.0
assert_array_equal( assert_array_equal(
mmcv.adjust_contrast(img, 0.), _adjust_contrast(img, 0.)) mmcv.adjust_contrast(img, 0.),
mmcv.adjust_contrast(img, 0., backend='pillow'))
# test adjust_contrast with randomly sampled images and factors. # test adjust_contrast with randomly sampled images and factors.
for _ in range(nb_rand_test): for _ in range(nb_rand_test):
img = np.clip( img = np.clip(
...@@ -198,10 +205,19 @@ class TestPhotometric: ...@@ -198,10 +205,19 @@ class TestPhotometric:
# a color image to gray image using mmcv or PIL. # a color image to gray image using mmcv or PIL.
np.testing.assert_allclose( np.testing.assert_allclose(
mmcv.adjust_contrast(img, factor).astype(np.int32), mmcv.adjust_contrast(img, factor).astype(np.int32),
_adjust_contrast(img, factor).astype(np.int32), mmcv.adjust_contrast(img, factor,
backend='pillow').astype(np.int32),
rtol=0, rtol=0,
atol=1) atol=1)
# the input type must be uint8 pillow backend
with pytest.raises(AssertionError):
mmcv.adjust_contrast(img.astype(np.float32), backend='pillow')
# backend must be 'cv2' or 'pillow'
with pytest.raises(ValueError):
mmcv.adjust_contrast(img.astype(np.uint8), backend='not support')
def test_auto_contrast(self, nb_rand_test=100): def test_auto_contrast(self, nb_rand_test=100):
def _auto_contrast(img, cutoff=0): def _auto_contrast(img, cutoff=0):
...@@ -380,24 +396,10 @@ class TestPhotometric: ...@@ -380,24 +396,10 @@ class TestPhotometric:
assert id(img_std) != id(self.img[:, :, i]) assert id(img_std) != id(self.img[:, :, i])
def test_adjust_hue(self): def test_adjust_hue(self):
# test case with img is not ndarray
from PIL import Image from PIL import Image
def _adjust_hue(img, hue_factor):
input_mode = img.mode
if input_mode in {'L', '1', 'I', 'F'}:
return img
h, s, v = img.convert('HSV').split()
np_h = np.array(h, dtype=np.uint8)
# uint8 addition take cares of rotation across boundaries
with np.errstate(over='ignore'):
np_h += np.uint8(hue_factor * 255)
h = Image.fromarray(np_h, 'L')
img = Image.merge('HSV', (h, s, v)).convert(input_mode)
return img
pil_img = Image.fromarray(self.img) pil_img = Image.fromarray(self.img)
# test case with img is not ndarray
with pytest.raises(TypeError): with pytest.raises(TypeError):
mmcv.adjust_hue(pil_img, hue_factor=0.0) mmcv.adjust_hue(pil_img, hue_factor=0.0)
...@@ -408,7 +410,17 @@ class TestPhotometric: ...@@ -408,7 +410,17 @@ class TestPhotometric:
mmcv.adjust_hue(self.img, hue_factor=0.6) mmcv.adjust_hue(self.img, hue_factor=0.6)
for i in np.arange(-0.5, 0.5, 0.2): for i in np.arange(-0.5, 0.5, 0.2):
pil_res = _adjust_hue(pil_img, hue_factor=i) pil_res = mmcv.adjust_hue(self.img, hue_factor=i, backend='pillow')
pil_res = np.array(pil_res) pil_res = np.array(pil_res)
cv2_res = mmcv.adjust_hue(self.img, hue_factor=i) cv2_res = mmcv.adjust_hue(self.img, hue_factor=i)
assert np.allclose(pil_res, cv2_res, atol=10.0) assert np.allclose(pil_res, cv2_res, atol=10.0)
# test pillow backend
with pytest.raises(AssertionError):
mmcv.adjust_hue(
self.img.astype(np.float32), hue_factor=0, backend='pillow')
# backend must be 'cv2' or 'pillow'
with pytest.raises(ValueError):
mmcv.adjust_hue(
self.img.astype(np.uint8), hue_factor=0, backend='not support')
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