在之前这篇博客中简单介绍了利用内插算法内插数据,在本篇博客中,继续以内插算法为核心,实现一个更“好玩”的应用。 如果你在很久之前,如零几年,使用过电脑,一定对“屏保”这个词不陌生。那个时候挑选各种各样的屏保倒也是一种乐趣。有时会故意等个一两分钟让屏保出来。 在那个时候,有一种“泡泡屏保”很火,如下图。 当年第一次看到这种屏保的时候觉得特别好玩,可以盯着看半天。 简单科普下屏保的作用,过去很多那种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。
本文作者原创,未经许可不得转载,谢谢配合