博文目录

文章目录

  • 效果展示
  • 环境准备 YOLO V5 6.2
    • PyTorch
  • 第一阶段 使用自带模型 实现实时目标检测
    • 屏幕截图
      • GDI MSS 和 Win32
      • DXGI D3DShot
      • 不同场景横评 含转换为 opencv 可用格式(BGR)的耗时
    • 实时屏幕检测
  • 第二阶段 训练模型 了解训练模型的方式方法 并能使用训练出来的模型
    • labelimg
    • classes.txt 与 标记文件说明
    • 简单尝试
      • 编写数据集配置文件
      • 编写训练文件参数
      • 运行训练文件
      • 运行结果
      • 测试训练结果
    • 如何取得最好的训练效果
  • 第三阶段 训练 Apex 射击场 假人 模型
    • 训练规划
    • 训练假人
      • 数据集图片获取
      • 标记
      • 参数
      • 训练
      • 测试
    • 粗浅的优化方案
    • 环境层面的优化
      • 优化过程 部署 TensorRT 推理加速
        • 安装 Python 环境的 TensorRT
        • 模型转换
        • 模型测试
    • 代码层面的优化
      • 更精准的鼠标移动距离计算
        • 鼠标灵敏度, ADS鼠标灵敏度加成, FOV视角, 位移像素之间的关系
        • 如何求 鼠标从中心跳到敌人位置对应的鼠标物理水平移动像素
        • 如何测 游戏内水平旋转 360° 对应鼠标水平移动的像素
        • 游戏内 鼠标灵敏度 与 鼠标移动量的关系
        • 关于 ADS 鼠标灵敏度加成
      • 更精准的控制鼠标设计时的稳定性
        • 鼠标移动过程分析
        • 如何消减鼠标震荡
      • 卡尔曼滤波器预测目标轨迹
        • 如何验证预测效果
    • 模型层面的优化
    • 瞄准效果
      • 目前欠缺部分
  • 第四阶段 实战
  • 工程源码
    • 相关资源
    • grab.for.apex.dummy.py
    • toolkit.py
    • test.detect.show.realtime.py
    • test.measure.palstance.py
    • aimbot.for.apex.dummy.py
    • label.for.apex.py
  • Yolov5 5.0 环境 (失败, 但先留着)

效果展示

效果展示 Python YOLO V5 实时截屏与目标检测

环境准备 YOLO V5 6.2

Python Windows 开发环境搭建

YOLO V5 官网
YOLO V5 6.2 下载

目标检测 YOLOv5 开源代码项目调试与讲解实战【土堆 x 布尔艺数】

本来是打算照着B站教程从Yolov5-5.0开始的, 依赖安装好后, 在运行的时候有一堆报错, 解决一个又出一个, 网上也没有什么好的解决办法, 索性直接上当时的最新版Yolov5-6.2了, 依赖安装好后, 直接就能跑起来(cpu模式). 之前5.0的流程也留着没删

先用 conda 创建好虚拟环境, 然后下载最新版 yolov5 6.2 源码, 解压到 pycharm workspace, 用 pycharm 打开, 选择之前创建的虚拟环境

# 创建虚拟环境, 建议不要创建在项目路径下, 包括数据集也是不要放在项目路径下, 不然pycharm可能会去读这些内容, 可能会很费时间
# PyTorch当时最高支持Python3.7-3.9, Conda当时最高支持3.9, 当时base环境的Python是3.9.12, 自动下载的Python是3.9.13
# 创建虚拟环境时要指定Python版本, 且不同于base环境中的Python版本, 不然不会真的创建虚拟环境, 且会污染base环境, 恶心 ...
conda create -n yolo python=3.9 # -n和-p不能同时设置 ...
# 激活虚拟环境
conda activate yolo
# cd 到项目路径, 执行安装依赖包命令
cd C:\mrathena\develop\workspace\pycharm\python.yolov5.starter
# 安装依赖包, 注意, 这里不要开科学上网, 不然可能失败
pip install -r requirements.txt
# 安装成功

直接运行 detect.py, 运行结果, (这里我添加了一张有人的图片看检测效果)

PyTorch

YOLOv5  2022-8-17 Python-3.9.13 torch-1.12.1+cpu CPU

默认情况下, YOLO V5 是以 CPU 的方式运行的, 我们要改成 GPU 的方式, 这样训练和检测会更快

好像先决条件是你的电脑装有包含 CUDA 核心的 Nvidia 显卡, 我这边是 Nvidia 2080 8G

网上的教程真的是五花八门, 推荐下面这篇, 凭我的经验, 我觉得更可信

一文搞懂PyTorch与CUDA那些事

总结下来就是, 安装 PyTorch 不需要电脑上有 CUDA 运行环境, 因为安装时会自动下载, 确保 CUDA 和显卡驱动版本对应就可以了

这里的 表3 就是 CUDA 和显卡驱动的关系, 新显卡驱动向前兼容旧的CUDA, 我们看 Windows, 只要显卡驱动版本大于等于 516.31, 就可以跑 CUDA 11.7.1, 大于等于 516.01, 就可以跑 11.7, 大于等于 511.23, 就可以跑 11.6, 以此类推


我的显卡驱动版本是516.94, 支持 CUDA 11.7.1 及以下版本, 先到 PyTorch 官网看看, 最新的是 11.6, 那就下它了

PyTorch 官网

# 使用 conda, pip 没试过, 我觉得 anaconda 在机器视觉等方便会做的更好, 所以我这里用 conda
conda install pytorch torchvision torchaudio cudatoolkit=11.6 -c pytorch -c conda-forge

记住这里的 CUDA Toolkit 版本 11.6, 如果需要安装 TensorRT, 则 TensorRT 版本要与之对应

安装好后, 虚拟环境从 1.3G 变成了 5.8G, 真的是 … 为什么不能搞一套类似Maven的项目管理工具呢 …

跟着官网的教程检查是否安装成功, 执行 python, 输入

import torch
torch.cuda.is_available()


看样子是 CUDA 可以使用了, 然后再试跑一下 detect.py, 应该是已经使用显卡在跑了

YOLOv5  2022-8-17 Python-3.9.13 torch-1.12.1 CUDA:0 (NVIDIA GeForce RTX 2080, 8192MiB)

第一阶段 使用自带模型 实现实时目标检测

屏幕截图

Windows桌面采集技术
D3DShot
D3DShot Issues#44 Bump pillow version for Python 3.9 support on Windows

GDI(CPU) 截图和 DXGI(GPU) 截图, 在不同的场景下有不同的效果, 要针对场景做测试, 再决定使用哪一种方式

GDI MSS 和 Win32

Win32 截图

主要使用 windows 自带的 gdi32.dll 中的函数完成截图, 使用 pywin32 包(对函数和常量的封装比较友好)

使用 pywin32 需要先安装该包, 建议使用 conda 来安装. 我这边开始是使用 pip 安装的, 其中的 win32ui 部分始终不能正确导入, 后来换了 conda 重新安装后, 一切正常了, 所以如果安装包有问题, 可以试试 conda 安装

DXGI D3DShot

DXGI 截图的性能和 GPU 算力是否充足有关, 当 GPU 占用很高时, D3DShot 耗时会急剧增大

安装 D3DShot 的时候不太顺利, 因为 D3DShot 在 Python 3.9 里会和 pillow 版本冲突, 所以使用大佬修复过的版本来替代

pip install git+https://github.com/fauskanger/D3DShot#egg=D3DShot

不同场景横评 含转换为 opencv 可用格式(BGR)的耗时

