> ## Documentation Index
> Fetch the complete documentation index at: https://felimet-hub.jmcores.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 乳牛跛行偵測桌面 GUI

> 即時乳牛跛行偵測 PyQt6 桌面應用程式，採用 YOLO11m-seg 進行個體分割，並以 MobileNetV3-Large 分類器解析 GEI 與 HEI 步態能量影像輸出跛行等級結果，介面支援 RTSP 串流與本地影片檔案輸入，適用於畜牧場即時影像監測。

export const LamenessGrades = ({lang = "zh"}) => {
  const t = lang === "en" ? {
    lead: "Click a grade to see what the class means, then drag the slider to see the GEI / HEI fusion weighting.",
    meaning: "Class meaning",
    fusion: "GEI / HEI fusion weight",
    gei: "GEI (gait energy)",
    hei: "HEI (height energy)",
    note: "The MobileNetV3-Large classifier fuses both branches; GEI is weighted higher by default. The slider is illustrative only."
  } : {
    lead: "點一個等級看類別含義，拖滑桿看 GEI / HEI 的融合加權。",
    meaning: "類別含義",
    fusion: "GEI / HEI 融合權重",
    gei: "GEI（步態能量）",
    hei: "HEI（高度能量）",
    note: "MobileNetV3-Large 分類器融合兩路輸出，預設 GEI 權重較高。滑桿僅為示意，不做實際預測。"
  };
  const GRADES = lang === "en" ? [{
    key: "normal",
    label: "Normal",
    tone: "ok",
    desc: "Normal gait, no lameness signs detected."
  }, {
    key: "slight",
    label: "Slight",
    tone: "warn",
    desc: "Slightly asymmetric gait, mild lameness."
  }, {
    key: "severe",
    label: "Severe",
    tone: "bad",
    desc: "Clearly abnormal gait, severe lameness."
  }, {
    key: "stationary",
    label: "Stationary",
    tone: "idle",
    desc: "The animal is not walking; gait data is insufficient to grade."
  }] : [{
    key: "normal",
    label: "Normal",
    tone: "ok",
    desc: "步態正常，未偵測到跛行徵兆。"
  }, {
    key: "slight",
    label: "Slight",
    tone: "warn",
    desc: "步態略有不對稱，屬輕微跛行。"
  }, {
    key: "severe",
    label: "Severe",
    tone: "bad",
    desc: "步態明顯異常，屬嚴重跛行。"
  }, {
    key: "stationary",
    label: "Stationary",
    tone: "idle",
    desc: "個體未行走或靜止，步態資訊不足以判定。"
  }];
  const [active, setActive] = useState(0);
  const [gei, setGei] = useState(65);
  const cur = GRADES[active];
  const css = `
  .lg-root{--lg-bg:#FAF8F3;--lg-surface:rgba(0,0,0,0.025);--lg-border:rgba(0,0,0,0.09);--lg-text:#2b2722;--lg-dim:#6f6a62;--lg-faint:#8a8378;--lg-accent:#bf7551;--lg-ok:#5f8a52;--lg-warn:#c0792f;--lg-bad:#b1503f;--lg-idle:#8a8378;border:1px solid var(--lg-border);border-radius:14px;background:var(--lg-bg);color:var(--lg-text);overflow:hidden;}
  .dark .lg-root{--lg-bg:#1b1a18;--lg-surface:rgba(255,255,255,0.03);--lg-border:rgba(255,255,255,0.08);--lg-text:#e7e3da;--lg-dim:#a8a299;--lg-faint:#8a8378;--lg-accent:#cf8a68;--lg-ok:#86b274;--lg-warn:#d7a043;--lg-bad:#d07060;--lg-idle:#a8a299;}
  .lg-head{padding:13px 18px 11px;border-bottom:1px solid var(--lg-border);font-size:12.5px;color:var(--lg-dim);display:flex;align-items:center;gap:8px;}
  .lg-head-ic{color:var(--lg-accent);flex-shrink:0;}
  .lg-body{padding:16px 18px;}
  .lg-grades{display:flex;gap:8px;flex-wrap:wrap;}
  .lg-g{flex:1 1 100px;min-width:96px;text-align:left;background:transparent;border:1px solid var(--lg-border);border-radius:11px;padding:11px 12px;cursor:pointer;color:inherit;font:inherit;transition:border-color .15s,background .15s,box-shadow .15s;}
  .lg-g:hover{background:var(--lg-surface);}
  .lg-g-dot{width:9px;height:9px;border-radius:50%;display:inline-block;margin-right:7px;vertical-align:middle;}
  .lg-g-ok .lg-g-dot{background:var(--lg-ok);} .lg-g-warn .lg-g-dot{background:var(--lg-warn);}
  .lg-g-bad .lg-g-dot{background:var(--lg-bad);} .lg-g-idle .lg-g-dot{background:var(--lg-idle);}
  .lg-g-name{font-size:14px;font-weight:600;vertical-align:middle;}
  .lg-g-on{box-shadow:0 0 0 3px rgba(191,117,81,.12);}
  .lg-g-on.lg-g-ok{border-color:var(--lg-ok);background:rgba(95,138,82,.08);}
  .lg-g-on.lg-g-warn{border-color:var(--lg-warn);background:rgba(192,121,47,.08);}
  .lg-g-on.lg-g-bad{border-color:var(--lg-bad);background:rgba(177,80,63,.09);}
  .lg-g-on.lg-g-idle{border-color:var(--lg-idle);background:rgba(138,131,120,.1);}
  .lg-detail{margin-top:13px;border:1px solid var(--lg-border);border-radius:10px;background:var(--lg-surface);padding:12px 14px;}
  .lg-detail-k{font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--lg-accent);margin-bottom:5px;}
  .lg-detail-d{font-size:13.5px;line-height:1.6;color:var(--lg-text);}
  .lg-fusion{margin-top:16px;}
  .lg-f-lab{font-size:12.5px;color:var(--lg-dim);margin-bottom:9px;display:flex;align-items:center;gap:8px;}
  .lg-f-lab-ic{color:var(--lg-accent);flex-shrink:0;}
  .lg-bars{display:flex;height:30px;border-radius:8px;overflow:hidden;border:1px solid var(--lg-border);}
  .lg-bar{display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:600;color:#fff;transition:flex-basis .12s linear;white-space:nowrap;overflow:hidden;}
  .lg-bar-g{background:var(--lg-accent);}
  .lg-bar-h{background:var(--lg-idle);}
  .lg-range{width:100%;margin-top:11px;accent-color:#bf7551;cursor:pointer;}
  .dark .lg-range{accent-color:#cf8a68;}
  .lg-note{font-size:12px;line-height:1.55;color:var(--lg-faint);margin-top:9px;}
  @media (max-width:560px){.lg-g{flex:1 1 44%;}}
  `;
  return <div className="lg-root">
      <style>{css}</style>
      <div className="lg-head">
        <svg className="lg-head-ic" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="m12 14 4-4" /><path d="M3.34 19a10 10 0 1 1 17.32 0" /></svg>
        {t.lead}
      </div>
      <div className="lg-body">
        <div className="lg-grades">
          {GRADES.map((g, i) => <button key={g.key} type="button" className={"lg-g lg-g-" + g.tone + (i === active ? " lg-g-on" : "")} onClick={() => setActive(i)}>
              <span className="lg-g-dot" />
              <span className="lg-g-name">{g.label}</span>
            </button>)}
        </div>
        <div className="lg-detail">
          <div className="lg-detail-k">{t.meaning} · {cur.label}</div>
          <div className="lg-detail-d">{cur.desc}</div>
        </div>
        <div className="lg-fusion">
          <div className="lg-f-lab">
            <svg className="lg-f-lab-ic" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M3 12h18" /><path d="m9 6-6 6 6 6" /><path d="m15 18 6-6-6-6" /></svg>
            {t.fusion}
          </div>
          <div className="lg-bars">
            <div className="lg-bar lg-bar-g" style={{
    flexBasis: gei + "%"
  }}>{t.gei} {gei}%</div>
            <div className="lg-bar lg-bar-h" style={{
    flexBasis: 100 - gei + "%"
  }}>{t.hei} {100 - gei}%</div>
          </div>
          <input className="lg-range" type="range" min="0" max="100" step="5" value={gei} onChange={e => setGei(Number(e.target.value))} />
          <div className="lg-note">{t.note}</div>
        </div>
      </div>
    </div>;
};

