Python | OpenCV 專案:天堂私服 YOLOv8 物件偵測實戰
⚠️ 免責聲明 本文章內容僅供學術研究與電腦視覺技術學習之用途 ,所有程式碼與技術說明均以教育目的為出發點,用於探討資料蒐集、影像標記、YOLOv8 模型訓練與即時物件偵測等技術在實際場景中的應用方式。
本文作者不提供任何形式的輔助程式販售、散佈或商業服務 。
本文所有範例程式僅限在自行架設的私有伺服器環境中測試 ,不得用於任何正式營運的線上遊戲伺服器。
使用遊戲輔助程式可能違反個別遊戲的使用者條款,並導致帳號封鎖、法律責任等後果,讀者須自行承擔一切相關風險與責任 。
本文技術內容若被用於任何違法或損害他人利益之行為,作者概不負責 。
私有伺服器的架設與使用涉及遊戲著作權相關法律問題,讀者應自行評估所在地區的法規,並確認於合法範圍內使用。
本文的核心目的是展示 資料蒐集 → 標記 → YOLOv8 訓練 → 即時偵測 這套完整流程,私服環境僅作為一個可控、高效的資料來源範例。相同的流程同樣適用於工廠瑕疵偵測、倉儲物件辨識、醫療影像分析等正當場景。
📚 前言 在上一篇 MediaPipe 手勢控制應用 中,我們完成了手勢辨識的互動應用。
這一篇是一個真實的遊戲輔助開發實戰:天堂私服 YOLOv8 物件偵測 。
天堂私服的最大優勢是你能完全掌控遊戲環境:可以任意召喚怪物、調整地圖、控制光線與場景,讓資料蒐集變得極為高效。這個流程可以套用到任何你想偵測的遊戲物件。
🎯 專案目標
用私服環境快速蒐集遊戲截圖資料集
使用 LabelImg 標記怪物、NPC、道具等物件
以 YOLOv8 訓練自訂偵測模型
即時擷取遊戲畫面並進行物件偵測
🛠️ 套件安裝 1 2 pip install ultralytics mss pygetwindow pip install labelImg
💻 步驟一:自動蒐集遊戲截圖 私服優勢:可在特定地圖、特定怪物旁邊定點蒐集,讓資料集場景一致。
自動定時截取遊戲視窗畫面並儲存為圖片,用於建立 YOLOv8 訓練的原始資料集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import mssimport cv2import numpy as npimport osimport timeimport pygetwindow as gwSAVE_DIR = "dataset/raw_images" INTERVAL = 0.5 TARGET = 500 WINDOW_TITLE = "Lineage" os.makedirs(SAVE_DIR, exist_ok=True ) def get_game_window () : wins = [w for w in gw.getAllWindows() if WINDOW_TITLE in w.title] if not wins: print(f"找不到視窗:{WINDOW_TITLE} " ) return None w = wins[0 ] return {"left" : w.left, "top" : w.top, "width" : w.width, "height" : w.height} region = get_game_window() if region is None : region = {"left" : 0 , "top" : 0 , "width" : 1920 , "height" : 1080 } count = 0 print(f"開始蒐集,目標 {TARGET} 張,間隔 {INTERVAL} 秒" ) print("切換到遊戲視窗後自動開始截圖,Ctrl+C 停止" ) with mss.mss() as sct: while count < TARGET: screenshot = sct.grab(region) img = np.array(screenshot) img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) fname = os.path.join(SAVE_DIR, f"{count:05 d} .jpg" ) cv2.imwrite(fname, img) count += 1 if count % 50 == 0 : print(f"已蒐集:{count} /{TARGET} " ) time.sleep(INTERVAL) print(f"蒐集完成!共 {count} 張,儲存於 {SAVE_DIR} " )
💡 私服蒐集技巧 :
在同一張地圖、同一區域定點蒐集,減少背景多樣性帶來的干擾
不同時間段(白天/夜間)各蒐集一批,讓模型適應光線變化
如果要偵測多種怪物,分別在各怪物出沒的地圖蒐集
💻 步驟二:LabelImg 標記
標記流程:
Open Dir → 選擇 dataset/raw_images/
Change Save Dir → 選擇 dataset/raw_labels/
左側切換格式為 YOLO
按 W 畫框 → 輸入類別名稱(如 orc、troll、item)
按 Ctrl+S 儲存,D 切換下一張
開啟 Auto Save 可省略手動存檔
建議標記的類別(依需求調整):
1 2 3 4 怪物類:orc, troll, dark_elf, zombie, ... NPC 類:npc_shop, npc_quest, ... 道具類:item_weapon, item_armor, item_misc 角色類:player, other_player
💡 先只標記 1~2 個最重要的類別,訓練成功後再擴充。類別越少,模型越好訓練。
💻 步驟三:分割資料集 將原始截圖與標籤按 8:2 比例隨機分割為訓練集與驗證集,建立對應目錄結構。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 import osimport shutilimport randomSRC_IMAGES = "dataset/raw_images" SRC_LABELS = "dataset/raw_labels" DST_ROOT = "dataset" VAL_RATIO = 0.2 random.seed(42 ) for split in ["train" , "val" ]: os.makedirs(f"{DST_ROOT} /images/{split} " , exist_ok=True ) os.makedirs(f"{DST_ROOT} /labels/{split} " , exist_ok=True ) files = [f for f in os.listdir(SRC_IMAGES) if f.lower().endswith((".jpg" , ".png" ))] random.shuffle(files) val_set = set(files[:int(len(files) * VAL_RATIO)]) for fname in files: split = "val" if fname in val_set else "train" stem = os.path.splitext(fname)[0 ] shutil.copy(f"{SRC_IMAGES} /{fname} " , f"{DST_ROOT} /images/{split} /{fname} " ) lbl = f"{SRC_LABELS} /{stem} .txt" if os.path.exists(lbl): shutil.copy(lbl, f"{DST_ROOT} /labels/{split} /{stem} .txt" ) else : open(f"{DST_ROOT} /labels/{split} /{stem} .txt" , "w" ).close() print(f"train: {len(files)-len(val_set)} , val: {len(val_set)} " )
💻 步驟四:建立 data.yaml 從 LabelImg 產生的 classes.txt 自動讀取類別,生成 YOLOv8 訓練所需的 data.yaml 設定檔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import oswith open("dataset/raw_labels/classes.txt" ) as f: classes = [line.strip() for line in f if line.strip()] yaml_content = f"""path: ./dataset train: images/train val: images/val nc: {len(classes)} names: """ for i, cls in enumerate(classes): yaml_content += f" {i} : {cls} \n" with open("data.yaml" , "w" ) as f: f.write(yaml_content) print("data.yaml 已產生:" ) print(yaml_content)
💻 步驟五:訓練 YOLOv8 以 YOLOv8s 預訓練模型為基礎,對遊戲截圖資料集進行訓練並輸出最佳偵測模型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 from ultralytics import YOLOimport torchprint(f"GPU:{'可用 - ' + torch.cuda.get_device_name(0 ) if torch.cuda.is_available() else 'CPU' } " ) model = YOLO("yolov8s.pt" ) model.train( data="data.yaml" , epochs=100 , imgsz=640 , batch=16 , device=0 if torch.cuda.is_available() else "cpu" , workers=0 , patience=20 , name="lineage_detector" , plots=True , ) print(f"\n訓練完成!實際模型路徑:{model.trainer.best} " )
💻 步驟六:即時遊戲畫面偵測 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 from ultralytics import YOLOfrom pathlib import Pathimport mssimport cv2import numpy as npimport pygetwindow as gwCONF = 0.5 WINDOW_TITLE = "Lineage" def find_latest_model (search_root="runs" ) : """自動找出 runs/ 目錄下最近訓練的 best.pt Ultralytics 在不同版本下實際存檔路徑可能是: runs/detect/lineage_detector/weights/best.pt runs/detect/runs/detect/lineage_detector/weights/best.pt runs/detect/lineage_detector2/weights/best.pt (訓練多次時) 用 rglob 通吃所有情況,並依修改時間挑最新。 """ candidates = list(Path(search_root).rglob("best.pt" )) if not candidates: return None return str(max(candidates, key=lambda p: p.stat().st_mtime)) MODEL_PATH = find_latest_model() if MODEL_PATH is None : print("找不到任何 best.pt,請先執行 train.py 完成訓練" ) exit() print(f"載入模型:{MODEL_PATH} " ) model = YOLO(MODEL_PATH) def find_game_window (title_keyword) : """回傳符合標題關鍵字,且可見、未最小化、尺寸正常的視窗""" for w in gw.getAllWindows(): if (title_keyword.lower() in w.title.lower() and w.visible and not w.isMinimized and w.width > 0 and w.height > 0 ): return w return None def window_to_region (win) : return {"left" : win.left, "top" : win.top, "width" : win.width, "height" : win.height} game_win = find_game_window(WINDOW_TITLE) if game_win is None : print(f"[錯誤] 找不到可見的『{WINDOW_TITLE} 』視窗。目前可見視窗:" ) for w in gw.getAllWindows(): if w.title.strip() and w.visible: print(f" - {w.title} " ) print("\n請確認遊戲已開啟且未最小化,或調整 WINDOW_TITLE 讓它能匹配遊戲視窗標題。" ) exit() print(f"目標視窗:{game_win.title} 位置:({game_win.left} ,{game_win.top} ) 大小:{game_win.width} x{game_win.height} " ) print("即時偵測啟動,按 q 離開(需點選偵測視窗)" ) DISPLAY_NAME = "Lineage Detector" cv2.namedWindow(DISPLAY_NAME, cv2.WINDOW_NORMAL) cv2.resizeWindow(DISPLAY_NAME, game_win.width, game_win.height) REFRESH_EVERY = 30 region = window_to_region(game_win) frame_count = 0 try : with mss.mss() as sct: while True : if frame_count % REFRESH_EVERY == 0 : game_win = find_game_window(WINDOW_TITLE) if game_win is None : print("偵測到遊戲視窗消失或被最小化,結束偵測" ) break region = window_to_region(game_win) frame_count += 1 screenshot = sct.grab(region) frame = cv2.cvtColor(np.array(screenshot), cv2.COLOR_BGRA2BGR) results = model(frame, conf=CONF, verbose=False ) boxes = results[0 ].boxes for box in boxes: cls_id = int(box.cls) conf = float(box.conf) name = model.names[cls_id] x1, y1, x2, y2 = map(int, box.xyxy[0 ]) cv2.rectangle(frame, (x1, y1), (x2, y2), (0 , 255 , 0 ), 2 ) cv2.putText(frame, f"{name} {conf:.2 f} " , (x1, max(y1 - 8 , 15 )), cv2.FONT_HERSHEY_SIMPLEX, 0.6 , (0 , 255 , 0 ), 2 ) cv2.imshow(DISPLAY_NAME, frame) if cv2.waitKey(1 ) & 0xFF == ord("q" ): break finally : cv2.destroyAllWindows()
圖:開啟遊戲視窗截圖並以 YOLOv8 即時推論,在單一顯示視窗內用綠色框線標注所有偵測到的物件與信心度
⚠️ 注意事項
私服蒐集資料的優勢在於可控性 :能隨時 GM 指令生成怪物、清空場景、調整時間,大幅降低蒐集成本。
截圖解析度要固定 :訓練與推論時的遊戲視窗解析度必須一致(或讓 YOLOv8 的 imgsz 涵蓋所有尺寸)。
標記品質比數量更重要 :500 張高品質標記遠勝 2000 張草率標記,邊界框要緊貼物件。
mss 截圖速度 :mss 在 Windows 上截圖速度可達 30fps 以上,適合即時偵測。
遊戲視窗不能最小化 :mss 擷取的是實際畫面,遊戲視窗若被最小化或遮擋,截到的是空畫面;detect_realtime.py 偵測到視窗最小化會自動結束,而非硬抓一片黑。
找不到遊戲視窗不 fallback 成全螢幕 :detect_realtime.py 會列出目前可見視窗清單並直接結束,請從清單挑選正確字串填回 WINDOW_TITLE(中文客戶端可能是「天堂」、英文客戶端可能是「Lineage」、私服可能帶版本名),避免在整個桌面上跑 YOLO 造成大量誤判。
訓練輸出路徑因版本而異 :不同 Ultralytics 版本可能把 best.pt 存到 runs/detect/<name>/weights/,或是多加一層變成 runs/detect/runs/detect/<name>/weights/;訓練多次時也會自動遞增成 <name>2、<name>3。detect_realtime.py 已改用 rglob("best.pt") 自動找最新的模型,不必手動維護路徑。
不要用 results[0].plot() 顯示 :某些 Ultralytics 版本在 plot() 或 predict 流程中會額外開啟顯示視窗(例如 show 相關設定、內建 debug 視窗),導致一次執行跑出一堆視窗。採用手動 cv2.rectangle + cv2.putText 畫在原始 frame 上,再用 cv2.namedWindow 建立的單一視窗顯示,是最可控的做法。
📊 進階應用方向
自動攻擊 :偵測到怪物後,取邊界框中心座標,用 pyautogui 點擊
自動撿取道具 :偵測 item 類別,自動移動到物品位置按撿取
血量監控 :偵測 HP 條區域,用 OCR 或色彩分析判斷血量百分比
🎯 結語 有了私服作為資料蒐集環境,整個 YOLOv8 訓練流程可以在幾小時內完成,從蒐集到即時偵測一氣呵成。 這個流程不只適用於天堂,任何遊戲或模擬器環境都可以用相同的方式快速建立偵測模型。
下一篇進入 天堂私服遊戲輔助(一)主程式 GUI 框架與 HP/MP 血量監控 ,把訓練好的模型整合進一套有 GUI 的完整輔助程式。
📖 如在學習過程中遇到疑問,或是想了解更多相關主題,建議回顧一下 Python | OpenCV 系列導讀 ,掌握完整的章節目錄,方便快速找到你需要的內容。
註:以上參考了Ultralytics YOLOv8 官方文件 mss 官方文件 LabelImg GitHub pygetwindow GitHub