【问题标题】:How to animate a 2D scatter plot given X, Y coordinates and time with appearing and disappearing points?如何在给定 X、Y 坐标和时间以及出现和消失点的情况下为 2D 散点图设置动画?
【发布时间】:2021-06-29 21:19:54
【问题描述】:

我有一个如下所示的数据框:

每一行代表一个人。它们在数据框上给出的一段时间内停留在 3 个不同的位置。前几个人不留在位置 1,但他们“出生”在位置 2。其余的留在每个位置(3 个位置)。

我想在数据框上给定的 X、Y 坐标上为每个人设置动画,并将它们表示为点或任何其他形状。这是流程:

  1. 每个人都应在给定时间出现在第一个给定位置 (location1)。在这种状态下,它们的颜色应该是蓝色的。
  2. 在 location1 停留直到 location2_time,然后出现在 location2。在这种状态下,它们的颜色应该是红色的。
  3. 在 location2 停留直到 location3_time,然后出现在 location3。在这种状态下,它们的颜色应该是红色的。
  4. 在位置 3 停留 3 秒,然后永远消失。

视觉上可以同时有几个人。我该怎么做?

以下链接中有一些很好的答案。但是,在这些解决方案中,分数不会消失。

  1. How can i make points of a python plot appear over time?
  2. How to animate a scatter plot?

【问题讨论】:

  • 另一种选择是使用 Vaex,vaex.io/docs/index.html
  • 时间是什么单位?毫秒?另外,对于最初的几个人,他们什么时候出生在位置 2?您是否希望它开始将它们显示为红点,直到它们到达位置 3?如果是这种情况,那么这些人的位置 2 时间在技术上是 0。
  • @GabeMorris 是的,先生。我希望前几个人显示为红色。此外,正确的位置 2 时间对他们来说是零!单位是秒。
  • 我正在研究一种可扩展的替代解决方案。 1-2 小时内完成。

标签: python matplotlib animation


【解决方案1】:

以下是使用 python-ffmpeg、pandas、matplotlib 和 seaborn 的实现。您可以在我的 YouTube 频道上找到输出 video(链接未列出)。

每个带有数字的帧都直接保存到内存中。只有当人口状态发生变化(人出现/移动/消失)时,才会生成新的数字。

如果你在 Python 包中使用它,你应该明确地将这段代码分成更小的块:

from numpy.random import RandomState, SeedSequence
from numpy.random import MT19937
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
import ffmpeg


RESOLUTION = (12.8, 7.2)        # * 100 pixels
NUMBER_OF_FRAMES = 900


class VideoWriter:
    # Courtesy of https://github.com/kylemcdonald/python-utils/blob/master/ffmpeg.py
    def __init__(
        self,
        filename,
        video_codec="libx265",
        fps=15,
        in_pix_fmt="rgb24",
        out_pix_fmt="yuv420p",
        input_args=None,
        output_args=None,
    ):
        self.filename = filename
        self.process = None
        self.input_args = {} if input_args is None else input_args
        self.output_args = {} if output_args is None else output_args
        self.input_args["r"] = self.input_args["framerate"] = fps
        self.input_args["pix_fmt"] = in_pix_fmt
        self.output_args["pix_fmt"] = out_pix_fmt
        self.output_args["vcodec"] = video_codec

    def add(self, frame):
        if self.process is None:
            height, width = frame.shape[:2]
            self.process = (
                ffmpeg.input(
                    "pipe:",
                    format="rawvideo",
                    s="{}x{}".format(width, height),
                    **self.input_args,
                )
                .filter("crop", "iw-mod(iw,2)", "ih-mod(ih,2)")
                .output(self.filename, **self.output_args)
                .global_args("-loglevel", "quiet")
                .overwrite_output()
                .run_async(pipe_stdin=True)
            )
        conv = frame.astype(np.uint8).tobytes()
        self.process.stdin.write(conv)

    def close(self):
        if self.process is None:
            return
        self.process.stdin.close()
        self.process.wait()


