Python自带模块实现屏幕像素高效操作
1、获取屏幕放缩比例:
from ctypes import wintypes
import ctypes
HORZRES = 8
LOGPIXELSX = 118
def get_scale_factor() -> float:
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
# 定义 HDC 和 UINT 类型
HDC = wintypes.HDC
UINT = wintypes.UINT
# 定义 GetDC 和 GetDeviceCaps 的参数类型和返回类型
user32.GetDC.argtypes = [wintypes.HWND]
user32.GetDC.restype = HDC
gdi32.GetDeviceCaps.argtypes = [HDC, UINT]
gdi32.GetDeviceCaps.restype = wintypes.INT
# 获取设备上下文
dc = user32.GetDC(None)
widthScale = gdi32.GetDeviceCaps(dc, HORZRES)
width = gdi32.GetDeviceCaps(dc, LOGPIXELSX)
scale = width / widthScale
return scale
2、获取屏幕指定坐标处像素颜色:
import ctypes
from ctypes import wintypes
from typing import Sequence, Generator
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
# 定义类型
HWND = wintypes.HWND
HDC = wintypes.HDC
HBITMAP = wintypes.HBITMAP
class BITMAPINFOHEADER(ctypes.Structure):
_fields_ = [
("biSize", wintypes.DWORD),
("biWidth", wintypes.LONG),
("biHeight", wintypes.LONG),
("biPlanes", wintypes.WORD),
("biBitCount", wintypes.WORD),
("biCompression", wintypes.DWORD),
("biSizeImage", wintypes.DWORD),
("biXPelsPerMeter", wintypes.LONG),
("biYPelsPerMeter", wintypes.LONG),
("biClrUsed", wintypes.DWORD),
("biClrImportant", wintypes.DWORD)
]
class BITMAPINFO(ctypes.Structure):
_fields_ = [
("bmiHeader", BITMAPINFOHEADER),
("bmiColors", wintypes.DWORD * 3)
]
def get_pixel_color(coords: Sequence[tuple[int, int]], hwnd: HWND) -> Generator[tuple[int, int, int], None, None]:
rect = wintypes.RECT()
user32.GetClientRect(hwnd, ctypes.byref(rect))
width = rect.right - rect.left
height = rect.bottom - rect.top
# 创建内存设备上下文
hdc_src = user32.GetDC(hwnd)
hdc_dst = gdi32.CreateCompatibleDC(hdc_src)
bmp = gdi32.CreateCompatibleBitmap(hdc_src, width, height)
gdi32.SelectObject(hdc_dst, bmp)
# 使用 BitBlt 复制窗口内容到内存设备上下文
gdi32.BitBlt(hdc_dst, 0, 0, width, height, hdc_src, 0, 0, 0x00CC0020) # SRCCOPY
# 获取位图信息
bmi = BITMAPINFO()
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
bmi.bmiHeader.biWidth = width
bmi.bmiHeader.biHeight = -height # 负值表示自底向上
bmi.bmiHeader.biPlanes = 1
bmi.bmiHeader.biBitCount = 32
bmi.bmiHeader.biCompression = 0
# 创建缓冲区并获取位图数据
buffer = ctypes.create_string_buffer(width * height * 4)
gdi32.GetDIBits(hdc_dst, bmp, 0, height, buffer, ctypes.byref(bmi), 0)
# 释放资源
gdi32.DeleteObject(bmp)
gdi32.DeleteDC(hdc_dst)
user32.ReleaseDC(hwnd, hdc_src)
# 遍历指定坐标并返回像素颜色
for x, y in coords:
if 0 <= x < width and 0 <= y < height:
offset = (y * width + x) * 4
color = buffer[offset:offset + 4]
yield color[2], color[1], color[0] # BGR -> RGB
else:
yield (0, 0, 0)
3、一个简单的使用案例
from typing import Sequence, Generator, Tuple
from tkinter import ttk
import tkinter as tk
from ctypes import wintypes
import ctypes
import requests
from io import BytesIO
from PIL import Image, ImageTk
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
HWND = wintypes.HWND
HDC = wintypes.HDC
HBITMAP = wintypes.HBITMAP
class BITMAPINFOHEADER(ctypes.Structure):
_fields_ = [
("biSize", wintypes.DWORD),
("biWidth", wintypes.LONG),
("biHeight", wintypes.LONG),
("biPlanes", wintypes.WORD),
("biBitCount", wintypes.WORD),
("biCompression", wintypes.DWORD),
("biSizeImage", wintypes.DWORD),
("biXPelsPerMeter", wintypes.LONG),
("biYPelsPerMeter", wintypes.LONG),
("biClrUsed", wintypes.DWORD),
("biClrImportant", wintypes.DWORD)
]
class BITMAPINFO(ctypes.Structure):
_fields_ = [
("bmiHeader", BITMAPINFOHEADER),
("bmiColors", wintypes.DWORD * 3)
]
def get_pixel_color(coords: Sequence[Tuple[int, int]], hwnd: HWND) -> Generator[Tuple[int, int, int], None, None]:
rect = wintypes.RECT()
user32.GetClientRect(hwnd, ctypes.byref(rect))
width = rect.right - rect.left
height = rect.bottom - rect.top
hdc_src = user32.GetDC(hwnd)
hdc_dst = gdi32.CreateCompatibleDC(hdc_src)
bmp = gdi32.CreateCompatibleBitmap(hdc_src, width, height)
gdi32.SelectObject(hdc_dst, bmp)
gdi32.BitBlt(hdc_dst, 0, 0, width, height, hdc_src, 0, 0, 0x00CC0020) # SRCCOPY
bmi = BITMAPINFO()
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
bmi.bmiHeader.biWidth = width
bmi.bmiHeader.biHeight = -height # 负值表示自底向上
bmi.bmiHeader.biPlanes = 1
bmi.bmiHeader.biBitCount = 32
bmi.bmiHeader.biCompression = 0
buffer = ctypes.create_string_buffer(width * height * 4)
gdi32.GetDIBits(hdc_dst, bmp, 0, height, buffer, ctypes.byref(bmi), 0)
gdi32.DeleteObject(bmp)
gdi32.DeleteDC(hdc_dst)
user32.ReleaseDC(hwnd, hdc_src)
for x, y in coords:
print(x, y, width, height)
if 0 <= x < width and 0 <= y < height:
offset = (y * width + x) * 4
color = buffer[offset:offset + 4]
yield color[2], color[1], color[0] # BGR -> RGB
else:
yield (0, 0, 0)
def get_window_handle(window):
window_name = window._w
if not window_name.startswith("."):
window_name = "." + window_name
hwnd = ctypes.windll.user32.FindWindowW(None, window.title())
if not hwnd:
raise ValueError("Cannot get the window handle.")
return hwnd
def download_image(url):
response = requests.get(url)
if response.status_code == 200:
return Image.open(BytesIO(response.content))
else:
raise Exception(f"Failed to download image: HTTP {response.status_code}")
def display_image_in_label(image):
photo = ImageTk.PhotoImage(image)
label = ttk.Label(root, image=photo)
label.image = photo # 保持对 PhotoImage 的引用,防止被垃圾回收
label.pack()
def show_color(event):
hwnd = get_window_handle(root)
x, y = event.x, event.y
# 注意这里的坐标是相对于窗口的坐标,且传入get_pixel_color的应该是包含多个坐标点的序列
# 此外,为了高效获取同一个画面多个点的颜色,此处我使用了生成器进行懒加载,因此获取数据时请完整遍历迭代器
result = get_pixel_color([(x, y)], hwnd)
colors = [i for i in result]
print(f"{event.x, event.y}: {colors}")
if __name__ == "__main__":
root = tk.Tk()
width, height = 900, 500
screenwidth = root.winfo_screenwidth()
screenheight = root.winfo_screenheight()
geometry = '%dx%d+%d+%d' % (width, height, (screenwidth - width) / 2, (screenheight - height) / 2)
root.title("测试样例")
root.geometry(geometry)
root.bind("<Motion>", show_color)
image_url = "https://ts1.cn.mm.bing.net/th/id/R-C.475631ce281b88c3cd465761b37c5256?rik=ZFMiTYFwaPypTQ&riu=http%3a%2f%2fpic.ntimg.cn%2ffile%2f20180102%2f21532952_215949247000_2.jpg&ehk=9NnCJ9JG44zfdF2%2fr373s25s68H9vxLvyfMsKgEzAwc%3d&risl=&pid=ImgRaw&r=0"
try:
img = download_image(image_url)
display_image_in_label(img)
except Exception as e:
print(f"Error: {e}")
ttk.Label(root, text="Failed to load image.").pack()
root.mainloop()
4、总结
上述方法比通常使用PIL的Image.ImageGrab方法要高效非常多,因为Image.ImageGrab是基于IO截屏操作的,频繁的IO操作使单纯进行屏幕像素访问十分低效。
而上述方法采用的是BitBlt。BitBlt
是一种高效的位图操作方法,可以将窗口的内容复制到内存设备上下文中,然后通过 GetPixel
或直接访问位图数据来获取像素颜色。就像素访问而言其性能显著强于前者。更多关于Window的API操作详见官方文档:
Windows GDI) (位图函数 - Win32 apps | Microsoft Learn