export const ArchFlow = ({nodes = [], lang = "zh"}) => {
  const t = lang === "en" ? {
    flow: "Data flow",
    hint: "Click any stage to see what it does",
    stage: "Stage"
  } : {
    flow: "資料流",
    hint: "點任一階段看它做什麼",
    stage: "階段"
  };
  const safe = Array.isArray(nodes) ? nodes : [];
  const [active, setActive] = useState(0);
  if (safe.length === 0) return null;
  const idx = Math.min(active, safe.length - 1);
  const current = safe[idx];
  const css = `
  .af-root{--af-bg:#FAF8F3;--af-surface:rgba(191,117,81,0.06);--af-surface2:rgba(0,0,0,0.025);--af-border:rgba(0,0,0,0.09);--af-text:#2b2722;--af-dim:#6f6a62;--af-faint:#8a8378;--af-rail:rgba(0,0,0,0.12);--af-accent:#bf7551;border:1px solid var(--af-border);border-radius:14px;background:var(--af-bg);color:var(--af-text);overflow:hidden;}
  .dark .af-root{--af-bg:#1b1a18;--af-surface:rgba(207,138,104,0.09);--af-surface2:rgba(255,255,255,0.03);--af-border:rgba(255,255,255,0.08);--af-text:#e7e3da;--af-dim:#a8a299;--af-faint:#8a8378;--af-rail:rgba(255,255,255,0.13);--af-accent:#cf8a68;}
  .af-head{padding:13px 18px 11px;display:flex;align-items:center;gap:9px;flex-wrap:wrap;border-bottom:1px solid var(--af-border);}
  .af-head-ic{color:var(--af-accent);flex-shrink:0;}
  .af-head-t{font-size:12px;font-weight:700;letter-spacing:.6px;color:var(--af-dim);text-transform:uppercase;}
  .af-head-h{font-size:12px;color:var(--af-faint);}
  .af-grid{display:flex;gap:0;align-items:stretch;}
  .af-list{flex:1 1 0;min-width:0;padding:12px 12px 16px;}
  .af-node{display:flex;align-items:stretch;gap:10px;width:100%;text-align:left;background:transparent;border:none;cursor:pointer;padding:0;color:inherit;font:inherit;-webkit-tap-highlight-color:transparent;}
  .af-railcol{width:22px;flex-shrink:0;display:flex;flex-direction:column;align-items:center;}
  .af-dot{width:9px;height:9px;border-radius:50%;margin-top:14px;background:var(--af-faint);opacity:.5;flex-shrink:0;transition:background .15s,box-shadow .15s,opacity .15s;}
  .af-line{width:2px;flex:1 1 0;background:var(--af-rail);min-height:10px;}
  .af-body{flex:1 1 0;min-width:0;padding:9px 12px;margin:2px 0;border-radius:9px;border:1px solid transparent;transition:background .15s,border-color .15s;}
  .af-node:hover .af-body{background:var(--af-surface2);}
  .af-node-on .af-body{background:var(--af-surface);border-color:rgba(191,117,81,.30);}
  .af-node-on .af-dot{background:var(--af-accent);opacity:1;box-shadow:0 0 0 4px rgba(191,117,81,.15);}
  .af-label{font-size:14.5px;font-weight:550;line-height:1.45;}
  .af-node-on .af-label{color:var(--af-accent);}
  .af-detail{width:300px;flex-shrink:0;border-left:1px solid var(--af-border);padding:16px 18px;background:var(--af-surface2);}
  .af-detail-k{font-size:11px;font-weight:700;letter-spacing:.5px;text-transform:uppercase;color:var(--af-accent);margin-bottom:7px;}
  .af-detail-l{font-size:16px;font-weight:600;margin-bottom:9px;line-height:1.35;}
  .af-detail-d{font-size:13.5px;line-height:1.65;color:var(--af-dim);}
  @media (max-width:640px){.af-grid{flex-direction:column;}.af-detail{width:auto;border-left:none;border-top:1px solid var(--af-border);}}
  `;
  return <div className="af-root">
      <style>{css}</style>
      <div className="af-head">
        <svg className="af-head-ic" xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect width="8" height="8" x="3" y="3" rx="2" /><path d="M7 11v4a2 2 0 0 0 2 2h4" /><rect width="8" height="8" x="13" y="13" rx="2" /></svg>
        <span className="af-head-t">{t.flow}</span>
        <span className="af-head-h">{t.hint}</span>
      </div>
      <div className="af-grid">
        <div className="af-list">
          {safe.map((n, i) => <button key={i} type="button" className={"af-node" + (i === idx ? " af-node-on" : "")} onClick={() => setActive(i)} onMouseEnter={() => setActive(i)}>
              <div className="af-railcol">
                <div className="af-dot" />
                {i < safe.length - 1 && <div className="af-line" />}
              </div>
              <div className="af-body">
                <div className="af-label">{n.label}</div>
              </div>
            </button>)}
        </div>
        <div className="af-detail">
          <div className="af-detail-k">{t.stage} {idx + 1} / {safe.length}</div>
          <div className="af-detail-l">{current.label}</div>
          <div className="af-detail-d">{current.detail}</div>
        </div>
      </div>
    </div>;
};

