畅联云平台丨02-Tile滑窗与NMS去重

2026年8月17日
畅联云平台丨02-Tile滑窗与NMS去重

Tile 滑窗与 NMS 去重:高分辨率人体检测的几何与抑制

本文讲透一件事:当原图远大于检测网络固定输入(如 640×640)时,本项目如何用 Tile 滑窗 保住远处小目标的像素密度,再用 核心区归属 + NMS / prefer-face NMS + 形态过滤 把重叠报告压回「一人一框」。重点不是「切块很常见」,而是网格如何选、正方形 tile 为何强制、中心落在重叠带为何丢弃、IoU 阈值与有脸偏好如何改写淘汰规则。


一、问题定义:整帧缩放为何会「看不见人」

检测网络对目标在输入张量上的像素高度有下限。监控源常见 1920×1080、2560×1440、3840×2160,而 YOLO 输入边长常固定为 640。若整帧 letterbox:

scale_full ≈ 640 / max(W, H)

人身高_in_net ≈ 人身高_原图 × scale_full

数值例子(3840×2160,人身高 80 px):

scale_full = 640/3840 ≈ 0.1667

人在网络上 ≈ 80 × 0.1667 ≈ 13.3 px

13 像素高的人,特征几乎塌成噪声;召回断崖。若把同一人切进约 1280 边长的 tile 再 letterbox 到 640:

scale_tile ≈ 640/1280 = 0.5

人在网络上 ≈ 80 × 0.5 = 40 px

同样的人,像素密度大约翻了三倍。Tile 的本质不是「多画几条网格线」,而是用多次局部推理换取「人物在网络输入里仍有足够像素」。

图:高分辨率帧划成 G×G 网格;tile 边长大于步长,相邻块形成重叠带,跨缝的人至少能完整落入一块。


二、何时启用 Tile:分辨率门槛

按帧规划 Tile 的第一道闸:

含义:

  • 宽 < 1280 且 高 < 720:返回空 plan(grid<=0),不做滑窗,整帧一次检测足够;
  • 否则进入网格规划。

注意是 AND 不是 OR:一张 1920×600 的超宽条(高不足 720)仍会触发 Tile,因为宽 ≥1280。一张 800×900 的竖图(宽不足 1280)若高 ≥720 也会触发。只有「两边都偏小」才跳过。

失败模式: 若把条件误写成 ||,大量 720p 竖拍会无意义地跑 9~16 次推理;若误写成只看宽,竖屏 1080×1920 可能永远不 Tile,远人永久漏检。


三、网格密度:4K 用 4,其余用 3

「误检更少」来自:同一物理噪声(衣架、海报人形)在更大 tile 里相对更小,分数不易过阈;网格过密时噪声也更容易被「放大」成候选。


四、重叠率、base 与正方形 tile_side

核心公式由按帧规划 Tile 给出:

拆开:

  1. overlap = 0.25:相对 base 放大 25%,制造重叠带;
  2. base_w/h = frame / grid(下限 320):无重叠时的「格子」尺寸,也是 步长 step;
  3. tile_side = max(640, max(base_w, base_h)×1.25):强制 正方形 tile;
  4. step = base:滑窗步进等于 base,不是 tile。

网格与 tile 边长规划:

PersonFocusYoloTilePlan person_focus_yolo_tile_plan_for_frame(int frame_w, int frame_h) {
 PersonFocusYoloTilePlan plan;
 if (frame_w < 1280 && frame_h < 720) return plan; // 不切块
 plan.grid = (frame_w >= 3840 || frame_h >= 2160) ? 4 : 3;
 const double overlap = 0.25;
 const int base_w = std::max(320, (int)std::lround((double)frame_w / plan.grid));
 const int base_h = std::max(320, (int)std::lround((double)frame_h / plan.grid));
 const int tile_side = std::max(640, (int)std::lround(std::max(base_w, base_h) * (1.0 + overlap)));
 plan.tile_w = std::min(frame_w, tile_side);
 plan.tile_h = std::min(frame_h, tile_side);
 plan.step_x = std::max(320, base_w);
 plan.step_y = std::max(320, base_h);
 return plan;
}

