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(图形设备接口)实现屏幕像素数据抓取,主要流程:
- 获取屏幕设备上下文(DC)
- 创建兼容位图(Bitmap)存储截图数据
- 通过
BitBlt
函数复制屏幕区域像素 - 解析位图数据获取 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
- 核心步骤:
BitBlt
实现屏幕区域拷贝(参数0x00CC0020
为 SRCCOPY 模式)- 通过
BITMAPINFOHEADER
定义位图格式(24 位真彩色,负高度表示自底向上存储) 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")
五、优化方向与注意事项
-
截图性能优化:
- 使用
GetDC
/ReleaseDC
时避免频繁调用,可改为缓存设备上下文 - 尝试
CreateDIBSection
替代GetDIBits
,减少内存拷贝
- 使用
-
帧率控制增强:
- 增加帧率平滑算法(如指数移动平均)
- 支持动态帧率调整(根据系统负载自动降帧)
-
跨平台适配:
当前仅支持 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)