Чит для PUBG на основе CV YOLO11. Дело декандентское и порочное.

Чит для PUBG на основе CV YOLO11. Дело декандентское и порочное.
Привет. В этой статье описано как я делал чит для PUBG на основе компьютерного зрения YOLO11 и наводки через нейросеть HumanizedTrajectoryGen.onnx, какие проблемы возникали в ходе работы и как получалось их решать.

Открыть видео / медиа

*Я не описываю каждую строчку кода, как устанавливать все необходимые зависимости и как в целом работают нейросети.

Нажмите, чтобы раскрыть...

​

Аннотация.​​
Если начать издалека, то мне нужно было обучить YOLO чтобы отрисовывать боксы поверх врагов чтобы я мог почаще долбиться в глаза. С обучением трудностей нет, как обычно нужен неебических размеров датасет и будет хороший результат.
Но в ходе работы заметил что YOLO не сильно нагружает систему и боксы «не поспевают» за реальным положением врага на экране.


Эта проблема будет сохраняться всегда и упирается она в мощности GPU, параметры инференса YOLO и код. В моем случае инференс занимает примерно 8-15мс, помимо этого есть другие процессы, которые наклдывают еще 1-2 мс на отрисовку и за это время появляется уже следующий кадр, при условии что время между кадрами в игре 16,67 мс. Соответственно при резких движениях бокс отрисовывается где то, но не поверх персонажа.

*ФПС в игре у меня 60, во всей статье отталкиваюсь от этого числа. И среднее время между кадрами при 60 фпс ~ 16,67 мс.

Нажмите, чтобы раскрыть...

Единственное на что я могу влиять, это код, выбор библиотек для захвата экрана и оверлея (отрисовки боксов). На что и сделал упор для решения проблемы.

Когда получилось более менее решить проблему с боксами, то появляется возможность прикрутить наводку для аимбота, ее я реализовал через нейросеть HumanizedTrajectoryGen.onnx. https://github.com/jason-snell/humanized-cursor-trajectory-gen



*Human-like cursor movement generator using a self-trained ONNX model. Simulates real mouse trajectories for testing, UI automation, or research.
Generates intermediate points between a start X,Y and end X,Y. Useful against anti-bots for automation.

Нажмите, чтобы раскрыть...

Эта модель обучена на кожаных движениях курсора в браузере, что не очень подходит для игры, но работает быстро.

Еще докупил arduino чтобы не водить мышь через winapi. Но тут ничего интересного нет. Ему на вход подается траектория сгенерированная, он ее реализовывает



Параметры моей системы.​​
i5 13400f.
4060 8VRAM.
16 RAM.
Arduino PRO-micro
ATMEGA32U4.

После часа игры с читом GPU нагревается до ~ 60 градусов.

YOLO.​​
Изначально я пробовал использовать предобученную модель yolo11s.pt на классе «person». Вблизи трекает уверенно, но на расстоянии, за объеками или персонаж сливается с местностью не всегда может определить.

*yolo11s.pt изначально умеет определять людей, нужно это подогнать под PUBG.

Нажмите, чтобы раскрыть...

Чтобы не обучать с нуля всю модель существует метод Transfer learning (дообучение). Для этого я замораживаю первые слои и провожу обучение на датасете из 287 изображений. На втором этапе размораживаю первые слои у полученной модели и провожу еще раз обучение.
После этого модель должна как бы подстроиться под PUBG и трекать лучше чем предобученная.

*Но на практике я без понятий какие именно слои нужно заморозить. Помимо этого есть шаг обучения, который я не знаю как работает и какие параметры указывать. Поэтому у меня после 2 этапа модель ломается и начинает галлюцинировать. Но на 1 этапе все проходит нормально и результаты обучения хорошие.

Нажмите, чтобы раскрыть...

​

Датасет.​​
Датасет я собрал маленький, 287 изображений. В основном делал скриншоты из реплеев от лица «свободной камеры», с полностью отключенным интерфейсом, где видно персонажей, иногда делал скриншоты от «1 лица». Или такие, где нет персонажей и никакой разметки соответственно, ~10% датасета. Старался делать «неудобные» скриншоты, где персонаж в окне, за препядствием, сливается с местностью или за сеточным забором.





В PUBG выставил низкие настройки графики.



Обучение.​​


Выше указывал по какому методу провожу обучение. imgsz =1280 чтобы вдали YOLO мог увидеть персонажа. При стандартном 640 это невозможно. После обучения при 1280 модель можно использовать в 640. Наоборот такой мув не пройдет.

Параметры lr0 и lrf отвечают за начальную скорость(шаг) обучения и финальную. На сколько понял, эти параметры нужно уменьшать чтобы за каждую эпоху модель не подвергалась сильным изменениям. Если изменения будут значительными, то есть вероятность, что значение некоторых параметров улетят за пределы нормы и дальше обучение не эффективно. Но я так и не понял почему к концу обучения скорость нужно увеличивать, но так рекомендуют делать в официальной документации YOLO.

from ultralytics import YOLO

def main():
model = YOLO('yolo11s.pt')
results = model.train(
data='data4.yaml',
epochs=50,
batch=6,
imgsz=1280,
freeze=10,
lr0=0.001,
device=0,
)

if __name__ == '__main__':
main()

Python

