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
Prev Previous commit
Next Next commit
fixed bug with awpy.plot not handling lower level points (for maps li…
…ke Vertigo and Nuke)

awpy.plot.utils.is_position_on_lower_level() was always returning False trivially; if statement in line 122 was checking if pos is greater than max instead off less than max.

Added `is_lower` argument to all plot functions. plot() (and _generate_frame_plot() & gif(), which use it) now correctly set alpha to 0.4 for lower points.

heatmap() now ignores points not on the level provided by the user and spits out a warning.

Also cleaned up map_data file.
ventsiR committed Oct 29, 2024
commit c93f6f4818cbe05427be4637a2d385c7b5f1743d
37 changes: 14 additions & 23 deletions awpy/data/map_data.py
Original file line number Diff line number Diff line change
@@ -8,119 +8,110 @@
"scale": 2.539062,
"rotate": 1,
"zoom": 1.3,
"selections": [
{"name": "default", "altitude_max": 10000, "altitude_min": -5},
{"name": "lower", "altitude_max": -5, "altitude_min": -10000},
],
"lower_level_max": -5.0,
Copy link
Owner

Choose a reason for hiding this comment

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

I like this simplification, but I may offer a rewording -- perhaps lower_level_max_units or something. Just to put the "units" into the name (which unfortunately in this case is just generic "units").

Copy link
Author

Choose a reason for hiding this comment

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

Sounds good!

Sidebar: I am a bit new to GitHub & commits; is it good etiquette for me to implement your feedback, or to let you implement it?

Copy link
Owner

Choose a reason for hiding this comment

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

I think it depends from person-to-person, repo-to-repo, but usually the one who opened the PR will make the changes, unless it's a super small change.

Copy link
Author

Choose a reason for hiding this comment

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

Sounds good. Will implement everything you've mentioned; they're pretty small changes and everything is still relatively fresh in my mind :D

},
"ar_shoots": {
"pos_x": -1368,
"pos_y": 1952,
"scale": 2.687500,
"rotate": None,
"zoom": None,
"selections": [],
"lower_level_max": float("-inf"),
},
"cs_office": {
"pos_x": -1838,
"pos_y": 1858,
"scale": 4.1,
"rotate": None,
"zoom": None,
"selections": [],
"lower_level_max": float("-inf"),
},
"cs_italy": {
"pos_x": -2647,
"pos_y": 2592,
"scale": 4.6,
"rotate": 1,
"zoom": 1.5,
"selections": [],
"lower_level_max": float("-inf"),
},
"de_ancient": {
"pos_x": -2953,
"pos_y": 2164,
"scale": 5,
"rotate": 0,
"zoom": 0,
"selections": [],
"lower_level_max": float("-inf"),
},
"de_anubis": {
"pos_x": -2796,
"pos_y": 3328,
"scale": 5.22,
"rotate": None,
"zoom": None,
"selections": [],
"lower_level_max": float("-inf"),
},
"de_dust": {
"pos_x": -2850,
"pos_y": 4073,
"scale": 6,
"rotate": 1,
"zoom": 1.3,
"selections": [],
"lower_level_max": float("-inf"),
},
"de_dust2": {
"pos_x": -2476,
"pos_y": 3239,
"scale": 4.4,
"rotate": 1,
"zoom": 1.1,
"selections": [],
"lower_level_max": float("-inf"),
},
"de_inferno": {
"pos_x": -2087,
"pos_y": 3870,
"scale": 4.9,
"rotate": None,
"zoom": None,
"selections": [],
"lower_level_max": float("-inf"),
},
"de_inferno_s2": {
"pos_x": -2087,
"pos_y": 3870,
"scale": 4.9,
"rotate": None,
"zoom": None,
"selections": [],
"lower_level_max": float("-inf"),
},
"de_mirage": {
"pos_x": -3230,
"pos_y": 1713,
"scale": 5,
"rotate": 0,
"zoom": 0,
"selections": [],
"lower_level_max": float("-inf"),
},
"de_nuke": {
"pos_x": -3453,
"pos_y": 2887,
"scale": 7,
"rotate": None,
"zoom": None,
"selections": [
{"name": "default", "altitude_max": 10000, "altitude_min": -495},
{"name": "lower", "altitude_max": -495, "altitude_min": -10000},
],
"lower_level_max": -495.0,
},
"de_overpass": {
"pos_x": -4831,
"pos_y": 1781,
"scale": 5.2,
"rotate": 0,
"zoom": 0,
"selections": [],
"lower_level_max": float("-inf"),
},
"de_vertigo": {
"pos_x": -3168,
"pos_y": 1762,
"scale": 4,
"rotate": None,
"zoom": None,
"selections": [
{"name": "default", "altitude_max": 20000, "altitude_min": 11700},
{"name": "lower", "altitude_max": 11700, "altitude_min": -10000},
],
"lower_level_max": 11700.0,
},
}
75 changes: 65 additions & 10 deletions awpy/plot/plot.py
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@
import importlib.resources
import io
import math
import warnings
from typing import Dict, List, Literal, Optional, Tuple