`Python` · `PyTorch` · `YOLO` · `Computer Vision` · `Precision Livestock`

## 專案概述

即時乳牛跛行偵測的桌面應用程式。輸入可以是 RTSP 串流或本地影片檔案，後端以 YOLO11m-seg 對每一幀進行乳牛個體實例分割，接著從時序幀中累積生成 GEI（步態能量影像）與 HEI（高度能量影像），再以 MobileNetV3-Large 分類器融合兩者，輸出 Normal、Slight、Severe、Stationary 四個等級的跛行判定。前端以 PyQt6 實作圖形介面，提供即時視覺化、能量影像視窗與每隻個體的統計表。

模型權重未包含於 repo，需聯繫維護者取得。

## 核心功能

* RTSP 串流或影片檔案輸入，支援多種格式與串流協定
* YOLO11m-seg 實例分割，同時追蹤畫面中多頭乳牛
* GEI 生成：多幀二值化輪廓平均累加，捕捉整體步態特徵（質心對齊、等比例裁切、縮放至 220x318）
* HEI 生成：三通道編碼（整體、前半、後半區間累加），分析背部高度變化模式
* MobileNetV3-Large 分類器融合 GEI 與 HEI 預測，輸出四等級結果（GEI 權重預設較高）
* GPU 加速（CUDA），搭配批次處理、記憶體池與環形緩衝區
* PyQt6 GUI，含即時標註視窗、GEI/HEI 能量影像視窗、FPS 監控、統計表
* 可調參數：YOLO 信心度門檻、累積幀數（window size）、GEI/HEI 融合權重、播放速度

