顯示具有 Python 標籤的文章。 顯示所有文章
顯示具有 Python 標籤的文章。 顯示所有文章

2026 新手指南——用 30 行 Python + OpenCV 做完你的第一個電腦視覺專案

筆電 Webcam 即時 Canny 邊緣偵測

跟身邊很多想入門電腦視覺的朋友聊過,最常聽到的卡關點是這句:

「我看了一堆教學,但好像每個都要我先讀完線性代數、再讀完 CNN、再讀完 transformer,那我什麼時候才能跑出一張有結果的圖?」

懂,我也經歷過這個階段。所以這篇我直接反過來——先讓你看到結果,再回頭講原理。三個範例由淺到深:

  1. 第一天:Webcam 即時 Canny 邊緣偵測(約 14 行 Python)
  2. 第一週:拍一張紙、自動轉成 A4 PDF 的文件掃描器(約 35 行 Python)
  3. 第一個月:拿 YOLO11 接上 Webcam 做即時物件偵測(約 15 行 Python)

整篇文章我不會碰太重的數學,只在你需要踩坑時提醒一下。Code 全部可以直接複製貼上跑。

環境與前置條件

不囉嗦,先給你一個能用的環境:

項目建議版本說明
Python3.11 或 3.123.13 也行,但部分套件 wheel 可能還沒到位
OpenCVopencv-python==4.13.x寫這篇時最新;想嚐鮮 5.0 可改 opencv-python>=5.0
NumPy2.xOpenCV 從 4.11.0 起正式支援 NumPy 2.x,本文用 4.13
Ultralyticsultralytics>=8.3第三個範例用到 YOLO11
攝影機內建 Webcam 或 USB cammacOS 第一次跑要授權 Terminal/IDE 攝影機權限

裝環境一行解決:

python -m venv .venv && source .venv/bin/activate
pip install "opencv-python>=4.13" "numpy>=2.0" "ultralytics>=8.3"

如果你不確定 Webcam 跑不跑得起來,先存這個 5 行的「相機測試」:

import cv2
cap = cv2.VideoCapture(0)  # macOS 上有時要試 1、2
ok, frame = cap.read()
print("frame shape =", frame.shape if ok else "FAILED")
cap.release()

跑得出 shape 就代表 OK。跑不出來最常見原因是權限——macOS 去「系統設定 → 隱私權與安全性 → 攝影機」把你的 Terminal 打勾。

範例 1:Webcam 即時 Canny(第一天就能跑)

Canny 邊緣偵測 是電腦視覺的「Hello World」。它是 1986 年 John Canny 提出來的演算法,用兩個閾值找出影像中「梯度夠陡」的位置,這些位置就是邊緣。

import cv2

cap = cv2.VideoCapture(0)

while True:
    ok, frame = cap.read()
    if not ok:
        break
    gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 1.4)
    edges = cv2.Canny(blurred, 80, 160)
    cv2.imshow("Canny", edges)
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()

這 14 行東西就是一個即時邊緣偵測程式。按 q 退出。手動把鏡頭對著房間裡任何東西——書本、馬克杯、貓——你會看到輪廓被白色線條精準畫出來。

三個重點記下來

  • cvtColor(BGR2GRAY):OpenCV 讀進來的圖預設是 BGR(不是 RGB),這是 1999 年留下來的歷史包袱,別問為什麼,習慣就好。
  • GaussianBlur 是 Canny 前必做的去噪:不模糊一下,邊緣會一堆雜訊。
  • Canny(img, low, high) 的兩個閾值用「low ≈ high / 2」這個經驗法則調起,效果不好再 fine-tune。

邊緣偵測的視覺示範

範例 2:自動文件掃描器(第一週的成就感)

這是 PyImageSearch 入門系列 裡的經典題目。你拍一張歪歪的紙本文件,程式自動找到文件邊框、把它矯正成正面、輸出乾淨的掃描結果。要用到三個技巧的組合:Canny → findContours → warpPerspective。

import cv2
import numpy as np

def order_points(pts):
    rect = np.zeros((4, 2), dtype="float32")
    s = pts.sum(axis=1)
    rect[0] = pts[np.argmin(s)]      # 左上
    rect[2] = pts[np.argmax(s)]      # 右下
    diff = np.diff(pts, axis=1)
    rect[1] = pts[np.argmin(diff)]   # 右上
    rect[3] = pts[np.argmax(diff)]   # 左下
    return rect

img = cv2.imread("paper.jpg")
ratio = img.shape[0] / 800.0
small = cv2.resize(img, (int(img.shape[1] / ratio), 800))

gray = cv2.cvtColor(small, cv2.COLOR_BGR2GRAY)
blurred = cv2.GaussianBlur(gray, (5, 5), 0)
edges = cv2.Canny(blurred, 75, 200)

contours, _ = cv2.findContours(edges, cv2.RETR_LIST, cv2.CHAIN_APPROX_SIMPLE)
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:5]

doc_cnt = None
for c in contours:
    peri = cv2.arcLength(c, True)
    approx = cv2.approxPolyDP(c, 0.02 * peri, True)
    if len(approx) == 4:
        doc_cnt = approx
        break

if doc_cnt is None:
    raise RuntimeError("找不到文件四邊形——請拍張對比度高一點的照片再試")

pts = order_points(doc_cnt.reshape(4, 2) * ratio)
(tl, tr, br, bl) = pts
maxW = int(max(np.linalg.norm(br - bl), np.linalg.norm(tr - tl)))
maxH = int(max(np.linalg.norm(tr - br), np.linalg.norm(tl - bl)))

dst = np.array([[0, 0], [maxW - 1, 0], [maxW - 1, maxH - 1], [0, maxH - 1]], dtype="float32")
M = cv2.getPerspectiveTransform(pts, dst)
warped = cv2.warpPerspective(img, M, (maxW, maxH))

cv2.imwrite("scanned.jpg", warped)

把任何一張包含 A4 紙的照片存成 paper.jpg,跑一次,會吐出 scanned.jpg——一張矯正後的乾淨文件圖。

流程拆解

原始照片
  → 縮小 + 灰階
  → GaussianBlur
  → Canny 邊緣
  → findContours 找最大輪廓
  → approxPolyDP 拿四個角
  → order_points 排序角點
  → getPerspectiveTransform + warpPerspective 矯正
  → 輸出 scanned.jpg

常見踩坑

症狀原因解法
找不到四邊形邊緣斷裂 / 文件邊框不完整拍照時讓紙本與背景對比大;可改用 cv2.morphologyEx 補邊
矯正結果上下顛倒order_points 排序錯檢查 s 和 diff 的方向;OpenCV 座標 y 軸向下
顏色發黃沒做白平衡對 warped 再做 cv2.adaptiveThreshold 變黑白文件

文件掃描的透視變換概念

範例 3:YOLO11 + OpenCV,第一個月就能玩現代物件偵測

範例 2 教你用幾何方法處理靜態文件,但你應該已經想到下一個問題——「畫面裡到底有什麼東西」這個問題傳統 CV 沒辦法回答,因為它需要語意理解。這就是深度學習接手的地方。

到了 2026 年,光會傳統 CV 不夠——你要會把 OpenCV 跟現代深度學習模型接起來。最簡單的接法就是 Ultralytics YOLO,這套工具把訓練、推論、export 整個包成幾行 Python。

import cv2
from ultralytics import YOLO

model = YOLO("yolo11n.pt")  # 第一次跑會自動下載 nano 模型
cap = cv2.VideoCapture(0)

while True:
    ok, frame = cap.read()
    if not ok:
        break
    results = model(frame, verbose=False)
    annotated = results[0].plot()
    cv2.imshow("YOLO11 + OpenCV", annotated)
    if cv2.waitKey(1) & 0xFF == ord("q"):
        break

cap.release()
cv2.destroyAllWindows()

15 行 Python,你的 Webcam 已經能即時辨識 80 類常見物件(人、貓、筆電、手機、椅子⋯⋯)。

為什麼這樣搭最聰明?

OpenCV 與 YOLO 是分工關係,不是替代關係:

  • OpenCV 負責:相機 I/O、影像前處理(resize、色彩空間)、後處理(畫框、寫字、輸出影片)
  • YOLO 負責:推論、給出 bounding box

如果你想做進階一點的「物件追蹤 + 計數 + 視覺化」,加上 Roboflow Supervision 這個套件會更省事——它是 model-agnostic 的,YOLO、SAM、Hugging Face Transformers 接哪個都一致。

Python vs C++:新手怎麼選?

這是入門最常糾結的問題。直接給你結論:

比較項PythonC++
學習曲線平緩陡到爆
開發速度快(Jupyter 互動式視覺化)慢
純 OpenCV 呼叫的執行速度跟 C++ 接近(底層仍是 C++)最快
自寫 for-loop 逐像素處理非常慢(必須改 NumPy 向量化)快
部署目標桌面、伺服器、Mac、PC嵌入式、即時、IoT

新手的話無腦選 Python。LearnOpenCV 在這篇對比文 也建議:「If you are a python programmer, use OpenCV with Python.」原因很簡單——Python 加 Jupyter Notebook 可以做到「改一行、馬上看到結果」,這對學 CV 是無可取代的優勢。

什麼時候才需要 C++?等你以後要把模型部署到 Jetson Nano、做即時 30fps 以上的工業檢測、或者自己寫像素級新演算法,再轉就好。多數應用其實一輩子不用碰 C++。

學會這三招之後要往哪走?

到這裡你已經會:讀寫影像、邊緣偵測、輪廓 + 透視變換、現代物件偵測。下一步建議按這個順序走:

OpenCV 經典 + YOLO 入門(你在這)
  → NumPy 向量化(避免 Python for-loop 地獄)
  → CS231n 講義(理解 CNN / ResNet / ViT)
  → 自訓資料集(Roboflow 標註 → YOLO 訓練)
  → ONNX export(跨框架部署)
  → 邊緣裝置(Raspberry Pi 5 / Jetson Orin Nano)
  → VLM 多模態(OpenCV 5 內建 Qwen / PaliGemma)

幾個我自己覺得最受用的資源:

新手最常踩的五個坑

最後分享幾個我自己踩過的坑,少走點冤枉路:

  1. 跳過經典 CV 直接學 YOLO:不懂 NMS、IoU、anchor 是什麼,模型一壞就只能換版本。
  2. 覺得 OpenCV 過時所以不學:5.0 才剛把 LLM/VLM 整合進來,反而比以前更值得學。
  3. Python 用 for-loop 處理像素:慢到懷疑人生,所有逐像素操作必須改 NumPy 向量化。
  4. 只會 model.predict() 不會部署:找工作會卡關。學會 ONNX export、INT8 量化、用 OpenCV DNN 跑推論。
  5. 資料集太乾淨:Kaggle 的影像太理想,去 Roboflow Universe 找接近真實雜訊的資料才有用。

結語

電腦視覺看起來很玄,其實 80% 的入門路徑就只是「讀進影像 → 做幾個變換 → 輸出結果」這個迴圈。三個範例你都跑過一次之後,這個迴圈會內化成肌肉記憶——剩下的就是把不同的工具(OpenCV、YOLO、Supervision、SAM2、VLM)塞進這個迴圈裡組合。

下一篇會深拆 2026 年 6 月剛發布的 OpenCV 5.0——它把整個 DNN 引擎重寫、ONNX 覆蓋率衝到 80%、還把 LLM 跟 VLM 直接塞進函式庫。如果你已經會用 OpenCV 4.x,那篇會告訴你升級該注意什麼;如果你剛入門,那篇會讓你看見未來兩年 CV 的樣貌。

延伸閱讀

Raspberry pico 使用 zephyr 進行開發

介紹

Zephyr是一個小型的即時作業系統,用於資源受限的嵌入式互聯裝置,支援多種體系並在Apache許可證 2.0下發行。它有一個BSD許可證的仿品出現在來自Intel的Arduino 101軟體資源包中。

來自:https://zh.wikipedia.org/zh-tw/Zephyr_(%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F)

可以專注在軟體開發,且在為來移植只需要在zephyr支持的清單中,可以快速進行開發平台的轉換,不用關注在HAL重新建構

當然ST,Nordic,ESP,Ti許多單片機都有支持.

環境安裝

  • 注意 如遭遇版本與本文內容有所差異,請參考Zephyr使用文檔.
  • Windows環境建置
  1. 安裝choco,主要用於安裝相依套件管理,類似ubuntu apt或是mac brew.Raspberry pico 使用 zephyr 進行開發(圖 1)圖一
  2. 以管理員身份打開cmd.exeRaspberry pico 使用 zephyr 進行開發(圖 2)
  3. 將圖一第二點命令貼上並運行安裝choco
  4. 使用choco安裝相依套件
choco feature enable -n allowGlobalConfirmation
choco install cmake --installargs 'ADD_CMAKE_TO_PATH=System'
choco install ninja gperf python git dtc-msys2 wget unzip
  1. 重新開啟cmd.exe不需要使用管理員身分
  2. 安裝python與python相依包(此處建議使用虛擬環境venv進行)
cd %HOMEPATH%
python -m venv zephyrproject\.venv
zephyrproject\.venv\Scripts\activate.bat

Raspberry pico 使用 zephyr 進行開發(圖 3) 注意 必須出現(.venv)才是在python的venv下面,才繼續進行後面步驟

  1. 安裝west
pip install west
  1. 使用west初始化Zephyr,並進行更新
west init zephyrproject
cd zephyrproject
west update
  1. 導出Cmake
west zephyr-export
  1. 使用pip安裝Zephry的requirements
pip install -r %HOMEPATH%\zephyrproject\zephyr\scripts\requirements.txt
  1. 安裝Zephyr SDK 於Home目錄下下載SDK(zephyr-sdk-0.15.2_windows-x86_64.zip)
cd %HOMEPATH%
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.15.2/zephyr-sdk-0.15.2_windows-x86_64.zip

解壓縮

unzip zephyr-sdk-0.15.2_windows-x86_64.zip

Raspberry pico 使用 zephyr 進行開發(圖 4)

  1. 配置Zephyr SDK的環境
cd zephyr-sdk-0.15.2
setup.cmd

到這步已經完成環境的安裝了,接下來建置Raspberry pico的Blinky的代碼

快速建構Blinky

Zephyr目錄結構

boards目前有支持的板子或是MCU Raspberry pico 使用 zephyr 進行開發(圖 5) 找到我們需要的板子 Raspberry pico 使用 zephyr 進行開發(圖 6) 可以找到{rpi_pico}這個資料夾 Raspberry pico 使用 zephyr 進行開發(圖 7) 今天的主角pico將

官方文件

https://docs.zephyrproject.org/latest/boards/arm/rpi_pico/doc/index.html

建構方式

cd %HOMEPATH%\zephyrproject\zephyr
west build -b rpi_pico samples/basic/blinky

沒錯,只要一行就可以完成編譯,並也貼心的建置zephyr.uf2檔案,uf2檔的路徑為(./zephyrproject\zephyr\build\zephyr) Raspberry pico 使用 zephyr 進行開發(圖 8)

此時可以插上pico

  1. 如果是第一次燒錄的pico插上會自動掛載一個磁碟出來 Raspberry pico 使用 zephyr 進行開發(圖 9) 將zephyr.uf2拖曳到pico裡面,板子自已就開始閃阿閃 Raspberry pico 使用 zephyr 進行開發(圖 10)
  2. 如果是之前有燒寫過的需要按著板子上BOOTSEL按鈕並且插上電,磁碟就會出現,在將zephyr.uf2放入即可.

修改Blinky內容

找到Zephyr所附上的範例所存放的位置 .\zephyrproject\zephyr\samples\basic\blinky\src Raspberry pico 使用 zephyr 進行開發(圖 11) 可以找到main.c文件,沒錯,程式碼就只有簡簡單單的這份! Raspberry pico 使用 zephyr 進行開發(圖 12) 將秒數從1000->100 Raspberry pico 使用 zephyr 進行開發(圖 13)

重新再跑一次

cd %HOMEPATH%\zephyrproject\zephyr
west build -b rpi_pico samples/basic/blinky

產出uf2檔後,老方法快速地把檔案拖曳進磁碟,即可完成

最後

針對Zephyr使用pico快速導入,如何從入門快速入土.

Zephyr不只支持pico..常見各家uC都有 Raspberry pico 使用 zephyr 進行開發(圖 14) 熱門的ST,Nordic,ESP32也是在其中的喔 Raspberry pico 使用 zephyr 進行開發(圖 15) Raspberry pico 使用 zephyr 進行開發(圖 16) Raspberry pico 使用 zephyr 進行開發(圖 17) 今天如果要把blinky轉換到ESP32上運行呢? west build -b <改為ESP32或是Nordci的板子>

因為之前有先編pico,可以添加-pristine參數避免報錯

cd %HOMEPATH%\zephyrproject\zephyr
west build -b esp32 samples/basic/blinky -pristine

Raspberry pico 使用 zephyr 進行開發(圖 18) 或是指定輸出的目錄即可.

簡簡單單的ESP32閃阿閃就完成拉,相同的代碼,直接可以在兩個不同平台上運行 Raspberry pico 使用 zephyr 進行開發(圖 19) Raspberry pico 使用 zephyr 進行開發(圖 20)

補充Nordic DK52

相同代碼不做修改直接編看看 Raspberry pico 使用 zephyr 進行開發(圖 21) 透過nRFConnect的Programmer燒寫hex Raspberry pico 使用 zephyr 進行開發(圖 22) Raspberry pico 使用 zephyr 進行開發(圖 23)

一樣順利OK!

換個方式 使用west flash進行

west flash --build-dir ../nrfbuild

../nrfbuild 為剛剛額外設定的輸出路徑 Raspberry pico 使用 zephyr 進行開發(圖 24) 這下連nrfConnect都不用開起來了


本文最初發布於 HackMD @BASHCAT。

