POE 자동 물약 먹기 !! if 체력 반피시 …


* 경고 *

해당 포스팅은 교육적인 목적입니다. 절대 악용, 남용 하면 아니되옵니다.

안녕하세요 라이프온룸 입니다. 

저번 포스팅에 업그레이드를 가미해서 이번엔 OpenCV 를 이용해서 체력 혹은 마나가 반토막이 되었을 때 물약을 자동으로 먹는 파이썬 코드를 만들어 봤어요 ! 참고로 해당기능은 주 모니터에서만 동작 합니다. 듀얼 모니터 쓰시는 분들은 Sub 모니터에서는 동작 안되실 거에요 !! 

우선 들어가기 이전에 추가되거나 변경된 파일에 대해 알아볼게요

  • drinkportion.py – 변경, 영상처리 결과로 특정 키를 누르도록 변경되었습니다. 
  • config.cfg – 변경, 영상처리 그리고 임계점 Trigger시 누를 키보드 값에 대한 옵션이 추가되었습니다. 
  • cvFunc.py – 추가, 영상처리에 관한 내용입니다.

이제 설치해야할 라이브러리를 알아봅시다 !

1. 라이브러리

pip install opencv-python
pip install pillow
pip install numpy
pip install mss
pip install pyautogui==0.9.38

여기선 pyautogui 의 경우 특정 버전을 지정 했는데요 ! pyautogui  최신 버전의 경우 한글 윈도우에선 설치 시 에러가 납니다… 그래서 위에 언급한 버전을 설치해야 정상 동작 합니다.

2. config.cfg

[portion_drink_delay]
delay = 0.1
delay_triggered = 1

[portion_key]
k_1 = 2,3,4,5
# m_right = 5
m_middle = 4
s_hp = 1
s_mp = 5

[screen]
bgr_lower = {'sheild': (89, 132, 72), 'mp': (109, 47, 13), 'hp': (33, 94, 30)}
bgr_upper = {'sheild': (169, 199, 137), 'mp': (158, 71, 32), 'hp': (47, 147, 33)}
hpmp_region = (1818, 655, 201, 38)
#hpmp_region = (911, 319, 101, 18) #full hd
#hpmp_region = (906, 314, 107, 20) # full hd whole screen mode
hpmp_trigger = 0.5

 

프로그램의 옵션을 주는 config.cfg 파일에 대해 한번 보고 갈게요 

screen 섹션, portion_drink_delay 섹션의 delay_triggered 옵션 그리고 portion_key 섹션에 s_hp, s_mp 옵션이 추가되었습니다. 각 옵션의 의미는 아래와 같아요

  • bgr_lower, bgr_upper : sheild, mp, hp 바의 BGR 최소값과 최대값입니다. 
  • hpmp_region: sheild, mp, hp 바가 존재하는 위치입니다. 
  • hpmp_trigger: mp, hp 어느정도 떨어 졌을때 물약을 먹을거냐에 대한 수치입니다. 이 경우 50% 남았으면 물약을 먹게 됩니다. 
  • delay_triggered: mp, hp 수치가 hpmp_trigger 밑에 있을때 어느 정도의 Delay를 두고 물약을 먹을지 입니다. 이 경우 1초 입니다. 
  • s_hp, s_mp: hp, mp 가 hpmp_trigger 밑으로 내려 갈 시 어떤 키를 누를거냐에 대한 내용입니다. 이 경우 hp는 1번키, mp는 2번키입니다. 

참고로 bgr_lower, bgr_uppwer의 값은 제가 모니터 3대로 테스트를 해봤는데 위 값으로 잘 동작합니다. 그럼으로 따로 구하지 않고 위 값을 그대로 써도 될 것 같네요 !

2. 코드 