为何强制正方形?

注释写得很直白:避免 2K 下出现类似 800×450 的扁条。扁条 letterbox 到 640 时,纵向有效像素被压得很矮,站立人体在网络里「又矮又糊」。取 max(base_w, base_h) 再 ×1.25,保证短边方向也有足够视野与像素。

数值例子 A:1920×1080,grid =3

base_w = round(1920/3) = 640

base_h = round(1080/3) = 360

tile_side = max(640, max(640,360)×1.25) = max(640, 800) = 800

tile_w = tile_h = 800

step_x = 640, step_y = 360

水平方向:tile 800、步长 640 → 重叠宽度约 (800-640)=160,即每侧约 80 像素的归属争议带。

垂直方向:tile 800、步长 360 → 重叠更大(正方形相对扁格子「探」得更远)。

数值例子 B:3840×2160,grid=4

base_w = 960, base_h = 540

tile_side = max(640, 960×1.25) = 1200

step_x = 960, step_y = 540

人站在两块水平缝上时,只要身体水平跨度不超过重叠量级,至少一块能包住完整躯干。

图:每个 tile 的边缘重叠带不「认领」框;只有中心落在非重叠核心区(core)的检测框才保留,从源头减少跨块重复。


五、 ROI 起点如何摆:居中回退

直觉:

  • 理想起点 = g×step - (tile-step)/2,让重叠关于格子对称;
  • 再夹到 [0, W-tile],避免右下角越界;
  • roi.width < 320 || roi.height < 320 的残片直接跳过。

失败模式: 若起点只写 gx*step 且不回退,最后一列可能只剩很窄的条;若回退公式用错符号,整行网格会整体错位,框呈「周期性平移」。


六、核心区过滤:中心不在 core 就丢

重叠带来的副作用是:同一人会在相邻 tile 各报一次。本项目不只靠事后 NMS,还在合并 前做 中心归属:

要点:

  • 用框中心,不用整框 IoU;半身残框若中心仍落在 core,仍可保留;
  • 画幅边缘的 tile:贴边一侧 margin=0,否则靠边的人中心永远进不了 core;
  • margin = (tile - step)/2 正是重叠带的一半宽度。

中心是否落在非重叠 core:

bool person_focus_yolo_box_center_in_tile_core(int frame_w, int frame_h, const cv::Rect &tile_roi,
 const PersonFocusYoloTilePlan &plan,
 const PersonFocusPersonBox &box) {
 const float cx = box.x + box.w * 0.5f;
 const float cy = box.y + box.h * 0.5f;
 const int margin_x = std::max(0, (plan.tile_w - plan.step_x) / 2);
 const int margin_y = std::max(0, (plan.tile_h - plan.step_y) / 2);
 const int left = tile_roi.x + (tile_roi.x > 0 ? margin_x : 0);
 const int top = tile_roi.y + (tile_roi.y > 0 ? margin_y : 0);
 const int right = tile_roi.x + tile_roi.width - (tile_roi.x + tile_roi.width < frame_w ? margin_x : 0);
 const int bottom = tile_roi.y + tile_roi.height - (tile_roi.y + tile_roi.height < frame_h ? margin_y : 0);
 return cx >= (float)left && cx < (float)right && cy >= (float)top && cy < (float)bottom;
}

数值例子(接 1920×1080)

某内陆 tile:x0=560, y0=180, w=800, h=800(示意),margin_x=(800-640)/2=80,margin_y=(800-360)/2=220。

core: left=560+80=640, right=560+800-80=1280

top=180+220=400, bottom=180+800-220=760

若人中心 (cx,cy)=(620,500):落在左重叠带 → 本 tile 丢弃(应归左侧邻居)。

若中心 (700,500):在 core → 保留。

