Skip to content

Commit

Permalink
Clean-up.
Browse files Browse the repository at this point in the history
  • Loading branch information
DivvyCr committed Aug 3, 2020
1 parent aa9bd9f commit 6922200
Showing 1 changed file with 78 additions and 36 deletions.
114 changes: 78 additions & 36 deletions carball/analysis/events/bump_detection/bump_analysis.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import itertools
import logging
from typing import Dict

import numpy as np
import pandas as pd
from carball.generated.api.player_pb2 import Player

from carball.generated.api.stats.events_pb2 import Bump

from carball.generated.api.player_id_pb2 import PlayerId
Expand All @@ -13,7 +17,7 @@

# Decreasing this, risks not counting bumps where one car is directly behind another (driving in the same direction).
# Increasing this, risks counting non-contact close proximity (e.g. one car cleanly jumped over another =/= bump).
PLAYER_CONTACT_MAX_DISTANCE = 200
PLAYER_CONTACT_DISTANCE = 200

# Needs to be relatively high to account for two cars colliding 'diagonally': /\
MAX_BUMP_ALIGN_ANGLE = 60
Expand All @@ -33,21 +37,23 @@ def __init__(self, game: Game, proto_game: game_pb2):

def get_bumps_from_game(self, data_frame: pd.DataFrame, player_map):
self.create_bumps_from_demos(self.proto_game)
self.create_bumps_from_player_contact(data_frame, player_map)
self.create_bumps_from_player_proximity(data_frame, player_map)

def create_bumps_from_demos(self, proto_game):
for demo in proto_game.game_metadata.demos:
self.add_bump(demo.frame_number, demo.victim_id, demo.attacker_id, True)

def create_bumps_from_player_contact(self, data_frame, player_map):
# NOTES:
# Currently, this yields more 'bumps' than there are actually.
# This is mostly due to aerial proximity, where a bump was NOT intended or no contact was made.
# This also occurs near the ground, where cars flip awkwardly past each other.

# POSSIBLE SOLUTIONS:
# Account for car hitboxes and/or rotations when calculating close proximity intervals.
# Do some post-bump analysis and see if car velocities aligned (i.e. analyse end of interval bump alignments)
def create_bumps_from_player_proximity(self, data_frame: pd.DataFrame, player_map: Dict[str, Player]):
"""
Attempt to find all instances between each possible player combination
where they got within PLAYER_CONTACT_DISTANCE.
Then, add each instance to the API.
NOTES:
Currently, this yields more 'bumps' than there are actually.
This is mostly due to aerial proximity, where a bump was NOT intended or no contact was made.
This also occurs near the ground, where cars flip awkwardly past each other.
"""

# An array of player names to get player combinations; and a dict of player names to their IDs to create bumps.
player_names = []
Expand All @@ -56,23 +62,23 @@ def create_bumps_from_player_contact(self, data_frame, player_map):
player_names.append(player.name)
player_name_to_id[player.name] = player.id

# For each player pair combination, get possible contact distances.
# For each player pair combination (nCr), get all frames where they got close and then filter those as bumps.
for player_pair in itertools.combinations(player_names, 2):
# Get all frame idxs where players were within PLAYER_CONTACT_MAX_DISTANCE.
players_close_frame_idxs = BumpAnalysis.get_players_close_frame_idxs(data_frame,
str(player_pair[0]),
str(player_pair[1]))

if len(players_close_frame_idxs) > 0:
likely_bumps = BumpAnalysis.filter_bumps(data_frame, player_pair, players_close_frame_idxs)
self.add_non_demo_bumps(likely_bumps, player_name_to_id)
else:
likely_bumps = None
logger.info("Players (" + player_pair[0] + " and " + player_pair[1] + ") did not get close "
"during the match.")

self.add_non_demo_bumps(likely_bumps, player_name_to_id)

