内插算法应用:泡泡动画屏保与Win10安装界面模拟

Nov 12,2018   8295 words   30 min


在之前这篇博客中简单介绍了利用内插算法内插数据,在本篇博客中,继续以内插算法为核心,实现一个更“好玩”的应用。 如果你在很久之前,如零几年,使用过电脑,一定对“屏保”这个词不陌生。那个时候挑选各种各样的屏保倒也是一种乐趣。有时会故意等个一两分钟让屏保出来。 在那个时候,有一种“泡泡屏保”很火,如下图。 当年第一次看到这种屏保的时候觉得特别好玩,可以盯着看半天。 简单科普下屏保的作用,过去很多那种CRT显示器,如果长时间显示同一内容,很容易烧屏,从而减少显示器寿命。所以为了避免人离开电脑后显示器一直显示同一内容,就有了各种动态屏保。 而现在之所以各种屏保都消失了是因为现在都是液晶显示屏了,不再存在烧屏的问题了,所以屏保就退出了历史舞台。

说了这么多屏保的事,你可能会比较好奇这跟内插有什么关系。答案是有关系的。因为在本篇博客中就会利用Python简单实现一个泡泡屏保(只有两个泡泡),”追忆”一下逝去的日子。

1.再说内插

在那篇博客中介绍了内插可以用来内插数据,这种思想非常有用。简单概括就是只要知道两个时刻的状态,就可以通过数学方法(如线性内插)获得中间任一时刻的状态。 这个思想的用处之一就是可以用来做动画。如果你用Flash这个软件做过动画,一定对“关键帧”这个概念不会陌生。创建了两个关键帧后,就可以在两帧之间创建补间动画了。 这个补间动画其实就可以理解为是利用内插的思想创建出来的。知道了t1和t2时刻物体的位置,即可内插出物体的运动过程(线性轨迹);知道了t1和t2时刻物体的颜色,即可内插出颜色的变化过程(线性变化);知道了t1和t2时刻物体的大小,即可内插出物体的缩放过程(线性缩放)。

2.实现思路

在有了上面的介绍之后,再来看泡泡动态屏保,就会发现它和内插是有比较密切的关系的。只需要随机生成不同时刻泡泡的位置、大小以及颜色,再利用内插算法内插,即可获得整个变化的动态过程。 具体到技术,要实现泡泡屏保,其实核心就是泡泡的绘制与展示,这些全部可以由OpenCV库完成,通过绘制的圆来代替泡泡。内插采用SciPy库的内插接口实现,随机数生成采用Numpy库实现。 一次内插对应泡泡的一次变化(位移、缩放、变色),因此我们需要不停地进行内插、循环,这可以使用while循环实现。 同时,为了让泡泡的每一次变化看起来不那么生硬,可以在指定范围内随机生成泡泡一次变化的时间,这样泡泡每次变化的速度就不同了。 以上便是实现的核心思路,具体细节在代码中介绍。

3.代码

# coding=utf-8
from scipy.interpolate import interp1d
import numpy as np
import cv2
from win32api import GetSystemMetrics


def interMotion(start_x, start_y, end_x, end_y, time,
                fps=60):
    """
    用于对泡泡的运动状态进行内插

    :param start_x: t1时刻的x
    :param start_y: t1时刻的y
    :param end_x: t2时刻的x
    :param end_y: t2时刻的y
    :param time: 变化所对应的时间,单位:秒
    :param fps: 最后生成动画时的fps(每秒帧数)
    :return: 内插出的变化过程中各帧中物体的位置
    """
    t = [0, 1]
    x = [start_x, end_x]
    y = [start_y, end_y]

    f_x = interp1d(t, x)
    f_y = interp1d(t, y)

    ts = np.linspace(t[0], t[1], int(time) * fps)
    xs = f_x(ts)
    ys = f_y(ts)

    return xs, ys