저번 포스팅에서 cvFunc.py라는 파일이 추가 되었어요. 해당 파일은 캐릭터의 피통과 마나통을 검출 하여 물약을 먹을 타이밍을 알려주는 역할을 하는데요! 추가적으로 아래 기능을 구현해놨습니다.  

  1. 특정 Image 영역의 좌표값 얻기 
  2. 특정 사진의 BRG 값의 채널별 최소값과 최대값 얻기 
  3. 이미지에서 특정 BGR 값을 갖는 사각형 검출하기 
  4. 스크린샷을 연속적으로 찍어서 피통과 마나통 검출하기 

 

cvFunc.py

#-*- coding: utf-8 -*-

from ast import literal_eval
import os
import time

import cv2.cv2 as cv2
import numpy as np
import pyautogui as pa
from PIL import Image
import copy
from mss import mss



class ScreenShot():
    finalRegion = 0
    def __init__(self, region=(0, 0, 0, 0)):
        if region == (0, 0, 0, 0):
            screen = np.array(pa.screenshot())
            self.img = screen[:, :, ::-1].copy()
        else:
            screen = np.array(pa.screenshot(region=region))
            self.img = screen[:, :, ::-1].copy()

        #self.overlay = self.img.copy()
        self.output = self.img.copy()
        self.drawing = False
        self.ix = 0
        self.iy = 0

        sz = pa.size()
        coord = (0, 0, sz[0], sz[1])
        self.scrCenter = pa.center(coord)

        self.mouseCoord = pa.position()


    def drawBlueRect(self, event, x, y, flags, param):
        #global ix, iy, drawing, mode, output

        if event == cv2.EVENT_LBUTTONDOWN:  # 마우스를 누른 상태

            self.drawing = True
            self.ix, self.iy = x, y
        elif event == cv2.EVENT_MOUSEMOVE:  # 마우스 이동
            if self.drawing == True:  # 마우스를 누른 상태 일경우
                self.output = self.img.copy()
                cv2.rectangle(self.output, (self.ix, self.iy), (x, y), (255, 0, 0), 2)

                pass

        elif event == cv2.EVENT_LBUTTONUP:
            self.drawing = False;  # 마우스를 때면 상태 변경
            cv2.rectangle(self.output, (self.ix, self.iy), (x, y), (255, 0, 0), 2)
            startX, endX = self.ix, x
            startY, endY = self.iy, y

            if x > self.ix :
                startX, endX = self.ix, x
            else:
                startX, endX = x, self.ix

            if y > self.iy:
                startY, endY = self.iy, y
            else:
                startY, endY = y, self.iy

            self.finalRegion = (startX, startY, endX - startX, endY - startY)

    def partScreenShot(self):
        cv2.namedWindow('image', cv2.WINDOW_NORMAL)
        cv2.setWindowProperty("image", cv2.WND_PROP_FULLSCREEN, cv2.WINDOW_FULLSCREEN);
        cv2.setMouseCallback('image', self.drawBlueRect)

        while True:
            cv2.imshow('image', self.output)

            k = cv2.waitKey(1) & 0xFF

            if k == 27:  # esc를 누르면 종료
                break

        cv2.destroyAllWindows()
        print("Screen Shot Region(x, y, w, h): ", self.finalRegion)
        return self.finalRegion

    def saveImage(self, fileName, region=()):
        if len(region) == 0:
            x, y, w, h = self.finalRegion
        else:
            x, y, w, h = region

        croped_img = self.img[y:y+h, x:x+w]

        path = os.path.dirname(os.path.realpath(__file__)) + '\\' + str(fileName)
        cv2.imwrite(path, croped_img)


