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

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

实现效果

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

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


主要思路讲解

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

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

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

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

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


目标识别

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

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

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

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

c 复制代码
int 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;
};

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

谢谢大家的观看!!!

相关推荐
Ddddddd_1583 小时前
C++ | Leetcode C++题解之第504题七进制数
c++·leetcode·题解
J_z_Yang3 小时前
LeetCode 202 - 快乐数
c++·算法·leetcode
Y.O.U..6 小时前
STL学习-容器适配器
开发语言·c++·学习·stl·1024程序员节
lihao lihao6 小时前
C++stack和queue的模拟实现
开发语言·c++
爱写代码的小朋友6 小时前
使用 OpenCV 进行人脸检测
人工智能·opencv·计算机视觉
姆路7 小时前
QT中使用图表之QChart概述
c++·qt
西几7 小时前
代码训练营 day48|LeetCode 300,LeetCode 674,LeetCode 718
c++·算法·leetcode
风清扬_jd7 小时前
Chromium HTML5 新的 Input 类型week对应c++
前端·c++·html5
南东山人8 小时前
C++静态成员变量需要在类外进行定义和初始化-error LNK2001:无法解析的外部符号
c++
lqqjuly8 小时前
C++ 中回调函数的实现方式-函数指针
开发语言·c++