def figure_to_array(figure):
    """adapted from: https://stackoverflow.com/questions/21939658/"""
    figure.canvas.draw()
    buf = figure.canvas.tostring_rgb()
    n_cols, n_rows = figure.canvas.get_width_height()
    return np.frombuffer(buf, dtype=np.uint8).reshape(n_rows, n_cols, 3)


# Generate data for the figure
rs1 = RandomState(MT19937(SeedSequence(123456789)))

time_1 = np.round(rs1.rand(232) * NUMBER_OF_FRAMES).astype(np.int16)
time_2 = time_1 + np.round(rs1.rand(232) * (NUMBER_OF_FRAMES - time_1)).astype(np.int16)
time_3 = time_2 + np.round(rs1.rand(232) * (NUMBER_OF_FRAMES - time_2)).astype(np.int16)

loc_1_x, loc_1_y, loc_2_x, loc_2_y, loc_3_x, loc_3_y = np.round(rs1.rand(6, 232) * 100, 1)

df = pd.DataFrame({
    "loc_1_time": time_1,
    "loc_1_x": loc_1_x,
    "loc_1_y": loc_1_y,
    "loc_2_time": time_2,
    "loc_2_x": loc_2_x,
    "loc_2_y": loc_2_y,
    "loc_3_time": time_3,
    "loc_3_x": loc_3_x,
    "loc_3_y": loc_3_y,
})
"""The stack answer starts here"""
# Add extra column for disappear time
df["disappear_time"] = df["loc_3_time"] + 3

all_times = df[["loc_1_time", "loc_2_time", "loc_3_time", "disappear_time"]]
change_times = np.unique(all_times)

# Prepare ticks for plotting the figure across frames
x_values = df[["loc_1_x", "loc_2_x", "loc_3_x"]].values.flatten()
x_ticks = np.array(np.linspace(x_values.min(), x_values.max(), 6), dtype=np.uint8)

y_values = df[["loc_1_y", "loc_2_y", "loc_3_y"]].values.flatten()
y_ticks = np.array(np.round(np.linspace(y_values.min(), y_values.max(), 6)), dtype=np.uint8)

sns.set_theme(style="whitegrid")
video_writer = VideoWriter("endermen.mp4")
if 0 not in change_times:
    # Generate empty figure if no person arrive at t=0
    fig, ax = plt.subplots(figsize=RESOLUTION)
    ax.set_xticklabels(x_ticks)
    ax.set_yticklabels(y_ticks)
    ax.set_title("People movement. T=0")

    video_writer.add(figure_to_array(fig))

    loop_range = range(1, NUMBER_OF_FRAMES)
else:
    loop_range = range(NUMBER_OF_FRAMES)

palette = sns.color_palette("tab10")        # Returns three colors from the palette (we have three groups)
animation_data_df = pd.DataFrame(columns=["x", "y", "location", "index"])
for frame_idx in loop_range:
    if frame_idx in change_times:
        plt.close("all")
        # Get person who appears/moves/disappears
        indexes, loc_nums = np.where(all_times == frame_idx)
        loc_nums += 1

        for i, loc in zip(indexes, loc_nums):
            if loc != 4:
                x, y = df[[f"loc_{loc}_x", f"loc_{loc}_y"]].iloc[i]

            if loc == 1:            # location_1
                animation_data_df = animation_data_df.append(
                    {"x": x, "y": y, "location": loc, "index": i},
                    ignore_index=True
                )
            else:
                data_index = np.where(animation_data_df["index"] == i)[0][0]
                if loc in (2, 3):   # location_2 or 3
                    animation_data_df.loc[[data_index], :] = x, y, loc, i
                elif loc == 4:      # Disappear
                    animation_data_df.iloc[data_index] = np.nan

        current_palette_size = np.sum(~np.isnan(np.unique(animation_data_df["location"])))
        fig, ax = plt.subplots(figsize=RESOLUTION)
        sns.scatterplot(
            x="x", y="y", hue="location", data=animation_data_df, ax=ax, palette=palette[:current_palette_size]
        )

        ax.set_xticks(x_ticks)
        ax.set_xticklabels(x_ticks)
        ax.set_yticks(y_ticks)
        ax.set_yticklabels(y_ticks)
        ax.legend(loc="center left", bbox_to_anchor=(1, 0.5))

    ax.set_title(f"People movement. T={frame_idx}")
    video_writer.add(figure_to_array(fig))

