练枪的时候发现打的靶子特征很醒目,而且操控的逻辑也不是说特别难,刚好会一点点C++和OpenCV,为什么不试试写一个小程序来帮助我们瞄准呢?
实现效果
我们主要是通过这款游戏测试自瞄
简单的调参之后本周世界排名也是打到了第一名,下面是实现的简单思路和逻辑,可以跟着我一起实战一下
主要思路讲解
我们实际上实现自动瞄准的逻辑也很简单
识别目标,确定目标坐标,移动鼠标,开枪
实现这四步就可以很简单的实现自瞄,转化为代码,我们则需要做到下面两个步骤
目标识别检测鼠标位置控制下面我们分别来讲解实现这两个功能
目标识别
这里用的是OpenCV来实现检测,因为这一次的目标比较简单,大概都是这样的蓝色小圆球
其实这里面思路有很多,可以进行进行边缘检测,阈值监测什么的,但是毕竟是自瞄,我们要选择一个计算任务小的,这样子响应速度更快
如果是识别圆形轮廓,就分为了利用算子进行边缘检测,识别圆形轮廓,这样子不仅计算量大,在手枪出现挡住靶子的情况也没有办法及时识别(上图所示),于是我们果断抛弃这种思路。
ps:为了方便看效果,我简单的写一个小程序来显示图片,捕获屏幕和移动鼠标后面会讲到,这里我放一下测试的小程序
int main(){ Mat img = cv::imread("D:\desktop\\test.png"); while (true) { cv::imshow("Original Image", img); cv::waitKey(100); }}
还有一种思路,我们直接寻找蓝色,这样子也分为以下几个步骤
创建一个掩膜,只保留蓝色区域
Scalar lower_blue(82, 199, 118); Scalar upper_blue(97, 255, 255);
使用掩膜提取蓝色区域
Mat mask; inRange(hsv, lower_blue, upper_blue, mask);
查找轮廓
Canny(mask, edges, 80, 255);
接下来就是确定鼠标点击的点,在这里也是测试了多种情况,最好的办法是直接识别质心,这样子可以使得鼠标的容错会更大,如何去除误识别区域呢,这个通过优化算法可以达到很好的效果,但是会极大地增大延时,所以在前面蓝色区域识别以及很精确地调试过了,这里只需要规定目标轮廓体积达到阈值即可
我们写一下代码,首先是质心的寻找
// 存储找到的轮廓 vector<vector<Point>> contours; // 在经过边缘检测的图像 edges 中查找轮廓 // RETR_EXTERNAL 表示只提取最外层的轮廓 // CHAIN_APPROX_SIMPLE 表示对轮廓进行简化,只保留轮廓的端点 findContours(edges, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); // 初始化最近轮廓的中心为一个无效的点(-1,-1) Point closestContourCenter(-1, -1);
那接下来就是找到并画出圆心
for (const auto& contour : contours) { Moments contourMoments = moments(contour); if (contourMoments.m00!= 0.0) { // 根据矩计算轮廓的中心位置(质心) Point contourCenter(contourMoments.m10 / contourMoments.m00, contourMoments.m01 / contourMoments.m00); // 在图像上标记质心 circle(img, contourCenter, 5, Scalar(0, 0, 255), -1);cout << "质心坐标: (" << contourCenter.x << ", " << contourCenter.y << ")" << endl; } }
这样子我们就成功找到了需要射击的点
屏幕捕获
我们需要引用一个系统库
#include <windows.h>
以及这个函数
Mat captureScreen(){ // 获取显示器的大小 int screenWidth = GetSystemMetrics(SM_CXSCREEN); int screenHeight = GetSystemMetrics(SM_CYSCREEN); // 创建设备描述表 HDC hSrcDC = GetDC(0); // 获取整个桌面的设备上下文 HDC hMemDC = CreateCompatibleDC(hSrcDC); // 创建与桌面兼容的设备上下文 // 创建位图对象,并准备将其存储在内存中 HBITMAP hBitmap = CreateCompatibleBitmap(hSrcDC, screenWidth, screenHeight); HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemDC, hBitmap); // 将屏幕复制到位图中 BitBlt(hMemDC, 0, 0, screenWidth, screenHeight, hSrcDC, 0, 0, SRCCOPY); // 从位图中获取像素数据 Mat screen(screenHeight, screenWidth, CV_8UC4); BYTE* data = (BYTE*)screen.data; GetBitmapBits(hBitmap, screenWidth * screenHeight * 4, data); // 清理 SelectObject(hMemDC, hOldBitmap); DeleteObject(hBitmap); DeleteDC(hMemDC); ReleaseDC(0, hSrcDC); return screen;}
然后两行代码就可以实现了
int main(){ namedWindow("screen capture", WINDOW_NORMAL); while (true) { Mat screen = captureScreen(); // 显示捕获的屏幕图像 imshow("screen capture", screen); waitKey(100); } return 0;}
打靶子!
其实打靶子就是移动鼠标然后点击
我们需要一些函数来移动鼠标和点击,这里同样还是windows.h库
大家直接用就好
POINT GetMouseCurPoint(){ POINT mypoint; GetCursorPos(&mypoint);//获取鼠标当前所在位置 return mypoint;}void MouseLeftDown()//鼠标左键按下 { INPUT Input = { 0 }; Input.type = INPUT_MOUSE; Input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN; SendInput(1, &Input, sizeof(INPUT));}void MouseLeftUp()//鼠标左键放开 { INPUT Input = { 0 }; Input.type = INPUT_MOUSE; Input.mi.dwFlags = MOUSEEVENTF_LEFTUP; SendInput(1, &Input, sizeof(INPUT));}void MouseMove(int x, int y)//鼠标移动到指定位置 { double fScreenWidth = ::GetSystemMetrics(SM_CXSCREEN) - 1;//获取屏幕分辨率宽度 double fScreenHeight = ::GetSystemMetrics(SM_CYSCREEN) - 1;//获取屏幕分辨率高度 double fx = x * (65535.0f / fScreenWidth); double fy = y * (65535.0f / fScreenHeight); //printf("fScreenWidth %lf , fScreenHeight %lf, fx %lf, fy %lf \n", fScreenWidth, fScreenHeight, fx, fy); INPUT Input = { 0 }; Input.type = INPUT_MOUSE; Input.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE; Input.mi.dx = fx; Input.mi.dy = fy; SendInput(1, &Input, sizeof(INPUT));}
有了这些库就能很轻松的操控鼠标点击
但是常玩fps的大家都知道,准星移动是滑动的,起始点都是屏幕中心,目标点是靶子的位置
所以我也是写了一个滑动鼠标的函数,这里我们需要用到PID来控制鼠标轨迹
从网上随便找一份PID类
class PIDController {public: PIDController(double Kp, double Ki, double Kd) : Kp(Kp), Ki(Ki), Kd(Kd), lastError(0), integral(0) {} // Update the position based on error between current and target void updatePosition(POINT& current, const POINT& target, double dt) { int errorX = target.x - current.x; int errorY = target.y - current.y; // Proportional term double pTermX = Kp * errorX; double pTermY = Kp * errorY; // Integral term (not used in this simple example) integral += errorX + errorY; // This is a simplification double iTermX = Ki * integral * dt; double iTermY = Ki * integral * dt; // Derivative term (not used in this simple example) double dTermX = Kd * (errorX - lastError); double dTermY = Kd * (errorY - lastError); // Update position current.x += pTermX; // + iTermX + dTermX; current.y += pTermY; // + iTermY + dTermY; // Save error for next derivative calculation lastError = errorX + errorY; }private: double Kp, Ki, Kd; double lastError; double integral;};
然后就是无聊的调参了,这里已经帮大家调教好了
void moveFromTo(POINT start, POINT target, double dt) { PIDController pid(0.1, 0, 0); // Kp, Ki, Kd values POINT current = start; while (abs(current.x - target.x) > 10 || abs(current.y - target.y) > 10) { pid.updatePosition(current, target, dt); std::cout << "Current Position: (" << current.x << ", " << current.y << ")" << std::endl; MouseMove(current.x, current.y); printf("1\n"); }}
这里有个小细节,因为是PID控制的鼠标,有些时候可能会震荡时间过长,我的解决方法就是扩大目标范围,在范围内直接点击就好
那么综上所述,我们就只剩最后一件事情了
设计打靶顺序可以很好的提高效率,我随便写了一份最简单的,大家要是有好想法可以在评论区分享
我的想法是找到离屏幕中心点距离最近的点优先考虑,这样子的坏处是计算量比较大,可能耗时很多,最终也是只能打到十七万分
// 获取图像的宽度 int width = screen.cols; // 获取图像的高度 int height = screen.rows; // 初始化最小距离为最大的双精度浮点数 double mindistance = DBL_MAX; // 初始化最近轮廓的中心为一个无效的点(-1,-1) Point closestContourCenter(-1, -1); // 遍历找到的所有轮廓 for (const auto& contour : contours) { // 计算当前轮廓的矩 Moments contourMoments = moments(contour); // 检查矩中的零阶矩是否为非零值,如果为零则说明轮廓不存在或无效 if (contourMoments.m00!= 0.0) { // 根据矩计算轮廓的中心位置 // m10/m00 为 x 坐标,m01/m00 为 y 坐标 Point contourCenter(contourMoments.m10 / contourMoments.m00, contourMoments.m01 / contourMoments.m00); // 计算当前轮廓中心与图像中心(假设图像中心为(960,540))的水平距离差 double dx = 960 - contourCenter.x; // 计算当前轮廓中心与图像中心(假设图像中心为(960,540))的垂直距离差 double dy = 540 - contourCenter.y; // 计算当前轮廓中心与图像中心的欧氏距离 double distance = sqrt(dx * dx + dy * dy); // 如果当前轮廓中心与图像中心的距离小于之前找到的最小距离 if (distance < mindistance) { // 更新最小距离 mindistance = distance; // 更新最近轮廓中心 closestContourCenter = contourCenter; } } }
最终整合
到此为止以及完成了所有的准备工作,我们可以识别到目标,可以移动可以点击
但是还是有一个问题就是程序退出的问题,如果程序一直运行将一直拉扯你的鼠标让你无法点击只能重启(别问我是怎么知道的)
这里有一个好办法就是使用时间库来控制程序执行时间,打靶一局一分钟,可以把程序设置成一分五秒自动结束
// 记录开始时间auto starttime = std::chrono::steady_clock::now();// 设置结束时间为开始时间加上 4 分钟auto endtime = starttime + std::chrono::minutes(1) + std::chrono::seconds(5);// 获取当前时间auto currenttime = std::chrono::steady_clock::now();// 如果当前时间大于等于结束时间if (currenttime >= endtime){ // 跳出循环 break;}
记得引用
#include <chrono>
好的那么接下来就是开始实战
我们需要先打开游戏,打开游戏的窗口模式,监测窗口小图标的位置,在程序一开始的时候先点击游戏最小化的图标,然后点击继续游戏,然后开始打靶子
那屏幕上的位置需要大家用提到的函数来自行测试
// 打印最近的轮廓中心 if (closestContourCenter.x!= -1 && closestContourCenter.y!= -1) { std::cout << "closest contour center: (" << closestContourCenter.x << ", " << closestContourCenter.y << ")" << std::endl; start = GetMouseCurPoint(); POINT aid = { closestContourCenter.x, closestContourCenter.y }; // 从一个点移动到另一个点 moveFromTo(target4, aid, 0.001); MouseLeftDown(); MouseLeftUp(); }
这一步是打靶子的逻辑,大家可以试试看,0.001是调好的dt,不用再管他
完整代码
main.cpp(前面需要小小改动)
#define _CRT_SECURE_NO_WARNINGS 1#include "sa.h"using namespace cv;int main(){ // 获取鼠标当前位置并输出 POINT mousePos = GetMouseCurPoint(); std::cout << "mouse position: " << mousePos.x << "," << mousePos.y << std::endl; // 记录起始位置 POINT start = GetMouseCurPoint(); POINT target = { 0, 0 }; // 模拟鼠标左键按下和抬起 MouseLeftDown(); MouseLeftUp(); POINT target1 = { 178, 345 }; POINT target2 = { 1780, 50 }; POINT target4 = { 960, 531 }; std::cout << "起始位置: (" << start.x << ", " << start.y << ")" << std::endl; // 从起始位置移动到目标位置 // 这两次点击一次是打开游戏,一次是点击继续游戏(根据自己的屏幕来修改) moveFromTo(start, target2, 0.05); MouseLeftDown(); MouseLeftUp(); start = GetMouseCurPoint(); moveFromTo(start, target1, 0.05); MouseLeftDown(); MouseLeftUp(); Sleep(1000); MouseLeftDown(); MouseLeftUp(); // 初始化一个窗口,窗口大小可调整 namedWindow("screen capture", WINDOW_NORMAL); // 记录开始时间 auto starttime = steady_clock::now(); // 设置结束时间为开始时间加上 4 分钟 auto endtime = starttime + minutes(4); while (true) { // 捕获屏幕 Mat screen = captureScreen(); // 转换为 BGR 格式以便后续处理 cvtColor(screen, screen, COLOR_BGRA2BGR); // 将 BGR 颜色空间转换为 HSV 颜色空间 Mat hsv; cvtColor(screen, hsv, COLOR_BGR2HSV); // 定义蓝色在 HSV 空间的下界和上界 Scalar lower_blue(82, 199, 118); Scalar upper_blue(97, 255, 255); // 创建一个掩膜,只保留蓝色区域 Mat mask; inRange(hsv, lower_blue, upper_blue, mask); // 使用掩膜提取蓝色区域,对掩膜进行边缘检测 Mat edges; Canny(mask, edges, 80, 255); // 显示边缘检测结果 imshow("edges", edges); // 查找轮廓 vector<vector<Point>> contours; findContours(edges, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE); int width = screen.cols; int height = screen.rows; double mindistance = DBL_MAX; Point closestContourCenter(-1, -1); for (const auto& contour : contours) { // 计算轮廓的矩 Moments contourMoments = moments(contour); if (contourMoments.m00 != 0.0) { // 根据矩计算轮廓中心 Point contourCenter(contourMoments.m10 / contourMoments.m00, contourMoments.m01 / contourMoments.m00); // 手动计算两点之间的距离 double dx = 960 - contourCenter.x; double dy = 540 - contourCenter.y; double distance = sqrt(dx * dx + dy * dy); if (distance < mindistance) { // 更新最小距离和最近轮廓中心 mindistance = distance; closestContourCenter = contourCenter; } } } // 创建一个图像用于绘制轮廓和最近的轮廓中心 Mat drawing = Mat::zeros(screen.size(), screen.type()); // 在图像上绘制轮廓 drawContours(drawing, contours, -1, Scalar(0, 255, 0), 2, LINE_8, vector<Vec4i>(), 0, Point()); if (closestContourCenter.x != -1 && closestContourCenter.y != -1) { // 在图像上绘制最近的轮廓中心和图像中心 circle(drawing, closestContourCenter, 5, Scalar(0, 0, 255), -1); circle(drawing, Point(960, 540), 5, Scalar(255, 0, 0), -1); } // 显示捕获到的屏幕图像 //imshow("screen capture", drawing); // 打印最近的轮廓中心 if (closestContourCenter.x != -1 && closestContourCenter.y != -1) { std::cout << "closest contour center: (" << closestContourCenter.x << ", " << closestContourCenter.y << ")" << std::endl; start = GetMouseCurPoint(); POINT aid = { closestContourCenter.x, closestContourCenter.y }; // 从一个点移动到另一个点 moveFromTo(target4, aid, 0.001); MouseLeftDown(); MouseLeftUp(); } // 检查是否有按键按下 if (waitKey(1) >= 0) break; MouseLeftDown(); MouseLeftUp(); Sleep(100); // 获取当前时间 auto currenttime = steady_clock::now(); if (currenttime >= endtime) { break; } } // 释放资源 destroyAllWindows(); return 0;}
函数文件demo.cpp
#define _CRT_SECURE_NO_WARNINGS 1#include "sa.h"POINT GetMouseCurPoint(){ POINT mypoint; GetCursorPos(&mypoint);//获取鼠标当前所在位置 return mypoint;}void MouseMove(int x, int y)//鼠标移动到指定位置 { double fScreenWidth = ::GetSystemMetrics(SM_CXSCREEN) - 1;//获取屏幕分辨率宽度 double fScreenHeight = ::GetSystemMetrics(SM_CYSCREEN) - 1;//获取屏幕分辨率高度 double fx = x * (65535.0f / fScreenWidth); double fy = y * (65535.0f / fScreenHeight); //printf("fScreenWidth %lf , fScreenHeight %lf, fx %lf, fy %lf \n", fScreenWidth, fScreenHeight, fx, fy); INPUT Input = { 0 }; Input.type = INPUT_MOUSE; Input.mi.dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE; Input.mi.dx = fx; Input.mi.dy = fy; SendInput(1, &Input, sizeof(INPUT));}void moveFromTo(POINT start, POINT target, double dt) { PIDController pid(0.1, 0, 0); // Kp, Ki, Kd values POINT current = start; while (abs(current.x - target.x) > 10 || abs(current.y - target.y) > 10) { pid.updatePosition(current, target, dt); std::cout << "Current Position: (" << current.x << ", " << current.y << ")" << std::endl; MouseMove(current.x, current.y); printf("1\n"); }}void MouseLeftDown()//鼠标左键按下 { INPUT Input = { 0 }; Input.type = INPUT_MOUSE; Input.mi.dwFlags = MOUSEEVENTF_LEFTDOWN; SendInput(1, &Input, sizeof(INPUT));}void MouseLeftUp()//鼠标左键放开 { INPUT Input = { 0 }; Input.type = INPUT_MOUSE; Input.mi.dwFlags = MOUSEEVENTF_LEFTUP; SendInput(1, &Input, sizeof(INPUT));}void MouseRightDown()//鼠标右键按下 { INPUT Input = { 0 }; Input.type = INPUT_MOUSE; Input.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN; SendInput(1, &Input, sizeof(INPUT));}void MouseRightUp()//鼠标右键放开 { INPUT Input = { 0 }; Input.type = INPUT_MOUSE; Input.mi.dwFlags = MOUSEEVENTF_RIGHTUP; SendInput(1, &Input, sizeof(INPUT));}Mat captureScreen(){ // 获取显示器的大小 int screenWidth = GetSystemMetrics(SM_CXSCREEN); int screenHeight = GetSystemMetrics(SM_CYSCREEN); // 创建设备描述表 HDC hSrcDC = GetDC(0); // 获取整个桌面的设备上下文 HDC hMemDC = CreateCompatibleDC(hSrcDC); // 创建与桌面兼容的设备上下文 // 创建位图对象,并准备将其存储在内存中 HBITMAP hBitmap = CreateCompatibleBitmap(hSrcDC, screenWidth, screenHeight); HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemDC, hBitmap); // 将屏幕复制到位图中 BitBlt(hMemDC, 0, 0, screenWidth, screenHeight, hSrcDC, 0, 0, SRCCOPY); // 从位图中获取像素数据 Mat screen(screenHeight, screenWidth, CV_8UC4); BYTE* data = (BYTE*)screen.data; GetBitmapBits(hBitmap, screenWidth * screenHeight * 4, data); // 清理 SelectObject(hMemDC, hOldBitmap); DeleteObject(hBitmap); DeleteDC(hMemDC); ReleaseDC(0, hSrcDC); return screen;}
库文件sa.h
#pragma once#include <iostream>#include <windows.h>#include <cmath>#include <opencv2/opencv.hpp>#include <chrono>using namespace std;using namespace cv;using namespace std::chrono; // 使用chrono命名空间POINT GetMouseCurPoint();void MouseMove(int x, int y);//鼠标移动到指定位置 void moveFromTo(POINT start, POINT target, double dt);void MouseLeftDown();void MouseLeftUp();//鼠标左键放开 void MouseRightDown();//鼠标右键按下void MouseRightUp();//鼠标右键放开Mat captureScreen();class PIDController {public: PIDController(double Kp, double Ki, double Kd) : Kp(Kp), Ki(Ki), Kd(Kd), lastError(0), integral(0) {} // Update the position based on error between current and target void updatePosition(POINT& current, const POINT& target, double dt) { int errorX = target.x - current.x; int errorY = target.y - current.y; // Proportional term double pTermX = Kp * errorX; double pTermY = Kp * errorY; // Integral term (not used in this simple example) integral += errorX + errorY; // This is a simplification double iTermX = Ki * integral * dt; double iTermY = Ki * integral * dt; // Derivative term (not used in this simple example) double dTermX = Kd * (errorX - lastError); double dTermY = Kd * (errorY - lastError); // Update position current.x += pTermX; // + iTermX + dTermX; current.y += pTermY; // + iTermY + dTermY; // Save error for next derivative calculation lastError = errorX + errorY; }private: double Kp, Ki, Kd; double lastError; double integral;};
程序是突发奇想的产物,还有很多的不足,大家如果有更好的建议可以在评论区指出