class ScreenProcess():
    drinkTime = {}
    HPMPText = 'Text'
    recogCnt = 0

    def __init__(self, dict_bgr_lower, dict_bgr_upper, trigger=0.5, delayTriggered = 3, region = (0, 0, 0, 0)):
        self.lower = dict_bgr_lower
        self.upper = dict_bgr_upper

        for key, _ in self.upper.items():
            self.drinkTime[key] = time.time()

        self.x, self.y, self.w, self.h = region
        self.mon = {'top': self.y, 'left': self.x, 'width': self.w, 'height': self.h}
        self.sct = mss()

        self.delayTriggered = delayTriggered
        self.trigger = trigger


    def checkHPMP(self, frame, show=0, videoShow = 0):
        rects = []
        triggered = []
        for key, value in self.upper.items():

            img = copy.copy(frame)
            height, width = img.shape[:2]
            #print(height, width)
            img = cv2.GaussianBlur(img, (11, 11), 0)
            mask = cv2.inRange(img, self.lower[key], self.upper[key])
            res = cv2.bitwise_and(img, img, mask=mask)
            gray = cv2.cvtColor(res, cv2.COLOR_BGR2GRAY)
            #ret, thresh = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY)

            #kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 5))
            #erosion = cv2.erode(thresh, kernel, iterations=1)

            cnts = cv2.findContours(gray.copy(), cv2.RETR_EXTERNAL,
                                    cv2.CHAIN_APPROX_SIMPLE)[-2]

            for cnt in cnts:
                approx = cv2.approxPolyDP(cnt, 0.1 * cv2.arcLength(cnt, True), True)
                if (len(approx) >= 2):
                    # print("approx:", len(approx), "CountourArea: ", cv2.contourArea(cnt))
                    (x, y, w, h) = cv2.boundingRect(approx)
                    rects.append((x, y, w, h))
                    currentHPMP = w/(width * 1.0)
                    if currentHPMP < self.trigger and x < 10:
                        if (time.time() - self.drinkTime[key] > self.delayTriggered and key != 'sheild'):
                            self.drinkTime[key] = time.time()
                            showText = key + ' need portion!' + str(self.recogCnt)
                            print(showText)
                            self.recogCnt += 1
                            self.HPMPText = showText
                            triggered.append(key)
                    break



        if show == 1:
            showImg = copy.copy(frame)
            textImg = np.zeros((height * 2, width*2, 3), np.uint8)
            cv2.putText(textImg, self.HPMPText, (5, height+ 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), lineType=cv2.LINE_AA)
            for detectRect in rects:
                (x, y, w, h) = detectRect
                cv2.rectangle(showImg, (x, y), (x + w, y + h), (255, 0, 255), 2)

            viewImage = np.hstack([frame, showImg])
            finalView = np.vstack([viewImage, textImg])
            cv2.imshow("Frame Result", finalView)
            cv2.resizeWindow('Frame Result', 300, 300)
            #cv2.imshow("OneFrame", np.hstack([thresh, gray, erosion]))
            if videoShow == 0:
                cv2.waitKey(0)


        if show == 1 and videoShow == 0:
            cv2.destroyAllWindows()

        return triggered

    def screenProcess(self, showVideo=0):

        self.sct.grab(self.mon)

        img = Image.frombytes('RGB', (self.w, self.h), self.sct.grab(self.mon).rgb)

        frame = cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR)
        return self.checkHPMP(frame, show=showVideo, videoShow=showVideo)

        # if cv2.waitKey(25) & 0xFF == ord('q'):
        #     cv2.destroyAllWindows()
        #     break

def checkColorMinMax(listImageName):
    cmin = {}
    cmax = {}
    for imageName in listImageName:
        frame = cv2.imread(imageName)
        frame = cv2.GaussianBlur(frame, (11, 11), 0)
        max_ch = (np.amax(frame[:, :, 0]), np.amax(frame[:, :, 1]), np.amax(frame[:, :, 2]))
        min_ch = (np.amin(frame[:, :, 0]), np.amin(frame[:, :, 1]), np.amin(frame[:, :, 2]))
        specName = imageName.split('.')[0]
        cmin[specName] = min_ch
        cmax[specName] = max_ch
    print("lower = ", cmin)
    print("upper = ", cmax)


