缺陷检测
前言
目标检测的比赛是真的少,研一时候因为疫情在家,荒废了。研二想着赶快补一补,想着参加一下比赛什么的,但是很多比赛都是一年一届,报名时间很短,错过许多。最近正好看到这个华为举办的比赛,虽然也不是什么大比赛,但咱也知足了,本来就是为了学到东西,大比赛牛人更多,更让人绝望。

如图,虽说这个比赛是缺陷检测,但是和目标检测基本一样,懂得都懂。。
比赛链接:
https://competition.huaweicloud.com/information/1000041490/circumstance
导入模型
这一步很简单,就是读取yaml文件。我用了yolov5。
def get_object_detector(num_classes):
# load an instance segmentation model pre-trained pre-trained on COCO
# logger.info('remove pretrained')
# model = torchvision.models.detection.fasterrcnn_resnet50_fpn(
# pretrained=False, pretrained_backbone=False)
# # get number of input features for the classifier
# in_features = model.roi_heads.box_predictor.cls_score.in_features
# # replace the pre-trained head with a new one
# logger.info('{}-{}'.format(in_features, num_classes))
# model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
logger.info('work_dir' + os.getcwd())
model = Model('/home/mind/model/yolov5m.yaml', nc=num_classes) # 因为在云端相对路径和本地不一样,所以我这里直接用绝对路径。官方的开发文档给了教程,懒得再改了。
return model
其实这一步完全不需要。baseline写这个函数只是为了修改检测头的输出类别数,也就是这一行:
model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
因为baseline是先加载模型字典,再加载参数:
self.model.load_state_dict(torch.load(self.model_path,map_location='cpu')['model'])
我们也可以一次性直接加载整个模型:
self.model = attempt_load(self.model_path, map_location='cpu')
这个加载方法是yolov5重写的:
def attempt_load(weights, map_location=None, inplace=True):
from models.yolo import Detect, Model
# Loads an ensemble of models weights=[a,b,c] or a single model weights=[a] or weights=a
model = Ensemble()
for w in weights if isinstance(weights, list) else [weights]:
ckpt = torch.load(w, map_location=map_location) # load
model.append(ckpt['ema' if ckpt.get('ema') else 'model'].float().fuse().eval()) # FP32 model
# Compatibility updates
for m in model.modules():
if type(m) in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU, Detect, Model]:
m.inplace = inplace # pytorch 1.7.0 compatibility
elif type(m) is Conv:
m._non_persistent_buffers_set = set() # pytorch 1.6.0 compatibility
if len(model) == 1:
return model[-1] # return model
else:
print(f'Ensemble created with {weights}\n')
for k in ['names']:
setattr(model, k, getattr(model[-1], k))
model.stride = model[torch.argmax(torch.tensor([m.stride.max() for m in model])).int()].stride # max stride
return model # return ensemble
到了这里,就涉及到yolov5的坑了:
v5加载模型的时候会加载路径,检查当前路径下有没有models这个module。即使你自己构建文件夹,也必须按照他的目录结构来使用yolo.py。大坑。
网上的一些解决方法。这个博客是正确的。
https://blog.csdn.net/weixin_41809530/article/details/116446002
最终我的目录结构:

因为这两个文件是模型中会用到的,所以必须这样。很烦。
数据处理
刚开始不了解,把数据集下载到本地,居然还花钱。。几块钱也就交了。
官方给了baseline的教程,用训练作业,云端训练。云端解压,不需要下载到本地。
我们应该清楚,一般模型传入的是tensor,ndarray等格式,也就是矩阵。而把图片处理成这样的格式就用到了opencv,PIL等图像处理包。
这里有一点,PIL支持读取字节流,如下。baseline用的PIL,我用了opencv,会报错。
Image.open(io.BytesIO(image))
1-PIL转换opencv
image = Image.open(io.BytesIO(image))
img = cv2.cvtColor(np.asarray(image), cv2.COLOR_RGB2BGR)
2-坐标变换
模型输出的是一个tensor向量,我们要得到的对应原图尺寸的真实坐标。这时候就用到了v5中的一个函数,scale_coords()
scale_coords()
def scale_coords(img1_shape, coords, img0_shape, ratio_pad=None):
# Rescale coords (xyxy) from img1_shape to img0_shape
if ratio_pad is None: # calculate from img0_shape
gain = min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) # gain = old / new
pad = (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 # wh padding
else:
gain = ratio_pad[0][0]
pad = ratio_pad[1]
coords[:, [0, 2]] -= pad[0] # x padding
coords[:, [1, 3]] -= pad[1] # y padding
coords[:, :4] /= gain
clip_coords(coords, img0_shape)
return coords
分别讲一下传入的参数的含义。
其中img0_shape顾名思义就是image_original,也就是原始图片的尺寸,
即
img0 = cv2.imread(file)
而这里的img1_shape对应的是resize完的图片,v5中用到的函数是latterbox()。所以也就是image_letterbox()。这个函数中封装了cv2.resize().包括了padding等操作。
在卷积前对图像的操作都分离开,应该是有利于加快训练的速度,应该对推理也有帮助。
letterbox()
def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32):
# Resize and pad image while meeting stride-multiple constraints
shape = img.shape[:2] # current shape [height, width]
if isinstance(new_shape, int):
new_shape = (new_shape, new_shape)
# Scale ratio (new / old)
r = min(new_shape[0] / shape[0], new_shape[1] / shape[1])
if not scaleup: # only scale down, do not scale up (for better test mAP)
r = min(r, 1.0)
# Compute padding
ratio = r, r # width, height ratios
new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r))
dw, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] # wh padding
if auto: # minimum rectangle
dw, dh = np.mod(dw, stride), np.mod(dh, stride) # wh padding
elif scaleFill: # stretch
dw, dh = 0.0, 0.0
new_unpad = (new_shape[1], new_shape[0])
ratio = new_shape[1] / shape[1], new_shape[0] / shape[0] # width, height ratios
dw /= 2 # divide padding into 2 sides
dh /= 2
if shape[::-1] != new_unpad: # resize
img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1))
left, right = int(round(dw - 0.1)), int(round(dw + 0.1))
img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border
return img, ratio, (dw, dh)
np.mod()
numpy.mod(arr1, arr2, /, out=None, *, where=True, casting=’same_kind’, order=’K’, dtype=None, subok=True[, signature, extobj], ufunc ‘remainder’)
numpy.mod()是一个用于在numpy中进行数学运算的函数,它返回两个数组arr1和arr2之间的除法元素余数,即 arr1 % arr2 当arr2为0且arr1和arr2都是整数数组时,返回0。
参数可以是单个数字,也可以是数组,也就是列表。
cv2.resize()
我们可以看到v5使用的resize方法是cv2.INTER_LINEAR。
关于resize的详细使用可以看官网https://docs.opencv.org/
或者这个博客,也就是翻译了一下。
https://blog.csdn.net/wzhrsh/article/details/101630396
if shape[::-1] != new_unpad: # resize
img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR)
img是我们的原始图像。
这里要讲一下,cv.resize()会将图像shape中的高宽交换。我们用cv2.imread()读一张图片:
import cv2
img = cv2.imread('zidane.jpg')
print(img.shape)
(720, 1280, 3) 对应(h,w,c)
我们再resize一下:
img1 = cv2.resize(img,(720,1280))
print(img1.shape)
(1280, 720, 3)
可以看到宽高互换了。我们也可以理解为resize的输入参数顺序为(w,h)也就是输出的图片的(w,h)正好与图片的shape顺序相反。有点绕。。
我们再看这个插值方法:interpolation=cv2.INTER_LINEAR。双线性插值,是默认方法,这里写出来只是为了直观。
也有一些别的方法,但是不了解区别。
最后又用了cv2.copyMakeBorder()
参考链接:https://blog.csdn.net/qq_36560894/article/details/105416273
img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border
其实这一步加不加都不影响代码运行,但是可能会对AP有影响。
到这里对图像的处理就完成了,
自定义推理代码
推理代码高了我好久,主要是这个service后还不能在本地跑。因为有的包是华为云自己的。所以我只能修改一次上传部署一次。每次都得十多分钟,这么折腾了两三天才把service配好。
懒得讲了,没什么意义。这里留作纪念贴一下。
# -*- coding: utf-8 -*-
import os
import torch
import torchvision
from torchvision.models.detection.faster_rcnn import FastRCNNPredictor
from model_service.pytorch_model_service import PTServingBaseService
import time
from metric.metrics_manager import MetricsManager
from torchvision.transforms import functional as F
import log
import json
from general import non_max_suppression,scale_coords
from models.yolo import Model
from experimental import attempt_load
from datasets import LoadImages
from datasets import letterbox
from PIL import Image
import cv2
import numpy as np
Image.MAX_IMAGE_PIXELS = 1000000000000000
logger = log.getLogger(__name__)
logger.info(torch.__version__)
logger.info(torchvision.__version__)
def get_object_detector(num_classes):
# load an instance segmentation model pre-trained pre-trained on COCO
# logger.info('remove pretrained')
# model = torchvision.models.detection.fasterrcnn_resnet50_fpn(
# pretrained=False, pretrained_backbone=False)
# # get number of input features for the classifier
# in_features = model.roi_heads.box_predictor.cls_score.in_features
# # replace the pre-trained head with a new one
# logger.info('{}-{}'.format(in_features, num_classes))
# model.roi_heads.box_predictor = FastRCNNPredictor(in_features, num_classes)
print(os.getcwd())
logger.info('work_dir' + os.getcwd())
model = Model('/home/mind/model/yolov5m.yaml', nc=num_classes)
return model
class ImageClassificationService(PTServingBaseService):
def __init__(self, model_name, model_path, **kwargs):
self.model_name = model_name
num_classes = 10
logger.info(' {} '.format(model_path))
for key in kwargs:
logger.info('{}-{}'.format(key, kwargs[key]))
self.model_path = model_path
self.model = get_object_detector(num_classes)
self.use_cuda = False
self.device = torch.device("cuda"
if torch.cuda.is_available() else "cpu")
if torch.cuda.is_available():
logger.info('Using GPU for inference')
self.model.to(self.device)
self.model = attempt_load(self.model_path)
else:
logger.info('Using CPU for inference')
self.model = attempt_load(self.model_path, map_location='cpu')
self.model.eval()
print("model already")
def _preprocess(self, data):
return data
def _inference(self, data):
preprocessed_data = {}
for k, v in data.items():
print(k, v)
for file_name, file_content in v.items():
img = Image.open(file_content).convert("RGB")
img0 = cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) # BGR
img = letterbox(img0)[0]
# Convert
img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x416x416
img = np.ascontiguousarray(img)
img = torch.from_numpy(img).to(self.device)
img = img.float() # uint8 to fp16/32
img /= 255.0 # 0 - 255 to 0.0 - 1.0
if img.ndimension() == 3:
img = img.unsqueeze(0)
preprocessed_data[k] = img
data = preprocessed_data
img = data["input_img"]
data = img
pred = self.model(data)[0]
pred = non_max_suppression(pred, conf_thres=0.25, iou_thres=0.45)
for i, det in enumerate(pred):
det[:, :4] = scale_coords(img.shape[2:], det[:, :4], img0.shape)
pred[i] = {'boxes': det[:, :4].cpu().detach().numpy().tolist(),
'labels': (det[:, 5:6].cpu().detach().int() + 1).numpy().flatten().tolist(),
'scores': det[:, 4:5].cpu().detach().numpy().flatten().tolist()}
results = {'result': pred}
return results
def _postprocess(self, data):
return data
def inference(self, data):
pre_start_time = time.time()
data = self._preprocess(data)
infer_start_time = time.time()
# Update preprocess latency metric
pre_time_in_ms = (infer_start_time - pre_start_time) * 1000
logger.info('preprocess time: ' + str(pre_time_in_ms) + 'ms')
if self.model_name + '_LatencyPreprocess' in MetricsManager.metrics:
MetricsManager.metrics[self.model_name + '_LatencyPreprocess'].update(pre_time_in_ms)
data = self._inference(data)
infer_end_time = time.time()
infer_in_ms = (infer_end_time - infer_start_time) * 1000
logger.info('infer time: ' + str(infer_in_ms) + 'ms')
data = self._postprocess(data)
# Update inference latency metric
post_time_in_ms = (time.time() - infer_end_time) * 1000
logger.info('postprocess time: ' + str(post_time_in_ms) + 'ms')
if self.model_name + '_LatencyInference' in MetricsManager.metrics:
MetricsManager.metrics[self.model_name + '_LatencyInference'].update(post_time_in_ms)
# Update overall latency metric
if self.model_name + '_LatencyOverall' in MetricsManager.metrics:
MetricsManager.metrics[self.model_name + '_LatencyOverall'].update(pre_time_in_ms + post_time_in_ms)
logger.info('latency: ' + str(pre_time_in_ms + infer_in_ms + post_time_in_ms) + 'ms')
data['latency_time'] = pre_time_in_ms + infer_in_ms + post_time_in_ms
return data
其实完全不需要写的这么麻烦。
是自己没看教程。v5官网写了加载自己模型的方法,用torch.hub.load()
model = torch.hub.load('ultralytics/yolov5', 'custom', path='path/to/best.pt') # default
model = torch.hub.load('path/to/yolov5', 'custom', path='path/to/best.pt', source='local') # local repo
但是懒得改了。
本地训练
因为我只有一块8gb的2070。。所以我先用的v5m,batch-size用的是1。。2都跑不起来。epoch为300。但是无奈用上v5速度还是太慢,一次一个小时多,300个epoch大概要用15天。。这块不需要讲什么,直接训练就行。
数据集划分
这里又学到一个函数,pytorch自带的。之前我都是按顺序划分的训练集验证集。。惭愧。。
torch.utils.data.random_split()
函数名:torch.utils.data.random_split()
dataset = os.listdir("E:\\qxdetection\\data\\images")
train_dataset, test_dataset = random_split(
dataset=dataset,
lengths=[4310, 333], # 两者和必须和数据集数量严格相等
generator=torch.Generator().manual_seed(0)
)
需要注意的是lengths,只能划分为两部分,你可以先划分训练集,测试集,然后拿得到的训练集再划分训练集,验证集。同时lengths的两部分数字的和必须等于你自己数据集的总量。这点比较坑,居然不能拿比例替代。
总共4643张,因为v5跑的时候必须又验证集。所以我简单的划分了一下。
先用v5m训练了几十个epoch,service跑通后开始换成v5l6。
6指的是总共有6层,从3层FPN变到4层FPN结构。
后记
最近事情太多,过了好久发现网页上这篇文章还是打开的,没写完。。。。给这篇文章划个句号吧。
这次比赛还是吸取到很多经验的,总结一下。
硬件
别人同样的v5模型,batch-size 64,image-size 1280,甚至原尺寸,我一块8g2070,batch-size 1,玩个毛。。。没得比,同样的模型人家比我高十几个点,训练时间比你少几十倍。
关于base
即使到了2021,大部分的baseline仍然用的fastrcnn或者比较老的模型,这是因为这些模型是pytorch模型官方库自带的,不需要写太多代码,方便参赛人员自定义,而且方便人们较短的时间内实现提升,有的baseline的conf,iou-thres都是故意往差了改,其实你把这些一改就能获得提升。但是到了后期想冲名次肯定不行。肯定是最新的模型,这次比赛前十基本都是transformer,主要检测相关的比赛还是以精度为准,并不把速度也当作一个成绩指标。因为精度在不同的机器上是一样的,但是速度每台机器都有差别。。
就这么多吧,要研三了,赶快开始找工作了,写毕业论文了。。可惜没进前五十,我觉得拿v5p6梭哈也能进前50。v5实在太优秀了。
顺便贴一下我的改进,欢迎大家尝试:https://github.com/SpongeBab/yolov5
就是把PANet变成四层了。。精度有提升,速度当然降低了(🤦。。)
395

被折叠的 条评论
为什么被折叠?



