""" 测试代码: 1.-dw 输入目标检测权重,为pt文件,必要选项 2.-cw 输入细粒度识别权重,为onnx文件,必要选项 3.-c 输入细粒度识别名录,为txt文件,中文名字,每个中文用引号表示,中间用“,”隔开 4.-m 系统执行模式,分两种,一种为推理图片,直接生成结果“inf”,一种为AI label,生成xml文件,“ai” 5.-s 源文件路径,可以是文件夹也可以是图片文件,如果是AI label,则在同级目录下面生成一个xml文件。,可以一直输入单张图片地址进行推理“always”,则源文件路径参数失效,需要输入1即可,也可以是超大分辨率的处理,返回的参数是7,要求文件夹目录必须有long字眼 6.-o 存储根路径 CUDA_VISIBLE_DEVICES=7 ./NewDetAiLabelFine -dw /data5/Train_Models/Detection_weigth/3090/202405/20240501-17413-pixel50-GHY/weights/best.pt -cw /data5/Train_Models/mmcls_onnx/CMB_GHY_Animal_52_1/convnext-base_4xb32_cmb200/end2end.onnx -c /data5/CAS_Project/CAS_ProjectAnimal/CMB_ProjectAnimal/CMB_GHY_Animal_52/animalListCNlist.txt -m "ai" -s /data5/Temp_ProjectData/mdh/GHY/20240604/NET_camera_images_01/ -o /data5/Hatcher/data/detection/testresult5 0: 路径不存在 1: 普通文件夹(不包含子文件夹) 2: 图像文件 3: 视频文件 4: 包含子文件夹的文件夹 6: TXT文本文件 7: 名称包含"long"的文件夹 "always": 特殊标识符 "文件": 其他类型文件 "未知类型": 无法识别的类型 """ import imp from unicodedata import name import torch,os import cv2,time import warnings from PIL import Image import onnxruntime as ort import numpy as np import torchvision import argparse,mimetypes from tqdm import tqdm from scipy.io import loadmat import cv2 import json from copy import deepcopy from torch import Tensor from configs.configs import model_head from tqdm import tqdm import os, sys, shutil import torch import platform,subprocess import math,urllib import random,requests from pathlib import Path from torch import nn from PIL import Image, ImageDraw, ImageFont from vizer.draw import draw_boxes try: import thop # for FLOPs computation except ImportError: thop = None import numpy as np import pkg_resources as pkg from fine.fineConfig import LoadConfig, load_data_transformers from torch.autograd import Variable from creat_xml_fordetection import make_xml import threading from common import global_para as gp import time, statistics from typing import Tuple import torch.nn.functional as F root_path = os.getcwd() sys.path.insert(0,root_path+"/Y5") import matplotlib.pyplot as plt import Y5.models DebugPrint_flag = True def parse_args(): parser = argparse.ArgumentParser(description='Hatcher detection inference ') parser.add_argument('-dw', '--detectionweights', type=str,default="/data5/PartDataset/007_BJNHZ/0018_20240608/20231211/best.pt", help='Input weights (.pt) file path (required)') parser.add_argument('-cw', '--fineweights', type=str,default="/data5/PartDataset/007_BJNHZ/0018_20240608/20231211/convnext_base_in22k-CMB_BJNHZ_20230202_31_84590-RP-RC-RP.onnx", help='Input weights (.onnx) file path (required)') parser.add_argument('-c', '--classes',type=str,default="/data5/PartDataset/007_BJNHZ/0018_20240608/20231211/CMB_BJNHZ_20230911_31.txt" , help='Input fine class label') parser.add_argument('-m','--mode', default="ai",choices=['inf', 'ai'], help='inference mode') parser.add_argument('-s', '--source', type=str,default="/data5/Hatcher/data/detection/test1/", help='Input test images file path (required)') parser.add_argument( '-o', '--out', type=str, default="/data5/Hatcher/data/detection/testresult", help='save path') args = parser.parse_args() if not os.path.isfile(args.detectionweights): raise SystemExit('Invalid weights file') return args.detectionweights, args.fineweights,args.classes,args.mode, args.source,args.out ###### def cv2AddChineseText(draw, text ,position, textColor=(0, 255, 0), textSize=30): tp2 = time.time() fontStyle = ImageFont.truetype( "./Font/msyh.ttc", textSize, encoding="utf-8") tp3 = time.time() if gp.DebugPrint_flag: print("tp2 is ", (tp3 - tp2) * 1000) draw.text(position, text, textColor, font=fontStyle) tp4 = time.time() if gp.DebugPrint_flag: print("tp3 is ", (tp4 - tp3) * 1000) return draw#cv2.cvtColor(np.asarray(img), cv2.COLOR_RGB2BGR) def plot_one_box(x, draw, height,width,color=None, label=None, Index = None, line_thickness=3): # Plots one bounding box on image img tl = line_thickness or round(0.002 * (height + width) / 2) + 1 # line/font thickness color = color or tuple(random.randint(0, 255) for _ in range(3)) c1, c2 = (int(x[0]), int(x[1])), (int(x[2]), int(x[3])) draw.rectangle((c1, c2), fill=None, outline=color, width=tl) if label: tf = max(tl - 1, 1) # font thickness t_size = cv2.getTextSize(label, fontFace=cv2.FONT_HERSHEY_COMPLEX, fontScale=gp.fontScale , thickness=tl)[0] print(label,t_size[0],t_size[1]) c1 = c1[0],c1[1]-t_size[1] c2 = c1[0] + t_size[0], c1[1]+t_size[1] draw.rectangle((c1, c2), fill=color, outline=color, width=tl) draw = cv2AddChineseText(draw, label,(c1[0], c1[1]), textColor = (225, 255, 255), textSize=tf*17) #img = cv2AddChineseText(img, "ÊýÁ¿: {}".format(str("011")), (2000,100), textColor=(0, 255, 0), textSize=80) return draw def clip_coords(boxes, shape): # Clip bounding xyxy bounding boxes to image shape (height, width) if isinstance(boxes, torch.Tensor): # faster individually boxes[:, 0].clamp_(0, shape[1]) # x1 boxes[:, 1].clamp_(0, shape[0]) # y1 boxes[:, 2].clamp_(0, shape[1]) # x2 boxes[:, 3].clamp_(0, shape[0]) # y2 else: # np.array (faster grouped) boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, shape[1]) # x1, x2 boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, shape[0]) # y1, y2 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 def box_area(box): # box = xyxy(4,n) return (box[2] - box[0]) * (box[3] - box[1]) def _upcast(t: Tensor) -> Tensor: # Protects from numerical overflows in multiplications by upcasting to the equivalent higher type if t.is_floating_point(): return t if t.dtype in (torch.float32, torch.float64) else t.float() else: return t if t.dtype in (torch.int32, torch.int64) else t.int() def _box_inter_union(boxes1: Tensor, boxes2: Tensor) -> Tuple[Tensor, Tensor]: area1 = box_area(boxes1) area2 = box_area(boxes2) lt = torch.max(boxes1[:, None, :2], boxes2[:, :2]) # [N,M,2] rb = torch.min(boxes1[:, None, 2:], boxes2[:, 2:]) # [N,M,2] wh = _upcast(rb - lt).clamp(min=0) # [N,M,2] inter = wh[:, :, 0] * wh[:, :, 1] # [N,M] union = area1[:, None] + area2 - inter return inter, union def xywh2xyxy(x): # Convert nx4 boxes from [x, y, w, h] to [x1, y1, x2, y2] where xy1=top-left, xy2=bottom-right y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x) y[:, 0] = x[:, 0] - x[:, 2] / 2 # top left x y[:, 1] = x[:, 1] - x[:, 3] / 2 # top left y y[:, 2] = x[:, 0] + x[:, 2] / 2 # bottom right x y[:, 3] = x[:, 1] + x[:, 3] / 2 # bottom right y return y def box_iou(boxes1: Tensor, boxes2: Tensor) -> Tensor: inter, union = _box_inter_union(boxes1, boxes2) iou = inter / union return iou def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45, classes=None, agnostic=False, multi_label=False, labels=(), max_det=300): """Non-Maximum Suppression (NMS) on inference results to reject overlapping bounding boxes Returns: list of detections, on (n,6) tensor per image [xyxy, conf, cls] """ bs = prediction.shape[0] # batch size nc = prediction.shape[2] - 5 # number of classes xc = prediction[..., 4] > conf_thres # candidates # Checks assert 0 <= conf_thres <= 1, f'Invalid Confidence threshold {conf_thres}, valid values are between 0.0 and 1.0' assert 0 <= iou_thres <= 1, f'Invalid IoU {iou_thres}, valid values are between 0.0 and 1.0' # Settings # min_wh = 2 # (pixels) minimum box width and height max_wh = 7680 # (pixels) maximum box width and height max_nms = 30000 # maximum number of boxes into torchvision.ops.nms() time_limit = 0.3 + 0.03 * bs # seconds to quit after redundant = True # require redundant detections multi_label &= nc > 1 # multiple labels per box (adds 0.5ms/img) merge = False # use merge-NMS t = time.time() output = [torch.zeros((0, 6), device=prediction.device)] * bs for xi, x in enumerate(prediction): # image index, image inference # Apply constraints # x[((x[..., 2:4] < min_wh) | (x[..., 2:4] > max_wh)).any(1), 4] = 0 # width-height x = x[xc[xi]] # confidence # Cat apriori labels if autolabelling if labels and len(labels[xi]): lb = labels[xi] v = torch.zeros((len(lb), nc + 5), device=x.device) v[:, :4] = lb[:, 1:5] # box v[:, 4] = 1.0 # conf v[range(len(lb)), lb[:, 0].long() + 5] = 1.0 # cls x = torch.cat((x, v), 0) # If none remain process next image if not x.shape[0]: continue # Compute conf x[:, 5:] *= x[:, 4:5] # conf = obj_conf * cls_conf # Box (center x, center y, width, height) to (x1, y1, x2, y2) box = xywh2xyxy(x[:, :4]) # Detections matrix nx6 (xyxy, conf, cls) if multi_label: i, j = (x[:, 5:] > conf_thres).nonzero(as_tuple=False).T x = torch.cat((box[i], x[i, j + 5, None], j[:, None].float()), 1) else: # best class only conf, j = x[:, 5:].max(1, keepdim=True) x = torch.cat((box, conf, j.float()), 1)[conf.view(-1) > conf_thres] # Filter by class if classes is not None: x = x[(x[:, 5:6] == torch.tensor(classes, device=x.device)).any(1)] # Apply finite constraint # if not torch.isfinite(x).all(): # x = x[torch.isfinite(x).all(1)] # Check shape n = x.shape[0] # number of boxes if not n: # no boxes continue elif n > max_nms: # excess boxes x = x[x[:, 4].argsort(descending=True)[:max_nms]] # sort by confidence # Batched NMS c = x[:, 5:6] * (0 if agnostic else max_wh) # classes boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS if i.shape[0] > max_det: # limit detections i = i[:max_det] if merge and (1 < n < 3E3): # Merge NMS (boxes merged using weighted mean) # update boxes as boxes(i,4) = weights(i,n) * boxes(n,4) iou = box_iou(boxes[i], boxes) > iou_thres # iou matrix weights = iou * scores[None] # box weights x[i, :4] = torch.mm(weights, x[:, :4]).float() / weights.sum(1, keepdim=True) # merged boxes if redundant: i = i[iou.sum(1) > 1] # require redundancy output[xi] = x[i] if (time.time() - t) > time_limit: break # time limit exceeded return output def letterbox(im, 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 = im.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 val 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 im = cv2.resize(im, 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)) im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) # add border return im, ratio, (dw, dh) def check_anchor_order(m): # Check anchor order against stride order for YOLOv5 Detect() module m, and correct if necessary a = m.anchors.prod(-1).mean(-1).view(-1) # mean anchor area per output layer da = a[-1] - a[0] # delta a ds = m.stride[-1] - m.stride[0] # delta s if da and (da.sign() != ds.sign()): # same order m.anchors[:] = m.anchors.flip(0) def check_version(current='0.0.0', minimum='0.0.0', name='version ', pinned=False, hard=False, verbose=False): # Check version vs. required version current, minimum = (pkg.parse_version(x) for x in (current, minimum)) result = (current == minimum) if pinned else (current >= minimum) # bool s = f'{name}{minimum} required by YOLOv5, but {name}{current} is currently installed' # string if hard: assert result, s # assert min requirements met if verbose and not result: pass return result class Detect(nn.Module): stride = None # strides computed during build onnx_dynamic = False # ONNX export parameter export = False # export mode def __init__(self, nc=80, anchors=(), ch=(), inplace=True): # detection layer super().__init__() self.nc = nc # number of classes self.no = nc + 5 # number of outputs per anchor self.nl = len(anchors) # number of detection layers self.na = len(anchors[0]) // 2 # number of anchors self.grid = [torch.zeros(1)] * self.nl # init grid self.anchor_grid = [torch.zeros(1)] * self.nl # init anchor grid self.register_buffer('anchors', torch.tensor(anchors).float().view(self.nl, -1, 2)) # shape(nl,na,2) self.m = nn.ModuleList(nn.Conv2d(x, self.no * self.na, 1) for x in ch) # output conv self.inplace = inplace # use in-place ops (e.g. slice assignment) def forward(self, x): z = [] # inference output 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.onnx_dynamic or self.grid[i].shape[2:4] != x[i].shape[2:4]: self.grid[i], self.anchor_grid[i] = self._make_grid(nx, ny, i) y = x[i].sigmoid() if self.inplace: y[..., 0:2] = (y[..., 0:2] * 2 + self.grid[i]) * self.stride[i] # xy y[..., 2:4] = (y[..., 2:4] * 2) ** 2 * self.anchor_grid[i] # wh else: # for YOLOv5 on AWS Inferentia https://github.com/ultralytics/yolov5/pull/2953 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.grid[i]) * self.stride[i] # xy wh = (wh * 2) ** 2 * self.anchor_grid[i] # wh y = torch.cat((xy, wh, conf), 4) z.append(y.view(bs, -1, self.no)) return x if self.training else (torch.cat(z, 1),) if self.export else (torch.cat(z, 1), x) def _make_grid(self, nx=20, ny=20, i=0): d = self.anchors[i].device t = self.anchors[i].dtype shape = 1, self.na, ny, nx, 2 # grid shape y, x = torch.arange(ny, device=d, dtype=t), torch.arange(nx, device=d, dtype=t) if check_version(torch.__version__, '1.10.0'): # torch>=1.10.0 meshgrid workaround for torch>=0.7 compatibility yv, xv = torch.meshgrid(y, x, indexing='ij') else: yv, xv = torch.meshgrid(y, x) grid = torch.stack((xv, yv), 2).expand(shape) - 0.5 # add grid offset, i.e. y = 2.0 * x - 0.5 anchor_grid = (self.anchors[i] * self.stride[i]).view((1, self.na, 1, 1, 2)).expand(shape) return grid, anchor_grid def initialize_weights(model): for m in model.modules(): t = type(m) if t is nn.Conv2d: pass # nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu') elif t is nn.BatchNorm2d: m.eps = 1e-3 m.momentum = 0.03 elif t in [nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU]: m.inplace = True def scale_img(img, ratio=1.0, same_shape=False, gs=32): # img(16,3,256,416) # Scales img(bs,3,y,x) by ratio constrained to gs-multiple if ratio == 1.0: return img h, w = img.shape[2:] s = (int(h * ratio), int(w * ratio)) # new size img = F.interpolate(img, size=s, mode='bilinear', align_corners=False) # resize if not same_shape: # pad/crop img h, w = (math.ceil(x * ratio / gs) * gs for x in (h, w)) return F.pad(img, [0, w - s[1], 0, h - s[0]], value=0.447) # value = imagenet mean def feature_visualization(x, module_type, stage, n=32, save_dir=Path('runs/detect/exp')): """ x: Features to be visualized module_type: Module type stage: Module stage within model n: Maximum number of feature maps to plot save_dir: Directory to save results """ if 'Detect' not in module_type: batch, channels, height, width = x.shape # batch, channels, height, width if height > 1 and width > 1: f = save_dir / f"stage{stage}_{module_type.split('.')[-1]}_features.png" # filename blocks = torch.chunk(x[0].cpu(), channels, dim=0) # select batch index 0, block by channels n = min(n, channels) # number of plots fig, ax = plt.subplots(math.ceil(n / 8), 8, tight_layout=True) # 8 rows x n/8 cols ax = ax.ravel() plt.subplots_adjust(wspace=0.05, hspace=0.05) for i in range(n): ax[i].imshow(blocks[i].squeeze()) # cmap='gray' ax[i].axis('off') plt.savefig(f, dpi=300, bbox_inches='tight') plt.close() np.save(str(f.with_suffix('.npy')), x[0].cpu().numpy()) # npy save def time_sync(): # PyTorch-accurate time if torch.cuda.is_available(): torch.cuda.synchronize() return time.time() def autopad(k, p=None): # kernel, padding # Pad to 'same' if p is None: p = k // 2 if isinstance(k, int) else [x // 2 for x in k] # auto-pad return p class Conv(nn.Module): # Standard convolution def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups super().__init__() self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g, bias=False) self.bn = nn.BatchNorm2d(c2) self.act = nn.SiLU() if act is True else (act if isinstance(act, nn.Module) else nn.Identity()) def forward(self, x): return self.act(self.bn(self.conv(x))) def forward_fuse(self, x): return self.act(self.conv(x)) class DWConv(Conv): # Depth-wise convolution class def __init__(self, c1, c2, k=1, s=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups super().__init__(c1, c2, k, s, g=math.gcd(c1, c2), act=act) class DWConvTranspose2d(nn.ConvTranspose2d): # Depth-wise transpose convolution class def __init__(self, c1, c2, k=1, s=1, p1=0, p2=0): # ch_in, ch_out, kernel, stride, padding, padding_out super().__init__(c1, c2, k, s, p1, p2, groups=math.gcd(c1, c2)) class TransformerLayer(nn.Module): # Transformer layer https://arxiv.org/abs/2010.11929 (LayerNorm layers removed for better performance) def __init__(self, c, num_heads): super().__init__() self.q = nn.Linear(c, c, bias=False) self.k = nn.Linear(c, c, bias=False) self.v = nn.Linear(c, c, bias=False) self.ma = nn.MultiheadAttention(embed_dim=c, num_heads=num_heads) self.fc1 = nn.Linear(c, c, bias=False) self.fc2 = nn.Linear(c, c, bias=False) def forward(self, x): x = self.ma(self.q(x), self.k(x), self.v(x))[0] + x x = self.fc2(self.fc1(x)) + x return x class TransformerBlock(nn.Module): # Vision Transformer https://arxiv.org/abs/2010.11929 def __init__(self, c1, c2, num_heads, num_layers): super().__init__() self.conv = None if c1 != c2: self.conv = Conv(c1, c2) self.linear = nn.Linear(c2, c2) # learnable position embedding self.tr = nn.Sequential(*(TransformerLayer(c2, num_heads) for _ in range(num_layers))) self.c2 = c2 def forward(self, x): if self.conv is not None: x = self.conv(x) b, _, w, h = x.shape p = x.flatten(2).permute(2, 0, 1) return self.tr(p + self.linear(p)).permute(1, 2, 0).reshape(b, self.c2, w, h) class Bottleneck(nn.Module): # Standard bottleneck def __init__(self, c1, c2, shortcut=True, g=1, e=0.5): # ch_in, ch_out, shortcut, groups, expansion super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_, c2, 3, 1, g=g) self.add = shortcut and c1 == c2 def forward(self, x): return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x)) class BottleneckCSP(nn.Module): # CSP Bottleneck https://github.com/WongKinYiu/CrossStagePartialNetworks def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = nn.Conv2d(c1, c_, 1, 1, bias=False) self.cv3 = nn.Conv2d(c_, c_, 1, 1, bias=False) self.cv4 = Conv(2 * c_, c2, 1, 1) self.bn = nn.BatchNorm2d(2 * c_) # applied to cat(cv2, cv3) self.act = nn.SiLU() self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n))) def forward(self, x): y1 = self.cv3(self.m(self.cv1(x))) y2 = self.cv2(x) return self.cv4(self.act(self.bn(torch.cat((y1, y2), 1)))) class CrossConv(nn.Module): # Cross Convolution Downsample def __init__(self, c1, c2, k=3, s=1, g=1, e=1.0, shortcut=False): # ch_in, ch_out, kernel, stride, groups, expansion, shortcut super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, (1, k), (1, s)) self.cv2 = Conv(c_, c2, (k, 1), (s, 1), g=g) self.add = shortcut and c1 == c2 def forward(self, x): return x + self.cv2(self.cv1(x)) if self.add else self.cv2(self.cv1(x)) class C3(nn.Module): # CSP Bottleneck with 3 convolutions def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): # ch_in, ch_out, number, shortcut, groups, expansion super().__init__() c_ = int(c2 * e) # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c1, c_, 1, 1) self.cv3 = Conv(2 * c_, c2, 1) # optional act=FReLU(c2) self.m = nn.Sequential(*(Bottleneck(c_, c_, shortcut, g, e=1.0) for _ in range(n))) def forward(self, x): return self.cv3(torch.cat((self.m(self.cv1(x)), self.cv2(x)), 1)) class C3x(C3): # C3 module with cross-convolutions def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): super().__init__(c1, c2, n, shortcut, g, e) c_ = int(c2 * e) self.m = nn.Sequential(*(CrossConv(c_, c_, 3, 1, g, 1.0, shortcut) for _ in range(n))) class C3TR(C3): # C3 module with TransformerBlock() def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): super().__init__(c1, c2, n, shortcut, g, e) c_ = int(c2 * e) self.m = TransformerBlock(c_, c_, 4, n) class C3SPP(C3): # C3 module with SPP() def __init__(self, c1, c2, k=(5, 9, 13), n=1, shortcut=True, g=1, e=0.5): super().__init__(c1, c2, n, shortcut, g, e) c_ = int(c2 * e) self.m = SPP(c_, c_, k) class C3Ghost(C3): # C3 module with GhostBottleneck() def __init__(self, c1, c2, n=1, shortcut=True, g=1, e=0.5): super().__init__(c1, c2, n, shortcut, g, e) c_ = int(c2 * e) # hidden channels self.m = nn.Sequential(*(GhostBottleneck(c_, c_) for _ in range(n))) class SPP(nn.Module): # Spatial Pyramid Pooling (SPP) layer https://arxiv.org/abs/1406.4729 def __init__(self, c1, c2, k=(5, 9, 13)): super().__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * (len(k) + 1), c2, 1, 1) self.m = nn.ModuleList([nn.MaxPool2d(kernel_size=x, stride=1, padding=x // 2) for x in k]) def forward(self, x): x = self.cv1(x) with warnings.catch_warnings(): warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning return self.cv2(torch.cat([x] + [m(x) for m in self.m], 1)) FONT = 'Arial.ttf' class SPPF(nn.Module): # Spatial Pyramid Pooling - Fast (SPPF) layer for YOLOv5 by Glenn Jocher def __init__(self, c1, c2, k=5): # equivalent to SPP(k=(5, 9, 13)) super().__init__() c_ = c1 // 2 # hidden channels self.cv1 = Conv(c1, c_, 1, 1) self.cv2 = Conv(c_ * 4, c2, 1, 1) self.m = nn.MaxPool2d(kernel_size=k, stride=1, padding=k // 2) def forward(self, x): x = self.cv1(x) with warnings.catch_warnings(): warnings.simplefilter('ignore') # suppress torch 1.9.0 max_pool2d() warning y1 = self.m(x) y2 = self.m(y1) return self.cv2(torch.cat((x, y1, y2, self.m(y2)), 1)) class Focus(nn.Module): # Focus wh information into c-space def __init__(self, c1, c2, k=1, s=1, p=None, g=1, act=True): # ch_in, ch_out, kernel, stride, padding, groups super().__init__() self.conv = Conv(c1 * 4, c2, k, s, p, g, act) # self.contract = Contract(gain=2) def forward(self, x): # x(b,c,w,h) -> y(b,4c,w/2,h/2) return self.conv(torch.cat((x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]), 1)) # return self.conv(self.contract(x)) class GhostConv(nn.Module): # Ghost Convolution https://github.com/huawei-noah/ghostnet def __init__(self, c1, c2, k=1, s=1, g=1, act=True): # ch_in, ch_out, kernel, stride, groups super().__init__() c_ = c2 // 2 # hidden channels self.cv1 = Conv(c1, c_, k, s, None, g, act) self.cv2 = Conv(c_, c_, 5, 1, None, c_, act) def forward(self, x): y = self.cv1(x) return torch.cat((y, self.cv2(y)), 1) class MixConv2d(nn.Module): # Mixed Depth-wise Conv https://arxiv.org/abs/1907.09595 def __init__(self, c1, c2, k=(1, 3), s=1, equal_ch=True): # ch_in, ch_out, kernel, stride, ch_strategy super().__init__() n = len(k) # number of convolutions if equal_ch: # equal c_ per group i = torch.linspace(0, n - 1E-6, c2).floor() # c2 indices c_ = [(i == g).sum() for g in range(n)] # intermediate channels else: # equal weight.numel() per group b = [c2] + [0] * n a = np.eye(n + 1, n, k=-1) a -= np.roll(a, 1, axis=1) a *= np.array(k) ** 2 a[0] = 1 c_ = np.linalg.lstsq(a, b, rcond=None)[0].round() # solve for equal weight indices, ax = b self.m = nn.ModuleList([ nn.Conv2d(c1, int(c_), k, s, k // 2, groups=math.gcd(c1, int(c_)), bias=False) for k, c_ in zip(k, c_)]) self.bn = nn.BatchNorm2d(c2) self.act = nn.SiLU() def forward(self, x): return self.act(self.bn(torch.cat([m(x) for m in self.m], 1))) class GhostBottleneck(nn.Module): # Ghost Bottleneck https://github.com/huawei-noah/ghostnet def __init__(self, c1, c2, k=3, s=1): # ch_in, ch_out, kernel, stride super().__init__() c_ = c2 // 2 self.conv = nn.Sequential( GhostConv(c1, c_, 1, 1), # pw DWConv(c_, c_, k, s, act=False) if s == 2 else nn.Identity(), # dw GhostConv(c_, c2, 1, 1, act=False)) # pw-linear self.shortcut = nn.Sequential(DWConv(c1, c1, k, s, act=False), Conv(c1, c2, 1, 1, act=False)) if s == 2 else nn.Identity() def forward(self, x): return self.conv(x) + self.shortcut(x) class Contract(nn.Module): # Contract width-height into channels, i.e. x(1,64,80,80) to x(1,256,40,40) def __init__(self, gain=2): super().__init__() self.gain = gain def forward(self, x): b, c, h, w = x.size() # assert (h / s == 0) and (W / s == 0), 'Indivisible gain' s = self.gain x = x.view(b, c, h // s, s, w // s, s) # x(1,64,40,2,40,2) x = x.permute(0, 3, 5, 1, 2, 4).contiguous() # x(1,2,2,64,40,40) return x.view(b, c * s * s, h // s, w // s) # x(1,256,40,40) class Expand(nn.Module): # Expand channels into width-height, i.e. x(1,64,80,80) to x(1,16,160,160) def __init__(self, gain=2): super().__init__() self.gain = gain def forward(self, x): b, c, h, w = x.size() # assert C / s ** 2 == 0, 'Indivisible gain' s = self.gain x = x.view(b, s, s, c // s ** 2, h, w) # x(1,2,2,16,80,80) x = x.permute(0, 3, 4, 1, 5, 2).contiguous() # x(1,16,80,2,80,2) return x.view(b, c // s ** 2, h * s, w * s) # x(1,16,160,160) class Concat(nn.Module): # Concatenate a list of tensors along dimension def __init__(self, dimension=1): super().__init__() self.d = dimension def forward(self, x): return torch.cat(x, self.d) def xyxy2xywh(x): # Convert nx4 boxes from [x1, y1, x2, y2] to [x, y, w, h] where xy1=top-left, xy2=bottom-right y = x.clone() if isinstance(x, torch.Tensor) else np.copy(x) y[:, 0] = (x[:, 0] + x[:, 2]) / 2 # x center y[:, 1] = (x[:, 1] + x[:, 3]) / 2 # y center y[:, 2] = x[:, 2] - x[:, 0] # width y[:, 3] = x[:, 3] - x[:, 1] # height return y def is_ascii(s=''): # Is string composed of all ASCII (no UTF) characters? (note str().isascii() introduced in python 3.7) s = str(s) # convert list, tuple, None, etc. to str return len(s.encode().decode('ascii', 'ignore')) == len(s) def is_writeable(dir, test=False): # Return True if directory has write permissions, test opening a file with write permissions if test=True if not test: return os.access(dir, os.R_OK) # possible issues on Windows file = Path(dir) / 'tmp.txt' try: with open(file, 'w'): # open file with write permissions pass file.unlink() # remove file return True except OSError: return False def user_config_dir(dir='Ultralytics', env_var='YOLOV5_CONFIG_DIR'): # Return path of user configuration directory. Prefer environment variable if exists. Make dir if required. env = os.getenv(env_var) if env: path = Path(env) # use environment variable else: cfg = {'Windows': 'AppData/Roaming', 'Linux': '.config', 'Darwin': 'Library/Application Support'} # 3 OS dirs path = Path.home() / cfg.get(platform.system(), '') # OS-specific config dir path = (path if is_writeable(path) else Path('/tmp')) / dir # GCP and AWS lambda fix, only /tmp is writeable path.mkdir(exist_ok=True) # make if required return path CONFIG_DIR = user_config_dir() def check_font(font=FONT, progress=False): # Download font to CONFIG_DIR if necessary font = Path(font) file = CONFIG_DIR / font.name if not font.exists() and not file.exists(): url = "https://ultralytics.com/assets/" + font.name torch.hub.download_url_to_file(url, str(file), progress=progress) def check_pil_font(font=FONT, size=10): # Return a PIL TrueType Font, downloading to CONFIG_DIR if necessary font = Path(font) font = font if font.exists() else (CONFIG_DIR / font.name) try: return ImageFont.truetype(str(font) if font.exists() else font.name, size) except Exception: # download if missing try: check_font(font) return ImageFont.truetype(str(font), size) except TypeError: pass # known issue https://github.com/ultralytics/yolov5/issues/5374 class Annotator: # YOLOv5 Annotator for train/val mosaics and jpgs and detect/hub inference annotations def __init__(self, im, line_width=None, font_size=None, font='Arial.ttf', pil=False, example='abc'): assert im.data.contiguous, 'Image not contiguous. Apply np.ascontiguousarray(im) to Annotator() input images.' non_ascii = not is_ascii(example) # non-latin labels, i.e. asian, arabic, cyrillic self.pil = pil or non_ascii if self.pil: # use PIL self.im = im if isinstance(im, Image.Image) else Image.fromarray(im) self.draw = ImageDraw.Draw(self.im) self.font = check_pil_font(font='Arial.Unicode.ttf' if non_ascii else font, size=font_size or max(round(sum(self.im.size) / 2 * 0.035), 12)) else: # use cv2 self.im = im self.lw = line_width or max(round(sum(im.shape) / 2 * 0.003), 2) # line width def box_label(self, box, label='', color=(128, 128, 128), txt_color=(255, 255, 255)): # Add one xyxy box to image with label if self.pil or not is_ascii(label): self.draw.rectangle(box, width=self.lw, outline=color) # box if label: w, h = self.font.getsize(label) # text width, height outside = box[1] - h >= 0 # label fits outside box self.draw.rectangle( (box[0], box[1] - h if outside else box[1], box[0] + w + 1, box[1] + 1 if outside else box[1] + h + 1), fill=color, ) # self.draw.text((box[0], box[1]), label, fill=txt_color, font=self.font, anchor='ls') # for PIL>8.0 self.draw.text((box[0], box[1] - h if outside else box[1]), label, fill=txt_color, font=self.font) else: # cv2 p1, p2 = (int(box[0]), int(box[1])), (int(box[2]), int(box[3])) cv2.rectangle(self.im, p1, p2, color, thickness=self.lw, lineType=cv2.LINE_AA) if label: tf = max(self.lw - 1, 1) # font thickness w, h = cv2.getTextSize(label, 0, fontScale=self.lw / 3, thickness=tf)[0] # text width, height outside = p1[1] - h >= 3 p2 = p1[0] + w, p1[1] - h - 3 if outside else p1[1] + h + 3 cv2.rectangle(self.im, p1, p2, color, -1, cv2.LINE_AA) # filled cv2.putText(self.im, label, (p1[0], p1[1] - 2 if outside else p1[1] + h + 2), 0, self.lw / 3, txt_color, thickness=tf, lineType=cv2.LINE_AA) def rectangle(self, xy, fill=None, outline=None, width=1): # Add rectangle to image (PIL-only) self.draw.rectangle(xy, fill, outline, width) def text(self, xy, text, txt_color=(255, 255, 255)): # Add text to image (PIL-only) w, h = self.font.getsize(text) # text width, height self.draw.text((xy[0], xy[1] - h + 1), text, fill=txt_color, font=self.font) def result(self): # Return annotated image as array return np.asarray(self.im) class Detections: # YOLOv5 detections class for inference results def __init__(self, imgs, pred, files, times=(0, 0, 0, 0), names=None, shape=None): super().__init__() d = pred[0].device # device gn = [torch.tensor([*(im.shape[i] for i in [1, 0, 1, 0]), 1, 1], device=d) for im in imgs] # normalizations self.imgs = imgs # list of images as numpy arrays self.pred = pred # list of tensors pred[0] = (xyxy, conf, cls) self.names = names # class names self.files = files # image filenames self.times = times # profiling times self.xyxy = pred # xyxy pixels self.xywh = [xyxy2xywh(x) for x in pred] # xywh pixels self.xyxyn = [x / g for x, g in zip(self.xyxy, gn)] # xyxy normalized self.xywhn = [x / g for x, g in zip(self.xywh, gn)] # xywh normalized self.n = len(self.pred) # number of images (batch size) self.t = tuple((times[i + 1] - times[i]) * 1000 / self.n for i in range(3)) # timestamps (ms) self.s = shape # inference BCHW shape def tolist(self): # return a list of Detections objects, i.e. 'for result in results.tolist():' r = range(self.n) # iterable x = [Detections([self.imgs[i]], [self.pred[i]], [self.files[i]], self.times, self.names, self.s) for i in r] # for d in x: # for k in ['imgs', 'pred', 'xyxy', 'xyxyn', 'xywh', 'xywhn']: # setattr(d, k, getattr(d, k)[0]) # pop out of list return x def __len__(self): return self.n # override len(results) def __str__(self): self.print() # override print(results) return '' def fuse_conv_and_bn(conv, bn): # Fuse Conv2d() and BatchNorm2d() layers https://tehnokv.com/posts/fusing-batchnorm-and-conv/ fusedconv = nn.Conv2d(conv.in_channels, conv.out_channels, kernel_size=conv.kernel_size, stride=conv.stride, padding=conv.padding, groups=conv.groups, bias=True).requires_grad_(False).to(conv.weight.device) # Prepare filters w_conv = conv.weight.clone().view(conv.out_channels, -1) w_bn = torch.diag(bn.weight.div(torch.sqrt(bn.eps + bn.running_var))) fusedconv.weight.copy_(torch.mm(w_bn, w_conv).view(fusedconv.weight.shape)) # Prepare spatial bias b_conv = torch.zeros(conv.weight.size(0), device=conv.weight.device) if conv.bias is None else conv.bias b_bn = bn.bias - bn.weight.mul(bn.running_mean).div(torch.sqrt(bn.running_var + bn.eps)) fusedconv.bias.copy_(torch.mm(w_bn, b_conv.reshape(-1, 1)).reshape(-1) + b_bn) return fusedconv class Classify(nn.Module): # Classification head, i.e. x(b,c1,20,20) to x(b,c2) def __init__(self, c1, c2, k=1, s=1, p=None, g=1): # ch_in, ch_out, kernel, stride, padding, groups super().__init__() self.aap = nn.AdaptiveAvgPool2d(1) # to x(b,c1,1,1) self.conv = nn.Conv2d(c1, c2, k, s, autopad(k, p), groups=g) # to x(b,c2,1,1) self.flat = nn.Flatten() def forward(self, x): z = torch.cat([self.aap(y) for y in (x if isinstance(x, list) else [x])], 1) # cat if list return self.flat(self.conv(z)) # flatten to x(b,c2) class Model(nn.Module): # YOLOv5 model def __init__(self, cfg='yolov5s.yaml', ch=3, nc=None, anchors=None): # model, input channels, number of classes super().__init__() if isinstance(cfg, dict): self.yaml = cfg # model dict else: # is *.yaml import yaml # for torch hub self.yaml_file = Path(cfg).name with open(cfg, encoding='ascii', errors='ignore') as f: self.yaml = yaml.safe_load(f) # model dict # Define model ch = self.yaml['ch'] = self.yaml.get('ch', ch) # input channels if nc and nc != self.yaml['nc']: self.yaml['nc'] = nc # override yaml value if anchors: self.yaml['anchors'] = round(anchors) # override yaml value self.model, self.save = parse_model(deepcopy(self.yaml), ch=[ch]) # model, savelist self.names = [str(i) for i in range(self.yaml['nc'])] # default names self.inplace = self.yaml.get('inplace', True) # Build strides, anchors m = self.model[-1] # Detect() if isinstance(m, Detect): s = 256 # 2x min stride m.inplace = self.inplace m.stride = torch.tensor([s / x.shape[-2] for x in self.forward(torch.zeros(1, ch, s, s))]) # forward check_anchor_order(m) # must be in pixel-space (not grid-space) m.anchors /= m.stride.view(-1, 1, 1) self.stride = m.stride self._initialize_biases() # only run once # Init weights, biases initialize_weights(self) self.info() def forward(self, x, augment=False, profile=False, visualize=False): if augment: return self._forward_augment(x) # augmented inference, None return self._forward_once(x, profile, visualize) # single-scale inference, train def _forward_augment(self, x): img_size = x.shape[-2:] # height, width s = [1, 0.83, 0.67] # scales f = [None, 3, None] # flips (2-ud, 3-lr) y = [] # outputs for si, fi in zip(s, f): xi = scale_img(x.flip(fi) if fi else x, si, gs=int(self.stride.max())) yi = self._forward_once(xi)[0] # forward # cv2.imwrite(f'img_{si}.jpg', 255 * xi[0].cpu().numpy().transpose((1, 2, 0))[:, :, ::-1]) # save yi = self._descale_pred(yi, fi, si, img_size) y.append(yi) y = self._clip_augmented(y) # clip augmented tails return torch.cat(y, 1), None # augmented inference, train def _forward_once(self, x, profile=False, visualize=False): y, dt = [], [] # outputs for m in self.model: if m.f != -1: # if not from previous layer x = y[m.f] if isinstance(m.f, int) else [x if j == -1 else y[j] for j in m.f] # from earlier layers if profile: self._profile_one_layer(m, x, dt) x = m(x) # run y.append(x if m.i in self.save else None) # save output if visualize: feature_visualization(x, m.type, m.i, save_dir=visualize) return x def _descale_pred(self, p, flips, scale, img_size): # de-scale predictions following augmented inference (inverse operation) if self.inplace: p[..., :4] /= scale # de-scale if flips == 2: p[..., 1] = img_size[0] - p[..., 1] # de-flip ud elif flips == 3: p[..., 0] = img_size[1] - p[..., 0] # de-flip lr else: x, y, wh = p[..., 0:1] / scale, p[..., 1:2] / scale, p[..., 2:4] / scale # de-scale if flips == 2: y = img_size[0] - y # de-flip ud elif flips == 3: x = img_size[1] - x # de-flip lr p = torch.cat((x, y, wh, p[..., 4:]), -1) return p def _clip_augmented(self, y): # Clip YOLOv5 augmented inference tails nl = self.model[-1].nl # number of detection layers (P3-P5) g = sum(4 ** x for x in range(nl)) # grid points e = 1 # exclude layer count i = (y[0].shape[1] // g) * sum(4 ** x for x in range(e)) # indices y[0] = y[0][:, :-i] # large i = (y[-1].shape[1] // g) * sum(4 ** (nl - 1 - x) for x in range(e)) # indices y[-1] = y[-1][:, i:] # small return y def _profile_one_layer(self, m, x, dt): c = isinstance(m, Detect) # is final layer, copy input as inplace fix o = thop.profile(m, inputs=(x.copy() if c else x,), verbose=False)[0] / 1E9 * 2 if thop else 0 # FLOPs t = time_sync() for _ in range(10): m(x.copy() if c else x) dt.append((time_sync() - t) * 100) if m == self.model[0]: pass pass if c: pass def _initialize_biases(self, cf=None): # initialize biases into Detect(), cf is class frequency # https://arxiv.org/abs/1708.02002 section 3.3 # cf = torch.bincount(torch.tensor(np.concatenate(dataset.labels, 0)[:, 0]).long(), minlength=nc) + 1. m = self.model[-1] # Detect() module for mi, s in zip(m.m, m.stride): # from b = mi.bias.view(m.na, -1).detach() # conv.bias(255) to (3,85) b[:, 4] += math.log(8 / (640 / s) ** 2) # obj (8 objects per 640 image) b[:, 5:] += math.log(0.6 / (m.nc - 0.999999)) if cf is None else torch.log(cf / cf.sum()) # cls mi.bias = torch.nn.Parameter(b.view(-1), requires_grad=True) def _print_biases(self): m = self.model[-1] # Detect() module for mi in m.m: # from b = mi.bias.detach().view(m.na, -1).T # conv.bias(255) to (3,85) # def _print_weights(self): # for m in self.model.modules(): # if type(m) is Bottleneck: # LOGGER.info('%10.3g' % (m.w.detach().sigmoid() * 2)) # shortcut weights def fuse(self): # fuse model Conv2d() + BatchNorm2d() layers for m in self.model.modules(): if isinstance(m, (Conv, DWConv)) and hasattr(m, 'bn'): m.conv = fuse_conv_and_bn(m.conv, m.bn) # update conv delattr(m, 'bn') # remove batchnorm m.forward = m.forward_fuse # update forward self.info() return self def info(self, verbose=False, img_size=640): # print model information pass #model_info(self, verbose, img_size) def _apply(self, fn): # Apply to(), cpu(), cuda(), half() to model tensors that are not parameters or registered buffers self = super()._apply(fn) m = self.model[-1] # Detect() if isinstance(m, Detect): m.stride = fn(m.stride) m.grid = list(map(fn, m.grid)) if isinstance(m.anchor_grid, list): m.anchor_grid = list(map(fn, m.anchor_grid)) return self def make_divisible(x, divisor): # Returns nearest x divisible by divisor if isinstance(divisor, torch.Tensor): divisor = int(divisor.max()) # to int return math.ceil(x / divisor) * divisor def parse_model(d, ch): # model_dict, input_channels(3) #LOGGER.info(f"\n{'':>3}{'from':>18}{'n':>3}{'params':>10} {'module':<40}{'arguments':<30}") anchors, nc, gd, gw = d['anchors'], d['nc'], d['depth_multiple'], d['width_multiple'] na = (len(anchors[0]) // 2) if isinstance(anchors, list) else anchors # number of anchors no = na * (nc + 5) # number of outputs = anchors * (classes + 5) layers, save, c2 = [], [], ch[-1] # layers, savelist, ch out for i, (f, n, m, args) in enumerate(d['backbone'] + d['head']): # from, number, module, args m = eval(m) if isinstance(m, str) else m # eval strings for j, a in enumerate(args): try: args[j] = eval(a) if isinstance(a, str) else a # eval strings except NameError: pass n = n_ = max(round(n * gd), 1) if n > 1 else n # depth gain if m in (Conv, GhostConv, Bottleneck, GhostBottleneck, SPP, SPPF, DWConv, MixConv2d, Focus, CrossConv, BottleneckCSP, C3, C3TR, C3SPP, C3Ghost, nn.ConvTranspose2d, DWConvTranspose2d, C3x): c1, c2 = ch[f], args[0] if c2 != no: # if not output c2 = make_divisible(c2 * gw, 8) args = [c1, c2, *args[1:]] if m in [BottleneckCSP, C3, C3TR, C3Ghost, C3x]: args.insert(2, n) # number of repeats n = 1 elif m is nn.BatchNorm2d: args = [ch[f]] elif m is Concat: c2 = sum(ch[x] for x in f) elif m is Detect: args.append([ch[x] for x in f]) if isinstance(args[1], int): # number of anchors args[1] = [list(range(args[1] * 2))] * len(f) elif m is Contract: c2 = ch[f] * args[0] ** 2 elif m is Expand: c2 = ch[f] // args[0] ** 2 else: c2 = ch[f] m_ = nn.Sequential(*(m(*args) for _ in range(n))) if n > 1 else m(*args) # module t = str(m)[8:-2].replace('__main__.', '') # module type np = sum(x.numel() for x in m_.parameters()) # number params m_.i, m_.f, m_.type, m_.np = i, f, t, np # attach index, 'from' index, type, number params #LOGGER.info(f'{i:>3}{str(f):>18}{n_:>3}{np:10.0f} {t:<40}{str(args):<30}') # print save.extend(x % i for x in ([f] if isinstance(f, int) else f) if x != -1) # append to savelist layers.append(m_) if i == 0: ch = [] ch.append(c2) return nn.Sequential(*layers), sorted(save) class Ensemble(nn.ModuleList): # Ensemble of models def __init__(self): super().__init__() def forward(self, x, augment=False, profile=False, visualize=False): y = [module(x, augment, profile, visualize)[0] for module in self] # y = torch.stack(y).max(0)[0] # max ensemble # y = torch.stack(y).mean(0) # mean ensemble y = torch.cat(y, 1) # nms ensemble return y, None # inference, train output def safe_download(file, url, url2=None, min_bytes=1E0, error_msg=''): # Attempts to download file from url or url2, checks and removes incomplete downloads < min_bytes file = Path(file) assert_msg = f"Downloaded file '{file}' does not exist or size is < min_bytes={min_bytes}" try: # url1 #LOGGER.info(f'Downloading {url} to {file}...') torch.hub.download_url_to_file(url, str(file), progress=True) assert file.exists() and file.stat().st_size > min_bytes, assert_msg # check except Exception as e: # url2 file.unlink(missing_ok=True) # remove partial downloads #LOGGER.info(f'ERROR: {e}\nRe-attempting {url2 or url} to {file}...') os.system(f"curl -L '{url2 or url}' -o '{file}' --retry 3 -C -") # curl download, retry and resume on fail finally: if not file.exists() or file.stat().st_size < min_bytes: # check file.unlink(missing_ok=True) # remove partial downloads #LOGGER.info(f"ERROR: {assert_msg}\n{error_msg}") pass #LOGGER.info('') def attempt_download(file, repo='ultralytics/yolov5', release='v6.1'): # Attempt file download from GitHub release assets if not found locally. release = 'latest', 'v6.1', etc. def github_assets(repository, version='latest'): # Return GitHub repo tag (i.e. 'v6.1') and assets (i.e. ['yolov5s.pt', 'yolov5m.pt', ...]) if version != 'latest': version = f'tags/{version}' # i.e. tags/v6.1 response = requests.get(f'https://api.github.com/repos/{repository}/releases/{version}').json() # github api return response['tag_name'], [x['name'] for x in response['assets']] # tag, assets file = Path(str(file).strip().replace("'", '')) if not file.exists(): # URL specified name = Path(urllib.parse.unquote(str(file))).name # decode '%2F' to '/' etc. if str(file).startswith(('http:/', 'https:/')): # download url = str(file).replace(':/', '://') # Pathlib turns :// -> :/ file = name.split('?')[0] # parse authentication https://url.com/file.txt?auth... if Path(file).is_file(): pass #LOGGER.info(f'Found {url} locally at {file}') # file already exists else: safe_download(file=file, url=url, min_bytes=1E5) return file # GitHub assets assets = [ 'yolov5n.pt', 'yolov5s.pt', 'yolov5m.pt', 'yolov5l.pt', 'yolov5x.pt', 'yolov5n6.pt', 'yolov5s6.pt', 'yolov5m6.pt', 'yolov5l6.pt', 'yolov5x6.pt'] try: tag, assets = github_assets(repo, release) except Exception: try: tag, assets = github_assets(repo) # latest release except Exception: try: tag = subprocess.check_output('git tag', shell=True, stderr=subprocess.STDOUT).decode().split()[-1] except Exception: tag = release file.parent.mkdir(parents=True, exist_ok=True) # make parent dir (if required) if name in assets: url3 = 'https://drive.google.com/drive/folders/1EFQTEUeXWSFww0luse2jB9M1QNZQGwNl' # backup gdrive mirror safe_download( file, url=f'https://github.com/{repo}/releases/download/{tag}/{name}', url2=f'https://storage.googleapis.com/{repo}/{tag}/{name}', # backup url (optional) min_bytes=1E5, error_msg=f'{file} missing, try downloading from https://github.com/{repo}/releases/{tag} or {url3}') return str(file) def attempt_load(weights, device=None, inplace=True, fuse=True): # 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(attempt_download(w), map_location='cpu') # load ckpt = (ckpt.get('ema') or ckpt['model']).to(device).float() # FP32 model model.append(ckpt.fuse().eval() if fuse else ckpt.eval()) # fused or un-fused model in eval mode # Compatibility updates for m in model.modules(): t = type(m) if t in (nn.Hardswish, nn.LeakyReLU, nn.ReLU, nn.ReLU6, nn.SiLU, Detect, Model): m.inplace = inplace # torch 1.7.0 compatibility if t is Detect and not isinstance(m.anchor_grid, list): delattr(m, 'anchor_grid') setattr(m, 'anchor_grid', [torch.zeros(1)] * m.nl) elif t is Conv: m._non_persistent_buffers_set = set() # torch 1.6.0 compatibility elif t is nn.Upsample and not hasattr(m, 'recompute_scale_factor'): m.recompute_scale_factor = None # torch 1.11.0 compatibility if len(model) == 1: return model[-1] # return model print(f'Ensemble created with {weights}\n') for k in 'names', 'nc', 'yaml': setattr(model, k, getattr(model[0], k)) model.stride = model[torch.argmax(torch.tensor([m.stride.max() for m in model])).int()].stride # max stride assert all(model[0].nc == m.nc for m in model), f'Models have different class counts: {[m.nc for m in model]}' return model # return ensemble ###### def identify_path_type(path): # 检查路径是否存在 print("原始路径为:{}".format(path)) if path =="always": return "always" if not os.path.exists(path): return 0 if 'long' in path.lower(): return 7 # 检查是否为文件夹 if os.path.isdir(path): items = os.listdir(path) for item in items: if os.path.isdir(os.path.join(path, item)): return 4 # Found a subfolder return 1 if os.path.splitext(path)[1].lower() == '.txt': return 6 # 检查是否为文件 if os.path.isfile(path): # 获取文件的 MIME 类型 mime_type, _ = mimetypes.guess_type(path) if mime_type: # 根据 MIME 类型判断是否为图像或视频 if mime_type.startswith('image'): return 2 elif mime_type.startswith('video'): return 3 return "文件" return "未知类型" class Fine_grained_T: def __init__(self, onnx_model): """ Initialization. Args: onnx_model (str): Path to the ONNX model. """ if 'CUDAExecutionProvider' in ort.get_available_providers(): print("CUDA is available. ONNXRuntime will use GPU.") self.session = ort.InferenceSession( onnx_model, providers=["CUDAExecutionProvider", "CPUExecutionProvider"] if ort.get_device() == "GPU" else ["CPUExecutionProvider"], ) else: print("CUDA is not available. ONNXRuntime will use CPU.") print(ort.get_device()) print("Fine is ready") def inference(self, img, imagelist, labellist, scorelist, fine_thresh1,infsize): with torch.no_grad(): for i, box in enumerate(imagelist): if box[0] < 0: box[0] = 0 if box[1] < 0: box[1] = 0 if box[2] < 0: box[2] = 0 if box[3] < 0: box[3] = 0 X_left = int(box[1])-10 if X_left < 0: X_left = 0 X_right = int(box[3])+10 if X_right>2159: X_right = 2159 y_up = int(box[0])-10 if y_up <0 : y_up = 0 y_down = int(box[2])+10 if y_down>3839: y_down =3839 image = img.copy()[X_left:X_right, y_up:y_down, :] image = Image.fromarray(image) #image.save("/data5/Hatcher/data/count/testresult/11111.jpg") #if gp.DebugPrint_flag: #print(box) #print(Img) #if (int(box[3])-int(box[1])>gp.fine_thresh) and (int(box[2])-int(box[0])>gp.fine_thresh): if ((int(box[3]) - int(box[1])) * (int(box[2]) - int(box[0]))) > fine_thresh1 : # 定义归一化参数 img_norm_cfg = dict( mean=[123.675, 116.28, 103.53], std=[58.395, 57.12, 57.375], to_rgb=True ) image = image.resize((infsize, infsize)) image = np.array(image).astype('float32') if img_norm_cfg['to_rgb']: image = image[..., ::-1] # BGR转RGB image = np.transpose(image, (2, 0, 1)) # HWC转CHW mean = np.array(img_norm_cfg['mean']).reshape(-1, 1, 1) std = np.array(img_norm_cfg['std']).reshape(-1, 1, 1) image = (image - mean) / std image = np.expand_dims(image, axis=0) image = image.astype('float32') # 模型推理 inputs = {self.session.get_inputs()[0].name: image} outputs = self.session.run(None, inputs) fineclass = np.argmax(outputs[0]) labellist[i] = fineclass else: pass return imagelist, labellist, scorelist def inferenceone(self, imagepath): image = Image.open(imagepath) image = image.resize((224, 224)) # 假设模型需要 224x224 的输入 image = np.array(image).astype('float32') image = (np.transpose(image, (2, 0, 1))) # 改变通道顺序 image = np.expand_dims(image, axis=0) inputs = {self.session.get_inputs()[0].name: image} outputs = self.session.run(None, inputs) fineclass = np.argmax(outputs[0]) #print(fineclass) return(fineclass) #print(fineclass) class Detection: def __init__(self,device,DetWeight,ChineseName): self.device = device self.img_size = 2176 self.conf_thres = 0.3#0.6 self.iou_thres = 0.4 self.stride = 1 self.weights = DetWeight self.fine_thresh = 0.6 self.fineinfsize = 224 model = attempt_load(self.weights, device=self.device) model.to(self.device).eval() model.half() self.model = model self.names = model.module.names if hasattr( model, 'module') else model.names self.ChineseName = ChineseName print("Detection is in ",self.device) print("Detection is ready") # while(1): # pass def preprocess(self, img): img0 = img.copy() #img = cv2.resize(img, (2000, 1000)) img = letterbox(img, new_shape=self.img_size, stride=self.stride)[0] img = img[:, :, ::-1].transpose(2, 0, 1) img = np.ascontiguousarray(img) img = torch.from_numpy(img).to(self.device) img = img.half() # ¡ã????¨¨ img /= 255.0 # ¨ª???1¨¦¨°??¡¥ if img.ndimension() == 3: img = img.unsqueeze(0) return img0, img def process_long_image(self, img_path, fine_grained, save_root): image = cv2.imread(img_path) if image is None: print(f"Failed to load image: {img_path}") return original_height, original_width = image.shape[:2] # 计算resize后的尺寸 target_width = 3840 num_splits = max(1, round(original_width / target_width)) new_width = target_width * num_splits scale_factor = new_width / original_width new_height = int(original_height * scale_factor) # resize图像 resized_image = cv2.resize(image, (new_width, new_height)) all_boxes = [] all_labels = [] all_scores = [] for i in range(num_splits): start_x = i * target_width end_x = (i + 1) * target_width sub_image = resized_image[:, start_x:end_x] # 检测处理 img_detect = cv2.resize(sub_image, (3840, 2160)) detect_height, detect_width = img_detect.shape[:2] image_scale, image = self.preprocess(img_detect) with torch.no_grad(): pred = self.model(image, augment=False)[0] pred = pred.float() pred = non_max_suppression(pred, self.conf_thres, self.iou_thres) for det in pred: if det is not None and len(det): # 先将坐标从模型输出转换到3840x2160 det[:, :4] = scale_coords( image.shape[2:], det[:, :4], img_detect.shape).round() # 计算从3840x2160到子图实际大小的缩放比例 width_scale = sub_image.shape[1] / detect_width height_scale = sub_image.shape[0] / detect_height # 转换坐标到子图实际大小 boxes = det[:, :4].cpu().numpy() boxes[:, [0, 2]] *= width_scale boxes[:, [1, 3]] *= height_scale # 添加偏移量 boxes[:, [0, 2]] += start_x scores = det[:, 4].cpu().numpy() labels = det[:, -1].cpu().numpy() if len(labels) > 0: boxes_fine, labels_fine, scores_fine = fine_grained.inference( resized_image, boxes, labels, scores, self.fine_thresh, self.fineinfsize) all_boxes.extend(boxes_fine) all_labels.extend(labels_fine) all_scores.extend(scores_fine) # 创建可视化结果 result_image = resized_image.copy() # 添加调试信息 for i, box in enumerate(all_boxes): print(f"Box {i}: {box}") for box, label, score in zip(all_boxes, all_labels, all_scores): box = [int(b) for b in box] color = (0, 255, 0) # 确保坐标不超出图像边界 x1 = max(0, min(box[0], result_image.shape[1]-1)) y1 = max(0, min(box[1], result_image.shape[0]-1)) x2 = max(0, min(box[2], result_image.shape[1]-1)) y2 = max(0, min(box[3], result_image.shape[0]-1)) cv2.rectangle(result_image, (x1, y1), (x2, y2), color, 2) label_text = "Oriental Stork"#f"{self.ChineseName[int(label)]} {score:.2f}" cv2.putText(result_image, label_text, (x1, max(y1-10, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) if not os.path.exists(save_root): os.makedirs(save_root) output_path = os.path.join(save_root, os.path.basename(img_path)) cv2.imwrite(output_path, result_image) xml_path = output_path.replace('.jpg', '.xml').replace('.JPG', '.xml') make_xml(img_path, resized_image.shape[0], resized_image.shape[1], all_boxes, all_labels, all_scores, xml_path) def AIlabelFine(self, imagename, fine_grained,saveroot,islabel):#imagename 是文件的完整名字 with torch.no_grad(): # t2 = time.time() try: t1 = time.time() image = cv2.imread(imagename) t2 = time.time() if DebugPrint_flag: print("加载图片的时间是 ",(t2-t1)*1000) except: #os.remove(imagename) print(imagename,"is broken") return if not type(image): print("skip") return image_or = image '''可视化注释 img_show = image_or.copy() if (isinstance(img_show, np.ndarray)): img_show = Image.fromarray(cv2.cvtColor(img_show, cv2.COLOR_BGR2RGB)) draw = ImageDraw.Draw(img_show) ''' try: t1 = time.time() img_detect = cv2.resize(image, (3840, 2160)) t2 = time.time() if DebugPrint_flag: print("resize图片的时间是 ",(t2-t1)*1000) except: print(imagename+" is broken") return # t1 = time.time() t1 = time.time() image_scale, image = self.preprocess(img_detect) t2 = time.time() if DebugPrint_flag: print("预处理的时间是 ",(t2-t1)*1000) # t2 = time.time() # if DebugPrint_flag: # tqdm.write("Detection data deal is ",(t2-t1)*1000) t1 = time.time() pred = self.model(image, augment=False)[0] pred = pred.float() pred = non_max_suppression(pred, self.conf_thres, self.iou_thres) # t3 = time.time() # if gp.DebugPrint_flag: # tqdm.write("Detection inference is ",(t3-t2)*1000) boxes = [] scores = [] labels = [] for det in pred: if det is not None and len(det): det[:, :4] = scale_coords( image.shape[2:], det[:, :4], image_scale.shape).round() boxes.append(det[:, :4]) scores.append(det[:, 4]) labels.append((det[:, -1])) boxes = boxes[0] scores = scores[0] labels = labels[0] t2 = time.time() if DebugPrint_flag: print("一次目标检测的时间是 ",(t2-t1)*1000) # t5 = time.time() # if gp.DebugPrint_flag: # tqdm.write("Detection get result is ", (t5 - t3) * 1000) t1 = time.time() if len(labels) > 0: ratio_x = (image_or.shape[1] / 3840.0) ratio_y = (image_or.shape[0] / 2160.0) for i in range(len(boxes)): boxes[i][0] = boxes[i][0] * ratio_x boxes[i][2] = boxes[i][2] * ratio_x boxes[i][1] = boxes[i][1] * ratio_y boxes[i][3] = boxes[i][3] * ratio_y if boxes[i][0] < 0: boxes[i][0] = 0 if boxes[i][1] < 0: boxes[i][1] = 0 if boxes[i][2] < 0: boxes[i][2] = 0 if boxes[i][3] < 0: boxes[i][3] = 0 for i in range(len(labels)): labels[i] = 0 boxes_fine, labels_fine, scores_fine = fine_grained.inference(image_or, boxes, labels, scores, self.fine_thresh,self.fineinfsize) t2 = time.time() if DebugPrint_flag: print("细粒度识别的时间是 ",(t2-t1)*1000) # t4 = time.time() # if gp.DebugPrint_flag: # tqdm.write("Detection final is ", (t4 - t3) * 1000) '''不进行可视化 for i in range(len(scores)): xyxy = boxes_fine[i] conf = scores_fine[i] cls = labels_fine[i] print(int(cls)) label = str(self.ChineseName[int(cls)]) #label = f'{int(cls)} {conf:.2f}' palette = (2 ** 11 - 1, 2 ** 15 - 1, 2 ** 20 - 1) colors = tuple(int((p * (cls ** 2 - cls + 1)) % 255) for p in palette) draw = plot_one_box(xyxy, draw, image_or.shape[1], image_or.shape[0], label=label, color=colors, line_thickness=int(3 * gp.radioShowtext)) savepath = os.path.join(saveroot,os.path.basename(imagename)) img_show = cv2.cvtColor(np.asarray(img_show), cv2.COLOR_RGB2BGR) cv2.imwrite(savepath,img_show) ''' if islabel: # for i in range(len(scores)): # cls = labels_fine[i] # print(int(cls)) # labels_fine[i] = str(self.ChineseName[int(cls)]) #print() t1 = time.time() savepath = os.path.join(saveroot,os.path.basename(imagename)) xml_outputpath = savepath.replace(".jpg", ".xml").replace(".JPG", ".xml") make_xml(imagename, image_or.shape[0], image_or.shape[1], boxes_fine, labels_fine, scores_fine, xml_outputpath) t2 = time.time() if DebugPrint_flag: print("结果存储的时间是 ",(t2-t1)*1000) else: pass '''不保存数据 savepath = os.path.join(saveroot,os.path.basename(imagename)) shutil.copy(imagename, savepath) ''' def get_all_image_paths(folder_path): # Define common image file extensions image_extensions = ('.jpg', '.jpeg', '.png', '.gif', '.bmp', '.tiff', '.tif') # Initialize an empty list to store the image paths image_paths = [] # Walk through the folder and its subfolders for root, dirs, files in tqdm(os.walk(folder_path)): for file in files: if file.lower().endswith(image_extensions): # Get the absolute path of the image file image_path = os.path.join(root, file) image_paths.append(image_path) return image_paths if __name__ == '__main__': # Fine = Fine_grained_T("/data5/Train_Models/mmcls_onnx/CMB_YNDC_20240115_29/convnext-base_4xb32_cmb200/end2end.onnx") # result = Fine.inferenceone("/data5/CAS_Project/CAS_ProjectBird/CMB/CMB_YNDC/CMB_YNDC_20231129_30/OrginalData/Bird/Bird___BJCH014_000447.jpg") """ 测试 -dw /data5/Train_Models/Detection_weigth/3090/202306/20230619-131704-pixel5000-BJNHZ_Person/weights/best.pt -cw /data5/Train_Models/mmcls_onnx/CMB_BJNHZ_20230910_144/convnext-base_4xb32_cmb200/end2end.onnx -c /data5/CAS_Project/CAS_ProjectBird/CMB/CMB_BJNHZ/CMB_BJNHZ_20230910_144/birdListbirdCNlist.txt -m "ai" -s "/data5/Hatcher/data/detection/test1" -o "/data5/Hatcher/data/detection/testresult" """ Det_weights,Fine_weights,Fine_class,infmode,sourcepath,saveroot = parse_args() # with open(Fine_class, 'r', encoding='utf-8') as file: # bird_list = file.readlines() with open(Fine_class, 'r', encoding='utf-8') as file: content = file.read().strip() # 读取文件内容并去除首尾空格 bird_list = content.split(';') # 使用分号分割字符串 #bird_list = [bird.strip() for bird in bird_list] print(bird_list) print(saveroot) devices = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu') detection = Detection(devices,Det_weights,bird_list) Fine = Fine_grained_T(Fine_weights) if not os.path.exists(saveroot): os.makedirs(saveroot) fileType = identify_path_type(sourcepath) print("处理模式为{}".format(str(fileType))) if infmode == "ai": islabel = 1 if (fileType == 2): print("开始处理单张图像数据") #缺一个处理单个的函数 detection.AIlabelFine(sourcepath,Fine,saveroot,islabel) if (fileType == 1): #说明是文件夹 print("开始处理文件夹批量数据") fileList = os.listdir(sourcepath) for filepath in tqdm(fileList): sourcePath = os.path.join(sourcepath,filepath) #缺一个处理单个的函数 #print(sourcePath) detection.AIlabelFine(sourcePath,Fine,saveroot,islabel) if (fileType == 4): #说明是文件夹 print("开始处理复杂文件夹批量数据") fileList = get_all_image_paths(sourcepath) #print(fileList) for filepath in tqdm(fileList): #sourcePath = os.path.join(sourcepath,filepath) #缺一个处理单个的函数 #print(sourcePath) t1 = time.time() detection.AIlabelFine(filepath,Fine,saveroot,islabel) t2 = time.time() if DebugPrint_flag: print("推理一张图片的时间是 ",(t2-t1)*1000) if (fileType == 6): #说明是文件路径的txt print("开始处理txt文件中的批量数据") with open(sourcepath, 'r') as file: image_paths = [line.strip() for line in file.readlines()] fileList = image_paths #print(fileList) for filepath in tqdm(fileList): #sourcePath = os.path.join(sourcepath,filepath) #缺一个处理单个的函数 #print(sourcePath) t1 = time.time() detection.AIlabelFine(filepath,Fine,saveroot,islabel) t2 = time.time() if DebugPrint_flag: print("推理一张图片的时间是 ",(t2-t1)*1000) if (fileType == 7): #说明是文件路径的txt print("开始处理长图像数据") parent_dir = os.path.dirname(sourcepath) save_root = os.path.join(parent_dir, 'output_') if not os.path.exists(save_root): os.makedirs(save_root) # 处理文件夹中的所有图像 for img_name in tqdm(os.listdir(sourcepath)): if img_name.lower().endswith(('.jpg', '.jpeg', '.png')): img_path = os.path.join(sourcepath, img_name) detection.process_long_image(img_path, Fine, save_root) if (fileType == "always"): print("开始循环处理输入数据") while(1): sourcepath = input("请输入测试文件路径: ") if sourcepath == "q": break detection.AIlabelFine(sourcepath,Fine,saveroot,islabel) #缺一个处理单个的函数