Код:
from ultralytics import YOLO

def main():
    model = YOLO('yolo11s.pt')
    results = model.train(
        data='data4.yaml',
        epochs=50,
        batch=6,
        imgsz=1280,
        freeze=10,
        lr0=0.001,
        device=0,
    )

if __name__ == '__main__':
    main()
Результат.





Все наштяк. Loss ползет вниз, mAP50 ползет вверх. И во время игры нормально справляется, для тестов пойдет.

from ultralytics import YOLO

def main():
model = YOLO('best.pt')

results = model.train(

data='data4.yaml',
epochs=25,
batch=4,
imgsz=1280,
device=0,

freeze=0,
lr0=0.0001, # скорость вначале
lrf=0.1, # скорость в конце
)

if __name__ == '__main__':
main()

Python

Код:
from ultralytics import YOLO

def main():
    model = YOLO('best.pt')

    results = model.train(
 
        data='data4.yaml',
        epochs=25,
        batch=4,
        imgsz=1280,
        device=0,
 
        freeze=0,
        lr0=0.0001,                  # скорость вначале
        lrf=0.1,                     # скорость в конце
    )

if __name__ == '__main__':
    main()
Результаты.




Тут все плохо, поэтому довольствуюсь результатом после 1 этапа. Я не знаю почему, может датасет маленький, может ошибка в разметке, может я че то не так понял из документации.

*По хорошему подходит модель или нет можно только на парктике. Даже если mAP50 у меня 0.94 (находит 94 объекта из 100) не значит, что все окей. По ходу игры становится ясно, где есть пробелы в обучении и каких изображений не хватает в валдиации и обучении. Хорошие / цыганские / красивые метрики могут быть просто из за удобных изображений в валидации.

Нажмите, чтобы раскрыть...

​

HumanizedTrajectoryGen.onnx.​​
Какая то генеративная нейросеть, обученная на работе человека в браузере.
Я ей на вход подаю координаты центра экрана и координаты бокса, + там есть нормализация потому что для нейросети нет такого понятия как «разрешение». Она знает только про -1, 1 и то что после запятой еще есть 16 знаков. И нормализация приводит 1920х1080 к -1х1.
На выходе массив точек от start до end.
Есть возможность указывать кол-во точек в траектории (максимум 40) и стохастичность («шум» из которого генерируется траектория), то есть если в двух случаях start и end одинаковые, траектория может быть разной.

В моей реализации код не запустится если ардуино не подключен.

Нажмите, чтобы раскрыть...






Архитектура WPreal_chlenix.​​


Config.​​
SCREEN_WIDTH = 1920
SCREEN_HEIGHT = 1080
MODEL_PATH = 'models/wpreal7_1.pt'
CONF = 0.6
IMGSZ = 1280
TARGET_FPS = 45
CAPTURE_FRAME_INTERVAL = 1.0 / TARGET_FPS
YOLO_MIN_INTERVAL = 1.0 / TARGET_FPS
UPDATE_INTERVAL_MS = 12
IDLE_SLEEP = 0.001
SPIN_SLEEP = 0.001
LABEL_TEXT = "4ELIK"
LABEL_FONT = ('Arial', 12, 'bold')
BOX_OUTLINE = 'red'
BOX_WIDTH = 3
USE_ARDUINO = True
ARDUINO_BAUD_RATE = 115200
FOV_RADIUS = 300
TRAJECTORY_MODEL_PATH = 'models/HumanizedTrajectoryGen.onnx'

Общий принцип работы.​​

Через DXcam делаю захват экрана, указываю нужный мне ФПС в config. Кадр сохраняется впеременную и хранится там пока не появится следующий кадр. Эта переменная нужна чтобы YOLO забрал из нее изображение на обработку.

CameraCapture._latest_frame — переменная для хранения последнего кадра.

Detection_loop — берет кадр из вышеуказанной переменной.

Если YOLO долго обрабатывает изображение, то следующий кадр будет самый свежий из вышеуказанной переменной. И наоборот если YOLO обработал изображение быстро и в переменной лежит то же самое
изображение, то он прогонит его еще раз :). Это может быть в случае если ФПС захвата экрана очень низкий. Изображение лежит в переменной долго, + одно и то же изображение в
ряд YOLO обрабатывать быстрее (2-5мс) и тогда YOLO обрабатывает 60-70 изображений в секунду при условии, что захват экрана делает 20 скриншотов в секунду. Поэтому когда увеличивается число кадров захвата экрана, ФПС YOLO потихоньку начинает падать, хотя по идеи должно быть наоборот.

YOLO обрабатывает кадр и хранит координаты бокса (x1, y1, x2, y2), центр бокса.
Он по КД обрабатывает кадры из переменной. Я уже знаю что при инференсе imgsz=1280
YOLO прогоняет примерно 28-35 уникальных изображений с КД между изображениями 0,001 с. При этом есть смысл ставить захват экрана на 60 фпс чтобы изображения были максимально свежие, это будет в среднем свежее чем при захвате экрана в 30 или 45 фпс.
Про инференс в imgsz=640 нет смысла что то писать. Это очень быстро, но персонажей видит хуже. Я никак не реализовываю это на практике.

IDLE_SLEEP = 0.001 — задержка между обработкой кадров.

