Python-Based Non-Intrusive Recoil Compensation for PUBG Using Image Recognition
This article explains how to create a Python script that automatically compensates weapon recoil in PUBG by capturing the screen, recognizing equipment with OpenCV and SSIM, and moving the mouse via pynput and pydirectinput without modifying the game memory.
The tutorial describes a non‑intrusive method to control recoil in PUBG by using image recognition instead of memory manipulation, ensuring the approach remains undetectable.
Prerequisites : knowledge of the game's various weapons, their recoil patterns, and basic Python libraries such as pynput , pyautogui , opencv , and pydirectinput .
1. Listening to Keyboard and Mouse
import pynput.keyboard as keyboard
# Listener for keyboard events
def listen_keybord():
listener = keyboard.Listener(on_press=onPressed, on_release=onRelease)
listener.start()The listener runs asynchronously; long‑running handlers should be off‑loaded to avoid blocking.
2. Handling Key Releases
def onRelease(key):
try:
if '1' == key.char:
c_equipment.switch = 1 # main weapon 1
elif '2' == key.char:
c_equipment.switch = 2
elif '3' == key.char:
c_equipment.switch = 3 # pistol, no recoil compensation
elif '4' == key.char:
c_equipment.switch = 3 # knife
elif '5' == key.char:
c_equipment.switch = 3 # grenade
except AttributeError:
if 'tab' == key.name:
asyncHandle() # open backpack and start screen capture
elif 'num_lock' == key.name:
changeOpen() # toggle script on/off
elif 'shift' == key.name:
c_contants.hold = FalseSpecial keys like tab trigger asynchronous equipment detection, while num_lock controls the script state.
3. Capturing the Screen
import pyautogui
def adaptive_binarization(img):
# Adaptive thresholding for binarization
maxval = 255
blockSize = 3
C = 5
img2 = cv2.adaptiveThreshold(img, maxval, cv2.ADAPTIVE_THRESH_MEAN_C,
cv2.THRESH_BINARY, blockSize, C)
return img2
def shotCut(x, y, w, h):
im = pyautogui.screenshot(region=[x, y, w, h])
screen = cv2.cvtColor(numpy.asarray(im), cv2.COLOR_BGR2GRAY)
temp = adaptive_binarization(screen)
return temp
def saveScreen():
screen1 = shotCut(1780, 125, 614, 570)
cv2.imwrite("./resource/shotcut/screen.bmp", screen1)The captured region corresponds to the equipment panel; the image is converted to grayscale and binarized for easier comparison.
4. Preparing Reference Images
Before comparison, a collection of template images for weapon names, stocks, grips, and muzzle devices must be stored in a directory (e.g., ./resource/guns/ ).
5. Cropping the Captured Panel
# Load the previously saved screenshot
screen = cv2.imread("./resource/shotcut/screen.bmp", 0)
# Crop the weapon‑1 name area (y:0‑40, x:45‑125)
screenWepon1 = screen[0:40, 45:125]
# Compare with reference images to obtain the name
w1Name = compareAndGetName(screenWepon1, "./resource/guns/")6. Image Comparison Using SSIM
def compareAndGetName(screenImg, dir):
content = os.listdir(dir)
name = 'none'
max_score = 0
for fileName in content:
curWepone = cv2.imread(dir + fileName, 0)
res = calculate_ssim(numpy.asarray(screenImg), numpy.asarray(curWepone))
if max_score < res and res > 0.5:
max_score = res
name = fileName[:-4]
return nameThe Structural Similarity Index (SSIM) measures how closely the cropped screenshot matches each template; the highest score above 0.5 is selected.
7. SSIM Helper Function
def calculate_ssim(img1, img2):
if not img1.shape == img2.shape:
raise ValueError('Input images must have the same dimensions.')
if img1.ndim == 2:
return ssim(img1, img2)
elif img1.ndim == 3:
if img1.shape[2] == 3:
ssims = []
for i in range(3):
ssims.append(ssim(img1, img2))
return numpy.array(ssims).mean()
elif img1.shape[2] == 1:
return ssim(numpy.squeeze(img1), numpy.squeeze(img2))
else:
raise ValueError('Wrong input image dimensions.')After obtaining the weapon name, the script can also retrieve attached accessories using the same workflow.
8. Moving the Mouse to Compensate Recoil
def moveMouse():
curWepone = getCurrentWepone()
if curWepone.name == 'none':
return
basic = curWepone.basic
speed = curWepone.speed
startTime = round(time.perf_counter(), 3) * 1000
for i in range(curWepone.maxBullets):
if not canFire():
break
holdK = 1.0
if c_contants.hold:
holdK = curWepone.hold
moveSum = int(round(basic[i] * curWepone.k * holdK, 2))
while True:
if moveSum > 10:
pydirectinput.move(xOffset=0, yOffset=10, relative=True)
moveSum -= 10
elif moveSum > 0:
pydirectinput.move(xOffset=0, yOffset=moveSum, relative=True)
moveSum = 0
elapsed = (round(time.perf_counter(), 3) * 1000 - startTime)
if not canFire() or elapsed > (i + 1) * speed + 10:
break
time.sleep(0.01)The function reads the recoil pattern (e.g., a list of y‑axis offsets) for the current weapon, applies a hold‑modifier if the player is aiming, and moves the mouse in small increments to match the required compensation within each bullet interval.
9. Final Remarks
The complete source code is hosted on Gitee (https://gitee.com/lookoutthebush/PUBG). Readers are encouraged to clone the repository, test the script, and adapt the recoil data for other weapons or games.
Python Programming Learning Circle
A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.