练枪的时候发现打的靶子特征很醒目,而且操控的逻辑也不是说特别难,刚好会一点点C++和OpenCV,为什么不试试写一个小程序来帮助我们瞄准呢?
实现效果
我们主要是通过这款游戏测试自瞄
简单的调参之后本周世界排名也是打到了第一名,下面是实现的简单思路和逻辑,可以跟着我一起实战一下
主要思路讲解
我们实际上实现自动瞄准的逻辑也很简单
识别目标,确定目标坐标,移动鼠标,开枪
实现这四步就可以很简单的实现自瞄,转化为代码,我们则需要做到下面两个步骤
- 目标识别检测
- 鼠标位置控制
下面我们分别来讲解实现这两个功能
目标识别
这里用的是OpenCV来实现检测,因为这一次的目标比较简单,大概都是这样的蓝色小圆球
其实这里面思路有很多,可以进行进行边缘检测,阈值监测什么的,但是毕竟是自瞄,我们要选择一个计算任务小的,这样子响应速度更快
如果是识别圆形轮廓,就分为了利用算子进行边缘检测,识别圆形轮廓,这样子不仅计算量大,在手枪出现挡住靶子的情况也没有办法及时识别(上图所示),于是我们果断抛弃这种思路。
ps:为了方便看效果,我简单的写一个小程序来显示图片,捕获屏幕和移动鼠标后面会讲到,这里我放一下测试的小程序
cint main() { Mat img = cv::imread("D:\desktop\\test.png"); while (true) { cv::imshow("Original Image", img); cv::waitKey(100); } }
还有一种思路,我们直接寻找蓝色,这样子也分为以下几个步骤
-
创建一个掩膜,只保留蓝色区域
c++Scalar lower_blue(82, 199, 118); Scalar upper_blue(97, 255, 255);
-
使用掩膜提取蓝色区域
c++Mat mask; inRange(hsv, lower_blue, upper_blue, mask);
-
查找轮廓
c++Canny(mask, edges, 80, 255);
接下来就是确定鼠标点击的点,在这里也是测试了多种情况,最好的办法是直接识别质心,这样子可以使得鼠标的容错会更大,如何去除误识别区域呢,这个通过优化算法可以达到很好的效果,但是会极大地增大延时,所以在前面蓝色区域识别以及很精确地调试过了,这里只需要规定目标轮廓体积达到阈值即可
我们写一下代码,首先是质心的寻找
c++
// 存储找到的轮廓
vector<vector<Point>> contours;
// 在经过边缘检测的图像 edges 中查找轮廓
// RETR_EXTERNAL 表示只提取最外层的轮廓
// CHAIN_APPROX_SIMPLE 表示对轮廓进行简化,只保留轮廓的端点
findContours(edges, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
// 初始化最近轮廓的中心为一个无效的点(-1,-1)
Point closestContourCenter(-1, -1);
那接下来就是找到并画出圆心
c++
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;
}
}
这样子我们就成功找到了需要射击的点
屏幕捕获
我们需要引用一个系统库
c++
#include <windows.h>
以及这个函数
c++
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;
}
然后两行代码就可以实现了
c
int main()
{
namedWindow("screen capture", WINDOW_NORMAL);
while (true)
{
Mat screen = captureScreen();
// 显示捕获的屏幕图像
imshow("screen capture", screen);
waitKey(100);
}
return 0;
}
打靶子!
其实打靶子就是移动鼠标然后点击
我们需要一些函数来移动鼠标和点击,这里同样还是windows.h库
大家直接用就好
c++
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类
c
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;
};
然后就是无聊的调参了,这里已经帮大家调教好了
c
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控制的鼠标,有些时候可能会震荡时间过长,我的解决方法就是扩大目标范围,在范围内直接点击就好
那么综上所述,我们就只剩最后一件事情了
设计打靶顺序可以很好的提高效率,我随便写了一份最简单的,大家要是有好想法可以在评论区分享
我的想法是找到离屏幕中心点距离最近的点优先考虑,这样子的坏处是计算量比较大,可能耗时很多,最终也是只能打到十七万分
c
// 获取图像的宽度
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;
}
}
}
最终整合
到此为止以及完成了所有的准备工作,我们可以识别到目标,可以移动可以点击
但是还是有一个问题就是程序退出的问题,如果程序一直运行将一直拉扯你的鼠标让你无法点击只能重启(别问我是怎么知道的)
这里有一个好办法就是使用时间库来控制程序执行时间,打靶一局一分钟,可以把程序设置成一分五秒自动结束
c
// 记录开始时间
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;
}
记得引用
c++
#include <chrono>
好的那么接下来就是开始实战
我们需要先打开游戏,打开游戏的窗口模式,监测窗口小图标的位置,在程序一开始的时候先点击游戏最小化的图标,然后点击继续游戏,然后开始打靶子
那屏幕上的位置需要大家用提到的函数来自行测试
c++
// 打印最近的轮廓中心
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(前面需要小小改动)
c++
#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
c++
#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
c
#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;
};
程序是突发奇想的产物,还有很多的不足,大家如果有更好的建议可以在评论区指出