Module deepsport_utilities.ds.instants_dataset

Expand source code
from .instants_dataset import InstantsDataset, Instant, InstantKey, DownloadFlags, Player, Ball, BallState
from .instants_transforms import GammaCorrectionTransform, CropBlockDividable
from .views_dataset import ViewsDataset, ViewKey, View, BuildBallViews, BuildCameraViews, \
    BuildHeadsViews, BuildCourtViews, BuildPlayersViews, BuildThumbnailViews, BuildBallViewsWithRandom
from .views_transforms import AddBallAnnotation, UndistortTransform, \
    ComputeDiff, GameGammaColorTransform, GameRGBColorTransform, \
    BayeringTransform, ViewRandomCropperTransform, AddCalibFactory, AddCourtFactory, AddDiffFactory, \
    AddNextImageFactory, BallViewRandomCropperTransform
from .dataset_splitters import DeepSportDatasetSplitter, KFoldsArenaLabelsTestingDatasetSplitter, \
    TestingArenaLabelsDatasetSplitter

try:
    from .views_transforms import AddBallDistance
except ImportError:
    pass

# all but "InstantsDataset"
__all__ = ["Instant", "InstantKey", "DownloadFlags", "Player", "BallState",
"Ball", "GammaCorrectionTransform", "ViewsDataset", "ViewKey", "View",
"BuildBallViews", "BuildCameraViews", "AddBallAnnotation", "UndistortTransform",
"DeepSportDatasetSplitter", "KFoldsArenaLabelsTestingDatasetSplitter",
"TestingArenaLabelsDatasetSplitter", "BuildHeadsViews", "BuildCourtViews",
"BuildPlayersViews", "BuildThumbnailViews", "ComputeDiff",
"GameGammaColorTransform", "GameRGBColorTransform", "BayeringTransform",
"ViewRandomCropperTransform", "AddCalibFactory", "AddCourtFactory",
"AddDiffFactory", "AddNextImageFactory", "BallViewRandomCropperTransform",
"CropBlockDividable", "BuildBallViewsWithRandom"]

Sub-modules

deepsport_utilities.ds.instants_dataset.dataset_splitters
deepsport_utilities.ds.instants_dataset.instants_dataset
deepsport_utilities.ds.instants_dataset.instants_transforms
deepsport_utilities.ds.instants_dataset.views_dataset
deepsport_utilities.ds.instants_dataset.views_transforms

Classes

class AddBallAnnotation
Expand source code
class AddBallAnnotation(Transform):
    def __call__(self, key, view):
        balls = [a for a in view.annotations if a.type == 'ball']
        assert len(balls) == 1, f"Expected one ball. received {len(balls)}."
        view.ball = balls[0]
        return view

Ancestors

class AddCalibFactory (as_dict=False)
Expand source code
class AddCalibFactory(Transform):
    def __init__(self, as_dict=False):
        self.as_dict = as_dict
    @staticmethod
    def to_basic_dict(calib):
        return {
            "K": calib.K,
            "r": cv2.Rodrigues(calib.R)[0].flatten(),
            "T": calib.T,
            "width": np.array([calib.width]),
            "height": np.array([calib.height]),
            "kc": np.array(calib.kc),
        }
    def __call__(self, view_key, view):
        if self.as_dict:
            return self.to_basic_dict(view.calib)
        return {"calib": view.calib}

Ancestors

Static methods

def to_basic_dict(calib)
Expand source code
@staticmethod
def to_basic_dict(calib):
    return {
        "K": calib.K,
        "r": cv2.Rodrigues(calib.R)[0].flatten(),
        "T": calib.T,
        "width": np.array([calib.width]),
        "height": np.array([calib.height]),
        "kc": np.array(calib.kc),
    }
class AddCourtFactory
Expand source code
class AddCourtFactory(Transform):
    def __call__(self, view_key, view):
        if not getattr(view, "court", None):
            view.court = Court()
        return {
            "court_width": np.array([view.court.w]),
            "court_height": np.array([view.court.h])
        }

Ancestors

class AddDiffFactory
Expand source code
class AddDiffFactory(Transform):
    def __call__(self, view_key, view):
        raise NotImplementedError() # code needs to be re-implemented: current implementation only adds next image
        return {"input_image2": view.all_images[1]}

Ancestors

class AddNextImageFactory
Expand source code
class AddNextImageFactory(Transform):
    def __call__(self, view_key, view):
        return {"input_image2": view.all_images[1]}

Ancestors

class Ball (data)
Expand source code
class Ball():
    state = BallState.NONE # default
    visible = None         # default
    value = None           # default
    def __init__(self, data):
        self.type = "ball"
        self.center = Point3D(*data['center'])
        self.origin = data.get('origin', "annotation")
        self.camera = data['image']
        self.visible = data.get('visible', None)
        self.state = BallState(data.get('state', 0))
        self.value = data.get('value', None)

    def to_dict(self):
        return {
            "type": self.type,
            "origin": self.origin,
            "center": self.center.to_list(),
            "image": self.camera,
            "visible": self.visible,
            "state": self.state,
            "value": self.value
        }

    def __repr__(self):
        return "Ball(" + ",".join([
            f"origin='{self.origin}', " \
            f"center=({self.center.x:.01f}, {self.center.y:.01f}, {self.center.z:.01f})" \
        ]) + ")"

Subclasses

Class variables

var state
var value
var visible

Methods

def to_dict(self)
Expand source code
def to_dict(self):
    return {
        "type": self.type,
        "origin": self.origin,
        "center": self.center.to_list(),
        "image": self.camera,
        "visible": self.visible,
        "state": self.state,
        "value": self.value
    }