失败模式:

Core 过滤是 硬几何先验:先减少候选,再让 NMS 收拾残余。


七、坐标映射:tile 局部 → 全图

每个 tile 裁切后再 letterbox 进网络。框还原顺序必须是:

  1. letterbox 坐标 → 去 pad、除 scale → tile 图像坐标;
  2. 加上 (tile_x0, tile_y0) → 全图坐标;
  3. 与全图边界求交。

写成:

x_full = (x_lb - pad_x) / scale + tile_x0

y_full = (y_lb - pad_y) / scale + tile_y0

w_full = w_lb / scale

h_full = h_lb / scale

调试口诀:

  1. 在单 tile 图上画框 → 已偏:解码/letterbox;
  2. tile 对、全图挤左上:忘加 tile_x0/y0;
  3. 整片平移:pad 符号反了;
  4. 网格状周期错位:某一行/列的 ROI 原点算错。

八、标准 NMS:分数排序 + IoU 淘汰

块内解码后,以及全图合并后,都会走 OpenCV cv::dnn::NMSBoxes。

全局人体框合并使用同一套 IoU 阈值逻辑。

这里 score 下界写成 0.01f:进入该函数的框通常已过检测阈,NMS 阶段不再二次狠滤分数,专注 IoU 去重。

IoU 定义与手算

IoU = 交集面积 / 并集面积

= inter / (area_a + area_b - inter)

prefer-face 路径会自实现 IoU 计算(与 OpenCV 语义一致):

手算例子:

框 A:(100,100,80,200),框 B:(120,120,80,200)

inter = 60 × 180 = 10800

ua = 16000 + 16000 - 10800 = 21200

IoU ≈ 0.509


若 nms_iou=0.45,B 会被抑制;若阈值升到 0.55,B 幸存——并肩两人更安全,但跨 tile 重复也可能留下。

图:按分数排序后,与最高分框 IoU 超过阈值的候选被抑制;阈值过高留重复,过低误伤贴靠两人。


Soft-NMS 一句话

标准 NMS「超阈即删」;Soft-NMS「超阈即降分」。本项目路径用经典硬 NMS(及下面的 prefer-face 变体),监控人体通常够用。


九、 prefer-face NMS:有脸的框优先留下

纯分数 NMS 的痛点:跨缝时 半身残框分数偶发更高,完整框反而被删;或 无脸噪声框 压过 有脸真人框。因此有优先有脸的人体框 NMS:

关键机制:

  1. 按人体 score 降序扫描;
  2. IoU ≤ NMS IoU:不冲突,继续;
  3. IoU > NMS IoU:看两边脸集合是否「互不相同」:
  • 一侧有对方没有的脸 → 保留有独特脸的一侧;
  • 两侧都有独特脸 → 两边都留(避免两人贴靠被合成一个);
  • 都无独特脸但都有脸且 IoU 很高(重复 IoU)→ 重复框决胜 决胜;
  • 一侧有脸一侧无脸 → 倾向留有脸,无脸仅在高度重叠时被删。

若 nms_iou=0.45,则 dup_iou=max(0.57,0.58)=0.58(再与 0.92 取 min)。也就是说:「有脸 vs 无脸」的删除门槛比普通 NMS 更严,避免 ROI 扫脸失败时误删真人。

决胜函数 重复框决胜 还看面积比与更靠上的框(站立人头部更靠上):

最后还有:被抑制但带有 保留集里没有的脸 的框可以复活——防止「大框吞小框」丢掉旁边另一张脸。



图:IoU 冲突时,优先保留框内有脸(或有独特脸)的人体框;两侧都有独特脸则并存,避免贴靠两人被压成一个。


失败模式:

  • face_scores 与 boxes 长度不一致 → 函数退回普通 NMSBoxes,有脸偏好失效;
  • 扫脸阈值过高 → 真人都「无脸」,prefer-face 退化成面积/分数战;
  • 重复 IoU 过低 → 有脸框过于强势,贴靠路人被误删。

