Python|GIF 解析与构建(5):手搓截屏和帧率控制

发布于:2025-06-09 ⋅ 阅读:(16) ⋅ 点赞:(0)

目录

Python|GIF 解析与构建(5):手搓截屏和帧率控制

一、引言

二、技术实现:手搓截屏模块

2.1 核心原理

2.2 代码解析:ScreenshotData类

2.2.1 截图函数:capture_screen

三、技术实现:帧率控制模块

3.1 原理:基于时间差的帧率控制

3.2 代码解析:control_frame类

四、效果验证:截屏与帧率测试

4.1 测试代码

五、优化方向与注意事项

六、总结


Python|GIF 解析与构建(5):手搓截屏和帧率控制

一、引言

在 GIF 构建场景中,实时捕获屏幕画面并精准控制帧率是核心需求之一。本文将基于 Windows API,通过ctypes库实现自定义截屏功能,并结合时间管理实现帧率控制,为后续 GIF 动态图生成奠定基础。

二、技术实现:手搓截屏模块

2.1 核心原理

利用 Windows API 中的 GDI(图形设备接口)实现屏幕像素数据抓取,主要流程:

  1. 获取屏幕设备上下文(DC)
  2. 创建兼容位图(Bitmap)存储截图数据
  3. 通过BitBlt函数复制屏幕区域像素
  4. 解析位图数据获取 RGB 像素值

2.2 代码解析:ScreenshotData

class ScreenshotData():
    def __init__(self):
        # 加载系统库
        self.gdi32 = ctypes.windll.gdi32
        self.user32 = ctypes.windll.user32
        
        # 获取屏幕尺寸(含DPI缩放)
        SM_CXSCREEN, SM_CYSCREEN = 0, 1
        hdc = self.user32.GetDC(None)
        try:
            dpi = self.gdi32.GetDeviceCaps(hdc, 88)  # 获取DPI
            zoom = dpi / 96.0  # 计算缩放比例
        finally:
            self.user32.ReleaseDC(None, hdc)
        self.screenWidth = int(self.user32.GetSystemMetrics(SM_CXSCREEN) * zoom)
        self.screenHeight = int(self.user32.GetSystemMetrics(SM_CYSCREEN) * zoom)
  • 关键点
    • 通过GetDeviceCaps获取 DPI,解决高分屏缩放问题
    • GetSystemMetrics获取原始屏幕分辨率,结合缩放比例计算实际尺寸
2.2.1 截图函数:capture_screen
def capture_screen(self, x, y, width, height):
    hwnd = self.user32.GetDesktopWindow()  # 获取桌面窗口句柄
    hdc_src = self.user32.GetDC(hwnd)      # 获取源设备上下文
    
    # 创建兼容内存DC和位图
    hdc_dest = self.gdi32.CreateCompatibleDC(hdc_src)
    bmp = self.gdi32.CreateCompatibleBitmap(hdc_src, width, height)
    old_bmp = self.gdi32.SelectObject(hdc_dest, bmp)
    
    # 复制屏幕区域(SRCCOPY为直接像素复制)
    self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, 0x00CC0020)
    
    # 解析位图像素数据(24位RGB格式)
    class RGBQUAD(ctypes.Structure): ...  # 颜色结构体定义
    class BITMAPINFOHEADER(ctypes.Structure): ...  # 位图信息头定义
    
    bmi = BITMAPINFO()
    bmi.bmiHeader = BITMAPINFOHEADER(
        ctypes.sizeof(BITMAPINFOHEADER), width, -height, 1, 24, 
        0, 0, 0, 0, 0, 0
    )
    pixel_data = (ctypes.c_ubyte * (width * height * 3))()  # 3字节/像素(RGB)
    
    # 获取像素数据
    if not self.gdi32.GetDIBits(hdc_src, bmp, 0, height, pixel_data, ctypes.byref(bmi), 0):
        print("GetDIBits failed:", ctypes.WinError())
    
    # 资源释放
    self.gdi32.SelectObject(hdc_dest, old_bmp)
    self.gdi32.DeleteDC(hdc_dest)
    self.user32.ReleaseDC(hwnd, hdc_src)
    self.gdi32.DeleteObject(bmp)
    
    return pixel_data
  • 核心步骤
    1. BitBlt实现屏幕区域拷贝(参数0x00CC0020为 SRCCOPY 模式)
    2. 通过BITMAPINFOHEADER定义位图格式(24 位真彩色,负高度表示自底向上存储)
    3. GetDIBits获取原始像素数据(BGR 顺序,需后续转换为 RGB)

