016、自动标注方案实战用大模型SAM/Grounding DINO生成 YOLO 格式伪标签从一次深夜调试说起上周三凌晨两点我盯着屏幕上那个标注到一半的无人机航拍数据集发呆。三千张图每张图上密密麻麻的车辆、行人、自行车标注团队说最快也要两周。两周模型迭代周期都够跑三轮了。更让人崩溃的是标注员把“轿车”标成了“SUV”把“行人”标成了“骑行者”——这种语义歧义在目标检测里简直是灾难。当时我就在想能不能让大模型先帮我标一遍哪怕只有80%的准确率我只需要人工修正那20%的错误也比从零开始画框快得多。于是就有了今天要聊的这个方案——用Grounding DINO做零样本检测用SAM做精细分割最后转成YOLO格式的伪标签。选型为什么是Grounding DINO SAM而不是别的市面上能用的零样本检测模型不少GLIP、OWL-ViT、Grounding DINO我都试过。GLIP在COCO上表现不错但换到工业场景比如检测电路板上的焊点就崩了。OWL-ViT推理速度慢得离谱一张图要跑3秒。Grounding DINO在速度和精度之间找到了平衡点特别是它的“文本-图像”对齐机制对长尾类别比如“带安全帽的工人”这种组合描述理解得比另外两个好。SAM呢它本身不做检测但它的分割能力是顶级的。Grounding DINO给出的边界框往往比较粗糙尤其是物体边缘不清晰的时候比如透明玻璃杯、毛绒玩具框会偏大或偏小。用SAM在框内做一次精细分割再取最小外接矩形框的精度能提升10-15个点。别这样写不要试图用SAM直接做全图分割再聚类成框。我试过一张1024x1024的图SAM跑一次全图分割要5秒而且会把背景里的纹理也分割出来后处理过滤比标注还累。环境搭建那些文档里没写的坑先列一下核心依赖版本号我踩过雷torch1.13.1cu117 torchvision0.14.1cu117 segment-anything1.0 groundingdino0.1.0 opencv-python4.8.0.74这里踩过坑Grounding DINO的官方代码对torch版本很敏感用2.0以上版本会报“torch.jit.script”相关的错误。我折腾了两天最后发现1.13.1是最稳的。SAM倒是不挑但如果你用MPSMac的GPU加速记得把device设成mps别用cuda否则会静默回退到CPU慢到怀疑人生。模型权重下载Grounding DINOgroundingdino_swint_ogc.pth1.2G别下那个swinb的太大推理速度慢一倍SAMsam_vit_h_4b8939.pth2.5Gvit_h精度最高但如果你显存不够用vit_l也行精度差3%左右核心流程从文本描述到YOLO标签整个流程分三步走每一步都有细节要处理。第一步Grounding DINO生成候选框输入是一张图和一段文本描述比如“car. person. bicycle.”。注意这里的写法类别之间用英文句点加空格分隔不要用逗号。Grounding DINO的文本编码器对句点更敏感用逗号会导致某些类别被忽略。fromgroundingdino.util.inferenceimportload_model,load_image,predict modelload_model(groundingdino/config/GroundingDINO_SwinT_OGC.py,weights/groundingdino_swint_ogc.pth)image,image_tensorload_image(test.jpg)# 文本描述类别名后面加句点别加空格text_promptcar. person. traffic light.box_threshold0.35# 这个阈值很关键后面细说text_threshold0.25boxes,logits,phrasespredict(modelmodel,imageimage_tensor,captiontext_prompt,box_thresholdbox_threshold,text_thresholdtext_threshold,devicecuda)别这样写不要把box_threshold设得太低比如0.2否则会出来一堆假阳性框尤其是背景里的纹理、阴影都会被当成目标。我一般从0.35开始调如果漏检多就降到0.3如果误检多就升到0.4。第二步SAM精细分割Grounding DINO返回的框是[x1, y1, x2, y2]格式归一化到[0,1]。SAM需要的是绝对坐标所以要先把框映射回原图尺寸。importnumpyasnpfromsegment_anythingimportsam_model_registry,SamPredictor samsam_model_registry[vit_h](checkpointweights/sam_vit_h_4b8939.pth)sam.to(devicecuda)predictorSamPredictor(sam)# 对每个框做精细分割defrefine_boxes_with_sam(image,boxes,predictor):predictor.set_image(image)refined_boxes[]forboxinboxes:# 框坐标从归一化转绝对坐标h,wimage.shape[:2]x1,y1,x2,y2box*np.array([w,h,w,h])input_boxnp.array([x1,y1,x2,y2])# SAM预测这里用box作为提示别用点提示masks,scores,_predictor.predict(boxinput_box[None,:],multimask_outputFalse)# 取最高分的maskbest_maskmasks[np.argmax(scores)]# 从mask提取最小外接矩形y_indices,x_indicesnp.where(best_mask)iflen(x_indices)0orlen(y_indices)0:continue# 空mask直接跳过x_min,x_maxx_indices.min(),x_indices.max()y_min,y_maxy_indices.min(),y_indices.max()refined_boxes.append([x_min,y_min,x_max,y_max])returnnp.array(refined_boxes)这里踩过坑SAM的predict方法如果传入的box太小比如小于10x10像素会返回空mask。我加了一个过滤如果原始框的宽或高小于20像素就不做SAM refine直接用原始框。另外multimask_outputFalse一定要设否则SAM会返回三个候选mask选起来麻烦。第三步转YOLO格式YOLO格式要求每张图对应一个txt文件每行是class_id x_center y_center width height所有坐标归一化到[0,1]。defconvert_to_yolo_format(refined_boxes,phrases,class_names,image_width,image_height):yolo_annotations[]forbox,phraseinzip(refined_boxes,phrases):# 类别名转IDifphrasenotinclass_names:continue# 不在预设类别里的跳过class_idclass_names.index(phrase)# 绝对坐标转归一化x1,y1,x2,y2box x_center(x1x2)/2/image_width y_center(y1y2)/2/image_height width(x2-x1)/image_width height(y2-y1)/image_height# 防止越界YOLO训练时如果坐标超出[0,1]会报错x_centernp.clip(x_center,0.0,1.0)y_centernp.clip(y_center,0.0,1.0)widthnp.clip(width,0.0,1.0)heightnp.clip(height,0.0,1.0)yolo_annotations.append(f{class_id}{x_center:.6f}{y_center:.6f}{width:.6f}{height:.6f})returnyolo_annotations别这样写不要直接拿Grounding DINO的原始框转YOLO格式。我对比过原始框的mAP比经过SAM refine的低8-10个点。尤其是小目标Grounding DINO的框经常偏大把背景也包进去了SAM能把它“切”出来。后处理伪标签的“去噪”技巧大模型生成的伪标签不可能100%准确需要做几件事来提升质量。1. 置信度过滤Grounding DINO返回的logits是每个框的置信度范围大概在0到1之间。我一般设两个阈值高置信度0.6直接保留中置信度0.3-0.6需要人工确认低置信度0.3直接丢弃但注意不同类别的置信度分布不一样。比如“car”这种常见类别置信度普遍高“traffic light”这种小目标置信度普遍低。所以最好按类别分别统计置信度分布动态调整阈值。2. NMS去重Grounding DINO对同一个目标可能会生成多个框尤其是当文本描述里有语义相近的类别时比如“car”和“vehicle”。用NMS非极大值抑制去重IOU阈值设0.5。fromtorchvision.opsimportnms# boxes是[x1,y1,x2,y2]格式scores是置信度keepnms(boxes,scores,iou_threshold0.5)这里踩过坑NMS的IOU阈值不要设得太低比如0.3否则会把紧密排列的目标比如停车场里的车误删。0.5是个比较安全的数值。3. 尺寸过滤有些框太小比如小于20x20像素可能是噪声。有些框太大比如占了整张图的80%可能是误检。根据你的目标尺寸分布设一个合理的范围。实战效果一个真实案例我用这个方案处理了一个无人机航拍数据集包含5000张图类别是“car, person, bicycle, motorcycle”。Grounding DINO SAM跑完大概花了6小时单卡V100生成了约4.2万个伪标签。人工抽检了200张图统计结果正确标签约78%漏检约12%主要是小目标比如远处的人误检约10%主要是阴影、树影被当成车用这些伪标签训练了一个YOLOv8s模型在真实标注的测试集上mAP0.5达到了0.72。作为对比用完全人工标注的数据训练mAP是0.81。差距9个点但人工标注花了三周我这个方案只花了两天6小时跑模型剩下时间人工修正。个人经验性建议别指望一次搞定伪标签方案的核心价值是“降低标注成本”不是“替代标注”。我现在的流程是先用大模型生成伪标签然后让标注员在伪标签基础上修正效率能提升3-5倍。标注员只需要调整框的位置和删除误检不用从零画框。类别选择要克制Grounding DINO虽然支持任意文本描述但类别越多准确率越低。我一般控制在10个类别以内。如果类别超过20个建议拆成多个子任务比如先检测“车辆”再检测“行人”。阈值调优是个体力活不同场景的最佳阈值不一样。我写了一个小脚本自动遍历box_threshold从0.2到0.5步长0.05然后对每个阈值抽检50张图统计准确率和召回率选F1最高的那个。虽然土但有效。SAM不是必须的如果你的目标都是刚性物体比如车牌、二维码Grounding DINO的框已经够用了加SAM反而增加耗时。但如果是柔性物体比如衣服、动物或者目标边缘模糊SAM的收益就很明显。留个后门伪标签数据集里一定要保留原始框的置信度信息。我习惯在YOLO格式的txt文件里每行末尾加一个空格和置信度值比如0 0.5 0.6 0.2 0.3 0.85这样训练时可以根据置信度做样本加权或者直接过滤掉低置信度的样本。最后说一句大模型不是万能的但它能帮你把标注这件事从“不可能”变成“可能”。下次再有人跟你说“标注太贵了”你就把这个方案甩给他。
016、自动标注方案实战:用大模型(SAM/Grounding DINO)生成 YOLO 格式伪标签
016、自动标注方案实战用大模型SAM/Grounding DINO生成 YOLO 格式伪标签从一次深夜调试说起上周三凌晨两点我盯着屏幕上那个标注到一半的无人机航拍数据集发呆。三千张图每张图上密密麻麻的车辆、行人、自行车标注团队说最快也要两周。两周模型迭代周期都够跑三轮了。更让人崩溃的是标注员把“轿车”标成了“SUV”把“行人”标成了“骑行者”——这种语义歧义在目标检测里简直是灾难。当时我就在想能不能让大模型先帮我标一遍哪怕只有80%的准确率我只需要人工修正那20%的错误也比从零开始画框快得多。于是就有了今天要聊的这个方案——用Grounding DINO做零样本检测用SAM做精细分割最后转成YOLO格式的伪标签。选型为什么是Grounding DINO SAM而不是别的市面上能用的零样本检测模型不少GLIP、OWL-ViT、Grounding DINO我都试过。GLIP在COCO上表现不错但换到工业场景比如检测电路板上的焊点就崩了。OWL-ViT推理速度慢得离谱一张图要跑3秒。Grounding DINO在速度和精度之间找到了平衡点特别是它的“文本-图像”对齐机制对长尾类别比如“带安全帽的工人”这种组合描述理解得比另外两个好。SAM呢它本身不做检测但它的分割能力是顶级的。Grounding DINO给出的边界框往往比较粗糙尤其是物体边缘不清晰的时候比如透明玻璃杯、毛绒玩具框会偏大或偏小。用SAM在框内做一次精细分割再取最小外接矩形框的精度能提升10-15个点。别这样写不要试图用SAM直接做全图分割再聚类成框。我试过一张1024x1024的图SAM跑一次全图分割要5秒而且会把背景里的纹理也分割出来后处理过滤比标注还累。环境搭建那些文档里没写的坑先列一下核心依赖版本号我踩过雷torch1.13.1cu117 torchvision0.14.1cu117 segment-anything1.0 groundingdino0.1.0 opencv-python4.8.0.74这里踩过坑Grounding DINO的官方代码对torch版本很敏感用2.0以上版本会报“torch.jit.script”相关的错误。我折腾了两天最后发现1.13.1是最稳的。SAM倒是不挑但如果你用MPSMac的GPU加速记得把device设成mps别用cuda否则会静默回退到CPU慢到怀疑人生。模型权重下载Grounding DINOgroundingdino_swint_ogc.pth1.2G别下那个swinb的太大推理速度慢一倍SAMsam_vit_h_4b8939.pth2.5Gvit_h精度最高但如果你显存不够用vit_l也行精度差3%左右核心流程从文本描述到YOLO标签整个流程分三步走每一步都有细节要处理。第一步Grounding DINO生成候选框输入是一张图和一段文本描述比如“car. person. bicycle.”。注意这里的写法类别之间用英文句点加空格分隔不要用逗号。Grounding DINO的文本编码器对句点更敏感用逗号会导致某些类别被忽略。fromgroundingdino.util.inferenceimportload_model,load_image,predict modelload_model(groundingdino/config/GroundingDINO_SwinT_OGC.py,weights/groundingdino_swint_ogc.pth)image,image_tensorload_image(test.jpg)# 文本描述类别名后面加句点别加空格text_promptcar. person. traffic light.box_threshold0.35# 这个阈值很关键后面细说text_threshold0.25boxes,logits,phrasespredict(modelmodel,imageimage_tensor,captiontext_prompt,box_thresholdbox_threshold,text_thresholdtext_threshold,devicecuda)别这样写不要把box_threshold设得太低比如0.2否则会出来一堆假阳性框尤其是背景里的纹理、阴影都会被当成目标。我一般从0.35开始调如果漏检多就降到0.3如果误检多就升到0.4。第二步SAM精细分割Grounding DINO返回的框是[x1, y1, x2, y2]格式归一化到[0,1]。SAM需要的是绝对坐标所以要先把框映射回原图尺寸。importnumpyasnpfromsegment_anythingimportsam_model_registry,SamPredictor samsam_model_registry[vit_h](checkpointweights/sam_vit_h_4b8939.pth)sam.to(devicecuda)predictorSamPredictor(sam)# 对每个框做精细分割defrefine_boxes_with_sam(image,boxes,predictor):predictor.set_image(image)refined_boxes[]forboxinboxes:# 框坐标从归一化转绝对坐标h,wimage.shape[:2]x1,y1,x2,y2box*np.array([w,h,w,h])input_boxnp.array([x1,y1,x2,y2])# SAM预测这里用box作为提示别用点提示masks,scores,_predictor.predict(boxinput_box[None,:],multimask_outputFalse)# 取最高分的maskbest_maskmasks[np.argmax(scores)]# 从mask提取最小外接矩形y_indices,x_indicesnp.where(best_mask)iflen(x_indices)0orlen(y_indices)0:continue# 空mask直接跳过x_min,x_maxx_indices.min(),x_indices.max()y_min,y_maxy_indices.min(),y_indices.max()refined_boxes.append([x_min,y_min,x_max,y_max])returnnp.array(refined_boxes)这里踩过坑SAM的predict方法如果传入的box太小比如小于10x10像素会返回空mask。我加了一个过滤如果原始框的宽或高小于20像素就不做SAM refine直接用原始框。另外multimask_outputFalse一定要设否则SAM会返回三个候选mask选起来麻烦。第三步转YOLO格式YOLO格式要求每张图对应一个txt文件每行是class_id x_center y_center width height所有坐标归一化到[0,1]。defconvert_to_yolo_format(refined_boxes,phrases,class_names,image_width,image_height):yolo_annotations[]forbox,phraseinzip(refined_boxes,phrases):# 类别名转IDifphrasenotinclass_names:continue# 不在预设类别里的跳过class_idclass_names.index(phrase)# 绝对坐标转归一化x1,y1,x2,y2box x_center(x1x2)/2/image_width y_center(y1y2)/2/image_height width(x2-x1)/image_width height(y2-y1)/image_height# 防止越界YOLO训练时如果坐标超出[0,1]会报错x_centernp.clip(x_center,0.0,1.0)y_centernp.clip(y_center,0.0,1.0)widthnp.clip(width,0.0,1.0)heightnp.clip(height,0.0,1.0)yolo_annotations.append(f{class_id}{x_center:.6f}{y_center:.6f}{width:.6f}{height:.6f})returnyolo_annotations别这样写不要直接拿Grounding DINO的原始框转YOLO格式。我对比过原始框的mAP比经过SAM refine的低8-10个点。尤其是小目标Grounding DINO的框经常偏大把背景也包进去了SAM能把它“切”出来。后处理伪标签的“去噪”技巧大模型生成的伪标签不可能100%准确需要做几件事来提升质量。1. 置信度过滤Grounding DINO返回的logits是每个框的置信度范围大概在0到1之间。我一般设两个阈值高置信度0.6直接保留中置信度0.3-0.6需要人工确认低置信度0.3直接丢弃但注意不同类别的置信度分布不一样。比如“car”这种常见类别置信度普遍高“traffic light”这种小目标置信度普遍低。所以最好按类别分别统计置信度分布动态调整阈值。2. NMS去重Grounding DINO对同一个目标可能会生成多个框尤其是当文本描述里有语义相近的类别时比如“car”和“vehicle”。用NMS非极大值抑制去重IOU阈值设0.5。fromtorchvision.opsimportnms# boxes是[x1,y1,x2,y2]格式scores是置信度keepnms(boxes,scores,iou_threshold0.5)这里踩过坑NMS的IOU阈值不要设得太低比如0.3否则会把紧密排列的目标比如停车场里的车误删。0.5是个比较安全的数值。3. 尺寸过滤有些框太小比如小于20x20像素可能是噪声。有些框太大比如占了整张图的80%可能是误检。根据你的目标尺寸分布设一个合理的范围。实战效果一个真实案例我用这个方案处理了一个无人机航拍数据集包含5000张图类别是“car, person, bicycle, motorcycle”。Grounding DINO SAM跑完大概花了6小时单卡V100生成了约4.2万个伪标签。人工抽检了200张图统计结果正确标签约78%漏检约12%主要是小目标比如远处的人误检约10%主要是阴影、树影被当成车用这些伪标签训练了一个YOLOv8s模型在真实标注的测试集上mAP0.5达到了0.72。作为对比用完全人工标注的数据训练mAP是0.81。差距9个点但人工标注花了三周我这个方案只花了两天6小时跑模型剩下时间人工修正。个人经验性建议别指望一次搞定伪标签方案的核心价值是“降低标注成本”不是“替代标注”。我现在的流程是先用大模型生成伪标签然后让标注员在伪标签基础上修正效率能提升3-5倍。标注员只需要调整框的位置和删除误检不用从零画框。类别选择要克制Grounding DINO虽然支持任意文本描述但类别越多准确率越低。我一般控制在10个类别以内。如果类别超过20个建议拆成多个子任务比如先检测“车辆”再检测“行人”。阈值调优是个体力活不同场景的最佳阈值不一样。我写了一个小脚本自动遍历box_threshold从0.2到0.5步长0.05然后对每个阈值抽检50张图统计准确率和召回率选F1最高的那个。虽然土但有效。SAM不是必须的如果你的目标都是刚性物体比如车牌、二维码Grounding DINO的框已经够用了加SAM反而增加耗时。但如果是柔性物体比如衣服、动物或者目标边缘模糊SAM的收益就很明显。留个后门伪标签数据集里一定要保留原始框的置信度信息。我习惯在YOLO格式的txt文件里每行末尾加一个空格和置信度值比如0 0.5 0.6 0.2 0.3 0.85这样训练时可以根据置信度做样本加权或者直接过滤掉低置信度的样本。最后说一句大模型不是万能的但它能帮你把标注这件事从“不可能”变成“可能”。下次再有人跟你说“标注太贵了”你就把这个方案甩给他。