KiCad Python API (kipy) 快速入門指南

最後更新日期: 2025年4月25日 適用版本: KiCad 9.0+

這份快速入門指南旨在幫助您快速開始使用 KiCad Python API (kipy) 進行開發。通過幾個簡單的示例,您將學習如何連接到 KiCad、操作電路板並執行常見任務。

目錄

KiCad Python API 簡介

KiCad 9.0 引入了全新的 IPC API(進程間通信應用程式介面),這是一個穩定的介面,旨在替代舊版的 SWIG Python 綁定。這個新 API 的主要特點有:

  • 穩定性:設計為穩定的介面,不會因為 KiCad 內部重構而變化
  • 語言無關:支持與 Python 以外的其他語言編寫的軟體互操作
  • 進程獨立:使用 Protocol Buffers 和 NNG 透過 UNIX 套接字在進程間傳輸消息
  • 易於使用:提供了 kipy 這個官方的 Python 綁定庫,使得與 IPC API 的交互更加容易 截圖 2025-04-25 晚上10.25.43

注意:SWIG 綁定在 KiCad 9.0 中已被標記為棄用,計劃在 KiCad 10.0(預計2026年2月發布)中被完全移除。建議新的開發工作使用新的 IPC API。

基礎設置

在開始之前,請確保:

  1. 您已安裝 KiCad 9.0 或更高版本
  2. 在 KiCad 中啟用了 API 服務器(首選項 > 插件中啟用)
  3. 已安裝 kipy(使用 pip install kicad-python)

啟用 API 服務器

要使用 kipy,您需要在 KiCad 中啟用 API 服務器:

  1. 打開 KiCad
  2. 進入 首選項 > 插件
  3. 勾選 啟用 API 服務器
  4. 重啟 KiCad 使設置生效

安裝 kipy 包

在命令行中運行以下命令安裝 kipy 包:

pip install kicad-python

如果您需要特定版本或開發版本,可以使用以下命令:

pip install kicad-python==0.1.0  # 安裝特定版本
# 或者
pip install git+https://gitlab.com/kicad/code/kicad-python.git  # 安裝開發版本

核心概念與架構

kipy 的核心架構基於以下幾個關鍵概念:

  1. KiCad 類 - 主要的入口點,用於連接到運行中的 KiCad 實例
  2. Board 類 - 代表一個 KiCad PCB 電路板,允許您查詢和修改電路板上的物件
  3. Project 類 - 代表一個 KiCad 項目,提供對項目級設置的訪問
  4. Wrapper 類 - 大多數 kipy 物件都繼承自 Wrapper,提供對底層 protobuf 消息的訪問
  5. 幾何類 - 提供 Vector2、Angle、Box2 等用於操作座標和幾何形狀的類

示例 1: 連接到 KiCad 並獲取版本信息

截圖 2025-04-25 晚上10.35.58

from kipy.kicad import KiCad

# 創建 KiCad 實例並連接到運行中的 KiCad
kicad = KiCad()

# 獲取 KiCad 版本
version = kicad.get_version()
print(f"連接到 KiCad 版本: {version}")

# 獲取 API 版本
api_version = kicad.get_api_version()
print(f"API 版本: {api_version}")

# 檢查版本兼容性
try:
    kicad.check_version()
    print("版本兼容!")
except Exception as e:
    print(f"版本不兼容: {e}")

示例 2: 獲取當前電路板信息

from kipy.kicad import KiCad

kicad = KiCad()

# 獲取當前活動文檔
active_doc = kicad.get_active_document()
if active_doc and active_doc.type == 2:  # 2 = DOCTYPE_BOARD
    # 獲取電路板
    board = kicad.get_board(active_doc)
    
    # 獲取基本信息
    print(f"電路板名稱: {board.name}")
    
    # 獲取軌道數量
    tracks = board.get_tracks()
    print(f"軌道數量: {len(tracks)}")
    
    # 獲取過孔數量
    vias = board.get_vias()
    print(f"過孔數量: {len(vias)}")
    
    # 獲取封裝數量
    footprints = board.get_footprints()
    print(f"封裝數量: {len(footprints)}")
    
    # 獲取網絡數量
    nets = board.get_nets()
    print(f"網絡數量: {len(nets)}")
else:
    print("當前沒有打開的電路板")

示例 3: 分析封裝和焊盤

from kipy.kicad import KiCad
from kipy.util.units import to_mm

kicad = KiCad()

# 獲取當前電路板
active_doc = kicad.get_active_document()
board = kicad.get_board(active_doc)

# 獲取所有封裝
footprints = board.get_footprints()

for fp in footprints:
    ref = fp.reference_field.text.value
    val = fp.value_field.text.value
    x_mm = to_mm(fp.position.x)
    y_mm = to_mm(fp.position.y)
    
    print(f"{ref} ({val}) 位於 ({x_mm:.2f}mm, {y_mm:.2f}mm)")
    
    # 獲取焊盤
    pads = fp.definition.pads
    print(f"  焊盤數量: {len(pads)}")
    
    # 列出焊盤信息
    for pad in pads:
        pad_num = pad.number
        net_name = pad.net.name if pad.net.name else "無網絡"
        print(f"  焊盤 {pad_num}: 連接到網絡 '{net_name}'")
    
    print()  # 空行

示例 4: 修改電路板 - 添加文本

from kipy.kicad import KiCad
from kipy.board_types import BoardText
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer
from kipy.proto.common.types.enums_pb2 import HorizontalAlignment, VerticalAlignment
from datetime import datetime

kicad = KiCad()
active_doc = kicad.get_active_document()
board = kicad.get_board(active_doc)

# 開始一個提交事務
commit = board.begin_commit()

# 創建文本
text = BoardText()
text.value = "添加於 " + datetime.now().strftime("%Y-%m-%d")
text.position = Vector2.from_xy(from_mm(100), from_mm(100))
text.layer = BoardLayer.BL_F_SilkS

# 設置文本屬性
text.attributes.size = Vector2.from_xy(from_mm(1.5), from_mm(1.5))
text.attributes.horizontal_alignment = HorizontalAlignment.HA_CENTER
text.attributes.vertical_alignment = VerticalAlignment.VA_CENTER
text.attributes.bold = True

# 添加到電路板
created_items = board.create_items(text)
print(f"添加了 {len(created_items)} 個新項目")

# 提交更改
board.push_commit(commit, "添加文本標籤")
print("更改已提交")

示例 5: 創建導線和過孔

from kipy.kicad import KiCad
from kipy.board_types import Track, Via
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer, ViaType

kicad = KiCad()
active_doc = kicad.get_active_document()
board = kicad.get_board(active_doc)

# 開始一個提交事務
commit = board.begin_commit()

# 創建網絡(這裡使用現有網絡)
nets = board.get_nets()
if len(nets) > 0:
    net = nets[0]  # 使用第一個網絡
    
    # 創建一個軌道
    track = Track()
    track.start = Vector2.from_xy(from_mm(100), from_mm(100))
    track.end = Vector2.from_xy(from_mm(120), from_mm(100))
    track.width = from_mm(0.25)  # 0.25mm 寬
    track.layer = BoardLayer.BL_F_Cu  # 頂層銅箔
    track.net = net
    
    # 添加軌道到電路板
    board.create_items(track)
    
    # 創建一個過孔
    via = Via()
    via.position = Vector2.from_xy(from_mm(120), from_mm(100))
    via.type = ViaType.VT_THROUGH
    via.diameter = from_mm(0.8)
    via.drill_diameter = from_mm(0.4)
    via.net = net
    
    # 添加過孔到電路板
    board.create_items(via)
    
    # 提交更改
    board.push_commit(commit, "添加軌道和過孔")
    print("添加了軌道和過孔")
else:
    board.drop_commit(commit)
    print("沒有可用的網絡")

示例 6: 創建一個簡單的 KiCad 插件

import os
import pcbnew
from kipy.kicad import KiCad
from kipy.board_types import BoardText
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer
import datetime

class DateStampPlugin(pcbnew.ActionPlugin):
    def __init__(self):
        super().__init__()
        self.name = "添加日期戳記"
        self.category = "修改 PCB"
        self.description = "在電路板上添加當前日期"
        self.show_toolbar_button = True
        # 插件圖標路徑
        self.icon_file_name = os.path.join(os.path.dirname(__file__), "date_icon.png")

    def Run(self):
        # 連接到 KiCad
        kicad = KiCad()
        
        # 獲取當前電路板
        active_doc = kicad.get_active_document()
        board = kicad.get_board(active_doc)
        
        # 開始提交事務
        commit = board.begin_commit()
        
        # 創建日期文本
        date_text = BoardText()
        date_text.value = "生成日期: " + datetime.datetime.now().strftime("%Y-%m-%d")
        date_text.position = Vector2.from_xy(from_mm(150), from_mm(150))
        date_text.layer = BoardLayer.BL_F_SilkS
        
        # 設置文本屬性
        date_text.attributes.size = Vector2.from_xy(from_mm(1.5), from_mm(1.5))
        
        # 添加到電路板
        board.create_items(date_text)
        
        # 提交更改
        board.push_commit(commit, "添加日期戳記")
        
        # 通知用戶
        print("已添加日期戳記到電路板")

# 注冊插件
DateStampPlugin().register()

保存上面的代碼為 date_stamp.py,然後將其放置在 KiCad 插件目錄中:

  • Windows: %APPDATA%\kicad\9.0\scripting\plugins\
  • Linux: <sub>/.local/share/kicad/9.0/scripting/plugins/
  • macOS: </sub>/Library/Application Support/kicad/9.0/scripting/plugins/

示例 7: 透過pytho繪製舉矩形

from kipy import KiCad
from kipy.board_types import BoardRectangle, BoardLayer
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.common_types import GraphicAttributes, Color

# 連線到 KiCad
kicad = KiCad()
board = kicad.get_board()
commit = board.begin_commit()

# 定義要繪製的層
layers = [
    BoardLayer.BL_F_Cu,        # 頂層銅箔
    BoardLayer.BL_B_Cu,        # 底層銅箔
    BoardLayer.BL_F_SilkS,     # 頂層絲印
    BoardLayer.BL_B_SilkS,     # 底層絲印
    BoardLayer.BL_F_Mask,      # 頂層阻焊
    BoardLayer.BL_B_Mask,      # 底層阻焊
    BoardLayer.BL_Edge_Cuts,   # 邊緣切割
    BoardLayer.BL_Dwgs_User,   # 用戶圖形層
]

# 獲取圖形元素預設設置
graphics_defaults = board.get_graphics_defaults()

# 設置方框的基本參數
center_x = from_mm(100)  # 中心點 X 座標
center_y = from_mm(100)  # 中心點 Y 座標
max_size = from_mm(50)   # 最大方框的寬度/高度
min_size = from_mm(10)   # 最小方框的寬度/高度
num_rectangles = 5       # 每一層繪製的方框數量

# 在每一層繪製同心方形
for layer_index, layer in enumerate(layers):
    # 方框間的大小差異
    size_step = (max_size - min_size) // (num_rectangles - 1)
    
    # 在當前層繪製多個同心方形
    for i in range(num_rectangles):
        # 計算當前方框的大小
        current_size = max_size - i * size_step
        
        # 計算方框的左上角和右下角座標
        half_size = current_size // 2
        top_left = Vector2.from_xy(center_x - half_size, center_y - half_size)
        bottom_right = Vector2.from_xy(center_x + half_size, center_y + half_size)
        
        # 創建方框
        rect = BoardRectangle()
        rect.top_left = top_left
        rect.bottom_right = bottom_right
        rect.layer = layer
        
        # 設置線寬
        rect.attributes.stroke.width = from_mm(0.2)
        
        # 根據層和方框大小設置不同的填充
        # 偶數層和偶數方框使用實心填充,其他使用無填充
        if (layer_index % 2 == 0) and (i % 2 == 0):
            rect.attributes.fill.mode = 1  # 實心填充
        else:
            rect.attributes.fill.mode = 0  # 無填充
        
        # 添加到電路板
        board.create_items(rect)

# 提交變更
board.push_commit(commit, message="在不同層繪製同心方形")
print("在多個層上繪製了同心方形")

截圖 2025-04-25 晚上10.28.10

截圖 2025-04-25 晚上10.28.31

常見任務快速參考

單位轉換

from kipy.util.units import from_mm, to_mm

# 毫米轉換為 KiCad 內部單位(納米)
position_nm = from_mm(10)  # 10mm 轉換為納米

# KiCad 內部單位轉換為毫米
size_mm = to_mm(1000000)  # 1,000,000 納米轉換為毫米

獲取層名稱

from kipy.proto.board.board_types_pb2 import BoardLayer
from kipy.util.board_layer import canonical_name

# 獲取層的標準名稱
layer = BoardLayer.BL_F_Cu
layer_name = canonical_name(layer)  # 返回 "F.Cu"

獲取選定的項目

# 獲取選定的項目
selected_items = board.get_selection()
print(f"選定了 {len(selected_items)} 個項目")

# 添加項目到選擇
board.add_to_selection(some_item)

# 清除選擇
board.clear_selection()

保存電路板

# 保存當前電路板
board.save()

# 另存為新文件
board.save_as("/path/to/new/board.kicad_pcb")

進階 API 使用技巧

處理事務提交

在 KiCad Python API 中,所有對電路板的修改都應該通過事務提交進行。這不僅可以確保修改被正確應用,還允許用戶撤銷操作。

from kipy.kicad import KiCad

kicad = KiCad()
board = kicad.get_board()

# 開始一個提交事務
commit = board.begin_commit()

# 進行修改...
# 例如創建、更新或刪除項目

try:
    # 提交更改並提供說明信息(顯示在撤銷/重做菜單中)
    board.push_commit(commit, "我的腳本修改")
    print("修改已成功提交")
except Exception as e:
    # 如果出現錯誤,放棄提交
    board.drop_commit(commit)
    print(f"修改失敗: {e}")

批量處理項目

當需要處理大量項目時,將它們批量處理可以提高效率:

from kipy.kicad import KiCad
from kipy.board_types import BoardText
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import BoardLayer

kicad = KiCad()
board = kicad.get_board()
commit = board.begin_commit()

# 創建多個文本項目
texts = []
for i in range(10):
    text = BoardText()
    text.value = f"項目 {i}"
    text.position = Vector2.from_xy(from_mm(100 + i*10), from_mm(100))
    text.layer = BoardLayer.BL_F_SilkS
    texts.append(text)

# 批量創建項目
created_items = board.create_items(texts)
print(f"批量創建了 {len(created_items)} 個項目")

# 提交更改
board.push_commit(commit, "批量添加文本項目")

自定義插件目錄結構

對於複雜的插件,建議使用以下目錄結構:

my_plugin/
├── plugin.json           # 插件配置文件
├── icon.png              # 插件圖標
├── __init__.py           # 初始化文件
├── main.py               # 主要入口點
├── dialog.py             # 對話框 UI
└── utils/                # 工具函數
    ├── __init__.py
    └── helpers.py

plugin.json 文件示例:

{
    "$schema": "https://go.kicad.org/api/schemas/v1",
    "identifier": "com.example.my-plugin",
    "name": "我的 KiCad 插件",
    "description": "一個實用的 KiCad PCB 編輯器插件",
    "version": "1.0.0",
    "author": {
        "name": "您的名字",
        "contact": {
            "web": "https://example.com"
        }
    },
    "runtime": {
        "type": "python",
        "min_version": "3.9"
    },
    "actions": [
        {
            "identifier": "my-action",
            "name": "執行我的功能",
            "description": "示範插件功能",
            "show-button": true,
            "scopes": ["pcb"],
            "entrypoint": "main.py"
        }
    ]
}

與圖形用戶界面集成

KiCad Python API 可以與 wxPython 結合,創建與 KiCad 風格一致的用戶界面:

import wx
from kipy.kicad import KiCad

class MyDialog(wx.Dialog):
    def __init__(self, parent):
        wx.Dialog.__init__(self, parent, title="我的插件對話框")
        
        # 創建控件
        self.text_ctrl = wx.TextCtrl(self)
        self.button = wx.Button(self, label="確定")
        
        # 設置佈局
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(wx.StaticText(self, label="請輸入文本:"), 0, wx.ALL, 5)
        sizer.Add(self.text_ctrl, 0, wx.EXPAND|wx.ALL, 5)
        sizer.Add(self.button, 0, wx.ALIGN_RIGHT|wx.ALL, 5)
        
        self.SetSizerAndFit(sizer)
        
        # 綁定事件
        self.button.Bind(wx.EVT_BUTTON, self.on_button_click)
    
    def on_button_click(self, event):
        text = self.text_ctrl.GetValue()
        print(f"用戶輸入: {text}")
        self.EndModal(wx.ID_OK)
        
def run_plugin():
    kicad = KiCad()
    board = kicad.get_board()
    
    # 創建對話框
    app = wx.App.Get()
    with MyDialog(wx.GetApp().GetTopWindow()) as dlg:
        if dlg.ShowModal() == wx.ID_OK:
            print("對話框確認")
            # 在這裡執行操作
        else:
            print("對話框取消")

常見問題解答 (FAQ)

Q: 新的 IPC API 與舊的 SWIG 綁定有什麼區別?

A: 新的 IPC API 與舊版 SWIG 綁定相比有以下主要區別:

  1. 穩定性:IPC API 設計為穩定接口,不會隨著 KiCad 內部代碼重構而改變
  2. 進程分離:IPC API 在單獨的進程中運行,通過 IPC 機制與 KiCad 通信,而 SWIG 綁定直接在 KiCad 進程中運行
  3. 語言無關:IPC API 可以從多種編程語言訪問,而不僅僅是 Python
  4. 更現代的 API 設計:提供了更一致、更易於使用的接口
  5. 穩定的 ABI:插件不需要針對每個 KiCad 版本重新編譯

