實戰(zhàn):預處理、輪廓篩選與軌跡欄調(diào)參)
簡介這份文檔面向計算機視覺入門者與本科課程設(shè)計學生主題為基于OpenCV的物體計數(shù)與表單信息系統(tǒng)解決圖像中目標自動框選、計數(shù)及檢測信息表格化記錄的問題可服務于零售、安防、交通監(jiān)控等場景的入門實踐也可作為軟件技術(shù)項目設(shè)計的報告范例。壓縮包約19.51MB內(nèi)含1個docx正文覆蓋任務書、摘要、需求分析、系統(tǒng)功能模塊圖、實現(xiàn)過程、學生日志與綜合評價表完整呈現(xiàn)從圖像預處理、邊緣檢測、目標檢測到軌跡欄參數(shù)調(diào)節(jié)、圖片與視頻雙模式及控制臺交互檢測的推進脈絡(luò)并附答辯評分維度方便對照梳理報告結(jié)構(gòu)與復現(xiàn)思路。目前已有215人學習適合需要理解輪廓檢測、Canny、Haar級聯(lián)等基礎(chǔ)方法并參考課程設(shè)計寫作與項目排期的讀者。1. 從一張數(shù)不準的零件圖說起OpenCV 物體計數(shù)為什么先調(diào)預處理產(chǎn)線上要數(shù)一批散放的零件攝像頭架好、畫面也清楚但腳本一跑兩個挨著的零件被數(shù)成一個換個工位光照一變閾值就全廢。這類問題往往不是換個模型能解決的先把 OpenCV 的預處理鏈路和計數(shù)判定規(guī)則理清楚比直接上深度學習見效更快。這份課程設(shè)計做的就是這件事基于 OpenCV 的物體計數(shù)與表單信息系統(tǒng)用軌跡欄實時調(diào)參用輪廓加外接矩形把物體框出來計數(shù)按s鍵把時間、文件名、數(shù)量寫進 Excel。全部邏輯收在一個tally.py里分實時檢測、圖片檢測、控制臺交互三種模式。適合剛接觸 OpenCV、需要一套能跑通又能改參數(shù)的計數(shù)基線的人也適合拿它當課程設(shè)計或小工具原型的從業(yè)者。2. 環(huán)境與模塊骨架tally.py 該拆成哪幾層、軌跡欄怎么掛2.1 依賴與版本把 opencv-python 釘在 4.x這套代碼對 OpenCV 版本敏感最典型的是cv2.findContours的返回值個數(shù)。3.x 返回三個值image、contours、hierarchy4.x 只返回兩個contours、hierarchy。如果你從舊環(huán)境拷代碼過來第一處報錯大概率在這里。所以先把版本釘死虛擬環(huán)境里裝python -m venv venv # Windows venv\Scripts\activate # Linux / macOS source venv/bin/activate pip install opencv-python4.9.0.80 pip install pandas2.1.4 openpyxl3.1.2 numpy1.26.2opencv-python選主包就夠了這個項目只用到imgproc、highgui、videoio三個模塊。opencv-contrib-python里的 SIFT 那套在方案對比階段試過最終沒走特征匹配路線裝上只是增加體積。openpyxl必須顯式安裝pandas.to_excel寫.xlsx時靠它做引擎不裝會在運行時報ImportError: Missing optional dependency openpyxl。numpy和 OpenCV 有 ABI 依賴關(guān)系1.26 與 4.9 搭配比較穩(wěn)別隨手升到 2.x。提示如果機器上已經(jīng)有別的項目在用 OpenCV 3.x不要動全局環(huán)境給這套代碼單獨建 venv否則改一個版本會連帶把老項目搞崩。2.2 模塊劃分六個職責一個文件課程設(shè)計規(guī)模不大全部塞進tally.py是合理的拆成包反而多出一堆 import 路徑問題。但函數(shù)邊界要清楚后面想把檢測層換成模型只改一處就行。模塊關(guān)鍵函數(shù) / 入口輸入 → 輸出職責采集data_collection.py主循環(huán)、cv2.VideoCapture攝像頭幀 → 圖片文件拍照并落盤按s保存預處理preProcessing(img)BGR 圖 → 單通道邊緣圖灰度、模糊、Canny、膨脹、閉運算檢測計數(shù)findContours(img, imgPre, ...)原圖 邊緣圖 → 畫框圖、輪廓列表、數(shù)量篩輪廓、畫框、計數(shù)表單寫入readPath、excelmation、ImagePath數(shù)據(jù)字典 → Excel 行路徑解析、追加寫入交互軌跡欄回調(diào)、cv2.waitKey鼠標/鍵盤 → 參數(shù)值實時調(diào)參、保存觸發(fā)調(diào)度控制臺main()菜單輸入 → 模式分支實時 / 多圖 / 單圖切換readPath(folder_path)負責把文件夾里的圖片路徑讀成列表只保留.jpg、.png后綴避免把臨時文件混進去。ImagePath(recongnitionMode)根據(jù)模式字符串長度決定保存目錄實時模式和多圖模式寫到不同文件夾防止圖片和表格互相覆蓋。excelmation(excel_path, data)是寫入入口輸入是一個字段有序的字典。這幾個函數(shù)看著瑣碎但它們是「檢測結(jié)果能不能變成可查數(shù)據(jù)」的關(guān)鍵比檢測本身更容易翻車。常見的目錄結(jié)構(gòu)是這樣project/ tally.py # 核心模塊 data_collection.py # 拍照腳本 images/ # 采集圖 output/ realtime/ # 實時模式截圖 multi/ # 多圖模式結(jié)果 records.xlsx # 表單2.3 軌跡欄綁定先建窗口再掛滑塊軌跡欄是這套系統(tǒng)能做參數(shù)實時修正的核心。它掛在窗口上窗口名必須和createTrackbar的目標完全一致包括大小寫和空格。順序錯了會直接拋cv2.error: ... window not found。import cv2 import numpy as np WINDOW counter def nothing(x): pass # 必須先建窗口再創(chuàng)建軌跡欄 cv2.namedWindow(WINDOW, cv2.WINDOW_NORMAL) cv2.resizeWindow(WINDOW, 960, 540) cv2.createTrackbar(Threshold1, WINDOW, 30, 255, nothing) cv2.createTrackbar(Threshold2, WINDOW, 80, 255, nothing) cv2.createTrackbar(MinArea, WINDOW, 1000, 20000, nothing) cv2.createTrackbar(BoxScale, WINDOW, 50, 100, nothing) # 實際值 讀數(shù) / 100邏輯上createTrackbar的第四個參數(shù)是滑塊最大值第五個是回調(diào)函數(shù)。回調(diào)體可以是空的因為讀取靠getTrackbarPos輪詢回調(diào)只在需要「拖動即觸發(fā)」的場景下才寫邏輯。BoxScale之所以用 0 到 100 的整數(shù)是因為軌跡欄只接受整數(shù)實際使用時除以 100 得到 0.0 到 1.0 的比例。參數(shù)上限不是隨便給的。Threshold1、Threshold2的上限 255 對應 Canny 的 8 位輸入范圍MinArea上限 20000 是按 640×480 畫面里最大物體約占 16000 像素估的如果換成 1280×720上限要同步放大到 60000 左右否則滑塊一拉就頂?shù)筋^調(diào)不出有效區(qū)間。主循環(huán)的骨架大致是while True: ok, frame cap.read() if not ok: break imgPre preProcessing(frame) imgBox, conFound, total findContours(frame, imgPre, minAreacv2.getTrackbarPos(MinArea, WINDOW)) cv2.putText(imgBox, fcount{total}, (20, 40), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 0, 255), 2) cv2.imshow(WINDOW, imgBox) key cv2.waitKey(1) 0xFF if key ord(s): # 保存截圖并寫一行表單 pass elif key ord(q): break每幀都重新讀一遍軌跡欄這是參數(shù)能「實時生效」的原因也是后面跨圖調(diào)參殘留問題的根源第 5 章會細說。waitKey(1)里的 1 是毫秒給 0 會阻塞到按鍵視頻流就卡住了 0xFF是為了消掉某些平臺上返回值的高位不寫這一句在部分環(huán)境里按s沒反應。3. 預處理鏈路高斯模糊、Canny 雙閾值與形態(tài)學閉運算的先后順序3.1 preProcessing 的固定順序預處理函數(shù)看著只有幾行但順序換了結(jié)果差很多這條鏈路基本是玄學重災區(qū)。項目里的實現(xiàn)是灰度化 → 高斯模糊 → Canny → 膨脹 → 閉運算每一步都為下一步服務。def preProcessing(img): imgGray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 核 5x5sigmaX 顯式給 3模糊強度不隨核大小漂移 imgBlur cv2.GaussianBlur(imgGray, (5, 5), 3) t1 cv2.getTrackbarPos(Threshold1, WINDOW) t2 cv2.getTrackbarPos(Threshold2, WINDOW) imgCanny cv2.Canny(imgBlur, t1, t2) kernel np.ones((3, 3), np.uint8) # 先膨脹接斷口再閉運算填內(nèi)部小洞 imgDil cv2.dilate(imgCanny, kernel, iterations1) imgClose cv2.morphologyEx(imgDil, cv2.MORPH_CLOSE, kernel, iterations1) return imgClose灰度化丟掉顏色是因為計數(shù)場景里判據(jù)通常是形狀和面積不是顏色。高斯模糊的sigmaX給 0 時 OpenCV 會按核大小自動推算這里顯式寫 3 是為了讓模糊強度固定下來換核大小的時候不至于一起漂。Canny 的雙閾值分工是低閾值決定邊緣的延續(xù)高閾值決定邊緣的起點兩者比值落在 1:2 到 1:3 之間比較穩(wěn)。膨脹一次把 Canny 斷開的邊緣接上閉運算填掉物體內(nèi)部的小空洞避免一個物體被拆成兩個輪廓。參數(shù)上(5, 5)和sigmaX3是一組匹配值核半徑約 2sigma 取 1.5 到 3 都合理。kernel用 3×3 是起點如果物體邊緣在 640 寬度下斷口超過 6 像素可以換 5×5但迭代次數(shù)保持 1。迭代次數(shù)加到 2 以上相鄰物體之間的窄縫會被填平兩個物體粘成一個計數(shù)直接少一半。3.2 雙閾值怎么調(diào)先定高閾值再放低閾值調(diào)參順序很關(guān)鍵反過來調(diào)會一直在噪聲里打轉(zhuǎn)。我的習慣是先用 Single-image 模式加載一張有代表性的圖按下面的步驟走把Threshold2拉到 150 以上觀察邊緣是否只剩物體外輪廓背景紋理應該基本消失。逐步降Threshold2直到物體輪廓連續(xù)但背景還沒起大片噪點記下這個值。把Threshold1從 30 開始往上加觀察輪廓是否閉合成環(huán)閉合即可停。切換到 Multi-image 跑 20 到 30 張看計數(shù)波動波動大的圖回單圖模式復查。不同場景的起點值差別不小可以拿這張表做參考再按實際畫面微調(diào)場景Threshold1Threshold2說明室內(nèi)白底小零件2570對比度高閾值可以壓低逆光傳送帶45120背景亮高閾值要抬高抑制反光低對比度泡沫件1550邊緣弱雙閾值都要降深色臺面金屬件3590反光點多配合面積過濾只調(diào)一個閾值的后果很明確Threshold2過高邊緣斷裂輪廓面積偏小會被minArea整片篩掉Threshold1過低背景噪點連成片閉運算之后糊成一個大塊計數(shù)虛高。3.3 形態(tài)學核尺寸與物體間隙的關(guān)系膨脹和閉運算都用同一個 3×3 核看起來省事但這里有個硬約束核的直徑必須小于相鄰物體的最小間隙。3×3 核膨脹一次物體邊緣向外擴約 1 像素兩個物體間距小于 2 像素時就會粘上。如果現(xiàn)場零件挨得很近要么把核降到 2×2要么在預處理前先做一次腐蝕把間隙拉開代價是弱邊緣會丟。閉運算的作用和膨脹不同它先膨脹后腐蝕填的是輪廓內(nèi)部的小孔對外輪廓的擴張比純膨脹小。順序不能反先閉運算再膨脹孔是填了但后續(xù)膨脹仍會把物體撐大先膨脹再閉運算斷口接上之后閉運算只處理內(nèi)部外形更接近真實邊界。注意形態(tài)學操作對二值圖有效Canny輸出是單通道 8 位圖值只有 0 和 255可以直接進dilate。如果中間插了一步歸一化或者浮點轉(zhuǎn)換形態(tài)學函數(shù)會報類型錯誤。3.4 拍照腳本與 ROI 遮罩圖像檢測模式的素材不是隨手拍的采集階段就要把背景干擾壓下去。data_collection.py里用了三個輔助函數(shù)Bbox_img在畫面中央畫紅框maskBbox對框外做高斯模糊Adjusted_image調(diào)亮度。def Bbox_img(img, bbox_scale0.5, bbox_color(0, 0, 255)): h, w img.shape[:2] bw, bh int(w * bbox_scale), int(h * bbox_scale) x1, y1 (w - bw) // 2, (h - bh) // 2 cv2.rectangle(img, (x1, y1), (x1 bw, y1 bh), bbox_color, 2) return img, (x1, y1, bw, bh) def maskBbox(img, position): x, y, w, h position mask np.zeros(img.shape[:2], np.uint8) mask[y:y h, x:x w] 255 # 大核模糊目的是讓框外失去高頻信息不是去噪 blur cv2.GaussianBlur(img, (21, 21), 0) return np.where(mask[..., None] 255, img, blur) def Adjusted_image(img, brightness_factor1.5): # 查表比逐像素乘法快uint8 溢出由 min 截斷 table np.array([min(255, int(i * brightness_factor)) for i in range(256)], np.uint8) return cv2.LUT(img, table)Bbox_img返回的元組必須原樣傳給maskBbox否則掩膜和紅框錯位框外該糊的地方?jīng)]糊。bbox_scale取 0.5 表示框占畫面一半小零件調(diào)到 0.2 到 0.3讓物體盡量填滿框。GaussianBlur的 21×21 是刻意給的大核作用是讓框外的紋理徹底消失不是常規(guī)去噪核小了背景邊緣還會殘留。Adjusted_image用cv2.LUT而不是img * 1.5差別在視頻流里很明顯后者每幀都要產(chǎn)生一個浮點臨時數(shù)組再轉(zhuǎn)回uint8兩次內(nèi)存拷貝幀率會掉。brightness_factor1.5是經(jīng)驗值畫面偏暗時給 1.8過曝時降到 0.8靠第二個軌跡欄實時調(diào)。4. 從輪廓到計數(shù)findContours 封裝與最大邊界框的三個參數(shù)4.1 封裝函數(shù)的簽名與參數(shù)含義原生的cv2.findContours只負責找輪廓篩面積、篩形狀、畫框、排序都得自己補。tally.py里把它封成一個函數(shù)簽名帶了六個參數(shù)每個都對應一類現(xiàn)場問題。def findContours(img, imgPre, minArea1000, sortTrue, filter0, c(255, 0, 0)): conFound [] imgContours img.copy() # OpenCV 4.x 返回兩個值 contours, _ cv2.findContours(imgPre, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) for cnt in contours: area cv2.contourArea(cnt) if area minArea: # 粗篩噪點 continue peri cv2.arcLength(cnt, True) approx cv2.approxPolyDP(cnt, 0.02 * peri, True) # 多邊形近似 if filter 0 and len(approx) ! filter: # 按邊數(shù)篩形狀 continue x, y, w, h cv2.boundingRect(approx) cx, cy x w // 2, y h // 2 if 3000 area 16000: # 有效面積區(qū)間才畫框 cv2.rectangle(imgContours, (x, y), (x w, y h), c, 2) cv2.circle(imgContours, (cx, cy), 5, (0, 255, 0), -1) conFound.append({cnt: cnt, area: area, bbox: (x, y, w, h), center: (cx, cy)}) if sort: conFound sorted(conFound, keylambda x: x[area], reverseTrue) return imgContours, conFound, len(conFound)邏輯上有幾處取舍值得說。RETR_EXTERNAL只取最外層輪廓大物體內(nèi)部的小孔不會被當成獨立物體這是輪廓計數(shù)不虛高的前提。CHAIN_APPROX_NONE存下輪廓上的所有點內(nèi)存占用大但approxPolyDP的精度依賴點的密度用CHAIN_APPROX_SIMPLE會丟掉直線段上的中間點近似結(jié)果偏向多邊形頂點。0.02 * peri是近似精度值越小越貼合原始輪廓越大越接近規(guī)則多邊形。filter參數(shù)按邊數(shù)篩形狀filter4只留四邊形適合方形容器圓形物體邊數(shù)在近似后不穩(wěn)定別用這個參數(shù)會漏計。boundingRect傳入的是近似后的approx而不是原始cnt目的是讓毛刺不把外接框撐大。4.2 為什么從輪廓計數(shù)改成 boundingRect 計數(shù)初版是用len(conFound)直接當數(shù)量也就是數(shù)輪廓個數(shù)。實測下來無論怎么調(diào)軌跡欄誤差都壓不住一個物體的邊緣被 Canny 斷成兩段就數(shù)成兩個兩個物體在二值圖上粘連就數(shù)成一個背景反光形成的小閉合環(huán)面積過了minArea又被算進去。這些誤差不是調(diào)參能消掉的因為判據(jù)本身就不穩(wěn)。改成以boundingRect為單位計數(shù)后情況好轉(zhuǎn)每個輪廓只產(chǎn)生一個外接矩形計數(shù)以矩形為單位至少不會因為輪廓破碎而一個物體出兩個框。邊界也很清楚——兩個物體粘連時外接矩形會把它們框成一個大矩形仍然計成 1 個。所以這個方案的準確率上限由預處理決定核尺寸和閾值調(diào)不好后面怎么改計數(shù)邏輯都救不回來。改進后的寫法會把面積判據(jù)換成外接矩形面積再加一道長寬比過濾valid [] for cnt in contours: area cv2.contourArea(cnt) if area minArea: continue x, y, w, h cv2.boundingRect(cnt) rect_area w * h # 用外接矩形面積做二次篩選對邊緣毛刺不敏感 if not (3000 rect_area 16000): continue # 長寬比過濾排除條狀反光和細長噪聲 aspect w / float(h) if aspect 0.3 or aspect 3.0: continue valid.append({bbox: (x, y, w, h), area: rect_area}) total len(valid)contourArea對斷口敏感rect_area對外形毛刺不敏感兩個都用一遍相當于雙重過濾。長寬比這一道是針對傳送帶上的條狀反光加的正常零件長寬比落在 0.5 到 2.0 之間設(shè) 0.3 和 3.0 是留了余量。4.3 面積閾值區(qū)間的標定方法3000 到 16000 這個區(qū)間不是拍腦袋定的是從數(shù)據(jù)里打出來的。步驟可以照抄用 Single-image 模式加載一張典型圖把MinArea軌跡欄拉到 0讓所有輪廓都進列表。在循環(huán)里臨時打印每個輪廓的contourArea和w*h觀察目標物體的面積分布。取目標物體面積的 10 分位做下限、90 分位做上限上下限比值控制在 3 到 5 倍。用 Multi-image 跑 20 到 30 張人工核對計數(shù)誤差超過 2 的圖單獨拎出來復查。把區(qū)間寫進代碼或者配置文件軌跡欄上限同步調(diào)整。臨時打印的代碼片段# 標定階段打開跑完刪掉 for i, cnt in enumerate(contours): a cv2.contourArea(cnt) x, y, w, h cv2.boundingRect(cnt) print(f#{i} contour{a:.0f} rect{w*h} fbbox({x},{y},{w},{h}) aspect{w/h:.2f})區(qū)間太窄是常見誤用同一批零件因為距離鏡頭遠近不同面積能差一倍閾值卡在 5000 到 6000 之間就會漏掉遠處的小件。反過來區(qū)間太寬噪聲也放進來計數(shù)虛高。如果物體尺度變化確實很大面積閾值就不是好判據(jù)得換輪廓周長或者干脆上檢測模型。4.4 三種模式的調(diào)度控制臺入口把三種模式串起來實時模式走攝像頭多圖模式走文件夾批量單圖模式是多圖模式的子集用來先調(diào)參再批量跑。def main(): print(1) Real-time 2) Multi-image 3) Single-image) mode input(select: ).strip() if mode 1: cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) run_realtime(cap) elif mode in (2, 3): folder input(image folder: ).strip() paths readPath(folder) if mode 3: paths paths[:1] # 單圖只取第一張 run_batch(paths)Single-image存在的意義是省時間批量跑 30 張要幾分鐘單圖調(diào)參只要幾十秒調(diào)好之后再批量驗證避免跑完一批才發(fā)現(xiàn)閾值全錯。readPath返回的路徑列表要按文件名排序否則多圖模式的表單順序和人工核對順序?qū)Σ簧吓挪闀r很痛苦。5. 避坑與排查五個把計數(shù)結(jié)果帶偏的細節(jié)5.1 API 版本差異findContours 的返回值個數(shù)現(xiàn)象代碼在別人的機器上跑得好好的換一臺就報ValueError: too many values to unpack (expected 2)指向contours, hierarchy cv2.findContours(...)這一行。原因OpenCV 3.x 的findContours返回三個值第一個是處理后的圖像后兩個才是輪廓和層級4.x 把圖像那個返回值去掉了只剩兩個。版本不同解包個數(shù)就不同。解決不想釘版本就寫兼容寫法用負索引取輪廓兩個版本都能對上found cv2.findContours(imgPre, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) contours found[-2] # 4.x 取 contours3.x 也取 contoursfound[-2]在 4.x 里是第一個元素在 3.x 里是第二個元素正好都是輪廓列表。另一處版本差異是cv2.CAP_PROP_FRAME_WIDTH這類常量3.x 里叫CV_CAP_PROP_FRAME_WIDTH新代碼統(tǒng)一用帶CAP_前綴的寫法。5.2 窗口與參數(shù)軌跡欄的三個典型翻車點現(xiàn)象一啟動直接拋cv2.error: OpenCV(4.9.0) ... window not found。原因createTrackbar寫在namedWindow之前。軌跡欄必須掛到一個已經(jīng)存在的窗口上窗口沒建掛載就失敗。解決把namedWindow提到最前面所有createTrackbar跟在后面。切換圖片時不要重復調(diào)namedWindow用setTrackbarPos重置滑塊位置就行?,F(xiàn)象二上一張圖把Threshold1調(diào)到 60換一張圖后沒動滑塊計數(shù)全錯。原因軌跡欄是窗口級狀態(tài)不是每張圖獨立。批量模式下窗口只建一次滑塊值會一直沿用。解決單圖模式每次加載新圖后顯式把參數(shù)重置到默認值def reset_trackbars(defaults: dict): for name, val in defaults.items(): cv2.setTrackbarPos(name, WINDOW, val) reset_trackbars({Threshold1: 30, Threshold2: 80, MinArea: 1000})更穩(wěn)的做法是把每張圖調(diào)好的最優(yōu)參數(shù)連同文件名一起記進表單下次遇到同類圖直接查表設(shè)參不用重新試?,F(xiàn)象三按s沒反應或者要連按好幾下才觸發(fā)一次保存。原因cv2.waitKey(1)的返回值在某些平臺帶高位直接和ord(s)比較可能不相等另一種情況是waitKey寫在imshow之前窗口還沒刷新按鍵事件丟了。解決判斷寫成cv2.waitKey(1) 0xFF ord(s)并且保證imshow在前、waitKey在后。視頻流里waitKey的參數(shù)給 1 到 10 毫秒給 0 會阻塞。5.3 數(shù)據(jù)落盤中文路徑與 Excel 追加現(xiàn)象一readPath返回的路徑明明存在cv2.imread讀出來全是None后面cvtColor報斷言失敗。原因cv2.imread底層用 C 字符串打開文件路徑里有中文或特殊字符時打不開而且不拋異常靜默返回None。解決用numpy讀字節(jié)流再解碼寫文件同理def imread_unicode(path): data np.fromfile(path, dtypenp.uint8) return cv2.imdecode(data, cv2.IMREAD_COLOR) def imwrite_unicode(path, img): ext os.path.splitext(path)[1] ok, buf cv2.imencode(ext, img) if ok: buf.tofile(path) return okimdecode的第二個參數(shù)用IMREAD_COLOR保持三通道和后續(xù)處理鏈一致如果用IMREAD_UNCHANGED帶透明通道的 PNG 會變成四通道cvtColor的轉(zhuǎn)換碼要跟著改。現(xiàn)象二第二次按s保存打開表格發(fā)現(xiàn)只剩一行之前的數(shù)據(jù)沒了。原因DataFrame.to_excel默認modew每次寫都重建文件。哪怕傳的是已有文件路徑也是覆蓋。解決要么先讀舊表再concat要么直接用openpyxl追加行from openpyxl import load_workbook, Workbook def append_row(xlsx_path, row: dict): if os.path.exists(xlsx_path): wb load_workbook(xlsx_path) ws wb.active else: wb Workbook() ws wb.active ws.append(list(row.keys())) # 首行寫表頭 ws.append(list(row.values())) wb.save(xlsx_path)字典的鍵順序在 Python 3.7 以后是插入序表頭和數(shù)據(jù)的列能對上。追加前最好用絕對路徑查一遍已有表里的文件名避免同一張圖按兩次s寫兩行。這一點在批量模式里尤其重要重復行會讓后續(xù)統(tǒng)計的數(shù)量翻倍。6. 進階用法把表單反哺成數(shù)據(jù)集標注與參數(shù)回歸驗證表單寫順了之后它的價值就不只是「記錄一下」而是變成一份可回歸的驗證集。命名規(guī)則里如果帶上數(shù)量比如類別_日期_序號_數(shù)量.jpg文件名本身就是弱標注不用再手工標一遍。import pandas as pd df pd.read_excel(records.xlsx) # 從文件名尾部解析數(shù)量例如 part_20230618_001_12.jpg df[label] df[filename].str.extract(r_(\d)\.jpg$).astype(int) df[err] (df[count] - df[label]).abs() print(df.groupby(label)[err].agg([mean, max, count])) hard df[df[err] 2]err的均值看整體偏移max看最壞情況count看樣本量夠不夠。均值在 0.5 以內(nèi)、最大值不超過 2說明當前參數(shù)在這一批圖上基本可用hard里拎出來的圖回去用 Single-image 模式逐張看面積分布和閾值調(diào)完再批量跑一遍形成「調(diào)參 → 批量 → 回歸」的閉環(huán)。這一步比盲目拖滑塊有效得多因為每次改動都有量化反饋。再往下走可以把hard里的圖當成失敗集單獨統(tǒng)計它們的共同特征是面積偏小被minArea篩掉了還是長寬比超限被過濾了。如果是前者說明面積區(qū)間下限要往下放如果是后者說明長寬比閾值太緊。參數(shù)和失敗樣本對上號之后下一次換場景就有據(jù)可依不用從零試。從那以后我每次動面積閾值或者形態(tài)學核尺寸都強制先用 Single-image 把面積分布打印一遍再跑 20 張批量看err分布最后才把參數(shù)寫死。省下來的返工時間比調(diào)參本身多得多。希望幫到你。本文還有配套的精品資源點擊獲取