CoolFace
Apppublic

YOU-LIN/cement-floating-black

sourceHugging Facemitupdated 4d agoView on Hugging Face
0likes
App README

水泥浮黑檢測系統

AI 影像辨識水泥試體浮黑百分比。上傳試體俯拍照片,自動分析浮黑面積比例。

近期更新

  • —2026-09-11:修好「乾淨試體被判 20%」——低浮黑的 5% 地板被閘門拆掉了(`CORE_VERSION` → 20.2.0)。20260911_093334_IMG_8526 相對暗 0.03%、絕對暗 0.00%,肉眼乾淨,系統卻判 20.33%「輕度」。程式裡本來就有 pct = min(pct, 5.0) 這道地板,但它的低向 abs 分支被多掛了一個 w_eff > 0.5 條件:w_eff = sigmoid((pr-50)/10) 要過半必須 pr > 50,而低向 abs 防呆的適用情境正是 pr ≈ 20 的乾淨試體 ⇒ 這道閘門讓地板對它要保護的樣本永久失效(8526 的 abs=0.0014% 數值上遠低於門檻 0.0220%,卻因 w_eff=0.0491 被擋)。這個閘門是 2026-09-09 修「肉眼看很黑卻判 0%」時順手加到低向的副作用——當時的原始訴求只講「不該讓 abs 的高值防呆蓋掉 rel 的低值防呆」,只需要高向有閘門。現在只把低向的 w_eff > 0.5 拿掉,A/B 實測 IMG_8526 由 20.33%(輕度)→ 5.00%(無浮黑),而 5 張標準片 20/40/50/70/100 完全不變。高向閘門保留,且因 pr 低 ⇒ w_eff 低 ⇒ 高向 abs 分支不可能成立,不會重回「看程式碼順序決定結果」的未定義行為。回歸測試擴充為 R1–R11,15 項全數 PASS。詳見 README_技術維護.md 問題九。
  • —2026-09-11:修正「校準與預測白平衡不一致」(`CORE_VERSION` → 20.1.0)。calibrate() 原本固定用 calib_use_white_balance=False(不套白平衡),predict() 卻用 enable_white_balance=True(套白平衡)——等於用 A 尺刻的曲線、拿 B 尺去查。實測 IMG_8526:不套白平衡時 rel=0.0300%、套了變 0.0381%;而 20% 標準片反過來由 0.0306% 降到 0.0244%,同一顆試體相對標準片的位置會翻轉,ROI 遮罩也差了約 900 px。更關鍵的是絕對暗度用的是 abs_threshold × wb_factor,這個補償只有兩邊都套白平衡時才成立。現在 calib_use_white_balance 預設改為 None=自動跟隨 enable_white_balance;若有人顯式設成不一致,log 會留 ERROR,不再默默發生。副產品:改用 WB on 後 5 張標準片的 LOO MAE 由 3.510 → 3.07,rel_threshold 由 0.700 → 0.680。⚠️ 此修正只改 `calibrate()`,線上預測值不變;但已上線的舊校準是在「不套白平衡」下建立的,請重新校準一次讓曲線與預測同基準。⚠️ 另外請注意:這個 bug 是真的,但它不是「相對暗 0%、卻判 20%」的原因(八種 WB × auto 組合全部 ≥20%;詳見 README_技術維護.md 問題八)。
  • —2026-09-11:「非 CNN 偵測口徑」警示與自動異常回報(v20)。這是前一版「人工回報」的反向補洞:人工回報要有人看才會發生,而 20260910_142201_IMG_4477 的 82% 誤判(應為 44%)說明了最危險的情況——偵測退回 Hough 口徑時,前端完全看不出來,使用者也就不會想去回報。現在:(1) 結果頁與趨勢頁詳細視窗都會顯示「偵測口徑:CNN / Hough(精修)…」;(2) 口徑不是 CNN 就自動跳警示,fallback(完全沒偵測到圓)是最嚴重的紅色警示;(3) 同時由系統自動把該筆寫入 roi_anomaly = 1,並記下來源 roi_anomaly_source = 'system';(4) 歷史紀錄頁新增「偵測口徑」欄,範圍異常 欄會區分 🤖 自動 與 👤 人工。⚠️ 這又是 schema 變更:predictions 由 16 欄增為 17 欄,部署前必須先跑 sql/00_supabase_schema.sql。
  • —2026-09-11:修正校準口徑標記「寫得進去、讀不出來」的 bug。上一版把口徑寫進 calibrations.description,但寫入端會把描述開頭多餘的 | 清掉、讀取端卻要求一定要有 |。UI 的「描述」欄預設是空的,標記正好落在開頭 → 被清掉 → DB 裡看得到標記,畫面卻永遠顯示「未標記」(校準當下的成功訊息是對的,因為它讀的是記憶體變數)。已改為不依賴 |、同時接受全形冒號,並在寫入後自我檢查「讀不回來就改用純標記」。既有那筆 CNN 校準不需重跑,畫面會直接顯示 `CNN`。
  • —2026-09-10:「偵測範圍異常回報」上線(前端人工回饋)。看過疊圖的使用者若發現圈跟試體實際範圍不符,可在測試結果頁的卡片右下角或趨勢頁的「測試詳細」視窗右下角按「⚠️ 偵測範圍異常回報」(v20 起位置;原本緊跟在疊圖下方),選原因(框太大/框太小/圓偏掉/不是試體/其他)+選填備註,寫入 predictions.roi_anomaly / roi_anomaly_note / roi_anomaly_time。管理員在歷史紀錄頁會看到 ⚠️ 標記與備註,頁首並顯示本頁回報筆數與比例(CSV 匯出也含這兩欄)。⚠️ 這是 schema 變更:predictions 由 13 欄增為 16 欄,部署前必須先跑 sql/00_supabase_schema.sql。2026-09-10 已於正式環境實測通過:回報可正常送出,頁面重新整理後顯示「已回報」,DB 寫入正常。
  • —2026-09-10:測試結果頁精簡。逐張結果移除「📈 詳細數據」與「🔄 重新測試此圖」,一般使用者只看浮黑%/等級/品質警示(+偵測範圍異常回報);詳細數據(相對暗/絕對暗/中位 L/四象限)仍保留在趨勢頁的「測試詳細」視窗,歷史查詢與編輯則在後端「📊 歷史紀錄」分頁。
  • —2026-09-10:CNN 量測圈不再二次內縮(`ROI_MEASURE_SHRINK_CNN = 1.0`,`CORE_VERSION` → 19.0.0)。CNN 的訓練標註本來就是人工量測圈(半徑 r、無內縮),再乘 0.85 等於二次內縮,量測範圍會小於標註口徑。以 5 張標準片 A/B 實測:LOO MAE 5.100 → 3.510(改善 31.2%);副產品是修掉一個真實退步——原本「CNN + 0.85」比更早的 Hough 基準(3.160)還差。Hough / fallback 路徑維持 0.85 不變(比例單一來源:roi.measure_shrink_ratio())。⚠️ 量測基準變更 → 必須重新校準,舊預測值不可直接與新值比較。
  • —2026-09-10:前端新增「顯示 AI 偵測範圍」(新模組 ui_overlay.py)。測試頁與趨勢頁的詳情視窗可在照片上疊加量測圈 + 時尚風「偵測範圍」標籤(科技藍漸層字 + 玻璃膠囊)。只作用在顯示用的副本,Storage 存的照片、file_hash、去重、「回報實際值」查詢完全不受影響(見下方「標註只存在於顯示層」)。測試頁有開關(🔍 顯示 AI 偵測範圍,預設開啟),關掉即完全恢復原本行為。
  • —2026-09-09:ROI 偵測改用 CNN(`ENABLE_ROI_CNN` 預設啟用)。用 130 筆真實試體人工複核,CNN 幾何定位大幅優於 Hough(圓心誤差 3px vs 34px),並修正了 15/20 筆原始 Hough 誤判「假超標」的案例。詳細驗證過程與資料見 `../dataset/README.md`。
  • —校準外推防呆修正:rel/abs 兩指標矛盾(例如 CNN 圈到一小塊不具代表性的暗斑)時,原本可能同時觸發高低兩個方向的外推保護、由程式碼判斷順序決定結果;改成只在該指標對最終預測值貢獻佔多數(權重 >0.5)時才套用同方向防呆,修正了「肉眼看很黑卻判 0%」這類異常(見 detector._compute_prediction、dataset/01_roi_manual_review/manual_roi_remeasure.py)。⚠️ 2026-09-11(v20.2.0)回溯更正:這條只適用於高向;侵向被順手加上的同一道閘門反而拆掉了 5% 地板,已移除(見最上方 20.2.0 說明)。
  • —背景陰影門檻調整(SHADOW_RATIO_WARN 0.15→0.25):實測 130 筆真實樣本後,0.15 對超過半數樣本都會觸發、失去分辨力;調高後仍非完美判別(陰影本身不是誤判的直接因,只是常伴隨出現),但至少减少無效警示。
  • —持續優化整體判定流程,讓不同拍攝條件下的結果更穩定。
  • —趨勢頁與歷史查詢流程重新整理,方便做跨廠、跨日期的比較。
  • —影像前處理持續強化,系統會自動補償部分不均勻影像,減少使用者額外調整。