video_writer.close()

编辑:存在 3 秒后未删除 location_3 的错误。现已修复。

【讨论】:

    【解决方案2】:

    修改this问题中的代码以仅包含您想要的职位,如果旧职位不包含在新职位中,则会自动删除旧职位。如果您想按时间或迭代或其他任何方式制作动画,这不会改变。我选择在这里使用迭代,因为它更容易而且我不知道您如何处理数据集。虽然代码确实有一个错误,但剩余的最后一点(或持续相同时间的点)不会消失,如果你不想再次绘制任何东西,这可以很容易解决,如果你这样做的话例如,如果您在没有人的情况下数据存在差距,然后数据恢复,我还没有找到任何解决方法

    import math
    import numpy as np
    import matplotlib.pyplot as plt
    from matplotlib.animation import FuncAnimation
    
    #The t0,t1,t2,t3 are the times (in iterations) that the position changes 
    #If t0 is None then the person will never be displayed
    people = [
        # t0          x1              y1             t1    x2   y2    t2   x3    y3    t4
        [ 0,          1,             0.1,             1,   2,   0.2,   2,   3,  0.3,   3],
        [ 2,          None,         None,          None,   2,   1,     3,   4,    1,   7],
        [ 2,  float("NaN"), float("NaN"),  float("NaN"),   2,   0.8,   4,   4,  0.8,   10],
    ]
    
    fig = plt.figure()
    plt.xlim(0, 5)
    plt.ylim(0, 1)
    graph = plt.scatter([], [])
    
    
    def animate(i):
        points = []
        colors = []
        for person in people:
            if person[0] is None or math.isnan(person[0]) or i < person[0]:
                continue
            # Position 1
            elif person[3] is not None and not (math.isnan(person[3])) and i <= person[3]:
                new_point = [person[1], person[2]]
                color = "b"
            # Position 2
            elif person[6] is not None and not (math.isnan(person[6])) and i <= person[6]:
                new_point = [person[4], person[5]]
                color = "r"
            # Position 3
            elif person[9] is not None and not (math.isnan(person[9])) and i <= person[9]:
                new_point = [person[7], person[8]]
                color = "r"
            else:
                people.remove(person)
                new_point = []
    
            if new_point != []:
                points.append(new_point)
                colors.append(color)
    
        if points != []:
            graph.set_offsets(points)
            graph.set_facecolors(colors)
        else:
            # You can use graph.remove() to fix the last point not disappiring but you won't be able to plot anything after that
            # graph.remove()
            pass
    
        return graph
    
    
    ani = FuncAnimation(fig, animate, repeat=False, interval=500)
    plt.show()
    

    【讨论】:

    • 此解决方案不适用于 NaN 值。
    • 感谢您的解决方案。正如@Bur 所说,此解决方案不适用于 NaN 值。实际上,我正在寻找可扩展并适用于任何其他数据框的东西。
    • 我添加了代码来解释 None 和 NaN。 “适用于任何其他数据框”我不明白您的意思,您拥有的数据框非常具体
    • 我可能有偏见(我也在回答这个问题),但我同意 OP 的观点,即此类应用程序需要考虑可扩展性。
    • 我查看了您的解决方案,是的,它看起来确实更健壮和可扩展,但我会保留这个答案,以防有人想要更简单的东西。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-12-10
    • 2019-07-07
    • 1970-01-01
    • 1970-01-01
    • 2013-10-23
    相关资源
    最近更新 更多