senjing/src/FireDetect/SengJingDetect.py

598 lines
25 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import math
import threading
import time
import contextlib
import numpy as np
import torch
import torchvision
from models.common import DetectMultiBackend
import cv2
from utils.plots import Annotator
import pkg_resources as pkg
import platform
import logging
import logging.config
import os
from PIL import Image, ImageDraw, ImageFont
from pathlib import Path
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)
LOGGING_NAME = "yolov5"
def set_logging(name=LOGGING_NAME, verbose=True):
# sets up logging for the given name
rank = int(os.getenv('RANK', -1)) # rank in world for Multi-GPU trainings
level = logging.INFO if verbose and rank in {-1, 0} else logging.ERROR
logging.config.dictConfig({
"version": 1,
"disable_existing_loggers": False,
"formatters": {
name: {
"format": "%(message)s"}},
"handlers": {
name: {
"class": "logging.StreamHandler",
"formatter": name,
"level": level,}},
"loggers": {
name: {
"level": level,
"handlers": [name],
"propagate": False,}}})
set_logging(LOGGING_NAME) # run before defining LOGGER
LOGGER = logging.getLogger(LOGGING_NAME) # define globally (used in train.py, val.py, detect.py, etc.)
def emojis(str=''):
# Return platform-dependent emoji-safe version of string
return str.encode().decode('ascii', 'ignore') if platform.system() == 'Windows' else str
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'WARNING ⚠️ {name}{minimum} is required by YOLOv5, but {name}{current} is currently installed' # string
if hard:
assert result, emojis(s) # assert min requirements met
if verbose and not result:
LOGGER.warning(s)
return result
def smart_inference_mode(torch_1_9=check_version(torch.__version__, '1.9.0')):
# Applies torch.inference_mode() decorator if torch>=1.9.0 else torch.no_grad() decorator
def decorate(fn):
return (torch.inference_mode if torch_1_9 else torch.no_grad)()(fn)
return decorate
def box_iou(box1, box2, eps=1e-7):
# https://github.com/pytorch/vision/blob/master/torchvision/ops/boxes.py
"""
Return intersection-over-union (Jaccard index) of boxes.
Both sets of boxes are expected to be in (x1, y1, x2, y2) format.
Arguments:
box1 (Tensor[N, 4])
box2 (Tensor[M, 4])
Returns:
iou (Tensor[N, M]): the NxM matrix containing the pairwise
IoU values for every element in boxes1 and boxes2
"""
# inter(N,M) = (rb(N,M,2) - lt(N,M,2)).clamp(0).prod(2)
(a1, a2), (b1, b2) = box1.unsqueeze(1).chunk(2, 2), box2.unsqueeze(0).chunk(2, 2)
inter = (torch.min(a2, b2) - torch.max(a1, b1)).clamp(0).prod(2)
# IoU = inter / (area1 + area2 - inter)
return inter / ((a2 - a1).prod(2) + (b2 - b1).prod(2) - inter + eps)
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 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 clip_boxes(boxes, shape):
# Clip boxes (xyxy) 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_boxes(img1_shape, boxes, img0_shape, ratio_pad=None):
# Rescale boxes (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]
boxes[:, [0, 2]] -= pad[0] # x padding
boxes[:, [1, 3]] -= pad[1] # y padding
boxes[:, :4] /= gain
clip_boxes(boxes, img0_shape)
return boxes
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 non_max_suppression(
prediction,
conf_thres=0.25,
iou_thres=0.45,
classes=None,
agnostic=False,
multi_label=False,
labels=(),
max_det=300,
nm=0, # number of masks
):
"""Non-Maximum Suppression (NMS) on inference results to reject overlapping detections
Returns:
list of detections, on (n,6) tensor per image [xyxy, conf, cls]
"""
# YOLOv5 model in validation model, output = (inference_out, loss_out)
prediction = prediction[0] # select only inference output
nc = prediction.shape[2] - nm - 5 # number of classes
xc = prediction[..., 4] > conf_thres # candidates
# Checks
mi = 5 + nc # mask start index
output = [torch.zeros((0, 6 + nm), device=prediction.device)]
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 + nm + 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/Mask
box = xywh2xyxy(x[:, :4]) # center_x, center_y, width, height) to (x1, y1, x2, y2)
mask = x[:, mi:] # zero columns if no masks
# Detections matrix nx6 (xyxy, conf, cls)
conf, j = x[:, 5:mi].max(1, keepdim=True)
x = torch.cat((box, conf, j.float(), mask), 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
x = x[x[:, 4].argsort(descending=True)] # sort by confidence
# Batched NMS
c = x[:, 5:6] # classes
boxes, scores = x[:, :4] + c, x[:, 4] # boxes (offset by class), scores
i = torchvision.ops.nms(boxes, scores, iou_thres) # NMS
# limit detections
i = i[:max_det]
# 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
i = i[iou.sum(1) > 1] # require redundancy
output[xi] = x[i]
return output
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.W_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() # Ultralytics settings dir
FONT = 'Arial.ttf' # https://ultralytics.com/assets/Arial.ttf
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 = f'https://ultralytics.com/assets/{font.name}'
LOGGER.info(f'Downloading {url} to {file}...')
torch.hub.download_url_to_file(url, str(file), progress=progress)
def check_python(minimum='3.7.0'):
# Check current python version vs. required python version
check_version(platform.python_version(), minimum, name='Python ', hard=True)
class TryExcept(contextlib.ContextDecorator):
# YOLOv5 TryExcept class. Usage: @TryExcept() decorator or 'with TryExcept():' context manager
def __init__(self, msg=''):
self.msg = msg
def __enter__(self):
pass
def __exit__(self, exc_type, value, traceback):
if value:
print(emojis(f"{self.msg}{': ' if self.msg else ''}{value}"))
return True
class TryExcept(contextlib.ContextDecorator):
# YOLOv5 TryExcept class. Usage: @TryExcept() decorator or 'with TryExcept():' context manager
def __init__(self, msg=''):
self.msg = msg
def __enter__(self):
pass
def __exit__(self, exc_type, value, traceback):
if value:
print(emojis(f"{self.msg}{': ' if self.msg else ''}{value}"))
return True
def colorstr(*input):
# Colors a string https://en.wikipedia.org/wiki/ANSI_escape_code, i.e. colorstr('blue', 'hello world')
*args, string = input if len(input) > 1 else ('blue', 'bold', input[0]) # color arguments, string
colors = {
'black': '\033[30m', # basic colors
'red': '\033[31m',
'green': '\033[32m',
'yellow': '\033[33m',
'blue': '\033[34m',
'magenta': '\033[35m',
'cyan': '\033[36m',
'white': '\033[37m',
'bright_black': '\033[90m', # bright colors
'bright_red': '\033[91m',
'bright_green': '\033[92m',
'bright_yellow': '\033[93m',
'bright_blue': '\033[94m',
'bright_magenta': '\033[95m',
'bright_cyan': '\033[96m',
'bright_white': '\033[97m',
'end': '\033[0m', # misc
'bold': '\033[1m',
'underline': '\033[4m'}
return ''.join(colors[x] for x in args) + f'{string}' + colors['end']
@TryExcept()
# def check_requirements(requirements='requirements.txt', exclude=(), install=True, cmds=''):
# # Check installed dependencies meet YOLOv5 requirements (pass *.txt file or list of packages or single package str)
# prefix = colorstr('red', 'bold', 'requirements:')
# check_python() # check python version
# if isinstance(requirements, Path): # requirements.txt file
# file = requirements.resolve()
# assert file.exists(), f"{prefix} {file} not found, check failed."
# with file.open() as f:
# requirements = [f'{x.name}{x.specifier}' for x in pkg.parse_requirements(f) if x.name not in exclude]
# elif isinstance(requirements, str):
# requirements = [requirements]
#
# s = ''
# n = 0
# for r in requirements:
# try:
# pkg.require(r)
# except (pkg.VersionConflict, pkg.DistributionNotFound): # exception if requirements not met
# s += f'"{r}" '
# n += 1
#
# if s and install and AUTOINSTALL: # check environment variable
# LOGGER.info(f"{prefix} YOLOv5 requirement{'s' * (n > 1)} {s}not found, attempting AutoUpdate...")
# try:
# # assert check_online(), "AutoUpdate skipped (offline)"
# LOGGER.info(check_output(f'pip install {s} {cmds}', shell=True).decode())
# source = file if 'file' in locals() else requirements
# s = f"{prefix} {n} package{'s' * (n > 1)} updated per {source}\n" \
# f"{prefix} ⚠️ {colorstr('bold', 'Restart runtime or rerun command for updates to take effect')}\n"
# LOGGER.info(s)
# except Exception as e:
# LOGGER.warning(f'{prefix} ❌ {e}')
# 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:
# check_requirements('Pillow>=8.4.0') # known issue https://github.com/ultralytics/yolov5/issues/5374
# except URLError: # not online
# return ImageFont.load_default()
def scale_image(im1_shape, masks, im0_shape, ratio_pad=None):
"""
img1_shape: model input shape, [h, w]
img0_shape: origin pic shape, [h, w, 3]
masks: [h, w, num]
"""
# Rescale coordinates (xyxy) from im1_shape to im0_shape
if ratio_pad is None: # calculate from im0_shape
gain = min(im1_shape[0] / im0_shape[0], im1_shape[1] / im0_shape[1]) # gain = old / new
pad = (im1_shape[1] - im0_shape[1] * gain) / 2, (im1_shape[0] - im0_shape[0] * gain) / 2 # wh padding
else:
pad = ratio_pad[1]
top, left = int(pad[1]), int(pad[0]) # y, x
bottom, right = int(im1_shape[0] - pad[1]), int(im1_shape[1] - pad[0])
if len(masks.shape) < 2:
raise ValueError(f'"len of masks shape" should be 2 or 3, but got {len(masks.shape)}')
masks = masks[top:bottom, left:right]
# masks = masks.permute(2, 0, 1).contiguous()
# masks = F.interpolate(masks[None], im0_shape[:2], mode='bilinear', align_corners=False)[0]
# masks = masks.permute(1, 2, 0).contiguous()
masks = cv2.resize(masks, (im0_shape[1], im0_shape[0]))
if len(masks.shape) == 2:
masks = masks[:, :, None]
return masks
# 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 masks(self, masks, colors, im_gpu=None, alpha=0.5):
# """Plot masks at once.
# Args:
# masks (tensor): predicted masks on cuda, shape: [n, h, w]
# colors (List[List[Int]]): colors for predicted masks, [[r, g, b] * n]
# im_gpu (tensor): img is in cuda, shape: [3, h, w], range: [0, 1]
# alpha (float): mask transparency: 0.0 fully transparent, 1.0 opaque
# """
# if self.pil:
# # convert to numpy first
# self.im = np.asarray(self.im).copy()
# if im_gpu is None:
# # Add multiple masks of shape(h,w,n) with colors list([r,g,b], [r,g,b], ...)
# if len(masks) == 0:
# return
# if isinstance(masks, torch.Tensor):
# masks = torch.as_tensor(masks, dtype=torch.uint8)
# masks = masks.permute(1, 2, 0).contiguous()
# masks = masks.cpu().numpy()
# # masks = np.ascontiguousarray(masks.transpose(1, 2, 0))
# masks = scale_image(masks.shape[:2], masks, self.im.shape)
# masks = np.asarray(masks, dtype=np.float32)
# colors = np.asarray(colors, dtype=np.float32) # shape(n,3)
# s = masks.sum(2, keepdims=True).clip(0, 1) # add all masks together
# masks = (masks @ colors).clip(0, 255) # (h,w,n) @ (n,3) = (h,w,3)
# self.im[:] = masks * alpha + self.im * (1 - s * alpha)
# else:
# if len(masks) == 0:
# self.im[:] = im_gpu.permute(1, 2, 0).contiguous().cpu().numpy() * 255
# colors = torch.tensor(colors, device=im_gpu.device, dtype=torch.float32) / 255.0
# colors = colors[:, None, None] # shape(n,1,1,3)
# masks = masks.unsqueeze(3) # shape(n,h,w,1)
# masks_color = masks * (colors * alpha) # shape(n,h,w,3)
#
# inv_alph_masks = (1 - masks * alpha).cumprod(0) # shape(n,h,w,1)
# mcs = (masks_color * inv_alph_masks).sum(0) * 2 # mask color summand shape(n,h,w,3)
#
# im_gpu = im_gpu.flip(dims=[0]) # flip channel
# im_gpu = im_gpu.permute(1, 2, 0).contiguous() # shape(h,w,3)
# im_gpu = im_gpu * inv_alph_masks[-1] + mcs
# im_mask = (im_gpu * 255).byte().cpu().numpy()
# self.im[:] = scale_image(im_gpu.shape, im_mask, self.im.shape)
# if self.pil:
# # convert im back to PIL and update draw
# self.fromarray(self.im)
#
# 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), anchor='top'):
# # Add text to image (PIL-only)
# if anchor == 'bottom': # start y from font bottom
# w, h = self.font.getsize(text) # text width, height
# xy[1] += 1 - h
# self.draw.text(xy, text, fill=txt_color, font=self.font)
#
# def fromarray(self, im):
# # Update self.im from a numpy array
# self.im = im if isinstance(im, Image.Image) else Image.fromarray(im)
# self.draw = ImageDraw.Draw(self.im)
#
# def result(self):
# # Return annotated image as array
# return np.asarray(self.im)
@smart_inference_mode()
def run():
# Load model
device = torch.device('cuda:0')
model = DetectMultiBackend(weights='fire.pt', device=device, dnn=False, data=False, fp16=True)
stride, names, pt = model.stride, model.names, model.pt
#IP摄像头
cap = cv2.VideoCapture("http://admin:admin@10.129.50.72:8081") # URL of the IP camera
# 读取图片
while True:
ret, img0 = cap.read()
#图片旋转
img0 = np.rot90(img0,3)
img0 = np.array(img0)
img0 = cv2.cvtColor(img0, cv2.COLOR_BGRA2BGR)
# 处理图片
im = letterbox(img0, (640, 640), stride=32, auto=True)[0] # padded resize
im = im.transpose((2, 0, 1))[::-1] # HWC to CHW, BGR to RGB
im = np.ascontiguousarray(im) # contiguous
im = torch.from_numpy(im).to(model.device)
im = im.half() if model.fp16 else im.float() # uint8 to fp16/32
im /= 255 # 0 - 255 to 0.0 - 1.0
if len(im.shape) == 3:
im = im[None] # expand for batch dim
# 推理
pred = model(im, augment=False, visualize=False)
# 非极大值抑制
pred = non_max_suppression(pred, conf_thres=0.4, iou_thres=0.05, classes=None, max_det=1000)
# 处理推理内容
for i, det in enumerate(pred):
# 画框
annotator = Annotator(img0, line_width=2)
if len(det):
target_list = []
# 将转换后的图片画框结果转换成原图上的结果
det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], img0.shape).round()
for *xyxy, conf, cls in reversed(det): # 处理推理出来每个目标的信息
# 将xyxy(左上角+右下角)格式转为xywh(中心点+宽长)格式并除上wh做归一化转化为列表再保存
xywh = (xyxy2xywh(torch.tensor(xyxy).view(1, 4))).view(-1).tolist() # normalized xywh
annotator.box_label(xyxy, label=f'[{names[int(cls)]} {conf:.2f}]',
color=(34, 139, 34),
txt_color=(0, 191, 255))
target_list.append(xywh)
im0 = annotator.result()
cv2.imshow('window', im0)
cv2.waitKey(1)
if __name__ == "__main__":
run()