当前位置: 首页 > news >正文

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

目录

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.gdi32self.user32 = ctypes.windll.user32# 获取屏幕尺寸(含DPI缩放)SM_CXSCREEN, SM_CYSCREEN = 0, 1hdc = self.user32.GetDC(None)try:dpi = self.gdi32.GetDeviceCaps(hdc, 88)  # 获取DPIzoom = 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 += 1def 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.gdi32self.user32 = ctypes.windll.user32# 定义常量SM_CXSCREEN = 0SM_CYSCREEN = 1# 缩放比例zoom = 1hdc = self.user32.GetDC(None)try:dpi = self.gdi32.GetDeviceCaps(hdc, 88)zoom = dpi / 96.0finally: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 = 0DIB_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 = widthbmi.bmiHeader.biHeight = -height  # 注意:负高度表示自底向上的位图bmi.bmiHeader.biPlanes = 1bmi.bmiHeader.biBitCount = 24  # 24即3*8   32bmi.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_dataimport time# 控制帧率
class control_frame():def __init__(self):self.start_time = float()  # 每次启动时间self.fps = int(10)  # fpsself.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_timereturn 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.gdi32self.user32 = ctypes.windll.user32# 定义常量SM_CXSCREEN = 0SM_CYSCREEN = 1# 缩放比例zoom = 1hdc = self.user32.GetDC(None)try:dpi = self.gdi32.GetDeviceCaps(hdc, 88)zoom = dpi / 96.0finally: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 = 0DIB_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 = widthbmi.bmiHeader.biHeight = -height  # 注意:负高度表示自底向上的位图bmi.bmiHeader.biPlanes = 1bmi.bmiHeader.biBitCount = 24  # 24即3*8   32bmi.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_dataimport time# 控制帧率
class control_frame():def __init__(self):self.start_time = float()  # 每次启动时间self.fps = int(10)  # fpsself.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_timereturn 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)

相关文章:

  • 思尔芯携手Andes晶心科技,加速先进RISC-V 芯片开发
  • 华为仓颉语言初识:并发编程之同步机制(上)
  • 当丰收季遇上超导磁测量:粮食产业的科技新征程
  • Redis 主从 + 哨兵集群部署
  • 智慧水务发展迅猛:从物联网架构到AIoT系统的跨越式升级
  • Redis配合唯一序列号实现接口幂等性方案
  • App/uni-app 离线本地存储方案有哪些?最推荐的是哪种方案?
  • uniapp 安卓 APP 后台持续运行(保活)的尝试办法
  • H_Prj06 8088单板机的串口
  • TDengine 开发指南——无模式写入
  • matlab不同版本对编译器的要求(sfunction 死机)
  • 储能方案设计:鹧鸪云模拟软件优势尽显
  • HTTP 请求协议简单介绍
  • 豆包和deepseek 元宝 百度ai区别是什么
  • VR视频制作有哪些流程?
  • 【JVM】Java虚拟机(一)——内存结构
  • Android 之 kotlin 语言学习笔记四(Android KTX)
  • 数据集-目标检测系列- 口红嘴唇 数据集 lips >> DataBall
  • Shell 编程核心基础:输入输出与运算详解
  • dexcap升级版之DexWild——面向户外环境的灵巧手交互策略:人类和机器人演示协同训练(人类直接带上动捕手套采集数据)
  • 银医网站建设方案/北京seo网站推广
  • wordpress菜单子页面/seo门户网价格是多少钱
  • 代理服务器地址怎么设置/百度关键词优化多久上首页
  • 红塔网站制作/推广软件排行榜前十名
  • 数据网站建设工具模板/抖音搜索seo排名优化
  • 京东网站建设策略/微信怎么做推广