瓷砖表面瑕疵检测
时间:2025-07-22 | 作者: | 阅读:0本项目聚焦佛山瓷砖表面瑕疵智能检测,针对质检依赖人工的问题,开发计算机视觉算法。处理含12类常见瑕疵的数据集,通过分块、翻转等七种数据增强,转换为COCO格式并解决类别不均衡。用PaddleDetection的Faster-RCNN等模型训练,经评估和预测,提升检测效率与准确性,减少人工依赖。
瓷砖表面瑕疵检测
1.背景介绍
佛山作为国内最大的瓷砖生产制造基地之一,拥有众多瓷砖厂家和品牌。经前期调研,瓷砖生产环节一般(不同类型砖工艺不一样,这里以抛釉砖为例)经过原材料混合研磨、脱水、压胚、喷墨印花、淋釉、烧制、抛光,最后进行质量检测和包装。得益于产业自动化的发展,目前生产环节已基本实现无人化。而质量检测环节仍大量依赖人工完成。一般来说,一条产线需要配2~6名质检工,长时间在高光下观察瓷砖表面寻找瑕疵。这样导致质检效率低下、质检质量层次不齐且成本居高不下。瓷砖表检是瓷砖行业生产和质量管理的重要环节,也是困扰行业多年的技术瓶颈。
聚焦瓷砖表面瑕疵智能检测,要求开发出高效可靠的计算机视觉算法,提升瓷砖表面瑕疵质检的效果和效率,降低对大量人工的依赖。要求算法尽可能快与准确的给出瓷砖疵点具体的位置和类别,主要考察疵点的定位和分类能力。
2. 数据处理
2.1 数据集介绍
本案例数据覆盖到了瓷砖产线所有常见瑕疵,包括粉团、角裂、滴釉、断墨、滴墨、B孔、落脏、边裂、缺角、砖渣、白边等。实拍图示例如下:
数据集:
白板数据包含有瑕疵图片、无瑕疵图片和标注数据。标注数据标注瑕疵位置和类别信息。训练集共15230张,测试集A共1762张
└── dataset ├── Readme.md ├── train_annos.json └── train_imgs登录后复制 ? ? ? ?
图片示例如下:
? ? ? ?新增:数据增强
- 将图像分割成小块后保存起来,包括翻转,旋转等七种不同组合的数据增强方法。
- 使用了paddle.vision.transforms内置的图像增强方法。
- 由于训练集太大,数据增强操作耗时较长,后续可采用随机数据增强。
# 安装需要的库函数!pip install scikit_image==0.15.0登录后复制 ? ?
通过下面的命令解压训练数据到工作目录:
In [?]#解压数据集,训练集先放work路径下,后面划分验证集时候在弄到paddledetection下,测试集直接放过去!unzip -q /home/aistudio/data/data66771/tile_round1_train_20201231.zip -d /home/aistudio/work/dataset登录后复制 ? ?
解压后目录形式如下:
└── dataset └── tile_round1_train_20201231 ├── Readme.md ├── train_annos.json └── train_imgs登录后复制 ? ?In [?]
# 定义离线数据增强方法def data_augmentation(image,label, mode): out = np.transpose(image, (1,2,0)) out_label = np.transpose(label, (1,2,0)) if mode == 0: # original out = out out_label = out_label elif mode == 1: # flip up and down out = np.flipud(out) out_label = np.flipud(out_label) elif mode == 2: # rotate counterwise 90 degree out = np.rot90(out) out_label = np.rot90(out_label) elif mode == 3: # rotate 90 degree and flip up and down out = np.rot90(out) out = np.flipud(out) out_label = np.rot90(out_label) out_label = np.flipud(out_label) elif mode == 4: # rotate 180 degree out = np.rot90(out, k=2) out_label = np.rot90(out_label, k=2) elif mode == 5: # rotate 180 degree and flip out = np.rot90(out, k=2) out = np.flipud(out) out_label = np.rot90(out_label, k=2) out_label = np.flipud(out_label) elif mode == 6: # rotate 270 degree out = np.rot90(out, k=3) out_label = np.rot90(out_label, k=3) elif mode == 7: # rotate 270 degree and flip out = np.rot90(out, k=3) out = np.flipud(out) out_label = np.rot90(out_label, k=3) out_label = np.flipud(out_label) return out,out_label登录后复制 ? ?In [5]
## 制作分块数据集import cv2import numpy as npimport mathimport glob import osdef Im2Patch(img, win, stride=1): k = 0 endc = img.shape[0] endw = img.shape[1] endh = img.shape[2] patch = img[:, 0:endw-win+0+1:stride, 0:endh-win+0+1:stride] TotalPatNum = patch.shape[1] * patch.shape[2] Y = np.zeros([endc, win*win,TotalPatNum], np.float32) for i in range(win): for j in range(win): patch = img[:,i:endw-win+i+1:stride,j:endh-win+j+1:stride] Y[:,k,:] = np.array(patch[:]).reshape(endc, TotalPatNum) k = k + 1 return Y.reshape([endc, win, win, TotalPatNum])def prepare_data(patch_size, stride, aug_times=1): ''' 该函数用于将图像切成方块,并进行数据增强 patch_size: 图像块的大小,本项目200*200 stride: 步长,每个图像块的间隔 aug_times: 数据增强次数,默认从八种增强方式中选择一种 ''' # train print('process training data') scales = [1] # 对数据进行随机放缩 files = glob.glob(os.path.join('work/dataset/tile_round1_train_20201231/train_imgs', '*.jpg')) files.sort() img_folder = 'work/img_patch' if not os.path.exists(img_folder): os.mkdir(img_folder) label_folder = 'work/label_patch' if not os.path.exists(label_folder): os.mkdir(label_folder) train_num = 0 for i in range(len(files)): img = cv2.imread(files[i]) label = cv2.imread(files[i].replace('images','gts')) h, w, c = img.shape for k in range(len(scales)): Img = cv2.resize(img, (int(h*scales[k]), int(w*scales[k])), interpolation=cv2.INTER_CUBIC) Label = cv2.resize(label, (int(h*scales[k]), int(w*scales[k])), interpolation=cv2.INTER_CUBIC) Img = np.transpose(Img, (2,0,1)) Label = np.transpose(Label, (2,0,1)) Img = np.float32(np.clip(Img,0,255)) Label = np.float32(np.clip(Label,0,255)) patches = Im2Patch(Img, win=patch_size, stride=stride) label_patches = Im2Patch(Label, win=patch_size, stride=stride) print(”file: %s scale %.1f # samples: %d“ % (files[i], scales[k], patches.shape[3]*aug_times)) for n in range(patches.shape[3]): data = patches[:,:,:,n].copy() label_data = label_patches[:,:,:,n].copy() for m in range(aug_times): data_aug,label_aug = data_augmentation(data,label_data, np.random.randint(1,8)) label_name = os.path.join(label_folder,str(train_num)+”_aug_%d“ % (m+1)+'.jpg') image_name = os.path.join(img_folder,str(train_num)+”_aug_%d“ % (m+1)+'.jpg') cv2.imwrite(image_name, data_aug,[int( cv2.IMWRITE_JPEG_QUALITY), 100]) cv2.imwrite(label_name, label_aug,[int( cv2.IMWRITE_JPEG_QUALITY), 100]) train_num += 1 print('training set, # samples %dn' % train_num) ## 生成数据prepare_data( patch_size=256, stride=200, aug_times=1)登录后复制 ? ?In [?]
# 重写数据读取类import paddleimport paddle.vision.transforms as Timport numpy as npimport globimport cv2# 重写数据读取类class DEshadowDataset(paddle.io.Dataset): def __init__(self,mode = 'train',is_transforms = False): label_path_ ='work/label_patch/*.jpg' jpg_path_ ='work/img_patch/*.jpg' self.label_list_ = glob.glob(label_path_) self.jpg_list_ = glob.glob(jpg_path_) self.is_transforms = is_transforms self.mode = mode scale_point = 0.95 self.transforms =T.Compose([ T.Normalize(data_format='HWC',), T.HueTransform(0.4), T.SaturationTransform(0.4), T.HueTransform(0.4), T.ToTensor(), ]) # 选择前95%训练,后5%验证 if self.mode == 'train': self.jpg_list_ = self.jpg_list_[:int(scale_point*len(self.jpg_list_))] self.label_list_ = self.label_list_[:int(scale_point*len(self.label_list_))] else: self.jpg_list_ = self.jpg_list_[int(scale_point*len(self.jpg_list_)):] self.label_list_ = self.label_list_[int(scale_point*len(self.label_list_)):] def __getitem__(self, index): jpg_ = self.jpg_list_[index] label_ = self.label_list_[index] data = cv2.imread(jpg_) # 读取和代码处于同一目录下的 lena.png # 转为 0-1 mask = cv2.imread(label_) data = cv2.cvtColor(data, cv2.COLOR_BGR2RGB) # BGR 2 RGB mask = cv2.cvtColor(mask, cv2.COLOR_BGR2RGB) # BGR 2 RGB data = np.uint8(data) mask = np.uint8(mask) if self.is_transforms: data = self.transforms(data) data = data/255 mask = T.functional.to_tensor(mask) return data,mask def __len__(self): return len(self.jpg_list_)登录后复制 ? ?In [?]
# 数据读取及增强可视化import paddle.vision.transforms as Timport matplotlib.pyplot as pltfrom PIL import Imagedataset = DEshadowDataset(mode='train',is_transforms = False )print('=============train dataset=============')img_,mask_ = dataset[3] # mask 始终大于 imgimg = Image.fromarray(img_)mask = Image.fromarray(mask_)#当要保存的图片为灰度图像时,灰度图像的 numpy 尺度是 [1, h, w]。需要将 [1, h, w] 改变为 [h, w]plt.figure(figsize=(12, 6))plt.subplot(1,2,1),plt.xticks([]),plt.yticks([]),plt.imshow(img)plt.subplot(1,2,2),plt.xticks([]),plt.yticks([]),plt.imshow(mask)plt.show()登录后复制 ? ?
2.2 环境准备
解压本案例用到的PaddleDetection源码:
In [?]#解压paddle的目标检测套件源码!unzip -o -q /home/aistudio/data/data113827/PaddleDetection-release-2.2_tile.zip -d /home/aistudio/work/登录后复制 ? ?
安装依赖库:
In [?]#安装至全局,如果重启项目,这几个依赖和库需要重新安装%cd /home/aistudio/cocoapi/PythonAPI!make install%cd ../..#安装依赖%cd /home/aistudio/work/PaddleDetection-release-2.2!pip install -r requirements.txt!python setup.py install登录后复制 ? ?
2.3 数据分析
运行下面的代码统计不同类别数据数量和图片的平均高度和宽度:
In [?]#调用一些需要的第三方库import numpy as npimport pandas as pdimport shutilimport jsonimport osimport cv2import globfrom PIL import Image#统计一下类别path = ”/home/aistudio/work/dataset/tile_round1_train_20201231/train_annos.json“dict_class = { ”0“:0, ”1“:0, ”2“:0, ”3“:0, ”4“:0, ”5“:0, ”6“:0}id_s = 0image_width,image_height = 0,0with open(path,”r“) as f: files = json.load(f) #遍历标注文件 for file_img in files: id_s += 1 #统计类别 file_class = file_img[”category“] dict_class[str(file_class)] += 1 #统计图片平均像素 image_height += file_img[”image_height“] image_width += file_img[”image_width“] #if id_s % 1000 is 0: # print(”处理到第{}个标注“.format(id_s))print(”类别:“,dict_class)print(”图片平均高{},图片平均宽{}“.format(image_height/id_s,image_width/id_s))登录后复制 ? ?
具体得到的结论如下:
- 类别不均衡,类别: {'0': 0, '1': 576, '2': 2151, '3': 2174, '4': 1112, '5': 8886, '6': 331}
- 图片像素尺寸比较大:图片平均高5562.212738017071,图片平均宽7474.7293499671705
2.4 将数据标注转为coco格式
新增:样本类别均衡化
- paddledetection支持coco格式和voc格式,原数据标注不是这2种格式;
- 其中0类也就是背景类是没有数据的,所以可以去了,类别是1-6,共6个类别。
- 样本类别均衡化方法: (1)收集数据 针对少量样本数据,可以尽可能去扩大这些少量样本的数据集,或者尽可能去增加他们特有的特征来丰富数据的多样性 (2)将分类任务转换成异常检测 如果少数类样本太少,少数类的结构可能并不能被少数类样本的分布很好地表示,那么用平衡数据或调整算法的方法不一定有效。如果这些少数类样本在特征空间中再分布的比较散,情况会更加糟糕。这时候不如将其转换为无监督的异常检测算法,不用过多的去考虑将数据转换为平衡问题来解决。 (3)调整权重 可以简单的设置损失函数的权重,更多的关注少数类。在python的scikit-learn中我们可以使用class_weight参数来设置权重。为了权衡不同类型错误所造成的不同损失,可为错误赋予“非均等代价”例如在医疗诊断中,错误地把健康人诊断为患者可能会带来进一步检查的麻烦,但是错误地把患者诊断为健康人,则可能会丧失了拯救生命的最佳时机。 (4)阈值调整 直接基于原始训练集进行学习,但在用训练好的分类器进行预测时,将原本默认为0.5的阈值调整到[公式]即可。(大部分是负样本,因此分类器倾向于给出较低的分数)
- 这里选择的是调整权重的办法,使用class_weight='balanced'方法来进行样本均衡处理
class Fabric2COCO: def __init__(self, is_mode = ”train“ ): self.images = [] self.annotations = [] self.categories = [] self.img_id = 0 self.ann_id = 0 self.is_mode = is_mode if not os.path.exists(”/home/aistudio/work/PaddleDetection-release-2.2/dataset/coco/{}“.format(self.is_mode)): os.makedirs(”/home/aistudio/work/PaddleDetection-release-2.2/dataset/coco/{}“.format(self.is_mode)) def to_coco(self, anno_file,img_dir): self._init_categories() anno_result= pd.read_json(open(anno_file,”r“)) ca=[] if self.is_mode == ”train“: anno_result = anno_result.head(int(anno_result['name'].count()*0.9))#取数据集前百分之90 elif self.is_mode == ”val“: anno_result = anno_result.tail(int(anno_result['name'].count()*0.1)) name_list=anno_result[”name“].unique()#返回唯一图片名字 for img_name in name_list: img_anno = anno_result[anno_result[”name“] == img_name]#取出此图片的所有标注 bboxs = img_anno[”bbox“].tolist()#返回list defect_names = img_anno[”category“].tolist() assert img_anno[”name“].unique()[0] == img_name ca.extend(defect_names) img_path=os.path.join(img_dir,img_name) #img =cv2.imread(img_path) #h,w,c=img.shape #这种读取方法更快 img = Image.open(img_path) w, h = img.size #h,w=6000,8192 self.images.append(self._image(img_path,h, w)) self._cp_img(img_path)#复制文件路径 if self.img_id % 200 is 0: print(”处理到第{}张图片“.format(self.img_id)) for bbox, label in zip(bboxs, defect_names): annotation = self._annotation(label, bbox) self.annotations.append(annotation) self.ann_id += 1 self.img_id += 1 #对于0,1,2,3,4,5,6的标签增加平衡权重 from sklearn.utils.class_weight import compute_class_weight class_weight = 'balanced' classes = np.array([1, 2, 3, 4, 5, 6]) #标签类别 weight = compute_class_weight(class_weight, classes, ca) instance = {} instance['info'] = 'fabric defect' instance['license'] = ['none'] instance['images'] = self.images instance['annotations'] = self.annotations instance['categories'] = self.categories return instance def _init_categories(self): #1,2,3,4,5,6个类别 for v in range(1,7): print(v) category = {} category['id'] = v category['name'] = str(v) category['supercategory'] = 'defect_name' self.categories.append(category) def _image(self, path,h,w): image = {} image['height'] = h image['width'] = w image['id'] = self.img_id image['file_name'] = os.path.basename(path)#返回path最后的文件名 return image def _annotation(self,label,bbox): area=(bbox[2]-bbox[0])*(bbox[3]-bbox[1]) points=[[bbox[0],bbox[1]],[bbox[2],bbox[1]],[bbox[2],bbox[3]],[bbox[0],bbox[3]]] annotation = {} annotation['id'] = self.ann_id annotation['image_id'] = self.img_id annotation['category_id'] = label annotation['segmentation'] = []# np.asarray(points).flatten().tolist() annotation['bbox'] = self._get_box(points) annotation['iscrowd'] = 0 annotation[”ignore“] = 0 annotation['area'] = area return annotation def _cp_img(self, img_path): shutil.copy(img_path, os.path.join(”/home/aistudio/work/PaddleDetection-release-2.2/dataset/coco/{}“.format(self.is_mode), os.path.basename(img_path))) def _get_box(self, points): min_x = min_y = np.inf max_x = max_y = 0 for x, y in points: min_x = min(min_x, x) min_y = min(min_y, y) max_x = max(max_x, x) max_y = max(max_y, y) '''coco,[x,y,w,h]''' return [min_x, min_y, max_x - min_x, max_y - min_y] def save_coco_json(self, instance, save_path): import json with open(save_path, 'w') as fp: json.dump(instance, fp, indent=1, separators=(',', ': '))#缩进设置为1,元素之间用逗号隔开 , key和内容之间 用冒号隔开登录后复制 ? ?In [?]
'''转换有瑕疵的样本为coco格式'''#训练集,划分90%做为训练集img_dir = ”/home/aistudio/work/dataset/tile_round1_train_20201231/train_imgs“anno_dir=”/home/aistudio/work/dataset/tile_round1_train_20201231/train_annos.json“fabric2coco = Fabric2COCO()train_instance = fabric2coco.to_coco(anno_dir,img_dir)if not os.path.exists(”/home/aistudio/work/PaddleDetection-release-2.2/dataset/coco/annotations/“): os.makedirs(”/home/aistudio/work/PaddleDetection-release-2.2/dataset/coco/annotations/“)fabric2coco.save_coco_json(train_instance, ”/home/aistudio/work/PaddleDetection-release-2.2/dataset/coco/annotations/“+'instances_{}.json'.format(”train“))登录后复制 ? ?In [?]
'''转换有瑕疵的样本为coco格式'''#验证集,划分10%做为验证集img_dir = ”/home/aistudio/work/dataset/tile_round1_train_20201231/train_imgs“anno_dir=”/home/aistudio/work/dataset/tile_round1_train_20201231/train_annos.json“fabric2coco = Fabric2COCO(is_mode = ”val“)train_instance = fabric2coco.to_coco(anno_dir,img_dir)if not os.path.exists(”/home/aistudio/work/PaddleDetection-release-2.2/dataset/coco/annotations/“): os.makedirs(”/home/aistudio/work/PaddleDetection-release-2.2/dataset/coco/annotations/“)fabric2coco.save_coco_json(train_instance, ”/home/aistudio/work/PaddleDetection-release-2.2/dataset/coco/annotations/“+'instances_{}.json'.format(”val“))登录后复制 ? ?
可视化
需要修改faster_rcnn_r50_fpn_2x网络配置文件
In [?]%cd PaddleDetection/%env CUDA_VISIBLE_DEVICES=0#使用visualDL记录曲线变化!python -u tools/train.py -c ../work/faster_rcnn_r50_fpn_2x.yml --use_vdl=True --vdl_log_dir=vdl_dir/scalar -r output/faster_rcnn_r50_fpn_2x/22000.pdparams登录后复制 ? ?
3. 模型训练
配置文件已经放在work下,work/faster_rcnn_r50_fpn_2x.yml。?--eval参数表示在训练过程中在验证集上验证模型。
在某个模型基础上继续训练加上如?-r output/faster_rcnn_r50_fpn_2x/12.pdparams的参数,在第12个epoch得到的模型上继续训练。
In [?]#开始训练%cd /home/aistudio/work/PaddleDetection-release-2.2/#%env CUDA_VISIBLE_DEVICES=0!python tools/train.py -c /home/aistudio/work/faster_rcnn_r50_fpn_2x.yml --eval # -r /home/aistudio/work/PaddleDetection-release-2.2/output/faster_rcnn_r50_fpn_2x/12.pdparams登录后复制 ? ?
4. 模型评估
模型评估需要指定被评估模型,如-o weights=output/faster_rcnn_r50_fpn_2x/best_model.pdparams:
In [?]#模型评估。该过程大概需要半小时。%cd /home/aistudio/work/PaddleDetection-release-2.2/!python tools/eval.py -c /home/aistudio/work/faster_rcnn_r50_fpn_2x.yml -o weights=/home/aistudio/work/PaddleDetection-release-2.2/output/faster_rcnn_r50_fpn_2x/best_model.pdparams登录后复制 ? ?
5. 模型预测
模型预测调用tools/infer.py文件,需要指定模型路径、被预测的图像路径如--infer_img=dataset/coco/val/235_7_t20201127123214965_CAM2.jpg、预测结果输出目录如--output_dir=infer_output/等:
预测结果会直接画在图像上保存在output_dir目录下。
In [?]#模型预测%cd /home/aistudio/work/PaddleDetection-release-2.2/!python -u tools/infer.py -c /home/aistudio/work/faster_rcnn_r50_fpn_2x.yml --output_dir=infer_output/ --save_txt=True -o weights=/home/aistudio/work/PaddleDetection-release-2.2/output/faster_rcnn_r50_fpn_2x/best_model.pdparams --infer_img=/home/aistudio/work/PaddleDetection-release-2.2/dataset/coco/val/235_7_t20201127123214965_CAM2.jpg登录后复制 ? ?
检测结果图:
? ? ? ?总结
本项目在基于PaddleDetection中Faster-RCNN模型实现瓷砖表面瑕疵检测项目上,为提升瓷砖表面瑕疵质检的效果和效率,对其进行了进一步优化和尝试,其中包括数据增强、数据类别均衡、可视化、尝试更换更优网络、尝试更换其他结构(损失函数和优化器)登录后复制 ? ? ? ?
- 其中数据增强和数据类别均衡均已完成,其具体描述见项目数据处理部分,美中不足的是,由于训练集数据过大,数据增强跑完时间稍长,后续可采用对随机样本增强进一步优化。
- 尝试更换网络结构中,这里引入了配置文件mask_rcnn_r50_fpn.yml,我们采用MASK RCNN,因为Mask RCNN可以看做是一个以Faster RCNN为原型的通用实例分割架构,增加了一个分支用于分割任务,对于Faster RCNN的每个Proposal Box都要使用FCN进行语义分割,分割任务与定位、分类任务是同时进行的。引入了RoI Align代替Faster RCNN中的RoI Pooling。这对于mask的精度有很大影响。引入语义分割分支,实现了mask和class预测的关系的解耦,mask分支只做语义分割,类型预测的任务交给另一个分支。
- 可视化中,我们利用visualDL实现可视化,但对配置文件的配置出了些许差错,但也都是一种不错的尝试。
福利游戏
相关文章
更多-
- 昇腾杯-变化检测赛道复赛方案分享——PaddleCD
- 时间:2025-07-22
-
- 【图像去噪】第七期论文复现赛——SwinIR
- 时间:2025-07-22
-
- 使用PaddleDetection2.0自定义数据集实现火焰识别预测
- 时间:2025-07-22
-
- 【遥感影像分类】使用PaddleAPI搭建ResNet50实现遥感影像分类任务
- 时间:2025-07-22
-
- 心音智能检测
- 时间:2025-07-22
-
- 电脑蓝屏后重启反复循环 无法进入系统如何处理
- 时间:2025-07-22
-
- 电脑打印文档时出现乱码,如何解决?
- 时间:2025-07-22
-
- “机器学习”系列之决策树
- 时间:2025-07-22
大家都在玩
大家都在看
更多-
- 剑桥数字货币交易所:开启资产新纪元
- 时间:2025-07-22
-
- 称亲自开上了陡坡 余承东晒享界S9T实车:颜值与实力并存
- 时间:2025-07-22
-
- MSN币未来展望:机遇与挑战并存
- 时间:2025-07-22
-
- 电脑蓝屏时屏幕出现乱码 是显卡问题还是显示器故障
- 时间:2025-07-22
-
- Switch 2 OLED中框遭曝光:闲鱼惊现研发样品
- 时间:2025-07-22
-
- 电脑安装软件时提示 “权限不足”,怎么获取权限?
- 时间:2025-07-22
-
- 全球首架“三证齐全”吨级以上eVTOL交付:用于低空货运场
- 时间:2025-07-22
-
- vivo在印度市场连续4季度销量夺冠:Q2狂销810万台
- 时间:2025-07-22