class BallState (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code
class BallState(IntEnum):
    NONE = 0
    FLYING = 1
    CONSTRAINT = 2
    DRIBBLING = 3

Ancestors

  • enum.IntEnum
  • builtins.int
  • enum.Enum

Class variables

var CONSTRAINT
var DRIBBLING
var FLYING
var NONE
class BallViewRandomCropperTransform (*args, on_ball=True, def_min=None, def_max=None, size_min=None, size_max=None, scale_min=None, scale_max=None, rectify=True, **kwargs)

Create random crops with annotated balls visible in them. If [min_size, max_size] range is given, scale factor is chosen s.t. ball size is in the [min_size, max_size] range. If [def_min, def_max] range is given, scale factor is chosen s.t. image definition [px/m] is in the [def_min, def_max] range at ball location. If [scale_min, scale_max] range is given, scale factor is chosen in that range. An error is raised if multiple options are given.

Arguments

on_ball: if True, ball is always visible on the random crop. If False, ball is visible in the random crop half of the time. Else, on_ball is the probability that the ball is kept visible. def_min, def_max: min and max definition [px/m] of the random crop. size_min, size_max: min and max size [px] of the ball in the random crop. scale_min, scale_max: image's min and max scale factor

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: (int) positive and negative bounds for random rotation (in degrees) do_flip: (bool) tells if random flip should be applied padding: (int) amount of padding (in pixels) in input image margin: (int) minimum margin (in pixels) 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.

Expand source code
class BallViewRandomCropperTransform(ViewRandomCropperTransform):
    """ Create random crops with annotated balls visible in them.
        If [min_size, max_size] range is given, scale factor is chosen s.t. ball
        size is in the [min_size, max_size] range.
        If [def_min, def_max] range is given, scale factor is chosen s.t. image
        definition [px/m] is in the [def_min, def_max] range at ball location.
        If [scale_min, scale_max] range is given, scale factor is chosen in
        that range.
        An error is raised if multiple options are given.

        Arguments:
            on_ball: if True, ball is always visible on the random crop.
                If False, ball is visible in the random crop half of the time.
                Else, on_ball is the probability that the ball is kept visible.
            def_min, def_max: min and max definition [px/m] of the random crop.
            size_min, size_max: min and max size [px] of the ball in the random crop.
            scale_min, scale_max: image's min and max scale factor
    """
    def __init__(self, *args, on_ball=True,
                 def_min=None, def_max=None,
                 size_min=None, size_max=None,
                 scale_min=None, scale_max=None,
                 rectify=True, **kwargs):
        msg = "Only one of ('size_min' and 'size_max') or ('def_min' and 'def_max') or ('scale_min' and 'scale_max') should be defined"
        if size_min is not None and size_max is not None:
            assert all([x is None for x in [def_min, def_max, scale_min, scale_max]]), msg
            super().__init__(*args, size_min=size_min, size_max=size_max, **kwargs)
            self.true_size = BALL_DIAMETER
        elif def_min is not None and def_max is not None:
            assert all([x is None for x in [size_min, size_max, scale_min, scale_max]]), msg
            super().__init__(*args, size_min=def_min, size_max=def_max, **kwargs)
            self.true_size = 100
        elif scale_min is not None and scale_max is not None:
            assert all([x is None for x in [size_min, size_max, def_min, def_max]]), msg
            super().__init__(*args, size_min=scale_min, size_max=scale_max, **kwargs)
            self.true_size = None
        else:
            raise ValueError(msg)

        self.rectify = rectify
        self.on_ball = {False: 0.5, True: 1.0, None: 0.0}.get(on_ball, on_ball)
        if self.debug and self.on_ball != 1.0:
            raise NotImplementedError("Random keypoint should be drawn in view.image (as well as it's projection on court for better visualization)")

    def random_ball_position(self, view):
        court = setdefaultattr(view, "court", Court(getattr(view, "rule_type", "FIBA")))
        #court_polygon = view.calib.get_region_visible_corners_2d(court.corners, 1)
        top_edge = list(court.visible_edges(view.calib))[0]
        start = top_edge[0][0][0]
        stop = top_edge[1][0][0]
        x = np.random.beta(2, 2)*(stop-start)+start
        y = np.random.beta(2, 2)*court.h/2+court.h/4
        z = 0
        return Point3D(x,y,z)

    def _apply_transformation(self, view, A):
        if not self.rectify:
            return super()._apply_transformation(view, A)

        # rectify warp caused by projection on a plane
        center = view.__shear_center
        vector = Point2D(center.x - view.calib.K[0,2], center.y - view.calib.K[1,2])
        R1 = np.eye(3)
        R1[0:2,:] = cv2.getRotationMatrix2D(center.to_int_tuple(), np.arctan2(vector.y, vector.x)*180/np.pi, 1.0)
        scale = np.cos(np.arctan(np.linalg.norm(vector)/view.calib.K[0,0]))
        T1 = np.array([[1, 0, -center.x], [0, 1, -center.y], [0, 0, 1]])
        S = np.array([[scale, 0, 0], [0, 1, 0], [0, 0, 1]])
        R2 = np.eye(3)
        R2[0:2,:] = cv2.getRotationMatrix2D((0, 0), -np.arctan2(vector.y, vector.x)*180/np.pi, 1.0)
        T2 = np.array([[1, 0,  center.x], [0, 1,  center.y], [0, 0, 1]])
        return super()._apply_transformation(view, A@T2@R2@S@T1@R1)

    def _get_current_parameters(self, view_key, view):
        ball = getattr(view, "ball", None)

        # If not `on_ball` use the ball anyway half of the samples
        if random.random() < self.on_ball and ball is not None:
            keypoint = ball.center
            view.ball = ball
        else:
            keypoint = self.random_ball_position(view)
            view.ball = Ball({
                'center': keypoint,
                'origin': "random",
                'image': view_key.camera,
                'visible': False,
            })

        # Use ball if any, else use the random ball (it only affects the strategy to scale)
        if self.true_size is None:
            size = 1
        else:
            point3D = ball.center if ball is not None else keypoint
            size = float(view.calib.compute_length2D(point3D, self.true_size)[0])

        keypoint = view.calib.project_3D_to_2D(keypoint)
        input_shape = view.calib.width, view.calib.height
        view.__shear_center = keypoint
        return keypoint, size, input_shape

Ancestors

Subclasses

Methods

def random_ball_position(self, view)
Expand source code
def random_ball_position(self, view):
    court = setdefaultattr(view, "court", Court(getattr(view, "rule_type", "FIBA")))
    #court_polygon = view.calib.get_region_visible_corners_2d(court.corners, 1)
    top_edge = list(court.visible_edges(view.calib))[0]
    start = top_edge[0][0][0]
    stop = top_edge[1][0][0]
    x = np.random.beta(2, 2)*(stop-start)+start
    y = np.random.beta(2, 2)*court.h/2+court.h/4
    z = 0
    return Point3D(x,y,z)

Inherited members

class BayeringTransform
Expand source code
class BayeringTransform(Transform):
    def __init__(self):
        self.R_filter = np.array([[1,0],[0,0]])
        self.G_filter = np.array([[0,1],[1,0]])
        self.B_filter = np.array([[0,0],[0,1]])
    def __call__(self, view_key, view):
        height, width, _ = view.image.shape
        R_mask = np.tile(self.R_filter, [height//2, width//2])
        G_mask = np.tile(self.G_filter, [height//2, width//2])
        B_mask = np.tile(self.B_filter, [height//2, width//2])
        mask = np.stack((R_mask, G_mask, B_mask), axis=2)
        mask = mask[np.newaxis]
        for i, image in enumerate(view.all_images):
            view.all_images[i] = np.sum(image*mask, axis=3)
        view.image = view.all_images[0]

Ancestors

class BuildBallViews (margin, *args, origins=['annotation'], **kwargs)

Margin given in world coordinates by default, except if margin_in_pixels is True.

Expand source code
class BuildBallViews(ViewBuilder):
    def __init__(self, margin, *args, origins=["annotation"], **kwargs):
        super().__init__(margin, *args, **kwargs)
        self.origins = origins
    def __call__(self, instant_key: InstantKey, instant: Instant):
        balls = [a for a in instant.annotations + getattr(instant, "detections", []) if a.type == 'ball']
        predicate = lambda a: a.origin in self.origins
        for idx, ball in enumerate(filter(predicate, balls)):
            if not instant.calibs[ball.camera].projects_in(ball.center):
                continue
            keypoints = [ball.center]
            c = int(ball.camera)
            timestamp = getattr(instant, "timestamps", [instant.timestamp]*instant.num_cameras)[c] # timestamps may be different for each camera
            yield ViewDescription(c, idx, self.compute_box(keypoints, instant.calibs[c]), ball=ball, timestamp=timestamp)

Ancestors

Subclasses

Class variables

var margin : float
var margin_in_pixels : bool
var padding : int
class BuildBallViewsWithRandom (*args, random: float = 0, **kwargs)

random: a proportion of random views that are at least margin away from any created view.

Expand source code
class BuildBallViewsWithRandom(BuildBallViews):
    """ random: a proportion of random views that are at least `margin` away from any created view.
    """
    def __init__(self, *args, random: float=0, **kwargs):
        assert 0 <= random < 1, "random must be in [0,1["
        super().__init__(*args, **kwargs)
        self.random = random
        self.record = {True: 0, False: 0}
    def compute_box(self, point3D_list: list, calib: Calib):
        if self.margin == np.inf:
            return BoundingBox(0, 0, calib.width, calib.height)
        return super().compute_box(point3D_list, calib)
    def __call__(self, instant_key: InstantKey, instant: Instant):
        idx = 0
        boxes = {}
        for view_description in super().__call__(instant_key, instant):
            yield view_description
            idx = view_description.index + 1
            boxes.setdefault(view_description.camera, []).append(view_description.box)
            self.record[True] += 1

        court = instant.court
        while (self.record[False] + 1)/(sum(self.record.values())+1) < self.random:
            camera = np.random.randint(instant.num_cameras)

            top_edge = list(court.visible_edges(instant.calibs[camera]))[0]
            start = top_edge[0][0][0]
            stop = top_edge[1][0][0]
            x = np.random.beta(2, 2)*(stop-start)+start
            y = np.random.beta(2, 2)*court.h/2+court.h/4
            z = np.random.randint(-400, 0)
            point3D = Point3D(x,y,z)
            if not instant.calibs[camera].projects_in(point3D):
                continue

            point2D = instant.calibs[camera].project_3D_to_2D(point3D)
            if any([box.x <= point2D.x <= box.x+box.w and box.y <= point2D.y <= box.y+box.h for box in boxes.get(camera, [])]):
                continue

            ball = Ball({
                'origin': 'random',
                'center': point3D,
                'image': camera,
                'visible': False,
                'status': BallState.NONE,
                'value': 0
            })
            keypoints = [point3D]
            c = int(ball.camera)
            timestamp = getattr(instant, "timestamps", [instant.timestamp]*instant.num_cameras)[c] # timestamps may be different for each camera
            yield ViewDescription(c, idx, self.compute_box(keypoints, instant.calibs[c]), ball=ball, timestamp=timestamp)
            idx += 1
            self.record[False] += 1

Ancestors

Class variables

var margin : float
var margin_in_pixels : bool
var padding : int

Methods

def compute_box(self, point3D_list: list, calib: Calib)
Expand source code
def compute_box(self, point3D_list: list, calib: Calib):
    if self.margin == np.inf:
        return BoundingBox(0, 0, calib.width, calib.height)
    return super().compute_box(point3D_list, calib)
class BuildCameraViews (margin: float = 0, padding: int = None, margin_in_pixels: bool = False)

Builds a view for each camera (margin parameter is useless)

Expand source code
class BuildCameraViews(ViewBuilder):
    """ Builds a view for each camera (margin parameter is useless)
    """
    def __call__(self, instant_key: InstantKey, instant:Instant):
        for c in range(instant.num_cameras):
            yield ViewDescription(c, 0, BoundingBox(0, 0, instant.calibs[c].width, instant.calibs[c].height))

Ancestors

Class variables

var margin : float
var margin_in_pixels : bool
var padding : int
class BuildCourtViews (margin: float = 0, padding: int = None, margin_in_pixels: bool = False, height: float = 300)

Builds a view including all the court keypoints visible on each camera Note: keypoints are duplicated at 2m from the floor

Expand source code
class BuildCourtViews(ViewBuilder):
    """ Builds a view including all the court keypoints visible on each camera
        Note: keypoints are duplicated at 2m from the floor
    """
    height: float = 300
    def __call__(self, instant_key: InstantKey, instant:Instant):
        for c in range(instant.num_cameras):
            calib = instant.calibs[c]
            visible_edges = Court(instant.rule_type).visible_edges(calib)
            court_keypoints = []
            for p1, p2 in visible_edges:
                court_keypoints = court_keypoints + [p1, p1+Point3D(0,0,-self.height), p2, p2+Point3D(0,0,-self.height)]
            yield ViewDescription(c, 0, self.compute_box(court_keypoints, calib))

Ancestors

Class variables

var height : float
class BuildHeadsViews (margin: float = 0, padding: int = None, margin_in_pixels: bool = False)

Margin given in world coordinates by default, except if margin_in_pixels is True.

Expand source code
class BuildHeadsViews(ViewBuilder):
    def __call__(self, instant_key: InstantKey, instant: Instant):
        for idx, player in enumerate([a for a in instant.annotations if a.type == "player"]):
            c = int(player.camera)
            keypoints = [player.head]
            yield ViewDescription(c, idx, self.compute_box(keypoints, instant.calibs[c]), annotation=player)

Ancestors

Class variables

var margin : float
var margin_in_pixels : bool
var padding : int
class BuildPlayersViews (margin: float = 0, padding: int = None, margin_in_pixels: bool = False, min_annotations: int = 1)

Builds a view around the players visible on each camera min_annotations: minimum required number of person to use that camera

Expand source code
class BuildPlayersViews(ViewBuilder):
    """ Builds a view around the players visible on each camera
        min_annotations: minimum required number of person to use that camera
    """
    min_annotations: int = 1
    def __call__(self, instant_key: InstantKey, instant: Instant):
        for c in range(instant.num_cameras):
            annotations = [a for a in instant.annotations if a.camera == c and a.type == "player" and a.team > 0]
            if len(annotations) < self.min_annotations:
                continue

            keypoints = []
            for a in annotations:
                keypoints += [a.head, a.hips, a.foot1, a.foot2]

            yield ViewDescription(c, 0, self.compute_box(keypoints, instant.calibs[c]))

Ancestors

Class variables

var min_annotations : int
class BuildThumbnailViews (margin: float = 0, padding: int = None, margin_in_pixels: bool = False, with_annotations: bool = True, with_detections: bool = False, with_random: Tuple[int, bool] = 0, with_occlusions: bool = False)

Builds a view around each person (players, referee)

Expand source code
class BuildThumbnailViews(ViewBuilder):
    """ Builds a view around each person (players, referee)
    """
    with_annotations: bool = True
    with_detections: bool = False
    with_random: Tuple[int, bool] = 0
    with_occlusions: bool = False
    BODY_HEIGHT = 180
    threshold = 0.25
    def __post_init__(self):
        super().__post_init__()
        self.with_random = 10 if isinstance(self.with_random, bool) and self.with_random else self.with_random

    def check_density_map(self, density_map, box, threshold):
        if 0 in density_map[box.y_slice, box.x_slice].shape:
            return False
        if np.mean(density_map[box.y_slice, box.x_slice]) <= threshold:
            return True
        return False

    def sample_density_map(self, density_map):
        # avoid going too close to other detections
        dilation = cv2.dilate(density_map, np.ones((3,3)), iterations=10)
        indices_y, indices_x = np.where(dilation == 0)
        # random choic a position in the image
        i = np.random.randint(0, len(indices_x))
        return np.array([[indices_x[i], indices_y[i]]])

    @staticmethod
    def fill_density_map(density_map, box):
        density_map[box.y_slice, box.x_slice] += 1

    def __call__(self, instant_key: InstantKey, instant:Instant):
        # Set random seed with timestamp
        random_state = np.random.get_state()
        np.random.seed(instant_key.timestamp & 0xFFFFFFFF)

        instant.density_maps = [np.zeros(img.shape[0:2], dtype=np.uint8) for img in instant.images]

        for c in range(instant.num_cameras):
            calib = instant.calibs[c]
            density_map = instant.density_maps[c]
            index = 0

            # From annotation
            for a in [a for a in instant.annotations if a.type == "player" and calib.projects_in(a.hips) and self.with_annotations]:
                keypoints = [a.head, a.hips, a.foot1, a.foot2]
                box = self.compute_box(keypoints, calib)
                #if self.check_density_map(instant.density_maps, box, c, 1+self.threshold) or self.with_occlusions:
                yield ViewDescription(c, index, box, origin='annotation', annotation=a, density_map=density_map)
                self.fill_density_map(density_map, box)
                index = index + 1

            if self.with_detections:
                # From keemotion foregrounddetector detections
                # FIXME:
                for detection in []:#[d for d in instant.fg_detections if d.camera == c and calib.projects_in(d.feet) and self.with_detections]:
                    keypoints = [ detection.feet, detection.feet + Point3D(0, 0, -self.BODY_HEIGHT) ]
                    box = self.compute_box(keypoints, calib)
                    if self.check_density_map(density_map, box, self.threshold):
                        yield ViewDescription(c, index, box, origin='detection', detection=detection, density_map=density_map)
                        self.fill_density_map(density_map, box)
                        index = index + 1


            # From random
            if self.with_random:
                raise NotImplementedError
                # TODO: use Calib.visible_edge() to replace the function "find_court_intersection_with_camera_border"
                court_keypoints_3D = []# find_court_intersection_with_camera_border(calib, instant.rule_type)
                court_keypoints_2D = np.array([calib.project_3D_to_2D(p).to_list() for p in court_keypoints_3D])
                convex_hull = ConvexHull(court_keypoints_2D)
                points = np.array([court_keypoints_2D[i,:] for i in convex_hull.vertices]).astype(np.int32)
                court = np.ones(instant.images[c].shape[0:2], dtype=np.uint8)
                density_map[cv2.fillPoly(court, [points], 0)==1] += 1

            for _ in range(self.with_random):
                feet = calib.project_2D_to_3D(Point2D(self.sample_density_map(density_map)), Z=0)
                keypoints = [feet, feet + Point3D(0, 0, -self.BODY_HEIGHT)]
                box = self.compute_box(keypoints, calib)
                if self.check_density_map(density_map, box, self.threshold):
                    yield ViewDescription(c, index, box, origin='random', density_map=density_map)
                    self.fill_density_map(density_map, box)
                    index = index + 1

        # Restore random seed
        np.random.set_state(random_state)

Ancestors

Class variables

var BODY_HEIGHT
var threshold
var with_annotations : bool
var with_detections : bool
var with_occlusions : bool
var with_random : Tuple[int, bool]

Static methods

def fill_density_map(density_map, box)
Expand source code
@staticmethod
def fill_density_map(density_map, box):
    density_map[box.y_slice, box.x_slice] += 1

Methods

def check_density_map(self, density_map, box, threshold)
Expand source code
def check_density_map(self, density_map, box, threshold):
    if 0 in density_map[box.y_slice, box.x_slice].shape:
        return False
    if np.mean(density_map[box.y_slice, box.x_slice]) <= threshold:
        return True
    return False
def sample_density_map(self, density_map)
Expand source code
def sample_density_map(self, density_map):
    # avoid going too close to other detections
    dilation = cv2.dilate(density_map, np.ones((3,3)), iterations=10)
    indices_y, indices_x = np.where(dilation == 0)
    # random choic a position in the image
    i = np.random.randint(0, len(indices_x))
    return np.array([[indices_x[i], indices_y[i]]])
class ComputeDiff (squash=False, inplace=False)
Expand source code
class ComputeDiff(Transform):
    def __init__(self, squash=False, inplace=False):
        self.squash = squash
        self.inplace = inplace

    def __call__(self, view_key: ViewKey, view: View):
        diff = np.abs(view.image.astype(np.int32) - view.all_images[1].astype(np.int32)).astype(np.uint8)
        if self.squash:
            diff = np.mean(diff, axis=2).astype(np.uint8)
        if self.inplace:
            view.image = np.dstack((view.image, diff))
        else:
            view.diff = diff
        return view

Ancestors

class CropBlockDividable (block_size=16)
Expand source code
class CropBlockDividable(Transform):
    def __init__(self, block_size=16):
        self.block_size = block_size

    def __call__(self, instant_key: InstantKey, instant: Instant):
        for k, image in instant.all_images.items():
            h, w = image.shape[:2]
            h = h - h % self.block_size
            w = w - w % self.block_size
            instant.all_images[k] = image[:h, :w]
        return instant

Ancestors

class DeepSportDatasetSplitter (validation_pc: int = 15, additional_keys_usage: str = 'skip', folds: str = 'ABCDEF', repetitions: dict = None)

DeepSportDatasetSplitter(validation_pc: int = 15, additional_keys_usage: str = 'skip', folds: str = 'ABCDEF', repetitions: dict = None)

Expand source code
class DeepSportDatasetSplitter: # pylint: disable=too-few-public-methods
    validation_pc: int = 15
    additional_keys_usage: str = "skip"
    folds: str = "ABCDEF"
    split = {
        "A": ['KS-FR-CAEN', 'KS-FR-LIMOGES', 'KS-FR-ROANNE'],
        "B": ['KS-FR-NANTES', 'KS-FR-BLOIS', 'KS-FR-FOS'],
        "C": ['KS-FR-LEMANS', 'KS-FR-MONACO', 'KS-FR-STRASBOURG'],
        "D": ['KS-FR-GRAVELINES', 'KS-FR-STCHAMOND', 'KS-FR-POITIERS'],
        "E": ['KS-FR-NANCY', 'KS-FR-BOURGEB', 'KS-FR-VICHY'],
        # F is a fictive fold for which all keys are used for training
    }
    repetitions: dict = None
    def __post_init__(self):
        self.repetitions = self.repetitions or {}

    def split_keys(self, keys, fold=0):
        fold_index = fold
        assert 0 <= fold_index <= len(self.folds)-1, "Invalid fold index"

        testing_fold = self.folds[fold_index]
        self.testing_arena_labels = set(self.split.get(testing_fold, []))
        remaining_arena_labels = [label for f in self.folds.replace(testing_fold, "") for label in self.split.get(f, [])]
        testing_keys = []
        remaining_keys = defaultdict(list)
        additional_keys = []
        additional_arena_labels = set()
        for key in keys:
            if key.arena_label in remaining_arena_labels:
                remaining_keys[key.instant_key].append(key)
            elif key.arena_label in self.testing_arena_labels:
                testing_keys.append(key)
            else:
                additional_keys.append(key)
                additional_arena_labels.add(key.arena_label)

        # Backup random seed
        random_state = random.getstate()
        random.seed(fold)

        total_length = len(remaining_keys)
        validation_keys, training_keys = [], []
        indices = np.zeros(total_length, dtype=np.int32) # a vector of 1s for validation keys
        indices[np.random.choice(total_length, total_length*self.validation_pc//100, replace=False)] = 1
        for i, instant_key in zip(indices, remaining_keys):
            (training_keys, validation_keys)[i].extend(remaining_keys[instant_key])

        # Restore random seed
        random.setstate(random_state)
        testing2_keys = None
        if additional_keys:
            if self.additional_keys_usage == "testing":
                testing_keys += additional_keys
                self.testing_arena_labels = self.testing_arena_labels.union(additional_arena_labels)
            elif self.additional_keys_usage == "training":
                print(f"WARNING: adding {len(additional_keys)} keys to the training set")
                training_keys += additional_keys
            elif self.additional_keys_usage == "validation":
                validation_keys += additional_keys
            elif self.additional_keys_usage in ["none", "skip"]:
                pass
            elif self.additional_keys_usage == "testing2":
                testing2_keys = additional_keys
            else:
                raise ValueError("They are additional arena labels that I don't know what to do with. Please tell me the 'additional_keys_usage' argument")

        self.testing_arena_labels = list(self.testing_arena_labels)

        return training_keys, validation_keys, testing_keys, testing2_keys

    def __call__(self, dataset, fold=0):
        training_keys, validation_keys, testing_keys, testing2_keys = self.split_keys(dataset.keys, fold)
        subsets = [
            Subset(name="training", subset_type=SubsetType.TRAIN, keys=training_keys, dataset=dataset),
            Subset(name="validation", subset_type=SubsetType.EVAL, keys=validation_keys, dataset=dataset, repetitions=self.repetitions.get('validation', 1)),
            Subset(name="testing", subset_type=SubsetType.EVAL, keys=testing_keys, dataset=dataset, repetitions=self.repetitions.get('testing', 1)),
        ]
        if testing2_keys:
            subsets.append(Subset(name="testing2", subset_type=SubsetType.EVAL, keys=testing2_keys, dataset=dataset, repetitions=1))

        return subsets

Subclasses

Class variables

var additional_keys_usage : str
var folds : str
var repetitions : dict
var split
var validation_pc : int

Methods

def split_keys(self, keys, fold=0)
Expand source code
def split_keys(self, keys, fold=0):
    fold_index = fold
    assert 0 <= fold_index <= len(self.folds)-1, "Invalid fold index"

    testing_fold = self.folds[fold_index]
    self.testing_arena_labels = set(self.split.get(testing_fold, []))
    remaining_arena_labels = [label for f in self.folds.replace(testing_fold, "") for label in self.split.get(f, [])]
    testing_keys = []
    remaining_keys = defaultdict(list)
    additional_keys = []
    additional_arena_labels = set()
    for key in keys:
        if key.arena_label in remaining_arena_labels:
            remaining_keys[key.instant_key].append(key)
        elif key.arena_label in self.testing_arena_labels:
            testing_keys.append(key)
        else:
            additional_keys.append(key)
            additional_arena_labels.add(key.arena_label)

    # Backup random seed
    random_state = random.getstate()
    random.seed(fold)

    total_length = len(remaining_keys)
    validation_keys, training_keys = [], []
    indices = np.zeros(total_length, dtype=np.int32) # a vector of 1s for validation keys
    indices[np.random.choice(total_length, total_length*self.validation_pc//100, replace=False)] = 1
    for i, instant_key in zip(indices, remaining_keys):
        (training_keys, validation_keys)[i].extend(remaining_keys[instant_key])

    # Restore random seed
    random.setstate(random_state)
    testing2_keys = None
    if additional_keys:
        if self.additional_keys_usage == "testing":
            testing_keys += additional_keys
            self.testing_arena_labels = self.testing_arena_labels.union(additional_arena_labels)
        elif self.additional_keys_usage == "training":
            print(f"WARNING: adding {len(additional_keys)} keys to the training set")
            training_keys += additional_keys
        elif self.additional_keys_usage == "validation":
            validation_keys += additional_keys
        elif self.additional_keys_usage in ["none", "skip"]:
            pass
        elif self.additional_keys_usage == "testing2":
            testing2_keys = additional_keys
        else:
            raise ValueError("They are additional arena labels that I don't know what to do with. Please tell me the 'additional_keys_usage' argument")

    self.testing_arena_labels = list(self.testing_arena_labels)

    return training_keys, validation_keys, testing_keys, testing2_keys
class DownloadFlags (value, names=None, *, module=None, qualname=None, type=None, start=1)

An enumeration.

Expand source code
class DownloadFlags(IntFlag):
    NONE = 0
    WITH_IMAGE = 1
    WITH_CALIB_FILE = 2
    #WITH_FOREGROUND_MASK_FILE = 4 # obsolete
    WITH_HUMAN_SEGMENTATION_MASKS = 8
    WITH_FOLLOWING_IMAGE = 16
    WITH_ALL_IMAGES = 32
    ALL = -1

Ancestors

  • enum.IntFlag
  • builtins.int
  • enum.Flag
  • enum.Enum

Class variables

var ALL
var NONE
var WITH_ALL_IMAGES
var WITH_CALIB_FILE
var WITH_FOLLOWING_IMAGE
var WITH_HUMAN_SEGMENTATION_MASKS
var WITH_IMAGE
class GameGammaColorTransform (transform_dict)
Expand source code
class GameGammaColorTransform(Transform):
    def __init__(self, transform_dict):
        assert all([isinstance(k, int) for k in transform_dict.keys()])
        #29582 : [1.04, 1.02, 0.93],
        #24651 : [1.05, 1.02, 0.92],
        #30046 : [1.01, 1.01, 1.01]
        self.transform_dict = transform_dict

    def __call__(self, view_key, view):
        if view_key.instant_key.game_id in self.transform_dict.keys():
            gammas = np.array(self.transform_dict[view_key.instant_key.game_id])
            view.image = gamma_correction(view.image, gammas)
        return view

Ancestors

class GameRGBColorTransform (transform_dict)
Expand source code
class GameRGBColorTransform(Transform):
    def __init__(self, transform_dict):
        assert all([isinstance(k, int) for k in transform_dict.keys()])
        self.transform_dict = transform_dict

    def __call__(self, view_key: ViewKey, view: View):
        if view_key.instant_key.game_id in self.transform_dict.keys():
            adaptation_vector = np.array(self.transform_dict[view_key.instant_key.game_id])
            view.image = np.clip(view.image.astype(np.float32)*adaptation_vector, 0, 255).astype(np.uint8)
        return view

Ancestors

class GammaCorrectionTransform (transform_dict=None)
Expand source code
class GammaCorrectionTransform(Transform):
    def __init__(self, transform_dict=None):
        self.transform_dict = {
            29582 : [1.04, 1.02, 0.93], # Gravelines game
            24651 : [1.05, 1.02, 0.92], # Gravelines game
            69244 : [1.035, 1.025, 0.990], # Gravelines game
            59201 : [1.040, 1.030, 0.990], # Gravelines game
            30046 : [0.98, 0.98, 0.98], # Strasbourg game
            # TODO: LAPUA
            # TODO: ESPOO
            **(transform_dict if transform_dict is not None else {})
        }
        # transform_dict is a dict of game_id, gamma correction triplets
        assert all([isinstance(k, int) for k in self.transform_dict.keys()])

    def __call__(self, instant_key: InstantKey, instant: Instant):
        if instant_key.game_id in self.transform_dict.keys():
            gammas = np.array(self.transform_dict[instant_key.game_id])
            for k, image in instant.all_images.items():
                instant.all_images[k] = gamma_correction(image, gammas)
        return instant

Ancestors

class Instant (db_item, dataset_folder, download_flags)

Python object describing dataset item.

Important

Attributes that require files to be downloaded (like images) should be decorated with functools.cached_property to prevent being read before they get downloaded.

Expand source code
class Instant(GenericItem):
    def __init__(self, db_item, dataset_folder, download_flags):
        self.dataset_folder = dataset_folder
        self.download_flags = download_flags

        self.arena_label = db_item["arena_label"]
        self.num_cameras = db_item["num_cameras"]

        self.game_id = db_item["game_id"]
        self.league_id = db_item["league_id"]
        self.rule_type = db_item["rule_type"]
        self.sport = db_item["sport"]

        self.timestamp = db_item["timestamp"]
        self.offsets = db_item["offsets"]

        self.annotation_state = db_item.get("annotation_state", None)
        self.annotator_id = db_item.get("annotator_id", None)
        self.annotation_ts = db_item.get("annotation_ts", None)
        self.annotation_duration = db_item.get("annotation_duration", None)
        self.annotation_game_state = db_item.get("annotation_game_state", "standard_game")
        self.annotated_human_masks = db_item.get("annotated_human_masks", False)

        self.format =  db_item["format"]

        annotation_map = {
            "player": Player,
            "ball": Ball
        }
        self.annotations = [annotation_map[a['type']](a)for a in (db_item.get('annotations', []) or [])]

        self.image_source = db_item.get("image_source", "raw")
        self.court_dim = COURT_DIM[self.rule_type]
        self.court = Court(self.rule_type)
        self.timestamps = [self.timestamp for _ in range(self.num_cameras)]

    def __str__(self):
        return "({}[{:5d}]@{})".format(self.arena_label, self.game_id, self.timestamp)

    def get_filekey(self, prefix, suffix):
        return os.path.join(self.arena_label, str(self.game_id), "{}{}{}".format(prefix, self.timestamp, suffix))

    @cached_property
    def calibs(self):
        return [self.__load_calib(c) for c in range(self.num_cameras)]

    @cached_property
    def all_images(self):
        _ = self.calibs # triggers calib loading
        all_images = {}
        for c in range(self.num_cameras):
            for idx, offset in enumerate(self.offsets):
                if     (idx == 0) \
                    or (idx == 1 and self.download_flags & DownloadFlags.WITH_FOLLOWING_IMAGE) \
                    or (self.download_flags & DownloadFlags.WITH_ALL_IMAGES):
                    try:
                        all_images[(c,offset)] = self.__load_image(c, offset)
                    except BaseException as e:
                        raise ValueError((self.offsets, self.key)) from e
        return all_images

    @property
    def images(self):
        return [img for (c, offset), img in self.all_images.items() if offset == 0]

    @cached_property
    def human_masks(self):
        assert self.download_flags & DownloadFlags.WITH_HUMAN_SEGMENTATION_MASKS, \
            "Provided flag doesn't contain 'human_masks'. Recreate your dataset with appropriate DownloadFlags"
        try:
            filenames = [os.path.join(self.dataset_folder, self.get_filekey("camcourt{}_".format(cam_idx+1), "_humans.png")) for cam_idx in range(self.num_cameras)]
            return [imageio.imread(filename) for filename in filenames] # imageio handles 16bits images while cv2 doesn't
        except FileNotFoundError:
            # If one human_masks file is missing for one camera, no human_masks will be available.
            # If file is missing because no human appears on that camera, you should upload an empty image to the bucket.
            return []

    def __load_image(self, cam_idx, offset=0):
        filename = os.path.join(self.dataset_folder, self.get_filekey("camcourt{}_".format(cam_idx+1), "_{}.png".format(offset)))
        image = cv2.imread(filename)
        if image is None:
            raise FileNotFoundError(filename)
        return cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

    def __load_calib(self, cam_idx):
        assert self.download_flags & DownloadFlags.WITH_CALIB_FILE, \
            "Provided flag doesn't contain calib files. Recreate your dataset with appropriate DownloadFlags"
        filename = os.path.join(self.dataset_folder, self.get_filekey("camcourt{}_".format(cam_idx+1), ".json"))
        return parse_DeepSport_calib(json.load(open(filename, 'r'))['calibration'])

    def draw(self, i=None, draw_players=True, draw_ball=True, draw_lines=False):
        if i is None:
            w, h = self.court_dim
            image = np.ones((int(h), int(w), 3), np.uint8)*255
            R = np.identity(3)
            C = Point3D(w/2, h/2, -3000)
            m = w/0.01  # pixel_size (pixels/meters) on 1 centimeter sensor
            f = 0.009  # focal (meters)
            R = np.identity(3)
            calib = Calib(width=w, height=h, T=-R@C[:3], R=R, K=np.array([[f*m,  0, w/2], [0, f*m, h/2], [0,  0,  1]]))
        else:
            image = self.images[i].copy()
            calib = self.calibs[i]

        if draw_lines:
            self.court.draw_lines(image, calib)
        # if fg_detections is not None:
        #     calib = self.calibs[i] if i is not None else None
        #     detections = self.fg_detections[i][fg_detections] if i is not None else \
        #                  itertools.chain(*self.fg_detections)
        #     for det in detections:
        #         v = calib.project_3D_to_2D(det.feet).to_int_tuple()
        #         cv2.circle(image, v[0:2].flatten(), 7, [255, 0, 255], -1)

        for annotation in self.annotations:
            if annotation.type == "player" and draw_players:
                head = calib.project_3D_to_2D(annotation.head).to_int_tuple()
                hips = calib.project_3D_to_2D(annotation.hips).to_int_tuple()
                foot1 = calib.project_3D_to_2D(annotation.foot1).to_int_tuple()
                foot2 = calib.project_3D_to_2D(annotation.foot2).to_int_tuple()

                if any([kp[0] < 0 or kp[1] > image.shape[1] or kp[1] < 0 or kp[1] > image.shape[0] for kp in [head, hips]]):
                    continue

                # head tip
                length = 70 # cm
                headT3D = length * Point3D(np.cos(annotation.headAngle), np.sin(annotation.headAngle), 0)
                headT = calib.project_3D_to_2D(annotation.head+headT3D).to_int_tuple()

                color = [0, 0, 0]
                color[annotation.team-1] = 255

                if i is not None:
                    cv2.line(image, hips, foot1, color, 3)
                    cv2.line(image, hips, foot2, color, 3)
                    cv2.line(image, head, hips, color, 3)
                else:
                    cv2.circle(image, head, 5, color, -1)
                cv2.line(image, head, headT, color, 3)

            elif annotation.type == "ball" and draw_ball:
                center = tuple(int(x) for x in calib.project_3D_to_2D(annotation.center).to_list())
                color = [255, 255, 0]
                cv2.circle(image, center, 5, color, -1)
        for detection in getattr(self, "detections", []):
            if detection.type == "ball" and draw_ball:
                center = tuple(int(x) for x in calib.project_3D_to_2D(detection.center).to_list())
                color = [0, 255, 255]
                cv2.circle(image, center, 5, color, -1)
        return image

    @property
    def files(self):
        for i in range(0, int(self.num_cameras)):
            if self.download_flags & DownloadFlags.WITH_IMAGE:
                for idx, offset in enumerate(self.offsets):
                    if     (self.download_flags & DownloadFlags.WITH_ALL_IMAGES) \
                        or (self.download_flags & DownloadFlags.WITH_FOLLOWING_IMAGE and idx == 1) \
                        or (idx == 0):
                        yield self.get_filekey("camcourt{}_".format(i+1), "_{}.png".format(offset))
            if self.download_flags & DownloadFlags.WITH_CALIB_FILE:
                yield self.get_filekey("camcourt{}_".format(i+1), ".json")
            if self.download_flags & DownloadFlags.WITH_HUMAN_SEGMENTATION_MASKS:
                yield self.get_filekey("camcourt{}_".format(i+1), "_humans.png")

    @property
    def key(self):
        return InstantKey(self.arena_label, self.game_id, self.timestamp)

    @property
    def db_item(self):
        db_item = {
            "format": self.format,
            "image_source": self.image_source,

            # arena relative infos
            "arena_label": self.arena_label,
            "num_cameras": self.num_cameras,

            # game relative infos
            "sport": self.sport,
            "game_id": self.game_id,
            "league_id": self.league_id,
            "rule_type": self.rule_type,

            # instant relative infos
            "timestamp": self.timestamp,
            "offsets": self.offsets,

            "annotation_state": self.annotation_state,
            "annotations": [a.to_dict() for a in self.annotations],
            "annotated_human_masks": self.annotated_human_masks
        }

        for attr in ["annotation_ts", "annotator_id", "annotation_duration", "annotation_game_state"]:
            if value := getattr(self, attr, None):
                db_item[attr] = value
        return db_item

    def to_dict(self):
        return {"db_item": self.db_item, "download_flags": self.download_flags, "dataset_folder": self.dataset_folder}

Ancestors

Instance variables

var all_images

Declares a property to be lazy (evaluated only if absent from the object) Also provides a first basic mechanism for a lazy property. Enhanced by Lazy

Expand source code
def __get__(self, instance, ownerclass=None):
    name = self.name
    value = instance.__dict__.get(name, _NOVALUE)
    if value is _NOVALUE:
        try:
            value = self.initializer(instance)
        except AttributeError as e:
            raise LazyPropertyError(name) from e
        instance.__dict__[name] = value
    return value
var calibs

Declares a property to be lazy (evaluated only if absent from the object) Also provides a first basic mechanism for a lazy property. Enhanced by Lazy

Expand source code
def __get__(self, instance, ownerclass=None):
    name = self.name
    value = instance.__dict__.get(name, _NOVALUE)
    if value is _NOVALUE:
        try:
            value = self.initializer(instance)
        except AttributeError as e:
            raise LazyPropertyError(name) from e
        instance.__dict__[name] = value
    return value
var human_masks

Declares a property to be lazy (evaluated only if absent from the object) Also provides a first basic mechanism for a lazy property. Enhanced by Lazy

Expand source code
def __get__(self, instance, ownerclass=None):
    name = self.name
    value = instance.__dict__.get(name, _NOVALUE)
    if value is _NOVALUE:
        try:
            value = self.initializer(instance)
        except AttributeError as e:
            raise LazyPropertyError(name) from e
        instance.__dict__[name] = value
    return value
var images
Expand source code
@property
def images(self):
    return [img for (c, offset), img in self.all_images.items() if offset == 0]

Methods

def draw(self, i=None, draw_players=True, draw_ball=True, draw_lines=False)
Expand source code
def draw(self, i=None, draw_players=True, draw_ball=True, draw_lines=False):
    if i is None:
        w, h = self.court_dim
        image = np.ones((int(h), int(w), 3), np.uint8)*255
        R = np.identity(3)
        C = Point3D(w/2, h/2, -3000)
        m = w/0.01  # pixel_size (pixels/meters) on 1 centimeter sensor
        f = 0.009  # focal (meters)
        R = np.identity(3)
        calib = Calib(width=w, height=h, T=-R@C[:3], R=R, K=np.array([[f*m,  0, w/2], [0, f*m, h/2], [0,  0,  1]]))
    else:
        image = self.images[i].copy()
        calib = self.calibs[i]

    if draw_lines:
        self.court.draw_lines(image, calib)
    # if fg_detections is not None:
    #     calib = self.calibs[i] if i is not None else None
    #     detections = self.fg_detections[i][fg_detections] if i is not None else \
    #                  itertools.chain(*self.fg_detections)
    #     for det in detections:
    #         v = calib.project_3D_to_2D(det.feet).to_int_tuple()
    #         cv2.circle(image, v[0:2].flatten(), 7, [255, 0, 255], -1)

    for annotation in self.annotations:
        if annotation.type == "player" and draw_players:
            head = calib.project_3D_to_2D(annotation.head).to_int_tuple()
            hips = calib.project_3D_to_2D(annotation.hips).to_int_tuple()
            foot1 = calib.project_3D_to_2D(annotation.foot1).to_int_tuple()
            foot2 = calib.project_3D_to_2D(annotation.foot2).to_int_tuple()

            if any([kp[0] < 0 or kp[1] > image.shape[1] or kp[1] < 0 or kp[1] > image.shape[0] for kp in [head, hips]]):
                continue

            # head tip
            length = 70 # cm
            headT3D = length * Point3D(np.cos(annotation.headAngle), np.sin(annotation.headAngle), 0)
            headT = calib.project_3D_to_2D(annotation.head+headT3D).to_int_tuple()

            color = [0, 0, 0]
            color[annotation.team-1] = 255

            if i is not None:
                cv2.line(image, hips, foot1, color, 3)
                cv2.line(image, hips, foot2, color, 3)
                cv2.line(image, head, hips, color, 3)
            else:
                cv2.circle(image, head, 5, color, -1)
            cv2.line(image, head, headT, color, 3)

        elif annotation.type == "ball" and draw_ball:
            center = tuple(int(x) for x in calib.project_3D_to_2D(annotation.center).to_list())
            color = [255, 255, 0]
            cv2.circle(image, center, 5, color, -1)
    for detection in getattr(self, "detections", []):
        if detection.type == "ball" and draw_ball:
            center = tuple(int(x) for x in calib.project_3D_to_2D(detection.center).to_list())
            color = [0, 255, 255]
            cv2.circle(image, center, 5, color, -1)
    return image
def get_filekey(self, prefix, suffix)
Expand source code
def get_filekey(self, prefix, suffix):
    return os.path.join(self.arena_label, str(self.game_id), "{}{}{}".format(prefix, self.timestamp, suffix))
def to_dict(self)
Expand source code
def to_dict(self):
    return {"db_item": self.db_item, "download_flags": self.download_flags, "dataset_folder": self.dataset_folder}

Inherited members

class InstantKey (arena_label: str, game_id: int, timestamp: int)

InstantKey(arena_label, game_id, timestamp)

Expand source code
class InstantKey(NamedTuple):
    arena_label: str
    game_id: int
    timestamp: int

Ancestors

  • builtins.tuple

Instance variables

var arena_label : str

Alias for field number 0

var game_id : int

Alias for field number 1

var timestamp : int

Alias for field number 2

class KFoldsArenaLabelsTestingDatasetSplitter (fold_count=8, validation_pc=15, evaluation_sets_repetitions=5)

DeepSportDatasetSplitter(validation_pc: int = 15, additional_keys_usage: str = 'skip', folds: str = 'ABCDEF', repetitions: dict = None)

Expand source code
class KFoldsArenaLabelsTestingDatasetSplitter(DeepSportDatasetSplitter):
    def __init__(self, fold_count=8, validation_pc=15, evaluation_sets_repetitions=5):
        self.fold_count = fold_count
        self.validation_pc = validation_pc
        self.evaluation_sets_repetitions = evaluation_sets_repetitions

    def __call__(self, dataset, fold=0):
        keys = list(dataset.keys.all())
        assert fold >= 0 and fold < self.fold_count

        keys_dict = count_keys_per_arena_label(keys)
        keys_lists = split_equally(keys_dict, self.fold_count)

        self.testing_arena_labels = keys_lists[fold]
        testing_keys = [k for k in keys if k.arena_label in self.testing_arena_labels]
        remaining_keys = [k for k in keys if k not in testing_keys]

        # Backup random seed
        random_state = random.getstate()
        random.seed(fold)

        validation_keys = random.sample(remaining_keys, len(keys)*self.validation_pc//100)

        # Restore random seed
        random.setstate(random_state)

        training_keys = [k for k in remaining_keys if k not in validation_keys]
        r = self.evaluation_sets_repetitions
        return [
            Subset(name="training", subset_type=SubsetType.TRAIN, keys=training_keys, dataset=dataset),
            Subset(name="validation", subset_type=SubsetType.EVAL, keys=validation_keys, dataset=dataset, repetitions=r),
            Subset(name="testing", subset_type=SubsetType.EVAL, keys=testing_keys, dataset=dataset, repetitions=r),
        ]

Ancestors

Class variables

var additional_keys_usage : str
var folds : str
var repetitions : dict
var validation_pc : int
class Player (data)
Expand source code
class Player():
    def __init__(self, data):
        self.type = "player"
        self.origin = data.get('origin', "annotation")
        self.team = data['team']
        self.head = Point3D(*data['head'])
        self.hips = Point3D(*data['hips'])
        self.foot1 = Point3D(*data['foot1'])
        self.foot2 = Point3D(*data['foot2'])
        self.foot1_at_the_ground = str(data["foot1_at_the_ground"]).lower() == "true"
        self.foot2_at_the_ground = str(data["foot2_at_the_ground"]).lower() == "true"
        self.headAngle = data['headOrientation']
        self.camera = data['image']
        self.hipsAngle = data.get('hipsOrientation', self.headAngle)
        self.feet = (self.foot1 + self.foot2) / 2

    def to_dict(self):
        return {
            "type": self.type,
            "origin": self.origin,
            "team": self.team,
            "head": self.head.to_list(),
            "headOrientation": self.headAngle,
            "hips": self.hips.to_list(),
            "foot1": self.foot1.to_list(),
            "foot2": self.foot2.to_list(),
            "foot1_at_the_ground": self.foot1_at_the_ground,
            "foot2_at_the_ground": self.foot2_at_the_ground,
            "image": self.camera
        }

Methods

def to_dict(self)
Expand source code
def to_dict(self):
    return {
        "type": self.type,
        "origin": self.origin,
        "team": self.team,
        "head": self.head.to_list(),
        "headOrientation": self.headAngle,
        "hips": self.hips.to_list(),
        "foot1": self.foot1.to_list(),
        "foot2": self.foot2.to_list(),
        "foot1_at_the_ground": self.foot1_at_the_ground,
        "foot2_at_the_ground": self.foot2_at_the_ground,
        "image": self.camera
    }
class TestingArenaLabelsDatasetSplitter (testing_arena_labels, validation_pc=15, repetitions=1)
Expand source code
class TestingArenaLabelsDatasetSplitter():
    def __init__(self, testing_arena_labels, validation_pc=15, repetitions=1):
        self.testing_arena_labels = testing_arena_labels
        self.validation_pc = validation_pc
        self.repetitions = repetitions
        assert isinstance(self.testing_arena_labels, (list, tuple))

    def __call__(self, dataset, fold=0):
        testing_keys, remaining_keys = [], []
        for key in dataset.keys:
            (remaining_keys, testing_keys)[key.arena_label in self.testing_arena_labels].append(key)

        # Backup random seed
        np_random_state = np.random.get_state()
        np.random.seed(fold)

        total_length = len(remaining_keys)
        validation_keys, training_keys = [], []
        validation_indices = np.zeros(total_length, dtype=np.int32) # a vector of 1s for validation keys
        validation_indices[np.random.choice(total_length, total_length*self.validation_pc//100, replace=False)] = 1
        for i, key in zip(validation_indices, remaining_keys):
            (training_keys, validation_keys)[i].append(key)

        # Restore random seed
        np.random.set_state(np_random_state)

        subsets = [
            Subset(name="training", subset_type=SubsetType.TRAIN, keys=training_keys, dataset=dataset),
            Subset(name="validation", subset_type=SubsetType.EVAL, keys=validation_keys, dataset=dataset, repetitions=self.repetitions),
            Subset(name="testing", subset_type=SubsetType.EVAL, keys=testing_keys, dataset=dataset, repetitions=self.repetitions),
        ]
        return [s for s in subsets if len(s.keys) > 0]
class UndistortTransform
Expand source code
class UndistortTransform(Transform):
    def __call__(self, key, view):
        all_images = []
        for image in view.all_images:
            all_images.append(cv2.undistort(image, view.calib.K, view.calib.kc))
        view.calib = view.calib.update(kc=np.array([0,0,0,0,0]))
        return view

Ancestors

class View (all_images, calib, annotations=None, **kwargs)
Expand source code
class View():
    def __init__(self, all_images, calib, annotations=None, **kwargs):
        self.image = all_images[0]
        self.all_images = all_images
        self.calib = calib
        self.annotations = annotations
        for k, v in kwargs.items():
            setattr(self, k, v)

    def draw(self):
        image = self.image.copy()

        coord_2D_int = lambda x: self.calib.project_3D_to_2D(x).to_int_tuple()

        for annotation in self.annotations:
            if annotation.type == "player":
                head = annotation.head
                hips = annotation.hips
                foot1 = annotation.foot1
                foot2 = annotation.foot2

                color = [0, 0, 0]
                color[annotation.team-1] = 255
                cv2.line(image, coord_2D_int(head), coord_2D_int(hips), color, 3)
                cv2.line(image, coord_2D_int(hips), coord_2D_int(foot1), color, 3)
                cv2.line(image, coord_2D_int(hips), coord_2D_int(foot2), color, 3)

            elif annotation.type == "ball":
                ball =  annotation.center
                color = [255, 255, 0]
                cv2.circle(image, coord_2D_int(ball), 5, color, -1)

        return image

Methods

def draw(self)
Expand source code
def draw(self):
    image = self.image.copy()

    coord_2D_int = lambda x: self.calib.project_3D_to_2D(x).to_int_tuple()

    for annotation in self.annotations:
        if annotation.type == "player":
            head = annotation.head
            hips = annotation.hips
            foot1 = annotation.foot1
            foot2 = annotation.foot2

            color = [0, 0, 0]
            color[annotation.team-1] = 255
            cv2.line(image, coord_2D_int(head), coord_2D_int(hips), color, 3)
            cv2.line(image, coord_2D_int(hips), coord_2D_int(foot1), color, 3)
            cv2.line(image, coord_2D_int(hips), coord_2D_int(foot2), color, 3)

        elif annotation.type == "ball":
            ball =  annotation.center
            color = [255, 255, 0]
            cv2.circle(image, coord_2D_int(ball), 5, color, -1)

    return image
class ViewKey (instant_key: InstantKey, camera: int, index: int)

ViewKey(instant_key, camera, index)

Expand source code
class ViewKey(NamedTuple):
    instant_key: InstantKey
    camera: int
    index: int

    @property
    def arena_label(self):
        return self.instant_key.arena_label

    @property
    def timestamp(self):
        return self.instant_key.timestamp

    @property
    def game_id(self):
        return self.instant_key.game_id

Ancestors

  • builtins.tuple

Instance variables

var arena_label
Expand source code
@property
def arena_label(self):
    return self.instant_key.arena_label
var camera : int

Alias for field number 1

var game_id
Expand source code
@property
def game_id(self):
    return self.instant_key.game_id
var index : int

Alias for field number 2

var instant_keyInstantKey

Alias for field number 0

var timestamp
Expand source code
@property
def timestamp(self):
    return self.instant_key.timestamp
class ViewRandomCropperTransform (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: (int) positive and negative bounds for random rotation (in degrees) do_flip: (bool) tells if random flip should be applied padding: (int) amount of padding (in pixels) in input image margin: (int) minimum margin (in pixels) 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.

Expand source code
class ViewRandomCropperTransform(RandomCropperTransform):
    def _apply_transformation(self, view, A):
        if self.debug:
            w, h = self.output_shape
            points = Point2D(np.linalg.inv(A)@Point2D([0,0,w,w],[0,h,h,0]).H)
            cv2.polylines(view.image, [points.T.astype(np.int32)], True, (255,0,0), self.linewidth)
        else:
            view.image = cv2.warpAffine(view.image, A[0:2,:], self.output_shape, flags=cv2.INTER_LINEAR)
            view.calib = view.calib.update(K=A@view.calib.K, width=self.output_shape[0], height=self.output_shape[1])

            if hasattr(view, "all_images"):
                for i in range(1, len(view.all_images)): # skip first image as it was already done
                    view.all_images[i] = cv2.warpAffine(view.all_images[i], A[0:2,:], self.output_shape, flags=cv2.INTER_LINEAR)
            if hasattr(view, "human_masks") and view.human_masks is not None:
                view.human_masks = cv2.warpAffine(view.human_masks, A[0:2,:], self.output_shape, flags=cv2.INTER_NEAREST)
        return view

Ancestors

Subclasses

Inherited members

class ViewsDataset (instants_dataset: InstantsDataset, view_builder: ViewBuilder, output_shape=None, rescale=True, crop=True, debug=False)

Dataset of views built according the given ViewBuilder, extracted from the given InstantDataset.

Arguments

instants_dataset - the InstantDataset from which views are built view_builder - the ViewBuilder dictating what type of view is to be created output_shape - view aspect ratio (or exact dimension if 'rescale' is given) rescale - tells whether the view should be rescaled to output_shape size crop - tells whether the original image should be cropped or a rectangle should be drawn instead (for debug purposes)

Expand source code
class ViewsDataset(AugmentedDataset):
    """ Dataset of views built according the given ViewBuilder, extracted from the given InstantDataset.
        Arguments:
            instants_dataset   - the InstantDataset from which views are built
            view_builder       - the ViewBuilder dictating what type of view is to be created
            output_shape       - view aspect ratio (or exact dimension if 'rescale' is given)
            rescale            - tells whether the view should be rescaled to output_shape size
            crop               - tells whether the original image should be cropped or a rectangle
                                 should be drawn instead (for debug purposes)
    """
    def __init__(self, instants_dataset: InstantsDataset, view_builder: ViewBuilder, output_shape=None, rescale=True, crop=True, debug=False):
        super().__init__(instants_dataset)
        self.view_builder = view_builder
        self.output_shape = output_shape
        self.rescale = rescale
        self.crop = crop
        self.debug = debug

    def _crop_view(self, view_description: ViewDescription, instant: Instant, **kwargs):
        padding = self.view_builder.padding
        c = view_description.camera
        input_height, input_width, _ = instant.images[c].shape
        aspect_ratio = self.output_shape[0]/self.output_shape[1] if self.output_shape else None
        x_slice, y_slice = view_description.box.increase_box(
            max_width=input_width,
            max_height=input_height,
            aspect_ratio=aspect_ratio,
            padding=self.view_builder.padding
        )

        if self.crop:
            all_images = []
            for offset in instant.offsets:
                index = (c,offset)
                if index not in instant.all_images:
                    continue # that offset was not downloaded with the download flag of instants dataset
                image = crop_padded(instant.all_images[index], x_slice, y_slice, padding)
                height, width, _ = image.shape
                if self.rescale and self.output_shape and (width, height) != self.output_shape:
                    image = cv2.resize(image, self.output_shape)
                all_images.append(image)
            calib = instant.calibs[c].crop(x_slice, y_slice) # handles negative `slice.start` positions
            if self.rescale and self.output_shape:
                calib = calib.scale(*self.output_shape)
            if instant.download_flags & DownloadFlags.WITH_HUMAN_SEGMENTATION_MASKS and instant.human_masks:
                human_masks = crop_padded(instant.human_masks[c], x_slice, y_slice, padding)
                if self.rescale and self.output_shape:
                    human_masks = cv2.resize(human_masks, self.output_shape)
            else:
                human_masks = None
        else:
            if self.debug:
                all_images = []
                for offset in instant.offsets:
                    # the coordinates of the rectangle below are probably wrong see documentation of cv2.rectangle
                    raise NotImplementedError("TODO: check rectangle coordinates")
                    image = cv2.rectangle(instant.all_images[(c, offset)], (x_slice.start, x_slice.stop), (y_slice.start, y_slice.stop), (255,0,0), 10)
                    all_images.append(image)
            else:
                all_images = instant.all_images
            calib = instant.calibs[c]
            human_masks = instant.human_masks[c]

        return View(all_images, calib, instant.annotations, rule_type=instant.rule_type, human_masks=human_masks, **kwargs)

    def augment(self, instant_key: InstantKey, instant: Instant):
        random_state = np.random.get_state()
        np.random.seed(instant_key.timestamp & 0xFFFFFFFF)

        for view_description in self.view_builder(instant_key, instant):
            view_key = ViewKey(instant_key, view_description.camera, view_description.index)
            yield view_key, self._crop_view(view_description, instant, **view_description.data)

        # required to keep the random seed random
        np.random.set_state(random_state)

Ancestors

  • mlworkflow.datasets.AugmentedDataset
  • mlworkflow.datasets.Dataset

Methods

def augment(self, instant_key: InstantKey, instant: Instant)
Expand source code
def augment(self, instant_key: InstantKey, instant: Instant):
    random_state = np.random.get_state()
    np.random.seed(instant_key.timestamp & 0xFFFFFFFF)

    for view_description in self.view_builder(instant_key, instant):
        view_key = ViewKey(instant_key, view_description.camera, view_description.index)
        yield view_key, self._crop_view(view_description, instant, **view_description.data)

    # required to keep the random seed random
    np.random.set_state(random_state)