当前位置:首页 » 《我的小黑屋》 » 正文

C++ OpenCV实现简单的自瞄脚本(OpenCV实战)

19 人参与  2024年11月15日 10:02  分类 : 《我的小黑屋》  评论

点击全文阅读


练枪的时候发现打的靶子特征很醒目,而且操控的逻辑也不是说特别难,刚好会一点点C++和OpenCV,为什么不试试写一个小程序来帮助我们瞄准呢?

实现效果

FabConvert.com_8c0d30f1f7092a48e2fa05163023e292

我们主要是通过这款游戏测试自瞄

image-20240912163608365

简单的调参之后本周世界排名也是打到了第一名,下面是实现的简单思路和逻辑,可以跟着我一起实战一下

dfd81ce633476a03a9abb4c7394a03c

主要思路讲解

我们实际上实现自动瞄准的逻辑也很简单

识别目标,确定目标坐标,移动鼠标,开枪

实现这四步就可以很简单的实现自瞄,转化为代码,我们则需要做到下面两个步骤

目标识别检测鼠标位置控制

下面我们分别来讲解实现这两个功能


目标识别

这里用的是OpenCV来实现检测,因为这一次的目标比较简单,大概都是这样的蓝色小圆球

image-20240912171122227

其实这里面思路有很多,可以进行进行边缘检测,阈值监测什么的,但是毕竟是自瞄,我们要选择一个计算任务小的,这样子响应速度更快

如果是识别圆形轮廓,就分为了利用算子进行边缘检测,识别圆形轮廓,这样子不仅计算量大,在手枪出现挡住靶子的情况也没有办法及时识别(上图所示),于是我们果断抛弃这种思路。

ps:为了方便看效果,我简单的写一个小程序来显示图片,捕获屏幕和移动鼠标后面会讲到,这里我放一下测试的小程序

int main(){ Mat img = cv::imread("D:\desktop\\test.png"); while (true) {     cv::imshow("Original Image", img);     cv::waitKey(100); }}

image-20241017091712047

还有一种思路,我们直接寻找蓝色,这样子也分为以下几个步骤

创建一个掩膜,只保留蓝色区域

 Scalar lower_blue(82, 199, 118); Scalar upper_blue(97, 255, 255);

使用掩膜提取蓝色区域

 Mat mask; inRange(hsv, lower_blue, upper_blue, mask);

image-20241017093230428

查找轮廓

Canny(mask, edges, 80, 255);

image-20241017093246285

接下来就是确定鼠标点击的点,在这里也是测试了多种情况,最好的办法是直接识别质心,这样子可以使得鼠标的容错会更大,如何去除误识别区域呢,这个通过优化算法可以达到很好的效果,但是会极大地增大延时,所以在前面蓝色区域识别以及很精确地调试过了,这里只需要规定目标轮廓体积达到阈值即可

我们写一下代码,首先是质心的寻找

// 存储找到的轮廓    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;        }    }

image-20241017101901658

这样子我们就成功找到了需要射击的点


屏幕捕获

我们需要引用一个系统库

#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;}

image-20241017102422698


打靶子!

其实打靶子就是移动鼠标然后点击

我们需要一些函数来移动鼠标和点击,这里同样还是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;};

程序是突发奇想的产物,还有很多的不足,大家如果有更好的建议可以在评论区指出

谢谢大家的观看!!!


点击全文阅读


本文链接:http://zhangshiyu.com/post/186405.html

<< 上一篇 下一篇 >>

  • 评论(0)
  • 赞助本站

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

关于我们 | 我要投稿 | 免责申明

Copyright © 2020-2022 ZhangShiYu.com Rights Reserved.豫ICP备2022013469号-1