if __name__=="__main__":
    import configparser
    configFile = os.path.dirname(os.path.realpath(__file__)) + '\\' + 'config.cfg'
    config = configparser.ConfigParser()
    config.read(configFile)

    delay_triggered = int(config['portion_drink_delay']['delay_triggered'])
    brg_lower = literal_eval(config['screen']['bgr_lower'])
    bgr_upper = literal_eval(config['screen']['bgr_upper'])
    hpmp_region = literal_eval(config['screen']['hpmp_region'])
    hpmp_trigger = float(config['screen']['hpmp_trigger'])

    test = 1
    # -------- ScreenSize ------------
    if test == 1:
        ip = ScreenShot()
        ip.partScreenShot()

    # -------- Check Color BGR min max ------------
    elif test == 2:
        checkColorMinMax(["sheild.png", "mana.png", "health.png"])

    # -------- detect HP/MP on image ------------
    elif test == 3:
        frame = cv2.imread("testpoe.png")
        sp = ScreenProcess(brg_lower, bgr_upper, hpmp_trigger, delay_triggered, hpmp_region)
        sp.checkHPMP(frame, show=1)

    # -------- detect HP/MP on continuous Screen ------------
    elif test == 4:
        sp = ScreenProcess(brg_lower, bgr_upper, hpmp_trigger, delay_triggered, hpmp_region)
        while True:
            sp.screenProcess(showVideo=1)
            if cv2.waitKey(25) & 0xFF == ord('q'):
                cv2.destroyAllWindows()
                break

 

아래 Main 보면 test 변수를 바꿔서 위에서 언급했던 1~4번 기능을 테스트 해 볼 수 있습니다. !! 코드의 동작은 영상을 참고해 주세요 ㅎㅎ.. 

drinkPortion.py

import mouse as mo
import keyboard as keys
import time
import configparser
import os
from ast import literal_eval
import cv2.cv2 as cv2
#try:
import keyEvent as ke
# import screen process module 
import cvFunc as cvf
# except:
#     import POE.keyEvent as ke
#     import POE.cvFunc as cvf


try:
    import serialFunc as sf
    ser = sf.ExternalHID('COM16')
except Exception as e:
    print(e)
    print("Hardware Macro Disabled")

def generateKeyEvent(val, key_s, delay):
    if val == True:
        for outVal in key_s:
            # hardware macro
            #ser.keyboardInput(outVal)
            # software macro
            ke.press(outVal)
            time.sleep(delay)


def drinkPortionWithInput(listDevKeyOutVal, delay=0.001, sProcess=None):
    listKeyState = [0] * len(listDevKeyOutVal)
    triggereds = []

    while True:
        if sProcess != None:
      # screenProcess if triggered return data
            triggereds = sProcess.screenProcess(showVideo=1)
            if cv2.waitKeyEx(25) == ord('`'):

                cv2.destroyAllWindows()
                break

        for idx, dictDevVal in enumerate(listDevKeyOutVal):
            keyormo = list(dictDevVal.keys())[0]
            generateKeys = dictDevVal[keyormo].split(',')

            # [0] = k is keyboard [1] = pushed key
            if keyormo.split('_')[0].strip() == 'k':
                value = keys.is_pressed(keyormo.split('_')[1].strip())
                if listKeyState[idx] != value:
                    generateKeyEvent(value, generateKeys, delay)
                    listKeyState[idx] = value

            # [0] = m is mouse [1] = pushed mouse btn
            elif keyormo.split('_')[0].strip() == 'm':
                possList = [mo.LEFT, mo.RIGHT, mo.MIDDLE]
                try:
                    possList.index(keyormo.split('_')[1].strip())
                except Exception as e:
                    continue

                value = mo.is_pressed(keyormo.split('_')[1].strip())

                if listKeyState[idx] != value:
                    generateKeyEvent(value, generateKeys, delay)
                    listKeyState[idx] = value

            elif keyormo.split('_')[0].strip() == 's':
                stat = keyormo.split('_')[1].strip()
        # drink potion if HP, MP below specific percentage
                for triggered in triggereds:
                    if triggered == stat:
                        generateKeyEvent(True, generateKeys, delay)

