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

Fixed plot bugs, added functionality, updated images #353

Merged
merged 9 commits into from
Jan 8, 2025
100 changes: 35 additions & 65 deletions awpy/plot/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,14 @@
from scipy.stats import gaussian_kde
from tqdm import tqdm

from awpy.plot.utils import is_position_on_lower_level, position_transform_axis
from awpy.plot.utils import is_position_on_lower_level, game_to_pixel_axis


def plot( # noqa: PLR0915
map_name: str,
points: Optional[List[Tuple[float, float, float]]] = None,
is_lower: Optional[bool] = False,
Copy link
Owner

Choose a reason for hiding this comment

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

What if this parameter were reworked into a "lower fraction"? As in, we assume that upper points are plotted with alpha = 1.0, and then lower are lower_frac*alpha ? This would allow for a more expressive form of what it appears you are going for.

Copy link
Owner

Choose a reason for hiding this comment

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

Which, we may also want to offer a param for the "dead player alpha" that we default to 0.15, too

Copy link
Author

Choose a reason for hiding this comment

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

Good idea!

The only thing to note is that when is_lower = True, it also changes the image to the lower part of the map, but then again, you can control this via the map_name argument.

Copy link
Owner

Choose a reason for hiding this comment

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

Good point. Perhaps in the docstring we can say something like "if you want to plot a specific lower part of a map, use *_lower. However, then this could imply that a user may want to plot only the top part of the map, too. Perhaps we search for a "_lower" and "_upper" if we want to plot only those.

Copy link
Owner

Choose a reason for hiding this comment

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

Now that I look at this, perhaps we want to just have a generic game_to_pixel(tuple). That tuple can be an (x,y) pair. What do you think?

point_settings: Optional[List[Dict]] = None,
ignore_extreme_points: Optional[bool] = False,
) -> Tuple[Figure, Axes]:
"""Plot a Counter-Strike map with optional points.

Expand All @@ -46,10 +45,6 @@ def plot( # noqa: PLR0915
- 'armor': int (0-100)
- 'direction': Tuple[float, float] (pitch, yaw in degrees)
- 'label': str (optional)
ignore_extreme_points (bool, optional): If set to True, will ignore
points that are outside of the map graphic. If set to False, will
draw those points, changing the resolution of the ouput image.
Defaults to False.

Raises:
FileNotFoundError: Raises a FileNotFoundError if the map image is not found.
Expand Down Expand Up @@ -86,16 +81,14 @@ def plot( # noqa: PLR0915

# Plot each point
for (x, y, z), settings in zip(points, point_settings):
transformed_x = position_transform_axis(map_name, x, "x")
transformed_y = position_transform_axis(map_name, y, "y")
transformed_x = game_to_pixel_axis(map_name, x, "x")
transformed_y = game_to_pixel_axis(map_name, y, "y")

if (ignore_extreme_points
and (
transformed_x < 0
or transformed_x > 1024
or transformed_y < 0
or transformed_y > 1024
)
if (
transformed_x < 0
or transformed_x > 1024
Copy link
Owner

Choose a reason for hiding this comment

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

Because these are constants, it would be helpful to have a comment above here stating that these are the maximum expected bounds

or transformed_y < 0
or transformed_y > 1024
):
continue
# Default settings
Expand All @@ -116,8 +109,8 @@ def plot( # noqa: PLR0915
# if drawing lower-level map and point is top-level, don't draw
alpha = 0

transformed_x = position_transform_axis(map_name, x, "x")
transformed_y = position_transform_axis(map_name, y, "y")
transformed_x = game_to_pixel_axis(map_name, x, "x")
transformed_y = game_to_pixel_axis(map_name, y, "y")

# Plot the marker
axes.plot(
Expand Down Expand Up @@ -240,7 +233,6 @@ def _generate_frame_plot(
map_name: str,
frames_data: List[Dict],
is_lower: Optional[bool] = False,
ignore_extreme_points: Optional[bool] = False,
) -> list[Image.Image]:
"""Generate frames for the animation.