Q: 如何調試 kipy 腳本?

A: 您可以使用標準的 Python 調試技術:

  1. 使用 print 語句輸出調試信息
  2. 使用 Python 的 logging 模塊記錄信息
  3. 使用 VSCode 或 PyCharm 等 IDE 的調試器進行交互式調試
  4. 使用 try-except 塊捕獲並打印詳細的錯誤信息
import logging

# 設置日誌記錄
logging.basicConfig(level=logging.DEBUG,
                   format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                   filename='kipy_debug.log')

try:
    # 您的代碼
    kicad = KiCad()
    board = kicad.get_board()
    logging.info(f"成功獲取電路板: {board.name}")
except Exception as e:
    logging.error(f"發生錯誤: {e}", exc_info=True)

Q: 我的 kipy 腳本無法連接到 KiCad,可能的原因是什麼?

A: 常見的連接問題包括:

  1. KiCad 中沒有啟用 API 服務器(首選項 > 插件中啟用)
  2. KiCad 版本與 kipy 版本不兼容
  3. 未運行 KiCad 或運行了多個 KiCad 實例
  4. 系統防火牆或安全軟件阻止了進程間通信

首先確保 KiCad 正在運行,並在設置中啟用了 API 服務器。然後嘗試重新安裝與您 KiCad 版本兼容的 kipy 版本。

Q: 如何創建使用 kipy 的獨立工具(非插件)?

A: 您可以創建直接使用 kipy 與運行中的 KiCad 通信的獨立 Python 腳本:

#!/usr/bin/env python3
from kipy.kicad import KiCad

def main():
    try:
        # 連接到運行中的 KiCad 實例
        kicad = KiCad()
        print(f"已連接到 KiCad {kicad.get_version()}")
        
        # 獲取當前電路板
        board = kicad.get_board()
        if not board:
            print("未找到打開的電路板")
            return
            
        # 執行您的操作
        # ...
        
    except Exception as e:
        print(f"錯誤: {e}")

if __name__ == "__main__":
    main()

運行這樣的腳本時,確保 KiCad 已經在運行並且已經打開了電路板。

Q: 我可以使用 kipy 創建完整的電路板嗎?

A: 是的,您可以從頭開始創建電路板,添加所有的元件和軌道。然而,通常更實用的做法是從現有的電路板開始,然後修改它。

Q: 我如何處理錯誤和例外?

A: kipy 會抛出 ApiError 和 ConnectionError 等異常。您應該處理這些異常以確保腳本在出現問題時能够正常處理:

from kipy.errors import ApiError, ConnectionError

try:
    # kipy 代碼
    kicad = KiCad()
    # ...
except ConnectionError as e:
    print(f"無法連接到 KiCad: {e}")
except ApiError as e:
    print(f"API 錯誤: {e}")

性能優化技巧

使用 KiCad Python API 處理大型電路板時,以下是一些提高性能的技巧:

  1. 批量處理:一次性提交多個更改,而不是逐個提交
  2. 限制重繪:在批量操作期間禁用重繪,完成後再恢復
  3. 使用適當的數據結構:例如使用 dict 建立網絡名稱到網絡對象的映射
  4. 避免不必要的查詢:緩存經常訪問的數據,而不是反复查詢

以下是優化批量更新操作的示例:

from kipy.kicad import KiCad

kicad = KiCad()
board = kicad.get_board()
commit = board.begin_commit()

# 預先獲取所有需要的數據
tracks = board.get_tracks()
nets = {net.name: net for net in board.get_nets()}

# 批量處理項目
to_update = []
for track in tracks:
    if track.width < from_mm(0.2):  # 找出寬度小於 0.2mm 的軌道
        track.width = from_mm(0.2)  # 設置為 0.2mm
        to_update.append(track)

# 一次性更新所有修改的軌道
if to_update:
    board.update_items(to_update)
    board.push_commit(commit, f"將 {len(to_update)} 條軌道寬度更新為 0.2mm")
    print(f"已更新 {len(to_update)} 條軌道")
else:
    board.drop_commit(commit)
    print("沒有需要更新的軌道")

實用範例與實際應用案例

自動化設計規則檢查

以下範例展示如何使用 kipy 進行設計規則檢查(DRC)並生成報告:

from kipy.kicad import KiCad
from kipy.util.units import to_mm
import csv
import datetime

def check_track_clearance(board, min_clearance_mm=0.2):
    """檢查軌道之間的最小間距"""
    tracks = board.get_tracks()
    violations = []
    
    # 這裡只是一個簡化的示例
    # 實際的間距檢查需要更複雜的算法
    for i, track1 in enumerate(tracks):
        for track2 in tracks[i+1:]:
            if track1.layer == track2.layer and track1.net != track2.net:
                # 這裡應有實際計算兩條軌道之間最小距離的代碼
                distance_mm = 0.1  # 假設值,實際需要計算
                if distance_mm < min_clearance_mm:
                    violations.append({
                        'track1': f"({to_mm(track1.start.x):.2f}, {to_mm(track1.start.y):.2f}) - ({to_mm(track1.end.x):.2f}, {to_mm(track1.end.y):.2f})",
                        'track2': f"({to_mm(track2.start.x):.2f}, {to_mm(track2.start.y):.2f}) - ({to_mm(track2.end.x):.2f}, {to_mm(track2.end.y):.2f})",
                        'distance': f"{distance_mm:.2f}mm",
                        'required': f"{min_clearance_mm:.2f}mm"
                    })
    return violations

def export_drc_report(violations, filename):
    """將違規導出為 CSV 報告"""
    with open(filename, 'w', newline='') as csvfile:
        writer = csv.DictWriter(csvfile, fieldnames=['track1', 'track2', 'distance', 'required'])
        writer.writeheader()
        for v in violations:
            writer.writerow(v)
    print(f"報告已保存至 {filename}")

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    print(f"正在檢查電路板: {board.name}")
    violations = check_track_clearance(board)
    
    if violations:
        print(f"發現 {len(violations)} 個間距違規")
        filename = f"drc_report_{datetime.datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
        export_drc_report(violations, filename)
    else:
        print("未發現間距違規")

if __name__ == "__main__":
    main()

自動生成裝配圖層

這個範例展示如何使用 kipy 在電路板上生成裝配圖層標記,用於製造和裝配:

from kipy.kicad import KiCad
from kipy.board_types import BoardText, BoardCircle
from kipy.geometry import Vector2
from kipy.util.units import from_mm, to_mm
from kipy.proto.board.board_types_pb2 import BoardLayer
import math

def create_fiducial_marks(board, layer=BoardLayer.BL_F_SilkS):
    """在電路板角落創建基準標記"""
    commit = board.begin_commit()
    
    # 獲取電路板邊界
    board_outline = board.get_board_outline()
    bb = board_outline.bounding_box
    
    # 計算角落位置(留出 5mm 邊距)
    margin = from_mm(5)
    corners = [
        Vector2.from_xy(bb.min_x + margin, bb.min_y + margin),  # 左下
        Vector2.from_xy(bb.max_x - margin, bb.min_y + margin),  # 右下
        Vector2.from_xy(bb.max_x - margin, bb.max_y - margin),  # 右上
        Vector2.from_xy(bb.min_x + margin, bb.max_y - margin)   # 左上
    ]
    
    # 創建基準標記(十字加圓)
    marks = []
    for i, pos in enumerate(corners):
        # 創建圓
        circle = BoardCircle()
        circle.center = pos
        circle.radius = from_mm(1)
        circle.layer = layer
        marks.append(circle)
        
        # 創建標籤
        text = BoardText()
        text.value = f"FID{i+1}"
        text.position = Vector2.from_xy(pos.x, pos.y + from_mm(2))
        text.layer = layer
        marks.append(text)
    
    # 創建元素並提交
    board.create_items(marks)
    board.push_commit(commit, "添加裝配基準標記")
    return len(marks)

def generate_assembly_labels(board):
    """為每個封裝生成裝配標籤"""
    commit = board.begin_commit()
    
    footprints = board.get_footprints()
    labels = []
    
    for fp in footprints:
        ref = fp.reference_field.text.value
        val = fp.value_field.text.value
        
        # 在元件上方2mm處創建標籤
        text = BoardText()
        text.value = f"{ref}:{val}"
        # 計算位置,考慮元件旋轉
        angle_rad = math.radians(fp.orientation.degrees)
        offset_x = -from_mm(2) * math.sin(angle_rad)
        offset_y = from_mm(2) * math.cos(angle_rad)
        text.position = Vector2.from_xy(fp.position.x + offset_x, fp.position.y + offset_y)
        text.layer = BoardLayer.BL_F_Fab  # 放在製造層
        labels.append(text)
    
    board.create_items(labels)
    board.push_commit(commit, "添加裝配標籤")
    return len(labels)

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    num_fiducials = create_fiducial_marks(board)
    print(f"已添加 {num_fiducials} 個基準標記")
    
    num_labels = generate_assembly_labels(board)
    print(f"已添加 {num_labels} 個裝配標籤")

if __name__ == "__main__":
    main()

PCB 自動佈局輔助工具

下面是一個簡單的工具,用於自動排列電路板上的某些元件:

from kipy.kicad import KiCad
from kipy.geometry import Vector2
from kipy.util.units import from_mm, to_mm
import re

def arrange_components_in_grid(board, ref_pattern, rows, cols, spacing_mm):
    """將匹配模式的元件按網格排列"""
    commit = board.begin_commit()
    
    # 查找匹配的元件
    footprints = board.get_footprints()
    matching_fps = []
    
    pattern = re.compile(ref_pattern)
    for fp in footprints:
        ref = fp.reference_field.text.value
        if pattern.match(ref):
            matching_fps.append(fp)
    
    if not matching_fps:
        print(f"找不到匹配 '{ref_pattern}' 的元件")
        return 0
    
    # 最多處理 rows*cols 個元件
    count = min(len(matching_fps), rows * cols)
    
    # 設置起始位置為第一個匹配元件的位置
    start_x = matching_fps[0].position.x
    start_y = matching_fps[0].position.y
    
    # 計算間距
    spacing_x = from_mm(spacing_mm)
    spacing_y = from_mm(spacing_mm)
    
    # 按網格排列元件
    for i in range(count):
        row = i // cols
        col = i % cols
        
        fp = matching_fps[i]
        fp.position = Vector2.from_xy(
            start_x + col * spacing_x,
            start_y + row * spacing_y
        )
    
    # 更新元件位置
    board.update_items(matching_fps[:count])
    board.push_commit(commit, f"按網格排列 {count} 個元件")
    
    return count

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    # 例如,將所有 LED 元件排成 3x4 網格,間距 10mm
    count = arrange_components_in_grid(board, r"LED\d+", 3, 4, 10)
    print(f"已排列 {count} 個元件")

if __name__ == "__main__":
    main()

自動創建PCB測試點

以下範例展示如何為特定網絡自動添加測試點:

from kipy.kicad import KiCad
from kipy.board_types import Via
from kipy.geometry import Vector2
from kipy.util.units import from_mm
from kipy.proto.board.board_types_pb2 import ViaType

def add_test_points(board, net_names, diameter_mm=1.0, drill_mm=0.5):
    """為指定網絡添加測試點"""
    commit = board.begin_commit()
    
    # 獲取所有網絡
    nets = board.get_nets()
    net_dict = {net.name: net for net in nets if net.name}
    
    # 查找匹配的網絡
    test_points = []
    for net_name in net_names:
        if net_name in net_dict:
            net = net_dict[net_name]
            
            # 獲取這個網絡的軌道,找到一個合適的位置
            tracks = [t for t in board.get_tracks() if t.net.code == net.code]
            if tracks:
                # 使用第一條軌道的中點作為測試點位置
                track = tracks[0]
                pos_x = (track.start.x + track.end.x) / 2
                pos_y = (track.start.y + track.end.y) / 2
                
                # 創建測試點(使用過孔)
                via = Via()
                via.position = Vector2.from_xy(pos_x, pos_y)
                via.type = ViaType.VT_THROUGH
                via.diameter = from_mm(diameter_mm)
                via.drill_diameter = from_mm(drill_mm)
                via.net = net
                
                test_points.append(via)
                print(f"為網絡 '{net_name}' 添加測試點")
            else:
                print(f"網絡 '{net_name}' 沒有軌道,無法添加測試點")
        else:
            print(f"找不到網絡 '{net_name}'")
    
    if test_points:
        board.create_items(test_points)
        board.push_commit(commit, f"添加 {len(test_points)} 個測試點")
        return len(test_points)
    else:
        board.drop_commit(commit)
        return 0

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    # 指定要添加測試點的網絡名稱
    net_names = ["GND", "VCC", "RST", "SCL", "SDA"]
    count = add_test_points(board, net_names)
    
    print(f"共添加了 {count} 個測試點")

if __name__ == "__main__":
    main()

與其他工具集成

與 Git 版本控制集成

這個範例展示如何使用 kipy 實現 KiCad 與 Git 版本控制的集成:

import os
import subprocess
from kipy.kicad import KiCad
import datetime

def git_commit_pcb_changes(board, commit_message=None):
    """保存電路板並將更改提交到 Git"""
    # 獲取電路板文件路徑
    board_path = board.filename
    
    if not os.path.isfile(board_path):
        print("電路板尚未保存,無法提交到 Git")
        return False
    
    # 保存電路板
    board.save()
    print(f"已保存電路板到: {board_path}")
    
    # 獲取 Git 倉庫根目錄
    try:
        repo_root = subprocess.check_output(
            ["git", "rev-parse", "--show-toplevel"],
            cwd=os.path.dirname(board_path),
            text=True
        ).strip()
    except subprocess.CalledProcessError:
        print("當前目錄不是 Git 倉庫")
        return False
    
    # 獲取相對路徑
    rel_path = os.path.relpath(board_path, repo_root)
    
    # 添加到暫存區
    try:
        subprocess.run(
            ["git", "add", rel_path],
            cwd=repo_root,
            check=True
        )
        
        # 創建提交信息
        if not commit_message:
            commit_message = f"更新電路板設計 {datetime.datetime.now().strftime('%Y-%m-%d %H:%M')}"
        
        # 提交更改
        subprocess.run(
            ["git", "commit", "-m", commit_message],
            cwd=repo_root,
            check=True
        )
        
        print(f"已將電路板更改提交到 Git: {commit_message}")
        return True
    except subprocess.CalledProcessError as e:
        print(f"Git 操作失敗: {e}")
        return False

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    # 進行一些修改...
    
    # 保存並提交修改
    git_commit_pcb_changes(board, "添加新的電源元件和軌道")

if __name__ == "__main__":
    main()

與 BOM 管理系統集成

以下範例展示如何生成物料清單(BOM)並與外部系統集成:

from kipy.kicad import KiCad
import csv
import json
import requests

def generate_bom(board):
    """生成電路板的物料清單"""
    footprints = board.get_footprints()
    
    # 按值和封裝分組元件
    components = {}
    for fp in footprints:
        ref = fp.reference_field.text.value
        val = fp.value_field.text.value
        pkg = fp.definition.identifier.lib_id.name if hasattr(fp.definition, 'identifier') else "Unknown"
        
        key = f"{val}|{pkg}"
        if key not in components:
            components[key] = {
                'value': val,
                'package': pkg,
                'references': [],
                'quantity': 0
            }
        
        components[key]['references'].append(ref)
        components[key]['quantity'] += 1
    
    # 轉換為列表
    bom_list = list(components.values())
    
    # 為每個項目添加引用字符串
    for item in bom_list:
        item['references_str'] = ", ".join(sorted(item['references']))
    
    return bom_list

def export_bom_csv(bom_list, filename):
    """將 BOM 導出為 CSV 文件"""
    with open(filename, 'w', newline='') as csvfile:
        writer = csv.DictWriter(
            csvfile,
            fieldnames=['value', 'package', 'references_str', 'quantity'],
            extrasaction='ignore'
        )
        writer.writeheader()
        writer.writerows(bom_list)
    print(f"BOM 已導出至 {filename}")
    return filename

def upload_bom_to_system(bom_list, api_url, api_key):
    """將 BOM 上傳到外部系統"""
    headers = {
        'Content-Type': 'application/json',
        'Authorization': f'Bearer {api_key}'
    }
    
    payload = {
        'project_name': 'My KiCad Project',
        'components': bom_list
    }
    
    try:
        response = requests.post(api_url, headers=headers, json=payload)
        response.raise_for_status()
        print(f"BOM 已成功上傳: {response.json().get('message', '')}")
        return True
    except requests.exceptions.RequestException as e:
        print(f"上傳失敗: {e}")
        return False

def main():
    kicad = KiCad()
    board = kicad.get_board()
    
    # 生成 BOM
    bom_list = generate_bom(board)
    
    # 導出為 CSV
    export_bom_csv(bom_list, "bom_export.csv")
    
    # 上傳到外部系統(需要實際的 API 端點和密鑰)
    # upload_bom_to_system(bom_list, "https://api.example.com/bom", "your_api_key")

if __name__ == "__main__":
    main()

API 參考

主要類和模塊

  • kipy.kicad:包含 KiCad 類,用於連接到 KiCad 實例
  • kipy.board_types:包含 Board、Track、Via、Footprint 等類
  • kipy.geometry:包含 Vector2、Angle、Box2 等幾何類
  • kipy.util:包含單位轉換和其他實用功能
  • kipy.proto:包含底層 protobuf 類型定義
  • kipy.errors:包含異常類型

常用常數

  • BoardLayer:定義電路板層(如 BL_F_Cu、BL_B_Cu、BL_F_SilkS)
  • ViaType:定義過孔類型(如 VT_THROUGH、VT_BLIND_BURIED)
  • DocumentType:定義文檔類型(如 DOCTYPE_BOARD、DOCTYPE_SCHEMATIC)