三、技术实现:帧率控制模块

3.1 原理:基于时间差的帧率控制

通过记录每帧开始时间,计算实际耗时并与目标帧率时间比较,通过sleep补正时间差,公式:

  • 目标单帧时间:time_one_frame = 1 / fps
  • 实际耗时:spend = 当前时间 - 开始时间
  • 补正时间:max(0, time_one_frame - spend)

3.2 代码解析:control_frame

class control_frame():
    def __init__(self):
        self.start_time = 0.0       # 帧开始时间
        self.fps = 10               # 目标帧率
        self.time_one_frame = 1/self.fps  # 单帧时间
        self.fps_count = 0          # 总帧数
        self.time_all = time.time() # 启动时间
    
    def start(self):
        self.start_time = time.time()  # 记录帧开始时间
        self.fps_count += 1
    
    def spend(self):
        return time.time() - self.start_time  # 计算已用时间
    
    def wait(self):
        spend = self.spend()
        # 动态计算实际帧率(用于监控)
        true_frame = self.fps_count / (time.time() - self.time_all)
        if true_frame > self.fps:
            # 补正时间差,确保不超过目标帧率
            if self.time_one_frame - spend > 0:
                time.sleep(self.time_one_frame - spend)
  • 优势
    • 动态监控实际帧率(true_frame
    • 自适应系统负载,通过sleep柔性控制帧率

四、效果验证:截屏与帧率测试

4.1 测试代码

s = ScreenshotData()
wait = control_frame()
wait.fps = 20  # 设置目标帧率20FPS
frame_number = 100  # 捕获100帧
st = time.time()

for i in range(1, frame_number+1):
    wait.start()
    # 捕获屏幕区域(0,0)到(20,20)
    data = s.capture_screen(0, 0, 20, 20)
    wait.wait()
    # 输出实时帧率
    print(f"第{i}帧 | 实时帧率:{1 / ((time.time()-st)/i):.2f} FPS")

print(f"总耗时:{time.time()-st:.2f}秒 | 平均帧率:{frame_number/(time.time()-st):.2f} FPS")

五、优化方向与注意事项

  1. 截图性能优化

    • 使用GetDC/ReleaseDC时避免频繁调用,可改为缓存设备上下文
    • 尝试CreateDIBSection替代GetDIBits,减少内存拷贝
  2. 帧率控制增强

    • 增加帧率平滑算法(如指数移动平均)
    • 支持动态帧率调整(根据系统负载自动降帧)
  3. 跨平台适配

    当前仅支持 Windows

六、总结

本文通过ctypes实现了 Windows 下的自定义截屏,并基于时间管理实现了帧率控制,下一个就是关于tk的录制软件制作。

以下是完整代码:

import ctypes


# 获取屏幕数据
class ScreenshotData():
    def __init__(self):
        self.gdi32 = ctypes.windll.gdi32
        self.user32 = ctypes.windll.user32

        # 定义常量
        SM_CXSCREEN = 0
        SM_CYSCREEN = 1
        # 缩放比例
        zoom = 1
        hdc = self.user32.GetDC(None)
        try:
            dpi = self.gdi32.GetDeviceCaps(hdc, 88)
            zoom = dpi / 96.0
        finally:
            self.user32.ReleaseDC(None, hdc)
        self.screenWidth = int(self.user32.GetSystemMetrics(SM_CXSCREEN) * zoom)
        self.screenHeight = int(self.user32.GetSystemMetrics(SM_CYSCREEN) * zoom)

    # 屏幕截取
    def capture_screen(self, x, y, width, height):
        # 获取桌面窗口句柄
        hwnd = self.user32.GetDesktopWindow()
        # 获取桌面窗口的设备上下文
        hdc_src = self.user32.GetDC(hwnd)

        if len(str(hdc_src)) > 16:
            return 0

        # 创建一个与屏幕兼容的内存设备上下文
        hdc_dest = self.gdi32.CreateCompatibleDC(hdc_src)

        # 创建一个位图
        bmp = self.gdi32.CreateCompatibleBitmap(hdc_src, width, height)

        # 将位图选入内存设备上下文
        old_bmp = self.gdi32.SelectObject(hdc_dest, bmp)

        # 定义SRCCOPY常量
        SRCCOPY = 0x00CC0020
        # 捕获屏幕
        self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, SRCCOPY)
        """
        gdi32.BitBlt(hdc_src,  # 目标设备上下文  
                 x_dest,   # 目标矩形左上角的x坐标  
                 y_dest,   # 目标矩形左上角的y坐标  
                 width,    # 宽度  
                 height,   # 高度  
                 hdc_dest, # 源设备上下文  
                 x_src,    # 源矩形左上角的x坐标(通常是0)  
                 y_src,    # 源矩形左上角的y坐标(通常是0)  
                 SRCCOPY)  # 复制选项
        """

        # 定义 RGBQUAD 结构体
        class RGBQUAD(ctypes.Structure):
            _fields_ = [("rgbBlue", ctypes.c_ubyte),
                        ("rgbGreen", ctypes.c_ubyte),
                        ("rgbRed", ctypes.c_ubyte),
                        ("rgbReserved", ctypes.c_ubyte)]

        # 定义 BITMAPINFOHEADER 结构体
        class BITMAPINFOHEADER(ctypes.Structure):
            _fields_ = [("biSize", ctypes.c_uint),
                        ("biWidth", ctypes.c_int),
                        ("biHeight", ctypes.c_int),
                        ("biPlanes", ctypes.c_ushort),
                        ("biBitCount", ctypes.c_ushort),
                        ("biCompression", ctypes.c_uint),
                        ("biSizeImage", ctypes.c_uint),
                        ("biXPelsPerMeter", ctypes.c_int),
                        ("biYPelsPerMeter", ctypes.c_int),
                        ("biClrUsed", ctypes.c_uint),
                        ("biClrImportant", ctypes.c_uint)]

        # 定义 BITMAPINFO 结构体
        class BITMAPINFO(ctypes.Structure):
            _fields_ = [("bmiHeader", BITMAPINFOHEADER),
                        ("bmiColors", RGBQUAD * 3)]  # 只分配了3个RGBQUAD的空间

        BI_RGB = 0
        DIB_RGB_COLORS = 0

        # 分配像素数据缓冲区(这里以24位位图为例,每个像素3字节)
        pixel_data = (ctypes.c_ubyte * (width * height * 3))()  # 4

        # 填充 BITMAPINFO 结构体
        bmi = BITMAPINFO()
        bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
        bmi.bmiHeader.biWidth = width
        bmi.bmiHeader.biHeight = -height  # 注意:负高度表示自底向上的位图
        bmi.bmiHeader.biPlanes = 1
        bmi.bmiHeader.biBitCount = 24  # 24即3*8   32
        bmi.bmiHeader.biCompression = BI_RGB  # 无压缩

        # 调用 GetDIBits 获取像素数据
        ret = self.gdi32.GetDIBits(hdc_src, bmp, 0, height, pixel_data, ctypes.byref(bmi), DIB_RGB_COLORS)
        if ret == 0:
            print("GetDIBits failed:", ctypes.WinError())

        # 恢复设备上下文
        self.gdi32.SelectObject(hdc_dest, old_bmp)
        # 删除内存设备上下文
        self.gdi32.DeleteDC(hdc_dest)
        # 释放桌面窗口的设备上下文
        self.user32.ReleaseDC(hwnd, hdc_src)
        # bmp已经被处理,现在删除它
        self.gdi32.DeleteObject(bmp)
        return pixel_data