def interColor(b_old, g_old, r_old, b_new, g_new, r_new, time, fps=60):
    """
    用于对泡泡的颜色进行内插

    :param b_old: t1时刻的blue通道灰度
    :param g_old: t1时刻的green通道灰度
    :param r_old: t1时刻的red通道灰度
    :param b_new: t2时刻的blue通道灰度
    :param g_new: t2时刻的green通道灰度
    :param r_new: t2时刻的red通道灰度
    :param time: 变化所对应的时间,单位:秒
    :param fps: 最后生成动画时的fps(每秒帧数)
    :return: 内插出的变化过程中各帧中物体的颜色
    """
    t = [0, 1]
    r = [r_old, r_new]
    g = [g_old, g_new]
    b = [b_old, b_new]

    f_r = interp1d(t, r)
    f_g = interp1d(t, g)
    f_b = interp1d(t, b)

    ts = np.linspace(t[0], t[1], int(time) * fps)
    rs = f_r(ts)
    gs = f_g(ts)
    bs = f_b(ts)
    return bs, gs, rs


def interRadius(r_old, r_new, time, fps=60):
    """
    用于内插泡泡半径

    :param r_old: t1时刻半径
    :param r_new: t2时刻半径
    :param time:变化对应时间,单位s
    :param fps:最后生成动画时的fps(每秒帧数)
    :return:内插出的变化过程中各帧中物体的半径
    """

    t = [0, 1]
    r = [r_old, r_new]
    f_r = interp1d(t, r)
    ts = np.linspace(t[0], t[1], int(time) * fps)
    rs = f_r(ts)
    return rs


def interBubble(img_width, img_height,
                max_radius, min_radius,
                every_time, fps,
                init_x, init_y,
                init_b, init_g, init_r,
                init_radius):
    """
    用于泡泡位置、大小、颜色的内插函数

    :param img_width:影像宽度
    :param img_height:影像高度
    :param max_radius:泡泡最大半径
    :param min_radius:泡泡最小半径
    :param every_time:两个状态间的时间
    :param fps:每秒帧数
    :param init_x:初始状态x坐标
    :param init_y:初始状态y坐标
    :param init_b:初始状态颜色blue波段
    :param init_g:初始状态颜色green波段
    :param init_r:初始状态颜色red波段
    :param init_radius:初始状态泡泡半径
    :return:各状态参数
    """
    end_x = np.random.randint(0, img_width + 1)
    end_y = np.random.randint(0, img_height + 1)
    xs, ys = interMotion(init_x, init_y, end_x, end_y, every_time, fps=fps)
    print "motion:", "(" + init_x.__str__() + "," + init_y.__str__() + ")->(" + end_x.__str__() + "," + end_y.__str__() + ")"

    end_b = np.random.randint(0, 256)
    end_g = np.random.randint(0, 256)
    end_r = np.random.randint(0, 256)
    bs, gs, rs = interColor(init_b, init_g, init_r, end_b, end_g, end_r, every_time, fps=fps)
    print "color:", "(" + init_b.__str__() + "," + init_g.__str__() + "," + init_r.__str__() + ")->(" + \
                    end_b.__str__() + "," + end_g.__str__() + "," + end_r.__str__() + ")"

    end_radius = np.random.randint(min_radius, max_radius + 1)
    radius_new = interRadius(init_radius, end_radius, every_time, fps=fps)
    print "radius:", init_radius, "->", end_radius
    return xs, ys, bs, gs, rs, radius_new, end_x, end_y, end_b, end_g, end_r, end_radius


def initParams(img_width, img_height, max_radius, min_radius):
    """
    用于首次运行初始化参数

    :param img_width: 影像宽度
    :param img_height: 影像高度
    :param max_radius: 泡泡最大半径
    :param min_radius: 泡泡最小半径
    :return: 初始状态参数
    """
    init_x = np.random.randint(0, img_width + 1)
    init_y = np.random.randint(0, img_height + 1)
    print "init (x,y):", init_x, init_y

    init_b = np.random.randint(0, 256)
    init_g = np.random.randint(0, 256)
    init_r = np.random.randint(0, 256)
    print "init (b,g,r):", init_b, init_g, init_r

    init_radius = np.random.randint(min_radius, max_radius + 1)
    print "init radius:", init_radius

    return init_x, init_y, init_b, init_g, init_r, init_radius


