Module deepsport_utilities.transforms
Expand source code
import abc
import copy
import random
import numpy as np
from calib3d.calib import parameters_to_affine_transform
from deepsport_utilities.utils import jpegBlur
class Transform(metaclass=abc.ABCMeta):
def __lt__(self, other):
return self.__repr__() < other.__repr__()
def __gt__(self, other):
return self.__repr__() > other.__repr__()
@abc.abstractmethod
def __call__(self, key, item):
pass
def __repr__(self):
config = getattr(self, "config", {k:v for k, v in self.__dict__.items() if not k.startswith("_")})
attributes = ",".join("{}={}".format(k, v) for k,v in config.items())
return "{}({})".format(self.__class__.__name__, attributes)
class DoNothing(Transform):
def __call__(self, key, item):
return item
class DeclutterItems(Transform):
""" Drops attributes from dataset items. Attributes to drop are given by the
'drop' argument.
"""
def __init__(self, drop):
self.drop = drop
def __call__(self, key, item):
for name in self.drop:
delattr(item, name)
return item
class JPEGCompressionTransform(Transform):
def __init__(self, key_names, q_range=(30,60)):
self.key_names = key_names
self.q_range = q_range
assert len(q_range) == 2 and q_range[0] < q_range[1] and q_range[0] > 0 and q_range[1] <= 100
def __call__(self, key, data):
if data is None:
return None
q = random.randint(*self.q_range)
for k in data:
if k in self.key_names:
data[k] = jpegBlur(data[k], q)
return data
class IncompatibleCropException(ValueError):
pass
class RandomCropperTransform(Transform, metaclass=abc.ABCMeta):
linewidth = 4
random_size = np.random.uniform
def __init__(self, output_shape, size_min, size_max, max_angle=0, do_flip=False, padding=0, margin=0, debug=False, regenerate=False):
""" Randomly scale, crop and rotate dataset items. The scale factor is
randomly selected to keep the given keypoints of interest between
`size_min` and `size_max` (At each call, the current keypoint size
is returned by `_get_current_parameters`).
Arguments:
output_shape: Tuple(int, int) final shape of image-like data.
size_min: (int) lower bound of keypoints random size. If `0`
`size_min` and `size_max` are ignored and no random scaling
is applied.
size_max: (int) upper bound of keypoints random size. If `0`
`size_min` and `size_max` are ignored and no random scaling
is applied.
max_angle: (degrees) positive and negative bounds for random
rotation
do_flip: (bool) tells if random flip should be applied
padding: (px) amount of padding
margin: (px) minimum margin between keypoints and output image
border
debug: (bool) if `True`, doesn't actually crop but display
debug information on image instead.
regenerate: (bool) if `True`, items are (deep)-copied before
calling `_apply_transformation`. Else, transformation can
occur in-place.
"""
self.output_shape = output_shape
self.size_min = size_min
self.size_max = size_max
self.max_angle = max_angle
self.do_flip = do_flip
assert not self.do_flip, "There seem to be a bug in the flip"
self.padding = padding
self.margin = margin
self.debug = debug
self.regenerate = regenerate
def compute(self, input_shape, keypoints, actual_size, size_min=None, size_max=None):
if size_min is not None and self.size_min*self.size_max == 0:
# updated size range after failing to create a crop at given scale
raise IncompatibleCropException("Impossible to crop image without changing the scale")
size_min = size_min or self.size_min
size_max = size_max or self.size_max
if size_min > size_max:
raise IncompatibleCropException("Impossible to crop image with object in the given size range")
target_size = self.random_size(size_min, size_max) if size_min and size_max else actual_size
ratio = target_size/actual_size
tmp_width, tmp_height = [int(x/ratio) for x in self.output_shape]
input_width, input_height = input_shape
# If target size makes the output image bigger than input image, try with a lower size
if tmp_width >= input_width + 2*self.padding or tmp_height >= input_height + 2*self.padding:
if not size_min or not size_max:
raise IncompatibleCropException("Impossible to crop image with object in the given size range")
return self.compute(input_shape, keypoints, actual_size, size_min=target_size*1.1, size_max=size_max)
margin = self.margin / ratio # margin expressed in pixels in the output image
if keypoints is not None:
# Restrict keypoints in input image limits
max_keypoints_x = min(int(np.max(keypoints.x))+margin, input_width)
min_keypoints_x = max(0, int(np.min(keypoints.x))-margin)
max_keypoints_y = min(int(np.max(keypoints.y))+margin, input_height)
min_keypoints_y = max(0, int(np.min(keypoints.y))-margin)
# Compute offsets to fit input image
x_offset_min = max(-self.padding, max_keypoints_x - tmp_width)
x_offset_max = min(min_keypoints_x, input_width + self.padding)
y_offset_min = max(-self.padding, max_keypoints_y - tmp_height)
y_offset_max = min(min_keypoints_y, input_height + self.padding)
else:
x_offset_min = -self.padding
x_offset_max = input_width - tmp_width + self.padding
y_offset_min = -self.padding
y_offset_max = input_height - tmp_height + self.padding
x_offset_max = int(np.ceil(x_offset_max))
x_offset_min = int(np.floor(x_offset_min))
y_offset_max = int(np.ceil(y_offset_max))
y_offset_min = int(np.floor(y_offset_min))
# If target size makes it incompatible with input image shape and keypoints positions, try with a higher size
if x_offset_max < x_offset_min or y_offset_max < y_offset_min:
return self.compute(input_shape, keypoints, actual_size, size_min=size_min, size_max=target_size*0.9)
x_offset = np.random.randint(x_offset_min, x_offset_max) if x_offset_min != x_offset_max else x_offset_max
y_offset = np.random.randint(y_offset_min, y_offset_max) if y_offset_min != y_offset_max else y_offset_max
x_slice = slice(x_offset, x_offset+tmp_width, None)
y_slice = slice(y_offset, y_offset+tmp_height, None)
angle = self.max_angle*(2*np.random.beta(2, 2)-1)
return angle, x_slice, y_slice
@abc.abstractmethod
def _get_current_parameters(self, key, item):
raise NotImplementedError(
"This method should return (keypoints, actual_size, shape) corresponding to the " \
"current keypoints and size of object of interest in the image, as well as the " \
"current image shape (width, height)")
@abc.abstractmethod
def _apply_transformation(self, item, A):
raise NotImplementedError(
"This method should return the final transformed item, based on the original" \
"item and the affine transformation matrix")
def __call__(self, key, item):
if item is None:
return None
parameters = self._get_current_parameters(key, item)
if parameters is None:
return None
keypoints, actual_size, input_shape = parameters
try:
angle, x_slice, y_slice = self.compute(input_shape, keypoints, actual_size)
flip = self.do_flip and bool(np.random.randint(0,2))
except IncompatibleCropException:
return None
A = parameters_to_affine_transform(angle, x_slice, y_slice, self.output_shape, flip)
if self.regenerate:
item = copy.deepcopy(item)
return self._apply_transformation(item, A)
class DataExtractorTransform(Transform):
def __init__(self, *factories):
self.factories = factories
def __call__(self, key, item):
if not item:
return None
data = {}
for factory in self.factories:
if factory is None:
continue
try:
data.update(**factory(key, item))
except:
print(factory)
raise
return data
Classes
class DataExtractorTransform (*factories)
-
Expand source code
class DataExtractorTransform(Transform): def __init__(self, *factories): self.factories = factories def __call__(self, key, item): if not item: return None data = {} for factory in self.factories: if factory is None: continue try: data.update(**factory(key, item)) except: print(factory) raise return data
Ancestors
class DeclutterItems (drop)
-
Drops attributes from dataset items. Attributes to drop are given by the 'drop' argument.
Expand source code
class DeclutterItems(Transform): """ Drops attributes from dataset items. Attributes to drop are given by the 'drop' argument. """ def __init__(self, drop): self.drop = drop def __call__(self, key, item): for name in self.drop: delattr(item, name) return item
Ancestors
class DoNothing
-
Expand source code
class DoNothing(Transform): def __call__(self, key, item): return item
Ancestors
class IncompatibleCropException (*args, **kwargs)
-
Inappropriate argument value (of correct type).
Expand source code
class IncompatibleCropException(ValueError): pass
Ancestors
- builtins.ValueError
- builtins.Exception
- builtins.BaseException
class JPEGCompressionTransform (key_names, q_range=(30, 60))
-
Expand source code
class JPEGCompressionTransform(Transform): def __init__(self, key_names, q_range=(30,60)): self.key_names = key_names self.q_range = q_range assert len(q_range) == 2 and q_range[0] < q_range[1] and q_range[0] > 0 and q_range[1] <= 100 def __call__(self, key, data): if data is None: return None q = random.randint(*self.q_range) for k in data: if k in self.key_names: data[k] = jpegBlur(data[k], q) return data
Ancestors
class RandomCropperTransform (output_shape, size_min, size_max, max_angle=0, do_flip=False, padding=0, margin=0, debug=False, regenerate=False)
-
Randomly scale, crop and rotate dataset items. The scale factor is randomly selected to keep the given keypoints of interest between
size_min
andsize_max
(At each call, the current keypoint size is returned by_get_current_parameters
).Arguments
output_shape: Tuple(int, int) final shape of image-like data. size_min: (int) lower bound of keypoints random size. If
0
size_min
andsize_max
are ignored and no random scaling is applied. size_max: (int) upper bound of keypoints random size. If0
size_min
andsize_max
are ignored and no random scaling is applied. max_angle: (degrees) positive and negative bounds for random rotation do_flip: (bool) tells if random flip should be applied padding: (px) amount of padding margin: (px) minimum margin between keypoints and output image border debug: (bool) ifTrue
, doesn't actually crop but display debug information on image instead. regenerate: (bool) ifTrue
, items are (deep)-copied before calling_apply_transformation
. Else, transformation can occur in-place.Expand source code
class RandomCropperTransform(Transform, metaclass=abc.ABCMeta): linewidth = 4 random_size = np.random.uniform def __init__(self, output_shape, size_min, size_max, max_angle=0, do_flip=False, padding=0, margin=0, debug=False, regenerate=False): """ Randomly scale, crop and rotate dataset items. The scale factor is randomly selected to keep the given keypoints of interest between `size_min` and `size_max` (At each call, the current keypoint size is returned by `_get_current_parameters`). Arguments: output_shape: Tuple(int, int) final shape of image-like data. size_min: (int) lower bound of keypoints random size. If `0` `size_min` and `size_max` are ignored and no random scaling is applied. size_max: (int) upper bound of keypoints random size. If `0` `size_min` and `size_max` are ignored and no random scaling is applied. max_angle: (degrees) positive and negative bounds for random rotation do_flip: (bool) tells if random flip should be applied padding: (px) amount of padding margin: (px) minimum margin between keypoints and output image border debug: (bool) if `True`, doesn't actually crop but display debug information on image instead. regenerate: (bool) if `True`, items are (deep)-copied before calling `_apply_transformation`. Else, transformation can occur in-place. """ self.output_shape = output_shape self.size_min = size_min self.size_max = size_max self.max_angle = max_angle self.do_flip = do_flip assert not self.do_flip, "There seem to be a bug in the flip" self.padding = padding self.margin = margin self.debug = debug self.regenerate = regenerate def compute(self, input_shape, keypoints, actual_size, size_min=None, size_max=None): if size_min is not None and self.size_min*self.size_max == 0: # updated size range after failing to create a crop at given scale raise IncompatibleCropException("Impossible to crop image without changing the scale") size_min = size_min or self.size_min size_max = size_max or self.size_max if size_min > size_max: raise IncompatibleCropException("Impossible to crop image with object in the given size range") target_size = self.random_size(size_min, size_max) if size_min and size_max else actual_size ratio = target_size/actual_size tmp_width, tmp_height = [int(x/ratio) for x in self.output_shape] input_width, input_height = input_shape # If target size makes the output image bigger than input image, try with a lower size if tmp_width >= input_width + 2*self.padding or tmp_height >= input_height + 2*self.padding: if not size_min or not size_max: raise IncompatibleCropException("Impossible to crop image with object in the given size range") return self.compute(input_shape, keypoints, actual_size, size_min=target_size*1.1, size_max=size_max) margin = self.margin / ratio # margin expressed in pixels in the output image if keypoints is not None: # Restrict keypoints in input image limits max_keypoints_x = min(int(np.max(keypoints.x))+margin, input_width) min_keypoints_x = max(0, int(np.min(keypoints.x))-margin) max_keypoints_y = min(int(np.max(keypoints.y))+margin, input_height) min_keypoints_y = max(0, int(np.min(keypoints.y))-margin) # Compute offsets to fit input image x_offset_min = max(-self.padding, max_keypoints_x - tmp_width) x_offset_max = min(min_keypoints_x, input_width + self.padding) y_offset_min = max(-self.padding, max_keypoints_y - tmp_height) y_offset_max = min(min_keypoints_y, input_height + self.padding) else: x_offset_min = -self.padding x_offset_max = input_width - tmp_width + self.padding y_offset_min = -self.padding y_offset_max = input_height - tmp_height + self.padding x_offset_max = int(np.ceil(x_offset_max)) x_offset_min = int(np.floor(x_offset_min)) y_offset_max = int(np.ceil(y_offset_max)) y_offset_min = int(np.floor(y_offset_min)) # If target size makes it incompatible with input image shape and keypoints positions, try with a higher size if x_offset_max < x_offset_min or y_offset_max < y_offset_min: return self.compute(input_shape, keypoints, actual_size, size_min=size_min, size_max=target_size*0.9) x_offset = np.random.randint(x_offset_min, x_offset_max) if x_offset_min != x_offset_max else x_offset_max y_offset = np.random.randint(y_offset_min, y_offset_max) if y_offset_min != y_offset_max else y_offset_max x_slice = slice(x_offset, x_offset+tmp_width, None) y_slice = slice(y_offset, y_offset+tmp_height, None) angle = self.max_angle*(2*np.random.beta(2, 2)-1) return angle, x_slice, y_slice @abc.abstractmethod def _get_current_parameters(self, key, item): raise NotImplementedError( "This method should return (keypoints, actual_size, shape) corresponding to the " \ "current keypoints and size of object of interest in the image, as well as the " \ "current image shape (width, height)") @abc.abstractmethod def _apply_transformation(self, item, A): raise NotImplementedError( "This method should return the final transformed item, based on the original" \ "item and the affine transformation matrix") def __call__(self, key, item): if item is None: return None parameters = self._get_current_parameters(key, item) if parameters is None: return None keypoints, actual_size, input_shape = parameters try: angle, x_slice, y_slice = self.compute(input_shape, keypoints, actual_size) flip = self.do_flip and bool(np.random.randint(0,2)) except IncompatibleCropException: return None A = parameters_to_affine_transform(angle, x_slice, y_slice, self.output_shape, flip) if self.regenerate: item = copy.deepcopy(item) return self._apply_transformation(item, A)
Ancestors
Subclasses
Class variables
var linewidth
Methods
def compute(self, input_shape, keypoints, actual_size, size_min=None, size_max=None)
-
Expand source code
def compute(self, input_shape, keypoints, actual_size, size_min=None, size_max=None): if size_min is not None and self.size_min*self.size_max == 0: # updated size range after failing to create a crop at given scale raise IncompatibleCropException("Impossible to crop image without changing the scale") size_min = size_min or self.size_min size_max = size_max or self.size_max if size_min > size_max: raise IncompatibleCropException("Impossible to crop image with object in the given size range") target_size = self.random_size(size_min, size_max) if size_min and size_max else actual_size ratio = target_size/actual_size tmp_width, tmp_height = [int(x/ratio) for x in self.output_shape] input_width, input_height = input_shape # If target size makes the output image bigger than input image, try with a lower size if tmp_width >= input_width + 2*self.padding or tmp_height >= input_height + 2*self.padding: if not size_min or not size_max: raise IncompatibleCropException("Impossible to crop image with object in the given size range") return self.compute(input_shape, keypoints, actual_size, size_min=target_size*1.1, size_max=size_max) margin = self.margin / ratio # margin expressed in pixels in the output image if keypoints is not None: # Restrict keypoints in input image limits max_keypoints_x = min(int(np.max(keypoints.x))+margin, input_width) min_keypoints_x = max(0, int(np.min(keypoints.x))-margin) max_keypoints_y = min(int(np.max(keypoints.y))+margin, input_height) min_keypoints_y = max(0, int(np.min(keypoints.y))-margin) # Compute offsets to fit input image x_offset_min = max(-self.padding, max_keypoints_x - tmp_width) x_offset_max = min(min_keypoints_x, input_width + self.padding) y_offset_min = max(-self.padding, max_keypoints_y - tmp_height) y_offset_max = min(min_keypoints_y, input_height + self.padding) else: x_offset_min = -self.padding x_offset_max = input_width - tmp_width + self.padding y_offset_min = -self.padding y_offset_max = input_height - tmp_height + self.padding x_offset_max = int(np.ceil(x_offset_max)) x_offset_min = int(np.floor(x_offset_min)) y_offset_max = int(np.ceil(y_offset_max)) y_offset_min = int(np.floor(y_offset_min)) # If target size makes it incompatible with input image shape and keypoints positions, try with a higher size if x_offset_max < x_offset_min or y_offset_max < y_offset_min: return self.compute(input_shape, keypoints, actual_size, size_min=size_min, size_max=target_size*0.9) x_offset = np.random.randint(x_offset_min, x_offset_max) if x_offset_min != x_offset_max else x_offset_max y_offset = np.random.randint(y_offset_min, y_offset_max) if y_offset_min != y_offset_max else y_offset_max x_slice = slice(x_offset, x_offset+tmp_width, None) y_slice = slice(y_offset, y_offset+tmp_height, None) angle = self.max_angle*(2*np.random.beta(2, 2)-1) return angle, x_slice, y_slice
def random_size(...)
-
uniform(low=0.0, high=1.0, size=None)
Draw samples from a uniform distribution.
Samples are uniformly distributed over the half-open interval
[low, high)
(includes low, but excludes high). In other words, any value within the given interval is equally likely to be drawn byuniform
.Note
New code should use the
uniform
method of adefault_rng()
instance instead; please see the :ref:random-quick-start
.Parameters
low
:float
orarray_like
offloats
, optional- Lower boundary of the output interval. All values generated will be greater than or equal to low. The default value is 0.
high
:float
orarray_like
offloats
- Upper boundary of the output interval. All values generated will be less than or equal to high. The default value is 1.0.
size
:int
ortuple
ofints
, optional- Output shape.
If the given shape is, e.g.,
(m, n, k)
, thenm * n * k
samples are drawn. If size isNone
(default), a single value is returned iflow
andhigh
are both scalars. Otherwise,np.broadcast(low, high).size
samples are drawn.
Returns
out
:ndarray
orscalar
- Drawn samples from the parameterized uniform distribution.
See Also
randint
- Discrete uniform distribution, yielding integers.
random_integers
- Discrete uniform distribution over the closed interval
[low, high]
. random_sample
- Floats uniformly distributed over
[0, 1)
. random
- Alias for
random_sample
. rand
- Convenience function that accepts dimensions as input, e.g.,
rand(2,2)
would generate a 2-by-2 array of floats, uniformly distributed over[0, 1)
. Generator.uniform
- which should be used for new code.
Notes
The probability density function of the uniform distribution is
anywhere within the interval
[a, b)
, and zero elsewhere.When
high
==low
, values oflow
will be returned. Ifhigh
<low
, the results are officially undefined and may eventually raise an error, i.e. do not rely on this function to behave when passed arguments satisfying that inequality condition. Thehigh
limit may be included in the returned array of floats due to floating-point rounding in the equationlow + (high-low) * random_sample()
. For example:>>> x = np.float32(5*0.99999999) >>> x 5.0
Examples
Draw samples from the distribution:
>>> s = np.random.uniform(-1,0,1000)
All values are within the given interval:
>>> np.all(s >= -1) True >>> np.all(s < 0) True
Display the histogram of the samples, along with the probability density function:
>>> import matplotlib.pyplot as plt >>> count, bins, ignored = plt.hist(s, 15, density=True) >>> plt.plot(bins, np.ones_like(bins), linewidth=2, color='r') >>> plt.show()
class Transform
-
Expand source code
class Transform(metaclass=abc.ABCMeta): def __lt__(self, other): return self.__repr__() < other.__repr__() def __gt__(self, other): return self.__repr__() > other.__repr__() @abc.abstractmethod def __call__(self, key, item): pass def __repr__(self): config = getattr(self, "config", {k:v for k, v in self.__dict__.items() if not k.startswith("_")}) attributes = ",".join("{}={}".format(k, v) for k,v in config.items()) return "{}({})".format(self.__class__.__name__, attributes)
Subclasses
- CropBlockDividable
- EnhanceColorTransform
- GammaCorrectionTransform
- ProjectBall
- RemoveAnnotationMetadata
- RemoveGroundTruth
- AddBallAnnotation
- AddBallPositionFactory
- AddBallSegmentationTargetViewFactory
- AddBallSizeFactory
- AddBallStateFactory
- AddBallVisibilityFactory
- AddCalibFactory
- AddCourtFactory
- AddDiffFactory
- AddHumansSegmentationTargetViewFactory
- AddImageFactory
- AddNextImageFactory
- BayeringTransform
- ComputeDiff
- GameGammaColorTransform
- GameRGBColorTransform
- UndistortTransform
- DataExtractorTransform
- DeclutterItems
- DoNothing
- JPEGCompressionTransform
- RandomCropperTransform