分享这阵自己应LP需求,写的一个虚拟摄像头项目
需求:在视频会议中播放倒计时,给大家当时间管理员,当前使用的是OBS作为倒计时管理器,功能强大,不过对于新手和小白并不友好,OBS的配置逻辑清晰,步骤复杂。现在就想要一个简单的倒计时软件,小白新手能直接上手。
需求拆分开来就有三个要解决的问题:
1、怎么在视频会议中播放倒计时;
2、倒计时的生成使用哪个库;
3、GUI界面的选用;
解决方案:
1、使用OBS安装默认的虚拟摄像头,python通过pyvirtualcam输出到虚拟摄像头;
2、倒计时开始使用的是opencv库,但是opencv库不支持外部字体,以后如果需要更换字体,那就是一个大麻烦,整个都得重写;后来通过查询资料,发现pillow能很好的解决这个问题,图像处理、合成、描边,一个就能完成;
3、刚开始用的是PySimpleGUI来做GUI显示,网络上的文章都介绍说这个上手速度快,用了之后发现的确开发速度快,但是很多功能判断要自己写。花了两周的空闲时间来做这个,做出来后发现这个的合成图片的时候会卡,怀疑是因为在循环中读取ui界面输入引起的(合成图片倒计时的循环用的是子线程)。最终换成了Pyside6来做GUI,点击、播放非常流畅。
下面的是虚拟摄像头倒计时图片合成代码:
# -*- coding: utf-8 -*-
import numpy as np
import time
from PIL import Image, ImageDraw, ImageFont, ImageTk
class Camera_start(QObject):
# 通过类成员对象定义信号对象
signal = Signal(Image.Image, bool, bool)
def __init__(self, image, countdown_seconds, resolution, font_file, start_color_array, end_color_array, num_colors,
music_file, parent=None) -> None:
"""
:param image: 背景图片
:param countdown_seconds: 倒计时时间
:param resolution: 摄像头分辨率
:param font_file: 字体文件
:param start_color_array: 开始颜色
:param end_color_array: 结束颜色
:param num_colors: 渐变色数量
:param music_file: 音乐文件
:param parent:
"""
super(Camera_start, self).__init__(parent)
self.font_file_cn = f'{path}/YSHaoShenTi-2.ttf'
self.flag1 = False
self.flag2 = False
self.image = image # 原始图像
self.countdown_seconds = countdown_seconds # 倒计时时间
self.resolution = resolution # 虚拟摄像头分辨率列表
self.font_file = font_file # 文本参数
self.start_color_array = start_color_array # 开始颜色
self.end_color_array = end_color_array # 结束颜色
self.num_colors = num_colors # 渐变色数量
self.music_file = music_file # 音乐文件
self.fps = 25 # 帧率
def gradient_color(self, start_color, end_color, num):
"""
颜色变化列表
:param start_color: 起始颜色
:param end_color: 终止颜色
:param num: 颜色数量
:return:
"""
start_rgb = np.array(start_color)
end_rgb = np.array(end_color)
color_list = []
for i in range(num):
ratio = i / float(num - 1)
color = tuple((1 - ratio) * start_rgb + ratio * end_rgb)
integer_color = tuple(round(num) for num in color) # 浮点转整数
# print(integer_color)
color_list.append(integer_color)
return color_list
def Text_time(self, remaining_time):
"""
格式化时间显示
:param remaining_time: 剩余时间
:return: 小时、分、秒
"""
# 在图像上显示时分秒
hours = int(remaining_time / 3600)
minutes = int((remaining_time % 3600) / 60)
seconds = remaining_time % 60
if hours == 0 and minutes == 0:
time_text = '{:02d}'.format(seconds)
elif hours == 0:
time_text = '{:02d}:{:02d}'.format(minutes, seconds)
else:
time_text = '{:02d}:{:02d}:{:02d}'.format(hours, minutes, seconds)
return time_text
def generate_countdown_image(self, image_resize, countdown, text_color, font_file, cam_width, cam_height):
"""
倒计时图片合成
:param image_resize: 根据分辨率调整大小后的图片
:param countdown: 倒计时时间
:param text_color: 字体颜色
:param font_file: 体文件
:param cam_width: 摄像头宽度
:param cam_height: 摄像头高度
:return: 合并后的图片
"""
# 创建一个新的图像
image = Image.new("RGB", (cam_width, cam_height), color=(0, 0, 0))
# 复制图像的一部分到新位置
image.paste(image_resize, (0, 0))
# 创建一个可绘制的图像对象
draw = ImageDraw.Draw(image)
# 设置一个初始字体大小
font_size = 10
# 计算文本大小
text = f"{countdown}"
# 逐渐增加字体大小,直到文本适应窗口
while True:
font_size += 1
font = ImageFont.truetype(font_file, size=font_size)
left, top, right, bottom = font.getbbox(text)
text_width = right - left
text_height = bottom - top
if text_width > cam_width or text_height > cam_height:
break
font_size = int((font_size - 1) * 0.9)
last_font = ImageFont.truetype(font_file, size=font_size)
last_left, last_top, last_right, last_bottom = last_font.getbbox(text)
last_text_width = last_right + last_left
last_text_height = last_bottom + last_top
# 绘制倒计时文本居中的像素位置
text_x = (cam_width - last_text_width) // 2
text_y = (cam_height - last_text_height) // 2
draw.text((text_x, text_y), text, fill=text_color, font=last_font, stroke_width=1, stroke_fill="black")
return image.convert('RGB')
def camera_countdown(self):
"""
读取颜色参数,倒计时总时间分割成num份,显示渐变色
:return:
"""
# 缩放背景图片适应分辨率
# image_resize = self.Resize(image, window_width=width, window_height=height)
image_resize = self.image.resize((self.resolution[0], self.resolution[1]))
# 获取渐变色列表
colors = self.gradient_color(start_color=self.start_color_array, end_color=self.end_color_array,
num=self.num_colors)
# 文字合成("时间到")
img_end = self.generate_countdown_image(image_resize, '时间到', tuple(self.end_color_array), self.font_file_cn,
self.resolution[0], self.resolution[1])
with Camera(width=self.resolution[0], height=self.resolution[1], fps=self.fps, fmt=PixelFormat.RGB) as cam:
start_time = time.time()
self.signal.emit(None, self.flag1, self.flag2)
while self.flag1:
# 计算倒计时剩余时间
elapsed_time = time.time() - start_time
remaining_time = max(self.countdown_seconds - int(elapsed_time), 0)
# print(f'实时时间: {remaining_time}')
# 退出循环条件
if remaining_time == 0:
self.flag2 = True
break
# 剩余时间/倒计时时间
progress = elapsed_time / self.countdown_seconds
# 在图像上显示时分秒,格式化时间
time_text = self.Text_time(remaining_time)
# 字体颜色
text_color = colors[int(progress * self.num_colors)]
# 矫正文字大小,适配图片,返回合成图片
img = self.generate_countdown_image(image_resize, time_text, text_color, self.font_file,
self.resolution[0], self.resolution[1])
# 将帧发送到虚拟摄像头
cam.send(np.array(img))
self.signal.emit(img, self.flag1, self.flag2)
self.flag1 = False
self.signal.emit(None, self.flag1, self.flag2)
while self.flag2:
# 将时间到的帧发送到虚拟摄像头
cam.send(np.array(img_end))
self.signal.emit(img_end, self.flag1, self.flag2)
self.flag2 = False
self.signal.emit(None, self.flag1, self.flag2)
Pysid6的部分代码是通过Designer制作的,这里就不放出来了
附件是打包好的单体文件
虚拟摄像头v1.2.exe