🔥 新作首發 🎮 天堂私服 YOLOv8 物件偵測實戰 — 從資料蒐集、模型訓練到即時偵測 立即閱讀 →
熱門系列
Like Share Discussion Bookmark Smile

J.J. Huang   2026-04-18   Python OpenCV 08.專案實作篇   瀏覽次數:次   DMCA.com Protection Status

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
# collect_screenshots.py
import mss
import cv2
import numpy as np
import os
import time
import pygetwindow as gw

SAVE_DIR = "dataset/raw_images"
INTERVAL = 0.5 # 每隔 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:05d}.jpg")
cv2.imwrite(fname, img)
count += 1

if count % 50 == 0:
print(f"已蒐集:{count}/{TARGET}")

time.sleep(INTERVAL)

print(f"蒐集完成!共 {count} 張,儲存於 {SAVE_DIR}")

💡 私服蒐集技巧

  • 在同一張地圖、同一區域定點蒐集,減少背景多樣性帶來的干擾
  • 不同時間段(白天/夜間)各蒐集一批,讓模型適應光線變化
  • 如果要偵測多種怪物,分別在各怪物出沒的地圖蒐集

💻 步驟二:LabelImg 標記

1
2
# 安裝後直接執行
labelImg

標記流程:

  1. Open Dir → 選擇 dataset/raw_images/
  2. Change Save Dir → 選擇 dataset/raw_labels/
  3. 左側切換格式為 YOLO
  4. W 畫框 → 輸入類別名稱(如 orctrollitem
  5. Ctrl+S 儲存,D 切換下一張
  6. 開啟 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
# split_dataset.py
import os
import shutil
import random

SRC_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
# create_yaml.py
import os

# 從 LabelImg 產生的 classes.txt 讀取類別
with 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
# train.py
from ultralytics import YOLO
import torch

print(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, # Windows 必設 0
patience=20,
name="lineage_detector", # 不顯式指定 project,避免不同版本加多一層 runs/detect
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
# detect_realtime.py
from ultralytics import YOLO
from pathlib import Path
import mss
import cv2
import numpy as np
import pygetwindow as gw

CONF = 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}

# 只抓 Lineage 視窗(找不到就列出所有可見視窗協助診斷,不再 fallback 成全螢幕)
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 # 每 N 幀重新查詢視窗位置,遊戲視窗被拖移也能跟上
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

# 手動畫框(不呼叫 results.plot(),避免某些版本會開額外視窗或觸發內建 show)
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:.2f}", (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>3detect_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