## 跛行等級與雙模態融合

分類器輸出 Normal、Slight、Severe、Stationary 四個等級，融合 GEI（步態能量影像）與 HEI（高度能量影像）兩路特徵：

<LamenessGrades />

## 專案架構

<ArchFlow
  nodes={[
{ label: "輸入", detail: "接受 RTSP 串流或本地影片檔案，支援多種格式與串流協定。" },
{ label: "YOLO11m-seg", detail: "對每一幀進行乳牛個體實例分割，同時追蹤畫面中多頭乳牛的 ID。" },
{ label: "GEI 生成", detail: "多幀二值化輪廓平均累加，質心對齊後縮放至 220x318，捕捉整體步態外形特徵。" },
{ label: "HEI 生成", detail: "三通道編碼（整體、前半、後半區間累加），分析背部高度隨時間的變化模式。" },
{ label: "MobileNetV3-Large 分類器", detail: "分別對 GEI 與 HEI 進行推論，再以可調權重融合兩路輸出，預設 GEI 權重較高，輸出 Normal / Slight / Severe / Stationary 四等級。" },
{ label: "PyQt6 GUI", detail: "即時顯示標註畫面、GEI/HEI 能量影像視窗、FPS 監控與每隻個體的統計表，結果可匯出。" }
]}
/>