def add_bump(self, frame: int, victim_id: PlayerId, attacker_id: PlayerId, is_demo: bool) -> Bump:
"""
Add a new bump to the proto_game object.
"""
bump = self.proto_game.game_stats.bumps.add()
bump.frame_number = frame
bump.attacker_id.id = attacker_id.id
Expand All @@ -81,10 +87,19 @@ def add_bump(self, frame: int, victim_id: PlayerId, attacker_id: PlayerId, is_de
bump.is_demo = True

def add_non_demo_bumps(self, likely_bumps, player_name_to_id):
"""
Add a new bump to the proto_game object.
This method takes an array of likely (filtered) bumps, in the following form:
(frame_idx, attacker_name, victim_name)
and carefully adds them to the proto_game object (i.e. check for demo duplicates).
"""

# Get an array of demo frame idxs to compare later.
demo_frame_idxs = []
for demo in self.proto_game.game_metadata.demos:
demo_frame_idxs.append(demo.frame_number)

# For each bump tuple, if its frame index is not similar to a demo frame index, add it via add_bump().
for likely_bump in likely_bumps:
likely_bump_frame_idx = likely_bump[0]
if not any(np.isclose(demo_frame_idxs, likely_bump_frame_idx, atol=10)):
Expand All @@ -94,42 +109,58 @@ def add_non_demo_bumps(self, likely_bumps, player_name_to_id):
@staticmethod
def filter_bumps(data_frame, player_pair, players_close_frame_idxs):
"""
Try to find 'real' bumps, and return them as a list of likely_bumps.
Filter the frames where two players got close - the filtered frames are likely bumps.
The main principle used is the angle between two vectors (aka 'alignment'):
the velocity vector of player A;
the positional vector of the difference between the positions of player B and player A.
Both of these vectors point away from player A, and if the angle between them is small - it is likely that
Player A bumped Player B. (Velocity going 'through' Player B's Position)
Some further checks are done to categorise the bump (i.e. is_aerial_bump(), is_bump_velocity() )
"""
likely_bumps = []

# Get all individual intervals where a player pair got close to each other.
# Split a list of frame indexes into intervals where indexes are within 3 of each other (i.e. consecutive).
players_close_frame_idxs_intervals = BumpAnalysis.get_players_close_intervals(players_close_frame_idxs)

# For each such interval, take (currently only) the beginning and analyse car behaviour.
# For each such interval, take (currently only) the first frame index and analyse car behaviour.
for interval in players_close_frame_idxs_intervals:
frame_before_bump = interval[0]

# Calculate alignments (angle between position vector and velocity vector, with regard to each player)
# Calculate both player bump alignments (see comment at method top).
p1_alignment_before = BumpAnalysis.get_player_bump_alignment(data_frame, frame_before_bump,
player_pair[0], player_pair[1])
p2_alignment_before = BumpAnalysis.get_player_bump_alignment(data_frame, frame_before_bump,
player_pair[1], player_pair[0])
# TODO Create Bump objects and add them to the API.
# Determine the attacker and the victim (if alignment is below MAX_BUMP_ALIGN_ANGLE, it's the attacker)

# Determine the attacker and the victim (see method for more info).
attacker, victim = BumpAnalysis.determine_attacker_victim(player_pair[0], player_pair[1],
p1_alignment_before, p2_alignment_before)

# Determine if the bump was above AERIAL_BUMP_HEIGHT.
is_aerial_bump = BumpAnalysis.is_aerial_bump(data_frame, player_pair[0], player_pair[1], frame_before_bump)

# Append the current bump data to likely bumps, if there is an attacker and a victim
# and if it wasn't an aerial bump (most often it isn't intended, and there is often awkward behaviour).
if attacker is not None and victim is not None and not is_aerial_bump:
# SUGGESTION: Perhaps take frame in the middle of the interval?
high_probability_bump = (frame_before_bump, attacker, victim)
likely_bumps.append(high_probability_bump)
likely_bump = (frame_before_bump, attacker, victim)
likely_bumps.append(likely_bump)

# Check if interval is quite long - players may be in rule 1 :) or might be a scramble.
# NOT YET IMPLEMENTED: Check if interval is quite long - players may be in rule 1 :) or might be a scramble.
# BumpAnalysis.analyse_prolonged_proximity(data_frame, interval, player_pair[0], player_pair[1])

return likely_bumps

@staticmethod
def get_player_bump_alignment(data_frame, frame_idx, p1_name, p2_name):
"""
Calculate and return the angle between:
the velocity vector of player A;
the positional vector of the difference between the positions of player B and player A.
"""

# Get the necessary data from the DataFrame at the given frame index.
p1_vel_df = data_frame[p1_name][['vel_x', 'vel_y', 'vel_z']].loc[frame_idx]
p1_pos_df = data_frame[p1_name][['pos_x', 'pos_y', 'pos_z']].loc[frame_idx]
p2_pos_df = data_frame[p2_name][['pos_x', 'pos_y', 'pos_z']].loc[frame_idx]
Expand All @@ -145,31 +176,41 @@ def get_player_bump_alignment(data_frame, frame_idx, p1_name, p2_name):
vel1 = [p1_vel_df.vel_x, p1_vel_df.vel_y, p1_vel_df.vel_z]
unit_vel1 = vel1 / np.linalg.norm(vel1)

# Find the angle between the position vector and the velocity vector.
# If this is relatively aligned - p1 probably significantly bumped p2.
# Find the angle between the positional vector and the velocity vector.
# NOTE: This is currently converted to DEGREES, not sure if this is bad..? ( - DivvyC)
ang = (np.arccos(np.clip(np.dot(unit_vel1, unit_pos1), -1.0, 1.0))) * 180 / np.pi
# print(p1_name + "'s bump angle=" + str(ang))
return ang

@staticmethod
def get_players_close_frame_idxs(data_frame, p1_name, p2_name):
"""
For a pair of players, find all frame indexes where they got within PLAYER_CONTACT_DISTANCE of each other.
Note that they did NOT necessarily make contact.
"""

# Separate the positional data of each given player from the full DataFrame and lose the NaN value rows.
p1_pos_df = data_frame[p1_name][['pos_x', 'pos_y', 'pos_z']].dropna(axis=0)
p2_pos_df = data_frame[p2_name][['pos_x', 'pos_y', 'pos_z']].dropna(axis=0)

# Calculate the vector distances between the players.
# Calculate the vector distances between the players, and store them as a pd.Series (1D DataFrame).
distances = (p1_pos_df.pos_x - p2_pos_df.pos_x) ** 2 + \
(p1_pos_df.pos_y - p2_pos_df.pos_y) ** 2 + \
(p1_pos_df.pos_z - p2_pos_df.pos_z) ** 2
distances = np.sqrt(distances)
# Only keep values < PLAYER_CONTACT_MAX_DISTANCE (see top of class).
players_close_series = distances[distances < PLAYER_CONTACT_MAX_DISTANCE]
# Get the frame indexes of the values (as ndarray).

# Only keep values < PLAYER_CONTACT_DISTANCE (see top of class).
players_close_series = distances[distances < PLAYER_CONTACT_DISTANCE]
# Get the frame indexes of the values (as an ndarray).
players_close_frame_idxs = players_close_series.index.to_numpy()
return players_close_frame_idxs

@staticmethod
def get_players_close_intervals(players_close_frame_idxs):
# Find continuous intervals of close proximity, and group them together.
"""
Separate a list of frame indexes into intervals with consecutive frame indexes.
E.g. [3, 4, 5, 7, 19, 21, 23, 24, 57] is turned into [[3, 4, 5, 7], [21, 23, 24], [57]]
"""

all_intervals = []
interval = []
for index, frame_idx in enumerate(players_close_frame_idxs):
Expand All @@ -184,9 +225,10 @@ def get_players_close_intervals(players_close_frame_idxs):
def determine_attacker_victim(p1_name, p2_name, p1_alignment, p2_alignment):
"""
Try to 'guesstimate' the attacker and the victim by comparing bump alignment angles.
If both alignments are within 45deg, then both players were going relatively towards each other.
If both bump alignments are above MAX_BUMP_ALIGN_ANGLE, both values are None (no solid attacker/victim)
If both bump alignments are within 45deg of each other, both values are None (both attackers)
:return: (Attacker, Victim)
:return: A tuple in the form (Attacker, Victim) or (None, None).
"""

if p1_alignment < MAX_BUMP_ALIGN_ANGLE or p2_alignment < MAX_BUMP_ALIGN_ANGLE:
Expand Down

5 comments on commit 6922200

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carball Benchmarks intensive_oce_rlcs

Benchmark suite Current: 6922200 Previous: 07297fe Ratio
carball/tests/benchmarking/benchmarking.py::test_intensive_oce_rlcs 0.06771742513126268 iter/sec (stddev: 0.1461619266187352) 0.06152116311170856 iter/sec (stddev: 0.20006056624924776) 0.91

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carball Benchmarks short_sample

Benchmark suite Current: 6922200 Previous: 07297fe Ratio
carball/tests/benchmarking/benchmarking.py::test_short_sample 0.7394452894487513 iter/sec (stddev: 0.016003529900350984) 0.8667843933942438 iter/sec (stddev: 0.033132074823376975) 1.17

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carball Benchmarks short_dropshot

Benchmark suite Current: 6922200 Previous: 07297fe Ratio
carball/tests/benchmarking/benchmarking.py::test_short_dropshot 0.605969811155438 iter/sec (stddev: 0.021538688229386847) 0.6623984673004569 iter/sec (stddev: 0.010878172765054418) 1.09

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carball Benchmarks oce_rlcs

Benchmark suite Current: 6922200 Previous: 07297fe Ratio
carball/tests/benchmarking/benchmarking.py::test_oce_rlcs 0.06558285978304346 iter/sec (stddev: 0.14869021220350004) 0.07512800799461093 iter/sec (stddev: 0.15956780047878102) 1.15

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Carball Benchmarks full_rumble

Benchmark suite Current: 6922200 Previous: 07297fe Ratio
carball/tests/benchmarking/benchmarking.py::test_full_rumble 0.06210334380379432 iter/sec (stddev: 0.13060443325180904) 0.06434159662703988 iter/sec (stddev: 0.16290135322043248) 1.04

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.