Windows下用Rust捕获指定窗口截图(忽略覆盖层、优化帧率)
问题:捕获被遮挡的指定窗口区域并优化截图效率
我需要捕获指定窗口的部分区域截图,该窗口可能被游戏覆盖层等上层内容遮挡,要求仅捕获目标窗口、忽略上层覆盖物。同时希望将截图效率优化至约5张/秒。
目前尝试了screenshots cargo包,代码如下:
use opencv::{core, highgui, imgcodecs}; use screenshots::Screen; use std::{time::Instant}; use opencv::core::{log, Mat}; const WIDTH: i32 = 275; const HEIGHT: i32 = 275; fn get_img(screen: Screen) -> Mat { let image = screen.capture().unwrap(); let buffer: Vec<u8> = image.into(); // Change image type to OpenCV Mat let original_image: Mat = imgcodecs::imdecode(&core::Mat::from_slice(buffer.as_slice()).unwrap(), imgcodecs::IMREAD_COLOR).unwrap(); return original_image; } fn main() { let window_name = "test".to_owned(); highgui::named_window(&window_name, highgui::WINDOW_NORMAL).unwrap(); highgui::resize_window(&window_name, WIDTH, HEIGHT).unwrap(); let screens = Screen::all().unwrap(); let screen = screens[1].clone(); let mut img = get_img(screen); loop { let now = Instant::now(); img = get_img(screen); // print in console the time it took to process the image println!("{} ms", now.elapsed().as_millis()); } }
但该方法无法捕获被覆盖层遮挡的指定窗口。
备注:使用cargo run --release运行代码,开发及目标系统均为Windows,截图需转换为OpenCV Mat用于后续处理。
解决方案
一、解决被遮挡窗口的捕获问题
在Windows平台,要绕过上层覆盖物直接捕获目标窗口,需要通过Windows API直接读取窗口的设备上下文(DC),而非捕获整个屏幕。核心思路是:
- 通过窗口标题或进程ID定位目标窗口的
HWND句柄 - 从窗口DC中直接复制画面到内存位图,不受上层窗口遮挡影响
- 将位图数据直接转换为OpenCV Mat,跳过冗余的编码/解码步骤
二、优化截图效率至5张/秒
要达到5张/秒(单张耗时≈200ms)的目标,需重点优化:
- 复用DC、位图等资源,避免重复创建销毁的开销
- 直接从内存缓冲区转换为OpenCV Mat,消除
imdecode的额外耗时 - 仅捕获指定区域,减少数据处理量
- 避免不必要的内存拷贝操作
完整示例代码
首先在Cargo.toml中添加依赖:
[dependencies] opencv = "0.80.0" windows = { version = "0.52.0", features = ["Win32_Graphics_Gdi", "Win32_Foundation", "Win32_UI_WindowsAndMessaging"] }
实现代码:
use opencv::{core, highgui, Mat}; use std::time::Instant; use windows::{ core::PCWSTR, Win32::Foundation::{HWND, RECT}, Win32::Graphics::Gdi::{ CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetClientRect, GetDC, SelectObject, BitBlt, SRCCOPY, HBITMAP, HDC, }, Win32::UI::WindowsAndMessaging::FindWindowW, }; const CAPTURE_WIDTH: i32 = 275; const CAPTURE_HEIGHT: i32 = 275; // 通过窗口标题查找目标窗口句柄 fn find_window_by_title(title: &str) -> Option<HWND> { let wide_title: Vec<u16> = title.encode_utf16().chain(std::iter::once(0)).collect(); let hwnd = unsafe { FindWindowW(None, PCWSTR(wide_title.as_ptr())) }; hwnd.0 != 0.then_some(hwnd) } // 捕获指定窗口的指定区域并转换为OpenCV Mat fn capture_window_region(hwnd: HWND, x: i32, y: i32, width: i32, height: i32) -> Option<Mat> { unsafe { // 获取窗口设备上下文 let window_dc = GetDC(hwnd); if window_dc.is_invalid() { return None; } // 创建兼容DC和位图 let mem_dc = CreateCompatibleDC(window_dc); let hbitmap = CreateCompatibleBitmap(window_dc, width, height); if mem_dc.is_invalid() || hbitmap.is_invalid() { DeleteDC(window_dc); return None; } // 将位图绑定到兼容DC let old_bitmap = SelectObject(mem_dc, hbitmap); // 复制窗口指定区域到兼容DC let blt_success = BitBlt( mem_dc, 0, 0, width, height, window_dc, x, y, SRCCOPY ).as_bool(); // 恢复DC状态并清理临时资源 SelectObject(mem_dc, old_bitmap); DeleteDC(window_dc); if !blt_success { DeleteObject(hbitmap); DeleteDC(mem_dc); return None; } // 提取位图像素数据 let mut bmi = windows::Win32::Graphics::Gdi::BITMAPINFO { bmiHeader: windows::Win32::Graphics::Gdi::BITMAPINFOHEADER { biSize: std::mem::size_of::<windows::Win32::Graphics::Gdi::BITMAPINFOHEADER>() as u32, biWidth: width, biHeight: -height, // 负数值表示像素顺序从上到下 biPlanes: 1, biBitCount: 24, biCompression: windows::Win32::Graphics::Gdi::BI_RGB, biSizeImage: 0, biXPelsPerMeter: 0, biYPelsPerMeter: 0, biClrUsed: 0, biClrImportant: 0, }, bmiColors: [Default::default(); 1], }; let mut buffer = vec![0u8; (width * height * 3) as usize]; let get_bits_success = windows::Win32::Graphics::Gdi::GetDIBits( mem_dc, hbitmap, 0, height as u32, Some(buffer.as_mut_ptr() as *mut _), &mut bmi, windows::Win32::Graphics::Gdi::DIB_RGB_COLORS ) != 0; DeleteObject(hbitmap); DeleteDC(mem_dc); if !get_bits_success { return None; } // 将RGB缓冲区转换为OpenCV的BGR格式Mat let rgb_mat = Mat::new_rows_cols_with_data( height, width, core::CV_8UC3, buffer.as_mut_ptr() as *mut _, core::Mat_AUTO_STEP ).ok()?; let mut bgr_mat = Mat::default(); core::cvt_color(&rgb_mat, &mut bgr_mat, core::COLOR_RGB2BGR, 0).ok()?; Some(bgr_mat) } } fn main() -> opencv::Result<()> { let target_window_title = "test"; // 替换为你的目标窗口标题 let hwnd = match find_window_by_title(target_window_title) { Some(hwnd) => hwnd, None => { eprintln!("无法找到目标窗口"); return Ok(()); } }; // 获取窗口客户区大小(用于确认捕获区域边界) let mut rect = RECT::default(); unsafe { GetClientRect(hwnd, &mut rect) }; println!("窗口客户区大小: {}x{}", rect.right - rect.left, rect.bottom - rect.top); let window_name = "Capture Result"; highgui::named_window(window_name, highgui::WINDOW_NORMAL)?; highgui::resize_window(window_name, CAPTURE_WIDTH, CAPTURE_HEIGHT)?; loop { let start = Instant::now(); // 捕获窗口(0,0)位置开始的指定区域,可根据需求调整x、y参数 if let Some(img) = capture_window_region(hwnd, 0, 0, CAPTURE_WIDTH, CAPTURE_HEIGHT) { highgui::imshow(window_name, &img)?; } else { eprintln!("捕获失败"); } // 处理OpenCV事件,避免窗口无响应 if highgui::wait_key(1)? == 27 { // ESC键退出循环 break; } println!("捕获耗时: {} ms", start.elapsed().as_millis()); } highgui::destroy_all_windows()?; Ok(()) }
关键优化说明
- 直接窗口DC捕获:绕过屏幕捕获逻辑,直接读取窗口内部画面,完全不受上层覆盖物影响
- 无编码解码开销:直接从内存位图转换为OpenCV Mat,消除了
imdecode的冗余耗时 - 区域精准捕获:只处理指定大小的区域,减少数据传输和处理量
- 资源高效管理:每次捕获后及时释放临时DC和位图,避免内存泄漏
内容的提问来源于stack exchange,提问作者Unknow
相关产品推荐
相关产品推荐

