2020年10月23日

PYTHON 實現 暗黑破壞神3 自動重鑄裝備(鍵盤監聽+滑鼠控制+Screenshot+OCR+多線程) PYTHON Implement Automatically Reforge in Diablo 3

    一、想法來由

    我最近在玩暗黑3 PTR 測試伺服器 
    結果發現要達到全身洪荒裝備 

    有兩個方向 
    1.用50血岩跟PTR商人換一個【裝備袋】 
    會掉洪荒裝備沒錯,但是一個箱子中少則6-7件裝備,多則2x件裝備,
    一箱子裡基本上有掉一件洪荒就不錯了 而且要剛好出你需要的裝備,這機率又更低啦 


     2.重鑄裝備 
    用50血岩跟PTR商人換取一個【PTR寶袋】,
    這會給1-5章所有懸賞材料 50血岩可換懸賞材料各100個+拆解裝備材料各1000個 
    而重鑄一次需要懸賞材料各5個+遺忘之魂50個 所以花50血岩換一次材料可以重鑄20次

    於是就展開了重鑄洪荒裝備之旅
    但是點著點著....10次,100次,1000次.....手好痠阿
    天啊,雖然測試伺服器掉寶率很多
    但是要出洪荒,看臉成分很大啊

    想想,全身洪荒裝備前,我手就廢了,這可不行
    我得想個好辦法來達成這件事情
    於是我找到Autohotkey來達成
    按某鍵後去點擊【放入材料】再點擊【轉化】
    但是是一次性的,所以我手還是得放在鍵盤上按
    這還是太麻煩啦,不夠懶人,不夠全自動

    於是想想,還是自己寫好了,掌控度也比較高
    會用PYTHON的考量是,我不需要視窗,加上PYTHON很多影像辨識的套件可以直接用
    於是再次展開懶人追求全自動重鑄之旅

    我想到分兩個階段來實現
    第一階段(就是先做到Autohotkey做的事情,然後再慢慢加)
    1. 鍵盤監聽(當按下某按鍵開始,案某按鍵結束)
    2. 自動重鑄(左鍵點擊放入材料後再點擊轉化)
    3. 加入While...Loop,實現一直重鑄
    4. 多線程(同時監聽鍵盤+自動重鑄)
    第二階段(我不需要坐在電腦前操作)
    1. Screenshot,只擷取角色數值區域
    2. OCR,圖片轉文字
    3. 設定目標,判定停止
    YA! 要做的事情擬定好後都感覺到那股美好了(都還沒完成呢!)


    二、運行環境

    • Windows 10 Home
    • Python 3.7.2
    • opencv 3.4.5.20
    • PIL 8.0.0
    • pyautogui 0.9.52
    • ctypes 1.1.0
    • numpy 1.19.2
    • pytesseract 0.3.6
    • easygui 0.98.1
    • pynput 1.6.8
    >>> import cv2
    >>> cv2.__version__
    '3.4.5'
    
    >>> import PIL
    >>> PIL.__version__
    '8.0.0'
    
    >>> import pyautogui
    >>> pyautogui.__version__
    '0.9.52'
    
    >>> import ctypes
    >>> ctypes.__version__
    '1.1.0'
    
    >>> import numpy
    >>> numpy.__version__
    '1.19.2'
    
    >>> import pkg_resources
    >>> pkg_resources.working_set.by_key['pytesseract'].version
    '0.3.6'
    
    >>> import pkg_resources
    >>> pkg_resources.working_set.by_key['easygui'].version
    '0.98.1'
    
    >>> import pkg_resources
    >>> pkg_resources.working_set.by_key['pynput'].version
    '1.6.8'
    

    三、Function介紹

    1.鍵盤監聽範例(參考自此篇文章

    程式碼
    # -*- coding: utf-8 -*-
    from pynput.keyboard import Controller, Key, Listener
    from pynput import keyboard
    
    #得到鍵入的值
    def get_key_name(key):
        return str(key)
    
        
    # 監聽按壓
    def on_press(key):
        global fun_start,time_interval,index,dict,count,count_dict
        print("正在按壓:", get_key_name(key))
        
    # 監聽釋放
    def on_release(key):
        global start,fun_start, time_inxterval, index,count,count_dict
        print("已經釋放:", get_key_name(key))
        
        if key == Key.esc:
            # 停止監聽
            return False
        
    # 開始監聽
    def start_listen():
        with Listener(on_press=on_press, on_release=on_release) as listener:
            listener.join()
    
    if __name__ == '__main__':
        # 開始監聽,按esc退出監聽
        start_listen()
    

    結果
    ===================== RESTART: D:/D3/Autoreforge/test.py =====================
    正在按壓: 'a' 已經釋放: 'a' 正在按壓: 'b' 已經釋放: 'b' 正在按壓: Key.space 已經釋放: Key.space 正在按壓: Key.shift 已經釋放: Key.shift 正在按壓: Key.esc 已經釋放: Key.esc >>>



    2.多線程範例

    我覺得這篇文章【一文看懂Python多程序與多執行緒程式設計】寫得很詳細,想多了解多線程可以參考這篇。

      基本上程式都是由上往下一行一行執行,如何做到同一個時間點,同時執行兩個不同段落的程式碼,就需要使用到多線程的概念。
      在此次需求中,我想同時間做鍵盤監控又要控制滑鼠點擊,所以才需要用到多線程。


    3.截圖並裁切
    我的螢幕解析度為1920*1080
    如果是其他解析度,座標要調整一下喔
    在切割圖片之後,我把所有非白色的pixel都變黑色(概念來自這篇)
    讓圖片呈現黑底白字的狀態

    程式碼
    # -*- coding: utf-8 -*-
    import pyautogui
    
    image = pyautogui.screenshot()#全螢幕截圖
    image_obj = image.crop((1440,240,1600,500)).convert('L')#切割圖片,只留需要的區域
    for a in range(image_obj.size[1]):
        for b in range(image_obj.size[0]):
            if image_obj.getpixel((b,a)) < 171:
                image_obj.putpixel((b,a),0)
    image_obj.save(r"D:\D3\Autoreforge\screenshotDefault20.png")
    


    結果




    4.OCR光學字元辨識
    白話說就是把圖片裡的文字解讀並轉成字串,讓我們可以在程式中使用(加減乘除做比較...等等)。
    在做OCR之前,我們還得要對圖片進行前處理(去雜訊/去噪),讓圖片比較容易被電腦判讀。

    • 圖片前處理
    因為使用imread來讀圖時,他的圖片參數排列是BGR
    而plt.imshow()顯示圖片時是RGB,所以我們必須將順序調整一下
    不然你的圖片會變得藍藍的
    透過這行代碼來做
    cv2.cvtColor(img, cv2.COLOR_BGR2RGB)


    程式碼
    # -*- coding: utf-8 -*-
    import cv2
    import matplotlib.pyplot as plt
    import numpy as np
    
    #影像前處理
    def get_clearImage():
        #原圖
        imagex = cv2.imread(r"D:\D3\Autoreforge\screenshotDefault2.png", cv2.IMREAD_COLOR)
    imagex = cv2.cvtColor(imagex,cv2.COLOR_BGR2RGB) #只留白色的圖 img = cv2.imread(r"D:\D3\Autoreforge\screenshotDefault8.png", cv2.IMREAD_COLOR)
    #轉灰階 coefficients = [0, 1, 1] m = np.array(coefficients).reshape((1, 3)) gray = cv2.transform(img, m) #閾值 maxval:255 142 binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1] #並排顯示四張圖 p_1 = plt.subplot(141) plt.imshow(imagex) p_1.set_title('Original') p_2 = plt.subplot(142) plt.imshow(img) p_2.set_title('Only white') p_3 = plt.subplot(143) plt.imshow(gray,cmap ='gray') p_3.set_title('Gray') p_4 = plt.subplot(144) plt.imshow(binary,cmap ='gray') p_4.set_title('Threshold') plt.show() return binary get_clearImage()

    結果

    圖片辨識小小心得
    • 白底黑字的辨識度 於 黑底白字
    • 如果圖片不是單純的白底黑字,那圖片進OCR之前的前處理很重要
    • 根據圖片特徵去做前處理以強化你想要辨識的地方,以我的例子,我要的是白色的數值部分,所以我就去凸顯白色,把非白色都去掉。


    四、完整程式碼
    # -*- coding: utf-8 -*- by Luca 
    import sys, os
    from pynput.keyboard import Controller, Key, Listener
    from pynput import keyboard #version 1.6.8
    from datetime import datetime
    from PIL import ImageGrab, Image #version 8.0.0
    import easygui #version 0.98.1
    import time
    import pyautogui #version 0.9.52
    import threading
    import inspect
    import ctypes #version 1.1.0
    import pytesseract
    import cv2 #version 3.4.5.20
    import numpy as np #version 1.19.2
    import skimage #0.24.0
     
    flag = False #自動重鑄的執行狀態,True表示重鑄中,False表示閒置中
    threads = [] #多線程陣列
    condition="DAMAGE:1054000" #目標值
    
     
    def _async_raise(tid, exctype):
        """raises the exception, performs cleanup if needed"""
        tid = ctypes.c_long(tid)
        if not inspect.isclass(exctype):
            exctype = type(exctype)
        res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
        if res == 0:
            raise ValueError("invalid thread id")
        elif res != 1:
            ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
            raise SystemError("PyThreadState_SetAsyncExc failed")
     
    #結束多線程
    def stop_thread(thread):
        _async_raise(thread.ident, SystemExit)
        
            
    #Inputbox
    def call_inputbox():
        global condition
        tmp = easygui.enterbox("請輸入條件","訊息",condition)
        if tmp != None:
            condition = tmp
            print('目前條件為:'+ condition)
        time.sleep(1)
     
    #影像前處理
    def get_clearImage(img):
    
        #放大照片
        #resize image to be 300 pixels wide
        r = 300.0 / img.shape[1]
        dim = (300, int(img.shape[0] * r))
        bigimg = cv2.resize(img, dim, interpolation=cv2.INTER_AREA)
    
        #轉灰階(去除顏色留下黑白)
        coefficients = [0, 1, 1]
        m = np.array(coefficients).reshape((1, 3))
        gray = cv2.transform(bigimg, m)
    
        #定義膨脹核
        kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
        #膨脹操作(把白字變粗)
        dilated = cv2.dilate(gray, kernel, iterations=1)
    
        #閾值 maxval:255 142 白轉黑 黑轉白 變成白底黑字
        binary = cv2.threshold(dilated, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
        return binary
    
    
     
    #圖片轉文字
    def get_charactfromIMG(cond):
        imgPath =r"D:\D3\Autoreforge\screenshotDefault.png"
        img = cv2.imread(imgPath, cv2.IMREAD_COLOR)
        
        result = []
        
        cong = r'--oem 3 --psm 6 outputbase digits'
        datas = pytesseract.image_to_string(get_clearImage(img),lang="eng", config=cong)#chi_tra
        for i in datas.splitlines():
            tempval = i.strip().replace('.','')
            if tempval != "":
                result.append(tempval)
    
        if cond == "STR" and len(result) > 0: #力量
            return result[0]
        elif cond == "AGI" and len(result) > 1: #敏捷
            return result[1]
        elif cond == "INT" and len(result) > 2: #智力
            return result[2]
        elif cond == "VIT" and len(result) > 3: #體能
            return result[3]
        elif cond == "DAMAGE" and len(result) > 4: #傷害
            return result[4]
        else:
            stop_rebuild()
            print("不在暗黑畫面中,停止鑄造")
            
    #比較條件與數值
    def compare_value():
        global condition
        if len(condition) > 0:
            condType = condition.split(':')[0]
            condValue = condition.split(':')[1]
            val1 = get_charactfromIMG(condType)
            if (len(val1)>1) and (int(val1) >= int(condValue)):
                return True
            else:
                return False
        else:
            return False
     
    #螢幕截圖
    def get_screenshot():
        image = pyautogui.screenshot()#全螢幕截圖
        image_obj = image.crop((1440,240,1600,500)).convert('L')#切割圖片,只留需要的區域
        #非白色都轉黑色,只留下白色數值部分
        for a in range(image_obj.size[1]):
            for b in range(image_obj.size[0]):
                if image_obj.getpixel((b,a)) < 171:
                    image_obj.putpixel((b,a),0)
        image_obj.save(r"D:\D3\Autoreforge\screenshotDefault.png")
     
    #停止重鑄
    def stop_rebuild():
        global threads, flag,condition
        flag = False
        if len(threads) > 0:
            stop_thread(threads[0])
        threads.clear()
        condition=""
        print(datetime.now().strftime("%H:%M:%S") + " : 停止重鑄...")
     
    # 自動重鑄
    def auto_rebuild():
        i=0
        while i<1000: #以防萬一程式停不了,跑到1000次時強制停止
            get_screenshot()#全螢幕截圖
            if compare_value() == False:
                pyautogui.click(699,839) #放入材料
                time.sleep(.200)
                pyautogui.click(277,834) #轉化
                time.sleep(3.500)
                i+=1
            else:
                break
        print(datetime.now().strftime("%H:%M:%S") + " : 目標達成,停止重鑄...")
        stop_rebuild()
        
     
    #得到鍵入的值
    def get_key_name(key):
        return str(key)
     
        
    # 監聽按壓
    def on_press(key):
        global fun_start,time_interval,index,dict,count,count_dict
        #print("正在按壓:", get_key_name(key))
        
    # 監聽釋放
    def on_release(key):
        global start,fun_start, time_inxterval, index,count,count_dict,flag,threads,condition
        tmpkey = get_key_name(key)
        #print("已經釋放:", tmpkey)
        
        if tmpkey == "'a'" and flag == False:
            threads.clear()
            print(datetime.now().strftime("%H:%M:%S") , " : 開始重鑄...")
            threads.append(threading.Thread(target = auto_rebuild))
            threads[0].start()
            flag = True
            
        if tmpkey == "'b'" and flag == True:
            stop_rebuild()
    
        if tmpkey == "'s'":
            call_inputbox()
        
        if tmpkey == "'x'":
            # 停止監聽
            return False
        
    # 開始監聽
    def start_listen():
        with Listener(on_press=on_press, on_release=on_release) as listener:
            listener.join()
     
    if __name__ == '__main__':
        # 開始監聽,按esc退出監聽
        print('暗黑破壞神3 自動重鑄程式開始執行....')
        print('按s設定目標值, 按a開始自動重鑄, 按b停止自動重鑄, 按x結束程式')
        print('力量STR,敏捷AGI,智力INT,體力VIT,條件使用預期目標,當力量>=10000才停止重鑄')
        print('條件範例: AGI:10000,這條件表示當敏捷大於等於10000會停止重鑄')
        print('目前條件為:'+ condition)
        start_listen()
    


    大部分情況下程式都可以正常運行

    但在多線程的處理上,還有進步空間,偶有Error會發生

    但至少現在可以不用守在電腦前面啦


    參考資料

    沒有留言:

    張貼留言