SPIN_SLEEP = 0.001 — время ожидания YOLO-ой появления кадра в переменной. Но у меня всегда в переменной лежит кадр и он удаляется только после появления нового. Кароче у меня это никак не используется.

MODEL_PATH = 'models/wpreal7_1.pt' — путь к модели YOLO.

CONF = 0.6 — уверенность.

IMGSZ = 1280 — размер изображения на вход.

Уже писал выше, что поверх игры у меня прозрачный оверлей. Работает через Tkinter. Он нужен чтобы я визуально видел боксы. Его ФПС зависит только от YOLO, как только YOLO обработал изображение обновляется оврлей с соответствующими боксами и информацией о центре бокса.

Боксы мне нужны для наглядности, а центр бокса передается в HumanizedTrajectoryGen.onnx для генерации траектории. Выше писал как это работает.

На этом этапе пакет с к траекторией отправляется в arduino. Траектория не
отправляется целиком, а только с линией между 2 точками. Условно в траектории 40 точек, отправится 20 пакетов, если 20 точек, отправится 10 пакетов. Можно отправить целиком траекторию. В моей текущей реализации нет разницы.

В это время в arduino прописан скетч, который читает пакеты и двигает курсор от центра экрана до центра бокса.

В коносоли отображается вся необходимая информация:​

  • Подключение к arduino.
  • ФПС захвата экрана, инференса YOLO, оверлея.
  • Сгенерированная траектория, плавность наводки



​

Реализация модулей.​​
Все начинается с захвата экрана в отдельном потоке через Dxcam в camera.py. Сделанный скриншот хранится в переменной пока его не заберет YOLO.

# src/wpreal/core/camera.py

import threading
import time
import dxcam
import numpy as np
from typing import Optional

class CameraCapture:

def __init__(self, fps: int = 60):
self.fps = fps
self._running = False
self._latest_frame: Optional[np.ndarray] = None
self._lock = threading.Lock()
self._camera = None
self._capture_count = 0
self._capture_time = time.time()
self._capture_fps = 0

def start(self):
self._running = True
self._camera = dxcam.create(output_color="BGR")
threading.Thread(target=self._capture_loop, daemon=True).start()

def stop(self):
self._running = False

def get_frame(self) -> Optional[np.ndarray]:
with self._lock:
return self._latest_frame.copy() if self._latest_frame is not None else None

def get_fps(self) -> int:

return self._capture_fps

def _capture_loop(self):
next_frame_time = time.perf_counter()
frame_interval = 1.0 / self.fps

while self._running:
current_time = time.perf_counter()
if current_time < next_frame_time:
time.sleep(0.001)
continue

frame = self._camera.grab()
if frame is not None:
with self._lock:
self._latest_frame = frame

self._capture_count += 1
now = time.time()
if now - self._capture_time >= 1.0:
self._capture_fps = self._capture_count
self._capture_count = 0
self._capture_time = now

next_frame_time += frame_interval
if next_frame_time < time.perf_counter():
next_frame_time = time.perf_counter() + frame_interval

Python

Код:
# src/wpreal/core/camera.py

import threading
import time
import dxcam
import numpy as np
from typing import Optional

class CameraCapture:
 
    def __init__(self, fps: int = 60):
        self.fps = fps
        self._running = False
        self._latest_frame: Optional[np.ndarray] = None
        self._lock = threading.Lock()
        self._camera = None
        self._capture_count = 0
        self._capture_time = time.time()
        self._capture_fps = 0
 
    def start(self):
        self._running = True
        self._camera = dxcam.create(output_color="BGR")
        threading.Thread(target=self._capture_loop, daemon=True).start()
 
    def stop(self):
        self._running = False
 
    def get_frame(self) -> Optional[np.ndarray]:
        with self._lock:
            return self._latest_frame.copy() if self._latest_frame is not None else None
 
    def get_fps(self) -> int:

        return self._capture_fps
 
    def _capture_loop(self):
        next_frame_time = time.perf_counter()
        frame_interval = 1.0 / self.fps
 
        while self._running:
            current_time = time.perf_counter()
            if current_time < next_frame_time:
                time.sleep(0.001)
                continue
 
            frame = self._camera.grab()
            if frame is not None:
                with self._lock:
                    self._latest_frame = frame
 
            self._capture_count += 1
            now = time.time()
            if now - self._capture_time >= 1.0:
                self._capture_fps = self._capture_count
                self._capture_count = 0
                self._capture_time = now
 
            next_frame_time += frame_interval
            if next_frame_time < time.perf_counter():
                next_frame_time = time.perf_counter() + frame_interval
Detector.py загружает модель YOLO, обрабатывает кадр и возвращает обнаруженный класс. Хранит координаты бокса (x1, y1, x2, y2), центр бокса.

# src/wpreal/core/detector.py
import time
import numpy as np
from ultralytics import YOLO
from typing import List, Tuple, Optional
from dataclasses import dataclass

@dataclass
class Detection:
x1: int
y1: int
x2: int
y2: int
confidence: float
class_id: int

