本项目聚焦佛山瓷砖表面瑕疵智能检测,针对质检依赖人工的问题,开发计算机视觉算法。处理含12类常见瑕疵的数据集,通过分块、翻转等七种数据增强,转换为COCO格式并解决类别不均衡。用PaddleDetection的Faster-RCNN等模型训练,经评估和预测,提升检测效率与准确性,减少人工依赖。
☞☞☞AI 智能聊天, 问答助手, AI 智能搜索, 免费无限量使用 DeepSeek R1 模型☜☜☜

佛山作为国内最大的瓷砖生产制造基地之一,拥有众多瓷砖厂家和品牌。经前期调研,瓷砖生产环节一般(不同类型砖工艺不一样,这里以抛釉砖为例)经过原材料混合研磨、脱水、压胚、喷墨印花、淋釉、烧制、抛光,最后进行质量检测和包装。得益于产业自动化的发展,目前生产环节已基本实现无人化。而质量检测环节仍大量依赖人工完成。一般来说,一条产线需要配2~6名质检工,长时间在高光下观察瓷砖表面寻找瑕疵。这样导致质检效率低下、质检质量层次不齐且成本居高不下。瓷砖表检是瓷砖行业生产和质量管理的重要环节,也是困扰行业多年的技术瓶颈。
聚焦瓷砖表面瑕疵智能检测,要求开发出高效可靠的计算机视觉算法,提升瓷砖表面瑕疵质检的效果和效率,降低对大量人工的依赖。要求算法尽可能快与准确的给出瓷砖疵点具体的位置和类别,主要考察疵点的定位和分类能力。
本案例数据覆盖到了瓷砖产线所有常见瑕疵,包括粉团、角裂、滴釉、断墨、滴墨、B孔、落脏、边裂、缺角、砖渣、白边等。实拍图示例如下:

针对某些缺陷在特定视角下的才能拍摄到,每块砖拍摄了三张图,包括低角度光照黑白图、高角度光照黑白图、彩色图,示例如下:
白板数据包含有瑕疵图片、无瑕疵图片和标注数据。标注数据标注瑕疵位置和类别信息。训练集共15230张,测试集A共1762张
└── dataset
├── Readme.md
├── train_annos.json
└── train_imgs图片示例如下:
# 安装需要的库函数!pip install scikit_image==0.15.0
通过下面的命令解压训练数据到工作目录:
#解压数据集,训练集先放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# 定义离线数据增强方法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## 制作分块数据集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 %d\n' % train_num)
## 生成数据prepare_data( patch_size=256, stride=200, aug_times=1)# 重写数据读取类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_)# 数据读取及增强可视化import paddle.vision.transforms as Timport matplotlib.pyplot as pltfrom PIL import Image
dataset = 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()解压本案例用到的PaddleDetection源码:
#解压paddle的目标检测套件源码!unzip -o -q /home/aistudio/data/data113827/PaddleDetection-release-2.2_tile.zip -d /home/aistudio/work/
安装依赖库:
#安装至全局,如果重启项目,这几个依赖和库需要重新安装%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
#调用一些需要的第三方库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))具体得到的结论如下:
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和内容之间 用冒号隔开'''转换有瑕疵的样本为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"))'''转换有瑕疵的样本为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网络配置文件
%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配置文件已经放在work下,work/faster_rcnn_r50_fpn_2x.yml。 --eval参数表示在训练过程中在验证集上验证模型。
在某个模型基础上继续训练加上如 -r output/faster_rcnn_r50_fpn_2x/12.pdparams的参数,在第12个epoch得到的模型上继续训练。
#开始训练%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模型评估需要指定被评估模型,如-o weights=output/faster_rcnn_r50_fpn_2x/best_model.pdparams:
#模型评估。该过程大概需要半小时。%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模型预测调用tools/infer.py文件,需要指定模型路径、被预测的图像路径如--infer_img=dataset/coco/val/235_7_t20201127123214965_CAM2.jpg、预测结果输出目录如--output_dir=infer_output/等:
预测结果会直接画在图像上保存在output_dir目录下。
#模型预测%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模型实现瓷砖表面瑕疵检测项目上,为提升瓷砖表面瑕疵质检的效果和效率,对其进行了进一步优化和尝试,其中包括数据增强、数据类别均衡、可视化、尝试更换更优网络、尝试更换其他结构(损失函数和优化器)
以上就是瓷砖表面瑕疵检测的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号