Expand All @@ -251,10 +243,6 @@ def _generate_frame_plot(
is_lower (optional, bool): If set to False, will not draw lower-level
points with alpha = 0.4. If True will draw only lower-level
points on the lower-level minimap. Defaults to False.
ignore_extreme_points (bool, optional): If set to True, will ignore
points that are outside of the map graphic. If set to False, will
draw those points, changing the resolution of the ouput image.
Defaults to False.

Returns:
List[Image.Image]: List of PIL Image objects representing each frame.
Expand All @@ -265,7 +253,6 @@ def _generate_frame_plot(
map_name,
frame_data["points"],
is_lower, frame_data["point_settings"],
ignore_extreme_points,
)

# Convert the matplotlib figure to a PIL Image
Expand All @@ -286,7 +273,6 @@ def gif(
output_filename: str,
duration: int = 500,
is_lower: Optional[bool] = False,
ignore_extreme_points: Optional[bool] = False,
) -> None:
"""Create an animated gif from a list of frames.

Expand All @@ -300,16 +286,11 @@ def gif(
is_lower (optional, bool): If set to False, will draw lower-level points
with alpha = 0.4. If True will draw only lower-level points on the
lower-level minimap. Defaults to False.
ignore_extreme_points (bool, optional): If set to True, will ignore
points that are outside of the map graphic. If set to False, will
draw those points, changing the resolution of the ouput image.
Defaults to False.
"""
frames = _generate_frame_plot(
map_name,
frames_data,
is_lower,
ignore_extreme_points,
)
frames[0].save(
output_filename,
Expand All @@ -325,13 +306,11 @@ def heatmap(
points: List[Tuple[float, float, float]],
method: Literal["hex", "hist", "kde"],
is_lower: Optional[bool] = False,
ignore_extreme_points: Optional[bool] = False,
size: int = 10,
cmap: str = "RdYlGn",
alpha: float = 0.5,
*,
vary_alpha: bool = False,
vary_alpha_range: Optional[List[float]] = None,
alpha_range: Optional[List[float]] = None,
kde_lower_bound: float = 0.1,
) -> tuple[Figure, Axes]:
"""Create a heatmap of points on a Counter-Strike map.
Expand All @@ -343,18 +322,13 @@ def heatmap(
is_lower (optional, bool): If set to False, will NOT draw lower-level
points. If True will draw only lower-level points on the
lower-level minimap. Defaults to False.
ignore_extreme_points (bool, optional): If set to True, will ignore
points that are outside of the map graphic. If set to False, will
draw those points, changing the resolution of the ouput image.
Defaults to False.
size (int, optional): Size of the heatmap grid. Defaults to 10.
cmap (str, optional): Colormap to use. Defaults to 'RdYlGn'.
alpha (float, optional): Transparency of the heatmap. Defaults to 0.5.
vary_alpha (bool, optional): Vary the alpha based on the density. Defaults
to False.
vary_alpha_range (List[float, float], optional): The min and max transparency
variance of points (respectively). Both values should be between `0`
and `1`. Defaults to `[]`, meaning min = `0` and max = `alpha`.
alpha_range (List[float, float], optional): When value is provided
here, points' transparency will vary based on the density, with
min transparency of `alpha_range[0]` and max of `alpha_range[1]`.
Defaults to `None`, meaning no variance of transparency.
kde_lower_bound (float, optional): Lower bound for KDE density values. Defaults
to 0.1.

Expand Down Expand Up @@ -396,43 +370,39 @@ def heatmap(

x, y = [], []
for point in points:
x_point = position_transform_axis(map_name, point[0], "x")
y_point = position_transform_axis(map_name, point[1], "y")
x_point = game_to_pixel_axis(map_name, point[0], "x")
y_point = game_to_pixel_axis(map_name, point[1], "y")
# Handle extreme points
if (ignore_extreme_points
and (
x_point < 0
or x_point > 1024
or y_point < 0
or y_point > 1024
)
if (
x_point < 0
or x_point > 1024
or y_point < 0
or y_point > 1024
):
continue

x.append(x_point)
y.append(y_point)

# If user set vary_alpha to True, check and/or set vary_alpha_range
# Check and/or set alpha_range
min_alpha, max_alpha = 0, 1
if vary_alpha:
if vary_alpha_range is None:
vary_alpha_range = [0, alpha]
if not isinstance(vary_alpha_range, list):
raise ValueError("vary_alpha_range must be a list of length 2.")
if len(vary_alpha_range) != 2:
raise ValueError("vary_alpha_range must have exactly 2 elements.")
min_temp, max_temp = vary_alpha_range[0], vary_alpha_range[1]
if alpha_range is not None:
if not isinstance(alpha_range, list):
raise ValueError("alpha_range must be a list of length 2.")
if len(alpha_range) != 2:
raise ValueError("alpha_range must have exactly 2 elements.")
min_temp, max_temp = alpha_range[0], alpha_range[1]
if not (min_temp >= 0 and min_temp <= 1) or not (
max_temp >= 0 and max_temp <= 1
):
raise ValueError(
"vary_alpha_range must have both values as floats \
"alpha_range must have both values as floats \
between 0 and 1."
)
if min_temp > max_temp:
raise ValueError(
"vary_alpha_range[0] (min alpha) cannot be greater "
"than vary_alpha[1] (max alpha)."
"alpha_range[0] (min alpha) cannot be greater "
"than alpha[1] (max alpha)."
)
min_alpha, max_alpha = min_temp, max_temp

Expand All @@ -443,7 +413,7 @@ def heatmap(
# Get array of counts in each hexbin
counts = heatmap.get_array()

if vary_alpha:
if alpha_range is not None:
# Normalize counts to use as alpha values
alphas = counts / counts.max()
alphas = alphas * (max_alpha - min_alpha) + min_alpha
Expand All @@ -461,7 +431,7 @@ def heatmap(
# Set counts of 0 to NaN to make them transparent
hist[hist == 0] = np.nan

if vary_alpha:
if alpha_range is not None:
# Normalize histogram values
hist_norm = hist.T / hist.max()
# Create a color array with variable alpha
Expand Down Expand Up @@ -494,7 +464,7 @@ def heatmap(
threshold = zi.max() * kde_lower_bound # You can adjust this threshold
zi[zi < threshold] = np.nan

if vary_alpha:
if alpha_range is not None:
# Normalize KDE values
zi_norm = zi / zi.max()
# Create a color array with variable alpha
Expand Down
82 changes: 73 additions & 9 deletions awpy/plot/utils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Utilities for plotting and visualization."""

from typing import Literal
import warnings
Copy link
Owner

Choose a reason for hiding this comment

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

should generally do

import warnings

from typing import Literal

from awpy.data.map_data import MAP_DATA


from awpy.data.map_data import MAP_DATA


# Position function courtesy of PureSkill.gg
def position_transform_axis(
def game_to_pixel_axis(
map_name: str, position: float, axis: Literal["x", "y"]
) -> float:
"""Transforms an X or Y-axis value. CS2 coordinate -> Minimap image pixel value.
Expand Down Expand Up @@ -34,7 +35,7 @@ def position_transform_axis(
return (start - position) / scale


def position_revert_axis(
def pixel_to_game_axis(
map_name: str, position: float, axis: Literal["x", "y"]
) -> float:
"""Reverts an X or Y-axis value. Minimap image pixel value -> CS2 coordinate.
Expand Down Expand Up @@ -64,7 +65,7 @@ def position_revert_axis(
return start - position * scale


def position_transform(
def game_to_pixel(
map_name: str, position: tuple[float, float, float]
) -> tuple[float, float, float]:
"""Transforms an single coordinate (X,Y,Z). CS2 coordinates -> Minimap image pixel values.
Expand All @@ -77,13 +78,13 @@ def position_transform(
Tuple[float, float, float]: Transformed coordinates (X,Y,Z).
"""
return (
position_transform_axis(map_name, position[0], "x"),
position_transform_axis(map_name, position[1], "y"),
game_to_pixel_axis(map_name, position[0], "x"),
game_to_pixel_axis(map_name, position[1], "y"),
position[2],
)


def position_revert(
def pixel_to_game(
map_name: str, position: tuple[float, float, float]
) -> tuple[float, float, float]:
"""Transforms an single coordinate (X,Y,Z). Minimap image pixel values -> CS2 coordinates.
Expand All @@ -96,8 +97,8 @@ def position_revert(
Tuple[float, float, float]: Transformed coordinates (X,Y,Z).
"""
return (
position_revert_axis(map_name, position[0], "x"),
position_revert_axis(map_name, position[1], "y"),
pixel_to_game_axis(map_name, position[0], "x"),
pixel_to_game_axis(map_name, position[1], "y"),
position[2],
)

Expand All @@ -115,6 +116,69 @@ def is_position_on_lower_level(
bool: True if the position on the lower level, False otherwise.
"""
metadata = MAP_DATA[map_name]
if position[2] <= metadata["lower_level_max"]:
if position[2] <= metadata["lower_level_max_units"]:
return True
return False


# Old function names:
def _renaming_warning(old: str, new: str):
return f"""Deprecation warning: Function {old} has been renamed to {new}.
Please update your code to avoid future deprecation.
"""


def position_transform_axis(
map_name: str, position: float, axis: Literal["x", "y"]
) -> float:
"""Calls `game_to_pixel_axis` and sends warning.

This is the old name of function `game_to_pixel_axis`. Please update
your code to avoid future deprecation.
"""
warnings.warn(
Copy link
Owner

Choose a reason for hiding this comment

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

Great idea to add the deprecation warnings. I think we can drop the _renaming_warning() however, and just include the specific strings in the deprecation messages!

_renaming_warning("position_transform_axis()", "game_to_pixel_axis()"),
DeprecationWarning)
return game_to_pixel_axis(map_name, position, axis)


def position_revert_axis(
map_name: str, position: float, axis: Literal["x", "y"]
) -> float:
"""Calls `pixel_to_game_axis` and sends warning.

This is the old name of function `pixel_to_game_axis`. Please update
your code to avoid future deprecation.
"""
warnings.warn(
_renaming_warning("position_revert_axis()", "pixel_to_game_axis()"),
DeprecationWarning)
return pixel_to_game_axis(map_name, position, axis)


def position_transform(
Copy link
Owner

Choose a reason for hiding this comment

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

I also think these functions would still live in plot, rather than live in utils (so the import path would be the same)

map_name: str, position: tuple[float, float, float]
) -> tuple[float, float, float]:
"""Calls `game_to_pixel` and sends warning.

This is the old name of function `game_to_pixel`. Please update
your code to avoid future deprecation.
"""
warnings.warn(
_renaming_warning("position_transform()", "game_to_pixel()"),
DeprecationWarning)
return game_to_pixel(map_name, position)


def position_revert(
map_name: str, position: tuple[float, float, float]
) -> tuple[float, float, float]:
"""Calls `pixel_to_game` and sends warning.

This is the old name of function `pixel_to_game`. Please update
your code to avoid future deprecation.
"""
warnings.warn(
_renaming_warning("position_revert()", "pixel_to_game()"),
DeprecationWarning)
return pixel_to_game(map_name, position)