功能

  • —多張批次檢測(支援 JPG/PNG)
  • —四象限分布分析
  • —拍攝品質自動評估:反光偵測(分級)、背景陰影偵測、遠近/框選偵測
  • —前處理全域曝光正規化 + 白紙偵測兩段式(strict + 自適應 fallback),光源偏暗/偏亮仍穩定
  • —前處理自動補償不均勻影像,降低因拍攝條件差異造成的波動
  • —量測 ROI 內縮,排除杯緣/彎月面暗環(Hough 0.85、 CNN 不內縮)
  • —偵測範圍視覺化:可在照片上疊加量測圈 + 時尚風「偵測範圍」標籤(純顯示層,不影響儲存的照片)
  • —偵測範圍異常回報:使用者直接在疊圖上回報「框太大/框太小/圓偏掉/不是試體」,管理員在歷史紀錄頁查看與統計
  • —偵測口徑透明化:結果頁/趨勢頁均顯示「偵測口徑」(CNN/Hough/中央 fallback),非 CNN 自動警示並自動標記異常,避免「退回 Hough 導致數值翻倍」這種無聲失效
  • —廠區浮黑趨勢頁(管理員 + test 帳號):每日最大值折線 + 50% 管制線 + 超標警示 + 下鑽照片
  • —30 廠 URL token 免密碼登入
  • —管理員校準管理、歷史紀錄編輯、實際值回報

部署架構

GitHub repo
  ├─ Actions 自動推 → HF Spaces(主站,Docker)
  └─ 自動部署 → Streamlit Cloud(備站)

