Python Pubg 武器自动识别与压枪 全过程记录
创始人
2024-03-07 09:04:59
0

博文目录

文章目录

  • 环境准备
  • 压枪原理
  • 需求分析
    • 求两张图片的相似度
    • 背包检测 是否在背包界面
    • 武器识别
      • 名称识别 纯白计数法
      • 配件识别 瞄具/枪口/握把/枪托 相似对比法
    • 模式识别 全自动/半自动/单发
    • 姿态识别 站/蹲/爬
    • 余弹识别
    • 激活识别 是否持有武器/一号武器/二号武器 (未完成, 做不下去了)
  • 压枪数据
  • 工程源码
    • 相关资源
    • GitHub
    • cfg.py
    • structure.py
    • toolkit.py
    • pubg.py
    • 分析测试等源码见 GitHub 工程


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

环境准备

参考 该文章 中的 环境准备 部分

压枪原理

在开枪后, 武器准星因后坐力会向上跳起一定幅度, 在下一发子弹射出前, 通过代码控制鼠标下拉, 促使武器准星快速回归原位, 这就是压枪

Pubg 的压枪主要针对的是垂直方向后坐力, 这个后坐力是相当的大, 不同武器配件不同射击姿态对该后坐力也有一定的影响

假设裸配武器的垂直后坐力是固定的, 可以通过执行一组鼠标下移让裸配武器在站立射击时准星基本保持在一条水平线上, 这组鼠标下移的距离就是该武器的基础下压数据

当装备影响垂直后坐力的配件或射击姿态发生改变, 这组下移距离也得跟着改变才能继续保持一条水平线

最终的下移距离=基础下移距离×瞄具影响倍数×枪口影响倍数×握把影响倍数×枪托影响倍数×姿态影响倍数

需求分析

求两张图片的相似度

有两张图片, 如果长宽相同, 相同位置点的颜色相同, 即可认为两张图片完全相同, 其相似度为 1

求相似度之前, 通常我们要对图片做一些处理, 如灰度化/自适应二值化/消除孤立点等, 以突出主体特征, 消除背景干扰

简单实现求两张图片相似度的思路如下

  • 定义相似度的取值范围是 [0, 1], 0 表示完全不同, 1 表示完全相同
  • 如果两张图片宽高不同, 通道数不同, 认为其相似度为 0
  • 遍历每一个点, 记录相同颜色的点的个数, 相同色点数与总点数的比值, 可近似认为是两张图片的相似度(有缺陷)
  • 分块统计相似度, 给块相似度做立方计算, 以扩大块相似度的影响, 立方后的块相似度之和与总块数的比值, 可近似认为是两张图片的最终相似度

通过测试, 效果相当好, 即使是最难看出区别的步枪消音和狙击枪消音都能完美识别

背包检测 是否在背包界面

预先截取背包界面顶部偏左 [背包] 两个字作为基准图, 检测时截取同一位置图与基准图求相似度, 相似度高于 0.9 即可认为当前在背包界面

按下 Tab 键如果立即截图, 则可能背包还没有打开, 而且 Tab 键同时控制背包打开与关闭, 所以不能做成按下 Tab 键, 延迟截图识别

这里可以采用状态机的方式, 定义下面 4 种状态, Tab 键按下触发状态变更, 然后做不同的操作

  • 0: 等待打开背包
  • 1: 背包检测中
  • 2: 武器识别中
  • 3: 武器已识别, 等待关闭背包

常规识别流程大概如下

  • 状态是 0
  • 按下 Tab 键(打开背包), 检测到当前状态是 0, 触发状态变更为 1, 开始检测背包是否成功打开
    • 如果在检测背包过程中按下了 Tab 键(关闭背包), 直接置状态为 0, 背包检测过后不再继续识别武器
  • 当背包成功打开后, 判断状态是否还是 1, 是的话触发状态变更为 2, 开始识别武器, 识别成功后会将武器数据存到内存中
    • 如果在识别武器过程中按下了 Tab 键(关闭背包), 直接置状态为 0, 武器识别过后不保存数据, 不再等待关闭背包
  • 当武器成功识别后, 判断状态是否还是 2, 是的话触发状态变更为 3, 等待关闭背包
  • 按下 Tab 键(关闭背包), 检测到当前状态是 3, 触发状态还原为 0, 等待下一轮识别流程的启动

介于各种原因, 可能打开关闭背包与状态不对应, 所以需要设置一个自动纠错机制, 单次检测背包最多循环 10 次, 超过则放弃操作, 还原状态 0

在这里插入图片描述
在这里插入图片描述

武器识别

检测到在背包界面, 则截取右边两把主武器的部分作为截图, 然后切割武器名称和武器配件部分, 分别识别

在这里插入图片描述

名称识别 纯白计数法

先截相同大小的所有武器的名称图作为基准图, 统计其中纯白色点的个数(通常不会有重复的), 做成字典, 数量为 Key, 名称为 Value

识别时, 从武器大图中切割出武器名称部分, 数纯白点个数, 到字典中直接取出对应名称, 耗时低, 时间复杂度为 O(1)

如果出现两把武器的纯白点个数相同的情况, 只需在对应武器名称上找一个纯白点, 确保该点在其他重复武器上不是纯白, 以此区分

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

配件识别 瞄具/枪口/握把/枪托 相似对比法

先截武器不同部位的所有配件图作为基准图, 按配件类型分类

识别时, 从武器大图中切割出配件图, 遍历同类配件的基准图并与之对比求相似度, 相似度超过 0.9 的即可认为识别成功

瞄具稍有不同, 基准图和切割图只需要瞄具方框的上面一半即可, 因为下半部分会受不同武器的背景影响

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

模式识别 全自动/半自动/单发

射击模式只关心突击步枪和冲锋枪, 他们的标识是一样的, 只有下面4种

识别时, 截取同位置图片, 做灰度化和全局二值化处理, 将颜色大于 230 的都转为 255, 其他转为 0

按照全自动的 5 颗子弹的位置, 在弹体上取 5 个点, 数纯白色的个数, 即可判断出射击模式

在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

姿态识别 站/蹲/爬

在各种背景下截图, 通过灰度化和自适应二值化操作, 分析可以看出, 姿态的描边上有些位置始终都是黑色的, 不受背景影响

每种姿态找几个这种不受背景影响的点, 作为识别姿态的检测点

识别时, 将截取的姿态图处理好, 按照站/蹲/爬的顺序, 遍历提前记录好的监测点, 如果全是黑色则说明当前处于该种姿态

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

余弹识别

弹夹打光后, 剩余子弹数会变成纯红色, 只需找个点在识别时判断其颜色是否为纯红色即可

在这里插入图片描述
在这里插入图片描述

激活识别 是否持有武器/一号武器/二号武器 (未完成, 做不下去了)

Pubg 只有在确实持枪时, 右下角的武器才会被激活

鼠标滚轮滚动/1/2/3/4/5/G(切雷)/F(落地捡枪)/X(收起武器)/Tab(调整位置) 等可触发激活识别

测试发现, 主界面上右下角武器位和主武器只有下面 4 种情况

  • 无武器, 1 号位和 2 号位上都为空
  • 1 号位上显示 1 号武器
  • 1号位上显示 1 号武器, 2 号位上显示 2 号武器
  • 1号位上显示 2 号武器
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    通过截取武器右边的那个序号, 然后做灰度化和自适应二值化处理, 可以将 1 和 2 变成纯黑色, 然后根据提前记录好的几个点, 来区分上面的几种情况

区分好后再根据位置判断哪把武器被激活

通过观察, 还有下面几个特征

  • 如果是空弹夹且激活, 则武器为纯红色, 不受背景色影响
  • 如果是空弹夹且未激活, 则武器为红色半透明, 受背景色影响严重
  • 如果弹夹非空且激活, 则武器为接近纯白色, 背景色有影响
  • 如果弹夹非空且未激活, 则武技为白色半透明, 受背景色影响严重

然后就做不下去了, 不得已先跳过这个判断

压枪数据

来源于下面参考文章与参考工程, 并未细调数据

FPS游戏自动枪械识别+压枪(以PUBG为例)
GitHub PUBGRecognizeAndGunpress

工程源码

目前只适配了 3440×1440 分辨率

判断当前激活的武器, 可以通过按 1 / 2 来指定激活的武器
我希望能实现自动识别, 而非手动指定的效果. 鉴于技术水平不足的原因, 目前还没做到

除此之外的其他功能都基本 OK 了

相关资源

