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
  1. >>> import cv2
  2. >>> cv2.__version__
  3. '3.4.5'
  4.  
  5. >>> import PIL
  6. >>> PIL.__version__
  7. '8.0.0'
  8.  
  9. >>> import pyautogui
  10. >>> pyautogui.__version__
  11. '0.9.52'
  12.  
  13. >>> import ctypes
  14. >>> ctypes.__version__
  15. '1.1.0'
  16.  
  17. >>> import numpy
  18. >>> numpy.__version__
  19. '1.19.2'
  20.  
  21. >>> import pkg_resources
  22. >>> pkg_resources.working_set.by_key['pytesseract'].version
  23. '0.3.6'
  24.  
  25. >>> import pkg_resources
  26. >>> pkg_resources.working_set.by_key['easygui'].version
  27. '0.98.1'
  28.  
  29. >>> import pkg_resources
  30. >>> pkg_resources.working_set.by_key['pynput'].version
  31. '1.6.8'

三、Function介紹

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

程式碼
  1. # -*- coding: utf-8 -*-
  2. from pynput.keyboard import Controller, Key, Listener
  3. from pynput import keyboard
  4.  
  5. #得到鍵入的值
  6. def get_key_name(key):
  7. return str(key)
  8.  
  9. # 監聽按壓
  10. def on_press(key):
  11. global fun_start,time_interval,index,dict,count,count_dict
  12. print("正在按壓:", get_key_name(key))
  13. # 監聽釋放
  14. def on_release(key):
  15. global start,fun_start, time_inxterval, index,count,count_dict
  16. print("已經釋放:", get_key_name(key))
  17. if key == Key.esc:
  18. # 停止監聽
  19. return False
  20. # 開始監聽
  21. def start_listen():
  22. with Listener(on_press=on_press, on_release=on_release) as listener:
  23. listener.join()
  24.  
  25. if __name__ == '__main__':
  26. # 開始監聽,按esc退出監聽
  27. start_listen()

結果
  1. ===================== RESTART: D:/D3/Autoreforge/test.py =====================
  2. 正在按壓: 'a'
  3. 已經釋放: 'a'
  4. 正在按壓: 'b'
  5. 已經釋放: 'b'
  6. 正在按壓: Key.space
  7. 已經釋放: Key.space
  8. 正在按壓: Key.shift
  9. 已經釋放: Key.shift
  10. 正在按壓: Key.esc
  11. 已經釋放: Key.esc
  12. >>>



2.多線程範例

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

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


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

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


結果




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

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


程式碼
  1. # -*- coding: utf-8 -*-
  2. import cv2
  3. import matplotlib.pyplot as plt
  4. import numpy as np
  5.  
  6. #影像前處理
  7. def get_clearImage():
  8. #原圖
  9. imagex = cv2.imread(r"D:\D3\Autoreforge\screenshotDefault2.png", cv2.IMREAD_COLOR)
  10. imagex = cv2.cvtColor(imagex,cv2.COLOR_BGR2RGB)
  11. #只留白色的圖
  12. img = cv2.imread(r"D:\D3\Autoreforge\screenshotDefault8.png", cv2.IMREAD_COLOR)
  13. #轉灰階
  14. coefficients = [0, 1, 1]
  15. m = np.array(coefficients).reshape((1, 3))
  16. gray = cv2.transform(img, m)
  17. #閾值 maxval:255 142
  18. binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
  19. #並排顯示四張圖
  20. p_1 = plt.subplot(141)
  21. plt.imshow(imagex)
  22. p_1.set_title('Original')
  23. p_2 = plt.subplot(142)
  24. plt.imshow(img)
  25. p_2.set_title('Only white')
  26. p_3 = plt.subplot(143)
  27. plt.imshow(gray,cmap ='gray')
  28. p_3.set_title('Gray')
  29. p_4 = plt.subplot(144)
  30. plt.imshow(binary,cmap ='gray')
  31. p_4.set_title('Threshold')
  32. plt.show()
  33. return binary
  34.  
  35. get_clearImage()

結果

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