import timefrom toolkit import Monitorprint('开始')
times = 100
begin = time.perf_counter_ns()
for i in range(times):img = Monitor.grab(region=(3440 // 7 * 3, 1440 // 3, 3440 // 7, 1440 // 3), mss=True, convert=True)# img = Monitor.grab(region=(0, 0, 3440, 1440), convert=True)
interval = time.perf_counter_ns() - begin
print(f'总耗时:{interval}ns, 约{round(interval / 1000000)}ms, 平均耗时:约{round(interval / times / 1000000)}ms')

我的配置是 AMD R7 2700X, Nvidia 2080(8G), 3440*1440, 取连续100次截图的平均耗时(ms), 测试结果如下

MSS Win32 D3DShot
GPU空闲 截全屏 不转换 40 44 22
GPU空闲 截全屏 转换 45 58 49
GPU空闲 截中心1/9(宽3等分高3等分, 1147*480) 不转换 10 10 23
GPU空闲 截中心1/9 转换 10 10 25
GPU空闲 截中心1/21(宽7等分高3等分, 492*480) 不转换 10 10 22
GPU空闲 截中心1/21转换 10 10 23
Apex射击场 截中心1/21 不转换 10 10 62
Apex射击场 截中心1/21 转换 10 10 62

据我了解, 大佬们甚至可以做到每秒250次截图, 即4毫秒截图, 这个我暂时做不到. 但是实测时发现, MSS截图(其他没试)耗时经常会小于10ms, 有时候甚至能低到 2ms

截图:3495000ns, 3ms, 检测:18ms, 总计:22ms, 数量:2/2
截图:5742200ns, 6ms, 检测:18ms, 总计:24ms, 数量:2/2
截图:3095600ns, 3ms, 检测:18ms, 总计:21ms, 数量:2/2
截图:10357700ns, 10ms, 检测:21ms, 总计:32ms, 数量:2/2
截图:2655000ns, 3ms, 检测:22ms, 总计:25ms, 数量:2/2
截图:4692700ns, 5ms, 检测:21ms, 总计:25ms, 数量:2/2
截图:3923300ns, 4ms, 检测:22ms, 总计:26ms, 数量:2/2
截图:3813700ns, 4ms, 检测:21ms, 总计:24ms, 数量:2/2
截图:5094600ns, 5ms, 检测:21ms, 总计:26ms, 数量:2/2
截图:3952300ns, 4ms, 检测:18ms, 总计:22ms, 数量:2/2
截图:7658000ns, 8ms, 检测:19ms, 总计:26ms, 数量:2/2
截图:7662700ns, 8ms, 检测:21ms, 总计:29ms, 数量:2/2
截图:10827200ns, 11ms, 检测:22ms, 总计:33ms, 数量:2/2
截图:5746600ns, 6ms, 检测:22ms, 总计:27ms, 数量:2/2
截图:14319900ns, 14ms, 检测:21ms, 总计:36ms, 数量:2/2
截图:7482200ns, 7ms, 检测:18ms, 总计:25ms, 数量:2/2
截图:6644800ns, 7ms, 检测:18ms, 总计:25ms, 数量:2/2

实时屏幕检测

效果展示 Python YOLO V5 实时截屏与目标检测

就是截图加检测, 截全屏时, 截图和检测耗时都较高, 截部分会有较高的提升(其实在FPS游戏中完全没必要截全屏, 截中心400像素就够了)

YOLO V5 中, detect.py 的作用就是使用某个训练好的模型来检测图片, 所以它里面肯定有使用模型和检测图片的相关代码

需要从 detect.py 中找到核心代码, 然后做好封装, 实现输入截图, 输出目标检测的结果(目标的类型/置信度/xywh等信息). 代码在本文 工程源码下 toolkit.py 部分

这里有一个坑需要注意, 不要自己新建文件夹, 就和其他 yolo 文件放在一起, 通过文件名前缀来区分. 不然在引入文件方面可能会出一堆奇怪的问题

import cv2
from win32con import HWND_TOPMOST, SWP_NOMOVE, SWP_NOSIZE
from win32gui import FindWindow, SetWindowPosfrom toolkit import Detectorregion = (3440 // 7 * 3, 1440 // 3, 3440 // 7, 1440 // 3)
weight = 'yolov5s.pt'
detector = Detector(region, weight)title = 'Realtime ScreenGrab Detect'while True:_, img = detector.detect(image=True)cv2.namedWindow(title, cv2.WINDOW_AUTOSIZE)cv2.imshow(title, img)# 寻找窗口, 设置置顶hwnd = FindWindow(None, title)SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)k = cv2.waitKey(1)  # 0:不自动销毁也不会更新, 1:1ms延迟销毁if k % 256 == 27:# ESC 关闭窗口cv2.destroyAllWindows()exit('ESC ...')

开发过程中, 可以使用 opencv 做实时效果查看. 在 PyCharm 中如果出现 cv2 没有提示/方法源码无法点击 等问题, 可做如下操作

第二阶段 训练模型 了解训练模型的方式方法 并能使用训练出来的模型

注意: 不一定都得自己手动标, 标图有多种方式, 如纯手动, 半手动(应用某模型先检测生成对应标图txt文件然后再微调), 伪真实(拼接图片生成数据), 内存全自动(读内存找到骨架数据)等, 以下是纯手动

labelimg

labelimg 是在训练模型过程中用来标记目标的工具

在虚拟环境中安装 labelimg, 用于标记, 安装完成后执行 labelimg 会打开GUI界面

pip install labelimg


快捷键

  • W: 创建一个标记的框选
  • A: 上一张图
  • D: 下一张图
  • Ctrl+S: 保存当前标图
  • Enter: 填好 class 后按 Enter 相当于点 OK
  • 我建议写个 pynput 程序, 检测到 F 键按下, 就触发 Enter 和 Ctrl+S, 这样自定义一个快捷键, 能省很多事儿

做了一下, 没成功, 可恶, 失败在莫名其妙的地方

from pynput.keyboard import Key, Controller, Listener, KeyCode
import winsounddef listener():keyboard = Controllerdef release(key):if key == Key.end:winsound.Beep(400, 200)return Falseelif key == KeyCode.from_char('s'):keyboard.press(Key.enter)keyboard.release(Key.enter)with keyboard.pressed(Key.ctrl_l):keyboard.press('s')keyboard.release('s')with Listener(on_release=release) as k:k.join()listener()
TypeError: press() missing 1 required positional argument: 'key'

创建数据集文件夹, 我的数据集目录是 D:\resource\develop\python\dataset.yolo.v5, 本次训练集叫做 test, 所以在数据集下新建 test 目录

test 下创建 data/images 作为原始图片库, 创建 data/labels 作为标记信息库

然后在 labelimg 中设置好读取路径和保存路径, 开始标图





标好一张图后, 记得保存, 在 data/labels 目录下会自动生成 classes.txt 文件和图片对应的标记文件如 bus.txt

把其他图也标好, 下面是图与标记的对应, 注意图片最好不要有中文, 防止万一

classes.txt 与 标记文件说明


classes.txt 中就是标记时分出来的两个类目, 这里一个是 head 一个是 body, 序号从0开始, 从上到下

标记文件中一行代表图片上的一个标记, 几行就是有几个标记

标记文件中每行有5个数据, 第一个是 类目索引, 后面4个是归一化(把长宽看成是1,其他点等比缩小)的 xCenter, yCenter, width, height

简单尝试

编写数据集配置文件

拷贝项目下的 coco128.yaml 更名为 dataset.for.me.test.yaml 并修改内容

path: D:\resource\develop\python\dataset.yolo.v5\test
train: data/images  # train images (relative to 'path') 128 images
val: data/labels  # val images (relative to 'path') 128 images# Classes
nc: 2  # number of classes
names: ['head', 'body']  # class names
  • path: 数据集根目录
  • train: 源图片目录(以 path 目录为基准)
  • val:
  • nc: 标记的类别的数目
  • names: 标记的类别, classes.txt 文件从上到下按顺序一个个写过来, 必须完全一致

编写训练文件参数

拷贝项目下的 train.py 更名为 train.for.me.test.py 并修改 parse_opt 的内容

  • –weights: ROOT / ‘yolov5s.pt’. 可以选择是否基于某个模型训练, 全新训练就 default=‘’
  • –data: data/dataset.for.me.test.yaml
  • –batch-size: GPU模式下, 每次取这么多个参数跑, 如果报错, 可以改小点
  • –project: default=‘D:\resource\develop\python\dataset.yolo.v5\test\runs/train’, 训练结果保存位置

运行训练文件

运行报错

OMP: Error #15: Initializing libiomp5md.dll, but found libiomp5md.dll already initialized.
OMP: Hint This means that multiple copies of the OpenMP runtime have been linked into the program. That is dangerous, since it can degrade performance or cause incorrect results. The best thing to do is to ensure that only a single OpenMP runtime is linked into the process, e.g. by avoiding static linking of the OpenMP runtime in any library. As an unsafe, unsupported, undocumented workaround you can set the environment variable KMP_DUPLICATE_LIB_OK=TRUE to allow the program to continue to execute, but that may cause crashes or silently produce incorrect results. For more information, please see http://www.intel.com/software/products/support/.


搜索发现, miniconda 下有两个, 其他的有3个, 其他的应该不影响, 但是 moniconda 下为什么有两个, 我不知道, 该怎么处理, 我不知道, 但我觉得, 不知道不要瞎搞, 所以就按它说的不推荐的方式试试看吧

import os
os.environ['KMP_DUPLICATE_LIB_OK']='True'

如果运行报错

RuntimeError: DataLoader worker (pid(s) 20496) exited unexpectedly

把启动参数里的 --workers 改成 0 试试, 原因我不知道也不会看也看不懂

运行结果

C:\mrathena\develop\miniconda\envs\yolov5\python.exe C:/mrathena/develop/workspace/pycharm/yolov5-6.2/train.for.me.test.py
train.for.me.test: weights=yolov5s.pt, cfg=, data=data\dataset.for.me.test.yaml, hyp=data\hyps\hyp.scratch-low.yaml, epochs=300, batch_size=16, imgsz=640, rect=False, resume=False, nosave=False, noval=False, noautoanchor=False, noplots=False, evolve=None, bucket=, cache=None, image_weights=False, device=, multi_scale=False, single_cls=False, optimizer=SGD, sync_bn=False, workers=8, project=D:\resource\develop\python\dataset.yolov5.6.2\test\runs/train, name=exp, exist_ok=False, quad=False, cos_lr=False, label_smoothing=0.0, patience=100, freeze=[0], save_period=-1, seed=0, local_rank=-1, entity=None, upload_dataset=False, bbox_interval=-1, artifact_alias=latest
github:  YOLOv5 is out of date by 2326 commits. Use `git pull ultralytics master` or `git clone https://github.com/ultralytics/yolov5` to update.
YOLOv5  b899afe Python-3.9.13 torch-1.12.1 CUDA:0 (NVIDIA GeForce RTX 2080, 8192MiB)hyperparameters: lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3.0, warmup_momentum=0.8, warmup_bias_lr=0.1, box=0.05, cls=0.5, cls_pw=1.0, obj=1.0, obj_pw=1.0, iou_t=0.2, anchor_t=4.0, fl_gamma=0.0, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, degrees=0.0, translate=0.1, scale=0.5, shear=0.0, perspective=0.0, flipud=0.0, fliplr=0.5, mosaic=1.0, mixup=0.0, copy_paste=0.0
Weights & Biases: run 'pip install wandb' to automatically track and visualize YOLOv5  runs in Weights & Biases
ClearML: run 'pip install clearml' to automatically track, visualize and remotely train YOLOv5  in ClearML
TensorBoard: Start with 'tensorboard --logdir D:\resource\develop\python\dataset.yolov5.6.2\test\runs\train', view at http://localhost:6006/
Overriding model.yaml nc=80 with nc=2from  n    params  module                                  arguments                     0                -1  1      3520  models.common.Conv                      [3, 32, 6, 2, 2]              1                -1  1     18560  models.common.Conv                      [32, 64, 3, 2]                2                -1  1     18816  models.common.C3                        [64, 64, 1]                   3                -1  1     73984  models.common.Conv                      [64, 128, 3, 2]               4                -1  2    115712  models.common.C3                        [128, 128, 2]                 5                -1  1    295424  models.common.Conv                      [128, 256, 3, 2]              6                -1  3    625152  models.common.C3                        [256, 256, 3]                 7                -1  1   1180672  models.common.Conv                      [256, 512, 3, 2]              8                -1  1   1182720  models.common.C3                        [512, 512, 1]                 9                -1  1    656896  models.common.SPPF                      [512, 512, 5]                 10                -1  1    131584  models.common.Conv                      [512, 256, 1, 1]              11                -1  1         0  torch.nn.modules.upsampling.Upsample    [None, 2, 'nearest']          12           [-1, 6]  1         0  models.common.Concat                    [1]                           13                -1  1    361984  models.common.C3                        [512, 256, 1, False]          14                -1  1     33024  models.common.Conv                      [256, 128, 1, 1]              15                -1  1         0  torch.nn.modules.upsampling.Upsample    [None, 2, 'nearest']          16           [-1, 4]  1         0  models.common.Concat                    [1]                           17                -1  1     90880  models.common.C3                        [256, 128, 1, False]          18                -1  1    147712  models.common.Conv                      [128, 128, 3, 2]              19          [-1, 14]  1         0  models.common.Concat                    [1]                           20                -1  1    296448  models.common.C3                        [256, 256, 1, False]          21                -1  1    590336  models.common.Conv                      [256, 256, 3, 2]              22          [-1, 10]  1         0  models.common.Concat                    [1]                           23                -1  1   1182720  models.common.C3                        [512, 512, 1, False]          24      [17, 20, 23]  1     18879  models.yolo.Detect                      [2, [[10, 13, 16, 30, 33, 23], [30, 61, 62, 45, 59, 119], [116, 90, 156, 198, 373, 326]], [128, 256, 512]]
Model summary: 270 layers, 7025023 parameters, 7025023 gradients, 16.0 GFLOPsTransferred 343/349 items from yolov5s.pt
AMP: checks passed
optimizer: SGD(lr=0.01) with parameter groups 57 weight(decay=0.0), 60 weight(decay=0.0005), 60 bias
train: Scanning 'D:\resource\develop\python\dataset.yolov5.6.2\test\data\labels.cache' images and labels... 3 found, 0 missing, 0 empty, 0 corrupt: 100%|██████████| 3/3 [00:00<?, ?it/s]
val: Scanning 'D:\resource\develop\python\dataset.yolov5.6.2\test\data\labels.cache' images and labels... 3 found, 0 missing, 0 empty, 0 corrupt: 100%|██████████| 3/3 [00:00<?, ?it/s]
Plotting labels to D:\resource\develop\python\dataset.yolov5.6.2\test\runs\train\exp\labels.jpg... AutoAnchor: 4.94 anchors/target, 1.000 Best Possible Recall (BPR). Current anchors are a good fit to dataset
Image sizes 640 train, 640 val
Using 3 dataloader workers
Logging results to D:\resource\develop\python\dataset.yolov5.6.2\test\runs\train\exp
Starting training for 300 epochs...Epoch   gpu_mem       box       obj       cls    labels  img_size0/299    0.621G    0.1239   0.05093    0.0282        24       640: 100%|██████████| 1/1 [00:02<00:00,  2.47s/it]Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100%|██████████| 1/1 [00:00<00:00,  6.26it/s]all          3         16    0.00131     0.0625    0.00074    0.00037Epoch   gpu_mem       box       obj       cls    labels  img_size1/299    0.774G    0.1243   0.04956   0.02861        22       640: 100%|██████████| 1/1 [00:00<00:00,  5.85it/s]Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100%|██████████| 1/1 [00:00<00:00,  6.07it/s]all          3         16    0.00342      0.188    0.00261    0.00032...
...
...Epoch   gpu_mem       box       obj       cls    labels  img_size298/299     0.83G   0.04905   0.04686  0.006326        26       640: 100%|██████████| 1/1 [00:00<00:00,  8.85it/s]Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100%|██████████| 1/1 [00:00<00:00, 12.50it/s]all          3         16      0.942      0.956      0.981      0.603Epoch   gpu_mem       box       obj       cls    labels  img_size299/299     0.83G   0.05362   0.05724   0.01014        39       640: 100%|██████████| 1/1 [00:00<00:00,  4.93it/s]Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100%|██████████| 1/1 [00:00<00:00, 12.50it/s]all          3         16      0.942      0.951      0.981      0.631300 epochs completed in 0.054 hours.
Optimizer stripped from D:\resource\develop\python\dataset.yolov5.6.2\test\runs\train\exp\weights\last.pt, 14.5MB
Optimizer stripped from D:\resource\develop\python\dataset.yolov5.6.2\test\runs\train\exp\weights\best.pt, 14.5MBValidating D:\resource\develop\python\dataset.yolov5.6.2\test\runs\train\exp\weights\best.pt...
Fusing layers...
Model summary: 213 layers, 7015519 parameters, 0 gradients, 15.8 GFLOPsClass     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100%|██████████| 1/1 [00:00<00:00,  9.34it/s]all          3         16      0.942      0.952      0.981      0.624head          3          8      0.884       0.96      0.967      0.504body          3          8          1      0.945      0.995      0.743
Results saved to D:\resource\develop\python\dataset.yolov5.6.2\test\runs\train\exp


测试训练结果

weights 里面的 best.pt 就是本次训练出来的模型了, 测试一下

拷贝项目中的 detect.py 为 detect.for.me.test.py, 修改部分参数, 运行查看效果

  • –weights: ‘D:\resource\develop\python\dataset.yolo.v5\test\runs\train\exp\weights\best.pt’



    有那味儿了, 毕竟样本也就3张图片, 就是 head 的检测有点问题, 调整下 --iou-thres 为 0 (同类交并比大于此值的框会被留下) 试试, 哈哈, 模型觉得脑袋更像是个 body, 无所谓了, 本来也就是个 test, 有效果就行了

如何取得最好的训练效果

官方教程 Tips for Best Training Results

官方推荐, 先使用默认参数来一轮训练, 通常这样就已经能够获得比较好的效果了. 如果效果不太好, 再尝试修改部分参数

对数据集的要求如下

  • 每种类型的图片要超过1500张 (如: 至少有1500张图片中包含head)
  • 每种类型标记出来的实例要超过10000个 (如: 1500张图片中包含head, 但是标记的head总数要达到10000个)
  • 图片要足够多样 (如: 对于现实世界的用例,我们推荐来自一天中不同时间、不同季节、不同天气、不同照明、不同角度、不同来源(在线抓取、本地收集、不同相机)等的图像。)
  • 必须标记所有图像中所有类的所有实例, 部分标记将不起作用 (每张图片中的所有目标必须全部标出来, 少了会影响效果)
  • 标签必须紧紧地包围每个对象, 对象与其边界框之间不应存在空间 (标记要准确, 不要多了, 也不要少了)
  • 最好是搞一些背景图片和目标对象在一起的图片一起训练(没有也没关系), 背景图像是没有添加到数据集以减少误报 (FP) 的对象的图像。我猜测类似路边广告牌上的人的照片, 用于让模型能识别真人假人

对参数的一些解释

  • 训练次数, 300次, 如果不到300次就达到了拟合(我猜是效果较好的一种状态, 再训练也不会有明显提升了), 可以适当减小, 反之则翻倍增加
  • 图片尺寸, 默认是用640训练的, 如果图片中有太多的小目标, 可以尝试使用1280的, 对小目标可能会更友好, 效果可能会更好
  • 批量大小, 使用硬件允许的最大 --batch-size. 小批量会产生较差的批量标准统计数据, 应避免使用(没明白, 反正尽量最大就是了)
  • 超参数, 先使用默认超参数训练, …

第三阶段 训练 Apex 射击场 假人 模型

Python Apex Legends 武器自动识别与压枪 全过程记录
yolov5实现机器视觉ai,本人跑代码训练时总结下来的坑(参数设置,服务器使用,自动打标签,训练速度,显存使用率…)
GitHub APEX-yolov5-aim-assist
GitHub APEX-yolov5-aim-assist 工程下载(防止原工程失效)

训练规划

我打算分两个训练, 先是拿射击场的假人来训练一波, 尝试达到较好的效果, 然后再开始真实游戏场景的训练

测试假人同样分两个步骤

  • 控制截图加检测的时间, 目标是 10ms 内, 可放宽到 20ms 左右, 有空再研究如何进一步提升效率
  • 达到时间要求后, 开始研究鼠标移动, 目标是精准且平滑

检测目标只有两个, 一个是 BODY (包括 HEAD ), 一个是 HEAD . 移动鼠标的时候可以选

训练距离, 在训练场看了下, 5米时, 假人高度大约占屏幕的 1/4, 30米时, 假人还是人样, 50米时, 假人就和竖线差不多了, 我觉得训练 5米-30米 范围内的假人大小, 应该就差不多了, 假人再缩小的话, 不好标. 而且无镜/单倍镜最远也就能打个三五十米的样子了

截图范围, 屏幕中心 1/21 (492*480) 已经足以把 5米开外的目标包括进来了

训练尺寸, 640足以, 因为截图只有 492*480, 使用640不仅不需要缩小图片, 反而还需要补充扩大, 所以不会丢失原截图的细节. 另外我也尝试过1280, 它包含更多的参数, 在训练的时候需要太大的虚拟内存(分了60G, 硬盘剩余空间都红了)和显存, 机器都有点吃不消了, 还是推荐640

训练假人

数据集图片获取

各个角度, 各个距离, 先截100张图, 看看效果行不行, 类似下面这样的图片

import time
import pynput
import mssfrom toolkit import Monitordef mouse():def down(x, y, button, pressed):if button == pynput.mouse.Button.x2:return Falseif button == pynput.mouse.Button.x1:if pressed:img = Monitor.grab((3440//7*3, 1440//3, 3440//7, 1440//3))name = f'D:\\resource\\develop\\python\\dataset.yolov5.6.2\\apex\\dummy\\data\\images\\{int(time.time())}.png'mss.tools.to_png(img.rgb, img.size, output=name)print(name)with pynput.mouse.Listener(on_click=down) as m:m.join()mouse()


标记

先正序标 BODY , 完了再倒序标 HEAD , 这样比较方便, 不然 head 和 body 的切换比较麻烦

也可以先标记 100 张, 然后训练一个模型出来, 然后用该模型去检测剩余的数据集, 并生成格式正确的对应图片的 txt 标记文件, 完了再精修

参数

拷贝 data/coco128.yaml 为 dataset.for.apex.dummy.yaml

path: D:\resource\develop\python\dataset.yolo.v5\apex\dummy
train: data/images  # train images (relative to 'path') 128 images
val: data/images  # val images (relative to 'path') 128 images# Classes
nc: 2  # number of classes
names: ['body', 'head']  # class names

这里 classes 中的 names 注意顺序要和 labelimg 中的顺序一致, 不然训练出来的模型, 类别是不对的

拷贝项目下的 train.py 更名为 train.for.apex.dummy.py 并修改 parse_opt 的内容

  • –weights: ROOT / ‘yolov5s.pt’ , 以 yolov5s.pt 作为基础模型, 在此基础上训练自己的模型
  • –data: data/dataset.for.apex.dummy.yaml
  • –imgsz: 640, 注意要和 weights 对应
  • –batch-size: 16, 先试默认的16, 如果报显存不足, 则改成8
  • –project: default=‘D:\resource\develop\python\dataset.yolo.v5\apex\dummy\runs\train’ 保存训练结果的位置

训练

OSError: [WinError 1455] 页面文件太小,无法完成操作。 Error loading "C:\mrathena\develop\miniconda\envs\yolo\lib\site-packages\torch\lib\cudnn_cnn_train64_8.dll" or one of its dependencies.

如果报错类似如上, 需要调整硬盘的虚拟内存

“OSError: [WinError 1455]页面文件太小,无法完成操作。”解决方案

Win11 的修改入口应该在这里

我固态C盘剩余70多G, 改成10G后还是报错, 改成40G还是报错, 改成50G才能跑, 但中途还是报了内存不足申请失败的错, 这尼玛假的吧, 最后改成60G虚拟内存才能正常跑

我们看看过程中的内存和显存使用情况

  • 内存: 内存加虚拟内存共 108G, 但看起来像是内存还剩 30G 空余, 放着内存不用非要用硬盘?
  • 显存: 每周期训练需要约 2.3G (每个 Epoch 的 gpu_mem)
     Epoch   gpu_mem       box       obj       cls    labels  img_size100/299     2.29G   0.02161   0.00893  0.001984        21       640: 100%|██████████| 15/15 [00:01<00:00,  8.19it/s]Class     Images     Labels          P          R     mAP@.5 mAP@.5:.95: 100%|██████████| 8/8 [00:01<00:00,  6.30it/s]all        116        228      0.998      0.989      0.995      0.845

查看输出文件夹中的内容, weights 文件夹里面的 best.pt 就是训练好的模型


测试

通过参数 +fps_max unlimited 不锁帧, 显卡全力工作, 在射击场, 帧数可以达到140左右, 通过参数 +fps_max 60 锁帧, 减少 GPU 占用, 为目标检测腾出资源

最终一个流程耗时大约 20-30ms

截图:5677000ns, 6ms, 检测:18ms, 总计:24ms, 数量:2/2
截图:3703300ns, 4ms, 检测:23ms, 总计:27ms, 数量:2/2
截图:11831700ns, 12ms, 检测:18ms, 总计:30ms, 数量:2/2
截图:5698400ns, 6ms, 检测:18ms, 总计:23ms, 数量:2/2
截图:14475000ns, 14ms, 检测:18ms, 总计:32ms, 数量:2/2
截图:2640700ns, 3ms, 检测:21ms, 总计:24ms, 数量:2/2
截图:5789900ns, 6ms, 检测:18ms, 总计:23ms, 数量:2/2
截图:3093300ns, 3ms, 检测:22ms, 总计:25ms, 数量:2/2
截图:3186900ns, 3ms, 检测:18ms, 总计:21ms, 数量:2/2
截图:2672800ns, 3ms, 检测:18ms, 总计:21ms, 数量:2/2
截图:3240200ns, 3ms, 检测:18ms, 总计:22ms, 数量:2/2
截图:9363400ns, 9ms, 检测:18ms, 总计:28ms, 数量:2/2
截图:9161500ns, 9ms, 检测:21ms, 总计:30ms, 数量:2/2
截图:2921900ns, 3ms, 检测:22ms, 总计:25ms, 数量:2/2
截图:3440800ns, 3ms, 检测:18ms, 总计:21ms, 数量:2/2
截图:10665800ns, 11ms, 检测:18ms, 总计:29ms, 数量:2/2
截图:3227900ns, 3ms, 检测:18ms, 总计:21ms, 数量:2/2
截图:2737400ns, 3ms, 检测:18ms, 总计:21ms, 数量:2/2
截图:3668100ns, 4ms, 检测:23ms, 总计:26ms, 数量:2/2
截图:5669300ns, 6ms, 检测:18ms, 总计:23ms, 数量:2/2
截图:14372800ns, 14ms, 检测:18ms, 总计:33ms, 数量:2/2
截图:6158300ns, 6ms, 检测:22ms, 总计:28ms, 数量:2/2
截图:5659200ns, 6ms, 检测:18ms, 总计:23ms, 数量:2/2
截图:4428100ns, 4ms, 检测:18ms, 总计:22ms, 数量:2/2
截图:3261700ns, 3ms, 检测:18ms, 总计:21ms, 数量:2/2
截图:3348900ns, 3ms, 检测:18ms, 总计:21ms, 数量:2/2
截图:3479400ns, 3ms, 检测:23ms, 总计:26ms, 数量:2/2
截图:10634300ns, 11ms, 检测:18ms, 总计:28ms, 数量:2/2
截图:10361500ns, 10ms, 检测:18ms, 总计:28ms, 数量:2/2
截图:3234600ns, 3ms, 检测:22ms, 总计:26ms, 数量:2/2
截图:10610200ns, 11ms, 检测:18ms, 总计:29ms, 数量:2/2
import cv2
from win32con import HWND_TOPMOST, SWP_NOMOVE, SWP_NOSIZE
from win32gui import FindWindow, SetWindowPosfrom toolkit import Detectorregion = (3440 // 7 * 3, 1440 // 3, 3440 // 7, 1440 // 3)
# weight = 'yolov5s.pt'
weight = 'model.apex.dummy.pt'
detector = Detector(region, weight)title = 'Realtime ScreenGrab Detect'while True:_, img = detector.detect(image=True)# _, img = aimbot.detect(image=True, includes=['head'])cv2.namedWindow(title, cv2.WINDOW_AUTOSIZE)cv2.imshow(title, img)# 寻找窗口, 设置置顶hwnd = FindWindow(None, title)SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)k = cv2.waitKey(1)  # 0:不自动销毁也不会更新, 1:1ms延迟销毁if k % 256 == 27:# ESC 关闭窗口cv2.destroyAllWindows()exit('ESC ...')

粗浅的优化方案

可能的优化方案, 目标是效率达到每秒检测100次, 即每个循环耗时10ms左右

  • Ncnn
  • Vulkan
  • TensorRT 加速
  • dml 加速, 支持 A 卡
  • FP16 模型
  • INT8 模型
  • paddleslim 压缩
  • 部署直接用的飞浆的FastDeploy (支持使用trt进行推理)
  • 降低游戏占用
    • 游戏锁帧数 (因为我的显卡性能还行, 所以锁帧 60 后, 空余算力足够做目标检测了, 效果显著)
    • 游戏特效调最低
    • 游戏分辨率调小
  • 提升截图效率 (有大佬说截图能做到 250 张每秒, 不知道怎么弄的, 我暂时做不到)
    • 缩小截图范围 (屏幕最中间的正方形, 3440,1440 的分辨率, 截图最中间的 492,480 就好了)
    • 使用高效的API, 当GPU性能不足时, 就放弃DXGI(GPU)截图, 使用GDI(CPU)截图(MSS/Win32). 横评取最快的方式
  • 双显卡, 一个用来玩游戏, 一个用来跑AI
  • 换4090

环境层面的优化

优化过程 部署 TensorRT 推理加速

TensorRT-优化-原理

安装 Python 环境的 TensorRT

YOLO V5 从 v6.0 起, 官方自带 export.py 工具可以将 .pt 权重文件转换成其他格式, 包括 TensorRT 的 .engine. 不再需要使用 C++ 构建生成

TFLite, ONNX, CoreML, TensorRT Export

官方提供的 .pt 转换 .engine 的例子如下, 需要先下载 nvidia-tensorrt 模块

# TensorRT
pip install -U nvidia-tensorrt --index-url https://pypi.ngc.nvidia.com  # install
python export.py --weights yolov5s.pt --include engine --imgsz 640 --device 0  # export
python detect.py --weights yolov5s.engine --imgsz 640 --device 0  # inference

目前这个方法已经无法下载 nvidia-tensorrt 了. 安装 nvidia-pyindex 也不行

本次使用到的 whl dll 等文件 百度网盘

千辛万苦找到了一个真实可靠行之有效的 方法, 具体如下

  • 首先确定自己 Python 环境中 CUDA 的版本, 查看安装 PyTorch 时, 命令行中 cudatoolkit 的版本即可
  • 从 Nvidia TensorRT 官网 下载并安装对应 CUDA 版本的 TensorRT, 之后在目录 TensorRT/python 中选择正确版本的 whl 文件, 如 tensorrt-8.4.3.1-cp39-none-win_amd64.whl, 执行 pip intall tensorrt-8.4.3.1-cp39-none-win_amd64.whl 安装 tensorrt
  • 把目录 TensorRT/lib 添加到 PATH 环境变量中
  • 当前的 Python 虚拟环境中, 我们安装过 PyTouch, 会自动安装 cuda 和 cudnn 的相关工具与环境, 我们从其中的 cudnn 中找到一个库文件 cudnn64_8.dll, 从 miniconda\pkgs\pytorch-1.12.1-py3.9_cuda11.6_cudnn8_0\Lib\site-packages\torch\lib 找到 cudnn64_8.dll 放到 TensorRT/lib 中. 没有的话从 我的网盘 下载涉及到的相关 Windows 平台库文件, 也可以参考 这篇文章 或者从 这里 搜索下载, 再不济也可以从 Nvidia CUDNN 官网 下载并安装对应 CUDA 版本的 cuDNN, 并从中找到对应库文件

环境准备好后. 执行下面代码测试是否成功

import tensorrt as trt
print(trt.__version__)

模型转换

python export.py --weights model.apex.dummy.pt --include engine --imgsz 640 --device 0

输入 model.apex.dummy.pt, 生成 model.apex.dummy.engine, 需要注意的是, 转换过程涉及文件的序列化与反序列化, 即我的 .engine 在其他电脑是无法运行的, 会报反序列化失败. 所以需要自行尝试将 .pt 转换为 .engine

(cuda) C:\mrathena\develop\workspace\pycharm\python.yolo.starter\5>python export.py --weights model.apex.dummy.pt --include engine --imgsz 640 --device 0
export: data=C:\mrathena\develop\workspace\pycharm\python.yolo.starter\5\data\coco128.yaml, weights=['model.apex.dummy.pt'], imgsz=[640], batch_size=1, device=0, half=False, inplace=False, train=False, keras=False, optimize=False, int8=False, dynamic=False, simplify=False, opset=12, verbose=False, workspace=4, nms=False, agnostic_nms=False, topk_per_class=100, topk_all=100, iou_thres=0.45, conf_thres=0.25, include=['engine']
YOLOv5  2022-8-17 Python-3.9.13 torch-1.12.1 CUDA:0 (NVIDIA GeForce RTX 2080, 8192MiB)Fusing layers...
Model summary: 213 layers, 7015519 parameters, 0 gradients, 15.8 GFLOPsPyTorch: starting from model.apex.dummy.pt with output shape (1, 25200, 7) (13.8 MB)
requirements: onnx not found and is required by YOLOv5, attempting auto-update...
WARNING: Ignore distutils configs in setup.cfg due to encoding errors.WARNING: Ignore distutils configs in setup.cfg due to encoding errors.
WARNING: Ignore distutils configs in setup.cfg due to encoding errors.
Looking in indexes: https://pypi.org/simple, https://pypi.ngc.nvidia.com
Collecting onnxDownloading onnx-1.12.0-cp39-cp39-win_amd64.whl (11.5 MB)--------------------------------------- 11.5/11.5 MB 59.5 MB/s eta 0:00:00
Requirement already satisfied: typing-extensions>=3.6.2.1 in c:\mrathena\develop\miniconda\envs\cuda\lib\site-packages (from onnx) (4.4.0)
Requirement already satisfied: numpy>=1.16.6 in c:\mrathena\develop\miniconda\envs\cuda\lib\site-packages (from onnx) (1.23.4)
Requirement already satisfied: protobuf<=3.20.1,>=3.12.2 in c:\mrathena\develop\miniconda\envs\cuda\lib\site-packages (from onnx) (3.19.6)
Installing collected packages: onnx
Successfully installed onnx-1.12.0requirements: 1 package updated per ['onnx']
requirements:  Restart runtime or rerun command for updates to take effectONNX: starting export with onnx 1.12.0...
WARNING: The shape inference of prim::Constant type is missing, so it may result in wrong shape inference for the exported graph. Please consider adding it in symbolic function.
WARNING: The shape inference of prim::Constant type is missing, so it may result in wrong shape inference for the exported graph. Please consider adding it in symbolic function.
WARNING: The shape inference of prim::Constant type is missing, so it may result in wrong shape inference for the exported graph. Please consider adding it in symbolic function.
WARNING: The shape inference of prim::Constant type is missing, so it may result in wrong shape inference for the exported graph. Please consider adding it in symbolic function.
WARNING: The shape inference of prim::Constant type is missing, so it may result in wrong shape inference for the exported graph. Please consider adding it in symbolic function.
WARNING: The shape inference of prim::Constant type is missing, so it may result in wrong shape inference for the exported graph. Please consider adding it in symbolic function.
ONNX: export success, saved as model.apex.dummy.onnx (27.2 MB)TensorRT: starting export with TensorRT 8.4.3.1...
[10/25/2022-13:58:19] [TRT] [I] [MemUsageChange] Init CUDA: CPU +310, GPU +0, now: CPU 12042, GPU 2187 (MiB)
[10/25/2022-13:58:20] [TRT] [I] [MemUsageChange] Init builder kernel library: CPU +195, GPU +68, now: CPU 12422, GPU 2255 (MiB)
C:\mrathena\develop\workspace\pycharm\python.yolo.starter\5\export.py:250: DeprecationWarning: Use set_memory_pool_limit instead.config.max_workspace_size = workspace * 1 << 30
[10/25/2022-13:58:20] [TRT] [I] ----------------------------------------------------------------
[10/25/2022-13:58:20] [TRT] [I] Input filename:   model.apex.dummy.onnx
[10/25/2022-13:58:20] [TRT] [I] ONNX IR version:  0.0.7
[10/25/2022-13:58:20] [TRT] [I] Opset version:    13
[10/25/2022-13:58:20] [TRT] [I] Producer name:    pytorch
[10/25/2022-13:58:20] [TRT] [I] Producer version: 1.12.1
[10/25/2022-13:58:20] [TRT] [I] Domain:
[10/25/2022-13:58:20] [TRT] [I] Model version:    0
[10/25/2022-13:58:20] [TRT] [I] Doc string:
[10/25/2022-13:58:20] [TRT] [I] ----------------------------------------------------------------
[10/25/2022-13:58:20] [TRT] [W] onnx2trt_utils.cpp:369: Your ONNX model has been generated with INT64 weights, while TensorRT does not natively support INT64. Attempting to cast down to INT32.
TensorRT: Network Description:
TensorRT:       input "images" with shape (1, 3, 640, 640) and dtype DataType.FLOAT
TensorRT:       output "output" with shape (1, 25200, 7) and dtype DataType.FLOAT
TensorRT: building FP32 engine in model.apex.dummy.engine
C:\mrathena\develop\workspace\pycharm\python.yolo.starter\5\export.py:278: DeprecationWarning: Use build_serialized_network instead.with builder.build_engine(network, config) as engine, open(f, 'wb') as t:
[10/25/2022-13:58:21] [TRT] [I] [MemUsageChange] Init cuBLAS/cuBLASLt: CPU +0, GPU +8, now: CPU 12327, GPU 2263 (MiB)
[10/25/2022-13:58:21] [TRT] [I] [MemUsageChange] Init cuDNN: CPU +0, GPU +8, now: CPU 12327, GPU 2271 (MiB)
[10/25/2022-13:58:21] [TRT] [W] TensorRT was linked against cuDNN 8.4.1 but loaded cuDNN 8.3.2
[10/25/2022-13:58:21] [TRT] [I] Local timing cache in use. Profiling results in this builder pass will not be stored.
[10/25/2022-13:59:09] [TRT] [I] Some tactics do not have sufficient workspace memory to run. Increasing workspace size will enable more tactics, please check verbose output for requested sizes.
[10/25/2022-14:00:13] [TRT] [I] Detected 1 inputs and 4 output network tensors.
[10/25/2022-14:00:13] [TRT] [I] Total Host Persistent Memory: 132368
[10/25/2022-14:00:13] [TRT] [I] Total Device Persistent Memory: 1715712
[10/25/2022-14:00:13] [TRT] [I] Total Scratch Memory: 600576
[10/25/2022-14:00:13] [TRT] [I] [MemUsageStats] Peak memory usage of TRT CPU/GPU memory allocators: CPU 0 MiB, GPU 0 MiB
[10/25/2022-14:00:13] [TRT] [I] [BlockAssignment] Algorithm ShiftNTopDown took 24.0225ms to assign 7 blocks to 133 nodes requiring 34150400 bytes.
[10/25/2022-14:00:13] [TRT] [I] Total Activation Memory: 34150400
[10/25/2022-14:00:13] [TRT] [I] [MemUsageChange] Init cuDNN: CPU +0, GPU +10, now: CPU 12408, GPU 2311 (MiB)
[10/25/2022-14:00:13] [TRT] [W] TensorRT was linked against cuDNN 8.4.1 but loaded cuDNN 8.3.2
[10/25/2022-14:00:13] [TRT] [I] [MemUsageChange] TensorRT-managed allocation in building engine: CPU +0, GPU +0, now: CPU 0, GPU 0 (MiB)
[10/25/2022-14:00:13] [TRT] [W] The getMaxBatchSize() function should not be used with an engine built from a network created with NetworkDefinitionCreationFlag::kEXPLICIT_BATCH flag. This function will always return 1.
[10/25/2022-14:00:13] [TRT] [W] The getMaxBatchSize() function should not be used with an engine built from a network created with NetworkDefinitionCreationFlag::kEXPLICIT_BATCH flag. This function will always return 1.
TensorRT: export success, saved as model.apex.dummy.engine (31.0 MB)Export complete (128.68s)
Results saved to C:\mrathena\develop\workspace\pycharm\python.yolo.starter\5
Detect:          python detect.py --weights model.apex.dummy.engine
Validate:        python val.py --weights model.apex.dummy.engine
PyTorch Hub:     model = torch.hub.load('ultralytics/yolov5', 'custom', 'model.apex.dummy.engine')
Visualize:       https://netron.app(cuda) C:\mrathena\develop\workspace\pycharm\python.yolo.starter\5>

模型测试

  • 使用 model.apex.dummy.pt 时, 检测时间基本在 20ms-30ms
  • 使用 model.apex.dummy.engine 时, 检测时间不太稳定, 但总耗时在 20ms 上下, 有了一些提升
截图:13161700ns, 13ms, 检测:19ms, 总计:32ms, 数量:2/2
截图:8432600ns, 8ms, 检测:15ms, 总计:24ms, 数量:2/2
截图:6732200ns, 7ms, 检测:18ms, 总计:25ms, 数量:2/2
截图:7197100ns, 7ms, 检测:18ms, 总计:25ms, 数量:2/2
截图:6340500ns, 6ms, 检测:10ms, 总计:16ms, 数量:2/2
截图:7504700ns, 8ms, 检测:13ms, 总计:20ms, 数量:2/2
截图:4388800ns, 4ms, 检测:17ms, 总计:21ms, 数量:2/2
截图:13979200ns, 14ms, 检测:11ms, 总计:25ms, 数量:2/2
截图:12032900ns, 12ms, 检测:13ms, 总计:25ms, 数量:2/2
截图:11104200ns, 11ms, 检测:17ms, 总计:28ms, 数量:2/2
截图:8334600ns, 8ms, 检测:19ms, 总计:27ms, 数量:2/2
截图:7665600ns, 8ms, 检测:18ms, 总计:26ms, 数量:2/2
截图:6467700ns, 6ms, 检测:12ms, 总计:18ms, 数量:2/2
截图:7098300ns, 7ms, 检测:15ms, 总计:22ms, 数量:2/2
截图:4570400ns, 5ms, 检测:19ms, 总计:23ms, 数量:2/2
截图:12455400ns, 12ms, 检测:8ms, 总计:20ms, 数量:0/0
截图:14481700ns, 14ms, 检测:8ms, 总计:22ms, 数量:0/0
截图:11965200ns, 12ms, 检测:7ms, 总计:19ms, 数量:0/0
截图:10780700ns, 11ms, 检测:8ms, 总计:19ms, 数量:0/0
截图:11974200ns, 12ms, 检测:7ms, 总计:19ms, 数量:0/0
截图:10010900ns, 10ms, 检测:9ms, 总计:19ms, 数量:0/0
截图:5927900ns, 6ms, 检测:7ms, 总计:12ms, 数量:0/0
截图:11824500ns, 12ms, 检测:12ms, 总计:24ms, 数量:0/0
截图:9265200ns, 9ms, 检测:6ms, 总计:16ms, 数量:0/0
截图:9581200ns, 10ms, 检测:7ms, 总计:16ms, 数量:0/0
截图:7700700ns, 8ms, 检测:6ms, 总计:14ms, 数量:0/0
截图:8608900ns, 9ms, 检测:7ms, 总计:16ms, 数量:0/0
截图:4388000ns, 4ms, 检测:7ms, 总计:11ms, 数量:0/0
截图:9418100ns, 9ms, 检测:6ms, 总计:16ms, 数量:0/0
截图:7273000ns, 7ms, 检测:6ms, 总计:14ms, 数量:0/0
截图:10393100ns, 10ms, 检测:7ms, 总计:17ms, 数量:0/0
截图:12809100ns, 13ms, 检测:7ms, 总计:20ms, 数量:0/0
截图:9774700ns, 10ms, 检测:12ms, 总计:22ms, 数量:0/0
截图:12545600ns, 13ms, 检测:7ms, 总计:19ms, 数量:0/0
截图:13030700ns, 13ms, 检测:9ms, 总计:22ms, 数量:0/0
截图:7677700ns, 8ms, 检测:6ms, 总计:14ms, 数量:0/0
截图:5476800ns, 5ms, 检测:14ms, 总计:19ms, 数量:0/0
截图:12235400ns, 12ms, 检测:7ms, 总计:19ms, 数量:0/0
截图:15943100ns, 16ms, 检测:7ms, 总计:23ms, 数量:0/0
截图:14692700ns, 15ms, 检测:10ms, 总计:25ms, 数量:0/0
截图:7443600ns, 7ms, 检测:7ms, 总计:14ms, 数量:0/0
截图:11564500ns, 12ms, 检测:7ms, 总计:18ms, 数量:0/0
截图:7232100ns, 7ms, 检测:7ms, 总计:14ms, 数量:0/0
截图:5854500ns, 6ms, 检测:7ms, 总计:13ms, 数量:0/0
截图:9986300ns, 10ms, 检测:7ms, 总计:17ms, 数量:0/0
截图:6873700ns, 7ms, 检测:6ms, 总计:13ms, 数量:0/0

代码层面的优化

Python Apex 武器自动识别与压枪 全过程记录

参考 上文 - 环境准备 - 操纵键鼠 部分, 安装罗技驱动, 导入链接库. 在检测目标后, 随便拿一个目标, 然后移动鼠标到目标正中心

更精准的鼠标移动距离计算

鼠标灵敏度, ADS鼠标灵敏度加成, FOV视角, 位移像素之间的关系

  • 鼠标灵敏度: 鼠标灵敏度是鼠标物理移动距离与游戏内视角旋转角度的倍数关系. 假设鼠标灵敏度为 1 时, 鼠标向右移动 100 像素, 游戏内向右转动 2°. 则鼠标灵敏度为 2 时, 鼠标向右移动 100 像素, 游戏内向右转动 (2×鼠标灵敏度)°
  • ADS鼠标灵敏度加成: 开镜后的鼠标灵敏度, 灵敏度=基本灵敏度×ADS加成
  • FOV: 第一人称角色视角范围? 可近似认为就是视线角度? 包括水平和垂直两种
  • DPI: 物理调整鼠标的移动幅度

如何求 鼠标从中心跳到敌人位置对应的鼠标物理水平移动像素

假设 AB 是屏幕, ∠AOB 是角色视角(即 FOV), C 是屏幕中心(准星), X是敌人位置(已知, 目标检测得到的坐标), 求鼠标向左移动多少像素能让准星正好落在敌人 X 身上?

假设当前游戏设置的 FOV 是 120, 即水平视角是 120°, 即 ∠AOB 是 120°, 可知 ∠ AOC 是 60°

因为 OC 垂直于 AB 所以三角形 AOC 是直角三角形, 根据三角函数可知, AC/OC=tan60°, 可得 OC=AC/tan60°

AB 的长度就是游戏的水平分辨率, 可知 AC 长度为 AB 的一半, 由此可得 OC=AB/2/tan60°

CX 可以由目标检测的结果算出来, OC 也知道了, 又因为三角形 OCX 是直角三角形, 可知 OX/OC=tan∠COX, 可得 ∠COX

当然, 计算角度的正切值和反正切值, 需要将角度转换为弧度, 才能代入函数. 角度=弧度×π/180, 弧度=角度×180/π

tan60°=tan(60×π/180), ∠COX=atan(OX/OC)×180/π

假设游戏内水平旋转 360°, 对应的鼠标需要水平移动 a 像素, 我们称之为 一周移动量, 即鼠标每移动 a/360 像素, 视角水平旋转 1°

求出了 ∠COX, 再乘以视角旋转 1° 需要移动的鼠标距离, 就可以求出让准星落在 OX 这条线上需要移动的鼠标距离了

所以, 鼠标移动的距离=∠COX*a/360

接下来就该测 一周移动量 了, 拿到这个值即可计算需要的 鼠标移动量

算鼠标垂直移动量也是一样的, 但是因为 OC 已经计算出来了, 可以直接使用, 只需要测游戏内垂直方向的 半周移动量 即可

如何测 游戏内水平旋转 360° 对应鼠标水平移动的像素

通常在 FPS 游戏内, 鼠标的位置是固定在屏幕中心的, 我们移动鼠标, 动的不是鼠标位置, 而是游戏内 FOV 视角的朝向, 所以不论如何旋转视角, 取鼠标位置的函数返回的鼠标坐标点, 永远都是屏幕的正中心(全屏游戏时)

所以, 测试游戏内的 一周移动量 得反着来. 记录移动鼠标的距离之和, 当正好旋转了 1 周时, 该值就是需要的距离

需要用本文 工程源码 下 test.measure.palstance.py 部分来测试, 该方法可测试水平和垂直两个方向的像素, 垂直方向通常只能测一半

  • 操作说明

    • 按右键: 模拟鼠标向右移动 100 像素, 按住 Shift 键再按右键, 模拟鼠标向右移动 10 像素, 反之同理
    • 按 Enter 键清零, 可重新测量
    • 按 End 键结束程序
  • 测试水平距离: 选一个点让准星对准该点, 按右键旋转一周, 让准星正好回到原位, 看日志里向右移动了的距离
  • 测试垂直距离: 下拉鼠标到底, 找一个特征点, 让准星对准该点. 然后按上键到视角不再变化, 再多按几次上键, 然后按回车键清零, 然后按下键和 shift+下键, 直到准星与之前选好的特征点刚好重合, 然后看日志里的移动距离. 切记不要按多了, 因为到底后再按下键, 会继续累加移动距离, 但准星受游戏机制影响却是不会动了

游戏内 鼠标灵敏度 与 鼠标移动量的关系

游戏内有鼠标灵敏度的概念, 这是一个倍数, 用于放大移动鼠标旋转视角时的效果. 假设鼠标物理 DPI 全程不会改变

假设我们在 鼠标灵敏度为 2 时测得的 一周移动量 为 b, 可以用来计算准星从中心移动到目标点需要的鼠标移动量. 如果我们调整鼠标灵敏度, 那么移动就不准确了, 假如当前鼠标灵敏度是 4, 需要的移动距离就是 b×2/4

我们设置鼠标灵敏度为 1, 这时候测得 一周移动量 a 可以认为是一个基准, 我们称之为 基准移动量, 后续如果变化了鼠标灵敏度, 则移动距离就是 基准移动量 a 除以 鼠标灵敏度. a×1 基本等于上面的 b×2, 有些许误差是正常的

另外, 我测得 Apex 内, 调整 FOV 的大小, 对 基准移动量 a 是没有影响的

关于 ADS 鼠标灵敏度加成

游戏内 鼠标灵敏度 2 / ADS 鼠标灵敏度加成 1 和 鼠标 DPI 都不变的情况下, 测得 游戏内水平旋转 360° 对应鼠标水平移动的像素

  • 腰射: 8400, 8400×2可以认为是 基准移动量
  • 一倍镜开镜: 11300
  • 二倍镜开镜: 20700
  • 三倍镜开镜: 32100

上面我们讨论的鼠标移动量都是基于腰射的, 如果我们用倍镜时, 鼠标移动量是不是会发生变化? 毕竟测得的一周移动量相差这么大

其实没有关系, 因为开镜后, FOV 视角范围其实会变化, 当 ADS 鼠标灵敏度为 1 时, 我们根据腰射 FOV 和对应基准移动量算出来的鼠标移动量 和 当前倍镜下的 FOV 和当前倍镜开镜后对应的一周移动量计算出来的鼠标移动距离, 是一致的

当我们调整 ADS 不为 1 后, 移动反而会不准. 调小会导致移动变慢, 但还是能稳定在目标点, 调大会无限左右横跳

更精准的控制鼠标设计时的稳定性

鼠标移动过程分析

将瞄准距离修改为 1000, 用一把没有子弹的武器, 我们来分析鼠标从一个点跳到目标点的过程

将显示和瞄准都打开, 鼠标移动到离目标较远的地方, 按下鼠标直到鼠标稳定在目标的瞄准点上. 观察这个过程, 可以很清晰的看到, 鼠标在到达目标点时, 发生了震荡, 超过目标点, 然后又回来, 来回几次, 幅度逐渐减小, 最后稳定在目标点上

效果展示 中期 Python Apex YOLO V5 鼠标移动 震荡现象

为什么会出现这样的情况? 照我的理解, 鼠标移动函数调用后, 鼠标应该从当前点消失, 然后在目标点出现, 对应画面也应该很突兀的变化一次, 而不是像现在一样, 鼠标移动和画面变化都是一步步逐渐完成的. 为了看的清晰, 我在 while True 循环末尾加了 100ms 延迟, 延迟加在了每波循环的最后, 所以不会对循环内的截图加检测加跳跃造成影响, 只是将每次循环之间的时间分割开了

效果展示 中期 Python Apex YOLO V5 鼠标移动 震荡现象拆解

通过放慢循环流程, 发现一个问题. 鼠标移动指令是直接从当前点跳到目标点, 但实际效果却是无法直接一步跳到位, 在游戏外我们测试过位置是准的, 在游戏内却只能说是向目标点靠近了一步, 移动鼠标的两个参数, 更像是指定了一个方向给出了一个力度, 力度大跳跃的距离就远, 力度小距离就近. 而且很重要的一点, 这个跳跃不是瞬移过去的而是飘过去的, 所以这个跳跃其实是耗时的. 所以, 在游戏内从一个点跳到另一个点, 其实是分拆多次实现的

再分析为什么会震荡, 我们认为跳跃是瞬移的, 但实际上跳跃有一个移动的过程. 循环中的一个流程结束后, 代码认为跳跃已经完成, 点已经在目标点了, 但事实上, 点还在路上, 还在从某点到目标点的中途, 并且还没有落地, 还会继续往目标方向走一段距离. 但是代码已经开始下一个循环, 截图, 截到的是还没完成的上一个跳跃过程, 截图结束后, 上一个跳跃还会继续走, 但是本轮截图后计算出来的新的跳跃力度, 却不考虑上一次跳跃还会继续走这个情况, 所以两者叠加, 导致跳跃超过了目标点. 最终形成了震荡

如何消减鼠标震荡

介于目前我只知道这一种在游戏内控制鼠标移动的方式, 所以没有办法在换鼠标移动方法上着手, 只能想办法消减震荡了

据说有一种叫做 FOV 一帧锁 的技术, 具体我不太清楚, 这种的话, 应该不会产生震荡, 也就不需要 PID 控制了

这就要看老哥们都在用的 PID 线性控制了

但是常规 PID 需要有输入有输出, 在本工程里, 输入就是计算出来的水平方向和垂直方向的移动距离, 输出不明显, 因为鼠标移动并没有对应的返回值, 所以我们只能认为下一轮循环时, 计算出来的移动距离就是上一轮的输出了

在我理解来看, PID 控制需要系统非常稳定, 即系统的入参和出参有固定的某种关系

在我的机器上, 每轮(截图加检测加移动)循环耗时不稳定, 少的时候可能只有 8ms, 多的时候能到 25ms, 这就导致不能认定系统的入参和出参具有固定的某种关系, 所以这种情况下 PID 可能并不适用, 所以我决定暂时放弃对 PID 的应用与调试

卡尔曼滤波器预测目标轨迹

利用卡尔曼滤波与Opencv进行目标轨迹预测
PySource - Kalman filter, predict the trajectory of an Object

我不清楚其中的原理的, 只是大概知道是根据上一帧的状态预测下一帧的轨迹

找了个预测橘子位置的案例, 然后做了个预测来回摆动的小球的例子, 接着就改吧改吧整合进来了

有一定的效果, 但是也并不好, 而且会导致鼠标移动很不稳定, 尤其是前几枪

如何验证预测效果

B 站搜索 社区服练枪, 照着教程安装好社区服和练枪 Mod, 选好武器, 设置 假人红甲, 假人最快速度, 击中填满弹夹 和 无限训练时间, 然后选训练中的第一项, 单独假人来回移动训练, 就可以用来验证和优化卡尔曼滤波器参数了

因为社区服和正式服的假人差距有点大, 需要针对社区服的假人再做一波训练, 不然识别率很低

模型层面的优化

训练模型要考虑到各种尽可能多的情况. 通常在测试过程中, 发现模型的缺陷, 然后再做针对性的训练, 会有很大的提升

  • 常规数据集
  • 距离影响, 截图时的不同距离, 最远 100 米左右应该就足够了
  • 假背景影响, 比如说地图里某些位置展示捍卫者的旗
  • 瞄准镜影响, 数据集要包含各种倍数的瞄准镜瞄准效果, 比如 4-10 倍金镜, 中心有两条大绿线, 会影响推理结果
  • 攻击特效影响, 被击中时, 受害者身上会泛光, 滋崩尤其严重, 打中人时全是光, 受害者都看不到了. 数据集要包含各种被攻击的特效
  • 半身位影响, 有没有掩体也有很大的影响

以上影响因素, 做笛卡尔积, 相互混合做出来的数据集, 训练的效果才会更好

瞄准效果

当前效果

  • 固定不动时, 满配三倍 R301/平行/哈沃克, 均可80米稳定打到假人, 100米偶尔也能一梭子打倒
  • 自身移动和目标移动会不精准

讲真, 这个程度已经可以在实战中使用了, 20 米内固定靶或轻微移动靶, 应该是有点看头的

各种层面的多种优化方法, 经过不断尝试, 验证, 循环, 最终得来当前的瞄准效果

目前欠缺部分

我知道大佬们还会加入 PID 控制, 用于调整自身和对手移动时瞄准的偏差, 但我目前还不会写 PID

卡尔曼滤波器预测轨迹, 效果还不太行

第四阶段 实战

工程源码

相关资源

Python Apex YOLO V5 6.2 目标检测 全过程记录 百度网盘

本文涉及的所有相关资源都已经汇总在上面的百度网盘链接里了

grab.for.apex.dummy.py

import timeimport cv2
import pynput
import winsoundfrom toolkit import Monitordef keyboard():region = (3440 // 7 * 3, 1440 // 3, 3440 // 7, 1440 // 3)def release(key):if key == pynput.keyboard.Key.end:return Falseelif key == pynput.keyboard.KeyCode.from_char('f'):name = f'D:\\resource\\develop\\python\\dataset.yolo.v5\\apex\\dummy\\data\\images\\{int(time.time())}.png'print(name)# img = Monitor.grab(region)# mss.tools.to_png(img.rgb, img.size, output=name)img = Monitor.grab(region, convert=True)cv2.imwrite(name, img)winsound.Beep(800, 200)with pynput.keyboard.Listener(on_release=release) as k:k.join()keyboard()

toolkit.py

import os.path
import timeimport cv2
import d3dshot
import mss
import numpy as np
import torch
from win32api import GetSystemMetrics
from win32con import SRCCOPY, SM_CXSCREEN, SM_CYSCREEN
from win32gui import GetDesktopWindow, GetWindowDC, DeleteObject, ReleaseDC
from win32ui import CreateDCFromHandle, CreateBitmapfrom models.common import DetectMultiBackend
from utils.augmentations import letterbox
from utils.general import non_max_suppression, scale_coords, xyxy2xywh, check_img_size
from utils.plots import Annotator, colors
from utils.torch_utils import select_device# dxgi = d3dshot.create(capture_output="numpy")
dxgi = None
sct = mss.mss()class Monitor:@staticmethoddef resolution():"""显示分辨率"""w = GetSystemMetrics(SM_CXSCREEN)h = GetSystemMetrics(SM_CYSCREEN)return w, h@staticmethoddef center():"""屏幕中心点"""w, h = Monitor.resolution()return w // 2, h // 2@staticmethoddef mss(region):"""region: tuple, (left, top, width, height)pip install mss"""left, top, width, height = regionreturn sct.grab(monitor={'left': left, 'top': top, 'width': width, 'height': height})@staticmethoddef win(region):"""region: tuple, (left, top, width, height)conda install pywin32, 用 pip 装的一直无法导入 win32ui 模块, 找遍各种办法都没用, 用 conda 装的一次成功"""left, top, width, height = regionhWin = GetDesktopWindow()hWinDC = GetWindowDC(hWin)srcDC = CreateDCFromHandle(hWinDC)memDC = srcDC.CreateCompatibleDC()bmp = CreateBitmap()bmp.CreateCompatibleBitmap(srcDC, width, height)memDC.SelectObject(bmp)memDC.BitBlt((0, 0), (width, height), srcDC, (left, top), SRCCOPY)array = bmp.GetBitmapBits(True)DeleteObject(bmp.GetHandle())memDC.DeleteDC()srcDC.DeleteDC()ReleaseDC(hWin, hWinDC)img = np.frombuffer(array, dtype='uint8')img.shape = (height, width, 4)return img@staticmethoddef d3d(region=None):"""region: tuple, (left, top, width, height)因为 D3DShot 在 Python 3.9 里会和 pillow 版本冲突, 所以使用大佬修复过的版本来替代pip install git+https://github.com/fauskanger/D3DShot#egg=D3DShot"""if region:left, top, width, height = regionreturn dxgi.screenshot((left, top, left + width, top + height))else:return dxgi.screenshot()@staticmethoddef grab(region=None, mss=False, win=False, d3d=False, convert=False):"""region: tuple, (left, top, width, height)convert: 是否转换为 opencv 需要的 numpy BGR 格式, 转换结果可直接用于 opencv"""# 补全范围if not region:w, h = Monitor.resolution()region = 0, 0, w, h# 范围截图img = Noneif mss:img = Monitor.mss(region)elif win:img = Monitor.win(region)elif d3d:img = Monitor.d3d(region)if img is None:img = Monitor.mss(region)mss = True# 图片转换if convert:if mss:img = cv2.cvtColor(np.array(img), cv2.COLOR_BGRA2BGR)elif win:img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)elif d3d:img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)  # 截图原本就是3通道的, opencv 可以直接显示, 只需要 np.ascontiguousarray(img) 处理就可以了return imgclass KalmanFilter:kf = cv2.KalmanFilter(4, 2)kf.measurementMatrix = np.array([[1, 0, 0, 0], [0, 1, 0, 0]], np.float32)kf.transitionMatrix = np.array([[1, 0, 1, 0], [0, 1, 0, 1], [0, 0, 1, 0], [0, 0, 0, 1]], np.float32)def predict(self, point):x, y = pointmeasured = np.array([[np.float32(x)], [np.float32(y)]])self.kf.correct(measured)predicted = self.kf.predict()px, py = int(predicted[0]), int(predicted[1])return px, pyclass Detector:def __init__(self, weights):self.weights = weightsself.source = 'data/images'  # file/dir/URL/glob, 0 for webcamself.data = 'data/coco128.yaml'  # dataset.yaml pathself.imgsz = (640, 640)  # inference size (height, width)self.conf_thres = 0.25  # confidence thresholdself.iou_thres = 0  # NMS IOU thresholdself.max_det = 1000  # maximum detections per imageself.device = ''  # cuda device, i.e. 0 or 0,1,2,3 or cpuself.view_img = False  # show resultsself.save_txt = False  # save results to *.txtself.save_conf = False  # save confidences in --save-txt labelsself.save_crop = False  # save cropped prediction boxesself.nosave = False  # do not save images/videosself.classes = None  # filter by class: --class 0, or --class 0 2 3, 数字, 需要自己将类别转成类别索引self.agnostic_nms = False  # class-agnostic NMSself.augment = False  # augmented inferenceself.visualize = False  # visualize featuresself.update = False,  # update all modelsself.project = 'runs/detect'  # save results to project/nameself.name = 'exp'  # save results to project/nameself.exist_ok = False  # existing project/name ok, do not incrementself.line_thickness = 2  # bounding box thickness (pixels)self.hide_labels = False  # hide labelsself.hide_conf = False  # hide confidencesself.half = False  # use FP16 half-precision inferenceself.dnn = False  # use OpenCV DNN for ONNX inference# 加载模型self.device = select_device(self.device)self.model = DetectMultiBackend(self.weights, device=self.device, dnn=self.dnn, data=self.data, fp16=self.half)# print(f'设备:{self.device.type}, 模型:{self.model.weights}')self.stride, self.names, self.pt = self.model.stride, self.model.names, self.model.ptself.imgsz = check_img_size(self.imgsz, s=self.stride)  # check image sizebs = 1self.model.warmup(imgsz=(1 if self.pt else bs, 3, *self.imgsz))  # warmupdef detect(self, region, classes=None, image=False, label=True, confidence=True):# 截图和转换t1 = time.perf_counter_ns()# 截屏范围 region = (left, top, width, height)img0 = Monitor.grab(region, convert=True)t2 = time.perf_counter_ns()# 检测aims = []im = letterbox(img0, self.imgsz, stride=self.stride, auto=self.pt)[0]im = im.transpose((2, 0, 1))[::-1]  # HWC to CHW, BGR to RGBim = np.ascontiguousarray(im)im = torch.from_numpy(im).to(self.device)im = im.half() if self.model.fp16 else im.float()  # uint8 to fp16/32im /= 255  # 0 - 255 to 0.0 - 1.0if len(im.shape) == 3:im = im[None]  # expand for batch dim# Inference# visualize = increment_path(save_dir / Path(path).stem, mkdir=True) if visualize else Falsepred = self.model(im, augment=self.augment, visualize=self.visualize)pred = non_max_suppression(pred, self.conf_thres, self.iou_thres, self.classes, self.agnostic_nms, max_det=self.max_det)det = pred[0]annotator = Noneif image:annotator = Annotator(img0, line_width=self.line_thickness, example=str(self.names))if len(det):im0 = img0# self.model.names, 里面是 coco 的 80 个 class# 如果是自己训练的 .pt, 则 hasattr(self.model, 'module') 为 True, 且自己的 class 在 self.model.module.names 中# 如果是转换的 .engine, 则 hasattr(self.model, 'module') 为 False, 没有 names, 应该直接使用 class 序号self.names = self.model.module.names if hasattr(self.model, 'module') else self.model.names  # get class namesdet[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round()for *xyxy, conf, cls in reversed(det):c = int(cls)  # integer classclazz = self.names[c] if not self.weights.endswith('.engine') else str(c)  # 类别if classes and clazz not in classes:continue# 屏幕坐标系下, 框的 ltwh 和 xysl = int(region[0] + xyxy[0])st = int(region[1] + xyxy[1])sw = int(xyxy[2] - xyxy[0])sh = int(xyxy[3] - xyxy[1])sx = int(sl + sw / 2)sy = int(st + sh / 2)# 截图坐标系下, 框的 ltwh 和 xygl = int(xyxy[0])gt = int(xyxy[1])gw = int(xyxy[2] - xyxy[0])gh = int(xyxy[3] - xyxy[1])gx = int((xyxy[0] + xyxy[2]) / 2)gy = int((xyxy[1] + xyxy[3]) / 2)# confidence 置信度aims.append((clazz, conf, (sx, sy), (gx, gy), (sl, st, sw, sh), (gl, gt, gw, gh)))if image:label2 = (f'{clazz} {conf:.2f}' if confidence else f'{clazz}') if label else Noneannotator.box_label(xyxy, label2, color=colors(0, True))# 下面是自己写的给框中心画点, 在 Annotator 类所在的 plots.py 中的 box_label 方法下添加如下方法"""def circle(self, center, radius, color, thickness=None, lineType=None, shift=None):cv2.circle(self.im, center, radius, color, thickness=thickness, lineType=lineType, shift=shift)""""""cx = int((xyxy[0] + xyxy[2]) / 2)cy = int((xyxy[1] + xyxy[3]) / 2)annotator.circle((cx, cy), 1, colors(6, True), 2)"""t3 = time.perf_counter_ns()print(f'截图:{t2 - t1}ns, {round((t2 - t1) / 1000000)}ms, 检测:{round((t3 - t2) / 1000000)}ms, 总计:{round((t3 - t1) / 1000000)}ms, 数量:{len(aims)}/{len(det)}')return (aims, annotator.result()) if image else aimsdef label(self, path):img0 = cv2.imread(path)im = letterbox(img0, self.imgsz, stride=self.stride, auto=self.pt)[0]im = im.transpose((2, 0, 1))[::-1]  # HWC to CHW, BGR to RGBim = np.ascontiguousarray(im)im = torch.from_numpy(im).to(self.device)im = im.half() if self.model.fp16 else im.float()  # uint8 to fp16/32im /= 255  # 0 - 255 to 0.0 - 1.0if len(im.shape) == 3:im = im[None]  # expand for batch dimpred = self.model(im, augment=self.augment, visualize=self.visualize)pred = non_max_suppression(pred, self.conf_thres, self.iou_thres, self.classes, self.agnostic_nms, max_det=self.max_det)det = pred[0]result = []if len(det):im0 = img0gn = torch.tensor(im0.shape)[[1, 0, 1, 0]]  # normalization gain whwhdet[:, :4] = scale_coords(im.shape[2:], det[:, :4], im0.shape).round()for *xyxy, conf, cls in reversed(det):xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist()c = int(cls)  # integer classresult.append((c, xywh))if result:directory = os.path.dirname(path)filename = os.path.basename(path)basename, ext = os.path.splitext(filename)name = os.path.join(directory, basename + '.txt')print(name)with open(name, 'w') as file:for item in result:index, xywh = itemfile.write(f'{index} {xywh[0]} {xywh[1]} {xywh[2]} {xywh[3]}\n')

test.detect.show.realtime.py

import timeimport cv2
from win32con import HWND_TOPMOST, SWP_NOMOVE, SWP_NOSIZE
from win32gui import FindWindow, SetWindowPosfrom toolkit import Detectorregion = (3440 // 7 * 3, 1440 // 3, 3440 // 7, 1440 // 3)
weight = 'yolov5s.pt'
# weight = 'yolov5s.engine'
# weight = 'model.for.apex.dummy.pt'
# weight = 'model.for.apex.dummy.engine'
detector = Detector(weight)title = 'Realtime ScreenGrab Detect'
t = time.time()
while True:_, img = detector.detect(image=True, region=region)cv2.namedWindow(title, cv2.WINDOW_AUTOSIZE)cv2.putText(img, f'{int((time.time() - t) * 1000)}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (255, 255, 255), 1)cv2.imshow(title, img)# 寻找窗口, 设置置顶hwnd = FindWindow(None, title)SetWindowPos(hwnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)t3 = time.time()k = cv2.waitKey(1)  # 0:不自动销毁也不会更新, 1:1ms延迟销毁t = time.time()if k % 256 == 27:# ESC 关闭窗口cv2.destroyAllWindows()exit('ESC ...')

test.measure.palstance.py

import ctypes
import multiprocessing
from multiprocessing import Process
import pynput
from win32api import GetCursorPosshift = 'shift'
point = 'point'
total = 'total'
vertical = 'vertical'
horizontal = 'horizontal'def keyboard(data):try:driver = ctypes.CDLL(r'mouse.device.lgs.dll')ok = driver.device_open() == 1if not ok:print('初始化失败, 未安装lgs/ghub驱动')except FileNotFoundError:print('初始化失败, 缺少文件')def move(x, y, absolute=False):if ok:if (x == 0) & (y == 0):returnmx, my = x, yif absolute:ox, oy = GetCursorPos()mx = x - oxmy = y - oydriver.moveR(mx, my, True)def press(key):if key == pynput.keyboard.Key.shift:data[shift] = Truedef release(key):if key == pynput.keyboard.Key.shift:data[shift] = Falseelse:x, y = 0, 0if key == pynput.keyboard.Key.end:return Falseelif key == pynput.keyboard.Key.up:y = -100elif key == pynput.keyboard.Key.down:y = 100elif key == pynput.keyboard.Key.left:x = -100elif key == pynput.keyboard.Key.right:x = 100elif key == pynput.keyboard.Key.enter:data[vertical] = 0data[horizontal] = 0if x != 0:if data[shift]:x = x // 10data[horizontal] += xif y != 0:if data[shift]:y = y // 10data[vertical] += ymove(x, y)print(f'水平:{abs(data[horizontal])}, 垂直:{abs(data[vertical])}')with pynput.keyboard.Listener(on_press=press, on_release=release) as k:k.join()if __name__ == '__main__':multiprocessing.freeze_support()manager = multiprocessing.Manager()data = manager.dict()data[shift] = Falsedata[horizontal] = 0data[vertical] = 0pk = Process(target=keyboard, args=(data,))pk.start()pk.join()

aimbot.for.apex.dummy.py

import ctypes
import math
import multiprocessing
import time
from multiprocessing import Process
import cv2
import pynput
from win32gui import GetCursorPos, FindWindow, SetWindowPos
from win32con import HWND_TOPMOST, SWP_NOMOVE, SWP_NOSIZE
import winsound
from simple_pid import PID  # pip install simple-pidfov = 'fov'
end = 'end'
box = 'box'
aim = 'aim'
show = 'show'
view = 'view'
fire = 'fire'
head = 'head'
size = 'size'
heads = {'head', '1'}
bodies = {'body', '0'}
region = 'region'
center = 'center'
radius = 'radius'
roundh = 'roundh'
roundv = 'roundv'
weights = 'weights'
predict = 'predict'
confidence = 'confidence'
sensitivity = 'sensitivity'
init = {center: None,  # 屏幕中心点fov: 110,  # 游戏内的 FOVroundh: 16420,  # 游戏内以鼠标灵敏度为1测得的水平旋转360°对应的鼠标移动距离, 多次测量验证. 经过测试该值与FOV无关. 移动像素理论上等于该值除以鼠标灵敏度roundv: 7710 * 2,  # 垂直, 注意垂直只能测一半, 即180°范围, 所以结果需要翻倍sensitivity: 2,  # 当前游戏鼠标灵敏度radius: 50,  # 瞄准生效半径weights: 'model.for.apex.dummy.engine',  # 权重文件size: 400,  # 截图的尺寸confidence: 0.5,  # 置信度, 低于该值的认为是干扰region: None,  # 截图范围end: False,  # 退出标记, Endbox: False,  # 显示开关, Upshow: False,  # 显示状态aim: False,  # 瞄准开关, Downfire: False,  # 开火状态view: False,  # 预瞄状态, F, 手枪狙击枪可提前预瞄一下head: False,  # 切换头和身体, Rightpredict: False,  # 准星跳目标点/预瞄点, Left
}def mouse(data):def down(x, y, button, pressed):if button == pynput.mouse.Button.left:data[fire] = pressedwith pynput.mouse.Listener(on_click=down) as m:m.join()def keyboard(data):def press(key):if key == pynput.keyboard.KeyCode.from_char('f'):data[view] = Truedef release(key):if key == pynput.keyboard.Key.end:# 结束程序data[end] = Truewinsound.Beep(400, 200)return Falseelif key == pynput.keyboard.KeyCode.from_char('f'):data[view] = Falseelif key == pynput.keyboard.Key.up:data[box] = not data[box]winsound.Beep(800 if data[box] else 400, 200)elif key == pynput.keyboard.Key.down:data[aim] = not data[aim]winsound.Beep(800 if data[aim] else 400, 200)elif key == pynput.keyboard.Key.left:data[predict] = not data[predict]winsound.Beep(800 if data[predict] else 600, 200)elif key == pynput.keyboard.Key.right:data[head] = not data[head]winsound.Beep(800 if data[head] else 600, 200)with pynput.keyboard.Listener(on_release=release, on_press=press) as k:k.join()def aimbot(data):# 为了防止因多进程导致的重复加载问题出现导致启动变慢, 把耗时较多的操作和其他涉及到 toolkit 的操作都放在同一个进程中from toolkit import Detector, Monitor, KalmanFilterdata[center] = Monitor.center()c1, c2 = data[center]data[region] = c1 - data[size] // 2, c2 - data[size] // 2, data[size], data[size]detector = Detector(data[weights])kf = KalmanFilter()try:driver = ctypes.CDLL('logitech.driver.dll')ok = driver.device_open() == 1if not ok:print('初始化失败, 未安装lgs/ghub驱动')except FileNotFoundError:print('初始化失败, 缺少文件')def move(x, y, absolute=False):if (x == 0) & (y == 0):returnmx, my = x, yif absolute:ox, oy = GetCursorPos()mx = x - oxmy = y - oydriver.moveR(mx, my, True)def oc():ac, _ = data[center]return ac / math.tan((data[fov] / 2 * math.pi / 180))def rx(x):angle = math.atan(x / oc()) * 180 / math.pireturn int(angle * data[roundh] / data[sensitivity] / 360)def ry(y):angle = math.atan(y / oc()) * 180 / math.pireturn int(angle * data[roundv] / data[sensitivity] / 360)def inner(point):"""判断该点是否在准星的瞄准范围内"""a, b = data[center]x, y = pointreturn math.pow(x - a, 2) + math.pow(y - b, 2) < math.pow(data[radius], 2)def highest(targets):"""选最高的框"""if len(targets) == 0:return Noneindex = 0maximum = 0for i, item in enumerate(targets):height, sc, _, _ = itemif maximum == 0:index = imaximum = heightelse:if height > maximum:index = imaximum = heightreturn targets[index]def nearest(targets):"""选距离准星最近的框"""if len(targets) == 0:return Nonecx, cy = data[center]index = 0minimum = 0for i, item in enumerate(targets):_, sc, _, _ = itemsx, sy = scdistance = math.pow(sx - cx, 2) + math.pow(sy - cy, 2)if minimum == 0:index = iminimum = distanceelse:if distance < minimum:index = iminimum = distancereturn targets[index]def follow(targets, last):"""从 targets 里选距离 last 最近的"""if len(targets) == 0 or last is None:return None_, lsc, _, _ = lastlx, ly = lscindex = 0minimum = 0for i, item in enumerate(targets):_, sc, _, _ = itemsx, sy = scdistance = math.pow(sx - lx, 2) + math.pow(sy - ly, 2)if minimum == 0:index = iminimum = distanceelse:if distance < minimum:index = iminimum = distancereturn targets[index]pidx = PID(1, 0, 0, setpoint=0, sample_time=0.001)pidx.output_limits = (-100, 100)last = None  # 上次瞄准的目标winsound.Beep(800, 200)title = 'Realtime ScreenGrab Detect'while True:# 检测是否需要退出if data[end]:break# 检测是否需要推测, 如需推测则推测if data[box] or data[aim]:t = time.time()aims, img = detector.detect(region=data[region], classes=heads.union(bodies), image=True, label=True)cv2.putText(img, f'{int((time.time() - t) * 1000)}', (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.75, (255, 255, 255), 1)else:continue# 拿到瞄准目标targets = []# class, confidence, screen target center, grab target center, screen target rectangle, grab target rectanglefor clazz, conf, sc, gc, sr, gr in aims:# 置信度过滤if conf < data[confidence]:continue# 拿到指定的分类_, _, _, height = srif data[head]:if clazz in heads:targets.append((height, sc, gc, gr))else:if clazz in bodies:cx, cy = sctargets.append((height, (cx, cy - (height // 2 - height // 3)), gc, gr))  # 检测身体的时候, 因为中心位置不太好, 所以对应往上调一点# 筛选该类中最符合的目标# 尽量跟一个目标, 不要来回跳, 直到未检测到目标, 就打断本次跟踪# 有目标就跟目标, 没目标就选距离准星最近的target = Noneif len(targets) != 0:target = follow(targets, last) if last else nearest(targets)# 重置上次瞄准的目标last = target# 解析目标里的信息predicted = Noneif target:_, sc, gc, gr = targetsx, sy = sc  # 当前截图中目标所在点gl, gt, gw, gh = grpredicted = kf.predict(sc)  # 下张截图中可能的目标所在点(预测)px, py = predictedif abs(px - sx) > 50 or abs(py - sy) > 50:predicted = scdx = predicted[0] - sxdy = predicted[1] - sy# 计算移动距离, 展示预瞄位置if data[box]:px1 = gl + dxpy1 = gt + dypx2 = px1 + gwpy2 = py1 + ghcv2.rectangle(img, (px1, py1), (px2, py2), (0, 256, 0), 2)# 检测瞄准开关if data[aim] and (data[view] or data[fire]):if target:_, sc, gc, _ = targetif inner(sc):# 计算要移动的像素cx, cy = data[center]  # 准星所在点(屏幕中心)sx, sy = sc  # 目标所在点# predicted  # 目标将在点if data[predict]:x = int((predicted[0] - cx))y = int((predicted[1] - cy))else:x = sx - cxy = sy - cyox = rx(x)oy = ry(y)px = int(pidx(ox))px = int(ox)py = int(oy)print(f'目标:{sc}, 预测:{predicted}, 移动像素:{(x, y)}, FOV:{(ox, oy)}, PID:{(px, py)}')move(px, py)# 检测显示开关if data[box]:data[show] = Truecv2.namedWindow(title, cv2.WINDOW_AUTOSIZE)cv2.imshow(title, img)SetWindowPos(FindWindow(None, title), HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE)cv2.waitKey(1)if not data[box] and data[show]:data[show] = Falsecv2.destroyAllWindows()if __name__ == '__main__':multiprocessing.freeze_support()  # windows 平台使用 multiprocessing 必须在 main 中第一行写这个manager = multiprocessing.Manager()data = manager.dict()  # 创建进程安全的共享变量data.update(init)  # 将初始数据导入到共享变量# 将键鼠监听和压枪放到单独进程中跑pa = Process(target=aimbot, args=(data,))pa.start()pm = Process(target=mouse, args=(data,))pm.start()pk = Process(target=keyboard, args=(data,))pk.start()pk.join()  # 不写 join 的话, 使用 dict 的地方就会报错 conn = self._tls.connection, AttributeError: 'ForkAwareLocal' object has no attribute 'connection'pm.terminate()  # 鼠标进程无法主动监听到终止信号, 所以需强制结束pa.terminate()

label.for.apex.py

import osfrom toolkit import Detectordetector = Detector('model.for.apex.dummy.engine')directory = r'D:\resource\develop\python\dataset.yolo\apex\dummy\data'
files = os.listdir(directory)
print(f'total files: {len(files)}')
paths = []
for file in files:path = os.path.join(directory, file)if path.endswith('.txt'):continuepaths.append(path)
print(f'image files: {len(paths)}')for i, path in enumerate(paths):print(f'{i + 1}/{len(paths)}')detector.label(path)

Yolov5 5.0 环境 (失败, 但先留着)

yolov5-5.0 下载

用 conda 创建虚拟环境, 下载最新版 yolov5 源码, 解压到 pycharm workspace, 用 pycharm 打开, 选择创建的虚拟环境

# 创建虚拟环境, 建议创建在项目路径下
# Python 3.8 or later with all requirements.txt dependencies installed, including torch>=1.7. To install run:
conda create -p C:\mrathena\develop\workspace\pycharm\yolov5-5.0\venv python=3.8 # -n和-p不能同时设置 ...
# 激活虚拟环境. 我猜 -n 其实就是 -p 的特殊版本, 相当于指定了路径前缀 conda\envs, 两者其实是一样的
conda activate C:\mrathena\develop\workspace\pycharm\yolov5-5.0\venv
# cd 到项目路径, 执行安装依赖包命令
cd C:\mrathena\develop\workspace\pycharm\yolov5-5.0
# 安装依赖包
pip install -r requirements.txt
Building wheel for pycocotools (pyproject.toml) ... error
error: Microsoft Visual C++ 14.0 or greater is required. Get it with "Microsoft C++ Build Tools": https://visualstudio.microsoft.com/visual-cpp-build-tools/

安装的过程中报了个错, 据说是需要使用 VC++14编译工具编译 wheel 文件, 但现在没有安装这个工具

常规的解决方法肯定是安装这个工具, 因为还有其他包可能也存在这个情况. 但是因为对c/c++不熟悉, 百度的结果也是需要安装一大堆东西, 大约6-7G的样子, 可以说把c++开发桌面程序的开发环境都准备好了 … 就为了编译一下这个包 … 感觉太恶心了

好在有老哥提供了编译并安装好的 pycocotools 的拷贝, 我们直接下载解压拷贝到虚拟环境的 Lib/site-packages 中, 就算我们成功安装了

pycocotools 2.0.2 installed copy.rar

为了解决这个问题, 或许也可以使用下面办法

  • 如果 python<=3.8, 可以试试下载下方别人编译好的二进制安装包 pycocotools-windows.whl, 通过 pip install xxx.whl 来安装
  • 如果 python>3.8, 或许得安装c++开发环境/那个1.1G的离线安装文件了, 真恶心

清华源的 pycocotools-windows, 可惜没有3.8以上版本的
非官方的Python扩展包的二进制存档

# 安装 pycocotools 报错后, 使用别人安装好的 pycocotools 替代. 下载解压拷贝到虚拟环境的 Lib/site-packages 中, 重新安装依赖包
pip install -r requirements.txt
# 安装成功

pycharm 中右下角选择 python 解释器, 使用该项目下的虚拟环境

运行 detect.py 测试效果, 报下列错误

AttributeError: Can't get attribute 'SPPF' on <module 'models.common' from 'C:\\mrathena\\develop\\workspace\\pycharm\\yolov5-5.0\\models\\common.py'>

解决方案: 到 6.0 的 /models/common.py 文件中, 找到 class SPPF 拷贝到当前版本的相同文件中, 然后引入 import warnings


什么玩意儿, 一堆问题, 恶心死了

Python Apex YOLO V5 6.2 目标检测 全过程记录相关推荐

  1. Python Apex YOLO V7 main 目标检测 全过程记录

    博文目录 文章目录 环境准备 YOLO V7 main 分支 TensorRT 环境 工程源码 假人权重文件 toolkit.py 测试.实时检测.py grab.for.apex.py label. ...

  2. realsense D455深度相机+YOLO V5结合实现目标检测(二)

    realsense D455深度相机+YOLO V5结合实现目标检测(二) 1.代码来源 2.环境配置 3.代码分析: 3.1 主要展示在将detect.py转换为realsensedetect.py ...

  3. YOLOv3目标检测全过程记录

    前提: 软硬件环境: python 3.6.5 Ubuntu18.04 LTS PyTorch 1.1.0 CUDA 10.0 cudnn 7.5.0 GPU: NVIDIA TITAN XP 一  ...

  4. 【目标检测】YOLO v5 吸烟行为识别检测

    提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 YOLO v5 吸烟行为目标检测模型:计算机配置.制作数据集.训练.结果分析和使用 前言 相关连接(look评论) 一.计算机配置 p ...

  5. 独家 | 在树莓派+Movidius NCS上运用YOLO和Tiny-YOLO完成目标检测(附代码下载)

    作者:Adrian Rosebrock 翻译:吴振东 校对:郑滋 本文约5000字,建议阅读10+分钟 本文教你如何在树莓派和Movidius神经加速棒上运用Tiny-YOLO来实现近乎实时的目标检测 ...

  6. Cython——Windows环境下配置faster-rcnn、yolo、ctpn等目标检测框架中Cython文件[cython_nms、bbox、gpu_nms]编译问题解决方案

    问题描述 AttributeError: 'MSVCCompiler' object has no attribute 'compiler_so' ValueError: Buffer dtype m ...

  7. 二十. 在ROS系统上实现基于PyTorch YOLO v5的实时物体检测

    一. 背景介绍 在我前面的博文 十八.在JetsonNano上为基于PyTorch的物体检测网络测速和选型 中,我介绍过在基于Jetson Nano硬件平台和Ubuntu 18.04 with Jet ...

  8. 《You Only Look Once: Unified, Real-Time Object Detection》YOLO一种实时目标检测方法 阅读笔记(未完成版)

    文章目录 1. one-stage与two-stage检测算法 1. 模型过程 1.1 grid cell 1.2 bounding box与confidence score 1.3 类别预测 1.4 ...

  9. OpenCV+YOLO+IP摄像头实现目标检测

    title: OpenCV+YOLO+IP摄像头实现目标检测 前言 学习OpenCV.YOLO到现在我实现了调用本地摄像头使用自己训练的模型进行目标识别,然后想着能不能远程获取视频数据,然后再PC端处 ...

  10. YOLO V1 实时的目标检测 论文翻译

    YOLO V1 实时的目标检测 论文翻译 注:学习记录用 摘要 我们提出了一种新的目标检测方法 YOLO.先前关于目标检测的工作重新使用分类器来执行检测. 相反,我们将目标检测框架作为一个回归问题,以 ...

最新文章

  1. 怎么连接屏幕_手机屏幕坏了也可以操作?这办法学会了再不怕碎屏
  2. 第一次在Linux系统上操作mysql数据库,看完这篇轻松应对
  3. Python示例-Logging
  4. mysql 怎么登陆远程服务器_教你手机怎么远程连接云服务器
  5. 关于slot、slot-scope的指令的一些操作记录
  6. java中的args参数
  7. html中如何实现选择存储路径的功能_Tomcat 路由请求的实现 Mapper
  8. 猫猫学iOS之小知识之xcode6自己主动提示图片插件 KSImageNamed的安装
  9. html怎么打开一个新窗口打开文件,js怎么打开新窗口
  10. 禁用某个程序,试试镜像劫持吧!
  11. 基于51单片机开发板8*8LED矩阵的贪吃蛇程序
  12. JS中的window对象和document对象是什么?有什么区别?
  13. Xshell官方免费版
  14. Visual Studio安装SVN过程及作用
  15. Java 开发中常用的 4 种加密方法。MD5加密工具类测试 base64加密工具类测试 SHA加密工具类测试 BCrypt加密工具类测试
  16. FPGA Altera Remote Update笔记
  17. 在Ubuntu上安装Docker Engine
  18. Ping++通过PCI DSS认证,保护企业用户信息安全
  19. 下载个PDF居然还要密码?想要密码就付费?这我能忍你!Python分分钟解密它!
  20. IPGuard忘记控制台密码处理方法

热门文章

  1. 华为网络篇 传输文件-08
  2. 2022最新软考考试时间已公布
  3. c语言餐桌游戏,教会你这十款酒桌游戏,让你在朋友圈稳站“C”位!
  4. 一张图表,人人都能建立自己的AARRR运营模型
  5. 【WPS文档】Shift+F3:切换英文大小写格式的快捷键
  6. SEFS安全透明加密内核
  7. 什么是大数据?65页PPT+50分钟视频讲解,小白也能看懂
  8. 备战数学建模7-MATLAB数值微积分与方程求解
  9. MCSA 70-740 windows 安装和部署工具汇总学习
  10. Redis常用命令大全