幾何操作

  • Vector2:表示二維向量或點
  • Angle:表示角度,提供度和弧度之間的轉換
  • Box2:表示二維軸對齊框

下一步


本文最初發布於 HackMD @BASHCAT。

用 Qwen3 1.7B 打造你的 macOS 桌面自動化 Agent 完整指南

qwen3-macos-agent-cover

想像一下,你對著電腦說「幫我整理下載資料夾,把所有 PDF 移到文件夾」,然後它就自動完成了。這不是科幻電影,而是你今天就能在自己的 Mac 上實現的事情。

本文將帶你從零開始,使用 Qwen3 1.7B 這個完全開源的小型語言模型,搭建一個能夠理解自然語言並執行實際操作的桌面自動化 Agent。

[!tip] 為什麼選擇 Qwen3 1.7B?

  • 完全本地運行:不需要 API 金鑰,不需要網路連線
  • 隱私保護:你的指令和資料永遠不會離開你的電腦
  • 原生 Function Calling:專門為工具調用優化,準確率高
  • 資源友好:僅需 1GB 記憶體,M1/M2 Mac 流暢運行
  • Apache 2.0 授權:完全開源,商用免費

架構概覽

qwen3-agent-architecture

我們要搭建的 Agent 遵循標準的 ReAct(Reasoning + Acting)架構:

[mermaid 圖表 — 原始 HackMD 版本可正常渲染]

flowchart LR A[用戶輸入] --> B[Qwen3 1.7B] B --> C{Function Calling} C --> D[檔案操作] C --> E[系統指令] C --> F[應用控制] C --> G[網頁操作] D --> H[執行結果] E --> H F --> H G --> H H --> B B --> I[回覆用戶]


環境準備

第一步:安裝 Ollama

Ollama 是在 Mac 上運行本地 LLM 的最佳選擇,安裝過程非常簡單:

# 方法一:使用 Homebrew(推薦)
brew install ollama

# 方法二:直接下載
# 前往 https://ollama.com/download 下載 macOS 版本

安裝完成後,啟動 Ollama 服務:

# 啟動服務(會在背景運行)
ollama serve

第二步:下載 Qwen3 1.7B 模型

# 下載模型(約 1GB)
ollama pull qwen3:1.7b

# 驗證安裝
ollama list

[!note] 模型變體選擇 Ollama 提供多種 Qwen3 版本:

  • qwen3:0.6b - 最輕量,適合資源受限環境
  • qwen3:1.7b - 平衡之選,Function Calling 表現優秀
  • qwen3:4b - 更強能力,需要更多記憶體
  • qwen3:8b - 最強能力,建議 16GB+ RAM

第三步:設置 Python 環境

# 建立虛擬環境
python3 -m venv agent-env
source agent-env/bin/activate

# 安裝必要套件
pip install ollama pyobjc pyautogui

# 可選:安裝 Qwen-Agent 框架(功能更強大)
pip install -U "qwen-agent[gui,rag,code_interpreter,mcp]"

基礎實作:最小可行 Agent

讓我們從最簡單的版本開始,理解核心概念:

定義工具函數

# agent_tools.py
import os
import subprocess
from datetime import datetime

def get_current_time() -> str:
    """取得目前時間"""
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

def list_files(directory: str) -> str:
    """列出指定目錄的檔案"""
    try:
        files = os.listdir(os.path.expanduser(directory))
        return "\n".join(files[:20])  # 限制回傳數量
    except Exception as e:
        return f"錯誤: {str(e)}"

def move_file(source: str, destination: str) -> str:
    """移動檔案"""
    try:
        src = os.path.expanduser(source)
        dst = os.path.expanduser(destination)
        os.rename(src, dst)
        return f"成功將 {source} 移動到 {destination}"
    except Exception as e:
        return f"錯誤: {str(e)}"

def run_shell_command(command: str) -> str:
    """執行 shell 指令(限制危險操作)"""
    # 安全檢查:禁止危險指令
    dangerous = ['rm -rf', 'sudo', 'mkfs', 'dd if=']
    if any(d in command for d in dangerous):
        return "錯誤: 不允許執行危險指令"

    try:
        result = subprocess.run(
            command,
            shell=True,
            capture_output=True,
            text=True,
            timeout=30
        )
        return result.stdout or result.stderr or "指令執行完成"
    except subprocess.TimeoutExpired:
        return "錯誤: 指令執行超時"
    except Exception as e:
        return f"錯誤: {str(e)}"

def open_application(app_name: str) -> str:
    """開啟 macOS 應用程式"""
    try:
        subprocess.run(['open', '-a', app_name], check=True)
        return f"已開啟 {app_name}"
    except Exception as e:
        return f"錯誤: {str(e)}"