四、完整程式碼
  1. # -*- coding: utf-8 -*- by Luca
  2. import sys, os
  3. from pynput.keyboard import Controller, Key, Listener
  4. from pynput import keyboard #version 1.6.8
  5. from datetime import datetime
  6. from PIL import ImageGrab, Image #version 8.0.0
  7. import easygui #version 0.98.1
  8. import time
  9. import pyautogui #version 0.9.52
  10. import threading
  11. import inspect
  12. import ctypes #version 1.1.0
  13. import pytesseract
  14. import cv2 #version 3.4.5.20
  15. import numpy as np #version 1.19.2
  16. import skimage #0.24.0
  17. flag = False #自動重鑄的執行狀態,True表示重鑄中,False表示閒置中
  18. threads = [] #多線程陣列
  19. condition="DAMAGE:1054000" #目標值
  20.  
  21. def _async_raise(tid, exctype):
  22. """raises the exception, performs cleanup if needed"""
  23. tid = ctypes.c_long(tid)
  24. if not inspect.isclass(exctype):
  25. exctype = type(exctype)
  26. res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype))
  27. if res == 0:
  28. raise ValueError("invalid thread id")
  29. elif res != 1:
  30. ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None)
  31. raise SystemError("PyThreadState_SetAsyncExc failed")
  32. #結束多線程
  33. def stop_thread(thread):
  34. _async_raise(thread.ident, SystemExit)
  35. #Inputbox
  36. def call_inputbox():
  37. global condition
  38. tmp = easygui.enterbox("請輸入條件","訊息",condition)
  39. if tmp != None:
  40. condition = tmp
  41. print('目前條件為:'+ condition)
  42. time.sleep(1)
  43. #影像前處理
  44. def get_clearImage(img):
  45.  
  46. #放大照片
  47. #resize image to be 300 pixels wide
  48. r = 300.0 / img.shape[1]
  49. dim = (300, int(img.shape[0] * r))
  50. bigimg = cv2.resize(img, dim, interpolation=cv2.INTER_AREA)
  51.  
  52. #轉灰階(去除顏色留下黑白)
  53. coefficients = [0, 1, 1]
  54. m = np.array(coefficients).reshape((1, 3))
  55. gray = cv2.transform(bigimg, m)
  56.  
  57. #定義膨脹核
  58. kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
  59. #膨脹操作(把白字變粗)
  60. dilated = cv2.dilate(gray, kernel, iterations=1)
  61.  
  62. #閾值 maxval:255 142 白轉黑 黑轉白 變成白底黑字
  63. binary = cv2.threshold(dilated, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)[1]
  64. return binary
  65.  
  66.  
  67. #圖片轉文字
  68. def get_charactfromIMG(cond):
  69. imgPath =r"D:\D3\Autoreforge\screenshotDefault.png"
  70. img = cv2.imread(imgPath, cv2.IMREAD_COLOR)
  71. result = []
  72. cong = r'--oem 3 --psm 6 outputbase digits'
  73. datas = pytesseract.image_to_string(get_clearImage(img),lang="eng", config=cong)#chi_tra
  74. for i in datas.splitlines():
  75. tempval = i.strip().replace('.','')
  76. if tempval != "":
  77. result.append(tempval)
  78.  
  79. if cond == "STR" and len(result) > 0: #力量
  80. return result[0]
  81. elif cond == "AGI" and len(result) > 1: #敏捷
  82. return result[1]
  83. elif cond == "INT" and len(result) > 2: #智力
  84. return result[2]
  85. elif cond == "VIT" and len(result) > 3: #體能
  86. return result[3]
  87. elif cond == "DAMAGE" and len(result) > 4: #傷害
  88. return result[4]
  89. else:
  90. stop_rebuild()
  91. print("不在暗黑畫面中,停止鑄造")
  92. #比較條件與數值
  93. def compare_value():
  94. global condition
  95. if len(condition) > 0:
  96. condType = condition.split(':')[0]
  97. condValue = condition.split(':')[1]
  98. val1 = get_charactfromIMG(condType)
  99. if (len(val1)>1) and (int(val1) >= int(condValue)):
  100. return True
  101. else:
  102. return False
  103. else:
  104. return False
  105. #螢幕截圖
  106. def get_screenshot():
  107. image = pyautogui.screenshot()#全螢幕截圖
  108. image_obj = image.crop((1440,240,1600,500)).convert('L')#切割圖片,只留需要的區域
  109. #非白色都轉黑色,只留下白色數值部分
  110. for a in range(image_obj.size[1]):
  111. for b in range(image_obj.size[0]):
  112. if image_obj.getpixel((b,a)) < 171:
  113. image_obj.putpixel((b,a),0)
  114. image_obj.save(r"D:\D3\Autoreforge\screenshotDefault.png")
  115. #停止重鑄
  116. def stop_rebuild():
  117. global threads, flag,condition
  118. flag = False
  119. if len(threads) > 0:
  120. stop_thread(threads[0])
  121. threads.clear()
  122. condition=""
  123. print(datetime.now().strftime("%H:%M:%S") + " : 停止重鑄...")
  124. # 自動重鑄
  125. def auto_rebuild():
  126. i=0
  127. while i<1000: #以防萬一程式停不了,跑到1000次時強制停止
  128. get_screenshot()#全螢幕截圖
  129. if compare_value() == False:
  130. pyautogui.click(699,839) #放入材料
  131. time.sleep(.200)
  132. pyautogui.click(277,834) #轉化
  133. time.sleep(3.500)
  134. i+=1
  135. else:
  136. break
  137. print(datetime.now().strftime("%H:%M:%S") + " : 目標達成,停止重鑄...")
  138. stop_rebuild()
  139. #得到鍵入的值
  140. def get_key_name(key):
  141. return str(key)
  142. # 監聽按壓
  143. def on_press(key):
  144. global fun_start,time_interval,index,dict,count,count_dict
  145. #print("正在按壓:", get_key_name(key))
  146. # 監聽釋放
  147. def on_release(key):
  148. global start,fun_start, time_inxterval, index,count,count_dict,flag,threads,condition
  149. tmpkey = get_key_name(key)
  150. #print("已經釋放:", tmpkey)
  151. if tmpkey == "'a'" and flag == False:
  152. threads.clear()
  153. print(datetime.now().strftime("%H:%M:%S") , " : 開始重鑄...")
  154. threads.append(threading.Thread(target = auto_rebuild))
  155. threads[0].start()
  156. flag = True
  157. if tmpkey == "'b'" and flag == True:
  158. stop_rebuild()
  159.  
  160. if tmpkey == "'s'":
  161. call_inputbox()
  162. if tmpkey == "'x'":
  163. # 停止監聽
  164. return False
  165. # 開始監聽
  166. def start_listen():
  167. with Listener(on_press=on_press, on_release=on_release) as listener:
  168. listener.join()
  169. if __name__ == '__main__':
  170. # 開始監聽,按esc退出監聽
  171. print('暗黑破壞神3 自動重鑄程式開始執行....')
  172. print('按s設定目標值, 按a開始自動重鑄, 按b停止自動重鑄, 按x結束程式')
  173. print('力量STR,敏捷AGI,智力INT,體力VIT,條件使用預期目標,當力量>=10000才停止重鑄')
  174. print('條件範例: AGI:10000,這條件表示當敏捷大於等於10000會停止重鑄')
  175. print('目前條件為:'+ condition)
  176. start_listen()


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

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

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


參考資料

沒有留言:

張貼留言