popyone
发布于 2023-11-20 / 42 阅读
1
0

Python 通过OBS虚拟摄像头实现会议倒计时

分享这阵自己应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


评论