import time


# 控制帧率
class control_frame():
    def __init__(self):
        self.start_time = float()  # 每次启动时间
        self.fps = int(10)  # fps
        self.time_one_frame = 1 / self.fps  # 补正时间
        self.fps_count = 0  # 总帧率
        self.time_all = time.time()  # 启动时间

    # 启动
    def start(self):
        self.start_time = time.time()
        self.fps_count += 1

    # 花销
    def spend(self):
        spend = time.time() - self.start_time
        return spend

    # 等待
    def wait(self):
        spend = self.spend()
        true_frame = self.fps_count / (time.time() - self.time_all)
        if true_frame > self.fps:
            if self.time_one_frame - spend > 0:
                time.sleep(self.time_one_frame - spend)


s = ScreenshotData()
wait = control_frame()
# 帧率
wait.fps = 20
# 总帧率
frame_number = 100
st = time.time()
for i in range(1, frame_number + 1):
    wait.start()
    data = s.capture_screen(0, 0, 20, 20)
    wait.wait()
    print("帧率输出:", 1 / ((time.time() - st) / i))
print("花费时间:", time.time() - st)
import ctypes


# 获取屏幕数据
class ScreenshotData():
    def __init__(self):
        self.gdi32 = ctypes.windll.gdi32
        self.user32 = ctypes.windll.user32

        # 定义常量
        SM_CXSCREEN = 0
        SM_CYSCREEN = 1
        # 缩放比例
        zoom = 1
        hdc = self.user32.GetDC(None)
        try:
            dpi = self.gdi32.GetDeviceCaps(hdc, 88)
            zoom = dpi / 96.0
        finally:
            self.user32.ReleaseDC(None, hdc)
        self.screenWidth = int(self.user32.GetSystemMetrics(SM_CXSCREEN) * zoom)
        self.screenHeight = int(self.user32.GetSystemMetrics(SM_CYSCREEN) * zoom)

    # 屏幕截取
    def capture_screen(self, x, y, width, height):
        # 获取桌面窗口句柄
        hwnd = self.user32.GetDesktopWindow()
        # 获取桌面窗口的设备上下文
        hdc_src = self.user32.GetDC(hwnd)

        if len(str(hdc_src)) > 16:
            return 0

        # 创建一个与屏幕兼容的内存设备上下文
        hdc_dest = self.gdi32.CreateCompatibleDC(hdc_src)

        # 创建一个位图
        bmp = self.gdi32.CreateCompatibleBitmap(hdc_src, width, height)

        # 将位图选入内存设备上下文
        old_bmp = self.gdi32.SelectObject(hdc_dest, bmp)

        # 定义SRCCOPY常量
        SRCCOPY = 0x00CC0020
        # 捕获屏幕
        self.gdi32.BitBlt(hdc_dest, 0, 0, width, height, hdc_src, x, y, SRCCOPY)
        """
        gdi32.BitBlt(hdc_src,  # 目标设备上下文  
                 x_dest,   # 目标矩形左上角的x坐标  
                 y_dest,   # 目标矩形左上角的y坐标  
                 width,    # 宽度  
                 height,   # 高度  
                 hdc_dest, # 源设备上下文  
                 x_src,    # 源矩形左上角的x坐标(通常是0)  
                 y_src,    # 源矩形左上角的y坐标(通常是0)  
                 SRCCOPY)  # 复制选项
        """

        # 定义 RGBQUAD 结构体
        class RGBQUAD(ctypes.Structure):
            _fields_ = [("rgbBlue", ctypes.c_ubyte),
                        ("rgbGreen", ctypes.c_ubyte),
                        ("rgbRed", ctypes.c_ubyte),
                        ("rgbReserved", ctypes.c_ubyte)]

        # 定义 BITMAPINFOHEADER 结构体
        class BITMAPINFOHEADER(ctypes.Structure):
            _fields_ = [("biSize", ctypes.c_uint),
                        ("biWidth", ctypes.c_int),
                        ("biHeight", ctypes.c_int),
                        ("biPlanes", ctypes.c_ushort),
                        ("biBitCount", ctypes.c_ushort),
                        ("biCompression", ctypes.c_uint),
                        ("biSizeImage", ctypes.c_uint),
                        ("biXPelsPerMeter", ctypes.c_int),
                        ("biYPelsPerMeter", ctypes.c_int),
                        ("biClrUsed", ctypes.c_uint),
                        ("biClrImportant", ctypes.c_uint)]

        # 定义 BITMAPINFO 结构体
        class BITMAPINFO(ctypes.Structure):
            _fields_ = [("bmiHeader", BITMAPINFOHEADER),
                        ("bmiColors", RGBQUAD * 3)]  # 只分配了3个RGBQUAD的空间

        BI_RGB = 0
        DIB_RGB_COLORS = 0

        # 分配像素数据缓冲区(这里以24位位图为例,每个像素3字节)
        pixel_data = (ctypes.c_ubyte * (width * height * 3))()  # 4

        # 填充 BITMAPINFO 结构体
        bmi = BITMAPINFO()
        bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
        bmi.bmiHeader.biWidth = width
        bmi.bmiHeader.biHeight = -height  # 注意:负高度表示自底向上的位图
        bmi.bmiHeader.biPlanes = 1
        bmi.bmiHeader.biBitCount = 24  # 24即3*8   32
        bmi.bmiHeader.biCompression = BI_RGB  # 无压缩

        # 调用 GetDIBits 获取像素数据
        ret = self.gdi32.GetDIBits(hdc_src, bmp, 0, height, pixel_data, ctypes.byref(bmi), DIB_RGB_COLORS)
        if ret == 0:
            print("GetDIBits failed:", ctypes.WinError())

        # 恢复设备上下文
        self.gdi32.SelectObject(hdc_dest, old_bmp)
        # 删除内存设备上下文
        self.gdi32.DeleteDC(hdc_dest)
        # 释放桌面窗口的设备上下文
        self.user32.ReleaseDC(hwnd, hdc_src)
        # bmp已经被处理,现在删除它
        self.gdi32.DeleteObject(bmp)
        return pixel_data


import time


# 控制帧率
class control_frame():
    def __init__(self):
        self.start_time = float()  # 每次启动时间
        self.fps = int(10)  # fps
        self.time_one_frame = 1 / self.fps  # 补正时间
        self.fps_count = 0  # 总帧率
        self.time_all = time.time()  # 启动时间

    # 启动
    def start(self):
        self.start_time = time.time()
        self.fps_count += 1

    # 花销
    def spend(self):
        spend = time.time() - self.start_time
        return spend

    # 等待
    def wait(self):
        spend = self.spend()
        true_frame = self.fps_count / (time.time() - self.time_all)
        if true_frame > self.fps:
            if self.time_one_frame - spend > 0:
                time.sleep(self.time_one_frame - spend)


s = ScreenshotData()
wait = control_frame()
# 帧率
wait.fps = 20
# 总帧率
frame_number = 100
st = time.time()
for i in range(1, frame_number + 1):
    wait.start()
    data = s.capture_screen(0, 0, 20, 20)
    wait.wait()
    print("帧率输出:", 1 / ((time.time() - st) / i))
print("花费时间:", time.time() - st)