def clickAndExit(event, x, y, flags, param):
    """
    OpenCV图片显示窗口的回调函数,用于实现点击窗口退出功能

    :param event:
    :param x:
    :param y:
    :param flags:
    :param param:
    :return:
    """

    if event == cv2.EVENT_LBUTTONDOWN:
        cv2.destroyWindow("Img")
        exit()


if __name__ == '__main__':
    img_width = 500
    img_height = 400
    max_radius = 50
    min_radius = 30
    min_time = 3
    max_time = 5
    fps = 120
    flag_fullscreen = False

    if flag_fullscreen:
        # 调用API获取屏幕分辨率
        img_width = GetSystemMetrics(0)
        img_height = GetSystemMetrics(1)
        print "screen width =", GetSystemMetrics(0)
        print "screen height =", GetSystemMetrics(1)

    init_x, init_y, init_b, init_g, init_r, init_radius = initParams(img_width, img_height, max_radius, min_radius)
    init_x2, init_y2, init_b2, init_g2, init_r2, init_radius2 = initParams(img_width, img_height, max_radius,
                                                                           min_radius)

    while True:

        # 每次随机指定一次变化的时间
        every_time = np.random.randint(min_time, max_time + 1)
        print "\ntime:", every_time

        # 随机指定两个泡泡的重叠情况
        overlay_flag = np.random.randint(0, 2)
        print "overlay flag:", overlay_flag

        xs, ys, bs, gs, rs, radius_new, end_x, end_y, end_b, end_g, end_r, end_radius = interBubble(img_width,
                                                                                                    img_height,
                                                                                                    max_radius,
                                                                                                    min_radius,
                                                                                                    every_time, fps,
                                                                                                    init_x, init_y,
                                                                                                    init_b, init_g,
                                                                                                    init_r,
                                                                                                    init_radius)
        # 将本阶段结束的状态赋给下一阶段作为初值
        init_x = end_x
        init_y = end_y
        init_b = end_b
        init_g = end_g
        init_r = end_r
        init_radius = end_radius

        xs2, ys2, bs2, gs2, rs2, radius_new2, end_x2, end_y2, end_b2, end_g2, end_r2, end_radius2 = interBubble(
            img_width,
            img_height,
            max_radius,
            min_radius,
            every_time,
            fps,
            init_x2,
            init_y2,
            init_b2,
            init_g2,
            init_r2,
            init_radius2)
        init_x2 = end_x2
        init_y2 = end_y2
        init_b2 = end_b2
        init_g2 = end_g2
        init_r2 = end_r2
        init_radius2 = end_radius2

        for i in range(xs.__len__()):
            img = np.zeros([img_height, img_width, 3], np.uint8)
            # 绘图
            if overlay_flag == 0:
                img2 = cv2.circle(img, (int(xs[i]), int(ys[i])),
                                  radius=int(radius_new[i]),
                                  color=(rs[i], gs[i], bs[i]),
                                  thickness=-1,
                                  lineType=cv2.LINE_AA)
                img3 = cv2.circle(img2, (int(xs2[i]), int(ys2[i])),
                                  radius=int(radius_new2[i]),
                                  color=(rs2[i], gs2[i], bs2[i]),
                                  thickness=-1,
                                  lineType=cv2.LINE_AA)
            else:
                img2 = cv2.circle(img, (int(xs2[i]), int(ys2[i])),
                                  radius=int(radius_new2[i]),
                                  color=(rs2[i], gs2[i], bs2[i]),
                                  thickness=-1,
                                  lineType=cv2.LINE_AA)
                img3 = cv2.circle(img2, (int(xs[i]), int(ys[i])),
                                  radius=int(radius_new[i]),
                                  color=(rs[i], gs[i], bs[i]),
                                  thickness=-1,
                                  lineType=cv2.LINE_AA)
            if flag_fullscreen:
                # 设置窗口回调函数
                cv2.namedWindow("Img", cv2.WND_PROP_FULLSCREEN)
                cv2.setMouseCallback("Img", clickAndExit)
                # 图片展示窗口全屏设置
                cv2.setWindowProperty("Img", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN)
                cv2.imshow("Img", img3)

            else:
                cv2.namedWindow("Img")
                cv2.setMouseCallback("Img", clickAndExit)
                cv2.imshow("Img", img3)
            cv2.waitKey(int(1000 / fps))
            k = cv2.waitKey(1) & 0xFF
            if k == 27:
                cv2.destroyWindow("Img")
                exit()

