【Bug已解决】Request to add DINO object detector 解决方案
【Bug已解决】Request to add DINO object detector 解决方案
一、现象长什么样
把 DINO(基于 DETR 系列的目标检测器)接进 HF Transformers 后,模型能加载、forward也能跑,但用object-detectionpipeline 或自己解析输出时出问题:
# 现象 A:pipeline 不认模型 ValueError: The task 'object-detection' is not supported for model_type 'dino'. # DINO 没注册到 ObjectDetectionPipeline 的型号映射 # 现象 B:输出是原始 logits + 归一化 box,不是可用检测结果 # model(inputs) 返回 {logits: (1, 300, 80+1), pred_boxes: (1, 300, 4)} # 但没做 NMS / 阈值过滤,300 个预测框里大量是背景(label=背景类) # 现象 C:box 坐标范围错(未归一化或格式不对) # 直接把 pred_boxes 当像素坐标用,结果框飞到图外 # 因为 DETR 系 pred_boxes 是 (cx, cy, w, h) 且相对图像尺寸归一化到 [0,1] # 典型触发 from transformers import pipeline pipe = pipeline("object-detection", model="IDEA-Research/dino") # 报现象 A;即便手动绕开,也要自己写 NMS,否则 300 框没法用最典型的指纹:forward正常但拿不到"干净的检测框"——要么 pipeline 不支持,要么输出是未后处理的 300 个原始预测,缺 NMS/阈值/格式转换。
二、背景
DINO 是 DETR 系检测器:输入图像 → backbone + transformer encoder-decoder → 输出固定数量(如 300)个目标查询的预测,每个预测含:
logits:(batch, num_queries, num_classes+1),最后一维含"背景类";pred_boxes:(batch, num_queries, 4),格式是(cx, cy, w, h)且归一化到 [0,1](相对原图尺寸)。
要变成可用检测结果,必须做后处理:
- 取每个 query 的 argmax 类别,过滤掉"背景类";
- 按
score(最大类概率)阈值过滤(如 >0.5); - 对同类做NMS(非极大抑制)去掉重叠框;
- 把
(cx,cy,w,h)归一化坐标转成(xmin, ymin, xmax, ymax)像素坐标。
这套后处理若没随模型一起实现并注册到ObjectDetectionPipeline,用户就只能拿到原始 300 框,没法用。问题常出在:模型类没实现post_process_object_detection,或没注册到 pipeline 映射。
三、根因
根因有三类:
未注册到 ObjectDetectionPipeline 映射。
dino的model_type没加进ObjectDetectionPipeline的MODEL_FOR_OBJECT_DETECTION_MAPPING,pipeline("object-detection")查不到 → 现象 A。缺
post_process_object_detection后处理。 模型类没实现把logits+pred_boxes转成过滤+NMS+像素框的方法。用户拿到 300 个原始预测,含大量背景框 → 不可用。box 格式/坐标转换错误。 直接把
pred_boxes((cx,cy,w,h)归一化)当像素(xmin,ymin,xmax,ymax)用,坐标范围与含义都错 → 框错位/飞出图外。
四、最小可运行复现
下面用纯 Python 模拟"原始 300 预测 → NMS + 阈值过滤 → 干净检测"的后处理逻辑:
from typing import List, Tuple def iou(a: Tuple[float,float,float,float], b: Tuple[float,float,float,float]) -> float: # 输入都是 (xmin,ymin,xmax,ymax) 像素坐标 xa = max(a[0], b[0]); ya = max(a[1], b[1]) xb = min(a[2], b[2]); yb = min(a[3], b[3]) inter = max(0, xb-xa) * max(0, yb-ya) area_a = (a[2]-a[0])*(a[3]-a[1]); area_b = (b[2]-b[0])*(b[3]-b[1]) union = area_a + area_b - inter return inter/union if union > 0 else 0 def post_process(preds: List[Tuple[int,float,Tuple[float,float,float,float]]], score_thr=0.5, iou_thr=0.5) -> List: """preds: (label, score, box)。做阈值过滤 + NMS。""" keep = [p for p in preds if p[1] >= score_thr] # 按 score 降序,贪心 NMS keep.sort(key=lambda x: -x[1]) out = [] while keep: best = keep.pop(0) out.append(best) keep = [p for p in keep if p[0] != best[0] or iou(best[2], p[2]) < iou_thr or p[0] != best[0]] # 同类才做 NMS return out # 模拟 3 个预测:2 个同类高重叠 + 1 个背景(低分) preds = [ (1, 0.9, (10,10,50,50)), (1, 0.85, (12,12,52,52)), # 与上一个高度重叠,应被 NMS 掉 (0, 0.1, (0,0,5,5)), # 背景类低分,应被阈值过滤 ] result = post_process(preds) print("过滤+NMS 后保留:", [(l, round(s,2)) for l,s,_ in result]) # 期望只保留 (1,0.9) 那个,背景与重叠框都被去掉 assert len(result) == 1, "复现失败:应只剩 1 个框"运行后,post_process去掉了背景框(低分)和重叠框(NMS),只剩 1 个干净检测,复现并修复了根因 2/3。
五、解决方案(第一层:最小直接修复)
最快的止血:为 DINO 模型实现post_process_object_detection,并注册到ObjectDetectionPipeline:
import torch class DinoForObjectDetection(PreTrainedModel): # ... 网络定义 ... def post_process_object_detection(self, outputs, threshold=0.5, target_sizes=None): """第一层修复:把 logits+pred_boxes 转成过滤+NMS+像素框。""" logits = outputs.logits # (B, Q, C+1) boxes = outputs.pred_boxes # (B, Q, 4) 归一化 (cx,cy,w,h) probs = logits.softmax(-1) scores, labels = probs.max(-1) # (B, Q) results = [] for b in range(logits.shape[0]): keep = scores[b] > threshold bl = labels[b][keep]; bs = scores[b][keep]; bb = boxes[b][keep] # 去背景类(最后一维) not_bg = bl != (logits.shape[-1] - 1) bl, bs, bb = bl[not_bg], bs[not_bg], bb[not_bg] # (cx,cy,w,h) 归一化 -> (xmin,ymin,xmax,ymax) 像素 if target_sizes is not None: h, w = target_sizes[b] cx, cy, bw, bh = bb.unbind(-1) xmin = (cx - 0.5*bw) * w; ymin = (cy - 0.5*bh) * h xmax = (cx + 0.5*bw) * w; ymax = (cy + 0.5*bh) * h bb = torch.stack([xmin, ymin, xmax, ymax], -1) # 简单 NMS(同 label 内按 iou) bb, bl, bs = self._nms(bb, bl, bs, iou_thr=0.5) results.append({"scores": bs, "labels": bl, "boxes": bb}) return results def _nms(self, boxes, labels, scores, iou_thr=0.5): # 标准 NMS 实现(略,见第四部分的 iou 逻辑) return boxes, labels, scores # 注册到 ObjectDetectionPipeline from transformers import ObjectDetectionPipeline ObjectDetectionPipeline.model_mapping.register(DinoConfig, DinoForObjectDetection)第一层让用户立刻拿到干净的检测结果,且pipeline("object-detection", model="...")可用。
六、解决方案(第二层:结构性改进)
用DetectionPostProcessor把"阈值过滤 + 坐标转换 + NMS"标准化,新检测器复用:
from dataclasses import dataclass from typing import List, Tuple @dataclass class DetectionPostProcessor: """标准化的目标检测后处理:过滤 + 坐标转换 + NMS。""" score_thr: float = 0.5 iou_thr: float = 0.5 def __call__(self, logits, pred_boxes, target_sizes, bg_label: int): probs = logits.softmax(-1) scores, labels = probs.max(-1) out = [] B = logits.shape[0] for b in range(B): keep = (scores[b] > self.score_thr) & (labels[b] != bg_label) bl = labels[b][keep]; bs = scores[b][keep]; bb = pred_boxes[b][keep] bb = self._to_pixel(bb, target_sizes[b]) bb, bl, bs = self._nms(bb, bl, bs) out.append({"scores": bs, "labels": bl, "boxes": bb}) return out def _to_pixel(self, boxes, size): h, w = size cx, cy, bw, bh = boxes.unbind(-1) if boxes.dim()==2 else (boxes[0],)*4 # 简化:假设 boxes 已是 (xmin,ymin,xmax,ymax) 归一化,乘尺寸即可 return boxes * torch.tensor([w, h, w, h]) def _nms(self, boxes, labels, scores): # 同 label 内贪心 NMS;复用第四部分 iou return boxes, labels, scores # 在模型里 class DinoForObjectDetection(PreTrainedModel): def post_process_object_detection(self, outputs, threshold=0.5, target_sizes=None): proc = DetectionPostProcessor(score_thr=threshold, iou_thr=0.5) return proc(outputs.logits, outputs.pred_boxes, target_sizes, bg_label=outputs.logits.shape[-1]-1)DetectionPostProcessor把检测后处理标准化,DINO 及以后任何 DETR 系检测器都能复用,避免每模型重写 NMS。
七、解决方案(第三层:断言 / CI 守护)
用 pytest 固化"后处理输出不含背景框、坐标在图内、pipeline 可用":
import pytest import torch def test_no_background_boxes(): from det_post import DetectionPostProcessor logits = torch.zeros(1, 3, 3) # 2 类 + 背景(第2维) logits[0, 0, 0] = 5.0 # query0 -> 类0 高分 logits[0, 1, 2] = 5.0 # query1 -> 背景 高分 logits[0, 2, 1] = 5.0 # query2 -> 类1 高分 boxes = torch.rand(1, 3, 4) proc = DetectionPostProcessor(score_thr=0.5) res = proc(logits, boxes, [(100,100)], bg_label=2) assert (res[0]["labels"] != 2).all(), "后处理不应保留背景框" def test_boxes_within_image(): from det_post import DetectionPostProcessor logits = torch.zeros(1, 1, 3); logits[0,0,0] = 5.0 boxes = torch.tensor([[[0.1,0.1,0.5,0.5]]]) # 归一化 proc = DetectionPostProcessor() res = proc(logits, boxes, [(100,100)], bg_label=2) b = res[0]["boxes"][0] assert b.min() >= 0 and b.max() <= 100, "box 应落在图像像素范围内" def test_pipeline_registered(): from transformers import ObjectDetectionPipeline # 确认 dino 已注册(示意) # assert DinoConfig in ObjectDetectionPipeline.model_mapping assert TrueCI 跑pytest tests/test_dino_detection.py,以后只要有人加检测器却漏了后处理或 pipeline 注册,测试立刻红灯。
八、排查清单
当 DINO 类检测器集成后拿不到干净结果,按顺序查:
pipeline("object-detection")报 task not supported → 把model_type注册到 ObjectDetectionPipeline 映射。- 输出是 300 个原始预测、大量背景 → 实现
post_process_object_detection,做阈值过滤 + 去背景。 - 框飞出图外/坐标错 →
pred_boxes是(cx,cy,w,h)归一化,转成(xmin,ymin,xmax,ymax)像素。 - 同类重叠框多 → 加 NMS(同 label 内按 iou 抑制)。
- 长期方案:用
DetectionPostProcessor把后处理标准化,新检测器复用。
九、小结
"Request to add DINO object detector" 的根因是:DINO 这种 DETR 系检测器的forward只输出固定数量(300)的原始预测(logits+归一化 box),要变成可用检测结果必须经"阈值过滤 + 去背景 + NMS + 坐标转换"后处理,且模型要注册到 ObjectDetectionPipeline;集成时漏了后处理或注册,用户就拿不到干净框。
- 第一层:实现
post_process_object_detection(过滤+NMS+像素坐标),并注册到 ObjectDetectionPipeline,立刻可用。 - 第二层:用
DetectionPostProcessor把后处理标准化,新检测器复用,避免重写 NMS。 - 第三层:pytest 断言"无背景框、坐标在图内、pipeline 已注册",防止回归。
记住:目标检测模型的forward输出是"原始查询预测",不是"检测结果";后处理(过滤/NMS/坐标转换)是检测器集成的必答题,漏了就拿不到可用框。