@property
def center(self) -> Tuple[int, int]:
"""Центр бокса"""
return ((self.x1 + self.x2) // 2, (self.y1 + self.y2) // 2)

@property
def width(self) -> int:
return self.x2 - self.x1

@property
def height(self) -> int:
return self.y2 - self.y1

def to_tuple(self) -> Tuple[int, int, int, int]:
return (self.x1, self.y1, self.x2, self.y2)

class YOLODetector:

def __init__(self, model_path: str, confidence: float = 0.5, image_size: int = 640):
self.model = YOLO(model_path)
self.model.model.half() # FP16
self.confidence = confidence
self.image_size = image_size
self._yolo_count = 0
self._yolo_time = time.time()
self._yolo_fps = 0

def detect(self, frame: np.ndarray) -> List[Detection]:

if frame is None:
return []

results = self.model(
frame,
conf=self.confidence,
imgsz=self.image_size,
verbose=False,
half=True
)

detections = []
if results[0].boxes is not None:
for box in results[0].boxes:
x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
detections.append(Detection(
x1=x1, y1=y1, x2=x2, y2=y2,
confidence=float(box.conf[0]),
class_id=int(box.cls[0])
))

# Подсчет FPS
self._yolo_count += 1
now = time.time()
if now - self._yolo_time >= 1.0:
self._yolo_fps = self._yolo_count
self._yolo_count = 0
self._yolo_time = now

return detections

def get_fps(self) -> int:
return self._yolo_fps

def filter_by_class(self, detections: List[Detection], class_ids: List[int]) -> List[Detection]:
return [d for d in detections if d.class_id in class_ids]

Python

Код:
# src/wpreal/core/detector.py
import time
import numpy as np
from ultralytics import YOLO
from typing import List, Tuple, Optional
from dataclasses import dataclass

@dataclass
class Detection:
    x1: int
    y1: int
    x2: int
    y2: int
    confidence: float
    class_id: int
    
    @property
    def center(self) -> Tuple[int, int]:
        """Центр бокса"""
        return ((self.x1 + self.x2) // 2, (self.y1 + self.y2) // 2)
    
    @property
    def width(self) -> int:
        return self.x2 - self.x1
    
    @property
    def height(self) -> int:
        return self.y2 - self.y1
    
    def to_tuple(self) -> Tuple[int, int, int, int]:
        return (self.x1, self.y1, self.x2, self.y2)

class YOLODetector:

    def __init__(self, model_path: str, confidence: float = 0.5, image_size: int = 640):
        self.model = YOLO(model_path)
        self.model.model.half()  # FP16
        self.confidence = confidence
        self.image_size = image_size
        self._yolo_count = 0
        self._yolo_time = time.time()
        self._yolo_fps = 0
    
    def detect(self, frame: np.ndarray) -> List[Detection]:

        if frame is None:
            return []
        
        results = self.model(
            frame,
            conf=self.confidence,
            imgsz=self.image_size,
            verbose=False,
            half=True
        )
        
        detections = []
        if results[0].boxes is not None:
            for box in results[0].boxes:
                x1, y1, x2, y2 = map(int, box.xyxy[0].tolist())
                detections.append(Detection(
                    x1=x1, y1=y1, x2=x2, y2=y2,
                    confidence=float(box.conf[0]),
                    class_id=int(box.cls[0])
                ))
        
        # Подсчет FPS
        self._yolo_count += 1
        now = time.time()
        if now - self._yolo_time >= 1.0:
            self._yolo_fps = self._yolo_count
            self._yolo_count = 0
            self._yolo_time = now
        
        return detections
    
    def get_fps(self) -> int:
        return self._yolo_fps
    
    def filter_by_class(self, detections: List[Detection], class_ids: List[int]) -> List[Detection]:
        return [d for d in detections if d.class_id in class_ids]
Полученные результаты от Detector.py отправляются в overlay.py. Он нужен для создания прозрачного окна, на котором отрисовываются боксы через tkinter. + на выходе число кадров с отрисованными боксами.

# src/wpreal/gui/overlay.py
import time
import tkinter as tk
from typing import List
from ..core.detector import Detection
from ..config import LABEL_TEXT, LABEL_FONT, BOX_OUTLINE, BOX_WIDTH

class GUIOverlay:

def __init__(self, screen_width: int, screen_height: int):
self.screen_width = screen_width
self.screen_height = screen_height
self.root = None
self.canvas = None
self.running = True
self._gui_count = 0
self._gui_time = time.time()
self._gui_fps = 0

self._init_gui()

def _init_gui(self):

self.root = tk.Tk()
self.root.title("PUBG Overlay")

self.root.attributes('-topmost', True)
self.root.attributes('-fullscreen', True)
self.root.attributes('-transparentcolor', 'black')
self.root.configure(bg='black')
self.root.overrideredirect(True)

self.canvas = tk.Canvas(
self.root,
width=self.screen_width,
height=self.screen_height,
bg='black',
highlightthickness=0
)
self.canvas.pack()

def update(self, detections: List[Detection]):

if not self.running:
return

self._gui_count += 1
now = time.time()
if now - self._gui_time >= 1.0:
self._gui_fps = self._gui_count
self._gui_count = 0
self._gui_time = now

self.canvas.delete("all")

for det in detections:
self.canvas.create_rectangle(
det.x1, det.y1, det.x2, det.y2,
outline=BOX_OUTLINE,
width=BOX_WIDTH
)
self.canvas.create_text(
det.x1, det.y1 - 10,
text=LABEL_TEXT,
fill=BOX_OUTLINE,
anchor='sw',
font=LABEL_FONT
)

self.root.update()

def get_fps(self) -> int:
return self._gui_fps

def start(self):

self.root.mainloop()

def stop(self):

self.running = False
if self.root:
self.root.quit()
self.root.destroy()

Python

Код:
# src/wpreal/gui/overlay.py
import time
import tkinter as tk
from typing import List
from ..core.detector import Detection
from ..config import LABEL_TEXT, LABEL_FONT, BOX_OUTLINE, BOX_WIDTH

class GUIOverlay:

    def __init__(self, screen_width: int, screen_height: int):
        self.screen_width = screen_width
        self.screen_height = screen_height
        self.root = None
        self.canvas = None
        self.running = True
        self._gui_count = 0
        self._gui_time = time.time()
        self._gui_fps = 0
 
        self._init_gui()
 
    def _init_gui(self):

        self.root = tk.Tk()
        self.root.title("PUBG Overlay")
 
        self.root.attributes('-topmost', True)
        self.root.attributes('-fullscreen', True)
        self.root.attributes('-transparentcolor', 'black')
        self.root.configure(bg='black')
        self.root.overrideredirect(True)
 
        self.canvas = tk.Canvas(
            self.root,
            width=self.screen_width,
            height=self.screen_height,
            bg='black',
            highlightthickness=0
        )
        self.canvas.pack()
 
    def update(self, detections: List[Detection]):

        if not self.running:
            return
 
        self._gui_count += 1
        now = time.time()
        if now - self._gui_time >= 1.0:
            self._gui_fps = self._gui_count
            self._gui_count = 0
            self._gui_time = now
 
        self.canvas.delete("all")
 
        for det in detections:
            self.canvas.create_rectangle(
                det.x1, det.y1, det.x2, det.y2,
                outline=BOX_OUTLINE,
                width=BOX_WIDTH
            )
            self.canvas.create_text(
                det.x1, det.y1 - 10,
                text=LABEL_TEXT,
                fill=BOX_OUTLINE,
                anchor='sw',
                font=LABEL_FONT
            )
 
        self.root.update()
 
    def get_fps(self) -> int:
        return self._gui_fps
 
    def start(self):

        self.root.mainloop()
 
    def stop(self):

        self.running = False
        if self.root:
            self.root.quit()
            self.root.destroy()
На этом этапе у меня есть информация о том где находятся песонажи в реальном времени. Задержка примерно 8-15мс. Как только я получаю положение персонажа в моем разрешении.

Эти данные отправляются в trajectory.py для генерации траектории для наводки через HumanizedTrajectoryGen.onnx. На
вход подаю координаты пикселей: центр экрана (start) и центр бокса (end), они нормализуются под нейросеть (из 1920х1080 в -1х1), генерируется траектория и денормализация под изначальное разрешение (-1х1 в 1920х1080).

# src/wpreal/aimbot/trajectory.py

import numpy as np
import onnxruntime as ort
from typing import List, Tuple, Optional

class TrajectoryGenerator:

def __init__(self, model_path: Optional[str] = None, screen_width: int = 1920, screen_height: int = 1080):
self.session = None
self.screen_width = screen_width
self.screen_height = screen_height

if model_path:
try:
self.session = ort.InferenceSession(model_path)
print(" Модель траекторий загружена")
except Exception as e:
print(f" Ошибка загрузки модели траекторий: {e}")
print("Будет использовано линейное наведение")

def generate(self, start: Tuple[int, int], end: Tuple[int, int]) -> List[Tuple[int, int]]:

if self.session is None:
return [start, end]

# Нормализация
norm_start = self._normalize(start)
norm_end = self._normalize(end)

input_data = np.array([norm_start, norm_end], dtype=np.float32)
input_tensor = input_data.reshape(1, 2, 2)
outputs = self.session.run(None, {"lstm_input": input_tensor})
raw_trajectory = outputs[0][0]

# Денормализация
trajectory = [
self._denormalize(p)
for p in raw_trajectory
]

if len(trajectory) > 0 and trajectory[-1] != end:
trajectory.append(end)

return trajectory

def _normalize(self, point: Tuple[int, int]) -> List[float]:
return [
(point[0] / (self.screen_width / 2)) - 1,
(point[1] / (self.screen_height / 2)) - 1
]

def _denormalize(self, point: List[float]) -> Tuple[int, int]:

x = int(((point[0] + 1) / 2) * self.screen_width)
y = int(((point[1] + 1) / 2) * self.screen_height)
return (x, y)

Python

Код:
# src/wpreal/aimbot/trajectory.py

import numpy as np
import onnxruntime as ort
from typing import List, Tuple, Optional

class TrajectoryGenerator:

    def __init__(self, model_path: Optional[str] = None, screen_width: int = 1920, screen_height: int = 1080):
        self.session = None
        self.screen_width = screen_width
        self.screen_height = screen_height
 
        if model_path:
            try:
                self.session = ort.InferenceSession(model_path)
                print(" Модель траекторий загружена")
            except Exception as e:
                print(f" Ошибка загрузки модели траекторий: {e}")
                print("Будет использовано линейное наведение")
 
    def generate(self, start: Tuple[int, int], end: Tuple[int, int]) -> List[Tuple[int, int]]:

        if self.session is None:
            return [start, end]
 
        # Нормализация
        norm_start = self._normalize(start)
        norm_end = self._normalize(end)
 
        input_data = np.array([norm_start, norm_end], dtype=np.float32)
        input_tensor = input_data.reshape(1, 2, 2)
        outputs = self.session.run(None, {"lstm_input": input_tensor})
        raw_trajectory = outputs[0][0]
 
        # Денормализация
        trajectory = [
            self._denormalize(p)
            for p in raw_trajectory
        ]
 
        if len(trajectory) > 0 and trajectory[-1] != end:
            trajectory.append(end)
 
        return trajectory
 
    def _normalize(self, point: Tuple[int, int]) -> List[float]:
        return [
            (point[0] / (self.screen_width / 2)) - 1,
            (point[1] / (self.screen_height / 2)) - 1
        ]
 
    def _denormalize(self, point: List[float]) -> Tuple[int, int]:

        x = int(((point[0] + 1) / 2) * self.screen_width)
        y = int(((point[1] + 1) / 2) * self.screen_height)
        return (x, y)

arduino_initialization.py. При первом запуске скрипта происходит подключение к порту с arduino. Если не удается подключиться код не запускается.

Нажмите, чтобы раскрыть...

# src/wpreal/arduino_initialization.py
import serial
import serial.tools.list_ports
import time

def find_arduino_port():

ports = serial.tools.list_ports.comports()
for port in ports:
if "Arduino" in port.description or "Arduino" in port.manufacturer:
return port.device
# if port.vid == 0x2341 and port.pid == 0x8037: # Pro Micro
# return port.device

print(" Не удалось найти Arduino.")
return None

def connect_to_arduino(baud_rate=115200):

arduino_port = find_arduino_port()
if not arduino_port:
return None

try:
ser = serial.Serial(arduino_port, baud_rate, timeout=1, write_timeout=5)
time.sleep(2)
print(f" Подключено к {arduino_port} со скоростью {baud_rate}")
return ser
except serial.SerialException as e:
print(f" Не удалось открыть порт {arduino_port}: {e}")
return None

Python

Код:
# src/wpreal/arduino_initialization.py
import serial
import serial.tools.list_ports
import time

def find_arduino_port():

    ports = serial.tools.list_ports.comports()
    for port in ports:
        if "Arduino" in port.description or "Arduino" in port.manufacturer:
            return port.device
        # if port.vid == 0x2341 and port.pid == 0x8037:  # Pro Micro
        #     return port.device
 
    print(" Не удалось найти Arduino.")
    return None

def connect_to_arduino(baud_rate=115200):

    arduino_port = find_arduino_port()
    if not arduino_port:
        return None
 
    try:
        ser = serial.Serial(arduino_port, baud_rate, timeout=1, write_timeout=5)
        time.sleep(2)
        print(f" Подключено к {arduino_port} со скоростью {baud_rate}")
        return ser
    except serial.SerialException as e:
        print(f" Не удалось открыть порт {arduino_port}: {e}")
        return None
На этом этапе пакет с к траекторией отправляется в arduino. Траектория не отправляется целиком, а только с линией между 2 точками. Условно в траектории 40 точек, отправится 20 пакетов, если 20 точек, отправится 10 пакетов. Можно отправить целиком траекторию. В моей текущей реализации нет разницы.

# src/wpreal/aimbot/aimbot.py

import math
import time
from typing import List, Tuple, Optional
from ..core.detector import Detection
from ..core.mouse import MouseController
from .trajectory import TrajectoryGenerator

class Aimbot:

def __init__(self, mouse: MouseController, trajectory: TrajectoryGenerator,
screen_width: int, screen_height: int, fov_radius: int = 300):
self.mouse = mouse
self.trajectory = trajectory
self.screen_width = screen_width
self.screen_height = screen_height

self.enabled = True
self.fov_radius = fov_radius
self.smoothness = 0.3
self.active = False

# Screen center
self.screen_center = (screen_width // 2, screen_height // 2)

def activate(self):
self.active = True
print("Aimbot активирован!")

def deactivate(self):
self.active = False

def find_best_target(self, detections: List[Detection]) -> Optional[Detection]:

if not detections:
return None

best = None
best_dist = float('inf')

for det in detections:
center = det.center
dist = math.sqrt(
(center[0] - self.screen_center[0]) ** 2 +
(center[1] - self.screen_center[1]) ** 2
)

if dist <= self.fov_radius and dist < best_dist:
best_dist = dist
best = det

return best

def aim_at(self, target: Detection) -> bool:

if not self.enabled or not self.active:
return False

target_center = target.center

trajectory = self.trajectory.generate(self.screen_center, target_center)

print(f"Траектория: {len(trajectory)} точек")
print(f"Цель: {target_center}")

for i in range(1, len(trajectory)):
prev = trajectory[i-1]
curr = trajectory[i]

dx = curr[0] - prev[0]
dy = curr[1] - prev[1]

self.mouse.move(dx, dy)
time.sleep(0.001)

self.active = False
print(f"Наведение завершено!")
return True

Python

Код:
# src/wpreal/aimbot/aimbot.py

import math
import time
from typing import List, Tuple, Optional
from ..core.detector import Detection
from ..core.mouse import MouseController
from .trajectory import TrajectoryGenerator

class Aimbot:

    def __init__(self, mouse: MouseController, trajectory: TrajectoryGenerator,
                 screen_width: int, screen_height: int, fov_radius: int = 300):
        self.mouse = mouse
        self.trajectory = trajectory
        self.screen_width = screen_width
        self.screen_height = screen_height
 
        self.enabled = True
        self.fov_radius = fov_radius
        self.smoothness = 0.3
        self.active = False
 
        # Screen center
        self.screen_center = (screen_width // 2, screen_height // 2)
 
    def activate(self):
        self.active = True
        print("Aimbot активирован!")
 
    def deactivate(self):
        self.active = False
 
    def find_best_target(self, detections: List[Detection]) -> Optional[Detection]:

        if not detections:
            return None
 
        best = None
        best_dist = float('inf')
 
        for det in detections:
            center = det.center
            dist = math.sqrt(
                (center[0] - self.screen_center[0]) ** 2 +
                (center[1] - self.screen_center[1]) ** 2
            )
 
            if dist <= self.fov_radius and dist < best_dist:
                best_dist = dist
                best = det
 
        return best
 
    def aim_at(self, target: Detection) -> bool:

        if not self.enabled or not self.active:
            return False
 
        target_center = target.center
 
        trajectory = self.trajectory.generate(self.screen_center, target_center)
 
        print(f"Траектория: {len(trajectory)} точек")
        print(f"Цель: {target_center}")
 
        for i in range(1, len(trajectory)):
            prev = trajectory[i-1]
            curr = trajectory[i]
 
            dx = curr[0] - prev[0]
            dy = curr[1] - prev[1]
 
            self.mouse.move(dx, dy)
            time.sleep(0.001)
 
        self.active = False
        print(f"Наведение завершено!")
        return True

# src/wpreal/core/mouse.py

from abc import ABC, abstractmethod
import serial
import ctypes
import time
from typing import Optional

class MouseController(ABC):

@abstractmethod
def move(self, dx: int, dy: int):

pass

@abstractmethod
def click(self, button: str = "left"):
pass

class ArduinoMouse(MouseController):

def __init__(self, ser: serial.Serial = None):
self.ser = ser
if self.ser:
print(f" Arduino готов к работе!")

def move(self, dx: int, dy: int):
if self.ser is None:
return

dx = max(-127, min(127, dx))
dy = max(-127, min(127, dy))

try:
self.ser.write(f"{dx},{dy}\n".encode())
except Exception as e:
print(f"Ошибка отправки: {e}")

def click(self, button: str = "left"):
if self.ser is None:
return
try:
self.ser.write(b"CLICK\n")
except Exception:
pass

Python

Код:
# src/wpreal/core/mouse.py

from abc import ABC, abstractmethod
import serial
import ctypes
import time
from typing import Optional

class MouseController(ABC):
 
    @abstractmethod
    def move(self, dx: int, dy: int):

        pass
 
    @abstractmethod
    def click(self, button: str = "left"):
        pass

class ArduinoMouse(MouseController):
 
    def __init__(self, ser: serial.Serial = None):
        self.ser = ser
        if self.ser:
            print(f" Arduino готов к работе!")
 
    def move(self, dx: int, dy: int):
        if self.ser is None:
            return
 
        dx = max(-127, min(127, dx))
        dy = max(-127, min(127, dy))
 
        try:
            self.ser.write(f"{dx},{dy}\n".encode())
        except Exception as e:
            print(f"Ошибка отправки: {e}")
 
    def click(self, button: str = "left"):
        if self.ser is None:
            return
        try:
            self.ser.write(b"CLICK\n")
        except Exception:
            pass
В arduino прописан скетч, который читает пакеты и двигает курсор.

#include <Mouse.h>

String inputString = "";

void setup() {
Serial.begin(115200);
Mouse.begin();
}

void loop() {
while (Serial.available()) {
char c = Serial.read();

if (c == '\n') {
int comma = inputString.indexOf(',');

if (comma != -1) {
int dx = inputString.substring(0, comma).toInt();
int dy = inputString.substring(comma + 1).toInt();
Mouse.move(dx, dy);
}

inputString = "";
} else {
inputString += c;
}
}

C

Код:
#include <Mouse.h>

String inputString = "";

void setup() {
  Serial.begin(115200);
  Mouse.begin();
}

void loop() {
  while (Serial.available()) {
    char c = Serial.read();

    if (c == '\n') {
      int comma = inputString.indexOf(',');

      if (comma != -1) {
        int dx = inputString.substring(0, comma).toInt();
        int dy = inputString.substring(comma + 1).toInt();
        Mouse.move(dx, dy);
      }

      inputString = "";
    } else {
      inputString += c;
    }
  }
В main.py вызываю все функции. Запускаю потоки, горячие клавиши (F/f или боковая кнопка мыши — для срабатывания aimbot-а, K/k — выход). Отображаю fps в консоли.

[CODE=python]# src/wpreal/core.py

import threading
import keyboard
import time
from pynput.mouse import Listener, Button

from src.wprealOOP.config import *
from src.wprealOOP.core.camera import CameraCapture
from src.wprealOOP.core.detector import YOLODetector
from src.wprealOOP.core.mouse import ArduinoMouse
from src.wprealOOP.aimbot.trajectory import TrajectoryGenerator
from src.wprealOOP.aimbot.aimbot import Aimbot
from src.wprealOOP.gui.overlay import GUIOverlay
from src.wprealOOP.utils.arduino_initialization import connect_to_arduino

class Wpreal:

def __init__(self):
self._init_components()
self._setup_hotkeys()
self._start_threads()
self._print_info()
self._update_loop()

def _init_components(self):
self.running = True
self.camera = CameraCapture(fps=TARGET_FPS)
self.camera.start()
self.detector = YOLODetector(
MODEL_PATH,
CONF,
IMGSZ
)

ser = connect_to_arduino(ARDUINO_BAUD_RATE)
if ser:
self.mouse = ArduinoMouse(ser)
print("Arduino готов к работе!")
else:
print("Arduino НЕ ПОДКЛЮЧЕН! Выход")
exit(1)
self.trajectory = TrajectoryGenerator(
TRAJECTORY_MODEL_PATH,
SCREEN_WIDTH,
SCREEN_HEIGHT
)

self.aimbot = Aimbot(
self.mouse,
self.trajectory,
SCREEN_WIDTH,
SCREEN_HEIGHT,
FOV_RADIUS
)

# GUI
self.gui = GUIOverlay(
SCREEN_WIDTH,
SCREEN_HEIGHT
)

self.mouse_listener = None

self._last_update_time = time.time()

def _setup_hotkeys(self):
keyboard.add_hotkey('f', self.aimbot.activate)
keyboard.add_hotkey('F', self.aimbot.activate)
keyboard.add_hotkey('k', self.quit)
keyboard.add_hotkey('K', self.quit)

def _start_threads(self):
threading.Thread(target=self._detection_loop, daemon=True).start()

self.mouse_listener = Listener(on_click=self._on_mouse_click)
self.mouse_listener.start()

def _detection_loop(self):
last_detection_time = 0

while self.running:
current_time = time.time()

if current_time - last_detection_time < YOLO_MIN_INTERVAL:
time.sleep(SPIN_SLEEP)
continue

frame = self.camera.get_frame()
if frame is None:
time.sleep(IDLE_SLEEP)
continue

try:
detections = self.detector.detect(frame)

self.gui.update(detections)

if self.aimbot.enabled and self.aimbot.active and detections:
target = self.aimbot.find_best_target(detections)
if target:
self.aimbot.aim_at(target)

last_detection_time = current_time

except Exception as e:
print(f"Ошибка в detection_loop: {e}")
time.sleep(IDLE_SLEEP)

def _on_mouse_click(self, x, y, button, pressed):
if pressed and (button == Button.x1 or button == Button.x2):
self.aimbot.activate()

def _update_loop(self):
if not self.running:
return

now = time.time()
if now - self._last_update_time >= 1.0:
self._last_update_time = now

capture_fps = self.camera.get_fps()
yolo_fps = self.detector.get_fps()
gui_fps = self.gui.get_fps()

print(f"\033[F\033[F\033[F", end="")
print(f"\033[KЗахват: {capture_fps} FPS")
print(f"\033[KYOLO: {yolo_fps} FPS")
print(f"\033[KGUI: {gui_fps} FPS")

if self.running:
self.gui.root.after(UPDATE_INTERVAL_MS, self._update_loop)

def _print_info(self):
print("=" * 50)
print(" Wpreal запущен!")
print(f"Модель: {MODEL_PATH}")
print(f"Порог: {CONF}")
print(f" Размер: {IMGSZ}")
print(f" FOV: {FOV_RADIUS}px")
print("=" * 50)
print(" F или боковая кнопка мыши для наведения")
print(" K для выхода")
print()
print(" Захват: 0 FPS")
print(" YOLO: 0 FPS")
print(" GUI: 0 FPS")

def quit(self, event=None):
print("\nЗавершение работы...")
self.running = False

self.camera.stop()
self.gui.stop()

if hasattr(self.mouse, 'ser') and self.mouse.ser:
try:
self.mouse.ser.close()
print("Соединение с Arduino закрыто")
except:
pass

if self.mouse_listener:
self.mouse_listener.stop()

self.gui.root.quit()
self.gui.root.destroy()

def run(self):
try:
self.gui.start()
except KeyboardInterrupt:
self.quit()
finally:
self.running = False
print("\nWpreal остановлен")

if __name__ == "__main__":
app = Wpreal()
app.run()[/CODE]

Если очень коротко.​​
1. DXCam захватывает кадр.
2. Кадр сохраняется в CameraCapture._latest_frame.
3. Detection_loop берет кадр из переменной.
4.YOLO обрабатывает кадр.
5. GUI отрисовывает боксы.
6.Aimbot находит бокс.
7.HumanizedTrajectoryGen.onnx генерирует траекторию.
8.Arduino передвигает курсор.

Выводы.​​
Чит клево наводится на попки персонажей. CV значит «cock’s vision» или «петушиный взор».

И я не знаю детектится он или нет. Где то читал, что ардуино нужно скрывать, кароче нельзя его реализовать как это делаю я в статье. Я только отрисовкой боксов в игре пользуюсь где то месяц и продолжаю собирать датасет, это вроде не палевно.

Комментарии 0

Пока нет комментариев. Начните обсуждение.
Войдите, чтобы оставить комментарий.
Назад
Верх