Skip to content

Commit

Permalink
Merge pull request #297 from probberechts/feat/pitchdimensions
Browse files Browse the repository at this point in the history
Accurate transformation of pitch dimensions
  • Loading branch information
koenvo authored Apr 4, 2024
2 parents d53550f + 355bde9 commit c00a461
Show file tree
Hide file tree
Showing 33 changed files with 1,058 additions and 278 deletions.
215 changes: 129 additions & 86 deletions kloppy/domain/models/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,17 @@
else:
from typing_extensions import Self

from .pitch import PitchDimensions, Point, Dimension
from .pitch import (
PitchDimensions,
Unit,
Point,
Dimension,
NormalizedPitchDimensions,
MetricPitchDimensions,
ImperialPitchDimensions,
OptaPitchDimensions,
WyscoutPitchDimensions,
)
from .formation import FormationType
from ...exceptions import (
OrientationError,
Expand Down Expand Up @@ -449,9 +459,8 @@ def __str__(self):

@dataclass
class CoordinateSystem(ABC):
normalized: bool
length: float = None
width: float = None
pitch_length: Optional[float] = None
pitch_width: Optional[float] = None

def __eq__(self, other):
if isinstance(other, CoordinateSystem):
Expand Down Expand Up @@ -483,6 +492,10 @@ def vertical_orientation(self) -> VerticalOrientation:
def pitch_dimensions(self) -> PitchDimensions:
raise NotImplementedError

@property
def normalized(self) -> bool:
return isinstance(self.pitch_dimensions, NormalizedPitchDimensions)


@dataclass
class KloppyCoordinateSystem(CoordinateSystem):
Expand All @@ -500,17 +513,21 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
if self.length is not None and self.width is not None:
return PitchDimensions(
if self.pitch_length is not None and self.pitch_width is not None:
return NormalizedPitchDimensions(
x_dim=Dimension(0, 1),
y_dim=Dimension(0, 1),
length=self.length,
width=self.width,
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)
else:
return PitchDimensions(
return NormalizedPitchDimensions(
x_dim=Dimension(0, 1),
y_dim=Dimension(0, 1),
pitch_length=105,
pitch_width=68,
standardized=True,
)


Expand All @@ -537,12 +554,13 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return PitchDimensions(
x_dim=Dimension(-1 * self.length * 100 / 2, self.length * 100 / 2),
y_dim=Dimension(-1 * self.width * 100 / 2, self.width * 100 / 2),
length=self.length,
width=self.width,
)
return MetricPitchDimensions(
x_dim=Dimension(-1 * self.pitch_length / 2, self.pitch_length / 2),
y_dim=Dimension(-1 * self.pitch_width / 2, self.pitch_width / 2),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
).convert(to_unit=Unit.CENTIMETERS)


@dataclass
Expand All @@ -561,11 +579,12 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return PitchDimensions(
x_dim=Dimension(-1 * self.length / 2, self.length / 2),
y_dim=Dimension(-1 * self.width / 2, self.width / 2),
length=self.length,
width=self.width,
return MetricPitchDimensions(
x_dim=Dimension(-1 * self.pitch_length / 2, self.pitch_length / 2),
y_dim=Dimension(-1 * self.pitch_width / 2, self.pitch_width / 2),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)


Expand All @@ -585,9 +604,8 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return PitchDimensions(
x_dim=Dimension(0, 100),
y_dim=Dimension(0, 100),
return OptaPitchDimensions(
pitch_length=self.pitch_length, pitch_width=self.pitch_width
)


Expand All @@ -607,11 +625,12 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return PitchDimensions(
x_dim=Dimension(0, self.length),
y_dim=Dimension(0, self.width),
length=self.length,
width=self.width,
return MetricPitchDimensions(
x_dim=Dimension(0, self.pitch_length),
y_dim=Dimension(0, self.pitch_width),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)


Expand All @@ -631,11 +650,12 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return PitchDimensions(
x_dim=Dimension(-self.length / 2, self.length / 2),
y_dim=Dimension(-self.width / 2, self.width / 2),
length=self.length,
width=self.width,
return MetricPitchDimensions(
x_dim=Dimension(-self.pitch_length / 2, self.pitch_length / 2),
y_dim=Dimension(-self.pitch_width / 2, self.pitch_width / 2),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)


Expand All @@ -655,9 +675,12 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return PitchDimensions(
return ImperialPitchDimensions(
x_dim=Dimension(0, 120),
y_dim=Dimension(0, 80),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=True,
)


Expand All @@ -676,9 +699,8 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return PitchDimensions(
x_dim=Dimension(0, 100),
y_dim=Dimension(0, 100),
return WyscoutPitchDimensions(
pitch_length=self.pitch_length, pitch_width=self.pitch_width
)


Expand All @@ -698,11 +720,12 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return PitchDimensions(
x_dim=Dimension(-1 * self.length / 2, self.length / 2),
y_dim=Dimension(-1 * self.width / 2, self.width / 2),
length=self.length,
width=self.width,
return MetricPitchDimensions(
x_dim=Dimension(-1 * self.pitch_length / 2, self.pitch_length / 2),
y_dim=Dimension(-1 * self.pitch_width / 2, self.pitch_width / 2),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)


Expand All @@ -722,10 +745,22 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return PitchDimensions(
x_dim=Dimension(-1, 1),
y_dim=Dimension(-1, 1),
)
if self.pitch_length is not None and self.pitch_width is not None:
return NormalizedPitchDimensions(
x_dim=Dimension(-1, 1),
y_dim=Dimension(-1, 1),
pitch_length=self.pitch_length,
pitch_width=self.pitch_width,
standardized=False,
)
else:
return NormalizedPitchDimensions(
x_dim=Dimension(-1, 1),
y_dim=Dimension(-1, 1),
pitch_length=105,
pitch_width=68,
standardized=True,
)


@dataclass
Expand All @@ -744,11 +779,13 @@ def vertical_orientation(self) -> VerticalOrientation:

@property
def pitch_dimensions(self) -> PitchDimensions:
return PitchDimensions(
# FIXME: This does not seem correct
return NormalizedPitchDimensions(
x_dim=Dimension(0, 100),
y_dim=Dimension(0, 100),
length=self.length,
width=self.width,
pitch_length=105,
pitch_width=68,
standardized=True,
)


Expand All @@ -771,45 +808,51 @@ def __repr__(self):


def build_coordinate_system(
provider: Provider, dataset_type: DatasetType = DatasetType.EVENT, **kwargs
):
if provider == Provider.TRACAB:
return TracabCoordinateSystem(normalized=False, **kwargs)

if provider == Provider.KLOPPY:
return KloppyCoordinateSystem(normalized=True, **kwargs)

if provider == Provider.METRICA:
return MetricaCoordinateSystem(normalized=True, **kwargs)

if provider == Provider.OPTA:
return OptaCoordinateSystem(normalized=False, **kwargs)

if provider == Provider.SPORTEC:
if dataset_type == DatasetType.TRACKING:
return SportecTrackingDataCoordinateSystem(
normalized=False, **kwargs
provider: Provider,
dataset_type: DatasetType = DatasetType.EVENT,
pitch_length: Optional[float] = None,
pitch_width: Optional[float] = None,
) -> CoordinateSystem:
"""Build a coordinate system for a given provider and dataset type.
Args:
provider: The provider of the dataset.
dataset_type: The type of the dataset.
pitch_length: The real length of the pitch.
pitch_width: The real width of the pitch.
Returns:
The coordinate system for the given provider and dataset type.
"""
coordinate_systems = {
Provider.TRACAB: TracabCoordinateSystem,
Provider.KLOPPY: KloppyCoordinateSystem,
Provider.METRICA: MetricaCoordinateSystem,
Provider.OPTA: OptaCoordinateSystem,
Provider.SPORTEC: {
DatasetType.EVENT: SportecEventDataCoordinateSystem,
DatasetType.TRACKING: SportecTrackingDataCoordinateSystem,
},
Provider.STATSBOMB: StatsBombCoordinateSystem,
Provider.WYSCOUT: WyscoutCoordinateSystem,
Provider.SKILLCORNER: SkillCornerCoordinateSystem,
Provider.DATAFACTORY: DatafactoryCoordinateSystem,
Provider.SECONDSPECTRUM: SecondSpectrumCoordinateSystem,
Provider.STATSPERFORM: StatsPerformCoordinateSystem,
}

if provider in coordinate_systems:
if isinstance(coordinate_systems[provider], dict):
assert dataset_type in coordinate_systems[provider]
return coordinate_systems[provider][dataset_type](
pitch_length=pitch_length, pitch_width=pitch_width
)
else:
return SportecEventDataCoordinateSystem(normalized=False, **kwargs)

if provider == Provider.STATSBOMB:
return StatsBombCoordinateSystem(normalized=False, **kwargs)

if provider == Provider.WYSCOUT:
return WyscoutCoordinateSystem(normalized=False, **kwargs)

if provider == Provider.SKILLCORNER:
return SkillCornerCoordinateSystem(normalized=False, **kwargs)

if provider == Provider.DATAFACTORY:
return DatafactoryCoordinateSystem(normalized=False, **kwargs)

if provider == Provider.SECONDSPECTRUM:
return SecondSpectrumCoordinateSystem(normalized=False, **kwargs)

if provider == Provider.STATSPERFORM:
return StatsPerformCoordinateSystem(normalized=False, **kwargs)
return coordinate_systems[provider](
pitch_length=pitch_length, pitch_width=pitch_width
)
else:
raise ValueError(f"Invalid provider: {provider}")


class DatasetFlag(Flag):
Expand Down
Loading

0 comments on commit c00a461

Please sign in to comment.