问题分析:
使用OpenCV中的DNN模块可以加载我们转化好的ONNX模型,但是由于模型的一些操作可能导致DNN模块中的ONNX加载模块识别不了,从而导致报错,这里会报 start (int)shape.size() && <= end (int)shape.size()的错误,在YOLOV7中如果不使用作者的export.py的情况下,直接对我们的模型转换的话会存在一个后处理操作,如下代码所示。
def forward(self, x):
# x = x.copy() # for profiling
z = [] # inference output
self.training |= self.export
for i in range(self.nl):
x[i] = self.m[i](x[i]) # conv
bs, _, ny, nx = x[i].shape # x(bs,255,20,20) to x(bs,3,20,20,85)
x[i] = x[i].view(bs, self.na, self.no, ny, nx).permute(0, 1, 3, 4, 2).contiguous()
if not self.training: # inference
if self.grid[i].shape[2:4] != x[i].shape[2:4]:
self.grid[i] = self._make_grid(nx, ny).to(x[i].device)
y = x[i].sigmoid()
if not torch.onnx.is_in_onnx_export():
y[..., 0:2] = (y[..., 0:2] * 2. - 0.5 + self.grid[i]) * self.stride[i] # xy
y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh 给他变身
else:
xy, wh, conf = y.split((2, 2, self.nc + 1), 4) # y.tensor_split((2, 4, 5), 4) # torch 1.8.0
xy = xy * (2. * self.stride[i]) + (self.stride[i] * (self.grid[i] - 0.5)) # new xy
wh = wh ** 2 * (4 * self.anchor_grid[i].data) # new wh
y = torch.cat((xy, wh, conf), 4)
z.append(y.view(bs, -1, self.no))
可以看到如果我们不把Export置为True的话,那么将会对YOLOV7中的三个特征图求每个特征图点对应到原图的信息,如果使用pytorch进行推理确实方便很多,并且不需要在C++上写这些处理,但是我不设置Export的值,将导出的ONNX模型放到OpenCV C++上做推理就会报标题错误。
使用的OpenCV版本是4.5.5,ONNX是1.13,这里我猜测可能是这些后处理操作所影响的,于是参考官方的export.py将export设置为True,转换后在OpenCV上推理,但是给我报了一个input和output的错误,找了大量资料,最终发现如果使用onnxsim进行简化错误将会消失。
import onnxsim
onnx_model = onnx.load_model(r"./yolov7_4.onnx")
print('\nStarting to simplify ONNX...')
onnx_model, check = onnxsim.simplify(onnx_model)
assert check, 'assert check failed'
# print(onnx.helper.printable_graph(onnx_model.graph)) # print a human readable model
model = onnx_model
model.ir_version = 8
onnx.save(onnx_model,r"./yolov7_4.onnx")
报如上错误可以使用这段代码进行简化ONNX。其中包括了改变onnx输出风格的代码。
C++编写后处理代码
将模型加载完成后,我们的模型进行推理得到的结果并没有做如上处理,所以需要用C++把如上的处理补上,代码如下。参考了作者:https://github.com/UNeedCryDear/yolov7-opencv-dnn-cpp
for (int stride = 0; stride < strideSize; stride++) { //stride
float* pdata = (float*)netOutputImg[stride].data;
int grid_x = (int)(netWidth / netStride[stride]);
int grid_y = (int)(netHeight / netStride[stride]);
for (int anchor = 0; anchor < 3; anchor++) { //anchors
const float anchor_w = netAnchors[stride][anchor * 2];
const float anchor_h = netAnchors[stride][anchor * 2 + 1];
for (int i = 0; i < grid_y; i++) {
for (int j = 0; j < grid_x; j++) {
float box_score = sigmoid_x(pdata[4]); ;//获取每一行的box框中含有某个物体的概率
if (box_score >= boxThreshold) {
cv::Mat scores(1, className.size(), CV_32FC1, pdata + 5);
Point classIdPoint;
double max_class_socre;
minMaxLoc(scores, 0, &max_class_socre, 0, &classIdPoint);
max_class_socre = sigmoid_x(max_class_socre);
if (max_class_socre >= classThreshold) {
float x = (sigmoid_x(pdata[0]) * 2.f - 0.5f + j) * netStride[stride]; //x
float y = (sigmoid_x(pdata[1]) * 2.f - 0.5f + i) * netStride[stride]; //y
float w = powf(sigmoid_x(pdata[2]) * 2.f, 2.f) * anchor_w; //w
float h = powf(sigmoid_x(pdata[3]) * 2.f, 2.f) * anchor_h; //h
int left = (int)(x - 0.5 * w) * ratio_w + 0.5;
int top = (int)(y - 0.5 * h) * ratio_h + 0.5;
classIds.push_back(classIdPoint.x);
confidences.push_back(max_class_socre * box_score);
boxes.push_back(Rect(left, top, int(w * ratio_w), int(h * ratio_h)));
}
}
pdata += net_width;//下一行
}
作者采用了一种串行处理方法,由于网络未进行后处理输出的结果默认为([1,3,20,20,85],[1,3,40,40,85],[1,3,80,80,85]),形成数据指针,每次指向每个特征图像素殿点的信息,85个进行迭代,那么就可以将之前没做的处理做完。在进行推理就可以了,NMS采用OpenCV自带的进行推导。
总结
在报各种OpenCV的错误时,如果差不到资料,就使用nerton模块查看一下自己的ONNX模型导出是否出现问题。如果发现没问题,在Python中的CV2的DNN模块测一下模型。如果可以运行但是C++的OpenCV加载报错,大概率是网络出现一些识别不了的操作了。还有一件事,OpenCV做ONNX推理的时候是不支持动态输入的,一定不要动态导出。