兩邊共用 Supabase(Postgres + Storage)

Secrets 設定

Secret說明
SUPABASE_URLSupabase 專案 URL
SUPABASE_KEYSupabase secret key
SUPABASE_DB_URLPostgres pooler URL(port 6543)
ACCOUNT_ADMIN_PW管理員密碼
CEMENT_TOKEN_SECRET各廠 token 的 master secret
ENABLE_TREND_PAGE(選用)趨勢頁開關;設 false 可即時停用、免改程式
部署需求:趨勢頁的浮動視窗使用 st.dialog,需 Streamlit ≥ 1.37(見 requirements.txt)。

cement_core 套件架構

config ────────────────┐  常數 + 帳號定義 + token 產生
                       │
models ────────────────┤  PredictionResult / QuadrantInfo / ImageQuality
                       │
compression ───────────┤
preprocessing ─────────┤  影像處理層
roi ───────────────────┤  Hough+顏色精修(預設 fallback)
roi_cnn ────────────────┤  CNN 分割(ENABLE_ROI_CNN 啟用時優先,失敗退回 roi)
                       │
measurement ───────────┤  量測層(rel/abs 雙指標 + 品質 + 反光/陰影/遠近)
                       │
calibration ───────────┤  Smooth Isotonic + LOO 自動優化
                       │
database ──────────────┤  Supabase Postgres + Storage
                       │
logger ────────────────┤  統一 logging
                       │
detector ──────────────┘  協調器(對外唯一入口)

模組職責

模組職責
config.py常數、30 廠帳號、token 產生/驗證、時區
models.pydataclass(含 specularok/specularratio)
compression.py影像壓縮、hash、JPG 編碼
preprocessing.py全域曝光正規化、白紙偵測(strict + 自適應 fallback)、LAB L 通道校正、ROI 正規化
roi.pyHough 圓 + 顏色精修兩階段偵測、量測 ROI 內縮 (shrink_to_inner)、內縮比例單一來源 (measure_shrink_ratio)、ENABLE_ROI_CNN 開關與 CNN 失敗時的 fallback 邏輯
roi_cnn.pyCNN(DeepLabV3-MobileNetV3)分割版 ROI 偵測,介面跟 roi.detect() 一致;模型權重放在 cement_core/roi_cnn_weights/best.pt(訓練腳本在 dataset/02_cnn_roi_pilot/train_roi_unet.py,權重需跟 dataset/02_cnn_roi_pilot/roi_cnn/best.pt 保持同一份)。⚠️ 權重固定是 44,309,759 bytes;.gitattributes 一定要有 *.pt filter=lfs diff=lfs merge=lfs -text(已於 2026-09-11 補上),否則 git checkout 不會跑 git-lfs smudge,image 裡只會有 133 bytes 的 LFS pointer,CNN 會靜默退回 Hough 口徑(詳見 §CNN 權重的 LFS 陷阱)
measurement.py浮黑量測、四象限分布、品質評估、反光(分級)+ 背景陰影 + 遠近/框選偵測
calibration.pySmooth Isotonic Regression + LOO MAE 優化
database.pyDB 操作(含 deletehistoryrecords、時區設定)
detector.py校準 + 預測整合(校準不寫 images 表)
logger.py統一 logging(stdout,雲端可看)
streamlit_app.pyWeb UI(URL token 登入、data_editor 歷史頁)
ui_overlay.py顯示層標註(純 PIL,不在模組層 import streamlit)。把量測圈 + 時尚風「偵測範圍」標籤疊在「顯示用副本」上;字型輪廓放在 assets/ai_badge_glyphs.json(只帶座標,不隨程式散布字型檔)。非 `cement_core` 成員,放專案根目錄

tools/ 部署診斷工具

跟 cement_core 業務邏輯無關,只讀、不寫任何資料,容器/部署層在用:

