|
import argparse |
|
import json |
|
import os |
|
from pathlib import Path |
|
from tqdm import tqdm |
|
import glob |
|
import math |
|
from PIL import ExifTags, Image |
|
import shutil |
|
from torch.utils.data import DataLoader |
|
from torch.utils.data import Dataset |
|
from utils import * |
|
import onnxruntime |
|
import matplotlib.pyplot as plt |
|
|
|
for orientation in ExifTags.TAGS.keys(): |
|
if ExifTags.TAGS[orientation] == 'Orientation': |
|
break |
|
|
|
|
|
def create_folder(path='./new_folder'): |
|
|
|
if os.path.exists(path): |
|
shutil.rmtree(path) |
|
os.makedirs(path) |
|
|
|
|
|
def exif_size(img): |
|
|
|
s = img.size |
|
try: |
|
rotation = dict(img._getexif().items())[orientation] |
|
if rotation == 6: |
|
s = (s[1], s[0]) |
|
elif rotation == 8: |
|
s = (s[1], s[0]) |
|
except BaseException: |
|
pass |
|
|
|
return s |
|
|
|
|
|
def ap_per_class(tp, conf, pred_cls, target_cls): |
|
""" Compute the average precision, given the recall and precision curves. |
|
Source: https://github.com/rafaelpadilla/Object-Detection-Metrics. |
|
# Arguments |
|
tp: True positives (nparray, nx1 or nx10). |
|
conf: Objectness value from 0-1 (nparray). |
|
pred_cls: Predicted object classes (nparray). |
|
target_cls: True object classes (nparray). |
|
# Returns |
|
The average precision as computed in py-faster-rcnn. |
|
""" |
|
|
|
|
|
i = np.argsort(-conf) |
|
tp, conf, pred_cls = tp[i], conf[i], pred_cls[i] |
|
|
|
|
|
unique_classes = np.unique(target_cls) |
|
|
|
|
|
pr_score = 0.1 |
|
|
|
|
|
|
|
s = [unique_classes.shape[0], tp.shape[1]] |
|
ap, p, r = np.zeros(s), np.zeros(s), np.zeros(s) |
|
for ci, c in enumerate(unique_classes): |
|
i = pred_cls == c |
|
n_gt = (target_cls == c).sum() |
|
n_p = i.sum() |
|
|
|
if n_p == 0 or n_gt == 0: |
|
continue |
|
else: |
|
|
|
fpc = (1 - tp[i]).cumsum(0) |
|
tpc = tp[i].cumsum(0) |
|
|
|
|
|
recall = tpc / (n_gt + 1e-16) |
|
|
|
r[ci] = np.interp(-pr_score, -conf[i], recall[:, 0]) |
|
|
|
|
|
precision = tpc / (tpc + fpc) |
|
p[ci] = np.interp(-pr_score, -conf[i], |
|
precision[:, 0]) |
|
|
|
|
|
for j in range(tp.shape[1]): |
|
ap[ci, j] = compute_ap(recall[:, j], precision[:, j]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
f1 = 2 * p * r / (p + r + 1e-16) |
|
|
|
return p, r, ap, f1, unique_classes.astype('int32') |
|
|
|
|
|
def time_synchronized(): |
|
torch.cuda.synchronize() if torch.cuda.is_available() else None |
|
return time.time() |
|
|
|
|
|
def plot_images( |
|
images, |
|
targets, |
|
paths=None, |
|
fname='images.jpg', |
|
names=None, |
|
max_size=640, |
|
max_subplots=16): |
|
tl = 3 |
|
tf = max(tl - 1, 1) |
|
if os.path.isfile(fname): |
|
return None |
|
|
|
if isinstance(images, torch.Tensor): |
|
images = images.cpu().numpy() |
|
|
|
if isinstance(targets, torch.Tensor): |
|
targets = targets.cpu().numpy() |
|
|
|
|
|
if np.max(images[0]) <= 1: |
|
images *= 255 |
|
|
|
bs, _, h, w = images.shape |
|
bs = min(bs, max_subplots) |
|
ns = np.ceil(bs ** 0.5) |
|
|
|
|
|
scale_factor = max_size / max(h, w) |
|
if scale_factor < 1: |
|
h = math.ceil(scale_factor * h) |
|
w = math.ceil(scale_factor * w) |
|
|
|
|
|
mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8) |
|
|
|
|
|
prop_cycle = plt.rcParams['axes.prop_cycle'] |
|
|
|
|
|
def hex2rgb(h): |
|
return tuple( |
|
int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4)) |
|
|
|
color_lut = [hex2rgb(h) for h in prop_cycle.by_key()['color']] |
|
|
|
for i, img in enumerate(images): |
|
if i == max_subplots: |
|
break |
|
|
|
block_x = int(w * (i // ns)) |
|
block_y = int(h * (i % ns)) |
|
|
|
img = img.transpose(1, 2, 0) |
|
if scale_factor < 1: |
|
img = cv2.resize(img, (w, h)) |
|
|
|
mosaic[block_y:block_y + h, block_x:block_x + w, :] = img |
|
if len(targets) > 0: |
|
image_targets = targets[targets[:, 0] == i] |
|
boxes = xywh2xyxy(image_targets[:, 2:6]).T |
|
classes = image_targets[:, 1].astype('int') |
|
gt = image_targets.shape[1] == 6 |
|
|
|
|
|
conf = None if gt else image_targets[:, 6] |
|
|
|
boxes[[0, 2]] *= w |
|
boxes[[0, 2]] += block_x |
|
boxes[[1, 3]] *= h |
|
boxes[[1, 3]] += block_y |
|
for j, box in enumerate(boxes.T): |
|
cls = int(classes[j]) |
|
color = color_lut[cls % len(color_lut)] |
|
cls = names[cls] if names else cls |
|
if gt or conf[j] > 0.3: |
|
label = '%s' % cls if gt else '%s %.1f' % (cls, conf[j]) |
|
plot_one_box(box, mosaic, label=label, |
|
color=color, line_thickness=tl) |
|
|
|
|
|
if paths is not None: |
|
label = os.path.basename(paths[i])[:40] |
|
t_size = cv2.getTextSize( |
|
label, 0, fontScale=tl / 3, thickness=tf)[0] |
|
cv2.putText(mosaic, label, (block_x + |
|
5, block_y + |
|
t_size[1] + |
|
5), 0, tl / |
|
3, [220, 220, 220], thickness=tf, lineType=cv2.LINE_AA) |
|
|
|
|
|
cv2.rectangle(mosaic, (block_x, block_y), (block_x + w, |
|
block_y + h), (255, 255, 255), thickness=3) |
|
|
|
if fname is not None: |
|
mosaic = cv2.resize(mosaic, |
|
(int(ns * w * 0.5), |
|
int(ns * h * 0.5)), |
|
interpolation=cv2.INTER_AREA) |
|
cv2.imwrite(fname, cv2.cvtColor(mosaic, cv2.COLOR_BGR2RGB)) |
|
|
|
return mosaic |
|
|
|
|
|
def random_affine(img, targets=(), degrees=10, translate=.1, |
|
scale=.1, shear=10, border=0): |
|
|
|
|
|
height = img.shape[0] + border * 2 |
|
width = img.shape[1] + border * 2 |
|
|
|
|
|
R = np.eye(3) |
|
a = random.uniform(-degrees, degrees) |
|
|
|
|
|
s = random.uniform(1 - scale, 1 + scale) |
|
|
|
R[:2] = cv2.getRotationMatrix2D(angle=a, |
|
center=(img.shape[1] / 2, |
|
img.shape[0] / 2), |
|
scale=s) |
|
|
|
|
|
T = np.eye(3) |
|
T[0, 2] = (random.uniform(-translate, translate) * |
|
img.shape[0] + border) |
|
T[1, 2] = (random.uniform(-translate, translate) * |
|
img.shape[1] + border) |
|
|
|
|
|
S = np.eye(3) |
|
S[0, 1] = math.tan(random.uniform(-shear, shear) * |
|
math.pi / 180) |
|
S[1, 0] = math.tan(random.uniform(-shear, shear) * |
|
math.pi / 180) |
|
|
|
|
|
M = S @ T @ R |
|
if (border != 0) or (M != np.eye(3)).any(): |
|
img = cv2.warpAffine(img, M[:2], dsize=(width, height), |
|
flags=cv2.INTER_LINEAR, |
|
borderValue=(114, 114, 114)) |
|
|
|
|
|
n = len(targets) |
|
if n: |
|
|
|
xy = np.ones((n * 4, 3)) |
|
xy[:, :2] = (targets[:, [1, 2, 3, 4, 1, 4, 3, 2]]. |
|
reshape(n * 4, 2)) |
|
|
|
xy = (xy @ M.T)[:, :2].reshape(n, 8) |
|
|
|
|
|
x = xy[:, [0, 2, 4, 6]] |
|
y = xy[:, [1, 3, 5, 7]] |
|
xy = np.concatenate((x.min(1), y.min(1), x.max(1), |
|
y.max(1))).reshape(4, n).T |
|
|
|
|
|
xy[:, [0, 2]] = xy[:, [0, 2]].clip(0, width) |
|
xy[:, [1, 3]] = xy[:, [1, 3]].clip(0, height) |
|
w = xy[:, 2] - xy[:, 0] |
|
h = xy[:, 3] - xy[:, 1] |
|
area = w * h |
|
area0 = ((targets[:, 3] - targets[:, 1]) * |
|
(targets[:, 4] - targets[:, 2])) |
|
ar = np.maximum(w / (h + 1e-16), h / (w + 1e-16)) |
|
|
|
i = (w > 4) & (h > 4) & (area / (area0 * s + 1e-16) |
|
> 0.2) & (ar < 10) |
|
|
|
targets = targets[i] |
|
targets[:, 1:5] = xy[i] |
|
|
|
return img, targets |
|
|
|
|
|
def output_to_target(output, width, height): |
|
""" |
|
Convert a YOLO model output to target format |
|
[batch_id, class_id, x, y, w, h, conf] |
|
""" |
|
if isinstance(output, torch.Tensor): |
|
output = output.cpu().numpy() |
|
|
|
targets = [] |
|
for i, o in enumerate(output): |
|
if o is not None: |
|
for pred in o: |
|
box = pred[:4] |
|
w = (box[2] - box[0]) / width |
|
h = (box[3] - box[1]) / height |
|
x = box[0] / width + w / 2 |
|
y = box[1] / height + h / 2 |
|
conf = pred[4] |
|
cls = int(pred[5]) |
|
|
|
targets.append([i, cls, x, y, w, h, conf]) |
|
|
|
return np.array(targets) |
|
|
|
|
|
def xyxy2xywh(x): |
|
|
|
|
|
y = torch.zeros_like(x) if isinstance( |
|
x, torch.Tensor) else np.zeros_like(x) |
|
y[:, 0] = (x[:, 0] + x[:, 2]) / 2 |
|
y[:, 1] = (x[:, 1] + x[:, 3]) / 2 |
|
y[:, 2] = x[:, 2] - x[:, 0] |
|
y[:, 3] = x[:, 3] - x[:, 1] |
|
return y |
|
|
|
|
|
def coco80_to_coco91_class(): |
|
|
|
x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 16, 17, 18, 19, 20, |
|
21, 22, 23, 24, 25, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, |
|
41, 42, 43, 44, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, |
|
59, 60, 61, 62, 63, 64, 65, 67, 70, 72, 73, 74, 75, 76, 77, 78, 79, |
|
80, 81, 82, 84, 85, 86, 87, 88, 89, 90] |
|
return x |
|
|
|
|
|
def check_file(file): |
|
|
|
if os.path.isfile(file): |
|
return file |
|
else: |
|
files = glob.glob('./**/' + file, recursive=True) |
|
assert len(files), 'File Not Found: %s' % file |
|
return files[0] |
|
|
|
|
|
def load_classes(path): |
|
|
|
with open(path, 'r') as f: |
|
names = f.read().split('\n') |
|
|
|
return list(filter(None, names)) |
|
|
|
|
|
def load_image(self, index): |
|
|
|
img = self.imgs[index] |
|
if img is None: |
|
path = self.img_files[index] |
|
img = cv2.imread(path) |
|
assert img is not None, 'Image Not Found ' + path |
|
h0, w0 = img.shape[:2] |
|
r = self.img_size / max(h0, w0) |
|
if r != 1: |
|
|
|
interp = cv2.INTER_AREA if r < 1 and not self.augment \ |
|
else cv2.INTER_LINEAR |
|
img = cv2.resize(img, (int(w0 * r), int(h0 * r)), |
|
interpolation=interp) |
|
return img, (h0, w0), img.shape[:2] |
|
else: |
|
|
|
return self.imgs[index], self.img_hw0[index], self.img_hw[index] |
|
|
|
|
|
def load_mosaic(self, index): |
|
|
|
|
|
labels4 = [] |
|
s = self.img_size |
|
xc, yc = [int(random.uniform(s * 0.5, s * 1.5)) |
|
for _ in range(2)] |
|
indices = [index] + [random.randint(0, len(self.labels) - 1) |
|
for _ in range(3)] |
|
for i, index in enumerate(indices): |
|
|
|
img, _, (h, w) = load_image(self, index) |
|
|
|
|
|
if i == 0: |
|
img4 = np.full((s * 2, s * 2, img.shape[2]), |
|
114, dtype=np.uint8) |
|
|
|
x1a, y1a, x2a, y2a = (max(xc - w, 0), |
|
max(yc - h, 0), xc, yc) |
|
|
|
x1b, y1b, x2b, y2b = (w - (x2a - x1a), h - |
|
(y2a - y1a), w, h) |
|
|
|
elif i == 1: |
|
x1a, y1a, x2a, y2a = (xc, max(yc - h, 0), |
|
min(xc + w, s * 2), yc) |
|
x1b, y1b, x2b, y2b = (0, h - (y2a - y1a), |
|
min(w, x2a - x1a), h) |
|
elif i == 2: |
|
x1a, y1a, x2a, y2a = (max(xc - w, 0), yc, |
|
xc, min(s * 2, yc + h)) |
|
x1b, y1b, x2b, y2b = (w - (x2a - x1a), 0, |
|
max(xc, w), min(y2a - y1a, h)) |
|
elif i == 3: |
|
x1a, y1a, x2a, y2a = xc, yc, min(xc + w, |
|
s * 2), min(s * 2, yc + h) |
|
x1b, y1b, x2b, y2b = (0, 0, |
|
min(w, x2a - x1a), min(y2a - y1a, h)) |
|
|
|
img4[y1a:y2a, x1a:x2a] = img[y1b:y2b, x1b:x2b] |
|
|
|
padw = x1a - x1b |
|
padh = y1a - y1b |
|
|
|
|
|
x = self.labels[index] |
|
labels = x.copy() |
|
if x.size > 0: |
|
labels[:, 1] = w * (x[:, 1] - x[:, 3] / 2) + padw |
|
labels[:, 2] = h * (x[:, 2] - x[:, 4] / 2) + padh |
|
labels[:, 3] = w * (x[:, 1] + x[:, 3] / 2) + padw |
|
labels[:, 4] = h * (x[:, 2] + x[:, 4] / 2) + padh |
|
labels4.append(labels) |
|
|
|
|
|
if len(labels4): |
|
labels4 = np.concatenate(labels4, 0) |
|
|
|
|
|
np.clip(labels4[:, 1:], 0, 2 * s, out=labels4[:, 1:]) |
|
|
|
|
|
|
|
|
|
|
|
img4, labels4 = random_affine(img4, labels4, |
|
degrees=self.hyp['degrees'], |
|
translate=self.hyp['translate'], |
|
scale=self.hyp['scale'], |
|
shear=self.hyp['shear'], |
|
border=-s // 2) |
|
|
|
return img4, labels4 |
|
|
|
|
|
def compute_ap(recall, precision): |
|
""" Compute the average precision, given the recall and precision curves. |
|
Source: https://github.com/rbgirshick/py-faster-rcnn. |
|
# Arguments |
|
recall: The recall curve (list). |
|
precision: The precision curve (list). |
|
# Returns |
|
The average precision as computed in py-faster-rcnn. |
|
""" |
|
|
|
|
|
mrec = np.concatenate(([0.], recall, [min(recall[-1] + 1E-3, 1.)])) |
|
mpre = np.concatenate(([0.], precision, [0.])) |
|
|
|
|
|
mpre = np.flip(np.maximum.accumulate(np.flip(mpre))) |
|
|
|
|
|
method = 'interp' |
|
if method == 'interp': |
|
x = np.linspace(0, 1, 101) |
|
ap = np.trapz(np.interp(x, mrec, mpre), x) |
|
else: |
|
|
|
i = np.where(mrec[1:] != mrec[:-1])[0] |
|
ap = np.sum((mrec[i + 1] - mrec[i]) * mpre[i + 1]) |
|
|
|
return ap |
|
|
|
|
|
def augment_hsv(img, hgain=0.5, sgain=0.5, vgain=0.5): |
|
r = (np.random.uniform(-1, 1, 3) * |
|
[hgain, sgain, vgain] + 1) |
|
hue, sat, val = cv2.split(cv2.cvtColor(img, cv2.COLOR_BGR2HSV)) |
|
dtype = img.dtype |
|
|
|
x = np.arange(0, 256, dtype=np.int16) |
|
lut_hue = ((x * r[0]) % 180).astype(dtype) |
|
lut_sat = np.clip(x * r[1], 0, 255).astype(dtype) |
|
lut_val = np.clip(x * r[2], 0, 255).astype(dtype) |
|
|
|
img_hsv = cv2.merge((cv2.LUT(hue, lut_hue), |
|
cv2.LUT(sat, lut_sat), |
|
cv2.LUT(val, lut_val))).astype(dtype) |
|
cv2.cvtColor(img_hsv, cv2.COLOR_HSV2BGR, dst=img) |
|
|
|
|
|
|
|
class LoadImagesAndLabels(Dataset): |
|
def __init__( |
|
self, |
|
path, |
|
img_size=416, |
|
batch_size=16, |
|
augment=False, |
|
hyp=None, |
|
rect=False, |
|
image_weights=False, |
|
cache_images=False, |
|
single_cls=False, |
|
pad=0.0): |
|
try: |
|
path = str(Path(path)) |
|
parent = str(Path(path).parent) + os.sep |
|
if os.path.isfile(path): |
|
with open(path, 'r') as f: |
|
f = f.read().splitlines() |
|
|
|
f = [ |
|
x.replace( |
|
'./', |
|
parent) if x.startswith('./') else x for x in f] |
|
elif os.path.isdir(path): |
|
f = glob.iglob(path + os.sep + '*.*') |
|
else: |
|
raise Exception('%s does not exist' % path) |
|
self.img_files = [x.replace( |
|
'/', os.sep) for x in f if |
|
os.path.splitext(x)[-1].lower() in img_formats] |
|
|
|
except BaseException: |
|
raise Exception( |
|
'Error loading data from %s. See %s' % |
|
(path, help_url)) |
|
|
|
n = len(self.img_files) |
|
assert n > 0, 'No images found in %s. See %s' % (path, help_url) |
|
bi = np.floor(np.arange(n) / batch_size).astype(int) |
|
nb = bi[-1] + 1 |
|
|
|
self.n = n |
|
self.batch = bi |
|
self.img_size = img_size |
|
self.augment = augment |
|
self.hyp = hyp |
|
self.image_weights = image_weights |
|
self.rect = False if image_weights else rect |
|
|
|
self.mosaic = self.augment and not self.rect |
|
|
|
|
|
self.label_files = [x.replace('images', 'labels').replace( |
|
os.path.splitext(x)[-1], '.txt') for x in self.img_files] |
|
|
|
|
|
sp = path.replace('.txt', '') + '.shapes' |
|
try: |
|
with open(sp, 'r') as f: |
|
s = [x.split() for x in f.read().splitlines()] |
|
assert len(s) == n, 'Shapefile out of sync' |
|
except BaseException: |
|
s = [exif_size(Image.open(f)) for f in tqdm( |
|
self.img_files, |
|
desc='Reading image shapes')] |
|
np.savetxt(sp, s, fmt='%g') |
|
|
|
self.shapes = np.array(s, dtype=np.float64) |
|
|
|
|
|
|
|
if self.rect: |
|
|
|
s = self.shapes |
|
ar = s[:, 1] / s[:, 0] |
|
irect = ar.argsort() |
|
self.img_files = [self.img_files[i] for i in irect] |
|
self.label_files = [self.label_files[i] for i in irect] |
|
self.shapes = s[irect] |
|
ar = ar[irect] |
|
|
|
|
|
shapes = [[1, 1]] * nb |
|
for i in range(nb): |
|
ari = ar[bi == i] |
|
mini, maxi = ari.min(), ari.max() |
|
if maxi < 1: |
|
shapes[i] = [maxi, 1] |
|
elif mini > 1: |
|
shapes[i] = [1, 1 / mini] |
|
|
|
self.batch_shapes = np.ceil( |
|
np.array(shapes) * img_size / 32. + pad).astype(int) * 32 |
|
|
|
|
|
self.imgs = [None] * n |
|
self.labels = [np.zeros((0, 5), dtype=np.float32)] * n |
|
create_datasubset, extract_bounding_boxes, labels_loaded = \ |
|
False, False, False |
|
|
|
nm, nf, ne, ns, nd = 0, 0, 0, 0, 0 |
|
|
|
np_labels_path = str(Path(self.label_files[0]).parent) + '.npy' |
|
if os.path.isfile(np_labels_path): |
|
s = np_labels_path |
|
|
|
print(np_labels_path) |
|
|
|
x = np.load(np_labels_path, allow_pickle=True) |
|
if len(x) == n: |
|
self.labels = x |
|
labels_loaded = True |
|
else: |
|
s = path.replace('images', 'labels') |
|
|
|
pbar = tqdm(self.label_files) |
|
for i, file in enumerate(pbar): |
|
if labels_loaded: |
|
l = self.labels[i] |
|
|
|
else: |
|
try: |
|
with open(file, 'r') as f: |
|
l = np.array( |
|
[x.split() for x in f.read().splitlines()], |
|
dtype=np.float32) |
|
except BaseException: |
|
|
|
|
|
nm += 1 |
|
continue |
|
|
|
if l.shape[0]: |
|
assert l.shape[1] == 5, '> 5 label columns: %s' % file |
|
assert (l >= 0).all(), 'negative labels: %s' % file |
|
assert (l[:, 1:] <= 1).all( |
|
), 'non-normalized or out of bounds coordinate labels: %s' % file |
|
if np.unique( |
|
l, axis=0).shape[0] < l.shape[0]: |
|
|
|
|
|
nd += 1 |
|
if single_cls: |
|
l[:, 0] = 0 |
|
self.labels[i] = l |
|
nf += 1 |
|
|
|
|
|
if create_datasubset and ns < 1E4: |
|
if ns == 0: |
|
create_folder(path='./datasubset') |
|
os.makedirs('./datasubset/images') |
|
exclude_classes = 43 |
|
if exclude_classes not in l[:, 0]: |
|
ns += 1 |
|
|
|
|
|
with open('./datasubset/images.txt', 'a') as f: |
|
f.write(self.img_files[i] + '\n') |
|
|
|
|
|
if extract_bounding_boxes: |
|
p = Path(self.img_files[i]) |
|
img = cv2.imread(str(p)) |
|
h, w = img.shape[:2] |
|
for j, x in enumerate(l): |
|
f = '%s%sclassifier%s%g_%g_%s' % ( |
|
p.parent.parent, os.sep, os.sep, x[0], j, p.name) |
|
if not os.path.exists(Path(f).parent): |
|
|
|
os.makedirs(Path(f).parent) |
|
|
|
b = x[1:] * [w, h, w, h] |
|
b[2:] = b[2:].max() |
|
b[2:] = b[2:] * 1.3 + 30 |
|
b = xywh2xyxy(b.reshape(-1, 4)).ravel().astype(int) |
|
|
|
|
|
b[[0, 2]] = np.clip(b[[0, 2]], 0, w) |
|
b[[1, 3]] = np.clip(b[[1, 3]], 0, h) |
|
assert cv2.imwrite( |
|
f, img[b[1]:b[3], b[0]:b[2]]), \ |
|
'Failure extracting classifier boxes' |
|
else: |
|
|
|
|
|
ne += 1 |
|
|
|
|
|
|
|
pbar.desc = 'Caching labels %s (%g found, %g missing, %g empty,\ |
|
%g duplicate, for %g images)' % ( |
|
s, nf, nm, ne, nd, n) |
|
assert nf > 0 or n == 20288, 'No labels found in %s. See %s' % ( |
|
os.path.dirname(file) + os.sep, help_url) |
|
if not labels_loaded and n > 1000: |
|
print( |
|
'Saving labels to %s for faster future loading' % |
|
np_labels_path) |
|
|
|
|
|
|
|
|
|
if cache_images: |
|
gb = 0 |
|
pbar = tqdm(range(len(self.img_files)), desc='Caching images') |
|
self.img_hw0, self.img_hw = [None] * n, [None] * n |
|
for i in pbar: |
|
self.imgs[i], self.img_hw0[i], self.img_hw[i] = load_image( |
|
self, i) |
|
gb += self.imgs[i].nbytes |
|
pbar.desc = 'Caching images (%.1fGB)' % (gb / 1E9) |
|
|
|
def __len__(self): |
|
return len(self.img_files) |
|
|
|
def __getitem__(self, index): |
|
if self.image_weights: |
|
index = self.indices[index] |
|
|
|
hyp = self.hyp |
|
if self.mosaic: |
|
|
|
img, labels = load_mosaic(self, index) |
|
shapes = None |
|
|
|
else: |
|
|
|
img, (h0, w0), (h, w) = load_image(self, index) |
|
|
|
|
|
shape = self.batch_shapes[self.batch[index]] if self.rect else self.img_size |
|
img, ratio, pad = letterbox(img, shape, auto=False, scaleup=self.augment) |
|
shapes = (h0, w0), ((h / h0, w / w0), pad) |
|
|
|
|
|
labels = [] |
|
x = self.labels[index] |
|
if x.size > 0: |
|
|
|
labels = x.copy() |
|
labels[:, 1] = ratio[0] * w * (x[:, 1] - x[:, 3] / 2) + pad[0] |
|
labels[:, 2] = ratio[1] * h * (x[:, 2] - x[:, 4] / 2) + pad[1] |
|
labels[:, 3] = ratio[0] * w * (x[:, 1] + x[:, 3] / 2) + pad[0] |
|
labels[:, 4] = ratio[1] * h * (x[:, 2] + x[:, 4] / 2) + pad[1] |
|
|
|
if self.augment: |
|
|
|
if not self.mosaic: |
|
img, labels = random_affine(img, labels, |
|
degrees=hyp['degrees'], |
|
translate=hyp['translate'], |
|
scale=hyp['scale'], |
|
shear=hyp['shear']) |
|
|
|
|
|
augment_hsv(img, hgain=hyp['hsv_h'], sgain=hyp['hsv_s'], vgain=hyp['hsv_v']) |
|
|
|
|
|
|
|
|
|
|
|
nL = len(labels) |
|
if nL: |
|
|
|
labels[:, 1:5] = xyxy2xywh(labels[:, 1:5]) |
|
|
|
|
|
labels[:, [2, 4]] /= img.shape[0] |
|
labels[:, [1, 3]] /= img.shape[1] |
|
|
|
if self.augment: |
|
|
|
lr_flip = True |
|
if lr_flip and random.random() < 0.5: |
|
img = np.fliplr(img) |
|
if nL: |
|
labels[:, 1] = 1 - labels[:, 1] |
|
|
|
|
|
ud_flip = False |
|
if ud_flip and random.random() < 0.5: |
|
img = np.flipud(img) |
|
if nL: |
|
labels[:, 2] = 1 - labels[:, 2] |
|
|
|
labels_out = torch.zeros((nL, 6)) |
|
if nL: |
|
labels_out[:, 1:] = torch.from_numpy(labels) |
|
|
|
|
|
img = img[:, :, ::-1].transpose(2, 0, 1) |
|
img = np.ascontiguousarray(img) |
|
|
|
return torch.from_numpy(img), labels_out, self.img_files[index], shapes |
|
|
|
@staticmethod |
|
def collate_fn(batch): |
|
img, label, path, shapes = zip(*batch) |
|
for i, l in enumerate(label): |
|
l[:, 0] = i |
|
return torch.stack(img, 0), torch.cat(label, 0), path, shapes |
|
|
|
|
|
def parse_data_cfg(path): |
|
|
|
if not os.path.exists(path) and os.path.exists( |
|
'data' + os.sep + path): |
|
path = 'data' + os.sep + path |
|
|
|
with open(path, 'r') as f: |
|
lines = f.readlines() |
|
|
|
options = dict() |
|
for line in lines: |
|
line = line.strip() |
|
if line == '' or line.startswith('#'): |
|
continue |
|
key, val = line.split('=') |
|
options[key.strip()] = val.strip() |
|
|
|
return options |
|
|
|
|
|
def create_grids(ng=(13, 13), device='cpu'): |
|
nx, ny = ng |
|
ng = torch.tensor(ng, dtype=torch.float) |
|
|
|
|
|
yv, xv = torch.meshgrid( |
|
[torch.arange(ny, device=device), torch.arange(nx, device=device)]) |
|
grid = torch.stack((xv, yv), 2).view((1, 1, ny, nx, 2)).float() |
|
|
|
return grid |
|
|
|
|
|
def post_process(x): |
|
stride = [32, 16, 8] |
|
anchors = [[10, 13, 16, 30, 33, 23], |
|
[30, 61, 62, 45, 59, 119], |
|
[116, 90, 156, 198, 373, 326]] |
|
temp = [13, 26, 52] |
|
|
|
res = [] |
|
for i in range(3): |
|
out = torch.from_numpy(x[i]) if not torch.is_tensor(x[i]) else x[i] |
|
|
|
bs, _, ny, nx = out.shape |
|
|
|
anchor = torch.Tensor(anchors[2 - i]).reshape(3, 2) |
|
anchor_vec = anchor / stride[i] |
|
anchor_wh = anchor_vec.view(1, 3, 1, 1, 2) |
|
|
|
grid = create_grids((nx, ny)) |
|
|
|
|
|
|
|
out = out.view( |
|
bs, 3, 85, temp[i], temp[i]).permute( |
|
0, 1, 3, 4, 2).contiguous() |
|
|
|
io = out.clone() |
|
|
|
io[..., :2] = torch.sigmoid(io[..., :2]) + grid |
|
io[..., 2:4] = torch.exp(io[..., 2:4]) * anchor_wh |
|
io[..., :4] *= stride[i] |
|
torch.sigmoid_(io[..., 4:]) |
|
|
|
res.append(io.view(bs, -1, 85)) |
|
return torch.cat(res, 1), x |
|
|
|
|
|
def test(data, |
|
batch_size=32, |
|
imgsz=416, |
|
conf_thres=0.001, |
|
iou_thres=0.6, |
|
save_json=False, |
|
single_cls=False, |
|
augment=False, |
|
model=None, |
|
dataloader=None, |
|
multi_label=True, |
|
names='data/coco.names', |
|
onnx_runtime=True, |
|
onnx_weights="yolov3-8", |
|
ipu=False, |
|
provider_config='vaip_config.json'): |
|
""" |
|
COCO average precision (AP) Evaluation. Iterate inference on the test dataset |
|
and the results are evaluated by COCO API. |
|
""" |
|
|
|
device = torch.device('cpu') |
|
verbose = False |
|
if isinstance(onnx_weights, list): |
|
onnx_weights = onnx_weights[0] |
|
|
|
if ipu: |
|
providers = ["VitisAIExecutionProvider"] |
|
provider_options = [{"config_file": provider_config}] |
|
else: |
|
providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] |
|
provider_options = None |
|
|
|
onnx_model = onnxruntime.InferenceSession( |
|
onnx_weights, |
|
providers=providers, |
|
provider_options=provider_options) |
|
|
|
|
|
data = parse_data_cfg(data) |
|
nc = 1 if single_cls else int(data['classes']) |
|
path = data['valid'] |
|
names = load_classes(data['names']) |
|
iouv = torch.linspace(0.5, 0.95, 10).to( |
|
device) |
|
iouv = iouv[0].view(1) |
|
niou = iouv.numel() |
|
|
|
|
|
if dataloader is None: |
|
dataset = LoadImagesAndLabels( |
|
path, |
|
imgsz, |
|
batch_size, |
|
rect=False, |
|
single_cls=opt.single_cls, |
|
pad=0.5) |
|
batch_size = min(batch_size, len(dataset)) |
|
dataloader = DataLoader(dataset, |
|
batch_size=batch_size, |
|
num_workers=min([os.cpu_count(), |
|
batch_size if |
|
batch_size > 1 else 0, |
|
8]), |
|
pin_memory=True, |
|
collate_fn=dataset.collate_fn) |
|
|
|
seen = 0 |
|
|
|
coco91class = coco80_to_coco91_class() |
|
s = ('%20s' + '%10s' * 6) % ('Class', 'Images', 'Targets', 'P', 'R', |
|
'mAP@0.5', 'F1') |
|
p, r, f1, mp, mr, map, mf1, t0, t1 = 0., 0., 0., 0., 0., 0., 0., 0., 0. |
|
loss = torch.zeros(3, device=device) |
|
jdict, stats, ap, ap_class = [], [], [], [] |
|
|
|
for batch_i, (imgs, targets, paths, shapes) in enumerate( |
|
tqdm(dataloader, desc=s)): |
|
|
|
imgs = imgs.to(device).float() / 255.0 |
|
targets = targets.to(device) |
|
nb, _, height, width = imgs.shape |
|
|
|
whwh = torch.Tensor([width, height, width, height]).to(device) |
|
|
|
if onnx_runtime: |
|
outputs = onnx_model.run( |
|
None, {onnx_model.get_inputs()[0].name: imgs.cpu().numpy()}) |
|
outputs = [torch.tensor(item).to(device) for item in outputs] |
|
inf_out, train_out = post_process(outputs) |
|
|
|
else: |
|
|
|
|
|
with torch.no_grad(): |
|
|
|
t = time_synchronized() |
|
|
|
|
|
inf_out, train_out = model(imgs, augment=augment) |
|
t0 += time_synchronized() - t |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
t = time_synchronized() |
|
output = non_max_suppression( |
|
inf_out, |
|
conf_thres=conf_thres, |
|
iou_thres=iou_thres, |
|
multi_label=multi_label) |
|
t1 += time_synchronized() - t |
|
|
|
|
|
for si, pred in enumerate(output): |
|
labels = targets[targets[:, 0] == si, 1:] |
|
nl = len(labels) |
|
tcls = labels[:, 0].tolist() if nl else [] |
|
seen += 1 |
|
|
|
if pred is None: |
|
if nl: |
|
stats.append( |
|
(torch.zeros( |
|
0, |
|
niou, |
|
dtype=torch.bool), |
|
torch.Tensor(), |
|
torch.Tensor(), |
|
tcls)) |
|
continue |
|
|
|
|
|
|
|
|
|
|
|
|
|
clip_coords(pred, (height, width)) |
|
|
|
|
|
if save_json: |
|
image_id = int(Path(paths[si]).stem.split('_')[-1]) |
|
box = pred[:, :4].clone() |
|
scale_coords(imgs[si].shape[1:], box, shapes[si] |
|
[0], shapes[si][1]) |
|
box = xyxy2xywh(box) |
|
box[:, :2] -= box[:, 2:] / 2 |
|
for p, b in zip(pred.tolist(), box.tolist()): |
|
jdict.append({'image_id': image_id, |
|
'category_id': coco91class[int(p[5])], |
|
'bbox': [round(x, 3) for x in b], |
|
'score': round(p[4], 5)}) |
|
|
|
|
|
correct = torch.zeros( |
|
pred.shape[0], |
|
niou, |
|
dtype=torch.bool, |
|
device=device) |
|
if nl: |
|
detected = [] |
|
tcls_tensor = labels[:, 0] |
|
|
|
|
|
tbox = xywh2xyxy(labels[:, 1:5]) * whwh |
|
|
|
|
|
for cls in torch.unique(tcls_tensor): |
|
ti = (cls == tcls_tensor).nonzero( |
|
).view(-1) |
|
pi = (cls == pred[:, 5]).nonzero( |
|
).view(-1) |
|
|
|
|
|
if pi.shape[0]: |
|
|
|
ious, i = box_iou(pred[pi, :4], tbox[ti].cpu()).max( |
|
1) |
|
|
|
|
|
for j in (ious > iouv[0].cpu()).nonzero(): |
|
d = ti[i[j]] |
|
if d not in detected: |
|
detected.append(d) |
|
|
|
correct[pi[j]] = ious[j] > iouv.cpu() |
|
if len( |
|
detected) == nl: |
|
|
|
break |
|
|
|
|
|
stats.append( |
|
(correct.cpu(), pred[:, 4].cpu(), pred[:, 5].cpu(), tcls)) |
|
|
|
|
|
if batch_i < 1: |
|
f = 'test_batch%g_gt.jpg' % batch_i |
|
plot_images(imgs, targets, paths=paths, names=names, |
|
fname=f) |
|
f = 'test_batch%g_pred.jpg' % batch_i |
|
plot_images(imgs, output_to_target(output, width, height), |
|
paths=paths, names=names, fname=f) |
|
|
|
|
|
|
|
|
|
stats = [np.concatenate(x, 0) for x in zip(*stats)] |
|
if len(stats): |
|
p, r, ap, f1, ap_class = ap_per_class(*stats) |
|
if niou > 1: |
|
p, r, ap, f1 = p[:, 0], r[:, 0], ap.mean( |
|
1), ap[:, 0] |
|
mp, mr, map, mf1 = p.mean(), r.mean(), ap.mean(), f1.mean() |
|
nt = np.bincount(stats[3].astype(np.int64), |
|
minlength=nc) |
|
else: |
|
nt = torch.zeros(1) |
|
|
|
|
|
pf = '%20s' + '%10.3g' * 6 |
|
print(pf % ('all', seen, nt.sum(), mp, mr, map, mf1)) |
|
|
|
|
|
if verbose and nc > 1 and len(stats): |
|
for i, c in enumerate(ap_class): |
|
print(pf % (names[c], seen, nt[c], p[i], r[i], ap[i], f1[i])) |
|
|
|
|
|
if verbose or save_json: |
|
t = tuple(x / seen * 1E3 for x in (t0, t1, t0 + t1)) + \ |
|
(imgsz, imgsz, batch_size) |
|
print( |
|
'Speed: %.1f/%.1f/%.1f ms \ |
|
inference/NMS/total per %gx%g image at batch-size %g' % t) |
|
|
|
|
|
if save_json and map and len(jdict): |
|
print('\nCOCO mAP with pycocotools...') |
|
imgIds = [int(Path(x).stem.split('_')[-1]) |
|
for x in dataloader.dataset.img_files] |
|
with open('results.json', 'w') as file: |
|
json.dump(jdict, file) |
|
|
|
try: |
|
from pycocotools.coco import COCO |
|
from pycocotools.cocoeval import COCOeval |
|
|
|
|
|
|
|
cocoGt = COCO( |
|
glob.glob('coco/annotations/instances_val*.json')[0]) |
|
cocoDt = cocoGt.loadRes('results.json') |
|
cocoEval = COCOeval(cocoGt, cocoDt, 'bbox') |
|
|
|
cocoEval.params.imgIds = imgIds |
|
cocoEval.evaluate() |
|
cocoEval.accumulate() |
|
cocoEval.summarize() |
|
|
|
|
|
except BaseException: |
|
print( |
|
'WARNING: pycocotools must be installed with \ |
|
numpy==1.17 to run correctly. ' |
|
'See https://github.com/cocodataset/cocoapi/issues/356') |
|
|
|
|
|
maps = np.zeros(nc) + map |
|
for i, c in enumerate(ap_class): |
|
maps[c] = ap[i] |
|
return (mp, mr, map, mf1, *(loss.cpu() / len(dataloader)).tolist()), maps |
|
|
|
|
|
if __name__ == '__main__': |
|
parser = argparse.ArgumentParser(prog='Test onnx model performance on COCO dataset') |
|
parser.add_argument( |
|
'--data', |
|
type=str, |
|
default='coco2017.data', |
|
help='Path of *.data') |
|
parser.add_argument( |
|
'--batch-size', |
|
type=int, |
|
default=1, |
|
help='Size of each image batch') |
|
parser.add_argument( |
|
'--img-size', |
|
type=int, |
|
default=416, |
|
help='Inference size (pixels)') |
|
parser.add_argument( |
|
'--conf-thres', |
|
type=float, |
|
default=0.001, |
|
help='Object confidence threshold') |
|
parser.add_argument( |
|
'--iou-thres', |
|
type=float, |
|
default=0.5, |
|
help='IOU threshold for NMS') |
|
parser.add_argument( |
|
'--save-json', |
|
action='store_true', |
|
help='Save a COCOapi-compatible JSON results file') |
|
parser.add_argument( |
|
'--device', |
|
default='', |
|
help='Device id (i.e. 0 or 0,1) or cpu') |
|
parser.add_argument( |
|
'--augment', |
|
action='store_true', |
|
help='Augmented inference') |
|
parser.add_argument('--sync_bn', action='store_true') |
|
parser.add_argument('--print_model', action='store_true') |
|
parser.add_argument('--test_rect', action='store_true') |
|
|
|
parser.add_argument( |
|
'--onnx_runtime', |
|
action='store_true', |
|
help='Use onnx runtime') |
|
parser.add_argument( |
|
'--onnx_weights', |
|
default='yolov3-8.onnx', |
|
nargs='+', |
|
type=str, |
|
help='Path of onnx weights') |
|
parser.add_argument( |
|
'--single-cls', |
|
action='store_true', |
|
help='Run as single-class dataset') |
|
parser.add_argument( |
|
"--ipu", |
|
action="store_true", |
|
help="Use IPU for inference") |
|
parser.add_argument( |
|
"--provider_config", |
|
type=str, |
|
default="vaip_config.json", |
|
help="Path of the config file for seting provider_options") |
|
|
|
opt = parser.parse_args() |
|
opt.save_json = opt.save_json or any( |
|
[x in opt.data for x in ['coco.data', |
|
'coco2014.data', 'coco2017.data']]) |
|
opt.data = check_file(opt.data) |
|
print(opt) |
|
|
|
help_url = 'https://github.com/ultralytics/yolov3/wiki/Train-Custom-Data' |
|
img_formats = ['.bmp', '.jpg', '.jpeg', '.png', '.tif', '.tiff', '.dng'] |
|
vid_formats = ['.mov', '.avi', '.mp4', '.mpg', '.mpeg', '.m4v', '.wmv', |
|
'.mkv'] |
|
|
|
test(opt.data, |
|
opt.batch_size, |
|
opt.img_size, |
|
opt.conf_thres, |
|
opt.iou_thres, |
|
opt.save_json, |
|
opt.single_cls, |
|
opt.augment, |
|
names='data/coco.names', |
|
onnx_weights=opt.onnx_weights, |
|
ipu=opt.ipu, |
|
provider_config=opt.provider_config |
|
) |
|
|