<Tree>
  <Tree.Folder name="GUI" defaultOpen>
    <Tree.File name="gui_app.py · GUI 主程式" />

    <Tree.File name="main_backend.py · 後端處理引擎（DetectionWorker）" />

    <Tree.File name="model_v3.py · MobileNetV3 模型定義" />

    <Tree.Folder name="lameness_models · 分類器權重（需自行取得）" defaultOpen>
      <Tree.File name="GEI_lameness_mobileNetv3_large.pth" />

      <Tree.File name="HEI_lameness_mobileNetv3_large.pth" />
    </Tree.Folder>

    <Tree.Folder name="yolo_model · YOLO 權重（需自行取得）" defaultOpen>
      <Tree.File name="cow_yolo11m-seg_model.pt" />
    </Tree.Folder>

    <Tree.Folder name="json" defaultOpen>
      <Tree.File name="class_indices.json" />

      <Tree.File name="PY_*_camera_params.json" />
    </Tree.Folder>
  </Tree.Folder>
</Tree>

## 快速開始

<Steps>
  <Step title="取得模型權重">
    模型權重未包含於 repo。聯繫維護者取得以下三個檔案，並放置至對應路徑：

    ```
    GUI/lameness_models/GEI_lameness_mobileNetv3_large.pth
    GUI/lameness_models/HEI_lameness_mobileNetv3_large.pth
    GUI/yolo_model/cow_yolo11m-seg_model.pt
    ```
  </Step>

  <Step title="安裝 PyTorch（CUDA 版）">
    ```bash theme={null}
    pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
    ```
  </Step>

  <Step title="安裝其他依賴">
    ```bash theme={null}
    cd GUI
    pip install -r requirements_gui.txt
    ```
  </Step>

  <Step title="啟動 GUI">
    ```bash theme={null}
    python gui_app.py
    ```

    在介面中設定輸入來源（RTSP URL 或影片路徑）、模型路徑與相機校正參數，點擊「開始分析」。
  </Step>
</Steps>

## 注意事項

<Warning>
  模型權重不在 repo 內，取得後自行放到對應目錄才能啟動推論。系統僅在 Windows 10/11（64 位元）上測試過；GPU 推論需要 NVIDIA GPU 與 CUDA 12.1，無 GPU 時程式可退回 CPU 模式但速度顯著較慢。RTSP 串流連線失敗時，先確認網址格式、網路連通性與攝影機 RTSP 支援。
</Warning>

## 實際應用面

適用於需要對乳牛個體行走影片進行跛行等級判定的研究或農場試驗場景，提供比命令列推論更易操作的圖形介面，結果可匯出為統計報告。模型以 GEI 與 HEI 雙模態融合，目的是結合步態外形與背部高度兩個維度的資訊來提升分類準確率。

## 相關連結

* GitHub：[felimet/dairycow\_lameness\_gui](https://github.com/felimet/dairycow_lameness_gui)
* Ultralytics YOLO11：[docs.ultralytics.com](https://docs.ultralytics.com)
* PyQt6 文件：[riverbankcomputing.com/software/pyqt](https://www.riverbankcomputing.com/software/pyqt/)
