ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

第5讲:数据集建设完整指南——质量决定上限

第5讲:数据集建设完整指南——质量决定上限 读者画像:从事工业机器视觉的算法工程师、视觉应用工程师,有一定Python基础,需要实际建设工业检测数据集的开发者。阅读时间:约35分钟前置知识:Python基础,了解图像数据基本概念,建议先阅读本专栏第1-4讲。一、为什么数据质量比模型更重要?在工业机器视觉项目中,有一条铁律:数据的上限决定模型的上限。你花3个月调出来的SOTA模型,如果训练数据本身有标注错误、样本不均衡、图像质量差等问题,上线后的效果可能还不如一个用高质量数据训练的YOLOv8n。根据我在多个产线项目的统计,数据问题占模型效果不达预期的70%以上。常见的数据"坑"包括:标注不一致:同一个缺陷,张三标"划痕",李四标"刮伤",模型学了个寂寞样本不均衡:OK品10000张,NG品只有50张,模型直接学会"全判OK"场景覆盖不足:只在白班采集数据,夜班光照变化后模型直接崩溃数据泄露:同一产品同时在训练集和验证集出现,评估指标虚高因此,在开始训练模型之前,我们至少要花30%的项目时间在数据集建设上。本讲将完整覆盖从采集、标注、审核到增强的全流程工程实践。二、采集方案设计——把问题想在前面2.1 光照变化覆盖工业现场的光照条件极其复杂,同一产品可能在不同工位、不同时间有不同的光照环境。一个鲁棒的数据集必须覆盖各种光照条件。importcv2importnumpyasnpfrompathlibimportPathimportrandomclassLightingSimulator:"""工业光照模拟器——在正常图像上模拟各种工业光照变化"""def__init__(self,random_seed=42):random.seed(random_seed)np.random.seed(random_seed)defsimulate_backlight(self,image,intensity=0.3):"""模拟背光(逆光)效果——常见于传送带侧光不足"""h,w=image.shape[:2]# 创建从中心到边缘的渐变遮罩y,x=np.ogrid[:h,:w]center_y,center_x=h/2,w/2distance=np.sqrt((x-center_x)**2+(y-center_y)**2)max_distance=np.sqrt((w/2)**2+(h/2)**2)# 中心亮、边缘暗的径向渐变mask=1-(distance/max_distance)*intensity mask=np.clip(mask,0.3,1.0)mask=np.dstack([mask]*3)iflen(image.shape)==3elsemask darkened=(image.astype(np.float32)*mask).astype(np.uint8)returndarkeneddefsimulate_strobe_flicker(self,image,flicker_ratio=0.5):"""模拟频闪灯效应——常见于高速产线的频闪照明"""h,w=image.shape[:2]result=image.copy()# 随机选择一些水平条纹区域变暗(模拟频闪不同步)num_stripes=random.randint(3,8)for_inrange(num_stripes):y_start=random.randint(0,h-50)stripe_height=random.randint(10,40)alpha=random.uniform(0.3,0.7)result[y_start:y_start+stripe_height,:]=(result[y_start:y_start+stripe_height,:]*alpha).astype(np.uint8)returnresultdefsimulate_led_banding(self,image,band_width=30):"""模拟LED灯条导致的明暗条纹——常见于线阵相机+LED照明"""h,w=image.shape[:2]result=image.copy().astype(np.float32)foryinrange(0,h,band_width):# 相邻条纹亮度交替变化brightness=0.85if(y//band_width)%2==0else1.15end_y=min(y+band_width,h)result[y:end_y,:]*=brightnessreturnnp.clip(result,0,255).astype(np.uint8)defapply_all(self,image):"""生成一张原始图像的所有光照变体"""variants={'original':image,'backlight':self.simulate_backlight(image),'strobe':self.simulate_strobe_flicker(image),'led_banding':self.simulate_led_banding(image),}returnvariants2.2 角度变化与位姿覆盖工业产品在传送带上的姿态千变万化,机械定位精度有限是客观事实。数据集必须覆盖各种角度变化。classPoseVariationGenerator:"""工业产品位姿变化生成器"""def__init__(self,rotation_range=(-15,15),scale_range=(0.9,1.1),translation_ratio=0.05):self.rotation_range=rotation_range self.scale_range=scale_range self.translation_ratio=translation_ratiodefrandom_affine(self,image):"""对图像施加随机仿射变换,模拟产品位姿变化"""h,w=image.shape[:2]center=(w/2,h/2)# 随机旋转角度angle=random.uniform(*self.rotation_range)# 随机缩放scale=random.uniform(*self.scale_range)# 随机平移dx=random.uniform(-w*self.translation_ratio,w*self.translation_ratio)dy=random.uniform(-h*self.translation_ratio,h*self.translation_ratio)# 构建旋转矩阵rot_mat=cv2.getRotationMatrix2D(center,angle,scale)rot_mat[0,2]+=dx rot_mat[1,2]+=dy result=cv2.warpAffine(image,rot_mat,(w,h),borderMode=cv2.BORDER_CONSTANT,borderValue=(128,128,128))returnresult,{'angle':angle,'scale':scale,'dx':dx,'dy':dy}defgenerate_variations(self,image,num_variations=5):"""为一幅图像生成多个位姿变体"""results=[image]# 保留原图for_inrange(num_variations):var_img,params=self.random_affine(image)results.append(var_img)returnresults2.3 样本均衡策略在工业检测中,正负样本通常高度不均衡。我们需要在采集阶段就制定均衡策略:classSampleBalancer:"""样本均衡管理器——在采集阶段控制样本分布"""def__init__(self,target_ratios=None):""" Args: target_ratios: 目标样本比例,如 {'OK': 0.4, 'scratch': 0.2, 'dent': 0.2, 'dirt': 0.2} """self.target_ratios=target_ratiosor{'OK':0.5,'NG':0.5}self.current_counts={cls:0forclsinself.target_ratios}self.total_collected=0defshould_collect(self,class_name):"""判断当前类别是否还需要采集更多样本"""ifself.total_collected==0:returnTruecurrent_ratio=(self.current_counts.get(class_name,0)/max(self.total_collected,1))target_ratio=self.target_ratios.get(class_name,0)# 如果当前比例低于目标比例,继续采集returncurrent_ratiotarget_ratiodefrecord_sample(self,class_name):"""记录采集了一个样本"""self.current_counts[class_name]=self.current_counts.get(class_name,0)+1self.total_collected+=1defget_sampling_report(self):"""生成当前采集进度报告"""report="========== 采集进度报告 ==========\n"forcls,countinself.current_counts.items():ratio=count/max(self.total_collected,1)target=self.target_ratios.get(cls,0)status="✓"ifratio=target*0.9else"⚠"report+=(f"{status}{cls:20s}:{count:5d}({ratio*100:5.1f}%)"f" 目标:{target*100:.0f}%\n")report+=f"\n总计:{self.total_collected}张"returnreport# === 使用示例 ===if__name__=="__main__":balancer=SampleBalancer({'OK':0.4,'scratch':0.2,'dent':0.2,'dirt':0.1,'color_diff':0.1})# 模拟采集过程test_samples=['OK']*30+['scratch']*5+['dent']*8+['dirt']*3forclsintest_samples:ifbalancer.should_collect(cls):balancer.record_sample(cls)print(f"采集{cls},当前进度 OK")print(balancer.get_sampling_report())三、标注规范——团队协作的基础3.1 类别定义书模板标注之前,必须输出一份类别定义书,明确每个类别的定义、边界规则和典型图例。这是多人标注一致性的基石。importjsonfromtypingimportDict,List,Optionalfromdataclassesimportdataclass,field,asdictfromdatetimeimportdatetimeimportos@dataclassclassCategoryDefinition:"""类别定义"""id:intname:str# 类别名称description:str# 详细描述bounding_rule:str# 边界框标注规则typical_size:str# 典型尺寸(相对于图像)iou_threshold:float=0.5# NMS时的IoU阈值priority:str="normal"# 优先级: critical/high/normal/lowexamples:List[str]=field(default_factory=list)# 示例图片路径confusing_with:List[str]=field(default_factory=list)# 易混淆类别classAnnotationStandard:"""标注规范管理器"""def__init__(self,project_name:str,version:str="1.0"):self.project_name=project_name self.version=version self.categories:Dict[int,CategoryDefinition]={}self.global_rules:List[str]=[]self.quality_requirements:Dict[str,any]={}defadd_category(self,category:CategoryDefinition):"""添加一个类别定义"""self.categories[category.id]=categorydefadd_global_rule(self,rule:str):"""添加全局标注规则"""self.global_rules.append(rule)defset_quality_requirements(self,**kwargs)/
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进