4.测试

在本机测试结果如下。 为减少上传大小,对动图进行了加速、抽帧等操作,所以看起来运动不是那么顺滑,在实际运行中效果会比这个好很多。 如果在代码中设置成了全屏模式,则就非常类似泡泡屏保了。

5.彩蛋

利用内插还可以模拟Win10的安装界面,代码比较简单,如下。

# coding=utf-8

from scipy.interpolate import interp1d
import numpy as np
import cv2


def interColor(b_old, g_old, r_old, b_new, g_new, r_new, time, fps=60):
    t = [0, 1]
    r = [r_old, r_new]
    g = [g_old, g_new]
    b = [b_old, b_new]

    f_r = interp1d(t, r)
    f_g = interp1d(t, g)
    f_b = interp1d(t, b)

    ts = np.linspace(t[0], t[1], int(time) * fps)
    rs = f_r(ts)
    gs = f_g(ts)
    bs = f_b(ts)
    return bs, gs, rs


if __name__ == '__main__':
    img_width = 500
    img_height = 300
    img = np.zeros([img_height, img_width, 3], np.uint8) + 255

    # blue -> red
    bs1, gs1, rs1 = interColor(255, 0, 0, 0, 0, 255, 3)
    # red -> green
    bs2, gs2, rs2 = interColor(0, 0, 255, 0, 255, 0, 3)
    # green -> blue
    bs3, gs3, rs3 = interColor(0, 255, 0, 255, 0, 0, 3)

    bs = np.hstack((bs1, bs2, bs3))
    gs = np.hstack((gs1, gs2, gs3))
    rs = np.hstack((rs1, rs2, rs3))

    center_x = img_width / 2
    center_y = int(0.4 * img_height)
    rect_width = 50
    margin = 2

    while True:
        for r, g, b in zip(rs, gs, bs):
            img[:, :, 0] = b
            img[:, :, 1] = g
            img[:, :, 2] = r

            cv2.rectangle(img,
                          (center_x - margin - rect_width, center_y - margin - rect_width),
                          (center_x - margin, center_y - margin),
                          color=(255, 255, 255), thickness=-1)
            cv2.rectangle(img,
                          (center_x - margin - rect_width, center_y + margin),
                          (center_x - margin, center_y + margin + rect_width),
                          color=(255, 255, 255), thickness=-1)
            cv2.rectangle(img,
                          (center_x + margin, center_y - margin - rect_width),
                          (center_x + margin + rect_width, center_y - margin),
                          color=(255, 255, 255), thickness=-1)
            cv2.rectangle(img,
                          (center_x + margin, center_y + margin),
                          (center_x + margin + rect_width, center_y + margin + rect_width),
                          color=(255, 255, 255), thickness=-1)
            cv2.putText(img, "Welcome to Secret Land",
                        (int(0.18 * img_width), int(0.75 * img_height)),
                        cv2.FONT_HERSHEY_SCRIPT_SIMPLEX, 1, (255, 255, 255),
                        1, cv2.LINE_AA)
            cv2.imshow("img", img)
            cv2.waitKey(int(1000 / 60.0))

实现的效果如下,应该比较熟悉。

最后,还是老惯例,代码放在了Github上,方便使用,点击查看,也欢迎Start或Fork。

本文作者原创,未经许可不得转载,谢谢配合

返回顶部