博文目录
文章目录
- 环境准备
- 压枪原理
- 需求分析
- 求两张图片的相似度
- 背包检测 是否在背包界面
- 武器识别
- 名称识别 纯白计数法
- 配件识别 瞄具/枪口/握把/枪托 相似对比法
- 模式识别 全自动/半自动/单发
- 姿态识别 站/蹲/爬
- 余弹识别
- 激活识别 是否持有武器/一号武器/二号武器 (未完成, 做不下去了)
- 压枪数据
- 工程源码
- 相关资源
- 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 cfg
class Weapon:
def __init__(self, name, sight, muzzle, foregrip, stock):
self.name = name
self.sight = sight
self.muzzle = muzzle
self.foregrip = foregrip
self.stock = stock
self.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 = 1
attachment = 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 time
import cv2
import d3dshot
import mss as pymss
import numpy as np
from skimage import measure # pip install scikit-image
from 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, CreateBitmap
class Capturer:
@staticmethod
def win(region):
"""
region: tuple, (left, top, width, height)
conda install pywin32, 用 pip 装的一直无法导入 win32ui 模块, 找遍各种办法都没用, 用 conda 装的一次成功
"""
left, top, width, height = region
hWin = 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
@staticmethod
def mss(instance, region):
"""
region: tuple, (left, top, width, height)
pip install mss
"""
left, top, width, height = region
return instance.grab(monitor={'left': left, 'top': top, 'width': width, 'height': height})
@staticmethod
def 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 = region
return instance.screenshot((left, top, left + width, top + height))
else:
return instance.screenshot()
@staticmethod
def d3d_latest_frame(instance):
"""
DXGI 缓存帧模式
"""
return instance.get_latest_frame()
@staticmethod
def 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 = region
dxgi.capture(target_fps=target_fps, region=(left, top, left + width, top + height)) # region: left, top, right, bottom, 需要适配入参为 left, top, width, height 格式的 region
return dxgi
@staticmethod
def 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 instance
region: tuple, (left, top, width, height)
d3d:
buffer: 是否为缓存帧模式
否: 需要 region
是: 不需要 region
instance: 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 img
class Monitor:
@staticmethod
def resolution():
"""
显示分辨率
"""
w = GetSystemMetrics(SM_CXSCREEN)
h = GetSystemMetrics(SM_CYSCREEN)
return w, h
@staticmethod
def center():
"""
屏幕中心点
"""
w, h = Monitor.resolution()
return w // 2, h // 2
class Timer:
@staticmethod
def 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:
@staticmethod
def 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
@staticmethod
def 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: 二值化, 大于阈值的赋最大色值, 其他赋0
THRESH_BINARY_INV: 二值化反转, 与 THRESH_BINARY 相反, 大于阈值的赋0, 其他赋最大色值
THRESH_TRUNC: 截断操作, 大于阈值的赋最大色值, 其他不变
THRESH_TOZERO: 化零操作, 大于阈值的不变, 其他赋0
THRESH_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_C
ADAPTIVE_THRESH_MEAN_C,为局部邻域块的平均值,该算法是先求出块中的均值。
ADAPTIVE_THRESH_GAUSSIAN_C,为局部邻域块的高斯加权和。该算法是在区域中(x, y)周围的像素根据高斯函数按照他们离中心点的距离进行加权计算。
thresholdType: 二值化方法,只能选 THRESH_BINARY 或者 THRESH_BINARY_INV
THRESH_BINARY: 二值化, 大于阈值的赋最大色值, 其他赋0
THRESH_BINARY_INV: 二值化反转, 与 THRESH_BINARY 相反, 大于阈值的赋0, 其他赋最大色值
blockSize: 分割计算的区域大小,取奇数
当blockSize越大,参与计算阈值的区域也越大,细节轮廓就变得越少,整体轮廓越粗越明显
C:常数,每个区域计算出的阈值的基础上在减去这个常数作为这个区域的最终阈值,可以为负数
当C越大,每个像素点的N*N邻域计算出的阈值就越小,中心点大于这个阈值的可能性也就越大,设置成255的概率就越大,整体图像白色像素就越多,反之亦然。
"""
return img
@staticmethod
def 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 *= 255
return 255 - resMatrix # 本来输出的是黑底百图, 这里特意转换了黑白
@staticmethod
def 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和h
w = 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 = 0
for 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 += 1
similarity = counter / (w * h)
# print(f'当前块的相似度是:{similarity}')
similarities.append(similarity ** 3)
# print(similarities)
return sum(similarities) / len(similarities)
@staticmethod
def cut(img, region):
"""
从 img 中截取 region 范围. 入参图片需为 OpenCV 格式
"""
left, top, width, height = region
return img[top:top + height, left:left + width]
@staticmethod
def 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 img
adaptive = 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 img
threshold = remove.get('threshold')
img = Image.binary_remove_small_objects(img, threshold)
return img
@staticmethod
def 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
@staticmethod
def 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 None
import cfg
from structure import Weapon
class Pubg:
@staticmethod
def 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.9
def 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, weapon2
def 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 != 255
def 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 = 0
points = data.get(cfg.stand)
for point in points:
if img[point] == 0:
counter += 1
if counter == len(points):
return cfg.stand
# 判断是否是蹲下
counter = 0
points = data.get(cfg.squat)
for point in points:
if img[point] == 0:
counter += 1
if counter == len(points):
return cfg.squat
# 判断是否是趴卧
counter = 0
points = data.get(cfg.prone)
for point in points:
if img[point] == 0:
counter += 1
if counter == len(points):
return cfg.prone
# 不是3种姿态
return None
def 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 = 0
points = data.get(cfg.points)
for point in points:
# print(img[point])
if img[point] == 255:
counter += 1
if counter == 1:
return cfg.only
elif counter == 2 or counter == 3:
return cfg.semi
elif counter == 4:
return cfg.auto
# 非四种射击模式
return None
def 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 = 0
for point in one.get(1):
# print(point, img[point])
if img[point] == 0:
counter += 1
if 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 += 1
if 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 += 1
if counter == len(one.get(2)):
indexes.append(2)
print(indexes)
# 根据识别到的武器序号判断激活的武器
if len(indexes) == 0:
return None
if len(indexes) == 1:
# 识别1号位是否激活
active = self.active(original, one)
return indexes[0] if active else None
if len(indexes) == 2:
# 识别1号位是否激活
active = self.active(original, one)
if active:
return 1
else:
# 判断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.shape
counter = 0
for row in range(0, height):
for col in range(0, width):
if 255 == img[row, col]:
counter += 1
return 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 name
return None
def 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.shape
for row in range(0, height):
for col in range(0, width):
counter = data.get(img[row, col])
if counter:
counter += 1
else:
counter = 1
data[img[row, col]] = counter
key = -1
counter = 0
for k, v in data.items():
if v > counter:
key = k
counter = v
print(f'最多的颜色:', key, counter)
# 方式2, 找大于某值的颜色数
height, width = img.shape
counter = 0
for row in range(0, height):
for col in range(0, width):
if img[row, col] > 210:
counter += 1
print(f'大于某值数:', counter)
# 方式3, 最大颜色值
value = -1
for 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 winsound
from toolkit import Pubg, Timer
end = '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] = pressed
if pressed:
data[timestamp] = time.time_ns()
elif button == pynput.mouse.Button.right:
if pressed:
data[right] = 1
elif 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] = 1
with 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] = True
return False
if Pubg.game():
if key == pynput.keyboard.Key.tab:
# tab: 背包检测与武器识别的状态
# 0: 默认状态
# 1: 背包检测中
# 2: 武器识别中
# 3: 等待关闭背包
if data[tab] == 0: # 等待打开背包
data[tab] = 1
elif data[tab] == 1: # 背包检测中, 中止检测, 恢复默认状态(循环中会有状态机式的状态感知)
data[tab] = 0
data[index] = 1
elif data[tab] == 2: # 武器识别中, 中止识别, 恢复默认状态
data[tab] = 0
data[index] = 1
elif data[tab] == 3: # 武器已识别, 等待关闭背包, 恢复默认状态
data[tab] = 0
data[index] = 1
elif key == pynput.keyboard.KeyCode.from_char('1'):
data[index] = 1
# todo
if 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
# todo
if 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] = 1
elif key == pynput.keyboard.KeyCode.from_char('4'):
data[index] = 1
elif key == pynput.keyboard.KeyCode.from_char('5'):
data[index] = 1
elif key == pynput.keyboard.KeyCode.from_char('g'):
data[index] = 1
elif key == pynput.keyboard.KeyCode.from_char('f'):
data[index] = 1
elif key == pynput.keyboard.KeyCode.from_char('x'):
data[index] = 1
with 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): # 退出程序
break
if not data.get(switch):
data[tab] = 0 # 开关关闭时, 每次循环都会重置背包检测信号
continue
if not pubg.game(): # 如果不在游戏中
continue
if data[tab] == 1: # 背包界面检测
counter += 1
if counter >= 10: # 举例: 开着背包的时候, 启动辅助并打开开关, 按Tab键关闭背包, 触发辅助更新为状态1, 因为背包已关闭不可能判定是在背包界面, 导致卡状态1
data[tab] = 0
counter = 0
if pubg.backpack() and data[tab] == 1: # 背包界面检测
data[tab] = 2
counter = 0
continue
if data[tab] == 2: # 背包中武器识别
counter += 1
if counter >= 10:
data[tab] = 0
counter = 0
first, second = pubg.weapon() # 背包中武器识别
if data[tab] == 2:
data[tab] = 3
counter = 0
winsound.Beep(600, 200) # 通知武器识别结束
data[weapons] = {
1: first,
2: second,
}
show()
continue
if data[index] != 0: # 检测当前激活的是几号武器
data[index] = 0
time.sleep(0.2) # 防止UI还没有改变
if not data[weapons]: # 如果还没有识别过背包中的武器, 则不检测当前激活的是几号武器
continue
count = 0 # 如果识别过背包中的武器, 但识别到的都是 None, 则不检测当前激活的是几号武器
for key, value in data[weapons].items():
if not value:
count += 1
if count == 0:
continue
# data[weapon] = data[weapons].get(pubg.index()) # 检测当前激活的是几号武器 # todo
show()
continue
if data[right] != 0: # 右键检测
data[right] = 0
time.sleep(0.2) # 防止UI还没有改变
data[attitude] = pubg.attitude() # 检测角色姿态
data[firemode] = pubg.firemode() # 检测射击模式
show()
if data[fire]: # 开火检测, 默认开火前一定按下了右键, 做了右键检测
gun = data[weapon]
if gun is None: # 如果不确定当前武器则不压枪
print('武器不确定')
continue
if gun.suppress is False: # 如果当前武器不支持压枪
print('武器不支持')
continue
data[firemode] = pubg.firemode()
if data[firemode] != 'auto': # 全自动才压枪(突击步枪/冲锋枪). 这里有隐藏效果,不在对局中/未持枪/背包界面等情景下返回值都是None
print('武器非自动')
continue
if not pubg.bullet(): # 如果弹夹空了
print('武器弹夹空')
continue
print('----------')
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:
pass
move(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()