絡(luò)實(shí)戰(zhàn):人群密度估計(jì)從原理到落地)
1. 從一張擁擠的地鐵站照片說(shuō)起為什么需要多尺度卷積網(wǎng)絡(luò)第一次接觸人群密度估計(jì)這個(gè)方向是因?yàn)橐粋€(gè)做安防監(jiān)控的朋友吐槽他們部署的傳統(tǒng)檢測(cè)模型在廣場(chǎng)、地鐵站這種人擠人的場(chǎng)景里幾乎廢掉——人一多互相遮擋檢測(cè)框疊成一團(tuán)數(shù)出來(lái)的數(shù)字跟實(shí)際差了好幾倍。后來(lái)他們把思路從“先檢測(cè)再計(jì)數(shù)”換成了“直接回歸密度圖”效果才慢慢穩(wěn)住。這個(gè)轉(zhuǎn)變背后的核心工具就是MSCNNMulti-Scale Convolutional Neural Network多尺度卷積神經(jīng)網(wǎng)絡(luò)。人群密度估計(jì)要解決的問(wèn)題很具體給一張任意場(chǎng)景的圖片輸出一張和原圖同尺寸的密度圖密度圖上每個(gè)像素的值代表該位置的人群密度整張圖求和就是估計(jì)人數(shù)。它比單純的目標(biāo)檢測(cè)更適合密集場(chǎng)景因?yàn)椴恍枰衙總€(gè)人都框出來(lái)而是把“人群”當(dāng)成一種可以累積的信號(hào)來(lái)處理。MSCNN 在這個(gè)任務(wù)里的定位是解決尺度變化這個(gè)老大難問(wèn)題——近處的人占幾百個(gè)像素遠(yuǎn)處的人可能只有十幾個(gè)像素單一尺度的卷積核根本沒(méi)法同時(shí)照顧到。這篇文章適合三類人看一是剛?cè)腴T計(jì)算機(jī)視覺(jué)、想找一個(gè)完整項(xiàng)目練手的學(xué)生或轉(zhuǎn)行者二是做安防、交通、零售客流分析需要落地人群計(jì)數(shù)功能的工程師三是已經(jīng)用過(guò)一些密度估計(jì)模型但對(duì)多尺度結(jié)構(gòu)的設(shè)計(jì)邏輯還不太清楚想搞明白“為什么這么搭”的從業(yè)者。我會(huì)從整體設(shè)計(jì)思路講到具體實(shí)現(xiàn)細(xì)節(jié)再到實(shí)操中踩過(guò)的坑盡量把每個(gè)選擇背后的理由說(shuō)透讓你看完能自己復(fù)現(xiàn)一版也能判斷什么場(chǎng)景該用、什么場(chǎng)景不該用。需要提前說(shuō)明的是MSCNN 不是某一個(gè)固定不變的網(wǎng)絡(luò)而是一類多分支、多感受野結(jié)構(gòu)的設(shè)計(jì)思想。不同論文里的具體實(shí)現(xiàn)會(huì)有差異下面我講的是一套經(jīng)過(guò)實(shí)踐驗(yàn)證、結(jié)構(gòu)清晰、容易復(fù)現(xiàn)的通用方案涉及具體參數(shù)的地方我會(huì)說(shuō)明取值依據(jù)你可以根據(jù)自己的數(shù)據(jù)集調(diào)整。2. 整體設(shè)計(jì)與思路拆解多尺度到底在解決什么問(wèn)題2.1 單尺度模型的根本困境要理解 MSCNN 為什么這么設(shè)計(jì)得先看清楚單尺度模型栽在哪。假設(shè)你用一層 3×3 卷積核去處理一張人群圖它的感受野是固定的。對(duì)于近處一個(gè)占 200×200 像素的人3×3 的核只能看到這個(gè)人身上極小一塊皮膚紋理根本感知不到“這是一個(gè)人”對(duì)于遠(yuǎn)處一個(gè)占 15×15 像素的人3×3 的核又相對(duì)偏大容易把旁邊的人一起卷進(jìn)來(lái)導(dǎo)致密度估計(jì)糊成一片。這就是尺度不匹配同一個(gè)卷積核在小目標(biāo)上“太大”在大目標(biāo)上“太小”。傳統(tǒng)做法是圖像金字塔——把原圖縮成好幾個(gè)尺寸分別處理再融合但這樣計(jì)算量翻幾倍而且多尺度信息是事后拼接的不是網(wǎng)絡(luò)自己學(xué)出來(lái)的。MSCNN 的思路更聰明在同一層里并行放多個(gè)不同尺寸的卷積核讓網(wǎng)絡(luò)自己決定該信哪個(gè)尺度。2.2 多分支并行讓網(wǎng)絡(luò)自己選尺度具體怎么并行最直接的做法是在同一輸入上同時(shí)跑 3×3、5×5、7×7 三個(gè)分支每個(gè)分支后面接各自的非線性激活最后把三路特征在通道維度拼接concatenate起來(lái)。這樣輸出的特征圖里既有小感受野捕捉的細(xì)節(jié)適合小目標(biāo)也有大感受野捕捉的輪廓適合大目標(biāo)。這里有個(gè)關(guān)鍵設(shè)計(jì)取舍用大卷積核還是用堆疊小卷積核。7×7 的卷積核參數(shù)量是 3×3 的約 5.4 倍計(jì)算量也大得多。實(shí)踐中更常見(jiàn)的做法是用**空洞卷積dilated convolution**來(lái)擴(kuò)大感受野——比如用 dilation2 的 3×3 卷積實(shí)際感受野等效于 5×5但參數(shù)量還是 3×3 的。這樣既拿到了多尺度又控制了模型體積。我在實(shí)際項(xiàng)目里對(duì)比過(guò)同等參數(shù)量下空洞卷積版本比直接堆大核版本在密集場(chǎng)景的 MAE 上能低 3% 到 5%。提示多尺度分支不是越多越好。我試過(guò) 5 個(gè)分支結(jié)果參數(shù)量上去了精度反而因?yàn)槿哂嗵卣鞲蓴_掉了。3 到 4 個(gè)分支是比較穩(wěn)的甜點(diǎn)區(qū)。2.3 為什么是密度圖回歸而不是直接回歸人數(shù)有人會(huì)問(wèn)既然最終要的是人數(shù)為什么不直接讓網(wǎng)絡(luò)輸出一個(gè)數(shù)字原因在于監(jiān)督信號(hào)的密度。直接回歸人數(shù)一張圖只有一個(gè)標(biāo)簽網(wǎng)絡(luò)學(xué)起來(lái)信號(hào)太稀疏而且完全丟失了空間信息——它不知道人分布在哪只知道總數(shù)。密度圖回歸則給每個(gè)像素都提供了監(jiān)督網(wǎng)絡(luò)被迫去理解“哪里有人、有多密”泛化能力明顯更強(qiáng)。MSCNN 輸出的密度圖通常用高斯核卷積點(diǎn)標(biāo)注生成真值把每個(gè)人頭標(biāo)注成一個(gè)點(diǎn)然后用固定或自適應(yīng)方差的高斯核在這個(gè)點(diǎn)上“抹開(kāi)”形成一張平滑的密度圖。整張圖積分就是人數(shù)。這個(gè)設(shè)計(jì)的好處是即使網(wǎng)絡(luò)在某些像素上預(yù)測(cè)有偏差只要整體分布對(duì)積分出來(lái)的人數(shù)依然準(zhǔn)。這也是密度估計(jì)比檢測(cè)更魯棒的根本原因。3. 核心細(xì)節(jié)解析與實(shí)操要點(diǎn)網(wǎng)絡(luò)結(jié)構(gòu)逐層拆解3.1 前端特征提取別一上來(lái)就多尺度很多人一提到 MSCNN 就急著堆多尺度分支其實(shí)前端還是需要一個(gè)扎實(shí)的基礎(chǔ)特征提取網(wǎng)絡(luò)。我的做法是先用幾層標(biāo)準(zhǔn)卷積比如 VGG 的前 10 層去掉全連接把圖像從 RGB 降到高維特征通道數(shù)從 3 升到 64、128、256。這幾層的作用是提取邊緣、紋理這些底層特征為后面的多尺度分支提供“原料”。這里有個(gè)容易忽略的點(diǎn)前端不要用太大的下采樣。人群密度估計(jì)對(duì)空間分辨率很敏感如果你像分類網(wǎng)絡(luò)那樣一路 max pooling 到 1/32 分辨率小人早就沒(méi)了。我一般控制總下采樣倍數(shù)在 8 倍以內(nèi)也就是最終特征圖是原圖的 1/8。再往上采樣回原圖尺寸時(shí)用雙線性插值就夠了不需要轉(zhuǎn)置卷積因?yàn)槊芏葓D本身是平滑的不需要太銳利的細(xì)節(jié)。3.2 多尺度模塊的具體搭建多尺度模塊我通常放在前端特征之后作為網(wǎng)絡(luò)的“中段”。結(jié)構(gòu)上分三路小尺度分支一個(gè) 3×3 卷積dilation1負(fù)責(zé)捕捉局部細(xì)節(jié)對(duì)應(yīng)遠(yuǎn)處的小人頭。中尺度分支一個(gè) 3×3 卷積dilation2感受野等效 5×5對(duì)應(yīng)中距離人群。大尺度分支一個(gè) 3×3 卷積dilation3感受野等效 7×7對(duì)應(yīng)近處的大人頭和人群團(tuán)塊。三路輸出通道數(shù)保持一致比如都是 128然后在通道維拼接成 384 通道。拼接后再接一個(gè) 1×1 卷積把通道壓回 128起到特征融合和降維的作用。這個(gè) 1×1 卷積很關(guān)鍵它相當(dāng)于一個(gè)可學(xué)習(xí)的加權(quán)融合讓網(wǎng)絡(luò)自己決定每個(gè)位置該更依賴哪個(gè)尺度。分支卷積配置等效感受野適用目標(biāo)輸出通道小尺度3×3, dilation13×3遠(yuǎn)距離小人128中尺度3×3, dilation25×5中距離人群128大尺度3×3, dilation37×7近距離大人128融合1×1-加權(quán)整合1283.3 后端回歸與損失函數(shù)選擇多尺度模塊之后接兩層普通 3×3 卷積做進(jìn)一步非線性變換最后用一個(gè) 1×1 卷積輸出單通道密度圖。輸出層不加激活函數(shù)因?yàn)槊芏戎凳欠秦?fù)實(shí)數(shù)加了 ReLU 反而可能把一些接近零的區(qū)域壓死。實(shí)踐中我直接輸出線性值靠損失函數(shù)約束非負(fù)性。損失函數(shù)用MSE均方誤差是最常見(jiàn)的但純 MSE 在密度極低的區(qū)域大片背景會(huì)主導(dǎo)梯度導(dǎo)致網(wǎng)絡(luò)對(duì)高密度區(qū)域不夠敏感。我的改進(jìn)做法是加權(quán) MSE對(duì)密度值大于閾值的像素給更高權(quán)重。具體權(quán)重可以按密度值線性映射比如權(quán)重 1 密度值 × 10。這樣網(wǎng)絡(luò)會(huì)更關(guān)注有人區(qū)域。實(shí)測(cè)下來(lái)加權(quán) MSE 比純 MSE 在 MAE 上能再降 2% 左右。注意不要用交叉熵或分類損失。密度估計(jì)是回歸任務(wù)輸出是連續(xù)值分類損失會(huì)破壞密度圖的連續(xù)性積分出來(lái)的人數(shù)會(huì)跳變。4. 實(shí)操過(guò)程與核心環(huán)節(jié)實(shí)現(xiàn)從數(shù)據(jù)到訓(xùn)練全流程4.1 數(shù)據(jù)準(zhǔn)備與密度圖生成數(shù)據(jù)這塊公開(kāi)數(shù)據(jù)集有幾個(gè)常用的但我不提具體名字你按自己場(chǎng)景找標(biāo)注數(shù)據(jù)就行。核心是每張圖要有人頭點(diǎn)標(biāo)注格式一般是 (x, y) 坐標(biāo)列表。拿到標(biāo)注后第一步是生成密度圖真值。生成邏輯初始化一張全零圖尺寸和原圖一致。對(duì)每個(gè)標(biāo)注點(diǎn)以該點(diǎn)為中心放一個(gè)高斯核。高斯核的方差怎么定固定方差比如 σ15在稀疏場(chǎng)景夠用但密集場(chǎng)景會(huì)糊成一片。更穩(wěn)的做法是自適應(yīng)方差根據(jù)每個(gè)人到最近 K 個(gè)鄰居的平均距離來(lái)定 σ。距離大說(shuō)明人稀疏σ 可以大一點(diǎn)距離小說(shuō)明人密集σ 要小一點(diǎn)避免密度圖過(guò)度重疊。import numpy as np from scipy.ndimage import gaussian_filter def generate_density_map(image_shape, points, k3): density np.zeros(image_shape[:2], dtypenp.float32) if len(points) 0: return density # 計(jì)算每個(gè)點(diǎn)的自適應(yīng)sigma from scipy.spatial import cKDTree tree cKDTree(points) dists, _ tree.query(points, kmin(k1, len(points))) avg_dists dists[:, 1:].mean(axis1) sigmas np.clip(avg_dists * 0.3, 4, 30) for (x, y), sigma in zip(points, sigmas): # 在局部窗口內(nèi)放高斯 tmp np.zeros_like(density) tmp[int(y), int(x)] 1 tmp gaussian_filter(tmp, sigmasigma) density tmp return density這段代碼里sigmas的系數(shù) 0.3 和上下限 4、30 是我調(diào)出來(lái)的經(jīng)驗(yàn)值。系數(shù)太大密度圖會(huì)太散太小又太尖。你可以先按這個(gè)跑看可視化效果再微調(diào)。4.2 訓(xùn)練配置與參數(shù)選擇優(yōu)化器用Adam學(xué)習(xí)率初始 1e-4每 20 個(gè) epoch 衰減 0.5。Batch size 看顯存8 到 16 都行。輸入圖像我一般隨機(jī)裁剪成 256×256 的 patch 來(lái)訓(xùn)練這樣既能增加樣本多樣性又能控制顯存。注意裁剪時(shí)要保證 patch 里至少有一定數(shù)量的人頭全空的 patch 對(duì)訓(xùn)練沒(méi)幫助我一般要求至少 5 個(gè)點(diǎn)。訓(xùn)練輪數(shù)上小數(shù)據(jù)集 200 到 300 epoch 差不多收斂大數(shù)據(jù)集可以到 500。早停策略看驗(yàn)證集 MAE連續(xù) 30 epoch 不降就停。這里有個(gè)坑驗(yàn)證集 MAE 和測(cè)試集 MAE 可能差很多因?yàn)椴煌瑘?chǎng)景的人群分布差異大。我一般會(huì)留一個(gè)和訓(xùn)練集場(chǎng)景不同的驗(yàn)證集這樣早停出來(lái)的模型泛化更好。4.3 推理與人數(shù)計(jì)算推理階段把整張圖或按滑動(dòng)窗口切塊送進(jìn)網(wǎng)絡(luò)得到密度圖后直接求和就是人數(shù)。如果是大圖滑動(dòng)窗口切塊時(shí)要注意重疊區(qū)域的處理相鄰窗口重疊 1/4 到 1/2重疊區(qū)的密度值取平均避免邊界處人數(shù)被重復(fù)計(jì)算或漏算。def predict_count(model, image, window_size256, stride128): h, w image.shape[:2] density np.zeros((h, w), dtypenp.float32) count_map np.zeros((h, w), dtypenp.float32) for y in range(0, h, stride): for x in range(0, w, stride): y2 min(y window_size, h) x2 min(x window_size, w) patch image[y:y2, x:x2] # 補(bǔ)齊到window_size ph, pw patch.shape[:2] padded np.zeros((window_size, window_size, 3), dtypenp.float32) padded[:ph, :pw] patch pred model.predict(padded[None])[0, :, :, 0] density[y:y2, x:x2] pred[:ph, :pw] count_map[y:y2, x:x2] 1 density density / np.maximum(count_map, 1) return density.sum(), density這個(gè)滑動(dòng)窗口邏輯里stride設(shè)成window_size的一半就是 50% 重疊。重疊越多越準(zhǔn)但計(jì)算越慢。實(shí)測(cè) 50% 重疊在精度和速度之間比較平衡。5. 常見(jiàn)問(wèn)題與排查技巧實(shí)錄5.1 密度圖全黑或全白怎么辦這是新手最常遇到的問(wèn)題。全黑說(shuō)明網(wǎng)絡(luò)輸出接近零通常是損失函數(shù)沒(méi)收斂或者學(xué)習(xí)率太大導(dǎo)致梯度爆炸。先檢查學(xué)習(xí)率降到 1e-5 試試。如果還不行看看密度圖真值的數(shù)值范圍如果真值本身就在 0 到 0.001 量級(jí)那網(wǎng)絡(luò)輸出 0.0001 和 0.001 的差異太小MSE 梯度幾乎為零。解決辦法是把密度圖真值乘以一個(gè)縮放因子比如 1000讓數(shù)值范圍到 0 到 1 量級(jí)訓(xùn)練完再除回來(lái)。全白則是輸出飽和通常是損失函數(shù)權(quán)重設(shè)置不當(dāng)或者輸出層加了不該加的激活。檢查輸出層是不是誤加了 ReLU 或 Sigmoid密度估計(jì)輸出層應(yīng)該是線性的。5.2 人數(shù)估計(jì)偏高或偏低的系統(tǒng)性偏差如果所有圖都偏高大概率是密度圖真值的高斯核方差太小導(dǎo)致真值積分比實(shí)際人數(shù)大。因?yàn)楦咚购嗽趫D像邊界會(huì)被截?cái)嗳绻丝拷吔缃財(cái)嗪蠓e分會(huì)小于 1但網(wǎng)絡(luò)學(xué)到的模式可能反過(guò)來(lái)。解決辦法是生成真值時(shí)做邊界補(bǔ)償或者干脆在訓(xùn)練時(shí)把邊界區(qū)域裁掉。系統(tǒng)性偏低則可能是下采樣太多小人頭在特征圖上消失了。檢查你的總下采樣倍數(shù)如果超過(guò) 8 倍試著減少 pooling 層或者用空洞卷積替代部分下采樣。5.3 常見(jiàn)問(wèn)題速查表現(xiàn)象可能原因排查方向解決辦法密度圖全黑學(xué)習(xí)率過(guò)大、真值量級(jí)太小看 loss 曲線、看真值范圍降學(xué)習(xí)率、縮放真值密度圖全白輸出層激活錯(cuò)誤、權(quán)重初始化不當(dāng)檢查網(wǎng)絡(luò)最后一層去掉激活、換初始化人數(shù)系統(tǒng)性偏高真值高斯方差過(guò)小、邊界截?cái)嗫梢暬嬷得芏葓D調(diào)大方差、邊界補(bǔ)償人數(shù)系統(tǒng)性偏低下采樣過(guò)多、小人丟失看特征圖分辨率減少下采樣、用空洞卷積訓(xùn)練 loss 不降數(shù)據(jù)標(biāo)注錯(cuò)誤、學(xué)習(xí)率過(guò)小抽查標(biāo)注、看梯度修正標(biāo)注、調(diào)學(xué)習(xí)率驗(yàn)證集效果好測(cè)試集差場(chǎng)景差異大、過(guò)擬合對(duì)比場(chǎng)景分布加數(shù)據(jù)增強(qiáng)、換驗(yàn)證集5.4 獨(dú)家避坑技巧第一個(gè)技巧訓(xùn)練前一定要可視化幾張密度圖真值。我見(jiàn)過(guò)太多人直接開(kāi)訓(xùn)結(jié)果真值生成代碼有 bug高斯核位置偏了或者方差算錯(cuò)了白跑幾天。花十分鐘可視化能省幾天時(shí)間。第二個(gè)技巧數(shù)據(jù)增強(qiáng)要慎用幾何變換。水平翻轉(zhuǎn)、隨機(jī)裁剪沒(méi)問(wèn)題但旋轉(zhuǎn)和縮放要小心因?yàn)樾D(zhuǎn)會(huì)改變?nèi)巳旱某蚍植伎s放會(huì)改變尺度分布可能讓網(wǎng)絡(luò)學(xué)偏。如果非要用旋轉(zhuǎn)角度控制在 ±10 度以內(nèi)。第三個(gè)技巧多尺度分支的初始化。三個(gè)分支如果都用相同初始化訓(xùn)練初期輸出會(huì)高度相關(guān)多尺度就退化成單尺度了。我的做法是給不同分支的卷積核用不同的初始化方差比如小尺度分支方差大一點(diǎn)大尺度分支方差小一點(diǎn)強(qiáng)迫它們學(xué)不同的東西。6. 影響范圍與適用邊界MSCNN 能做什么、不能做什么6.1 適用場(chǎng)景MSCNN 這套多尺度密度估計(jì)方案最適合的是中高密度、尺度變化明顯的場(chǎng)景。比如地鐵站臺(tái)、商場(chǎng)中庭、景區(qū)入口、體育場(chǎng)看臺(tái)這些地方近處和遠(yuǎn)處的人尺度差異大單尺度模型容易顧此失彼。另外它對(duì)遮擋也有一定魯棒性因?yàn)槊芏葓D回歸不依賴每個(gè)人的完整輪廓只要局部紋理像人就能貢獻(xiàn)密度值。在零售客流分析里這套方案可以用來(lái)統(tǒng)計(jì)進(jìn)店人數(shù)、分析熱區(qū)分布。在交通樞紐可以用來(lái)做人群擁堵預(yù)警。在公共安全領(lǐng)域可以用來(lái)評(píng)估人群聚集風(fēng)險(xiǎn)。這些場(chǎng)景的共同點(diǎn)是不需要精確到每個(gè)人的身份只需要知道“大概多少人、分布在哪”。6.2 不適用或需謹(jǐn)慎的場(chǎng)景極稀疏場(chǎng)景比如一張圖只有兩三個(gè)人用 MSCNN 有點(diǎn)殺雞用牛刀而且密度圖回歸在稀疏時(shí)反而不如直接檢測(cè)準(zhǔn)。這時(shí)候用 YOLO 之類的檢測(cè)器更合適。極端密集場(chǎng)景比如每平米十幾個(gè)人人頭完全糊成一片MSCNN 也會(huì)吃力因?yàn)槊芏葓D真值本身就不準(zhǔn)了標(biāo)注人員都數(shù)不清。這種場(chǎng)景需要結(jié)合時(shí)序信息或者更高分辨率的輸入。非人頭目標(biāo)比如數(shù)車輛、數(shù)細(xì)胞理論上也能用但密度圖的高斯核參數(shù)和網(wǎng)絡(luò)尺度要重新調(diào)不能直接套用。6.3 后續(xù)可擴(kuò)展方向如果這套基礎(chǔ)版跑通了有幾個(gè)方向可以繼續(xù)挖。一是引入注意力機(jī)制讓網(wǎng)絡(luò)自動(dòng)聚焦到有人區(qū)域抑制背景干擾。二是多任務(wù)學(xué)習(xí)同時(shí)輸出密度圖和人群分布熱力圖互相監(jiān)督。三是時(shí)序擴(kuò)展把單幀 MSCNN 擴(kuò)展到視頻用光流或者 3D 卷積捕捉人群流動(dòng)這對(duì)客流預(yù)測(cè)很有價(jià)值。我個(gè)人在實(shí)際操作中的體會(huì)是MSCNN 這類多尺度結(jié)構(gòu)核心價(jià)值不在于某個(gè)具體網(wǎng)絡(luò)有多深而在于它把“尺度變化”這個(gè)物理問(wèn)題轉(zhuǎn)化成了“多分支特征融合”這個(gè)可學(xué)習(xí)的工程問(wèn)題。你把這個(gè)思路吃透了換成其他 backbone、其他任務(wù)照樣能遷移。最后再分享一個(gè)小技巧訓(xùn)練時(shí)定期保存密度圖的可視化結(jié)果和原圖疊在一起看比只看 loss 曲線直觀得多很多問(wèn)題一眼就能發(fā)現(xiàn)。