Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accurate transformation of pitch dimensions #297

Merged
merged 6 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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