Python Pubg 武器自动识别与压枪 全过程记录 百度网盘

GitHub

python.pubg.weapon.auto.recognize.and.suppress

cfg.py

one = 'one'
two = 'two'
game = 'game'
only = 'only'
semi = 'semi'
auto = 'auto'
name = 'name'
data = 'data'
sight = 'sight'
color = 'color'
point = 'point'
index = 'index'
speed = 'speed'
count = 'count'
armed = 'armed'
empty = 'empty'
stock = 'stock'
stand = 'stand'
squat = 'squat'
prone = 'prone'
weapon = 'weapon'
region = 'region'
points = 'points'
muzzle = 'muzzle'
switch = 'switch'
bullet = 'bullet'
active = 'active'
backpack = 'backpack'
foregrip = 'foregrip'
attitude = 'attitude'
firemode = 'firemode'
interval = 'interval'
ballistic = 'ballistic'# 检测数据
detect = {"3440.1440": {backpack: (936, 78, 80, 40),  # 检测背包是否打开的位置weapon: {region: (2212, 125, 632, 577),  # 一二号武器全截图one: {index: 1,point: (16, 20),  # (y, x), 判断一号武器是否存在的点(纯白色)name: (42, 0, 260, 42),sight: (365, 29, 62, 31),muzzle: (2, 207, 62, 62),foregrip: (138, 207, 62, 62),stock: (568, 207, 62, 62),},two: {index: 2,point: (319, 18),  # (y, x), 判断二号武器是否存在的点(纯白色)name: (42, 308, 260, 42),sight: (365, 335, 62, 31),muzzle: (2, 514, 62, 62),foregrip: (138, 514, 62, 62),stock: (568, 514, 62, 62),},name: {769: 'ACE32',561: 'AKM',511: 'AUG',1269: 'Beryl M762',716: 'G36C',568: 'Groza',309: 'K2',794: 'M16A4',646: 'M416',1360: 'Mk47 Mutant',552: 'QBZ',798: 'SCAR-L',669: 'Mini14',602: 'Mk12',588: 'Mk14',597: 'QBU',494: 'SKS',1627: 'SLR',464: 'VSS',636: 'Crossbow',715: 'DP-28',710: 'M249 ',605: 'MG3',846: 'Mortar',1325: 'Panzerfaust',564: 'DBS',423: 'O12',556: 'S12K',758: 'S1897',737: 'S686',647: 'AWM',872: 'Kar98k',993: 'Lynx AMR',513: 'M24',1611: 'Mosin Nagant',740: 'Win94',934: 'Micro UZI',742: 'MP5K',608: 'MP9',559: 'P90',1207: 'PP-19 Bizon',1567: 'Tommy Gun',880: 'UMP45',624: 'Vector',1917: 'Blue Chip Detector',1553: 'Drone Tablet',1664: 'EMT Gear',843: 'Spotter Scope',947: 'Tactical Pack',},},attitude: {  # 姿态识别region: (1374, 1312, 66, 59),stand: [(37, 33), (37, 28), (17, 28), (20, 17)],  # (y, x), 纯黑色squat: [(19, 39), (20, 51), (36, 13), (41, 28)],prone: [(33, 48), (34, 60), (39, 25), (41, 18)],},firemode: {  # 武器模式识别region: (1649, 1331, 27, 31),points: [(3, 13), (8, 13), (14, 13), (19, 13)]},bullet: (1712, 1324),  # 纯红色则没有子弹active: {  # 识别当前使用的武器序号region: (2810, 1250, 240, 153),one: {  # 主界面右下角一号武器展示位# region: (0, 89, 207, 65),region: (0, 112, 207, 1),1: [(98, 217), (98, 215), (100, 217), (102, 217), (104, 217), (106, 217)],  # 一号武器展示位展示1号武器2: [(99, 215), (97, 215), (97, 217), (97, 219), (97, 217), (102, 217), (106, 215), (106, 220)],  # 一号武器展示位展示2号武器},two: {# region: (0, 10, 207, 65),region: (0, 33, 207, 1),2: [(20, 215), (18, 215), (18, 217), (18, 219), (20, 219), (22, 219), (27, 215), (27, 220)],  # 主界面右下角二号武器展示位},},},"2560.1440": {},"2560.1080": {},"1920.1080": {}
}# 翻译数据
translation = {'ACE32': 'ACE32','AKM': 'AKM','AUG': 'AUG','Beryl M762': 'Beryl M762','G36C': 'G36C','Groza': 'Groza','K2': 'K2','M16A4': 'M16A4','M416': 'M416','Mk47 Mutant': 'Mk47 Mutant','QBZ': 'QBZ','SCAR-L': 'SCAR-L','Mini14': 'Mini14','Mk12': 'Mk12','Mk14': 'Mk14','QBU': 'QBU','SKS': 'SKS','SLR': '自动装填步枪','VSS': 'VSS','Crossbow': '十字弩','DP-28': 'DP-28','M249 ': 'M249 ','MG3': 'MG3','Mortar': '迫击炮','Panzerfaust': '铁拳火箭筒','DBS': 'DBS','O12': 'O12','S12K': 'S12K','S1897': 'S1897','S686': 'S686','AWM': 'AWM','Kar98k': 'Kar98k','Lynx AMR': 'Lynx AMR','M24': 'M24','Mosin Nagant': '莫辛纳甘步枪','Win94': 'Win94','Micro UZI': '微型 UZI','MP5K': 'MP5K','MP9': 'MP9','P90': 'P90','PP-19 Bizon': 'PP-19 Bizon','Tommy Gun': '汤姆逊冲锋枪','UMP45': 'UMP45','Vector': 'Vector','Blue Chip Detector': '蓝色晶片探测器','Drone Tablet': '无人机控制器','EMT Gear': '应急处理装备','Spotter Scope': '观测镜','Tactical Pack': '战术背包','Angled Foregrip': '直角前握把','Haalfgrip': '半截式握把','Laser Sight': '激光瞄准器','Lightweight Grip': '轻型握把','Quiver': '箭袋','Thumbgrip': '拇指握把','Vertical Foregrip': '垂直握把','Choke SG': '扼流圈','Compensator AR': '后座补偿器','Compensator SMG': '枪口补偿器','Compensator SR': '后座补偿器','Duckbill SG': '鸭嘴枪口','Flash Hider AR': '消焰器','Flash Hider SMG': '消焰器','Flash Hider SR': '消焰器','Suppressor AR': '消音器','Suppressor SMG': '消音器','Suppressor SR': '消音器','15x Scope': '15x镜','2x Scope': '2x镜','3x Scope': '3x镜','4x Scope': '4x镜','6x Scope': '6x镜','8x Scope': '8x镜','Holographic Sight': '全息','Red Dot Sight': '红点','Bullet Loops': '子弹袋','Cheek Pad': '托腮板','Folding Stock': '折叠式枪托','Heavy Stock': '重型枪托','Tactical Stock': '战术枪托',
}# 武器数据, 在列表中的武器才会执行压制
weapons = {'M416': {interval: 85,  # 全自动射击间隔attitude: {  # 姿态影响因子stand: 1,squat: 0.75,prone: 0.5,},sight: {  # 瞄具影响因子'2x Scope': 1.9,'3x Scope': 3,'4x Scope': 4,'6x Scope': 6,'Holographic Sight': 1,'Red Dot Sight': 1,},muzzle: {  # 枪口影响因子'Compensator AR': 0.78,'Flash Hider AR': 0.87,'Suppressor AR': 1,},foregrip: {  # 握把影响因子'Angled Foregrip': 1,'Haalfgrip': 0.8,'Laser Sight': 1,'Lightweight Grip': 0.92,'Thumbgrip': 0.92,'Vertical Foregrip': 0.77,},stock: {  # 枪托影响因子'Heavy Stock': 0.9,'Tactical Stock': 0.965,},ballistic: [36, 23, 24, 23, 33, 34, 34, 34, 40, 40, 40, 40, 41, 41, 41, 42, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 46, 50, 51, 50, 51, 50, 50, 50]},"ACE32": {"ballistic": [30,30,30,30,40,40,40,40,46,46,46,46,49,49,49,49,56,56,56,56,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58,58],"interval": 88,"attitude": {"stand": 1,"prone": 0.5,"squat": 0.75},"stock": {"Tactical Stock": 0.97},"foregrip": {"Haalfgrip": 0.8,"Lightweight Grip": 0.92,"Thumbgrip": 0.92,"Angled Foregrip": 1,"Vertical Foregrip": 0.77},"sight": {"none": 1,"2x Scope": 1.8,"3x Scope": 2.8,"4x Scope": 4,"6x Scope": 6},"muzzle": {"Compensator AR": 0.84,"Flash Hider AR": 0.84,"Suppressor AR": 1}},"AKM": {"ballistic": [20,30,30,30,42,42,43,43,46,46,46,47,52,52,53,53,52,53,52,53,52,53,52,53,52,53,52,53,53,54,54,54,53,54,54,54,54,54,54,54,54,54],"interval": 100,"attitude": {"stand": 1,"prone": 0.43,"squat": 0.75},"sight": {"none": 1,"2x Scope": 1.7,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.2},"muzzle": {"Compensator AR": 0.84,"Flash Hider AR": 0.84,"Suppressor AR": 1}},"AUG": {"ballistic": [15,20,20,20,28,28,28,28,38,28,28,28,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34],"interval": 86,"attitude": {"stand": 1,"prone": 0.55,"squat": 0.8},"foregrip": {"Haalfgrip": 0.82,"Lightweight Grip": 0.8,"Thumbgrip": 0.92,"Angled Foregrip": 1,"Vertical Foregrip": 0.8},"sight": {"none": 1,"2x Scope": 1.7,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.1},"muzzle": {"Compensator AR": 0.86,"Flash Hider AR": 0.86,"Suppressor AR": 1}},"DP-28": {"ballistic": [14,24,24,24,37,37,37,37,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50],"interval": 109,"attitude": {"stand": 1,"prone": 0.04,"squat": 0.36},"sight": {"none": 1,"2x Scope": 1.7,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.1}},"G36C": {"ballistic": [12,22,22,22,32,32,32,32,38,38,38,38,43,43,43,43,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48,48],"interval": 86,"attitude": {"stand": 1,"prone": 0.5,"squat": 0.75},"foregrip": {"Haalfgrip": 0.75,"Lightweight Grip": 0.77,"Thumbgrip": 0.92,"Angled Foregrip": 1,"Vertical Foregrip": 0.77},"sight": {"none": 1,"2x Scope": 1.72,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.1},"muzzle": {"Compensator AR": 0.84,"Flash Hider AR": 0.84,"Suppressor AR": 1}},"Groza": {"ballistic": [15,24,24,24,30,30,30,30,30,30,30,30,38,38,38,38,38,38,38,38,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40,40],"interval": 80,"attitude": {"stand": 1,"prone": 0.45,"squat": 0.67},"sight": {"none": 1,"2x Scope": 1.75,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.2},"muzzle": {"Compensator AR": 1,"Flash Hider AR": 1,"Suppressor AR": 1}},"K2": {"ballistic": [15,25,26,26,32,34,34,34,40,40,40,40,40,40,42,42,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,57,58,58,58,58,58],"interval": 43,"attitude": {"stand": 1,"prone": 0.5,"squat": 0.75},"sight": {"none": 1,"2x Scope": 1.75,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.2},"muzzle": {"Compensator AR": 0.84,"Flash Hider AR": 0.84,"Suppressor AR": 1}},"M16A4": {"ballistic": [23,19,19,19,28,29,29,29,26,26,26,27,26,26,26,27,26,26,26,27,26,26,26,27,26,26,26,27,26,26,26,27,26,26,26,27,32,33,33,33],"interval": 125,"attitude": {"stand": 1,"prone": 0.6,"squat": 0.84},"stock": {"Tactical Stock": 1},"sight": {"none": 1,"2x Scope": 1.8,"3x Scope": 2.75,"4x Scope": 3.75,"6x Scope": 5.4},"muzzle": {"Compensator AR": 0.93,"Flash Hider AR": 0.93,"Suppressor AR": 1}},"M249": {"ballistic": [10,18,18,18,18,28,28,28,28,28,20,20,20,20,20,14,14,14,14,14,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16,18,18,16,16,16],"interval": 93.75,"attitude": {"stand": 1,"prone": 0.25,"squat": 0.6},"stock": {"Tactical Stock": 1},"sight": {"none": 1,"2x Scope": 1.7,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.1}},"M4162": {"ballistic": [20,26,32,34,34,34,40,40,40,40,40,40,42,42,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46,46],"interval": 85,"attitude": {"stand": 1,"prone": 0.5,"squat": 0.75},"stock": {"Tactical Stock": 0.965},"foregrip": {"Haalfgrip": 0.77,"Lightweight Grip": 0.77,"Thumbgrip": 0.92,"Angled Foregrip": 1,"Vertical Foregrip": 0.77},"sight": {"none": 1,"2x Scope": 1.7,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.1},"muzzle": {"Compensator AR": 0.84,"Flash Hider AR": 0.84,"Suppressor AR": 1}},"Beryl M762": {"ballistic": [28,38,38,38,42,42,43,43,54,54,55,55,54,54,55,55,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62,62],"interval": 86,"attitude": {"stand": 1,"prone": 0.58,"squat": 0.83},"foregrip": {"Haalfgrip": 0.8,"Lightweight Grip": 0.78,"Thumbgrip": 0.93,"Angled Foregrip": 1,"Vertical Foregrip": 0.78},"sight": {"none": 1,"2x Scope": 1.72,"3x Scope": 2.62,"4x Scope": 3.62,"6x Scope": 5.2},"muzzle": {"Compensator AR": 0.86,"Flash Hider AR": 0.86,"Suppressor AR": 1}},"MG3": {"ballistic": [22,16,16,16,17,18,17,18,11,12,12,12,11,12,12,12,11,12,12,12,11,12,12,12,11,12,12,12,11,12,12,12,11,12,12,12,11,12,12,12,11,12,12,12,11,12,12,12,11,12,12,12,9,9,9,9],"interval": 76.25,"attitude": {"stand": 1,"prone": 0.25,"squat": 0.45},"sight": {"none": 1,"2x Scope": 1.7,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.1}},"Mini14": {"ballistic": [14,24,24,24,37,37,37,37,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50],"interval": 125,"attitude": {"stand": 1,"prone": 0.58,"squat": 0.73},"sight": {"none": 1,"15x Scope": 10.6,"2x Scope": 1.8,"3x Scope": 2.65,"4x Scope": 3.65,"6x Scope": 5.3,"8x Scope": 7.1},"muzzle": {"Compensator AR": 0.9,"Flash Hider AR": 0.9,"Suppressor AR": 1,"Compensator SR": 0.9,"Flash Hider SR": 0.9,"Suppressor SR": 1}},"Mk12": {"ballistic": [24,24,24,24,37,37,37,37,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50],"interval": 93.8,"attitude": {"stand": 1,"prone": 0.2,"squat": 0.74},"foregrip": {"Haalfgrip": 0.92,"Lightweight Grip": 0.77,"Thumbgrip": 0.96,"Angled Foregrip": 1,"Vertical Foregrip": 0.85},"sight": {"none": 1,"15x Scope": 10.85,"2x Scope": 1.8,"3x Scope": 2.75,"4x Scope": 3.85,"6x Scope": 5.5,"8x Scope": 7.35},"muzzle": {"Compensator AR": 0.9,"Flash Hider AR": 0.9,"Suppressor AR": 1,"Compensator SR": 0.9,"Flash Hider SR": 0.9,"Suppressor SR": 1}},"Mk14": {"ballistic": [11,11,11,12,4,4,4,5,12,13,12,13,14,14,14,15,38,38,38,39,40,41,41,41,40,41,41,41,40,41,41,41,40,41,41,41,40,41,41,41,40,41,41,41,60,61,61,61],"interval": 22.5,"attitude": {"stand": 1,"prone": 0.17,"squat": 0.68},"stock": {"Cheek Pad": 0.73},"sight": {"none": 1,"15x Scope": 10.2,"2x Scope": 1.7,"3x Scope": 2.55,"4x Scope": 3.55,"6x Scope": 5.2,"8x Scope": 6.9},"muzzle": {"Compensator AR": 0.89,"Flash Hider AR": 0.89,"Suppressor AR": 1,"Compensator SR": 0.89,"Flash Hider SR": 0.89,"Suppressor SR": 1}},"Mk47 Mutant": {"ballistic": [14,24,24,24,37,37,37,37,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50],"interval": 125,"attitude": {"stand": 1,"prone": 0.68,"squat": 0.85},"stock": {"Tactical Stock": 0.98},"foregrip": {"Haalfgrip": 0.86,"Lightweight Grip": 0.68,"Thumbgrip": 0.92,"Angled Foregrip": 1,"Vertical Foregrip": 0.82},"sight": {"none": 1,"2x Scope": 1.75,"3x Scope": 2.6,"4x Scope": 3.65,"6x Scope": 5.2},"muzzle": {"Compensator AR": 0.87,"Flash Hider AR": 0.87,"Suppressor AR": 1}},"MP5K": {"ballistic": [17,17,17,17,17,33,33,33,33,33,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29],"interval": 83.75,"attitude": {"stand": 1,"prone": 0.5,"squat": 0.65},"stock": {"Tactical Stock": 1},"foregrip": {"Haalfgrip": 0.89,"Lightweight Grip": 0.74,"Thumbgrip": 0.91,"Angled Foregrip": 1,"Vertical Foregrip": 0.74},"sight": {"none": 1,"2x Scope": 1.9,"3x Scope": 2.9,"4x Scope": 4,"6x Scope": 5.8},"muzzle": {"Suppressor SMG": 1,"Flash Hider SMG": 1,"Compensator SMG": 1}},"P90": {"ballistic": [7,14,14,14,14,14,14,19,19,19,19,19,19,19,15,15,15,15,15,15,15,12,12,12,12,12,12,12,14,14,14,14,14,14,14,14,13,13,13,13,13,13,13,13,13,12,12,12,12,12],"interval": 105,"attitude": {"stand": 1,"prone": 0.65,"squat": 0.75},"sight": {"none": 1,"2x Scope": 1.8}},"PP-19 Bizon": {"ballistic": [8,16,16,16,16,25,25,25,25,26,23,23,23,23,23,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21],"interval": 107.5,"attitude": {"stand": 1,"prone": 0.68,"squat": 0.78},"sight": {"none": 1,"2x Scope": 1.9,"3x Scope": 2.9,"4x Scope": 4,"6x Scope": 5.7},"muzzle": {"Suppressor SMG": 1,"Flash Hider SMG": 1,"Compensator SMG": 1}},"QBU": {"ballistic": [11,24,24,24,37,37,37,37,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50,50],"interval": 62.5,"attitude": {"stand": 1,"prone": 0.2,"squat": 0.74},"sight": {"none": 1,"15x Scope": 10.6,"2x Scope": 1.8,"3x Scope": 2.65,"4x Scope": 3.75,"6x Scope": 5.3,"8x Scope": 7.1},"muzzle": {"Compensator AR": 0.88,"Flash Hider AR": 0.88,"Suppressor AR": 1,"Compensator SR": 0.88,"Flash Hider SR": 0.88,"Suppressor SR": 1}},"QBZ": {"ballistic": [14,24,24,24,30,30,30,30,39,39,39,39,47,47,47,48,47,47,47,48,47,47,47,48,47,47,47,47,47,48,47,47,47,47,47,47,47,47,47,47,47,48],"interval": 92,"attitude": {"stand": 1,"prone": 0.5,"squat": 0.75},"foregrip": {"Haalfgrip": 0.75,"Lightweight Grip": 0.77,"Thumbgrip": 0.92,"Angled Foregrip": 1,"Vertical Foregrip": 0.77},"sight": {"none": 1,"2x Scope": 1.72,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.1},"muzzle": {"Compensator AR": 0.84,"Flash Hider AR": 0.84,"Suppressor AR": 1}},"SCAR-L": {"ballistic": [15,23,23,24,30,30,30,30,38,38,38,39,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43,43],"interval": 96,"attitude": {"stand": 1,"prone": 0.53,"squat": 0.75},"foregrip": {"Haalfgrip": 0.77,"Lightweight Grip": 0.77,"Thumbgrip": 0.92,"Angled Foregrip": 1,"Vertical Foregrip": 0.77},"sight": {"none": 1,"2x Scope": 1.72,"3x Scope": 2.6,"4x Scope": 3.6,"6x Scope": 5.1},"muzzle": {"Compensator AR": 0.84,"Flash Hider AR": 0.84,"Suppressor AR": 1}},"SKS": {"ballistic": [14,24,24,24,37,37,37,37,50,50,50,50,50,50,50,50,50,50,50,50,50,50],"interval": 125,"attitude": {"stand": 1,"prone": 0.44,"squat": 0.64},"stock": {"Cheek Pad": 0.84},"foregrip": {"Haalfgrip": 0.88,"Lightweight Grip": 0.65,"Thumbgrip": 0.94,"Angled Foregrip": 1,"Vertical Foregrip": 0.78},"sight": {"none": 1,"15x Scope": 10.3,"2x Scope": 1.7,"3x Scope": 2.55,"4x Scope": 3.55,"6x Scope": 5.1,"8x Scope": 6.8},"muzzle": {"Compensator AR": 0.88,"Flash Hider AR": 0.88,"Suppressor AR": 1,"Compensator SR": 0.88,"Flash Hider SR": 0.88,"Suppressor SR": 1}},"SLR": {"ballistic": [14,24,24,24,37,37,37,37,50,50,50,50,50,50,50,50,50,50,50,50,50,50],"interval": 125,"attitude": {"stand": 1,"prone": 0.43,"squat": 0.64},"stock": {"Cheek Pad": 0.7},"sight": {"none": 1,"15x Scope": 10.3,"2x Scope": 1.7,"3x Scope": 2.55,"4x Scope": 3.55,"6x Scope": 5.1,"8x Scope": 6.8},"muzzle": {"Compensator AR": 0.84,"Flash Hider AR": 0.84,"Suppressor AR": 1,"Compensator SR": 0.84,"Flash Hider SR": 0.84,"Suppressor SR": 1}},"Tommy Gun": {"ballistic": [10,18,18,18,18,26,26,26,26,39,39,39,39,39,39,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41,41],"interval": 90,"attitude": {"stand": 1,"prone": 0.58,"squat": 0.68},"foregrip": {"Vertical Foregrip": 0.77},"sight": {"none": 1}},"UMP45": {"ballistic": [10,19,19,19,26,26,26,28,26,26,26,28,32,32,32,34,30,30,30,32,30,30,30,32,30,30,30,32,30,30,30,30,30,30,30,30,30],"interval": 90,"attitude": {"stand": 1,"prone": 0.64,"squat": 0.74},"foregrip": {"Haalfgrip": 0.84,"Lightweight Grip": 0.84,"Thumbgrip": 0.91,"Angled Foregrip": 1,"Vertical Foregrip": 0.77},"sight": {"none": 1,"2x Scope": 1.9,"3x Scope": 2.9,"4x Scope": 4,"6x Scope": 5.7},"muzzle": {"Suppressor SMG": 1,"Flash Hider SMG": 1,"Compensator SMG": 1}},"Micro UZI": {"ballistic": [8,8,8,8,8,16,16,16,16,16,24,24,24,24,24,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,31,32,32,32,32,32,32],"interval": 60,"attitude": {"stand": 1,"prone": 0.6,"squat": 0.75},"stock": {"Folding Stock": 0.7},"sight": {"none": 1},"muzzle": {"Suppressor SMG": 1,"Flash Hider SMG": 0.85,"Compensator SMG": 0.65}},"Vector": {"ballistic": [7,13,13,13,14,24,24,24,24,25,25,25,25,25,25,37,37,37,37,37,33,33,33,33,33,33,33,33,33,33,33,33,33,33,33],"interval": 68.75,"attitude": {"stand": 1,"prone": 0.64,"squat": 0.74},"stock": {"Tactical Stock": 0.97},"foregrip": {"Haalfgrip": 0.85,"Lightweight Grip": 0.85,"Thumbgrip": 0.92,"Angled Foregrip": 1,"Vertical Foregrip": 0.8},"sight": {"none": 1,"2x Scope": 1.9,"3x Scope": 2.9,"4x Scope": 4,"6x Scope": 5.7},"muzzle": {"Suppressor SMG": 1,"Flash Hider SMG": 1,"Compensator SMG": 1}},"VSS": {"ballistic": [29,29,29,29,86,86,86,86,86,86,86,86,92,92,94,94,106,106,106,106,106,106],"interval": 86,"attitude": {"stand": 1,"prone": 0.57,"squat": 0.75},"stock": {"Cheek Pad": 0.77},"sight": {"none": 1}}}

structure.py

import cfgclass Weapon:def __init__(self, name, sight, muzzle, foregrip, stock):self.name = nameself.sight = sightself.muzzle = muzzleself.foregrip = foregripself.stock = stockself.data = cfg.weapons.get(self.name)self.suppress = True if self.data else False  # 该武器是否可以执行压制if self.data:self.interval = self.data.get(cfg.interval)  # 射击间隔self.ballistic = self.data.get(cfg.ballistic)  # 垂直弹道self.factor = 1attachment = self.data.get(cfg.sight)if attachment:self.factor *= attachment.get(self.sight, 1)attachment = self.data.get(cfg.muzzle)if attachment:self.factor *= attachment.get(self.muzzle, 1)attachment = self.data.get(cfg.foregrip)if attachment:self.factor *= attachment.get(self.foregrip, 1)attachment = self.data.get(cfg.stock)if attachment:self.factor *= attachment.get(self.stock, 1)def attitude(self, attitude):"""根据传入的姿态, 获取该武器对应数据中的姿态影响因子"""return self.data.get(cfg.attitude).get(attitude, 1)def __str__(self):name = cfg.translation.get(self.name)sight = cfg.translation.get(self.sight)muzzle = cfg.translation.get(self.muzzle)foregrip = cfg.translation.get(self.foregrip)stock = cfg.translation.get(self.stock)string = f'[{name}]'if sight:string += f', {sight}'if muzzle:string += f', {muzzle}'if foregrip:string += f', {foregrip}'if stock:string += f', {stock}'# print(f'武器:{self.name}, 瞄具:{self.sight}, 枪口:{self.muzzle}, 握把:{self.foregrip}, 枪托:{self.stock}')return string

toolkit.py

import os
import timeimport cv2
import d3dshot
import mss as pymss
import numpy as np
from skimage import measure  # pip install scikit-imagefrom win32api import GetSystemMetrics  # conda install pywin32
from win32con import SRCCOPY, SM_CXSCREEN, SM_CYSCREEN
from win32gui import GetDesktopWindow, GetWindowDC, DeleteObject, GetWindowText, GetForegroundWindow, GetDC, ReleaseDC, GetPixel
from win32ui import CreateDCFromHandle, CreateBitmapclass Capturer:@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 mss(instance, region):"""region: tuple, (left, top, width, height)pip install mss"""left, top, width, height = regionreturn instance.grab(monitor={'left': left, 'top': top, 'width': width, 'height': height})@staticmethoddef d3d(instance, region=None):"""DXGI 普通模式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 instance.screenshot((left, top, left + width, top + height))else:return instance.screenshot()@staticmethoddef d3d_latest_frame(instance):"""DXGI 缓存帧模式"""return instance.get_latest_frame()@staticmethoddef instance(mss=False, d3d=False, buffer=False, frame_buffer_size=60, target_fps=60, region=None):if mss:return pymss.mss()elif d3d:"""buffer: 是否使用缓存帧模式否: 适用于 dxgi.screenshot是: 适用于 dxgi.get_latest_frame, 需传入 frame_buffer_size, target_fps, region"""if not buffer:return d3dshot.create(capture_output="numpy")else:dxgi = d3dshot.create(capture_output="numpy", frame_buffer_size=frame_buffer_size)left, top, width, height = regiondxgi.capture(target_fps=target_fps, region=(left, top, left + width, top + height))  # region: left, top, right, bottom, 需要适配入参为 left, top, width, height 格式的 regionreturn dxgi@staticmethoddef grab(win=False, mss=False, d3d=False, instance=None, region=None, buffer=False, convert=False):"""win:region: tuple, (left, top, width, height)mss:instance: mss instanceregion: tuple, (left, top, width, height)d3d:buffer: 是否为缓存帧模式否: 需要 region是: 不需要 regioninstance: d3d instance, 区分是否为缓存帧模式region: tuple, (left, top, width, height), 区分是否为缓存帧模式convert: 是否转换为 opencv 需要的 numpy BGR 格式, 转换结果可直接用于 opencv"""# 补全范围if (win or mss or (d3d and not buffer)) and not region:w, h = Monitor.resolution()region = 0, 0, w, h# 范围截图if win:img = Capturer.win(region)elif mss:img = Capturer.mss(instance, region)elif d3d:if not buffer:img = Capturer.d3d(instance, region)else:img = Capturer.d3d_latest_frame(instance)else:img = Capturer.win(region)win = True# 图片转换if convert:if win:img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)elif mss:img = cv2.cvtColor(np.array(img), cv2.COLOR_BGRA2BGR)elif d3d:img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)return imgclass Monitor:@staticmethoddef resolution():"""显示分辨率"""w = GetSystemMetrics(SM_CXSCREEN)h = GetSystemMetrics(SM_CYSCREEN)return w, h@staticmethoddef center():"""屏幕中心点"""w, h = Monitor.resolution()return w // 2, h // 2class Timer:@staticmethoddef cost(interval):"""转换耗时, 输入纳秒间距, 转换为合适的单位"""if interval < 1000:return f'{interval}ns'elif interval < 1_000_000:return f'{round(interval / 1000, 3)}us'elif interval < 1_000_000_000:return f'{round(interval / 1_000_000, 3)}ms'else:return f'{round(interval / 1_000_000_000, 3)}s'class Image:@staticmethoddef gray(img, max=False):"""灰度化:param img: OpenCV BGR:param max: 使用BGR3通道中的最大值作为灰度色值""""""BGR3通道色值取平均值就是灰度色值, 灰度图将BGR3通道转换为灰度通道"""if not max:return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)else:new = np.zeros(img.shape[:2], np.uint8)for row in range(0, img.shape[0]):for col in range(0, img.shape[1]):(b, g, r) = img[row][col]value = (b if b >= g else g)new[row][col] = (value if value >= r else r)return new@staticmethoddef binary(img, adaptive=False, threshold=None, block=3, c=1):"""二值化:param img: 灰度图:param adaptive: 是否自适应二值化:param threshold: 二值化阈值(全局二值化), 大于该值的转为白色, 其他值转为黑色:param block: 分割的邻域大小(自适应二值化). 值越大, 参与计算阈值的邻域面积越大, 细节轮廓就变得越少, 整体轮廓将越粗越明显:param c: 常数(自适应二值化), 可复数. 值越大, 每个邻域内计算出的阈值将越小, 转换为 maxVal 的点将越多, 整体图像白色像素将越多"""if not adaptive:# 全局二值化_, img = cv2.threshold(img, threshold, 255, cv2.THRESH_BINARY)"""threshold(src, thresh, maxVal, type, dst=None)src: 灰度图thresh: 阈值maxVal: 指定的最大色值type:THRESH_BINARY: 二值化, 大于阈值的赋最大色值, 其他赋0THRESH_BINARY_INV: 二值化反转, 与 THRESH_BINARY 相反, 大于阈值的赋0, 其他赋最大色值THRESH_TRUNC: 截断操作, 大于阈值的赋最大色值, 其他不变THRESH_TOZERO: 化零操作, 大于阈值的不变, 其他赋0THRESH_TOZERO_INV: 化零操作反转, 大于阈值的赋0, 其他不变"""else:# 自适应二值化img = cv2.adaptiveThreshold(img, maxValue=255, adaptiveMethod=cv2.ADAPTIVE_THRESH_GAUSSIAN_C, thresholdType=cv2.THRESH_BINARY, blockSize=block, C=c)"""adaptiveThreshold(src, maxValue, adaptiveMethod, thresholdType, blockSize, C, dst=None)src: 灰度图maxValue: 指定的最大色值adaptiveMethod: 自适应方法。有2种:ADAPTIVE_THRESH_MEAN_C 或 ADAPTIVE_THRESH_GAUSSIAN_CADAPTIVE_THRESH_MEAN_C,为局部邻域块的平均值,该算法是先求出块中的均值。ADAPTIVE_THRESH_GAUSSIAN_C,为局部邻域块的高斯加权和。该算法是在区域中(x, y)周围的像素根据高斯函数按照他们离中心点的距离进行加权计算。thresholdType: 二值化方法,只能选 THRESH_BINARY 或者 THRESH_BINARY_INVTHRESH_BINARY: 二值化, 大于阈值的赋最大色值, 其他赋0THRESH_BINARY_INV: 二值化反转, 与 THRESH_BINARY 相反, 大于阈值的赋0, 其他赋最大色值blockSize: 分割计算的区域大小,取奇数当blockSize越大,参与计算阈值的区域也越大,细节轮廓就变得越少,整体轮廓越粗越明显C:常数,每个区域计算出的阈值的基础上在减去这个常数作为这个区域的最终阈值,可以为负数当C越大,每个像素点的N*N邻域计算出的阈值就越小,中心点大于这个阈值的可能性也就越大,设置成255的概率就越大,整体图像白色像素就越多,反之亦然。"""return img@staticmethoddef binary_remove_small_objects(img, threshold):"""消除二值图像中面积小于某个阈值的连通域(消除孤立点):param img: 二值图像(白底黑图):param threshold: 符合面积条件大小的阈值"""img_label, num = measure.label(img, background=255, connectivity=2, return_num=True)  # 输出二值图像中所有的连通域props = measure.regionprops(img_label)  # 输出连通域的属性,包括面积等resMatrix = np.zeros(img_label.shape)  # 创建0图for i in range(0, len(props)):if props[i].area > threshold:tmp = (img_label == i + 1).astype(np.uint8)resMatrix += tmp  # 组合所有符合条件的连通域resMatrix *= 255return 255 - resMatrix  # 本来输出的是黑底百图, 这里特意转换了黑白@staticmethoddef similarity(img1, img2, block=10):"""求两张二值化图片的相似度(简单实现):param img1: 图片1:param img2: 图片2:param block: 分块对比的块边长, 从1开始, 边长越大精度越低"""if img1.shape != img2.shape:return 0# 遍历图片, 计算同一位置相同色占总色数的比例height, width = img1.shape  # 经过处理后, 通道数只剩1个了# 相似度列表similarities = []# 根据给定的block大小计算分割的行列数, 将图片分为row行col列个格子(注意最后一行和最后一列的格子不一定是block大小)row = 1 if block >= height else (height // block + (0 if height % block == 0 else 1))col = 1 if block >= width else (width // block + (0 if width % block == 0 else 1))# print(f'图片宽度:{width},高度:{height}, 以块边长:{block}, 分为{row}行{col}列')for i in range(0, row):for j in range(0, col):# print('-')# 计算当前格子的w和hw = block if j + 1 < col else (width - (col - 1) * block)h = block if i + 1 < row else (height - (row - 1) * block)# print(f'当前遍历第{i + 1}行第{j + 1}列的块, 该块的宽度:{w},高度:{h}, 即该块有{h}行{w}列')counter = 0for x in range(block * i, block * i + h):for y in range(block * j, block * j + w):# print(f'x:{x},y:{y}')if img1[x][y] == img2[x][y]:counter += 1similarity = counter / (w * h)# print(f'当前块的相似度是:{similarity}')similarities.append(similarity ** 3)# print(similarities)return sum(similarities) / len(similarities)@staticmethoddef cut(img, region):"""从 img 中截取 region 范围. 入参图片需为 OpenCV 格式"""left, top, width, height = regionreturn img[top:top + height, left:left + width]@staticmethoddef convert(img, gray=False, binary=None, remove=None):"""图片(OpenCV BGR 格式)做灰度化和二值化处理:param img: OpenCV BGR 图片:param gray: 是否做灰度化处理:param binary: dict 格式, 非 None 做二值化处理具体参照 binary 方法的参数说明adaptive: 是否自适应二值化threshold: 非自适应二值化, 二值化阈值block: 自适应二值化邻域大小c: 常数:param remove: dict 格式, 非 None 做孤立点消除操作threshold: 连通域面积阈值, 小于该面积的连通域将被消除(黑转白)"""if gray:img = Image.gray(img)if binary:if not isinstance(binary, dict):return imgadaptive = binary.get('adaptive')threshold = binary.get('threshold')block = binary.get('block')c = binary.get('c')img = Image.binary(img, adaptive, threshold, block, c)if remove:if not isinstance(remove, dict):return imgthreshold = remove.get('threshold')img = Image.binary_remove_small_objects(img, threshold)return img@staticmethoddef read(path, gray=False, binary=None, remove=None):"""读取一张图片(OpenCV BGR 格式)并做灰度化和二值化处理:param path: 图片路径:param gray: 是否做灰度化处理:param binary: dict 格式, 非 None 做二值化处理:param remove: dict 格式, 非 None 做孤立点消除操作"""img = cv2.imread(path)# img = cv2.imdecode(np.fromfile(path, dtype=np.uint8), cv2.IMREAD_COLOR)  # 适配中文字符路径img = Image.convert(img, gray, binary, remove)return img@staticmethoddef load(directory, gray=False, binary=None, remove=None):"""递归载入指定路径下的所有图片(OpenCV BGR 格式), 按照 (name, img) 的格式组合成为列表并返回"""imgs = []for item in os.listdir(directory):path = os.path.join(directory, item)if os.path.isdir(path):temp = Image.load(path, gray, binary, remove)imgs.extend(temp)elif os.path.isfile(path):name = os.path.splitext(item)[0]img = Image.read(path, gray, binary, remove)imgs.append((name, img))return imgs if imgs else Noneimport cfg
from structure import Weaponclass Pubg:@staticmethoddef game():"""是否游戏窗体在最前"""return '绝地求生' in GetWindowText(GetForegroundWindow())def __init__(self):w, h = Monitor.resolution()self.key = f'{w}.{h}'  # 分辨率键self.binary = {'adaptive': True, 'block': 3, 'c': 1}self.remove = {'threshold': 10}self.std_img_backpack = Image.read(rf'image/{self.key}/backpack.png', gray=True, binary=self.binary, remove=self.remove)self.std_imgs_sight_1 = Image.load(rf'image/{self.key}/weapon/attachment/sight/1', gray=True, binary=self.binary, remove=self.remove)self.std_imgs_sight_2 = Image.load(rf'image/{self.key}/weapon/attachment/sight/2', gray=True, binary=self.binary, remove=self.remove)self.std_imgs_muzzle = Image.load(rf'image/{self.key}/weapon/attachment/muzzle', gray=True, binary=self.binary, remove=self.remove)self.std_imgs_foregrip = Image.load(rf'image/{self.key}/weapon/attachment/foregrip', gray=True, binary=self.binary, remove=self.remove)self.std_imgs_stock = Image.load(rf'image/{self.key}/weapon/attachment/stock', gray=True, binary=self.binary, remove=self.remove)self.std_names = cfg.detect.get(self.key).get(cfg.weapon).get(cfg.name)def backpack(self):"""是否在背包界面"""region = cfg.detect.get(self.key).get(cfg.backpack)img = Capturer.grab(win=True, region=region, convert=True)img = Image.convert(img, gray=True, binary=self.binary, remove=self.remove)return Image.similarity(self.std_img_backpack, img) > 0.9def weapon(self):"""在背包库存界面识别两把主武器及其配件"""data = cfg.detect.get(self.key).get(cfg.weapon)region = data.get(cfg.region)# 截图主武器部分img = Capturer.grab(win=True, region=region, convert=True)# 识别两把主武器武器weapon1 = self.recognize(img, data.get(cfg.one))weapon2 = self.recognize(img, data.get(cfg.two))return weapon1, weapon2def bullet(self):"""是否有子弹效率很低且不稳定, 单点检测都要耗时1-10ms获取颜色, COLORREF 格式, 0x00FFFFFF结果是int,可以通过 print(hex(color)) 查看十六进制值可以通过 print(color == 0x00FFFFFF) 进行颜色判断"""x, y = cfg.detect.get(self.key).get(cfg.bullet)hdc = GetDC(None)color = GetPixel(hdc, x, y)# print(color)ReleaseDC(None, hdc)return color != 255def attitude(self):"""姿态识别"""data = cfg.detect.get(self.key).get(cfg.attitude)region = data.get(cfg.region)# 截图姿态部分img = Capturer.grab(win=True, region=region, convert=True)# 灰度化二值化img = Image.convert(img, gray=True, binary=self.binary)# cv2.imwrite('1.jpg', img)# 判断是否是站立counter = 0points = data.get(cfg.stand)for point in points:if img[point] == 0:counter += 1if counter == len(points):return cfg.stand# 判断是否是蹲下counter = 0points = data.get(cfg.squat)for point in points:if img[point] == 0:counter += 1if counter == len(points):return cfg.squat# 判断是否是趴卧counter = 0points = data.get(cfg.prone)for point in points:if img[point] == 0:counter += 1if counter == len(points):return cfg.prone# 不是3种姿态return Nonedef firemode(self):"""射击模式识别, 只限突击步枪和冲锋枪"""data = cfg.detect.get(self.key).get(cfg.firemode)region = data.get(cfg.region)# 截图模式部分img = Capturer.grab(win=True, region=region, convert=True)# 灰度化img = Image.gray(img)# 二值化img = Image.binary(img, threshold=230)# cv2.imwrite(f'{int(time.perf_counter_ns())}.jpg', img)# 判断射击模式counter = 0points = data.get(cfg.points)for point in points:# print(img[point])if img[point] == 255:counter += 1if counter == 1:return cfg.onlyelif counter == 2 or counter == 3:return cfg.semielif counter == 4:return cfg.auto# 非四种射击模式return Nonedef index(self):"""1/2号武器识别, 0:未持有1/2武器, 1:持有1号武器, 2:持有2号武器判定时机:鼠标滚轮滚动/1/2/3/4/5/G(切雷)/F(落地捡枪)/X(收起武器)/Tab(调整位置)投掷武器,近战武器和单发火箭炮等,用光后不会导致切换武器能量和药包等消耗品,使用前如果持有武器,使用后会切回该武器,使用前未持有武器,使用后不会切换武器""""""测试发现主界面上右下角武器位和主武器只有下面3种情况1号位上显示1号武器1号位上显示1号武器, 2号位上显示2号武器1号位上显示2号武器"""data = cfg.detect.get(self.key).get(cfg.active)region = data.get(cfg.region)# 截图模式部分# original = Capturer.grab(win=True, region=region, convert=True)original = Image.read(rf'image/test/1668496773254551600.png')img = Image.gray(original)# cv2.imshow('res', img)# cv2.waitKey(0)# cv2.destroyAllWindows()img = Image.binary(img, adaptive=True, block=9)# cv2.imwrite(rf'image/result/{time.time_ns()}.jpg', img)# 识别存在的武器序号indexes = []one = data.get(cfg.one)two = data.get(cfg.two)counter = 0for point in one.get(1):# print(point, img[point])if img[point] == 0:counter += 1if counter == len(one.get(1)):indexes.append(1)counter = 0# print()for point in two.get(2):# print(point, img[point])if img[point] == 0:counter += 1if counter == len(two.get(2)):indexes.append(2)else:counter = 0# print()for point in one.get(2):# print(point, img[point])if img[point] == 0:counter += 1if counter == len(one.get(2)):indexes.append(2)print(indexes)# 根据识别到的武器序号判断激活的武器if len(indexes) == 0:return Noneif len(indexes) == 1:# 识别1号位是否激活active = self.active(original, one)return indexes[0] if active else Noneif len(indexes) == 2:# 识别1号位是否激活active = self.active(original, one)if active:return 1else:# 判断2号位是否激活active = self.active(original, two)return 2 if active else None# 其他情况return None"""---------- ---------- ---------- ---------- ----------"""def name(self, img):"""识别武器名称, 入参图片需为 OpenCV 格式"""# 截图灰度化img = Image.gray(img)# 截图二值化img = Image.binary(img, threshold=254)# 数纯白色点height, width = img.shapecounter = 0for row in range(0, height):for col in range(0, width):if 255 == img[row, col]:counter += 1return self.std_names.get(counter)def attachment(self, imgs, img):"""识别武器配件, 入参图片需为 OpenCV 格式"""img = Image.convert(img, gray=True, binary=self.binary, remove=self.remove)for name, standard in imgs:similarity = Image.similarity(standard, img)# print(similarity, name)if similarity > 0.925:return namereturn Nonedef recognize(self, img, config):"""传入武器大图和识别名称配件的配置项, 返回识别到的武器. 入参图片需为 OpenCV 格式"""# 判断武器是否存在exist = np.mean(img[config.get(cfg.point)]) == 255  # 取 BGR 列表的均值, 判断是不是纯白色if not exist:return None# 武器存在, 先识别名称name = self.name(Image.cut(img, config.get(cfg.name)))if not name:return None# 识别出武器名称后再识别配件index = config.get(cfg.index)sight = self.attachment(self.std_imgs_sight_1 if index == 1 else self.std_imgs_sight_2, Image.cut(img, config.get(cfg.sight)))muzzle = self.attachment(self.std_imgs_muzzle, Image.cut(img, config.get(cfg.muzzle)))foregrip = self.attachment(self.std_imgs_foregrip, Image.cut(img, config.get(cfg.foregrip)))stock = self.attachment(self.std_imgs_stock, Image.cut(img, config.get(cfg.stock)))return Weapon(name, sight, muzzle, foregrip, stock)def active(self, img, config):region = config.get(cfg.region)img = Image.cut(img, region)img = Image.gray(img, True)# img = Image.binary(img, adaptive=True, block=9)# img = Image.binary(img, threshold=230)# cv2.imwrite(rf'image/result/{time.time_ns()}.jpg', img)# cv2.imshow('res', img)# cv2.waitKey(0)# cv2.destroyAllWindows()# 方式1, 找最多的颜色, 不太行data = {}height, width = img.shapefor row in range(0, height):for col in range(0, width):counter = data.get(img[row, col])if counter:counter += 1else:counter = 1data[img[row, col]] = counterkey = -1counter = 0for k, v in data.items():if v > counter:key = kcounter = vprint(f'最多的颜色:', key, counter)# 方式2, 找大于某值的颜色数height, width = img.shapecounter = 0for row in range(0, height):for col in range(0, width):if img[row, col] > 210:counter += 1print(f'大于某值数:', counter)# 方式3, 最大颜色值value = -1for row in range(0, height):for col in range(0, width):if img[row, col] > value:value = img[row, col]print(f'最大颜色值:', value)return False

pubg.py

import ctypes
import multiprocessing
import time
from multiprocessing import Process
import pynput  # pip install pynput
import winsoundfrom toolkit import Pubg, Timerend = 'end'
tab = 'tab'
ads = 'ads'
fire = 'fire'
temp = 'temp'
debug = 'debug'
index = 'index'
right = 'right'
switch = 'switch'
weapon = 'weapon'
weapons = 'weapons'
attitude = 'attitude'
firemode = 'firemode'
recognize = 'recognize'
timestamp = 'timestamp'
init = {end: False,  # 退出标记switch: True,  # 压枪开关tab: 0,  # 背包检测信号, 非0触发检测. Tab键触发修改, 用于检测背包界面中的武器信息weapons: None,  # 背包界面中的两把主武器信息, 字典格式, {1:武器1, 2:武器2}index: 0,  # 激活检测信号, 非0触发检测. 鼠标滚轮滚动/1/2/3/4/5/G(切雷)/F(落地捡枪)/X(收起武器)/Tab(调整位置)/ 等按键触发修改weapon: None,  # 当前持有的主武器right: 0,  # 右键检测信号, 非0触发检测. 右键触发修改, 包括当前的角色姿态, 当前激活的武器, 武器的射击模式attitude: None,  # 姿态, stand:站, squat:蹲, prone:爬, 开火时检测(开火时要按右键,按右键后会出现姿态标识)firemode: None,  # 射击模式, auto:全自动, semi:半自动(点射), only:单发timestamp: None,  # 按下左键开火时的时间戳fire: False,  # 开火状态ads: 2,  # 基准倍数debug: False,  # 调试模式开关temp: None,  # 调试下压力度数据使用
}def mouse(data):def down(x, y, button, pressed):if Pubg.game():if button == pynput.mouse.Button.x1:# 侧下键if pressed:# 压枪开关data[switch] = not data.get(switch)winsound.Beep(800 if data[switch] else 400, 200)elif button == pynput.mouse.Button.left:data[fire] = pressedif pressed:data[timestamp] = time.time_ns()elif button == pynput.mouse.Button.right:if pressed:data[right] = 1elif button == pynput.mouse.Button.x2:  # todo 调试弹道if pressed and data[debug]:with open('debug', 'r') as file:try:exec(file.read())print(data[temp])except Exception as e:print(e.args)print(str(e))print(repr(e))def scroll(x, y, dx, dy):if Pubg.game():data[index] = 1with pynput.mouse.Listener(on_click=down, on_scroll=scroll) as m:m.join()def keyboard(data):def release(key):if key == pynput.keyboard.Key.end:# 结束程序winsound.Beep(400, 200)data[end] = Truereturn Falseif Pubg.game():if key == pynput.keyboard.Key.tab:# tab: 背包检测与武器识别的状态# 0: 默认状态# 1: 背包检测中# 2: 武器识别中# 3: 等待关闭背包if data[tab] == 0:  # 等待打开背包data[tab] = 1elif data[tab] == 1:  # 背包检测中, 中止检测, 恢复默认状态(循环中会有状态机式的状态感知)data[tab] = 0data[index] = 1elif data[tab] == 2:  # 武器识别中, 中止识别, 恢复默认状态data[tab] = 0data[index] = 1elif data[tab] == 3:  # 武器已识别, 等待关闭背包, 恢复默认状态data[tab] = 0data[index] = 1elif key == pynput.keyboard.KeyCode.from_char('1'):data[index] = 1# todoif data[weapons] is not None and data[weapons].get(1) is not None:data[weapon] = data[weapons].get(1)elif key == pynput.keyboard.KeyCode.from_char('2'):data[index] = 1# todoif data[weapons] is not None and data[weapons].get(2) is not None:data[weapon] = data[weapons].get(2)elif key == pynput.keyboard.KeyCode.from_char('3'):data[index] = 1elif key == pynput.keyboard.KeyCode.from_char('4'):data[index] = 1elif key == pynput.keyboard.KeyCode.from_char('5'):data[index] = 1elif key == pynput.keyboard.KeyCode.from_char('g'):data[index] = 1elif key == pynput.keyboard.KeyCode.from_char('f'):data[index] = 1elif key == pynput.keyboard.KeyCode.from_char('x'):data[index] = 1with pynput.keyboard.Listener(on_release=release) as k:k.join()def suppress(data):try:driver = ctypes.CDLL('logitech.driver.dll')ok = driver.device_open() == 1  # 该驱动每个进程可打开一个实例if not ok:print('Error, GHUB or LGS driver not found')except FileNotFoundError:print('Error, DLL file not found')def move(x, y):if ok:driver.moveR(x, y, True)pubg = Pubg()winsound.Beep(800, 200)counter = 0  # 检测计数器, 防止因不正常状态导致背包检测和武器识别陷入死循环, 10个循环内没有结果就会强制退出def show():print('==========')if data[weapons]:for k, v in data[weapons].items():print(f'{k}: {v}')print(f'index: {data[index]}, {data[attitude]}, {data[firemode]}')while True:if data.get(end):  # 退出程序breakif not data.get(switch):data[tab] = 0  # 开关关闭时, 每次循环都会重置背包检测信号continueif not pubg.game():  # 如果不在游戏中continueif data[tab] == 1:  # 背包界面检测counter += 1if counter >= 10:  # 举例: 开着背包的时候, 启动辅助并打开开关, 按Tab键关闭背包, 触发辅助更新为状态1, 因为背包已关闭不可能判定是在背包界面, 导致卡状态1data[tab] = 0counter = 0if pubg.backpack() and data[tab] == 1:  # 背包界面检测data[tab] = 2counter = 0continueif data[tab] == 2:  # 背包中武器识别counter += 1if counter >= 10:data[tab] = 0counter = 0first, second = pubg.weapon()  # 背包中武器识别if data[tab] == 2:data[tab] = 3counter = 0winsound.Beep(600, 200)  # 通知武器识别结束data[weapons] = {1: first,2: second,}show()continueif data[index] != 0:  # 检测当前激活的是几号武器data[index] = 0time.sleep(0.2)  # 防止UI还没有改变if not data[weapons]:  # 如果还没有识别过背包中的武器, 则不检测当前激活的是几号武器continuecount = 0  # 如果识别过背包中的武器, 但识别到的都是 None, 则不检测当前激活的是几号武器for key, value in data[weapons].items():if not value:count += 1if count == 0:continue# data[weapon] = data[weapons].get(pubg.index())  # 检测当前激活的是几号武器  # todoshow()continueif data[right] != 0:  # 右键检测data[right] = 0time.sleep(0.2)  # 防止UI还没有改变data[attitude] = pubg.attitude()  # 检测角色姿态data[firemode] = pubg.firemode()  # 检测射击模式show()if data[fire]:  # 开火检测, 默认开火前一定按下了右键, 做了右键检测gun = data[weapon]if gun is None:  # 如果不确定当前武器则不压枪print('武器不确定')continueif gun.suppress is False:  # 如果当前武器不支持压枪print('武器不支持')continuedata[firemode] = pubg.firemode()if data[firemode] != 'auto':  # 全自动才压枪(突击步枪/冲锋枪). 这里有隐藏效果,不在对局中/未持枪/背包界面等情景下返回值都是Noneprint('武器非自动')continueif not pubg.bullet():  # 如果弹夹空了print('武器弹夹空')continueprint('----------')cost = time.time_ns() - data[timestamp]  # 开火时长base = gun.interval * 1_000_000  # 基准间隔时间转纳秒i = cost // base  # 本回合的压枪力度数值索引distance = int(data[ads] * gun.ballistic[i] * gun.factor * gun.attitude(data[attitude]))  # 下移距离distance = int(data[ads] * gun.ballistic[i] * data[temp]) if data[temp] else distance  # 下移距离, 去除武器因子和姿态因子的影响, 用于测试当前弹道力度下某单一因素的影响因子值(比如测不同握把的影响)print(f'开火时长:{Timer.cost(cost)}, {i}, 压制力度:{distance}, 武器因子:{gun.factor}, 姿态因子:{gun.attitude(data[attitude])}')cost = time.time_ns() - data[timestamp]left = base - cost % base  # 本回合剩余时间纳秒mean = left / distance  # 平缓压枪每个实际力度的延时for i in range(0, distance):begin = time.perf_counter_ns()while time.perf_counter_ns() - begin < mean:passmove(0, 1)if __name__ == '__main__':multiprocessing.freeze_support()manager = multiprocessing.Manager()data = manager.dict()data.update(init)# 将键鼠监听和压枪放到单独进程中跑pm = Process(target=mouse, args=(data,))pk = Process(target=keyboard, args=(data,))ps = Process(target=suppress, args=(data,))pm.start()pk.start()ps.start()pk.join()pm.terminate()

分析测试等源码见 GitHub 工程

相关内容

热门资讯

常用商务英语口语   商务英语是以适应职场生活的语言要求为目的,内容涉及到商务活动的方方面面。下面是小编收集的常用商务...
六年级上册英语第一单元练习题   一、根据要求写单词。  1.dry(反义词)__________________  2.writ...
复活节英文怎么说 复活节英文怎么说?复活节的英语翻译是什么?复活节:Easter;"Easter,anniversar...
2008年北京奥运会主题曲 2008年北京奥运会(第29届夏季奥林匹克运动会),2008年8月8日到2008年8月24日在中华人...
英语道歉信 英语道歉信15篇  在日常生活中,道歉信的使用频率越来越高,通过道歉信,我们可以更好地解释事情发生的...
六年级英语专题训练(连词成句... 六年级英语专题训练(连词成句30题)  1. have,playhouse,many,I,toy,i...
上班迟到情况说明英语   每个人都或多或少的迟到过那么几次,因为各种原因,可能生病,可能因为交通堵车,可能是因为天气冷,有...
小学英语教学论文 小学英语教学论文范文  引导语:英语教育一直都是每个家长所器重的,那么有关小学英语教学论文要怎么写呢...
英语口语学习必看的方法技巧 英语口语学习必看的方法技巧如何才能说流利的英语? 说外语时,我们主要应做到四件事:理解、回答、提问、...
四级英语作文选:Birth ... 四级英语作文范文选:Birth controlSince the Chinese Governmen...
金融专业英语面试自我介绍 金融专业英语面试自我介绍3篇  金融专业的学生面试时,面试官要求用英语做自我介绍该怎么说。下面是小编...
我的李老师走了四年级英语日记... 我的李老师走了四年级英语日记带翻译  我上了五个学期的小学却换了六任老师,李老师是带我们班最长的语文...
小学三年级英语日记带翻译捡玉... 小学三年级英语日记带翻译捡玉米  今天,我和妈妈去外婆家,外婆家有刚剥的`玉米棒上带有玉米籽,好大的...
七年级英语优秀教学设计 七年级英语优秀教学设计  作为一位兢兢业业的人民教师,常常要写一份优秀的教学设计,教学设计是把教学原...
我的英语老师作文 我的英语老师作文(通用21篇)  在日常生活或是工作学习中,大家都有写作文的经历,对作文很是熟悉吧,...
英语老师教学经验总结 英语老师教学经验总结(通用19篇)  总结是指社会团体、企业单位和个人对某一阶段的学习、工作或其完成...
初一英语暑假作业答案 初一英语暑假作业答案  英语练习一(基础训练)第一题1.D2.H3.E4.F5.I6.A7.J8.C...
大学生的英语演讲稿 大学生的英语演讲稿范文(精选10篇)  使用正确的写作思路书写演讲稿会更加事半功倍。在现实社会中,越...
VOA美国之音英语学习网址 VOA美国之音英语学习推荐网址 美国之音网站已经成为语言学习最重要的资源站点,在互联网上还有若干网站...
商务英语期末试卷 Part I Term Translation (20%)Section A: Translate ...