diff --git a/kloppy/domain/models/pitch.py b/kloppy/domain/models/pitch.py index 6acec4845..2b4e7ce9a 100644 --- a/kloppy/domain/models/pitch.py +++ b/kloppy/domain/models/pitch.py @@ -417,6 +417,7 @@ def from_metric_base( Returns: The point in the regular pitch dimensions """ + if ( self.x_dim.min is None or self.x_dim.max is None diff --git a/kloppy/domain/services/transformers/dataset.py b/kloppy/domain/services/transformers/dataset.py index 3e2890669..075c9a322 100644 --- a/kloppy/domain/services/transformers/dataset.py +++ b/kloppy/domain/services/transformers/dataset.py @@ -115,8 +115,6 @@ def change_point_dimensions( point_base = self._from_pitch_dimensions.to_metric_base( point, pitch_length=base_pitch_length, pitch_width=base_pitch_width ) - print(point_base) - print(self._to_pitch_dimensions.from_metric_base) point_to = self._to_pitch_dimensions.from_metric_base( point=point_base, pitch_length=base_pitch_length, @@ -179,26 +177,36 @@ def __needs_flip( ) return flip - def transform_frame(self, frame: Frame) -> Frame: + def transform_frame( + self, frame: Frame, transform_ball_coordinates: bool = True + ) -> Frame: # Change coordinate system if self._needs_coordinate_system_change: - frame = self.__change_frame_coordinate_system(frame) + frame = self.__change_frame_coordinate_system( + frame, transform_ball_coordinates=transform_ball_coordinates + ) # Change dimensions elif self._needs_pitch_dimensions_change: - frame = self.__change_frame_dimensions(frame) + frame = self.__change_frame_dimensions( + frame, transform_ball_coordinates=transform_ball_coordinates + ) # Flip frame based on orientation if self._needs_orientation_change: if self.__needs_flip( - period=frame.period, - ball_owning_team=frame.ball_owning_team, + period=frame.period, ball_owning_team=frame.ball_owning_team ): - frame = self.__flip_frame(frame) + frame = self.__flip_frame( + frame, + transform_ball_coordinates=transform_ball_coordinates, + ) return frame - def __change_frame_coordinate_system(self, frame: Frame): + def __change_frame_coordinate_system( + self, frame: Frame, transform_ball_coordinates: bool = True + ): return Frame( # doesn't change timestamp=frame.timestamp, @@ -209,7 +217,9 @@ def __change_frame_coordinate_system(self, frame: Frame): # changes ball_coordinates=self.__change_point_coordinate_system( frame.ball_coordinates - ), + ) + if transform_ball_coordinates + else frame.ball_coordinates, ball_speed=frame.ball_speed, players_data={ key: PlayerData( @@ -226,7 +236,9 @@ def __change_frame_coordinate_system(self, frame: Frame): statistics=frame.statistics, ) - def __change_frame_dimensions(self, frame: Frame): + def __change_frame_dimensions( + self, frame: Frame, transform_ball_coordinates: bool = True + ): return Frame( # doesn't change timestamp=frame.timestamp, @@ -237,7 +249,9 @@ def __change_frame_dimensions(self, frame: Frame): # changes ball_coordinates=self.change_point_dimensions( frame.ball_coordinates - ), + ) + if transform_ball_coordinates + else frame.ball_coordinates, players_data={ key: PlayerData( coordinates=self.change_point_dimensions( @@ -278,7 +292,6 @@ def __change_point_coordinate_system( point_base, y=base_pitch_width - point_base.y, ) - point_to = self._to_pitch_dimensions.from_metric_base( point_base, pitch_length=base_pitch_length, @@ -287,7 +300,9 @@ def __change_point_coordinate_system( return point_to - def __flip_frame(self, frame: Frame): + def __flip_frame( + self, frame: Frame, transform_ball_coordinates: bool = True + ): players_data = {} for player, data in frame.players_data.items(): players_data[player] = PlayerData( @@ -305,7 +320,9 @@ def __flip_frame(self, frame: Frame): ball_state=frame.ball_state, period=frame.period, # changes - ball_coordinates=self.flip_point(frame.ball_coordinates), + ball_coordinates=self.flip_point(frame.ball_coordinates) + if transform_ball_coordinates + else frame.ball_coordinates, players_data=players_data, other_data=frame.other_data, statistics=frame.statistics, @@ -329,8 +346,8 @@ def transform_event(self, event: Event) -> Event: ): event = self.__flip_event(event) - if event.freeze_frame: - event.freeze_frame = self.transform_frame(event.freeze_frame) + if event.freeze_frame: + event.freeze_frame = self.transform_frame(event.freeze_frame) return event diff --git a/kloppy/infra/serializers/event/statsbomb/deserializer.py b/kloppy/infra/serializers/event/statsbomb/deserializer.py index ff007af07..52d2112b8 100644 --- a/kloppy/infra/serializers/event/statsbomb/deserializer.py +++ b/kloppy/infra/serializers/event/statsbomb/deserializer.py @@ -69,6 +69,7 @@ def deserialize( # Create events with performance_logging("parse events", logger=logger): events = [] + freeze_frames = [] for raw_event in raw_events.values(): new_events = ( raw_event.set_version(data_version) @@ -94,6 +95,7 @@ def deserialize( **additional_metadata, ) dataset = EventDataset(metadata=metadata, records=events) + for event in dataset: if "freeze_frame" in event.raw_event.get("shot", {}): event.freeze_frame = self.transformer.transform_frame( @@ -103,7 +105,8 @@ def deserialize( away_team=teams[1], event=event, fidelity_version=data_version.shot_fidelity_version, - ) + ), + transform_ball_coordinates=False, ) if not event.freeze_frame and event.event_id in three_sixty_data: freeze_frame = three_sixty_data[event.event_id] @@ -115,7 +118,8 @@ def deserialize( event=event, fidelity_version=data_version.xy_fidelity_version, visible_area=freeze_frame["visible_area"], - ) + ), + transform_ball_coordinates=False, ) return dataset diff --git a/kloppy/tests/test_statsbomb.py b/kloppy/tests/test_statsbomb.py index c2d2e9a14..3f7ebda7a 100644 --- a/kloppy/tests/test_statsbomb.py +++ b/kloppy/tests/test_statsbomb.py @@ -85,6 +85,26 @@ def dataset() -> EventDataset: return dataset +@pytest.fixture(scope="module") +def dataset_kl() -> EventDataset: + """Load StatsBomb data for Belgium - Portugal at Euro 2020""" + dataset = statsbomb.load( + event_data=f"{API_URL}/events/3794687.json", + lineup_data=f"{API_URL}/lineups/3794687.json", + three_sixty_data=f"{API_URL}/three-sixty/3794687.json", + coordinates="kloppy", + additional_metadata={ + "date": datetime(2020, 8, 23, 0, 0, tzinfo=timezone.utc), + "game_week": "7", + "game_id": "3888787", + "home_coach": "R. Martínez Montoliù", + "away_coach": "F. Fernandes da Costa Santos", + }, + ) + assert dataset.dataset_type == DatasetType.EVENT + return dataset + + def test_get_enum_type(): """Test retrieving enum types for StatsBomb IDs""" # retrieve by id @@ -417,6 +437,21 @@ def get_color(player): base_dir / "outputs" / "test_statsbomb_freeze_frame_shot.png" ) + def test_freeze_frame_360_transform(self, dataset_kl: EventDataset): + post_transform_pass = dataset_kl.transform( + to_coordinate_system="secondspectrum", + to_orientation="ACTION_EXECUTING_TEAM", + ).filter(lambda event: event.event_type == EventType.PASS)[4] + + assert ( + post_transform_pass.coordinates.x + == post_transform_pass.freeze_frame.ball_coordinates.x + ) + assert ( + post_transform_pass.coordinates.y + == post_transform_pass.freeze_frame.ball_coordinates.y + ) + def test_freeze_frame_360(self, dataset: EventDataset, base_dir: Path): """Test if 360 freeze-frame is properly parsed and attached to shot events""" pass_event = dataset.get_event_by_id(