檔案用途
boot_watch.py容器內獨立心跳 process,跟 Streamlit 並行執行(Dockerfile CMD 背景啟動),每隔數秒印 uptime、記憶體、/_stcore/health 探測、port 連線數、cement_core.boot 狀態檔,用來判斷「HF Spaces 顯示 Running 但網頁無響應」時卡在哪一層(HTTP/WebSocket、Python 應用層,還是 process 已死掉)
diagnose.py一次性深度診斷腳本,每項檢查都有硬性 timeout(版本、secrets、Supabase REST/PG、Storage、cv2、cement_core import、SOP/範例圖檔案、本機 Streamlit health),結尾印 VERDICT 指出最可疑環節;App 發生 DB 相關錯誤時的提示訊息會引導使用者在容器內執行 python tools/diagnose.py
verify_cnn_weights.py建置時驗證 roi_cnn_weights/best.pt 是真的 checkpoint(不是 Git LFS pointer / 空檔 / 截斷檔),Dockerfile 在 COPY . . 之後直接執行,不符就中止 build。也可手動跑來確認部署包
gen_glyph_outline.py開發用(非執行期、不進容器):從系統 OFL 字型抽出「偵測範圍」四個字的輪廓座標,寫成 assets/ai_badge_glyphs.json 給 ui_overlay.py 用。需 fontTools(只在開發機安裝,不能放進 `requirements.txt`,否則 HF 映像會多裝一個用不到的套件)。平時不用重跑,只有想換字型/字重/字樣時才跑
specular_probe.py(反光/陰影/對比批次探針,輸出 artifacts/*.csv)不是現行系統執行期使用的工具(App 與 Docker 只會呼叫上表的 boot_watch.py、diagnose.py),但一樣放在 `tools/`,離線批次分析用,輸出放在 `tools/artifacts/`(specular_probe_lab.csv、specular_probe_field_sample*.csv、specular_probe_smoke*.csv);詳見檔案開頭註解與 `../dataset/WIKI.md` §0。

帳號機制

  • —管理員:側邊欄密碼登入,全功能(測試 + 校準 + 歷史 + 趨勢 + 系統 + 身份切換)
  • —30 廠:URL token 自動登入,僅測試功能
  • —test 帳號:測試 + 趨勢兩分頁(趨勢頁僅管理員與 test 可見)
  • —管理員在系統 tab 產生各廠連結,操作員存書籤即可使用

關鍵設計決策

Smooth Isotonic Regression(Jiang 2011)

IsotonicRegression 找最佳單調近似 → 合併 flat regions → PCHIP 平滑。 對校準雜訊穩健,異常點不會破壞整條曲線。

rel/abs 雙指標加權

  • —rel(相對暗區):對亮度變化穩健,高浮黑時飽和
  • —abs(絕對暗區):高浮黑時有判別力,對光源敏感
  • —加權:rel 估算越高越偏向 abs(sigmoid,w_eff),超出範圍完全用 abs
  • —外推防呆(EXTRAPOLATE_HIGH_RATIO/LOW_RATIO):rel 或 abs 遠超出校準訓練範圍時,不相信外推出的數值方向,改用飽和值(95%/5%)。2026-09-09 修正:高向 abs 防呆要在 w_eff 過半(真的是主要決定因素)時才套用,否則 CNN 圈到一小塊不具代表性暗斑時,rel 低、abs 矛盾地高,兩個方向的防呆會同時滿足,變成看程式碼判斷順序決定結果的未定義行為(實例:imageid 183,abs 遠超出範圍卻被判成 0%,肉眼看試體明顯偏黑)。⚠️ **2026-09-11(v20.2.0)修正**:那次把 `weff > 0.5 也順手加到了**低向**,而且**低向的適用情況(乾淨試體 pr≈20)剛好永遠過不了這道閘門**,導致 **5% 地板形同不存在**、乾淨試體全被判 20%。現已只保留高向的閘門(見上方 20.2.0 說明與 README_技術維護.md` 問題九)。

校準不寫 images 表

校準資料的本體是 calibrations.rel_anchors_json + abs_anchors_json。 影像不再寫入 images/measurements/calibration_samples,消除孤兒問題。

URL token 免密碼

用 HMAC-SHA256(mastersecret, 廠名) 產生 16 字元 token。 只需 1 個 Secret(mastersecret),自動推導 30 個 token。

前處理穩定化(曝光正規化 + ROI 內縮)

  • —全域曝光正規化(ENABLE_EXPOSURE_NORM):先把整張影像亮度拉到「最亮白紙 ≈ 目標值」,弭平手機/距離/光線造成的曝光差,且不依賴偵測到白紙。
  • —對比自動補償(ENABLE_CONTRAST_NORM):若影像整體對比不足,系統會在前處理自動補強一次,必要時再做第二次補償,避免把調整責任留給使用者。
  • —量測 ROI 內縮(ROI_MEASURE_SHRINK / ROI_MEASURE_SHRINK_CNN):量測只取內盤,排除杯緣玻璃/彎月面暗環,避免 rel 被灌高。比例依偵測方法而定(單一來源 roi.measure_shrink_ratio()):
  • —Hough / fallback → 0.85:圓偵測找到的是燒杯輪廓,邊緣含杯壁與彎月面暗環,必須內縮。
  • —CNN → 1.0(不內縮,但仍以偵測圓為界):CNN 的訓練標註本來就是人工量測圈(半徑 r、無內縮),再乘 0.85 等於二次內縮,量測範圍會比標註口徑還小。ratio = 1.0 仍做 mask ∩ 圓盤(r),不會退化成 CNN 輸出的不規則區塊。
  • —⚠️ 兩者都會改變量測基準 → 啟用或調整後必須用標準片重新校準(校準與預測共用同一 _process,重校後即一致)。

標註只存在於顯示層(ui_overlay.py)

量測圈與「偵測範圍」標籤只畫在「要顯示的副本」上,儲存層完全不受影響:

  • —detector.predict() 在前端標註之前就已經把乾淨的壓縮圖上傳 Storage 並算好 image_hash,所以標註不可能改到存檔照片、file_hash、去重判斷,也不會影響「回報實際值」用 image_hash 反查 prediction_id。
  • —顯示用的副本由 compression.compress(np.array(pil_img)) 重新產生,跟 roi_circle 同一個 1280 長邊坐標系;管線中沒有任何 EXIF 旋轉,所以圓的位置天然對齊,不需要額外換算。
  • —標籤文字用輪廓坐標 JSON(assets/ai_badge_glyphs.json)而不是字型檔:python:3.11-slim 與 Streamlit Cloud 都沒有中文字型,內嵌輪廓才能保證「偵測範圍」在兩邊平台長得一模一樣。坐標是用 tools/gen_glyph_outline.py 從 Noto Sans TC 抽出來的(字型授權 OFL 1.1,授權檔 assets/OFL-NotoSansTC.txt)。
  • —Streamlit 的 DOMPurify 消毒會移除 inline SVG,所以標註走 PIL 產生 bytes 再 st.image(),不用 HTML/SVG overlay。
  • —測試頁的開關(st.toggle)放在 render_user_view() 內、只建立一次;st.tabs 會執行所有分頁內容,開關若放在分頁裡會撞 key(StreamlitDuplicateElementKey)或重複渲染同一個 widget。
  • —任何一步失敗(圓無效、bytes 壞掉、字型輪廓缺失)都回 None,前端自動退回原圖,不會因為標註而讓預測失敗。

拍攝品質三道防線

  • —反光:於 ROI 內盤量測近飽和亮點,依比例分級(輕微/中等/嚴重)並說明影響。
  • —背景陰影:量測試體周圍白紙被壓暗比例,過高即警告重拍(SHADOW_RATIO_WARN,目前 0.25)。 ⚠️ 實測 130 筆真實樣本,這個比例跟「預測值是否誤判」的相關性很弱(correct 組 p90=0.445、incorrect 組 max=0.484,兩組幾乎完全重疊),調高門檻只能減少無效警示次數,不能當成可靠的誤判偵測器。
  • —遠近/框選:ROI 內盤若混入大量白紙背景(拍太遠、Hough 框到比杯大的圓),判定「太遠/未置中」並提示靠近置中重拍。

CNN ROI 上線(2026-09-09)

用 dataset/ 累積的 130 筆「原始 Hough 判定 >50%」真實試體照片人工複核,CNN 幾何定位(roi_cnn.py)大幅優於 Hough——圓心誤差 3px vs 34px,且 130 筆中有 20 筆 Hough 判定跟人工目視不符,其中 15 筆是「假超標」(CNN/Hough 說 >50%,實際沒超標),CNN 成功修正了大部分。

⚠️ 已知限制、CNN 沒有解決的問題:

  • —現行校準曲線只用 5 張人工合成標準片(有光澤的深色圓餅)擬合,對真實試體在 rel/abs 落在訓練範圍內的中高濃度區段,仍系統性預測偏高(複核樣本平均 +36 個百分點)——這是曲線本身跟真實材質分布不同造成的,不是 ROI 方法能解決的,CNN 换掉 Hough 也一樣會遇到。
  • —真正的修法需要真實試體的 actual_pct 真值重新擬合曲線,但目前資料庫裡這個欄位幾乎是空的(見 dataset/README.md 發現⑥)。
  • —已經修正的是「abs 遠超出訓練範圍時外推方向不可靠」這個特定 bug(見下方 rel/abs 加權說明),不是曲線整體偏差。

詳細驗證方法、逐筆資料與複核紀錄見 `../dataset/README.md` §2.6 與 dataset/03_cnn_vs_hough_validation/ 下的 cnn_vs_hough_over50.py/review_cnn_vs_hough.py/stage3_finalize.py。

CNN 權重的 LFS 陷阱(2026-09-11 事故)

上線後一直被「同一顆標準片應該 44% 卻量到 82%」困住,真正的原因不在曲線也不在相機,而在部署的 image 裡,權重檔是 133 bytes 的文字檔。

症狀 HF log 先出現 CNN ROI 狀態: … exists=True size=0.0MB,接著每次預測都印 CNN ROI 推論發生例外(…將退回 Hough),traceback 是:

_pickle.UnpicklingError: Weights only load failed. ...
WeightsUnpickler error: Unsupported operand 118

真相 Unsupported operand 118 的 118 是 ASCII 的 'v',也就是 version https://git-lfs.github.com/spec/v1 的第一個字元。best.pt 在 image 裡是 133 bytes 的 Git LFS pointer,不是真正的 44,309,759 bytes checkpoint。torch.load() 讀到純文字就丟 UnpicklingError,訊息裡完全沒提到 LFS。

為什麼「GitHub / HF 網頁都顯示 42 MB」還是會中招 這一點最反直覺:網頁顯示的大小查的是儲存後端,一直是對的;出問題的是 git checkout。git 只會對 .gitattributes 宣告過的檔案執行 git-lfs smudge(把 pointer 換回實體檔)。本 repo 的規則只有:

*.jpg filter=lfs diff=lfs merge=lfs -text
*.png filter=lfs diff=lfs merge=lfs -text

*`.pt` 不在裡面**,所以 best.pt 的 LFS 物件雖然存在,checkout 卻不會被還原,磁碟上留下的是 pointer 文字。三個地方看到三種「大小」,其實互不矛盾:

看的地方顯示為什麼
HF / GitHub 網頁42 MB查儲存後端,看得到 LFS 物件
git ls-tree -l HEAD(本地)44309759本地 checkout 裝了 git-lfs 且檔案內容正確
image 裡的檔案133 bytescheckout 沒跑 smudge,留下 pointer

連鎖後果 detect_cnn() 回傳 None → roi.detect() 靜默退回 Hough + 0.85 內縮 → 所有在 UI 建立的校準(含 校準_20260910_1635)都是 Hough 口徑(rel_threshold 0.860 而非 CNN 的 0.700)→ 同一顆試體 median_L=80:80×0.70=56.7 得 rel 0.86%、80×0.86=68.8 得 rel 20.55%,差 24 倍,分數就從 43.64% 變成 82.40%。

也就是說 CNN 從上線第一天就沒有生效過,這解釋了為什麼每次改版行為都一模一樣。

為什麼以前都沒發現 size 被 round(x/1048576, 1) 印成 0.0MB(看起來像「檔案很小但存在」);calibrations 表沒有任何口徑欄位;CNN 失效只是 roi.detect() 裡的一行 logger.warning。

防線(三層,2026-09-11 補上)

  1. 1.建置時:Dockerfile 在 COPY . . 之後執行 RUN python tools/verify_cnn_weights.py(ENV ALLOW_CNN_UNAVAILABLE=0)。偵測到 pointer/截斷/缺檔就直接讓 build 失敗。要刻意允許無 CNN 才設 ALLOW_CNN_UNAVAILABLE=1。
  2. 2.啟動時:roi_cnn.inspect_checkpoint() 會讀檔頭與 zip 目錄,辨識 LFS pointer、過小檔、zip 尾端 central directory 不見(截斷)。啟動 log 一律印 bytes 不印 MB:… exists=True bytes=44309759 size=42.3MB lfs_pointer=False;有問題時同一行直接附 ⚠️ 原因。self_check(load=False) 不必真的載入就能給出原因。
  3. 3.UI/校準時:系統頁「🧠 ROI 偵測口徑」會把 LFS pointer 畫成獨立的 🪤 錯誤框(附 44,309,759 bytes 的期望值與修法),並提供「🔍 實際載入權重驗證」按鈕。校準則由 _resolve_roi_basis() 守門,CNN 沒生效就拒絕寫入 Hough 口徑的校準(除非明確傳 allow_non_cnn=True),錯誤訊息會說明差數十個百分點。

第 1 層第一次上線就抓到問題(HF build cf79a65):

--> COPY . .
DONE 0.0s

--> RUN python tools/verify_cnn_weights.py
  checkpoint         /app/cement_core/roi_cnn_weights/best.pt
  checkpoint_bytes   133
  checkpoint_mb      0.0
  lfs_pointer        True
  head               b'version https://git-lfs.github.com/spec/v1\noid s'
!! CNN ROI 不可用!!

--> ERROR: process "/bin/sh -c python tools/verify_cnn_weights.py" did not complete successfully: exit code: 1

修法(在部署用的 repo 上做,也就是 HF Space,做完重新部署)

sh
echo '*.pt filter=lfs diff=lfs merge=lfs -text' >> .gitattributes
git add .gitattributes cement_core/roi_cnn_weights/best.pt
git commit -m "track *.pt with git-lfs"
git push

推送端要裝 git-lfs。推送後 git ls-tree -l HEAD -- …/best.pt 會顯示 133 bytes,這是正確的 —— 實體檔案改存在 LFS 物件庫,checkout 時才會還原(本 repo 的 .gitattributes 已於 2026-09-11 補上 *.pt / *.pth 規則)。

部署檢查清單(每次改到權重都要做)

  • —部署用的 repo(HF Space).gitattributes 要有 *.pt filter=lfs diff=lfs merge=lfs -text,且推送端裝有 git-lfs。
  • —build log 的 RUN python tools/verify_cnn_weights.py 要印 checkpoint_bytes 44309759、lfs_pointer False、load OK,並以 exit 0 通過。
  • —啟動 log 要是 bytes=44309759 lfs_pointer=False(不是 size=0.0MB)。
  • —按一次「🔍 實際載入權重驗證」,要顯示 load OK。
  • —重新建立校準,版本列表要顯示 ROI口徑 CNN(不是 Hough)。帶著舊的 Hough 口徑校準去跑,數字會差數十個百分點。若顯示「未標記」請先看下一節,它不等於沒問題。
  • —網頁顯示 42 MB 不能當成通過的證據;要看 build log 裡的 checkpoint_bytes 或啟動 log 的 bytes=。

校準口徑「未標記」是什麼意思(2026-09-11 補上)

「未標記」不是新問題,是「這筆校準建立得比口徑機制還早」。 calibrations 表沒有口徑欄位(見 sql/00_supabase_schema.sql),所以 v19 以前的校準只能靠寫在 description 裡的 |ROI口徑:xxx 標記來辨識;沒有這個標記時,calib_basis_text() 就回「未標記」。

⚠️ 重點:CNN 成功載入 ≠ 分數會變對。 決定換算結果的是校準曲線的口徑,不是 CNN 有沒有載入。而「未標記」的校準幾乎都是 Hough 口徑 —— 那正是同一顆試體從 44% 變 82% 的組合。所以修好 LFS、CNN 能載入之後,分數不會自己變,一定要重新校準。

為了不讓使用者卡在「未標記」三個字上,FloatingBlackDetector.infer_roi_basis() 會用校準自己的數字反推口徑:

口徑100% 標準片的 `rel`優化後 `rel_threshold`
CNN(ROI 內縮 1.0)約 10~20%0.60~0.74(典型 0.700)
Hough(內縮 0.85)約 30~37%0.82~0.90(典型 0.860)

兩個指紋一致才下結論;互相矛盾或落在中間地帶就回 None,不猜。結果直接寫進顯示字串:

未標記(v19 以前建立;依 100% 片 rel=34.4%、rel_thr=0.860 推測為 hough 口徑)
未標記(v19 以前建立;依 100% 片 rel=14.1%、rel_thr=0.700 推測為 cnn 口徑)
未標記(v19 以前建立;無 anchors 可用 → 無法判斷口徑)

calib_basis_info() 另外提供 effective(未標記時以推測值代入)與 risky(CNN 啟用中、有效口徑不是 cnn)給 UI 判斷。風險判定採寬鬆側:推測不出來(None)也算 risky,因為「不知道」不該被當成「安全」。

log 級別:未標記且推測為 Hough → logger.error(不要混在 warning 裡被忽略);未標記但推測為 CNN → logger.warning(與目前基準一致);已標記為 CNN → 不說話。

UI:校準版本列的標題會顯示 未標記(推測 hough),展開後列出推測依據;若那筆剛好是目前載入的校準,會多一段說明:「CNN 已能載入,但曲線還是 Hough 口徑的,所以同一顆試體的分數不會變(IMG_4477 仍會得到 ~82%,正確約 44%),請重新校準」。

要做的事:用 5 張標準片重新校準。成功訊息應出現 ROI 口徑:cnn,且該筆的 rel_threshold 會落在 ~0.70(不是 0.86)。

修改指南

想做什麼改哪裡
調整演算法閾值config.py
換 ROI 演算法roi.py 的 detect()
換資料庫新增 database_xxx.py,介面一致即可
加新分析項目measurement.py 加函數 → models.py 擴充 → detector.py 補呼叫
加新帳號config.py 的 ACCOUNTS + ACCOUNTSTORAGEKEYS 各加一行
改 spline 演算法calibration.py 的 smooth_isotonic_fit()
調整陰影/白紙偵測config.py 的 SHADOW_*、WHITE_ADAPTIVE_*、WHITE_EDGE_DIST_MAX_RATIO_RELAX
調整曝光正規化/ROI 內縮config.py 的 ENABLE_EXPOSURE_NORM、EXPOSURE_*、ROI_MEASURE_SHRINK(Hough)、ROI_MEASURE_SHRINK_CNN(CNN)
調整「偵測範圍」標註外觀(顏色/字距/膠囊/位置)ui_overlay.py 開頭的常數;字型輪廓重生成用 tools/gen_glyph_outline.py(需開發機裝 fontTools)。字樣定義在該工具的 DEFAULT_TEXT
調整「偵測範圍異常回報」的原因選項streamlit_app.py 的 ROI_ANOMALY_REASONS(三個回報入口共用同一個常數)
調整「非 CNN 口徑」的警示文字/嚴重度streamlit_app.py 的 _roi_method_warning()(唯一來源;cnn 不警示、fallback 為 error 級、其餘為 warning 級)
調整口徑的中文標籤(hough_refined → 「Hough(精修)」等)cement_core/roi.py 的 METHOD_LABELS(method_label() 為唯一入口,未知名稱原樣顯示、None/NaN 顯示「未知」)
調整反光/遠近偵測config.py 的 SPECULAR_*、ROI_BG_*(+ measurement.specular_severity)
趨勢頁門檻/開關config.py 的 TREND_ALERT_THRESHOLD、ENABLE_TREND_PAGE
切換 ROI 偵測方式(CNN/Hough)config.py 的 ENABLE_ROI_CNN;CNN 模型權重在 cement_core/roi_cnn_weights/best.pt,換模型要同步更新 dataset/02_cnn_roi_pilot/roi_cnn/best.pt(訓練腳本 dataset/02_cnn_roi_pilot/train_roi_unet.py)

資料庫 Migration

只有一個檔案要執行:`sql/00_supabase_schema.sql`(整合版 v18 + v19 + v20)——整檔貼進 Supabase SQL Editor / Cloud SQL 執行一次即可,新舊環境都適用、可重複執行,不需要再跑任何 migration。

sql/ 資料夾就只有這一個檔。原本依序執行的 01_migration_indexes.sql、02_migration_specular.sql、03_migration_v16.sql、04_migration_shadow.sql,以及放在專案根目錄 sql/ 的 migration_roi_bg.sql,內容已全部吸收進 00,原始檔已於 2026-09-10 刪除。若在舊文件或舊 commit 看到這些檔名,請直接忽略、不要再執行(例如 01 會把 00 刻意解除的 images.file_hash 唯一索引加回來)。v18 遠近/框選的完整說明(程式端 4 處改動、歷史回填)在 00 的 §7。

⚠️ 部署順序:先在 DB 跑完 `00_supabase_schema.sql`,再部署新版程式。 v20 又動了一次 schema(且這一項特別難察覺):predictions 再新增 roi_anomaly_source ('system' 系統自動 / 'user' 人工,16 → 17 欄)。這一欄同樣已納入 query_history() / query_trend() 的 SELECT,所以沒先加欄就上程式,「歷史紀錄」頁與趨勢頁的「測試詳細」視窗會整頁失敗。 更隱蔽的是:系統自動回報是包在 try/except 裡的(回報失敗不能讓一筆成功的預測變成「處理失敗」), 所以它不會在畫面上報錯,只會留一行 log 自動異常回報失敗——這是本版最容易被漏掉的一項。 要確認是否有漏,請看 §8 的 roi_method非cnn近7日筆數 > 0 但 系統自動異常筆數 = 0 這種組合。 v19 提醒仍有效:predictions 新增 roi_anomaly / roi_anomaly_note / roi_anomaly_time 三欄,外加一個部分索引 idx_predictions_roi_anomaly。 沒先跑 SQL 就上程式的話,「偵測範圍異常回報」送出時會直接因 column "roi_anomaly" does not exist 而失敗,而歷史紀錄頁也會整頁查詢失敗 (query_history() 的 SELECT 已包含這三欄);症狀是「回報按下去就報錯」而不是「App 起不來」。 舊的 v18 提醒仍有效:v18 新增的 measurements.quality_roi_bg_ok / quality_roi_bg_ratio 兩欄(measurements 共 33 欄)在 database.insert_measurement() 有寫入,DB 尚未加欄就上程式 的話所有量測寫入都會失敗。既有環境的 748 筆舊資料仍是 DEFAULT 0(= 缺值,不是「沒問題」), 回填方式見 00 §7。00 是冪等整合版,三個版本的變更一起跑一次即可。
ℹ️ `CORE_VERSION` 與 schema 版號是兩條獨立的線,但「v20」是兩邊共用的編號: CORE_VERSION(目前 20.2.0)指的是「量測基準 + 顯示層」的程式版本,不代表 DB 需不需要更新。 20.1.0 只修 calibrate() 的白平衡預設,20.2.0(見上方近期更新)只拿掉低向 abs 防呆的 w_eff > 0.5 閘門,兩者都完全沒有 schema 變更。 v19 / v20 除了程式改動,確實也各帶了 predictions 的新欄位(見上),所以請以 「00_supabase_schema.sql 跑過了沒」為準,不要用 CORE_VERSION 推論 schema 狀態。 另外,00 末尾的 §8 總驗收會檢查 predictions欄位_應為17,並一併輸出 已標記異常筆數 / 系統自動異常筆數 / 人工回報異常筆數 / 異常筆數缺來源_應為0 / roi_method非cnn近7日筆數。v20 把這一段由 19 欄增為 23 欄(_應為N 斷言由 10 個增為 11 個)—— 所以 sql/Supabase Snippet Result.csv 在那之前是 19 欄、之後會變 23 欄, 舊的 19 欄 CSV 與現行 §8 對不起來是預期的,不是 CSV 壞掉、也不是驗收失敗。 請在 Supabase 重跑一次 §8 並重新匯出覆蓋。 ⚠️ 別把 §8 的 `roi_bg疑似誤框筆數_應為0` 當成「人工回報數」:那一欄讀的是 measurements.quality_roi_bg_ok,是系統自動從內盤白紙比例算出來的;人工回報寫的是 predictions.roi_anomaly。所以回報了異常之後,§8 的疑似誤框筆數仍然是 0,這是正常的, 不代表回報沒寫進去。要確認人工回報有沒有落庫,請執行 00 末尾那段「v19 選配查詢」 (看 已回報異常筆數 / 有寫備註筆數 / 最後回報時間);v20 之後請改看 §8 內建的 系統自動異常筆數(source='system')與 人工回報異常筆數(source='user')——兩者要分開看, 前者高不代表有人反映框錯,只代表最近有很多筆不是走 CNN 口徑。那段選配查詢刻意保持註解狀態—— Supabase SQL Editor 只顯示最後一個 SELECT 的結果,取消註解會把 §8 的驗收結果蓋掉。
🧪 `診斷檔/_diag_methodwarn.py`(v20 離線回歸測試):本機 .venv 沒有 streamlit / pandas / supabase,跑不起 App,所以 v20 的驗證是用這支腳本完成的——它用 ast 從 streamlit_app.py 取出真正的 `_roi_method_warning()` 原始碼來執行(不是複製一份來測),用 object.__new__ + monkeypatch _exec 攔截 SQL(不連 DB),並直接測「口徑標記寫進去後讀得回來」。 目前 全部 PASS,報告在 診斷檔/_diag_methodwarn.txt。

技術維護

實際推廣遇到的技術問題、診斷過程與修正邏輯,整理於 `README_技術維護.md`:

  • —白背景大片陰影導致白平衡被跳過、結果異常 → 白紙偵測兩段式 + 背景陰影警告
  • —Storage 檔名改為時間戳前置,方便依時間排序查找
  • —遠近拍攝結果差異巨大 → 主因為 ROI 含杯緣暗環 + 白紙觸邊條件 → ROI 內縮 + 曝光正規化 + 放寬白紙偵測 + 遠近警告
  • —反光嚴重卻只警示 ~1% 或漏偵測 → 反光改在 ROI 內盤量測 + 分級
  • —CNN ROI 圈到極端小/暗區域時,abs 遠超出校準範圍,外推方向不可靠、肉眼看很黑卻判 0% → 外推防呆改成依 w_eff(該指標對預測值的實際貢獻)決定是否套用,避免 rel/abs 矛盾時被判斷順序決定結果
⚠️ 重新校準提醒:ROI 內縮、曝光正規化、切換 CNN/Hough ROI 都會改變量測基準,更新後請以標準片在 App「校準管理」重新校準,欄位預測才準確。目前 CNN ROI 上線沿用舊校準(未重新校準),已知在中高濃度區段會系統性偏高,見上方「CNN ROI 上線」小節。
🚨 2026-09-10 起量測基準已變更(v19):CNN 路徑取消二次內縮(ROI_MEASURE_SHRINK_CNN 0.85 → 1.0)。 舊的預測值與新值不可直接比較(predictions.calibration_id 可分辨新舊基準)。 上線後需以 5 張標準片重新校準;A/B 實測建議新值約 rel_threshold ≈ 0.700、abs_threshold = 75.0(重跑:dataset/02_cnn_roi_pilot/ab_cnn_shrink.py,證據存於 dataset/02_cnn_roi_pilot/ab_cnn_shrink_result.txt)。