十、形态过滤:长宽比与最大占比

NMS 之后(或前后配合)还有人体框几何过滤:

规则表:

直觉:

  • 无脸 时比例要像站立人体,挡广告牌横条、细杆;
  • 有脸 时放宽:蹲姿、侧躺、被裁切的半身仍可能是真人;
  • 0.85 帧:接近整幅的「人」多半是误检或场景分割错误,直接丢。

数值例子

1920×1080 帧:max_w=1632, max_h=918。

框 1800×1000:超限 → 丢。

框 40×200,ar=5.0,无脸 → 丢;若有脸且 ar=4.2 → 可留。

失败模式: 夜间低分导致大量「无脸」,比例过滤偏严,蹲着的人被杀光;或 max 比例放宽到 1.0,过曝光斑整幅框涌进跟踪。


十一、算力账本:Tile 不是免费的

粗算(忽略重叠与 batch  效率):


重叠使实际裁切面积更大,但 GPU batch 可摊薄 kernel 启动。工程上常配合:隔帧检测、分数阈、动态是否 Tile。选型不是「网格越大越好」,而是「人物在 640 输入上是否 ≥ 约 30~40 px」与时延预算的交点。

与「整帧改用 1280 输入」对比:

  • 中心少数人、算力紧:先试更大输入边长;
  • 人散布四角、4K 远景:倾向 Tile;
  • 两者可折中:Tile 内仍用 640,而不是 Tile 后再拉到过大边长。

十二、端到端故事线(带数字)

  1. 3840×2160 帧进入,w≥3840 → grid=4,overlap=0.25,tile_side=1200,step=(960,540);
  2. 收集至多 16 个 ROI,每块 letterbox→YOLO→逆变换到全图;
  3. 对每条框问:中心是否在该 tile 的 core?否 → 丢;
  4. 全图框列表做 NMSBoxes 或 prefer-face NMS;
  5. 过滤:无脸 ar∈[0.55,3.5],有脸放宽;边长 ≤0.85 帧;
  6. 输出给后续模块:原图像素系、尽量一人一框。

任一步偷懒(无重叠、无 core、无 NMS、比例过滤关掉)都会在跟踪里以「闪烁双人」「ID 乱跳」爆发。


十三、参数耦合怎么拧

一次只改一个旋钮,并在「远广场」「门口近景」「两人并肩」三类片段回归。


十四、常见故障对照表


十五、与「只靠全局 NMS」的本质差别

许多教程只写「切块 + NMS」。本项目多了两层:

  1. 几何归属(core):重叠带不认领,从源头减候选;
  2. 语义偏好(face):同样重叠时,有脸/独特脸说了算,而不是裸分数。

这使 Tile 在商场、展厅等人脸可检场景更稳:残缺高分框更难打败完整有脸框。


十六、重叠率的定量直觉

无重叠时,人中心恰好在 x=base_w 的缝上,左右各只看到约半身。若人体宽 W_person,每侧可见宽约 W_person/2。检测器对「半身」分数常系统性偏低。

设需要「至少一侧看到完整人」,则重叠宽度应 ≳ W_person。对本项目:

overlap_width ≈ tile_side - step ≈ 0.25 × max(base_w, base_h) (正方形情形下还受 max 影响)

1080p 例子中水平重叠约 160 px:对肩宽 ~80–120 的远人对付得了;对近景大肩宽 ~300 的人,仍可能双边残缺——此时依赖 prefer-face 与更大面积框决胜,或接受近景本可用整帧检测(但 1080p 已过 Tile 门槛时仍会切)。overlap=0.25 是召回与算力的工程折中,不是数学最优解。


美畅美畅物联畅联畅联云平台视频监控云平台云视频监控视频云平台视频开放平台视频感知云视频接入网关AIoT综合接入网关视频中台物联网中台视频上云网关EhomeI连锁行业智慧交通AI算法