if __name__=="__main__":
    configFile = os.path.dirname(os.path.realpath(__file__)) + '\\' + 'config.cfg'
    config = configparser.ConfigParser()
    config.read(configFile)

    itemList = []
    for option in config.options('portion_key'):
        itemList.append({option:config['portion_key'][option]})

    drinkDelay = float(config['portion_drink_delay']['delay'])
    delay_triggered = int(config['portion_drink_delay']['delay_triggered'])
    brg_lower = literal_eval(config['screen']['bgr_lower'])
    bgr_upper = literal_eval(config['screen']['bgr_upper'])
    hpmp_region = literal_eval(config['screen']['hpmp_region'])
    hpmp_trigger = float(config['screen']['hpmp_trigger'])


    print("macro start drink delay  %s " % str(drinkDelay))
  # make screenProcess instance
    sp = cvf.ScreenProcess(brg_lower, bgr_upper, hpmp_trigger, delay_triggered, hpmp_region)
    drinkPortionWithInput(itemList, drinkDelay, sp)

 

새로운 drinkPortion.py입니다. 주요 부분에 주석으로 설명 달아놨어요 ㅎㅎ 

3. 실행 

이렇게 코딩을 한 뒤 CMD 환경에서 가상환경 Enable 후 python drinkPortion 하면 코드가 실행됩니다. 그리고 영상처리 윈도우를 안뜨게 하고 싶다면 showVideo = 0 으로 변경하면 됩니다. 

넵 오늘 포스팅은 여기까지 입니다. 저는 이제 액트4 밀로 가야겠네요 ㅋㅋ 그나저나 빌드를 제 맘대로 했더니 전직하다가 게임 접을 뻔 했어요 ㅜ ㅋㅋ .. 

 

You may also like...