import matplotlib.image as mpimg
@@ -22,6 +23,7 @@
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,
) -> Tuple[Figure, Axes]:
"""Plot a Counter-Strike map with optional points.
@@ -30,6 +32,9 @@ def plot( # noqa: PLR0915
map_name (str): Name of the map to plot.
points (List[Tuple[float, float, float]], optional):
List of points to plot. Each point is (X, Y, Z). Defaults to None.
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.
point_settings (List[Dict], optional):
List of dictionaries with settings for each point. Each dictionary
should contain:
@@ -49,8 +54,13 @@ def plot( # noqa: PLR0915
Returns:
Tuple[Figure, Axes]: Matplotlib Figure and Axes objects.
"""
if is_lower:
image = f"{map_name}_lower.png"
else:
image = f"{map_name}.png"

# Check for the main map image
with importlib.resources.path("awpy.data.maps", f"{map_name}.png") as map_img_path:
with importlib.resources.path("awpy.data.maps", image) as map_img_path:
if not map_img_path.exists():
map_img_path_err = f"Map image not found: {map_img_path}"
raise FileNotFoundError(map_img_path_err)
@@ -71,9 +81,6 @@ 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")

# Default settings
marker = settings.get("marker", "o")
color = settings.get("color", "red")
@@ -85,7 +92,15 @@ def plot( # noqa: PLR0915

alpha = 0.15 if hp == 0 else 1.0
if is_position_on_lower_level(map_name, (x, y, z)):
alpha *= 0.4
# check that user is not drawing lower level map
if not is_lower:
alpha *= 0.4
elif is_lower:
# 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")

# Plot the marker
axes.plot(
@@ -204,20 +219,27 @@ def plot( # noqa: PLR0915
return figure, axes


def _generate_frame_plot(map_name: str, frames_data: List[Dict]) -> list[Image.Image]:
def _generate_frame_plot(
map_name: str, frames_data: List[Dict], is_lower: Optional[bool] = False
) -> list[Image.Image]:
"""Generate frames for the animation.

Args:
map_name (str): Name of the map to plot.
frames_data (List[Dict]): List of dictionaries, each containing 'points'
and 'point_settings' for a frame.
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.

Returns:
List[Image.Image]: List of PIL Image objects representing each frame.
"""
frames = []
for frame_data in tqdm(frames_data):
fig, _ax = plot(map_name, frame_data["points"], frame_data["point_settings"])
fig, _ax = plot(
map_name, frame_data["points"], is_lower, frame_data["point_settings"]
)

# Convert the matplotlib figure to a PIL Image
buf = io.BytesIO()
@@ -232,7 +254,11 @@ def _generate_frame_plot(map_name: str, frames_data: List[Dict]) -> list[Image.I


def gif(
map_name: str, frames_data: List[Dict], output_filename: str, duration: int = 500
map_name: str,
frames_data: List[Dict],
output_filename: str,
duration: int = 500,
is_lower: Optional[bool] = False,
) -> None:
"""Create an animated gif from a list of frames.

@@ -243,8 +269,11 @@ def gif(
frames (List[Image.Image]): List of PIL Image objects.
output_filename (str): Name of the output GIF file.
duration (int): Duration of each frame in milliseconds.
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.
"""
frames = _generate_frame_plot(map_name, frames_data)
frames = _generate_frame_plot(map_name, frames_data, is_lower)
frames[0].save(
output_filename,
save_all=True,
@@ -258,6 +287,7 @@ def heatmap(
map_name: str,
points: List[Tuple[float, float, float]],
method: Literal["hex", "hist", "kde"],
is_lower: Optional[bool] = False,
size: int = 10,
cmap: str = "RdYlGn",
alpha: float = 0.5,
@@ -271,6 +301,9 @@ def heatmap(
map_name (str): Name of the map to plot.
points (List[Tuple[float, float, float]]): List of points to plot.
method (Literal["hex", "hist", "kde"]): Method to use for the 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.
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.
@@ -287,11 +320,33 @@ def heatmap(
"""
fig, ax = plt.subplots(figsize=(1024 / 300, 1024 / 300), dpi=300)

if is_lower:
image = f"{map_name}_lower.png"
else:
image = f"{map_name}.png"

# Load and display the map
with importlib.resources.path("awpy.data.maps", f"{map_name}.png") as map_img_path:
with importlib.resources.path("awpy.data.maps", image) as map_img_path:
map_bg = mpimg.imread(map_img_path)
ax.imshow(map_bg, zorder=0, alpha=0.5)

temp_points = points
points = []
warning = ""
for point in temp_points:
is_point_lower = is_position_on_lower_level(map_name, point)
# If point level different from provided level by user, ignore point and warn.
if is_point_lower == is_lower:
points.append(point)
else:
warning = (
"You provided points on the lower level of the map "
"but they were ignored! To draw lower level points, "
"set is_lower argument to True."
)
if warning:
warnings.warn(warning, UserWarning)

# Transform coordinates
x = [position_transform_axis(map_name, p[0], "x") for p in points]
y = [position_transform_axis(map_name, p[1], "y") for p in points]
9 changes: 2 additions & 7 deletions awpy/plot/utils.py
Original file line number Diff line number Diff line change
@@ -115,11 +115,6 @@ def is_position_on_lower_level(
bool: True if the position on the lower level, False otherwise.
"""
metadata = MAP_DATA[map_name]
if len(metadata["selections"]) == 0:
return False

for level in metadata["selections"]:
if position[2] > level["altitude_max"] and position[2] <= level["altitude_min"]:
return True

if position[2] <= metadata["lower_level_max"]:
return True
return False