# 工具定義(供 LLM 使用)
TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "get_current_time",
            "description": "取得目前的日期和時間",
            "parameters": {"type": "object", "properties": {}, "required": []}
        }
    },
    {
        "type": "function",
        "function": {
            "name": "list_files",
            "description": "列出指定目錄中的檔案和資料夾",
            "parameters": {
                "type": "object",
                "properties": {
                    "directory": {
                        "type": "string",
                        "description": "目錄路徑,例如 </sub>/Downloads 或 /Users/name/Documents"
                    }
                },
                "required": ["directory"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "move_file",
            "description": "將檔案從一個位置移動到另一個位置",
            "parameters": {
                "type": "object",
                "properties": {
                    "source": {"type": "string", "description": "來源檔案路徑"},
                    "destination": {"type": "string", "description": "目標路徑"}
                },
                "required": ["source", "destination"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "run_shell_command",
            "description": "執行 shell 指令,用於系統操作",
            "parameters": {
                "type": "object",
                "properties": {
                    "command": {"type": "string", "description": "要執行的 shell 指令"}
                },
                "required": ["command"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "open_application",
            "description": "開啟 macOS 應用程式",
            "parameters": {
                "type": "object",
                "properties": {
                    "app_name": {"type": "string", "description": "應用程式名稱,如 Safari、Finder、Notes"}
                },
                "required": ["app_name"]
            }
        }
    }
]

# 函數映射
FUNCTION_MAP = {
    "get_current_time": get_current_time,
    "list_files": list_files,
    "move_file": move_file,
    "run_shell_command": run_shell_command,
    "open_application": open_application
}

建立 Agent 核心

# agent_core.py
import json
from ollama import chat
from agent_tools import TOOLS, FUNCTION_MAP

class MacOSAgent:
    def __init__(self, model: str = "qwen3:1.7b"):
        self.model = model
        self.conversation_history = []
        self.system_prompt = """你是一個 macOS 桌面自動化助手。
你可以幫助用戶執行檔案管理、開啟應用程式、執行系統指令等任務。
請使用繁體中文回覆。

重要規則:
1. 在執行任何操作前,先確認用戶的意圖
2. 對於可能影響系統的操作,要謹慎處理
3. 執行完成後,清楚回報結果
4. 如果遇到錯誤,提供解決建議"""

    def process_tool_calls(self, tool_calls):
        """處理工具調用並返回結果"""
        results = []
        for tool_call in tool_calls:
            func_name = tool_call.function.name

            # 解析參數
            try:
                args = json.loads(tool_call.function.arguments)
            except json.JSONDecodeError:
                args = {}

            # 執行函數
            if func_name in FUNCTION_MAP:
                print(f"  🔧 執行工具: {func_name}")
                print(f"     參數: {args}")
                result = FUNCTION_MAP[func_name](**args)
                print(f"     結果: {result[:100]}..." if len(str(result)) > 100 else f"     結果: {result}")
                results.append({
                    "role": "tool",
                    "content": result
                })
            else:
                results.append({
                    "role": "tool",
                    "content": f"未知工具: {func_name}"
                })

        return results

    def chat(self, user_message: str) -> str:
        """與 Agent 對話"""
        # 添加用戶訊息
        self.conversation_history.append({
            "role": "user",
            "content": user_message
        })

        # 準備完整訊息
        messages = [
            {"role": "system", "content": self.system_prompt}
        ] + self.conversation_history

        # 調用 LLM
        response = chat(
            model=self.model,
            messages=messages,
            tools=TOOLS
        )

        assistant_message = response.message

        # 檢查是否有工具調用
        if assistant_message.tool_calls:
            # 添加助手的工具調用訊息
            self.conversation_history.append({
                "role": "assistant",
                "content": assistant_message.content or "",
                "tool_calls": [
                    {
                        "function": {
                            "name": tc.function.name,
                            "arguments": tc.function.arguments
                        }
                    } for tc in assistant_message.tool_calls
                ]
            })

            # 執行工具並獲取結果
            tool_results = self.process_tool_calls(assistant_message.tool_calls)
            self.conversation_history.extend(tool_results)

            # 讓 LLM 根據工具結果生成最終回覆
            messages = [
                {"role": "system", "content": self.system_prompt}
            ] + self.conversation_history

            final_response = chat(
                model=self.model,
                messages=messages,
                tools=TOOLS
            )

            final_content = final_response.message.content
            self.conversation_history.append({
                "role": "assistant",
                "content": final_content
            })
            return final_content
        else:
            # 沒有工具調用,直接返回回覆
            self.conversation_history.append({
                "role": "assistant",
                "content": assistant_message.content
            })
            return assistant_message.content

    def reset(self):
        """重置對話歷史"""
        self.conversation_history = []


def main():
    """互動式 Agent 介面"""
    print("=" * 50)
    print("🤖 macOS 桌面自動化 Agent")
    print("   模型: Qwen3 1.7B (本地運行)")
    print("=" * 50)
    print("輸入 'quit' 退出,'reset' 重置對話\n")

    agent = MacOSAgent()

    while True:
        try:
            user_input = input("👤 你: ").strip()

            if not user_input:
                continue
            if user_input.lower() == 'quit':
                print("再見!")
                break
            if user_input.lower() == 'reset':
                agent.reset()
                print("對話已重置。\n")
                continue

            print("\n🤖 Agent: ", end="")
            response = agent.chat(user_input)
            print(response)
            print()

        except KeyboardInterrupt:
            print("\n再見!")
            break


if __name__ == "__main__":
    main()

進階功能:macOS 專屬自動化

AppleScript 整合

macOS 的殺手鐧是 AppleScript,它能控制幾乎所有原生應用:

# applescript_tools.py
import subprocess

def run_applescript(script: str) -> str:
    """執行 AppleScript"""
    try:
        result = subprocess.run(
            ['osascript', '-e', script],
            capture_output=True,
            text=True,
            timeout=30
        )
        return result.stdout.strip() or result.stderr.strip() or "執行完成"
    except Exception as e:
        return f"錯誤: {str(e)}"

def get_frontmost_app() -> str:
    """取得目前最前方的應用程式"""
    script = '''
    tell application "System Events"
        return name of first application process whose frontmost is true
    end tell
    '''
    return run_applescript(script)

def set_volume(level: int) -> str:
    """設定系統音量 (0-100)"""
    level = max(0, min(100, level))
    script = f'set volume output volume {level}'
    run_applescript(script)
    return f"音量已設為 {level}%"

def send_notification(title: str, message: str) -> str:
    """發送系統通知"""
    script = f'''
    display notification "{message}" with title "{title}"
    '''
    run_applescript(script)
    return "通知已發送"

def get_clipboard() -> str:
    """取得剪貼簿內容"""
    script = 'return the clipboard'
    return run_applescript(script)

def set_clipboard(content: str) -> str:
    """設定剪貼簿內容"""
    # 轉義特殊字元
    content = content.replace('\\', '\\\\').replace('"', '\\"')
    script = f'set the clipboard to "{content}"'
    run_applescript(script)
    return "已複製到剪貼簿"

def create_reminder(title: str, due_date: str = None) -> str:
    """在提醒事項中建立新提醒"""
    if due_date:
        script = f'''
        tell application "Reminders"
            make new reminder with properties {{name:"{title}", due date:date "{due_date}"}}
        end tell
        '''
    else:
        script = f'''
        tell application "Reminders"
            make new reminder with properties {{name:"{title}"}}
        end tell
        '''
    run_applescript(script)
    return f"提醒 '{title}' 已建立"

def create_calendar_event(title: str, start_time: str, end_time: str) -> str:
    """在行事曆建立新事件"""
    script = f'''
    tell application "Calendar"
        tell calendar "行事曆"
            make new event with properties {{summary:"{title}", start date:date "{start_time}", end date:date "{end_time}"}}
        end tell
    end tell
    '''
    run_applescript(script)
    return f"事件 '{title}' 已建立"

def get_safari_url() -> str:
    """取得 Safari 目前頁面的 URL"""
    script = '''
    tell application "Safari"
        return URL of current tab of front window
    end tell
    '''
    return run_applescript(script)

def open_url_in_safari(url: str) -> str:
    """在 Safari 開啟 URL"""
    script = f'''
    tell application "Safari"
        activate
        open location "{url}"
    end tell
    '''
    run_applescript(script)
    return f"已在 Safari 開啟 {url}"

PyAutoGUI 視覺自動化

對於沒有 AppleScript 支援的應用程式,可以使用 PyAutoGUI:

# gui_automation.py
import pyautogui
import time

# 安全設定
pyautogui.FAILSAFE = True  # 移動到螢幕角落可以中止
pyautogui.PAUSE = 0.5  # 每個動作間隔 0.5 秒

def take_screenshot(filename: str = None) -> str:
    """擷取螢幕截圖"""
    if filename is None:
        filename = f"screenshot_{int(time.time())}.png"

    screenshot = pyautogui.screenshot()
    screenshot.save(filename)
    return f"截圖已保存: {filename}"

def click_at(x: int, y: int) -> str:
    """在指定座標點擊"""
    pyautogui.click(x, y)
    return f"已點擊座標 ({x}, {y})"

def type_text(text: str, interval: float = 0.05) -> str:
    """輸入文字"""
    pyautogui.typewrite(text, interval=interval)
    return f"已輸入: {text}"

def hotkey(*keys) -> str:
    """按下組合鍵"""
    pyautogui.hotkey(*keys)
    return f"已按下組合鍵: {'+'.join(keys)}"

def scroll_screen(clicks: int, x: int = None, y: int = None) -> str:
    """滾動螢幕"""
    pyautogui.scroll(clicks, x=x, y=y)
    direction = "上" if clicks > 0 else "下"
    return f"已向{direction}滾動 {abs(clicks)} 格"

def get_mouse_position() -> str:
    """取得目前滑鼠位置"""
    x, y = pyautogui.position()
    return f"滑鼠位置: ({x}, {y})"

def locate_on_screen(image_path: str) -> str:
    """在螢幕上尋找圖片位置"""
    try:
        location = pyautogui.locateOnScreen(image_path, confidence=0.9)
        if location:
            center = pyautogui.center(location)
            return f"找到圖片,中心位置: ({center.x}, {center.y})"
        return "未找到圖片"
    except Exception as e:
        return f"錯誤: {str(e)}"

完整整合:Production-Ready Agent

將所有工具整合為一個完整的 Agent:

# macos_agent.py
import json
from ollama import chat
from agent_tools import TOOLS as BASE_TOOLS, FUNCTION_MAP as BASE_FUNCTIONS
from applescript_tools import (
    run_applescript, get_frontmost_app, set_volume,
    send_notification, get_clipboard, set_clipboard,
    create_reminder, get_safari_url, open_url_in_safari
)
from gui_automation import (
    take_screenshot, click_at, type_text, hotkey,
    scroll_screen, get_mouse_position
)

# 擴展工具定義
EXTENDED_TOOLS = BASE_TOOLS + [
    {
        "type": "function",
        "function": {
            "name": "set_volume",
            "description": "設定系統音量",
            "parameters": {
                "type": "object",
                "properties": {
                    "level": {"type": "integer", "description": "音量等級 (0-100)"}
                },
                "required": ["level"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "send_notification",
            "description": "發送 macOS 系統通知",
            "parameters": {
                "type": "object",
                "properties": {
                    "title": {"type": "string", "description": "通知標題"},
                    "message": {"type": "string", "description": "通知內容"}
                },
                "required": ["title", "message"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "create_reminder",
            "description": "在提醒事項 App 建立新提醒",
            "parameters": {
                "type": "object",
                "properties": {
                    "title": {"type": "string", "description": "提醒標題"},
                    "due_date": {"type": "string", "description": "到期日期,格式如 'January 15, 2025 10:00 AM'"}
                },
                "required": ["title"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "take_screenshot",
            "description": "擷取螢幕截圖",
            "parameters": {
                "type": "object",
                "properties": {
                    "filename": {"type": "string", "description": "儲存檔名(可選)"}
                },
                "required": []
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_clipboard",
            "description": "取得剪貼簿內容",
            "parameters": {"type": "object", "properties": {}, "required": []}
        }
    },
    {
        "type": "function",
        "function": {
            "name": "set_clipboard",
            "description": "設定剪貼簿內容",
            "parameters": {
                "type": "object",
                "properties": {
                    "content": {"type": "string", "description": "要複製的內容"}
                },
                "required": ["content"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "open_url_in_safari",
            "description": "在 Safari 瀏覽器開啟網址",
            "parameters": {
                "type": "object",
                "properties": {
                    "url": {"type": "string", "description": "要開啟的網址"}
                },
                "required": ["url"]
            }
        }
    }
]

# 擴展函數映射
EXTENDED_FUNCTIONS = {
    **BASE_FUNCTIONS,
    "set_volume": set_volume,
    "send_notification": send_notification,
    "create_reminder": create_reminder,
    "take_screenshot": take_screenshot,
    "get_clipboard": get_clipboard,
    "set_clipboard": set_clipboard,
    "open_url_in_safari": open_url_in_safari,
}


class ProductionAgent:
    """Production-ready macOS 自動化 Agent"""

    def __init__(self, model: str = "qwen3:1.7b"):
        self.model = model
        self.history = []
        self.max_iterations = 5  # 防止無限循環

    def run(self, task: str) -> str:
        """執行任務"""
        self.history = [{"role": "user", "content": task}]

        system = """你是一個專業的 macOS 桌面自動化助手。
你的能力包括:
- 檔案和資料夾管理
- 系統設定調整
- 應用程式控制
- 提醒事項和行事曆管理
- 螢幕截圖
- 剪貼簿操作
- 網頁瀏覽

執行任務時:
1. 分析用戶需求,選擇合適的工具
2. 一步一步執行,確認每步結果
3. 遇到問題時提供解決方案
4. 完成後總結執行結果

請使用繁體中文回覆。"""

        for i in range(self.max_iterations):
            response = chat(
                model=self.model,
                messages=[{"role": "system", "content": system}] + self.history,
                tools=EXTENDED_TOOLS
            )

            msg = response.message

            if msg.tool_calls:
                # 記錄工具調用
                self.history.append({
                    "role": "assistant",
                    "content": msg.content or "",
                    "tool_calls": [
                        {"function": {"name": tc.function.name, "arguments": tc.function.arguments}}
                        for tc in msg.tool_calls
                    ]
                })

                # 執行工具
                for tc in msg.tool_calls:
                    func_name = tc.function.name
                    try:
                        args = json.loads(tc.function.arguments)
                    except:
                        args = {}

                    if func_name in EXTENDED_FUNCTIONS:
                        result = EXTENDED_FUNCTIONS[func_name](**args)
                    else:
                        result = f"未知工具: {func_name}"

                    self.history.append({"role": "tool", "content": str(result)})
            else:
                # 沒有工具調用,任務完成
                self.history.append({"role": "assistant", "content": msg.content})
                return msg.content

        return "任務執行超過最大迭代次數"


if __name__ == "__main__":
    agent = ProductionAgent()

    # 測試範例
    tasks = [
        "現在幾點了?",
        "幫我把音量調到 50%",
        "列出我下載資料夾裡的檔案",
        "發送一個通知提醒我開會"
    ]

    for task in tasks:
        print(f"\n📋 任務: {task}")
        print(f"🤖 回應: {agent.run(task)}")
        print("-" * 40)

實用範例

範例一:整理下載資料夾

👤: 幫我把下載資料夾裡的 PDF 檔案移到文件資料夾

🤖: 我來幫你整理。首先讓我看看下載資料夾裡有什麼...

    🔧 執行工具: list_files
       參數: {"directory": "<sub>/Downloads"}
       結果: report.pdf, image.png, video.mp4, document.pdf...

    找到 2 個 PDF 檔案。現在移動它們...

    🔧 執行工具: move_file
       參數: {"source": "</sub>/Downloads/report.pdf", "destination": "<sub>/Documents/report.pdf"}
       結果: 成功移動

    🔧 執行工具: move_file
       參數: {"source": "</sub>/Downloads/document.pdf", "destination": "<sub>/Documents/document.pdf"}
       結果: 成功移動

    完成!已將 2 個 PDF 檔案移動到文件資料夾。

範例二:建立提醒

👤: 提醒我明天下午三點開會

🤖: 🔧 執行工具: create_reminder
       參數: {"title": "開會", "due_date": "January 7, 2025 3:00 PM"}
       結果: 提醒 '開會' 已建立

    已在提醒事項中建立「開會」提醒,時間設定為明天下午 3:00。

範例三:快速工作流

👤: 開啟 Safari 並幫我截圖

🤖: 🔧 執行工具: open_application
       參數: {"app_name": "Safari"}
       結果: 已開啟 Safari

    🔧 執行工具: take_screenshot
       參數: {"filename": "safari_screenshot.png"}
       結果: 截圖已保存: safari_screenshot.png

    Safari 已開啟,螢幕截圖已保存為 safari_screenshot.png。

安全考量

[!warning] 重要安全提醒 讓 AI 控制你的電腦是有風險的。請務必實作以下安全措施:

1. 指令白名單

SAFE_COMMANDS = {
    'ls', 'pwd', 'echo', 'cat', 'head', 'tail',
    'mkdir', 'cp', 'mv', 'find', 'grep'
}

def is_safe_command(command: str) -> bool:
    """檢查指令是否安全"""
    first_word = command.split()[0] if command.split() else ""
    return first_word in SAFE_COMMANDS

2. 確認機制

def execute_with_confirmation(action: str, func, *args):
    """執行前要求用戶確認"""
    print(f"⚠️  即將執行: {action}")
    confirm = input("確認執行?(y/n): ")
    if confirm.lower() == 'y':
        return func(*args)
    return "操作已取消"

3. 沙箱目錄

SANDBOX_DIR = os.path.expanduser("</sub>/AgentSandbox")

def validate_path(path: str) -> bool:
    """確保路徑在沙箱範圍內"""
    abs_path = os.path.abspath(os.path.expanduser(path))
    return abs_path.startswith(SANDBOX_DIR)

延伸閱讀


結語

使用 Qwen3 1.7B 搭建本地自動化 Agent 是一個完美的起點。它足夠小巧可以在任何現代 Mac 上流暢運行,同時具備出色的 Function Calling 能力。

從這個基礎出發,你可以:

  • 添加更多工具(郵件、訊息、音樂控制等)
  • 整合 MCP 協議連接更多服務
  • 加入語音輸入實現真正的語音助手
  • 建立自定義工作流自動化日常任務

本地 AI 的時代已經來臨,而你的 Mac 正是最佳的實驗場所。


本文最後更新:2025-01-06


本文最初發布於 HackMD @BASHCAT。

TileLang 完整解析:當 80 行 Python 打敗 500 行 CUDA,GPU 編程的新革命

tilelang-cover

80 行 Python,實現了跟 FlashMLA 手寫 CUDA 版本同等效能的 GPU kernel。比 PyTorch 快 3.76 倍,比 Triton 快將近 2 倍。而且同一份程式碼,能同時跑在 NVIDIA H100、AMD MI300X、華為 Ascend NPU 上面。

這不是某個 AI 生成的幻覺數據。這是 TileLang 在 ICLR 2026 以 Oral paper 身份發表的實測結果 —— 在近兩萬篇投稿中,只有 1.18% 的論文拿到 Oral。

你可能已經隱約聽過這個名字。2025 年 9 月 DeepSeek 發布 V3.2-Exp 模型時,技術報告裡藏了一段耐人尋味的話:「我們使用高階語言 TileLang 進行快速原型設計......建議社群使用 TileLang 版本進行研究實驗。」同一天,華為、寒武紀、海光同步宣布支持。

這到底是一個純粹的技術突破,還是 GPU 編程生態的板塊位移?我花了相當長的時間研究這個項目的每一個面向。這篇文章,就是我的完整拆解。


TileLang 到底是什麼?一句話說清楚

先釐清一個容易混淆的點:TileLang 不是一個新的程式語言,也不是 CUDA 的替代品。它是一套 Python 嵌入式的領域特定語言(DSL),專門用來寫 GPU/CPU/加速器上的高效能運算 kernel。

什麼意思呢?你用 Python 的語法寫程式碼,但 TileLang 的編譯器會把你寫的東西轉換成跟手寫 CUDA 同等效能的底層指令。

它的核心抽象是 Tile(方塊/磁磚)。在 GPU 的世界裡,效能瓶頸往往不在運算本身,而在資料搬運。資料要從全域記憶體搬到共享記憶體,再搬到暫存器,每一層的頻寬和延遲都天差地別。TileLang 讓你用「方塊」的視角來思考這些資料搬運 —— 你告訴編譯器「把這塊資料放到共享記憶體」、「在暫存器裡做矩陣乘法」,至於底下怎麼分配線程、怎麼做記憶體排列,編譯器幫你搞定。

這跟 CUDA、Triton 有什麼不一樣?看一個直覺的對比:

框架 你需要管什麼 編譯器幫你做什麼
CUDA 線程、warp、共享記憶體、同步、指令排程 幾乎不幫你
Triton Block 級運算邏輯 記憶體管理、部分排程
TileLang Tile 級資料流和記憶體放置 Layout 推導、流水線、warp 特化

簡單說,CUDA 是手動擋,Triton 是半自動,TileLang 是帶主動安全輔助的自動擋 —— 但你仍然可以隨時切回手動模式。


從北大實驗室到 ICLR Oral:背後的故事

TileLang 的故事要從北京大學說起。主要開發者 LeiWang1999(王磊)在楊智教授的指導下,和團隊成員 chengyupku、nox-410 一起打造了這個專案。部分工作是在微軟研究院實習期間完成的,微軟的 Lingxiao Ma、Yuqing Xia 等研究員也提供了重要指導。

時間線大致是這樣的:

  • 2024 年:核心開發期,同時開發了 BitBLAS(混合精度計算庫)作為早期驗證
  • 2025 年 1 月 20 日:正式開源,首個版本發布
  • 2025 年 2 月:v0.1.0 發布,加入 debug 工具和 WebGPU 支持
  • 2025 年 3 月:80 行 Python 實現 FlashMLA,效能打平手寫 CUDA
  • 2025 年 4 月:論文提交 arXiv(2504.17577)
  • 2025 年 9 月:DeepSeek V3.2-Exp 正式採用 TileLang operators
  • 2025 年 12 月:加入 CuTe DSL 後端;同月 NVIDIA 推出 CUDA Tile
  • 2026 年 1 月:被 ICLR 2026 接受為 Oral paper
  • 2026 年 2 月:TileLang Puzzles 互動學習工具上線

一年出頭的時間,從零到 5,200+ GitHub stars,被頂級會議 Oral 接受,被 DeepSeek 這種量級的項目採用。這個速度在開源基礎設施項目裡相當罕見。


編譯器深度拆解:五階段管線如何運作

tilelang-compiler-pipeline

TileLang 真正的技術含量藏在它的編譯器裡。你寫的那 80 行 Python,要經過五個階段才能變成 GPU 上飛速運行的機器碼。

管線全貌

Python Code → Parser → IR Builder → Optimization → Codegen → GPU Execution
                ↓          ↓            ↓              ↓
           Python AST   TVM IR    Layout/Pipeline   CUDA C/HIP C/
           → TileLang             Inference         LLVM IR
              AST

第一步 Parser:你用 @T.prim_func 裝飾器寫的 kernel 函式,先被解析成 Python AST,再轉成 TileLang 自己的 AST。

第二步 IR Builder:AST 被轉換成 TVM 的中間表示(IR)。TileLang 建構在 Apache TVM 之上,這讓它能直接利用 TVM 成熟的語法樹基礎設施。

第三步 Optimization:這是魔法發生的地方。三大核心優化在這裡執行。

第四步 Codegen:優化後的 IR 被翻譯成目標平台的程式碼 —— CUDA C/C++、HIP C/C++、或 LLVM IR。

第五步 Execution:JIT 編譯並在目標硬體上執行。

三大核心優化機制

要理解 TileLang 為什麼能用這麼少的程式碼達到這麼高的效能,關鍵在這三個編譯器 pass:

1. Layout Inference(佈局推導)

你在 TileLang 裡分配一塊共享記憶體或暫存器,不需要指定它該怎麼在線程之間分配。編譯器會根據後續的運算(比如 T.gemm)自動推導出最佳的記憶體佈局和線程綁定。

這用一個分層優先級系統實現 —— 越高優先級的運算(比如 Tensor Core 的 GEMM)對佈局有越嚴格的要求,編譯器從上往下逐層推導,直到所有 buffer 的佈局都確定。

2. Pipeline Inference(流水線推導)

在高效能 kernel 裡,資料搬運和計算要重疊執行。傳統做法需要開發者手動設計 software pipeline —— 在 CUDA 裡這意味著大量的 barrier、async copy、多重 buffer。TileLang 只需要你指定 T.Pipelined(iters, num_stages=N),編譯器會自動分析依賴關係,決定哪些操作可以重疊。

3. Warp Specialization(自動 warp 特化)

在 NVIDIA Hopper(H100)架構上,最佳效能需要 warp specialization:讓一部分 warp 專門負責資料搬運(用 TMA),另一部分專門做計算。手動實現這個需要管理 mbarrier 同步物件、生產者/消費者邏輯 —— 這在 CUDA 裡是極其痛苦的工作。

TileLang 完全自動化了這個過程。它分析 buffer 的使用模式,自動將語句分類為生產者和消費者,然後插入正確的同步屏障。

實際程式碼長什麼樣?

一個用 TileLang 寫的矩陣乘法 kernel,核心邏輯大概是這樣:

import tilelang
import tilelang.language as T

@tilelang.jit
def matmul(A, B, block_M=128, block_N=128, block_K=32):
    M, K = A.shape
    K, N = B.shape
    C = T.empty((M, N), A.dtype)

    with T.Kernel(T.ceildiv(N, block_N), T.ceildiv(M, block_M), threads=128) as (bx, by):
        A_shared = T.alloc_shared((block_M, block_K), A.dtype)
        B_shared = T.alloc_shared((block_K, block_N), B.dtype)
        C_local  = T.alloc_fragment((block_M, block_N), "float32")
        T.clear(C_local)

        for ko in T.Pipelined(T.ceildiv(K, block_K), num_stages=3):
            T.copy(A[by * block_M, ko * block_K], A_shared)
            T.copy(B[ko * block_K, bx * block_N], B_shared)
            T.gemm(A_shared, B_shared, C_local)

        T.copy(C_local, C[by * block_M, bx * block_N])
    return C

注意看 —— 沒有 threadIdx,沒有手動同步,沒有記憶體佈局計算,沒有 warp 分工。你只需要說「分配共享記憶體」、「複製資料」、「做矩陣乘法」、「用 3 階段流水線」。剩下的,編譯器全包了。


效能數據不會說謊:TileLang 的硬實力

tilelang-performance

空談設計哲學沒有意義,看數字。以下數據來自 TileLang 論文和 AMD ROCm 技術部落格的實測結果。

FlashMLA Benchmark(DeepSeek 的 MLA 注意力機制)

框架 相對延遲(越低越好) 代碼量
PyTorch 1.00x(基準) N/A
Triton 0.655x 200+ 行
TileLang 0.371x 80 行
FlashMLA (CUDA) ~0.37x 數千行 CUDA + CUTLASS

TileLang 用 80 行 Python 達到了跟手寫 CUTLASS 模板相當的效能,這不是小幅改進,這是量級上的開發效率提升。

跨平台效能摘要

根據 ICLR 2026 論文的 Abstract:

  • NVIDIA H100:比 Triton 快最高 5 倍
  • AMD MI300X:比 Triton 快最高 6 倍

在 AMD MI300X 上的 Flash Attention 測試中,TileLang 實現比原生 PyTorch 快了 2.7 倍,比 Triton 快 1.53 倍。而且自動調優框架搜尋 108 個配置只需要約 1 秒。

什麼場景效能最突出?

TileLang 的效能優勢在 融合型 kernel(fused kernels)上最為明顯 —— 也就是把多個操作合併到一個 kernel 裡的場景。FlashAttention、MLA Decoding、Linear Attention 這類需要精細記憶體管理的複雜 kernel,正是 TileLang 的主戰場。

對於簡單的 elementwise 操作或標準 GEMM,TileLang 和 cuBLAS/Triton 的差距就沒這麼大。Tile 級抽象的優勢在複雜度越高的 kernel 中越能體現。


四方混戰:TileLang vs Triton vs CUDA vs cuTile

tilelang-competition

GPU kernel 編程領域正在經歷一場前所未有的混戰。讓我把幾個主要玩家攤開來比較。

完整對比表

維度 TileLang Triton CUDA NVIDIA cuTile
開發方 北大 / tile-ai OpenAI NVIDIA NVIDIA
語言 Python DSL Python DSL C/C++ Python DSL
抽象層級 Tile-level Block-level Thread-level Tile-level
記憶體控制 明確放置 隱式管理 完全手動 隱式
跨平台 NVIDIA + AMD + 華為 + 摩爾 NVIDIA + AMD 僅 NVIDIA 僅 NVIDIA
自動 Warp Specialization 有 開發中 手動 有
自動 Software Pipeline 有 有 手動 有
FP8 支持 部分 完整 完整 完整
生態成熟度 早期 中等 非常成熟 初始
開源 Apache 2.0 MIT 部分 開源

關鍵差異解讀

TileLang vs Triton:最核心的差異在於記憶體控制的粒度。Triton 隱藏了共享記憶體的細節,讓編譯器自動決定。TileLang 則明確暴露記憶體層級,讓開發者可以精確控制資料放在哪裡。這在簡單 kernel 上差異不大,但在複雜融合 kernel(如 FlashAttention)上,TileLang 的明確控制帶來顯著的效能優勢。

TileLang vs cuTile:NVIDIA 在 2025 年 12 月隨 CUDA 13.1 推出了 cuTile,這被廣泛認為是對 TileLang 和 tile 編程趨勢的直接回應。cuTile 的定位跟 TileLang 很像 —— Python DSL、tile 級抽象。但關鍵差異是:cuTile 只支持 NVIDIA GPU,而且目前僅限 Blackwell 架構。TileLang 則是跨平台的。

為什麼 NVIDIA 也跟著做? Hacker News 上一位用戶一針見血地指出:「NVIDIA 不希望 CUDA 的開發(如 FlashAttention)遷移到 Triton,因為 Triton 也支持 AMD。如果生態從純 CUDA 遷移到 Triton,那對 NVIDIA 的鎖定效應不利。」cuTile 的出現,某種程度上是 NVIDIA 在 tile 編程趨勢下的防禦性佈局。


不只是技術:GPU 編程的地緣棋局

tilelang-geopolitics

如果你只把 TileLang 當成一個技術項目來看,你會錯過故事最重要的一章。

2025 年 9 月 29 日,DeepSeek 發布 V3.2-Exp。同一天,華為 Ascend 團隊更新了 CANN 對 V3.2-Exp 的支持,寒武紀更新了 vLLM-MLU,SGLang 確認了多後端支持。Tom's Hardware 的報導指出,這種同步性暗示著預先協調。

分析機構 HelloChinaTech 的評論更直接:「TileLang 的推出標誌著建構中國自主 AI 軟體棧的第二階段。」第一階段是 FP8 精度標準的建立(DeepSeek V3 的訓練方法論),第二階段就是透過 TileLang 建立跨硬體的編程抽象層。

這個邏輯鏈條是這樣的:

  1. 中國的 AI 晶片廠商(華為、寒武紀、摩爾線程、海光)各自有不同的硬體架構
  2. 如果每家都要開發者學一套新的編程模型,生態碎片化會極為嚴重
  3. TileLang 提供了一個統一的抽象層 —— 同一份程式碼可以編譯到不同後端
  4. 這降低了從 CUDA 遷移的門檻,也減少了對 NVIDIA 生態的依賴

但也要保持冷靜。正如 IEEE Spectrum 的分析所指出的,中國晶片廠商的硬體能力與 NVIDIA 仍有代差。軟體棧的統一是必要條件,但不是充分條件。CUDA 的護城河不僅是語言本身,更是 15 年積累的函式庫、工具鏈、開發者社群和 debug 生態。


冷靜看待:TileLang 的局限與挑戰

技術文章如果只說好的,不說問題,那就是廣告。TileLang 確實有幾個需要正視的局限。

FP8 支持仍不完善。 學術論文 Tawa(arXiv: 2510.14719)的實測數據顯示,在 FP8 精度下,TileLang 的 GEMM 效能落後 Tawa 最高 3.99 倍,FlashAttention 的 FP8 配置甚至無法成功執行。論文指出原因是 TileLang 缺乏處理 FP8 WGMMA 路徑所需的佈局管理和流水線調度。在 FP8 訓練越來越普及的當下,這是一個必須盡快補上的短板。

生態仍處於早期階段。 5,200 個 GitHub stars 聽起來不少,但跟 Triton 的生態規模相比仍有明顯差距。文件、教學資源、第三方整合都還在建設中。好消息是 TileLang Puzzles 等互動學習工具正在降低入門門檻。

設計上的限制。 社群開發者在實作 DeepSeek DSA(Dynamic Sparse Attention)時發現,TileLang 0.1.6 版本在非 per-head layout 的場景下存在限制,無法很好地支持某些進階開發思路(相關 Issue #1199)。

主要針對 AI workload。 TileLang 的抽象是為深度學習運算(GEMM、Attention、Convolution)量身設計的。如果你的需求是通用的 GPU 計算(圖形渲染、物理模擬、密碼學),CUDA 仍然是更好的選擇。


開發者該怎麼做?實際行動建議

tilelang-roadmap

說了這麼多,對於不同角色的讀者,我的建議是不一樣的。

如果你是 AI 研究者

TileLang 是你現在就該開始學的工具。原因很簡單:用 80 行 Python 實現一個跟 CUTLASS 同等效能的 MLA kernel,這種開發效率的提升對研究的迭代速度有根本性的影響。DeepSeek 官方推薦研究者使用 TileLang 版本,這不是客套話。

上手路徑:

  1. pip install tilelang 安裝
  2. 跑一遍 TileLang Puzzles(10 個漸進式題目)
  3. 閱讀 FlashMLA 的 80 行實現
  4. 在你的研究中嘗試用 TileLang 做快速原型

如果你是 GPU 工程師

把 TileLang 加入你的工具箱,但不要丟掉 CUDA。TileLang 擅長的是快速原型和跨平台部署,CUDA 擅長的是極致效能調優和通用計算。一個合理的工作流程是:先用 TileLang 快速驗證想法和算法正確性,再視需要用 CUDA 做最後一哩路的效能榨取。

同時,密切關注 NVIDIA cuTile 的發展。tile 編程模型已經是確定的趨勢,無論你最終選擇哪個框架,理解 tile 級抽象的思維方式都是值得投資的。

如果你是技術決策者

關鍵觀察:GPU 編程正在從 thread-level 往 tile-level 遷移,這是一個跟 2012 年深度學習框架從手寫反向傳播到自動微分一樣量級的抽象層級提升。TileLang、Triton、cuTile 代表的是同一個方向的不同實現。

現在就要做的:

  • 評估你的團隊對 CUDA 的依賴程度
  • 在非關鍵路徑上試點 TileLang 或 Triton
  • 如果你需要跨 NVIDIA/AMD 部署,TileLang 的跨平台能力是一個實質優勢

Tile 編程時代已經到來

回到開頭的問題:TileLang 是真正的技術革命,還是曇花一現?

我的判斷是:TileLang 本身還太早期,無法預測它是否會成為最終的贏家。但 tile 編程模型作為一個範式,已經是確定的趨勢。

證據就在競爭者的反應裡。NVIDIA 推出 cuTile,OpenAI 的 Triton 也在開發 warp specialization,NVIDIA 還發表了 Tilus —— 每個人都在往同一個方向跑。當一個技術方向能讓所有主要玩家同時響應,你就知道這不是曇花一現。

TileLang 的獨特價值在於三件事:它是開源的、它是跨平台的、它已經被工業級項目(DeepSeek)驗證。在一個 NVIDIA 鎖定效應主導的世界裡,這三個特性加在一起,足以讓它成為值得長期關注的項目。

至於那 80 行 Python?那不是重點。重點是它背後代表的理念:GPU 編程不必這麼痛苦,開發者值得更好的抽象。


延伸閱讀


本文最初發布於 HackMD @BASHCAT。

Darknet YOLO 入坑手冊

電腦視覺物件偵測實戰課程

Oliver's Tech Workshop 2025


今日議程

  • 🎯 電腦視覺與物件偵測簡介
  • 🧠 機器學習與神經網路基礎
  • 🚀 YOLO 技術原理與最新發展
  • 💻 開發環境建置 (CUDA/Docker)
  • 🔧 實戰訓練流程 (PyTorch & Darknet)
  • 📱 部署到邊緣設備 (Ameba Pro2)
  • 🛠️ 故障排除與最佳化技巧

第一章:電腦視覺基礎


什麼是物件偵測?

YOLO架構圖

核心任務

  • 🔍 定位 (Localization) - 找出物體在哪裡
  • 🏷️ 分類 (Classification) - 判斷物體是什麼
  • 📦 邊界框 (Bounding Box) - 框出物體範圍
  • 📊 置信度 (Confidence) - 預測的可信度

YOLO 家族演進 (2025更新)

版本 特點 年份 mAP 速度
YOLOv1 開創性的實時檢測 2016 63.4% 45 FPS
YOLOv3 Darknet-53, 多尺度預測 2018 55.3% 20 FPS
YOLOv4 CSPDarknet53, Mish激活 2020 65.7% 65 FPS
YOLOv5 PyTorch實現, 輕量化 2020 68.9% 140 FPS
YOLOv7 可訓練的包袋式自注意力 2022 69.7% 161 FPS
YOLOv8 統一框架, 多任務 2023 68.2% 280 FPS
YOLOv9 PGI + GELAN架構 2024 70.4% 104 FPS
YOLOv10 實時檢測最佳化 2024 71.2% 300+ FPS

為什麼選擇 YOLO?

🚀 速度優勢

  • 實時處理: 30+ FPS
  • 端到端: 單一神經網路
  • 推理快速: 適合邊緣計算

🎯 精度提升

  • 多尺度特徵: 檢測大小物體
  • 豐富的增強: 資料增強策略
  • 損失函數: 針對檢測任務優化

🛠️ 易用性

  • 生態成熟: 大量預訓練模型
  • 文檔完善: 豐富的學習資源
  • 社群活躍: 持續更新維護

第二章:前置準備


學習路線圖

[mermaid 圖表 — 原始 HackMD 版本可正常渲染]

graph TD A[YOLO前置準備] --> B[硬體準備] A --> C[軟體環境] A --> D[知識基礎]

B --> E[GPU/NPU]
B --> F[CUDA/cuDNN]

C --> G[Linux/Docker]
C --> H[Python/PyTorch]

D --> I[神經網路]
D --> J[深度學習]</div>

硬體需求

基本配置

  • CPU: i5 以上
  • RAM: 16GB+
  • GPU: NVIDIA GTX 1060+

推薦配置

  • CPU: i7/i9 或 AMD Ryzen 7/9
  • RAM: 32GB+
  • GPU: RTX 3060+ (12GB VRAM)

GPU vs NPU

類型 優點 缺點
GPU 通用性強、生態完整 功耗高、成本高
NPU 專為AI優化、低功耗 生態受限、彈性較低

💡 沒有GPU也能學習!可使用:

  • Google Colab (免費GPU)
  • Kaggle Kernel
  • 雲端服務 (AWS, GCP)

第三章:機器學習基礎


什麼是機器學習?

定義

讓電腦從數據中自動學習規律,而無需明確編程

三大類型

  1. 監督式學習 - 有標註數據 (分類、回歸)
  2. 非監督式學習 - 無標註數據 (聚類、降維)
  3. 強化學習 - 透過獎懲機制學習 (遊戲、機器人)

物件偵測屬於監督式學習

  • 需要標註的邊界框
  • 需要物體類別標籤
  • 使用損失函數優化

神經網路 (NN)

神經網路架構

基本概念

  • 神經元: 最基本的計算單元
  • 權重: 控制輸入的重要性
  • 激活函數: 增加非線性能力
  • 反向傳播: 調整權重的方法

卷積神經網路 (CNN)

為什麼要用 CNN?

  • 🖼️ 空間不變性: 位置改變不影響識別
  • 🔍 局部感受野: 關注局部特徵
  • 📉 參數共享: 減少參數數量
  • 🎯 平移不變性: 特徵可以出現在任何位置

CNN 核心組件

# CNN 基本結構
Input Image (224x224x3)
    ↓
Conv2D (32 filters, 3x3) → ReLU → MaxPool (2x2)
    ↓
Conv2D (64 filters, 3x3) → ReLU → MaxPool (2x2)
    ↓
Conv2D (128 filters, 3x3) → ReLU → MaxPool (2x2)
    ↓
Flatten → Dense (512) → ReLU → Dropout
    ↓
Dense (num_classes) → Softmax

CNN 層級特徵學習

低層特徵 (前幾層)

  • 邊緣檢測: 水平線、垂直線、對角線
  • 紋理識別: 點狀、條紋、格子
  • 色彩梯度: 顏色變化、亮度變化

中層特徵 (中間層)

  • 形狀組合: 圓形、矩形、三角形
  • 物體局部: 眼睛、耳朵、輪廓
  • 複雜紋理: 毛髮、皮膚、表面材質

高層特徵 (最後幾層)

  • 完整物體: 人臉、汽車、動物
  • 場景理解: 室內、戶外、街道
  • 語義概念: 抽象的物體類別

從分類到檢測

圖像分類 vs 物件偵測

任務 輸入 輸出 難度
分類 整張圖片 類別標籤 較簡單
檢測 整張圖片 位置+類別 較複雜

檢測的挑戰

  • 🔍 多尺度: 大小物體都要檢測
  • 📍 精確定位: 邊界框要準確
  • 🚀 實時性: 速度要夠快
  • 🎯 多物體: 同時檢測多個物體

YOLO 的解決方案

  • 單一網路: 端到端訓練
  • 網格劃分: 全圖同時預測
  • 多尺度: 不同層級特徵融合

第四章:Dataset 資料集


為什麼需要 Dataset?

  • 🎯 訓練模型的"教材"
  • 📊 評估模型性能
  • 🔄 持續改進的基礎

資料集三要素

  1. 訓練集 (Train) - 70%
  2. 驗證集 (Valid) - 20%
  3. 測試集 (Test) - 10%

YOLOv7 資料集結構

dataset/
├── train/
│   ├── images/
│   └── labels/
├── valid/
│   ├── images/
│   └── labels/
├── test/
│   ├── images/
│   └── labels/
└── data.yaml

YOLOv7 資料集結構範例

資料集結構

分別有測試、訓練、驗證三個部分

資料集分割

data.yaml 訓練時會使用到


常用資料集資源

公開資料集平台

Roboflow 資料集下載範例

Roboflow 水族館資料集

使用 Yolo v7 PyTorch 格式下載

下載格式選擇

標註工具

  • LabelImg
  • CVAT
  • Roboflow Annotate

第五章:環境建置


GPU 設備檢查

1. 確認 GPU 支援

# 檢查 GPU 資訊
nvidia-smi

# 檢查 CUDA 版本
nvcc --version

# 檢查 GPU 計算能力
nvidia-smi --query-gpu=compute_cap --format=csv

2. 推薦 GPU 配置

GPU 型號 VRAM 適用場景
RTX 4060 16GB 學習/小規模訓練
RTX 4070 12GB 中等規模訓練
RTX 4080+ 16GB+ 大規模訓練

CUDA & cuDNN 完整安裝

Ubuntu 20.04/22.04 安裝步驟

# 1. 移除舊版本
sudo apt-get --purge remove "*nvidia*" "*cuda*" "*cudnn*"

# 2. 安裝 NVIDIA 驅動
sudo apt update
sudo apt install nvidia-driver-535

# 3. 重開機
sudo reboot

# 4. 安裝 CUDA 11.8
wget https://developer.download.nvidia.com/compute/cuda/11.8.0/local_installers/cuda_11.8.0_520.61.05_linux.run
sudo sh cuda_11.8.0_520.61.05_linux.run

# 5. 設定環境變數
echo 'export PATH=/usr/local/cuda-11.8/bin:$PATH' >> <sub>/.bashrc
echo 'export LD_LIBRARY_PATH=/usr/local/cuda-11.8/lib64:$LD_LIBRARY_PATH' >> </sub>/.bashrc
source <sub>/.bashrc

cuDNN 安裝

# 1. 下載 cuDNN 8.7.0 (需要 NVIDIA 帳號)
# 從 https://developer.nvidia.com/cudnn 下載 tar 檔案

# 2. 解壓縮並複製檔案
tar -xvf cudnn-linux-x86_64-8.7.0.84_cuda11-archive.tar.xz

# 3. 複製標頭檔
sudo cp cudnn-*/include/cudnn*.h /usr/local/cuda/include 

# 4. 複製函式庫
sudo cp -P cudnn-*/lib64/libcudnn* /usr/local/cuda/lib64 

# 5. 設定權限
sudo chmod a+r /usr/local/cuda/include/cudnn*.h 
sudo chmod a+r /usr/local/cuda/lib64/libcudnn*

# 6. 驗證安裝
nvidia-smi
nvcc --version

Python 環境設置

Conda 完整設置

# 1. 下載 Miniconda
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
bash Miniconda3-latest-Linux-x86_64.sh

# 2. 建立專用環境
conda create -n yolo python=3.8 -y
conda activate yolo

# 3. 安裝 PyTorch (CUDA 11.8)
pip install torch<mark>2.0.1 torchvision</mark>0.15.2 torchaudio==2.0.2 --index-url https://download.pytorch.org/whl/cu118

# 4. 驗證 PyTorch CUDA 支援
python -c "import torch; print(torch.cuda.is_available())"
python -c "import torch; print(torch.cuda.get_device_name(0))"

必要套件安裝

# 基本套件
pip install numpy opencv-python pillow matplotlib seaborn
pip install pandas scikit-learn tqdm

# 深度學習相關
pip install ultralytics  # YOLOv8
pip install roboflow     # 資料集管理
pip install wandb        # 訓練監控

# 圖像處理
pip install albumentations  # 資料增強
pip install imgaug         # 圖像增強

# 視覺化
pip install tensorboard
pip install plotly

# 驗證安裝
python -c "import cv2; print(cv2.__version__)"
python -c "import ultralytics; print('YOLOv8 安裝成功')"

Docker 環境 (推薦)

Docker 工作流程

優點

  • ✅ 環境隔離: 不會影響主系統
  • ✅ 可重現性: 團隊環境一致
  • ✅ 快速部署: 一鍵啟動完整環境
  • ✅ 版本控制: 不同專案用不同版本

Docker 完整設置

1. 安裝 Docker

# Ubuntu 安裝 Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# 安裝 NVIDIA Docker
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list

sudo apt-get update && sudo apt-get install -y nvidia-docker2
sudo systemctl restart docker

2. 使用預建映像

# 方式1: 使用官方映像
docker pull nvcr.io/nvidia/pytorch:22.12-py3

# 方式2: 使用自定義映像
docker pull oliver0804/darknet-opencv:latest

# 運行容器
docker run --gpus all -it --rm \
  -v $PWD:/workspace \
  -p 8888:8888 \
  nvcr.io/nvidia/pytorch:22.12-py3

自建 Docker 映像

Dockerfile 範例

FROM nvidia/cuda:11.8-devel-ubuntu20.04

# 安裝基本套件
RUN apt-get update && apt-get install -y \
    python3 python3-pip git wget \
    libopencv-dev python3-opencv \
    && rm -rf /var/lib/apt/lists/*

# 安裝 Python 套件
RUN pip3 install torch torchvision torchaudio \
    opencv-python ultralytics

# 設定工作目錄
WORKDIR /workspace

# 複製專案檔案
COPY . .

# 暴露端口
EXPOSE 8888

# 啟動命令
CMD ["bash"]

建置與使用

# 建置映像
docker build -t my-yolo-env .

# 運行容器
docker run --gpus all -it --rm \
  -v $PWD:/workspace \
  my-yolo-env

第六章:YOLO 訓練實戰


訓練前準備檢查清單

📋 硬體需求確認

  • ✅ GPU 記憶體 > 8GB
  • ✅ 系統記憶體 > 16GB
  • ✅ 儲存空間 > 50GB
  • ✅ CUDA 版本相容

📋 軟體環境確認

# 檢查清單
python --version         # Python 3.8+
pip list | grep torch    # PyTorch 1.12+
nvidia-smi              # GPU 狀態
nvcc --version          # CUDA 版本

YOLOv8 完整訓練流程

1. 環境設置

# 安裝 Ultralytics YOLOv8
pip install ultralytics

# 驗證安裝
yolo version

2. 準備資料集

# 資料集結構
dataset/
├── images/
│   ├── train/
│   ├── val/
│   └── test/
├── labels/
│   ├── train/
│   ├── val/
│   └── test/
└── data.yaml

資料集配置檔案

data.yaml 範例

# 資料集路徑
path: ./dataset
train: images/train
val: images/val
test: images/test

# 類別數量
nc: 3

# 類別名稱
names:
  0: person
  1: bicycle
  2: car

標註格式 (YOLO)

# 每行一個物體: class_id x_center y_center width height
# 座標為相對值 (0-1)
0 0.5 0.5 0.3 0.4
1 0.2 0.3 0.1 0.2

訓練腳本詳解

資料集配置範例

data.yaml 配置

此檔案經過修改,將 Path 改為絕對路徑

模型配置選擇

YOLOv7 配置檔案

路徑中包含 v7-tiny 版本選項

基本訓練

from ultralytics import YOLO

# 載入模型
model = YOLO('yolov8n.pt')  # nano 版本

# 開始訓練
results = model.train(
    data='data.yaml',
    epochs=100,
    imgsz=640,
    batch=16,
    name='my_experiment'
)

進階訓練設定

# 進階參數設定
results = model.train(
    data='data.yaml',
    epochs=300,
    imgsz=640,
    batch=32,
    lr0=0.01,           # 初始學習率
    lrf=0.01,           # 最終學習率
    momentum=0.937,     # 動量
    weight_decay=0.0005,# 權重衰減
    warmup_epochs=3,    # 預熱期
    warmup_momentum=0.8,# 預熱動量
    box=7.5,            # 邊界框損失權重
    cls=0.5,            # 分類損失權重
    dfl=1.5,            # 分布焦點損失權重
    patience=50,        # 早停耐心值
    save_period=10,     # 保存週期
    name='advanced_training'
)

訓練監控與視覺化

1. TensorBoard 監控

# 啟動 TensorBoard
import tensorboard
%load_ext tensorboard
%tensorboard --logdir runs/train

2. Weights & Biases 整合

# 安裝 wandb
pip install wandb

# 登入 wandb
wandb login

# 訓練時自動記錄
results = model.train(
    data='data.yaml',
    epochs=100,
    project='yolo-training',  # W&B 專案名稱
    name='experiment-1'
)

訓練過程監控

即時監控腳本

import psutil
import GPUtil
import time

def monitor_training():
    while True:
        # CPU 使用率
        cpu_percent = psutil.cpu_percent()
        
        # 記憶體使用率
        memory = psutil.virtual_memory()
        
        # GPU 使用率
        gpus = GPUtil.getGPUs()
        
        print(f"CPU: {cpu_percent}%")
        print(f"RAM: {memory.percent}%")
        
        for gpu in gpus:
            print(f"GPU {gpu.id}: {gpu.load*100:.1f}%")
            print(f"VRAM: {gpu.memoryUsed}MB/{gpu.memoryTotal}MB")
        
        time.sleep(5)

# 背景執行監控
import threading
monitor_thread = threading.Thread(target=monitor_training, daemon=True)
monitor_thread.start()

訓練過程展示

訓練開始

訓練開始截圖

GPU 資源監控

nvtop GPU 監控

可以使用 nvtop 進行 GPU 使用資源檢視


超參數調整策略

學習率調整

# 學習率調度策略
schedulers = {
    'cosine': 'cos',      # 餘弦退火
    'linear': 'linear',   # 線性衰減
    'constant': 'constant' # 常數
}

# 自動學習率尋找
model.tune(
    data='data.yaml',
    epochs=30,
    iterations=300,
    optimizer='AdamW',
    plots=True,
    save=True
)

批次大小優化

# 自動批次大小調整
def find_optimal_batch_size(model, data_yaml):
    batch_sizes = [8, 16, 32, 64, 128]
    best_batch = 8
    
    for batch in batch_sizes:
        try:
            results = model.train(
                data=data_yaml,
                epochs=5,
                batch=batch,
                verbose=False
            )
            best_batch = batch
            print(f"Batch {batch}: 成功")
        except RuntimeError as e:
            if "out of memory" in str(e):
                print(f"Batch {batch}: 記憶體不足")
                break
    
    return best_batch

模型驗證與測試

驗證腳本

# 驗證模型性能
metrics = model.val(
    data='data.yaml',
    imgsz=640,
    batch=32,
    conf=0.001,
    iou=0.6,
    save_json=True,
    save_hybrid=True
)

# 輸出指標
print(f"mAP50: {metrics.box.map50:.3f}")
print(f"mAP50-95: {metrics.box.map:.3f}")
print(f"Precision: {metrics.box.mp:.3f}")
print(f"Recall: {metrics.box.mr:.3f}")

測試腳本

# 單張圖片測試
results = model.predict(
    source='test.jpg',
    conf=0.25,
    save=True,
    show_labels=True,
    show_conf=True
)

# 批次圖片測試
results = model.predict(
    source='test_images/',
    conf=0.25,
    save=True,
    save_txt=True,  # 保存標註
    save_crop=True  # 保存裁切圖片
)

# 即時攝影機測試
results = model.predict(
    source=0,       # 攝影機ID
    conf=0.25,
    show=True,
    save=True
)

模型匯出與部署

多格式匯出

# 匯出為不同格式
model.export(
    format='onnx',      # ONNX 格式
    imgsz=640,
    dynamic=True,       # 動態輸入尺寸
    half=True,          # FP16 精度
    int8=True,          # INT8 量化
    optimize=True       # 優化
)

# 其他格式
formats = [
    'torchscript',  # PyTorch Script
    'tflite',       # TensorFlow Lite
    'edgetpu',      # Edge TPU
    'tfjs',         # TensorFlow.js
    'coreml'        # Core ML
]

for fmt in formats:
    model.export(format=fmt)

性能基準測試

# 推理速度測試
import time
import numpy as np

def benchmark_model(model, input_size=(640, 640), iterations=100):
    model.eval()
    dummy_input = np.random.randn(1, 3, *input_size).astype(np.float32)
    
    # 預熱
    for _ in range(10):
        model.predict(dummy_input, verbose=False)
    
    # 測試
    times = []
    for _ in range(iterations):
        start = time.time()
        model.predict(dummy_input, verbose=False)
        end = time.time()
        times.append(end - start)
    
    avg_time = np.mean(times)
    fps = 1.0 / avg_time
    
    print(f"平均推理時間: {avg_time*1000:.2f}ms")
    print(f"FPS: {fps:.2f}")
    
    return avg_time, fps

訓練成果展示

魚類偵測範例

偵測結果

魚類偵測結果

好多好多魚魚 <>< <3

最終魚類偵測

訓練指標

測試集結果

測試集結果

訓練集結果

訓練集結果

混淆矩陣

混淆矩陣

攝影機即時檢測

攝影機檢測


常見問題解決

記憶體不足

# 解決方案
strategies = {
    '降低批次大小': 'batch=8',
    '使用混合精度': 'amp=True',
    '減少輸入尺寸': 'imgsz=320',
    '梯度累積': 'accumulate=2'
}

# 梯度累積範例
model.train(
    data='data.yaml',
    epochs=100,
    batch=8,
    accumulate=4,  # 等效批次大小 = 8*4 = 32
    amp=True       # 自動混合精度
)

訓練不收斂

# 診斷與解決
diagnostic_steps = [
    "檢查學習率是否過高",
    "確認資料集標註正確性",
    "檢查類別平衡性",
    "調整損失函數權重",
    "增加資料增強"
]

# 學習率調整
model.train(
    data='data.yaml',
    lr0=0.001,      # 降低初始學習率
    warmup_epochs=5, # 增加預熱期
    patience=100     # 增加早停耐心
)

第七章:Darknet 框架


Darknet 簡介

特點

  • ✨ C/CUDA 原生實現 - 極致性能
  • 🚀 輕量級框架 - 無複雜依賴
  • 📦 易於修改 - 源碼清晰
  • 🔧 原生支援 - YOLO 官方框架

作者

Joseph Redmon (YOLO v1-v3 原作者)
Alexey Bochkovskiy (YOLOv4 維護者)

與 PyTorch 比較

特性 Darknet PyTorch
速度 極快 快
易用性 中等 高
生態系統 有限 豐富
自定義 需要 C 代碼 Python 即可

Darknet 完整安裝

1. 系統依賴安裝

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install -y build-essential cmake git

# 編譯工具
sudo apt-get install -y gcc g++ make

# 可選:OpenCV 支援
sudo apt-get install -y libopencv-dev

# 可選:OpenMP 支援
sudo apt-get install -y libomp-dev

2. 克隆與編譯

# 克隆改良版 Darknet (推薦)
git clone https://github.com/AlexeyAB/darknet.git
cd darknet

# 編輯 Makefile
nano Makefile

Makefile 配置

# GPU 支援
GPU=1
CUDNN=1
CUDNN_HALF=1

# OpenCV 支援
OPENCV=1

# OpenMP 支援
OPENMP=1

# 偵錯模式
DEBUG=0

# 其他選項
LIBSO=1  # 建立 .so 檔案
ZED_CAMERA=0  # ZED 攝影機支援

編譯指令

# 清理舊檔案
make clean

# 編譯 (使用多核心)
make -j$(nproc)

# 驗證編譯結果
./darknet version

OpenCV 安裝過程截圖

OpenCV 3.4.19 版本確認

OpenCV 版本

編譯過程

編譯進度1 編譯進度2 編譯完成


基本測試

1. 下載預訓練模型

# YOLOv4 權重
wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.weights

# YOLOv4-tiny 權重
wget https://github.com/AlexeyAB/darknet/releases/download/yolov4/yolov4-tiny.weights

# YOLOv7-tiny 權重
wget https://github.com/AlexeyAB/darknet/releases/download/yolov4/yolov7-tiny.weights

2. 測試圖片檢測

# YOLOv4 檢測
./darknet detect cfg/yolov4.cfg yolov4.weights data/dog.jpg

# YOLOv4-tiny 檢測
./darknet detect cfg/yolov4-tiny.cfg yolov4-tiny.weights data/dog.jpg

# 自訂閾值
./darknet detect cfg/yolov4.cfg yolov4.weights data/dog.jpg -thresh 0.5

測試結果展示

YOLOv3 檢測結果

YOLOv3 狗狗檢測

YOLOv7-tiny 檢測結果

YOLOv7-tiny 結果


影片與攝影機檢測

影片檢測

# 檢測影片
./darknet detector demo cfg/coco.data cfg/yolov4.cfg yolov4.weights test.mp4

# 儲存結果
./darknet detector demo cfg/coco.data cfg/yolov4.cfg yolov4.weights test.mp4 -out_filename result.avi

攝影機檢測

# 使用 webcam
./darknet detector demo cfg/coco.data cfg/yolov4.cfg yolov4.weights -c 0

# 使用外接攝影機
./darknet detector demo cfg/coco.data cfg/yolov4.cfg yolov4.weights -c 1

# 設定參數
./darknet detector demo cfg/coco.data cfg/yolov4.cfg yolov4.weights -c 0 -thresh 0.25 -here

自定義訓練準備

1. 建立專案資料夾

mkdir my_yolo_project
cd my_yolo_project

# 建立目錄結構
mkdir -p {data,cfg,weights,results}
mkdir -p data/{images,labels}
mkdir -p data/images/{train,valid,test}
mkdir -p data/labels/{train,valid,test}

2. 準備資料集

# 資料集結構
my_yolo_project/
├── data/
│   ├── images/
│   │   ├── train/
│   │   ├── valid/
│   │   └── test/
│   ├── labels/
│   │   ├── train/
│   │   ├── valid/
│   │   └── test/
│   ├── train.txt
│   ├── valid.txt
│   └── test.txt
├── cfg/
│   ├── my_dataset.data
│   ├── my_yolov4.cfg
│   └── my_classes.names
└── weights/

配置檔案設定

1. 建立 .data 檔案

# my_dataset.data
classes = 3
train = data/train.txt
valid = data/valid.txt
names = cfg/my_classes.names
backup = weights/

2. 建立 .names 檔案

# my_classes.names
person
car
bicycle

3. 建立圖片列表

# 產生訓練列表
find $(pwd)/data/images/train -name "*.jpg" > data/train.txt

# 產生驗證列表
find $(pwd)/data/images/valid -name "*.jpg" > data/valid.txt

# 檢查列表
head -5 data/train.txt

模型配置調整

1. 複製基礎配置

# 複製 YOLOv4-tiny 配置
cp ../darknet/cfg/yolov4-tiny.cfg cfg/my_yolov4.cfg

2. 修改配置檔案

# 編輯配置
nano cfg/my_yolov4.cfg

# 關鍵修改點:
# 1. 修改 classes 數量
# 2. 修改 filters 數量
# 3. 調整學習率
# 4. 設定批次大小

重要參數說明

[net]
batch=64          # 批次大小
subdivisions=8    # 子批次
width=416         # 輸入寬度
height=416        # 輸入高度
channels=3        # 通道數
momentum=0.9      # 動量
decay=0.0005      # 權重衰減
angle=0           # 隨機旋轉角度
saturation=1.5    # 飽和度變化
exposure=1.5      # 曝光變化
hue=.1            # 色調變化

learning_rate=0.001  # 學習率
max_batches=6000     # 最大批次 (classes * 2000)
policy=steps         # 學習率策略
steps=4800,5400      # 降低學習率的步驟
scales=.1,.1         # 學習率縮放

預訓練權重準備

1. 下載預訓練權重

# YOLOv4-tiny 預訓練權重
wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v4_pre/yolov4-tiny.conv.29

# YOLOv4 預訓練權重
wget https://github.com/AlexeyAB/darknet/releases/download/darknet_yolo_v3_optimal/yolov4.conv.137

2. 驗證權重檔案

# 檢查檔案大小
ls -lh weights/

# 測試權重載入
./darknet detector test cfg/my_dataset.data cfg/my_yolov4.cfg weights/yolov4-tiny.conv.29

開始訓練

自定義資料集範例

手語識別資料集

手語資料集

自定義設定資料夾

自定義設定

基本訓練指令

# 單 GPU 訓練
./darknet detector train cfg/my_dataset.data cfg/my_yolov4.cfg weights/yolov4-tiny.conv.29

# 多 GPU 訓練
./darknet detector train cfg/my_dataset.data cfg/my_yolov4.cfg weights/yolov4-tiny.conv.29 -gpus 0,1

# 從檢查點繼續訓練
./darknet detector train cfg/my_dataset.data cfg/my_yolov4.cfg weights/my_yolov4_last.weights

# 計算 mAP
./darknet detector train cfg/my_dataset.data cfg/my_yolov4.cfg weights/yolov4-tiny.conv.29 -map

訓練過程監控

訓練進行中


訓練監控

1. 查看訓練日誌

# 即時查看訓練進度
tail -f training.log

# 提取損失值
grep "avg" training.log | tail -10

2. 損失函數分析

# 訓練輸出範例
Region xx: cfg: (anchors) ...
1000: 2.950644, 2.950644 avg, 0.000010 rate, 3.456 seconds, 64000 images

參數說明

  • 1000: 當前迭代次數
  • 2.950644: 當前損失值
  • 2.950644 avg: 平均損失值
  • 0.000010 rate: 當前學習率
  • 3.456 seconds: 此批次訓練時間
  • 64000 images: 已處理圖片數

訓練結果評估

1. 計算 mAP

# 計算驗證集 mAP
./darknet detector map cfg/my_dataset.data cfg/my_yolov4.cfg weights/my_yolov4_best.weights

2. 測試個別圖片

# 測試單張圖片
./darknet detector test cfg/my_dataset.data cfg/my_yolov4.cfg weights/my_yolov4_best.weights data/test.jpg

# 批次測試
./darknet detector test cfg/my_dataset.data cfg/my_yolov4.cfg weights/my_yolov4_best.weights < data/test.txt

3. 調整檢測閾值

# 不同閾值測試
for thresh in 0.1 0.25 0.5 0.75; do
    echo "Testing with threshold: $thresh"
    ./darknet detector test cfg/my_dataset.data cfg/my_yolov4.cfg weights/my_yolov4_best.weights data/test.jpg -thresh $thresh
done

高級功能

1. 資料增強

# 在 [net] 部分加入
mosaic=1          # 馬賽克增強
cutmix=1          # CutMix 增強
mixup=1           # MixUp 增強

2. 多尺度訓練

# 隨機尺度訓練
random=1

# 多尺度設定
width=416
height=416

3. 學習率調度

# 學習率策略
policy=steps
steps=4800,5400
scales=.1,.1

# 或使用多項式衰減
policy=poly
power=4

疑難排解

常見錯誤解決

# 1. CUDA 記憶體不足
# 解決:降低 batch size 或 subdivisions

# 2. 找不到圖片
# 解決:檢查 .txt 檔案中的路徑

# 3. 標註格式錯誤
# 解決:檢查 YOLO 格式是否正確

# 4. 類別數量不匹配
# 解決:確認 classes 和 filters 設定正確

優化建議

  1. 記憶體優化: 調整 subdivisions
  2. 速度優化: 使用 -dont_show 參數
  3. 精度優化: 增加 max_batches
  4. 穩定訓練: 使用 -map 監控 mAP

Docker 部署 (完整版)

1. 使用現成映像

Docker 工作流程示意

Docker 工作流程

# 拉取映像
docker pull oliver0804/darknet-opencv:latest

# 運行容器
docker run --gpus all -it --rm \
    -v $(pwd):/workspace \
    -v $(pwd)/data:/darknet/data \
    -v $(pwd)/cfg:/darknet/cfg \
    -v $(pwd)/weights:/darknet/weights \
    oliver0804/darknet-opencv:latest

Docker 執行結果

Docker 執行結果

最終檢測結果

Docker 檢測結果

2. 在容器中訓練

# 進入容器
docker exec -it <container_id> bash

# 開始訓練
./darknet detector train cfg/my_dataset.data cfg/my_yolov4.cfg weights/yolov4-tiny.conv.29 -dont_show -map

3. 自動化腳本

#!/bin/bash
# auto_train.sh

# 設定參數
DATA_FILE="cfg/my_dataset.data"
CFG_FILE="cfg/my_yolov4.cfg"
WEIGHTS_FILE="weights/yolov4-tiny.conv.29"

# 執行訓練
docker run --gpus all -it --rm \
    -v $(pwd):/workspace \
    oliver0804/darknet-opencv:latest \
    ./darknet detector train $DATA_FILE $CFG_FILE $WEIGHTS_FILE -dont_show -map

第八章:邊緣設備部署


Ameba Pro2 簡介

硬體規格

  • 處理器: ARM Cortex-M55 @ 500MHz
  • AI 加速器: 0.4 TOPS NPU
  • 記憶體: 768KB SRAM + 16MB PSRAM
  • 攝像頭: 2MP 感光元件
  • 連接: Wi-Fi 6 + BLE 5.0
  • 尺寸: 35mm x 28mm

為什麼選擇邊緣 AI?

  • 🔒 隱私保護: 資料不上雲
  • ⚡ 低延遲: 即時推理
  • 💰 成本效益: 無雲端費用
  • 🔋 低功耗: 適合 IoT 應用

重要注意事項

支援時程

注意: ameba pro2 對 yolo 的支援時間會比較晚

當前限制

  • 模型轉換需要線上工具
  • 暫無離線版本工具
  • 如果在意模型保密需要考慮

適用場景

  • 🏠 智慧家居監控
  • 🏭 工業品質檢測
  • 🚗 車內駕駛行為分析
  • 📱 IoT 邊緣計算

模型轉換詳解

1. 準備轉換檔案

檔案要求

# 打包內容
yolo_model.zip
├── yolov4-tiny.weights    # 訓練好的權重檔
└── yolov4-tiny.cfg        # 對應的配置檔

# 重要限制
❌ 不能包含中文字符
❌ cfg 檔案註解也不能有中文
❌ 檔案路徑不能有中文

配置檔案清理

# 移除中文註解
sed -i '/[\u4e00-\u9fff]/d' yolov4-tiny.cfg

# 檢查檔案編碼
file yolov4-tiny.cfg

# 確保 UTF-8 編碼
iconv -f GB2312 -t UTF-8 yolov4-tiny.cfg > yolov4-tiny_clean.cfg

線上轉換流程

1. 上傳模型

模型轉換上傳

網址: https://www.amebaiot.com/en/amebapro2-ai-convert-model/

2. 轉換過程

  • ⏱️ 等待時間: 10-20 分鐘
  • 📧 結果通知: 轉換完成會發送郵件
  • 📦 輸出格式: .nb 檔案 (Neural Network Binary)

3. 轉換參數

參數 說明 建議值
Input Size 輸入圖片尺寸 416x416
Quantization 量化位元數 INT8
Optimization 優化等級 High

Arduino IDE 環境設置

1. 安裝 Arduino IDE

# 下載 Arduino IDE 2.0+
wget https://downloads.arduino.cc/arduino-ide/arduino-ide_2.2.1_Linux_64bit.zip

# 或使用套件管理器
sudo apt install arduino

2. 添加開發板支援

開發板管理員 URL

https://github.com/ambiot/ambpro2_arduino/raw/main/Arduino_package/package_realtek.com_amebapro2_index.json

開發板管理員設定

安裝步驟

  1. 開啟 Arduino IDE
  2. 檔案 → 偏好設定 → 額外的開發板管理員網址
  3. 貼上上述 URL
  4. 工具 → 開發板 → 開發板管理員
  5. 搜尋 "Realtek Ameba"
  6. 安裝 "Realtek Ameba Pro2 Boards"

選擇開發板

開發板設定

選擇 Ameba Pro2

設定路徑: 工具 → 開發板 → Realtek Ameba Pro2 → "Ameba Pro2"

編譯選項

Board: "Ameba Pro2"
CPU Speed: "500MHz"
Optimization: "Smallest (-Os)"
Upload Speed: "115200"

神經網路範例程式

1. 開啟範例

NN 範例

路徑: 檔案 → 範例 → AmebaNN → YOLO

2. 基本程式結構

#include "NeuralNetwork.h"
#include "VideoStream.h"
#include "RTSP.h"

// 物件類別定義
#define YOLO_CLASSES 26  // 自定義類別數

// 類別名稱陣列
String objectList[YOLO_CLASSES] = {
    "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
    "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", 
    "U", "V", "W", "X", "Y", "Z"
};

void setup() {
    Serial.begin(115200);
    
    // 初始化攝像頭
    Camera.configVideoChannel(VIDEO_CHN, 416, 416, 30, VIDEO_H264_JPEG);
    Camera.videoInit();
    
    // 初始化神經網路
    NNObjectDetection.configVideo(VIDEO_CHN);
    NNObjectDetection.modelSelect(OBJECT_DETECTION, YOLO_CLASSES, DEFAULT_YOLO, NA_MODEL);
    NNObjectDetection.begin();
    
    // 初始化 RTSP
    RTSP.configVideo(VIDEO_CHN);
    RTSP.begin();
}

void loop() {
    // 執行物件偵測
    if (NNObjectDetection.available()) {
        ObjectDetectionResult result = NNObjectDetection.getResult();
        
        // 處理偵測結果
        for (int i = 0; i < result.count(); i++) {
            int obj_type = result.type(i);
            int score = result.score(i);
            
            if (score > 50) {  // 信心度閾值
                Serial.printf("偵測到: %s (信心度: %d%%)\n", 
                             objectList[obj_type].c_str(), score);
            }
        }
    }
    delay(100);
}

模型替換詳解

1. 找到模型檔案位置

模型檔案路徑

典型路徑:

<sub>/Arduino/libraries/AmebaNN/src/
├── models/
│   ├── yolov4_tiny.nb      # 原始模型
│   └── yolov4_tiny_new.nb  # 你的新模型

2. 備份原始模型

# 進入模型目錄
cd </sub>/Arduino/libraries/AmebaNN/src/models/

# 備份原始模型
cp yolov4_tiny.nb yolov4_tiny_original.nb

# 替換新模型
cp /path/to/your/model.nb yolov4_tiny.nb

3. 修改類別設定

更新類別數量

// 原始 COCO dataset (80 類)
#define YOLO_CLASSES 80

// 改為你的類別數 (例如: 26 類手語)
#define YOLO_CLASSES 26

更新類別名稱

// 原始 COCO 類別
String objectList[YOLO_CLASSES] = {
    "person", "bicycle", "car", ... // 80 個類別
};

// 改為手語字母 (26 類)
String objectList[YOLO_CLASSES] = {
    "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
    "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", 
    "U", "V", "W", "X", "Y", "Z"
};

程式碼修改詳解

1. 修改 OSD 標籤顯示

原始標籤清單

OSD 標籤修改

修改後的手語標籤

手語標籤清單

原始代碼:

// 舊版本 (可能已修正)
printf("%s", item.name());

修正後:

// 新版本
printf("%s", objectList[obj_type].c_str());

2. 物件類型處理

物件類型修正

修改位置: 約第 114 行

// 修改前
item.name()

// 修改後  
objectList[obj_type].objectName

測試與除錯

1. 序列埠監控

void printDetectionInfo(ObjectDetectionResult result) {
    Serial.printf("偵測到 %d 個物件\n", result.count());
    
    for (int i = 0; i < result.count(); i++) {
        int obj_type = result.type(i);
        int score = result.score(i);
        int x = result.xMin(i);
        int y = result.yMin(i);
        int w = result.width(i);
        int h = result.height(i);
        
        Serial.printf("物件 %d: %s\n", i+1, objectList[obj_type].c_str());
        Serial.printf("  信心度: %d%%\n", score);
        Serial.printf("  位置: (%d, %d) 大小: %dx%d\n", x, y, w, h);
    }
}

2. 性能監控

unsigned long prev_time = 0;
int frame_count = 0;

void loop() {
    if (NNObjectDetection.available()) {
        frame_count++;
        unsigned long curr_time = millis();
        
        if (curr_time - prev_time >= 1000) {  // 每秒統計
            Serial.printf("FPS: %d\n", frame_count);
            frame_count = 0;
            prev_time = curr_time;
        }
        
        // 處理偵測結果...
    }
}

實際展示效果

1. 手語識別展示

YouTube 影片: Ameba pro2 yolo展示

功能展示

  • ✅ 即時手語字母識別
  • ✅ 邊界框繪製
  • ✅ 信心度顯示
  • ✅ RTSP 串流輸出

2. 性能表現

指標 數值
推理速度 10 FPS
延遲 <100ms
功耗 <500mW
精確度 85%+

模型快速替換工具

1. 問題描述

  • 🔍 路徑深層: Arduino 目錄結構複雜
  • 🔄 頻繁更換: 需要測試不同模型
  • 💾 備份需求: 保存之前的模型版本

2. 自動化腳本

YouTube 教學: Ameba pro2 快速替換模型腳本

Windows 批次檔 (model_swap.bat)

@echo off
setlocal enabledelayedexpansion

echo Ameba Pro2 模型快速替換工具
echo ================================

set ARDUINO_PATH=%USERPROFILE%\Documents\Arduino
set MODEL_PATH=%ARDUINO_PATH%\libraries\AmebaNN\src\models
set BACKUP_PATH=%MODEL_PATH%\backup

REM 建立備份目錄
if not exist "%BACKUP_PATH%" mkdir "%BACKUP_PATH%"

REM 顯示當前模型
echo 當前模型檔案:
dir "%MODEL_PATH%\*.nb" /b

echo.
echo 選擇操作:
echo 1. 替換模型
echo 2. 還原備份
echo 3. 建立備份
set /p choice="請輸入選項 (1-3): "

if "%choice%"=="1" goto REPLACE_MODEL
if "%choice%"=="2" goto RESTORE_BACKUP  
if "%choice%"=="3" goto CREATE_BACKUP
goto END

:REPLACE_MODEL
set /p new_model="請輸入新模型檔案路徑: "
if not exist "%new_model%" (
    echo 錯誤: 找不到指定的模型檔案
    goto END
)

REM 備份當前模型
copy "%MODEL_PATH%\yolov4_tiny.nb" "%BACKUP_PATH%\yolov4_tiny_%date:</sub>0,4%%date:<sub>5,2%%date:</sub>8,2%.nb"

REM 替換模型
copy "%new_model%" "%MODEL_PATH%\yolov4_tiny.nb"
echo 模型替換完成!
goto END

:RESTORE_BACKUP
echo 可用的備份檔案:
dir "%BACKUP_PATH%\*.nb" /b
set /p backup_file="請輸入要還原的備份檔案名: "
copy "%BACKUP_PATH%\%backup_file%" "%MODEL_PATH%\yolov4_tiny.nb"
echo 模型還原完成!
goto END

:CREATE_BACKUP
copy "%MODEL_PATH%\yolov4_tiny.nb" "%BACKUP_PATH%\yolov4_tiny_%date:<sub>0,4%%date:</sub>5,2%%date:~8,2%.nb"
echo 備份建立完成!
goto END

:END
pause

3. Linux/Mac 腳本 (model_swap.sh)

#!/bin/bash

ARDUINO_PATH="$HOME/Arduino"
MODEL_PATH="$ARDUINO_PATH/libraries/AmebaNN/src/models"
BACKUP_PATH="$MODEL_PATH/backup"

# 建立備份目錄
mkdir -p "$BACKUP_PATH"

echo "Ameba Pro2 模型快速替換工具"
echo "================================"

echo "當前模型檔案:"
ls -la "$MODEL_PATH"/*.nb 2>/dev/null || echo "找不到模型檔案"

echo ""
echo "選擇操作:"
echo "1. 替換模型"
echo "2. 還原備份"
echo "3. 建立備份"
read -p "請輸入選項 (1-3): " choice

case $choice in
    1)
        read -p "請輸入新模型檔案路徑: " new_model
        if [ ! -f "$new_model" ]; then
            echo "錯誤: 找不到指定的模型檔案"
            exit 1
        fi
        
        # 備份當前模型
        cp "$MODEL_PATH/yolov4_tiny.nb" "$BACKUP_PATH/yolov4_tiny_$(date +%Y%m%d_%H%M%S).nb"
        
        # 替換模型
        cp "$new_model" "$MODEL_PATH/yolov4_tiny.nb"
        echo "模型替換完成!"
        ;;
    2)
        echo "可用的備份檔案:"
        ls -1 "$BACKUP_PATH"/*.nb 2>/dev/null || echo "沒有備份檔案"
        read -p "請輸入要還原的備份檔案名: " backup_file
        cp "$BACKUP_PATH/$backup_file" "$MODEL_PATH/yolov4_tiny.nb"
        echo "模型還原完成!"
        ;;
    3)
        cp "$MODEL_PATH/yolov4_tiny.nb" "$BACKUP_PATH/yolov4_tiny_$(date +%Y%m%d_%H%M%S).nb"
        echo "備份建立完成!"
        ;;
    *)
        echo "無效的選項"
        ;;
esac

疑難排解

常見問題

  1. 模型轉換失敗

    • 檢查檔案是否包含中文
    • 確認 cfg 和 weights 對應
    • 重新清理配置檔案
  2. 編譯錯誤

    • 確認開發板選擇正確
    • 檢查類別數量設定
    • 更新 Arduino 核心
  3. 推理速度慢

    • 降低輸入解析度
    • 使用 INT8 量化
    • 優化模型架構

效能優化建議

  • 🎯 模型選擇: 使用 YOLOv4-tiny 而非完整版
  • 📏 輸入尺寸: 建議 320x320 或 416x416
  • ⚡ 量化: 使用 INT8 量化減少記憶體用量
  • 🔄 批次處理: 避免頻繁的模型切換

第九章:實用技巧


OpenCV 應用

電腦視覺基礎操作

OpenCV 基本操作

OpenCV 可以處理顏色、物件邊緣、光流等,是代碼版本的 Photoshop

影像前處理

import cv2

# 讀取圖片
img = cv2.imread('image.jpg')

# 轉換顏色空間
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# 邊緣檢測
edges = cv2.Canny(gray, 50, 150)

# 影像增強
enhanced = cv2.equalizeHist(gray)

進階應用範例

人臉識別應用

OpenCV 人臉識別

透過 OpenCV contrib 實現人臉偵測

OCR 文字識別

OpenCV OCR

使用 Tesseract 進行光學字符識別


資料增強技巧

常用方法

  1. 旋轉 - 增加角度變化
  2. 翻轉 - 水平/垂直鏡像
  3. 縮放 - 尺度不變性
  4. 色彩調整 - 光照變化
  5. 加噪 - 提升魯棒性
# Albumentations 範例
transform = A.Compose([
    A.RandomRotate90(),
    A.Flip(),
    A.RandomBrightnessContrast(p=0.2),
])

第十章:常見問題


GPU 記憶體不足

解決方案

  1. 降低 batch size
  2. 使用混合精度訓練
  3. 減少輸入圖片尺寸
  4. 使用梯度累積
# 梯度累積範例
accumulation_steps = 4
for i, (imgs, targets) in enumerate(dataloader):
    loss = model(imgs, targets)
    loss = loss / accumulation_steps
    loss.backward()
    
    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

訓練不收斂

檢查清單

  • ✅ 學習率是否合適
  • ✅ 資料集是否平衡
  • ✅ 標註是否正確
  • ✅ 增強是否過度

調試技巧

# 學習率調度
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
    optimizer, T_max=epochs
)

總結與展望


學習要點回顧

技術堆疊

  • ✅ 深度學習基礎
  • ✅ YOLO 架構原理
  • ✅ 訓練流程實作
  • ✅ 模型優化技巧
  • ✅ 邊緣設備部署

實戰經驗

  • 💡 資料集準備的重要性
  • 🔧 超參數調整的藝術
  • 📊 性能指標的理解
  • 🚀 部署優化的考量

未來發展方向

技術趨勢

  1. Vision Transformer - 新架構崛起
  2. 自監督學習 - 減少標註需求
  3. 邊緣 AI - 更強大的端側推理
  4. 多模態融合 - 結合語言理解

應用場景

  • 🏭 智慧製造
  • 🚗 自動駕駛
  • 🏥 醫療影像
  • 🛡️ 安防監控

參考資源

官方資源

社群資源

  • GitHub: AlexeyAB/darknet
  • GitHub: WongKinYiu/yolov7
  • Roboflow Blog
  • PyImageSearch

Q&A 時間

聯絡方式

感謝聆聽!🎉


附錄:快速參考

常用指令

# PyTorch 訓練
python train.py --batch 16 --epochs 100

# Darknet 訓練
./darknet detector train cfg/data.data cfg/yolo.cfg weights.conv

# 模型測試
python detect.py --weights best.pt --source test.jpg

# Docker 運行
docker run --gpus all -it darknet:latest

本文最初發布於 HackMD @BASHCAT。

8GB 的 RK3588 能跑多聰明的 LLM?ROCK 5C 用 NPU 實測 5 個模型

先講結論,省得你滑到最後: 在一片 8GB 的 RK3588 板子上,用 NPU 跑得最聰明的是 Qwen3-4B-Instruct-2507,我出的 7 題全對,但每秒只吐 3.7 個 token。 想要順一點的對話體驗,Qwen3.5-2B 的 8.4 tok/s 是比...