20 Responses

  1. 물약 댓글:

    이건 진짜 복잡하네요…혹시 파이썬 기초 초보자 강좌 같은거 하실 생각은 없으신가요? ㅎㅎ;

    • 호그 댓글:

      아직은 계획이 없습니다. ㅜㅜ ㅎㅎ 하지만 인터넷에도 파이썬 기본 강좌는 잘 되어있으니 그 글을 참고 하시는 것도 좋을 것 같습니다. ㅎㅎ
      추가적으로 앞으로는 주석 혹은 설명을 좀 더 자세하게 적어 놓을 테니 참고 부탁드릴게요 ㅎㅎㅎ

  2. GGGM 댓글:

    덕분에 아주 잘 쓰고 있습니다 ㅎㅎ 트리거딜레이를 0초로 하고 hp70%로 트리거 잡아놓으니까 안죽네요 ㅋㅋㅋ!! 참고로 이거 보시는 분들 이 페이지만 보시면 안되고 POE관련 이전 페이지부터 읽으면서 따라오셔야 해요~ KeyEvent 추가 같은거 ㅇㅇ .. 저는 HP좌표를 창모드후에 Print screen키로 따서 그림판에서 확인했습니다(그림판 좌측 하단에 좌표가 뜨거든요)

    • GGGM 댓글:

      아 혹시 착각하실까봐 위에거 전체창모드 입니다. 그리고 hpmp_region = (1818, 655, 201, 38) 여기에서 각 좌표가 의미하는 것은 hpmp_region = ( hpmp바 시작좌표 x, hpmp바 시작좌표 y, ex) 30(시작좌표 x + 30을 의미), ex) 30(시작좌표 y + 30을 의미) )
      저는 1920X1080인데 hpmp_region = (908, 344, 107, 17)하니까 딱 맞더군요 참고하세용 ㅇㅇ ..

      • 호그 댓글:

        ㅎㅎ 감사합니다. 하지만 GGG 정책에 위배되는 매크로니 영정 반드시 주의하시기 바랍니다. ㅜㅜ

      • 돌맹쓰 댓글:

        저도 이 분 수치 넣으니까 제대로 위치 잡힙니다. 1920*1080환경이구요.

        위 유튜브 설명대로 파이참에서 위치 찾으려고 하니까 파이참 살짝 내리고 ctrl+shift+f10 하니까 멈추기는 하는데

        십자가로 변하지 않고 드래그 하려니까 멈춘게 다시 풀리는 현상이 생기네요.

        몇번 시도하다 안되서 댓글 내려봤고 설마 싶어서 수치 넣어보니 잡히네요.^^

        그리고 게임 케릭터별로 mp통 25%나 50%씩 기본으로 땡겨서 보호막으로 쓰는 케릭터들이 있는데

        지금 셋팅상으로는 mp가 딸린다고 계속 나오면서 포션을 먹어대네요. hp와 mp를 구분해서 만들기는 어려울까요?

      • 돌맹쓰 댓글:

        질문 좀 드릴께요.

        hpmp_region = ( hpmp바 시작좌표 x, hpmp바 시작좌표 y, ex) 30(시작좌표 x + 30을 의미), ex) 30(시작좌표 y + 30을 의미)

        에서 ex) 30(시작좌표 x + 30을 의미), ex) 30(시작좌표 y + 30을 의미 요부분 이해가 어려운데 추가 설명 좀 부탁합니다.

        즉 908+30, 344+30 말씀하신걸로 이해했는데 107, 17은 나오질 않아서 제가 뭘 잘못 이해하고 있나 싶어서요.

        • 호그 댓글:

          hpmp_region = (x, y, w, h) 를 의미합니다.
          x = hpmp 바 시작 x 좌표
          y = hpmp 바 시작 y 좌표
          w = hpmp 바 가로 길이
          h = hpmp 바 세로 길이 입니다. ㅎㅎ

          • 돌맹쓰 댓글:

            호그님 답변 감사드립니다. 좌표가 맞는줄 알았는데 안맞더라구요.

            절묘하게 hp부분은 잡히지 않고 mp부분만 인식이 되길래 조금 올려볼까 그러고 있습니다.

            설명해주신 파이참으로 검출하는 방법은 제 머리로는 도저히 이해하는데 한계가 있어서요.ㅎㅎ

            그림판에서도 이상하게 좌표가 안나오고.. y값만 임의 변경해서 조금 올리면 작동할듯 합니다.

  3. ㅇㅇ 댓글:

    이 매크로도 잘 쓰고 있는데 혹시 1번누르면 2345 발동하는 부분이 제가 물건팔때 자꾸 2345가 눌러져서 불편한데
    이걸 ` 키 누를때 12345 연발적으로 나가게끔 바꾸고 싶은데요. 혹시 config부분에 k_1를 바꿔줘야할까요?

  4. dating sites 댓글:

    I couldn’t resist commenting. Well written!

  5. case 댓글:

    호그님 덕분에 잘사용 하고 있습니다.

    하나로 추가 질문이 있는데..

    기존에 있는 트리거 외에

    70프로 일때 물약 1번

    50프로 일때 물약 2번

    이렇게 지정을 하고 싶은데..

    트리거 구문을 추가 해줘야 되나요??

    파이썬 알못 이라 헷갈리네요 ㅠ

    염치 없지만 도움 부탁드립니다

    • 호그 댓글:

      일단 config.cfg 고치는 것 만으로는 불가능 합니다. ㅜㅜ checkHPMP 함수를 좀 고칠 필요 가 있을것 같아요 ㅜ

  6. 익명 댓글:

    호그님 잘 쓰고 있습니다.

  7. 익명 댓글:

    2560 x 1080 사용자 입니다. hp 위치는 그림판 보고 대충 한거 같은데 아무리 해도 물약버튼을 누를생각을안하네요 ㅠㅠ
    동영상은 비공개라 보지도 못하구 ㅠㅠ