ultralytics 8.0.238__py3-none-any.whl → 8.0.239__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of ultralytics might be problematic. Click here for more details.
- ultralytics/__init__.py +2 -2
- ultralytics/cfg/__init__.py +241 -138
- ultralytics/data/__init__.py +9 -2
- ultralytics/data/annotator.py +4 -4
- ultralytics/data/augment.py +186 -169
- ultralytics/data/base.py +54 -48
- ultralytics/data/build.py +34 -23
- ultralytics/data/converter.py +242 -70
- ultralytics/data/dataset.py +117 -95
- ultralytics/data/explorer/__init__.py +3 -1
- ultralytics/data/explorer/explorer.py +120 -100
- ultralytics/data/explorer/gui/__init__.py +1 -0
- ultralytics/data/explorer/gui/dash.py +123 -89
- ultralytics/data/explorer/utils.py +37 -39
- ultralytics/data/loaders.py +75 -62
- ultralytics/data/split_dota.py +44 -36
- ultralytics/data/utils.py +160 -142
- ultralytics/engine/exporter.py +348 -292
- ultralytics/engine/model.py +102 -66
- ultralytics/engine/predictor.py +74 -55
- ultralytics/engine/results.py +61 -41
- ultralytics/engine/trainer.py +192 -144
- ultralytics/engine/tuner.py +66 -59
- ultralytics/engine/validator.py +31 -26
- ultralytics/hub/__init__.py +54 -31
- ultralytics/hub/auth.py +28 -25
- ultralytics/hub/session.py +282 -133
- ultralytics/hub/utils.py +64 -42
- ultralytics/models/__init__.py +1 -1
- ultralytics/models/fastsam/__init__.py +1 -1
- ultralytics/models/fastsam/model.py +6 -6
- ultralytics/models/fastsam/predict.py +3 -2
- ultralytics/models/fastsam/prompt.py +55 -48
- ultralytics/models/fastsam/val.py +1 -1
- ultralytics/models/nas/__init__.py +1 -1
- ultralytics/models/nas/model.py +9 -8
- ultralytics/models/nas/predict.py +8 -6
- ultralytics/models/nas/val.py +11 -9
- ultralytics/models/rtdetr/__init__.py +1 -1
- ultralytics/models/rtdetr/model.py +11 -9
- ultralytics/models/rtdetr/train.py +18 -16
- ultralytics/models/rtdetr/val.py +25 -19
- ultralytics/models/sam/__init__.py +1 -1
- ultralytics/models/sam/amg.py +13 -14
- ultralytics/models/sam/build.py +44 -42
- ultralytics/models/sam/model.py +6 -6
- ultralytics/models/sam/modules/decoders.py +6 -4
- ultralytics/models/sam/modules/encoders.py +37 -35
- ultralytics/models/sam/modules/sam.py +5 -4
- ultralytics/models/sam/modules/tiny_encoder.py +95 -73
- ultralytics/models/sam/modules/transformer.py +3 -2
- ultralytics/models/sam/predict.py +39 -27
- ultralytics/models/utils/loss.py +99 -95
- ultralytics/models/utils/ops.py +34 -31
- ultralytics/models/yolo/__init__.py +1 -1
- ultralytics/models/yolo/classify/__init__.py +1 -1
- ultralytics/models/yolo/classify/predict.py +8 -6
- ultralytics/models/yolo/classify/train.py +37 -31
- ultralytics/models/yolo/classify/val.py +26 -24
- ultralytics/models/yolo/detect/__init__.py +1 -1
- ultralytics/models/yolo/detect/predict.py +8 -6
- ultralytics/models/yolo/detect/train.py +47 -37
- ultralytics/models/yolo/detect/val.py +100 -82
- ultralytics/models/yolo/model.py +31 -25
- ultralytics/models/yolo/obb/__init__.py +1 -1
- ultralytics/models/yolo/obb/predict.py +13 -11
- ultralytics/models/yolo/obb/train.py +3 -3
- ultralytics/models/yolo/obb/val.py +70 -59
- ultralytics/models/yolo/pose/__init__.py +1 -1
- ultralytics/models/yolo/pose/predict.py +17 -12
- ultralytics/models/yolo/pose/train.py +28 -25
- ultralytics/models/yolo/pose/val.py +91 -64
- ultralytics/models/yolo/segment/__init__.py +1 -1
- ultralytics/models/yolo/segment/predict.py +10 -8
- ultralytics/models/yolo/segment/train.py +16 -15
- ultralytics/models/yolo/segment/val.py +90 -68
- ultralytics/nn/__init__.py +26 -6
- ultralytics/nn/autobackend.py +144 -112
- ultralytics/nn/modules/__init__.py +96 -13
- ultralytics/nn/modules/block.py +28 -7
- ultralytics/nn/modules/conv.py +41 -23
- ultralytics/nn/modules/head.py +60 -52
- ultralytics/nn/modules/transformer.py +49 -32
- ultralytics/nn/modules/utils.py +20 -15
- ultralytics/nn/tasks.py +215 -141
- ultralytics/solutions/ai_gym.py +59 -47
- ultralytics/solutions/distance_calculation.py +17 -14
- ultralytics/solutions/heatmap.py +57 -55
- ultralytics/solutions/object_counter.py +46 -39
- ultralytics/solutions/speed_estimation.py +13 -16
- ultralytics/trackers/__init__.py +1 -1
- ultralytics/trackers/basetrack.py +1 -0
- ultralytics/trackers/bot_sort.py +2 -1
- ultralytics/trackers/byte_tracker.py +10 -7
- ultralytics/trackers/track.py +7 -7
- ultralytics/trackers/utils/gmc.py +25 -25
- ultralytics/trackers/utils/kalman_filter.py +85 -42
- ultralytics/trackers/utils/matching.py +8 -7
- ultralytics/utils/__init__.py +173 -152
- ultralytics/utils/autobatch.py +10 -10
- ultralytics/utils/benchmarks.py +76 -86
- ultralytics/utils/callbacks/__init__.py +1 -1
- ultralytics/utils/callbacks/base.py +29 -29
- ultralytics/utils/callbacks/clearml.py +51 -43
- ultralytics/utils/callbacks/comet.py +81 -66
- ultralytics/utils/callbacks/dvc.py +33 -26
- ultralytics/utils/callbacks/hub.py +44 -26
- ultralytics/utils/callbacks/mlflow.py +31 -24
- ultralytics/utils/callbacks/neptune.py +35 -25
- ultralytics/utils/callbacks/raytune.py +9 -4
- ultralytics/utils/callbacks/tensorboard.py +16 -11
- ultralytics/utils/callbacks/wb.py +39 -33
- ultralytics/utils/checks.py +189 -141
- ultralytics/utils/dist.py +15 -12
- ultralytics/utils/downloads.py +112 -96
- ultralytics/utils/errors.py +1 -1
- ultralytics/utils/files.py +11 -11
- ultralytics/utils/instance.py +22 -22
- ultralytics/utils/loss.py +117 -67
- ultralytics/utils/metrics.py +224 -158
- ultralytics/utils/ops.py +38 -28
- ultralytics/utils/patches.py +3 -3
- ultralytics/utils/plotting.py +217 -120
- ultralytics/utils/tal.py +19 -13
- ultralytics/utils/torch_utils.py +138 -109
- ultralytics/utils/triton.py +12 -10
- ultralytics/utils/tuner.py +49 -47
- {ultralytics-8.0.238.dist-info → ultralytics-8.0.239.dist-info}/METADATA +2 -1
- ultralytics-8.0.239.dist-info/RECORD +188 -0
- ultralytics-8.0.238.dist-info/RECORD +0 -188
- {ultralytics-8.0.238.dist-info → ultralytics-8.0.239.dist-info}/LICENSE +0 -0
- {ultralytics-8.0.238.dist-info → ultralytics-8.0.239.dist-info}/WHEEL +0 -0
- {ultralytics-8.0.238.dist-info → ultralytics-8.0.239.dist-info}/entry_points.txt +0 -0
- {ultralytics-8.0.238.dist-info → ultralytics-8.0.239.dist-info}/top_level.txt +0 -0
|
@@ -31,43 +31,42 @@ class ClassificationValidator(BaseValidator):
|
|
|
31
31
|
super().__init__(dataloader, save_dir, pbar, args, _callbacks)
|
|
32
32
|
self.targets = None
|
|
33
33
|
self.pred = None
|
|
34
|
-
self.args.task =
|
|
34
|
+
self.args.task = "classify"
|
|
35
35
|
self.metrics = ClassifyMetrics()
|
|
36
36
|
|
|
37
37
|
def get_desc(self):
|
|
38
38
|
"""Returns a formatted string summarizing classification metrics."""
|
|
39
|
-
return (
|
|
39
|
+
return ("%22s" + "%11s" * 2) % ("classes", "top1_acc", "top5_acc")
|
|
40
40
|
|
|
41
41
|
def init_metrics(self, model):
|
|
42
42
|
"""Initialize confusion matrix, class names, and top-1 and top-5 accuracy."""
|
|
43
43
|
self.names = model.names
|
|
44
44
|
self.nc = len(model.names)
|
|
45
|
-
self.confusion_matrix = ConfusionMatrix(nc=self.nc, conf=self.args.conf, task=
|
|
45
|
+
self.confusion_matrix = ConfusionMatrix(nc=self.nc, conf=self.args.conf, task="classify")
|
|
46
46
|
self.pred = []
|
|
47
47
|
self.targets = []
|
|
48
48
|
|
|
49
49
|
def preprocess(self, batch):
|
|
50
50
|
"""Preprocesses input batch and returns it."""
|
|
51
|
-
batch[
|
|
52
|
-
batch[
|
|
53
|
-
batch[
|
|
51
|
+
batch["img"] = batch["img"].to(self.device, non_blocking=True)
|
|
52
|
+
batch["img"] = batch["img"].half() if self.args.half else batch["img"].float()
|
|
53
|
+
batch["cls"] = batch["cls"].to(self.device)
|
|
54
54
|
return batch
|
|
55
55
|
|
|
56
56
|
def update_metrics(self, preds, batch):
|
|
57
57
|
"""Updates running metrics with model predictions and batch targets."""
|
|
58
58
|
n5 = min(len(self.names), 5)
|
|
59
59
|
self.pred.append(preds.argsort(1, descending=True)[:, :n5])
|
|
60
|
-
self.targets.append(batch[
|
|
60
|
+
self.targets.append(batch["cls"])
|
|
61
61
|
|
|
62
62
|
def finalize_metrics(self, *args, **kwargs):
|
|
63
63
|
"""Finalizes metrics of the model such as confusion_matrix and speed."""
|
|
64
64
|
self.confusion_matrix.process_cls_preds(self.pred, self.targets)
|
|
65
65
|
if self.args.plots:
|
|
66
66
|
for normalize in True, False:
|
|
67
|
-
self.confusion_matrix.plot(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
on_plot=self.on_plot)
|
|
67
|
+
self.confusion_matrix.plot(
|
|
68
|
+
save_dir=self.save_dir, names=self.names.values(), normalize=normalize, on_plot=self.on_plot
|
|
69
|
+
)
|
|
71
70
|
self.metrics.speed = self.speed
|
|
72
71
|
self.metrics.confusion_matrix = self.confusion_matrix
|
|
73
72
|
self.metrics.save_dir = self.save_dir
|
|
@@ -88,24 +87,27 @@ class ClassificationValidator(BaseValidator):
|
|
|
88
87
|
|
|
89
88
|
def print_results(self):
|
|
90
89
|
"""Prints evaluation metrics for YOLO object detection model."""
|
|
91
|
-
pf =
|
|
92
|
-
LOGGER.info(pf % (
|
|
90
|
+
pf = "%22s" + "%11.3g" * len(self.metrics.keys) # print format
|
|
91
|
+
LOGGER.info(pf % ("all", self.metrics.top1, self.metrics.top5))
|
|
93
92
|
|
|
94
93
|
def plot_val_samples(self, batch, ni):
|
|
95
94
|
"""Plot validation image samples."""
|
|
96
95
|
plot_images(
|
|
97
|
-
images=batch[
|
|
98
|
-
batch_idx=torch.arange(len(batch[
|
|
99
|
-
cls=batch[
|
|
100
|
-
fname=self.save_dir / f
|
|
96
|
+
images=batch["img"],
|
|
97
|
+
batch_idx=torch.arange(len(batch["img"])),
|
|
98
|
+
cls=batch["cls"].view(-1), # warning: use .view(), not .squeeze() for Classify models
|
|
99
|
+
fname=self.save_dir / f"val_batch{ni}_labels.jpg",
|
|
101
100
|
names=self.names,
|
|
102
|
-
on_plot=self.on_plot
|
|
101
|
+
on_plot=self.on_plot,
|
|
102
|
+
)
|
|
103
103
|
|
|
104
104
|
def plot_predictions(self, batch, preds, ni):
|
|
105
105
|
"""Plots predicted bounding boxes on input images and saves the result."""
|
|
106
|
-
plot_images(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
106
|
+
plot_images(
|
|
107
|
+
batch["img"],
|
|
108
|
+
batch_idx=torch.arange(len(batch["img"])),
|
|
109
|
+
cls=torch.argmax(preds, dim=1),
|
|
110
|
+
fname=self.save_dir / f"val_batch{ni}_pred.jpg",
|
|
111
|
+
names=self.names,
|
|
112
|
+
on_plot=self.on_plot,
|
|
113
|
+
) # pred
|
|
@@ -22,12 +22,14 @@ class DetectionPredictor(BasePredictor):
|
|
|
22
22
|
|
|
23
23
|
def postprocess(self, preds, img, orig_imgs):
|
|
24
24
|
"""Post-processes predictions and returns a list of Results objects."""
|
|
25
|
-
preds = ops.non_max_suppression(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
preds = ops.non_max_suppression(
|
|
26
|
+
preds,
|
|
27
|
+
self.args.conf,
|
|
28
|
+
self.args.iou,
|
|
29
|
+
agnostic=self.args.agnostic_nms,
|
|
30
|
+
max_det=self.args.max_det,
|
|
31
|
+
classes=self.args.classes,
|
|
32
|
+
)
|
|
31
33
|
|
|
32
34
|
if not isinstance(orig_imgs, list): # input images are a torch.Tensor, not a list
|
|
33
35
|
orig_imgs = ops.convert_torch2numpy_batch(orig_imgs)
|
|
@@ -30,7 +30,7 @@ class DetectionTrainer(BaseTrainer):
|
|
|
30
30
|
```
|
|
31
31
|
"""
|
|
32
32
|
|
|
33
|
-
def build_dataset(self, img_path, mode=
|
|
33
|
+
def build_dataset(self, img_path, mode="train", batch=None):
|
|
34
34
|
"""
|
|
35
35
|
Build YOLO Dataset.
|
|
36
36
|
|
|
@@ -40,33 +40,37 @@ class DetectionTrainer(BaseTrainer):
|
|
|
40
40
|
batch (int, optional): Size of batches, this is for `rect`. Defaults to None.
|
|
41
41
|
"""
|
|
42
42
|
gs = max(int(de_parallel(self.model).stride.max() if self.model else 0), 32)
|
|
43
|
-
return build_yolo_dataset(self.args, img_path, batch, self.data, mode=mode, rect=mode ==
|
|
43
|
+
return build_yolo_dataset(self.args, img_path, batch, self.data, mode=mode, rect=mode == "val", stride=gs)
|
|
44
44
|
|
|
45
|
-
def get_dataloader(self, dataset_path, batch_size=16, rank=0, mode=
|
|
45
|
+
def get_dataloader(self, dataset_path, batch_size=16, rank=0, mode="train"):
|
|
46
46
|
"""Construct and return dataloader."""
|
|
47
|
-
assert mode in [
|
|
47
|
+
assert mode in ["train", "val"]
|
|
48
48
|
with torch_distributed_zero_first(rank): # init dataset *.cache only once if DDP
|
|
49
49
|
dataset = self.build_dataset(dataset_path, mode, batch_size)
|
|
50
|
-
shuffle = mode ==
|
|
51
|
-
if getattr(dataset,
|
|
50
|
+
shuffle = mode == "train"
|
|
51
|
+
if getattr(dataset, "rect", False) and shuffle:
|
|
52
52
|
LOGGER.warning("WARNING ⚠️ 'rect=True' is incompatible with DataLoader shuffle, setting shuffle=False")
|
|
53
53
|
shuffle = False
|
|
54
|
-
workers = self.args.workers if mode ==
|
|
54
|
+
workers = self.args.workers if mode == "train" else self.args.workers * 2
|
|
55
55
|
return build_dataloader(dataset, batch_size, workers, shuffle, rank) # return dataloader
|
|
56
56
|
|
|
57
57
|
def preprocess_batch(self, batch):
|
|
58
58
|
"""Preprocesses a batch of images by scaling and converting to float."""
|
|
59
|
-
batch[
|
|
59
|
+
batch["img"] = batch["img"].to(self.device, non_blocking=True).float() / 255
|
|
60
60
|
if self.args.multi_scale:
|
|
61
|
-
imgs = batch[
|
|
62
|
-
sz = (
|
|
63
|
-
|
|
61
|
+
imgs = batch["img"]
|
|
62
|
+
sz = (
|
|
63
|
+
random.randrange(self.args.imgsz * 0.5, self.args.imgsz * 1.5 + self.stride)
|
|
64
|
+
// self.stride
|
|
65
|
+
* self.stride
|
|
66
|
+
) # size
|
|
64
67
|
sf = sz / max(imgs.shape[2:]) # scale factor
|
|
65
68
|
if sf != 1:
|
|
66
|
-
ns = [
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
ns = [
|
|
70
|
+
math.ceil(x * sf / self.stride) * self.stride for x in imgs.shape[2:]
|
|
71
|
+
] # new shape (stretched to gs-multiple)
|
|
72
|
+
imgs = nn.functional.interpolate(imgs, size=ns, mode="bilinear", align_corners=False)
|
|
73
|
+
batch["img"] = imgs
|
|
70
74
|
return batch
|
|
71
75
|
|
|
72
76
|
def set_model_attributes(self):
|
|
@@ -74,33 +78,32 @@ class DetectionTrainer(BaseTrainer):
|
|
|
74
78
|
# self.args.box *= 3 / nl # scale to layers
|
|
75
79
|
# self.args.cls *= self.data["nc"] / 80 * 3 / nl # scale to classes and layers
|
|
76
80
|
# self.args.cls *= (self.args.imgsz / 640) ** 2 * 3 / nl # scale to image size and layers
|
|
77
|
-
self.model.nc = self.data[
|
|
78
|
-
self.model.names = self.data[
|
|
81
|
+
self.model.nc = self.data["nc"] # attach number of classes to model
|
|
82
|
+
self.model.names = self.data["names"] # attach class names to model
|
|
79
83
|
self.model.args = self.args # attach hyperparameters to model
|
|
80
84
|
# TODO: self.model.class_weights = labels_to_class_weights(dataset.labels, nc).to(device) * nc
|
|
81
85
|
|
|
82
86
|
def get_model(self, cfg=None, weights=None, verbose=True):
|
|
83
87
|
"""Return a YOLO detection model."""
|
|
84
|
-
model = DetectionModel(cfg, nc=self.data[
|
|
88
|
+
model = DetectionModel(cfg, nc=self.data["nc"], verbose=verbose and RANK == -1)
|
|
85
89
|
if weights:
|
|
86
90
|
model.load(weights)
|
|
87
91
|
return model
|
|
88
92
|
|
|
89
93
|
def get_validator(self):
|
|
90
94
|
"""Returns a DetectionValidator for YOLO model validation."""
|
|
91
|
-
self.loss_names =
|
|
92
|
-
return yolo.detect.DetectionValidator(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
_callbacks=self.callbacks)
|
|
95
|
+
self.loss_names = "box_loss", "cls_loss", "dfl_loss"
|
|
96
|
+
return yolo.detect.DetectionValidator(
|
|
97
|
+
self.test_loader, save_dir=self.save_dir, args=copy(self.args), _callbacks=self.callbacks
|
|
98
|
+
)
|
|
96
99
|
|
|
97
|
-
def label_loss_items(self, loss_items=None, prefix=
|
|
100
|
+
def label_loss_items(self, loss_items=None, prefix="train"):
|
|
98
101
|
"""
|
|
99
102
|
Returns a loss dict with labelled training loss items tensor.
|
|
100
103
|
|
|
101
104
|
Not needed for classification but necessary for segmentation & detection
|
|
102
105
|
"""
|
|
103
|
-
keys = [f
|
|
106
|
+
keys = [f"{prefix}/{x}" for x in self.loss_names]
|
|
104
107
|
if loss_items is not None:
|
|
105
108
|
loss_items = [round(float(x), 5) for x in loss_items] # convert tensors to 5 decimal place floats
|
|
106
109
|
return dict(zip(keys, loss_items))
|
|
@@ -109,18 +112,25 @@ class DetectionTrainer(BaseTrainer):
|
|
|
109
112
|
|
|
110
113
|
def progress_string(self):
|
|
111
114
|
"""Returns a formatted string of training progress with epoch, GPU memory, loss, instances and size."""
|
|
112
|
-
return (
|
|
113
|
-
|
|
115
|
+
return ("\n" + "%11s" * (4 + len(self.loss_names))) % (
|
|
116
|
+
"Epoch",
|
|
117
|
+
"GPU_mem",
|
|
118
|
+
*self.loss_names,
|
|
119
|
+
"Instances",
|
|
120
|
+
"Size",
|
|
121
|
+
)
|
|
114
122
|
|
|
115
123
|
def plot_training_samples(self, batch, ni):
|
|
116
124
|
"""Plots training samples with their annotations."""
|
|
117
|
-
plot_images(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
125
|
+
plot_images(
|
|
126
|
+
images=batch["img"],
|
|
127
|
+
batch_idx=batch["batch_idx"],
|
|
128
|
+
cls=batch["cls"].squeeze(-1),
|
|
129
|
+
bboxes=batch["bboxes"],
|
|
130
|
+
paths=batch["im_file"],
|
|
131
|
+
fname=self.save_dir / f"train_batch{ni}.jpg",
|
|
132
|
+
on_plot=self.on_plot,
|
|
133
|
+
)
|
|
124
134
|
|
|
125
135
|
def plot_metrics(self):
|
|
126
136
|
"""Plots metrics from a CSV file."""
|
|
@@ -128,6 +138,6 @@ class DetectionTrainer(BaseTrainer):
|
|
|
128
138
|
|
|
129
139
|
def plot_training_labels(self):
|
|
130
140
|
"""Create a labeled training plot of the YOLO model."""
|
|
131
|
-
boxes = np.concatenate([lb[
|
|
132
|
-
cls = np.concatenate([lb[
|
|
133
|
-
plot_labels(boxes, cls.squeeze(), names=self.data[
|
|
141
|
+
boxes = np.concatenate([lb["bboxes"] for lb in self.train_loader.dataset.labels], 0)
|
|
142
|
+
cls = np.concatenate([lb["cls"] for lb in self.train_loader.dataset.labels], 0)
|
|
143
|
+
plot_labels(boxes, cls.squeeze(), names=self.data["names"], save_dir=self.save_dir, on_plot=self.on_plot)
|
|
@@ -34,7 +34,7 @@ class DetectionValidator(BaseValidator):
|
|
|
34
34
|
self.nt_per_class = None
|
|
35
35
|
self.is_coco = False
|
|
36
36
|
self.class_map = None
|
|
37
|
-
self.args.task =
|
|
37
|
+
self.args.task = "detect"
|
|
38
38
|
self.metrics = DetMetrics(save_dir=self.save_dir, on_plot=self.on_plot)
|
|
39
39
|
self.iouv = torch.linspace(0.5, 0.95, 10) # iou vector for mAP@0.5:0.95
|
|
40
40
|
self.niou = self.iouv.numel()
|
|
@@ -42,25 +42,30 @@ class DetectionValidator(BaseValidator):
|
|
|
42
42
|
|
|
43
43
|
def preprocess(self, batch):
|
|
44
44
|
"""Preprocesses batch of images for YOLO training."""
|
|
45
|
-
batch[
|
|
46
|
-
batch[
|
|
47
|
-
for k in [
|
|
45
|
+
batch["img"] = batch["img"].to(self.device, non_blocking=True)
|
|
46
|
+
batch["img"] = (batch["img"].half() if self.args.half else batch["img"].float()) / 255
|
|
47
|
+
for k in ["batch_idx", "cls", "bboxes"]:
|
|
48
48
|
batch[k] = batch[k].to(self.device)
|
|
49
49
|
|
|
50
50
|
if self.args.save_hybrid:
|
|
51
|
-
height, width = batch[
|
|
52
|
-
nb = len(batch[
|
|
53
|
-
bboxes = batch[
|
|
54
|
-
self.lb =
|
|
55
|
-
|
|
56
|
-
|
|
51
|
+
height, width = batch["img"].shape[2:]
|
|
52
|
+
nb = len(batch["img"])
|
|
53
|
+
bboxes = batch["bboxes"] * torch.tensor((width, height, width, height), device=self.device)
|
|
54
|
+
self.lb = (
|
|
55
|
+
[
|
|
56
|
+
torch.cat([batch["cls"][batch["batch_idx"] == i], bboxes[batch["batch_idx"] == i]], dim=-1)
|
|
57
|
+
for i in range(nb)
|
|
58
|
+
]
|
|
59
|
+
if self.args.save_hybrid
|
|
60
|
+
else []
|
|
61
|
+
) # for autolabelling
|
|
57
62
|
|
|
58
63
|
return batch
|
|
59
64
|
|
|
60
65
|
def init_metrics(self, model):
|
|
61
66
|
"""Initialize evaluation metrics for YOLO."""
|
|
62
|
-
val = self.data.get(self.args.split,
|
|
63
|
-
self.is_coco = isinstance(val, str) and
|
|
67
|
+
val = self.data.get(self.args.split, "") # validation path
|
|
68
|
+
self.is_coco = isinstance(val, str) and "coco" in val and val.endswith(f"{os.sep}val2017.txt") # is COCO
|
|
64
69
|
self.class_map = converter.coco80_to_coco91_class() if self.is_coco else list(range(1000))
|
|
65
70
|
self.args.save_json |= self.is_coco and not self.training # run on final val if training COCO
|
|
66
71
|
self.names = model.names
|
|
@@ -74,25 +79,28 @@ class DetectionValidator(BaseValidator):
|
|
|
74
79
|
|
|
75
80
|
def get_desc(self):
|
|
76
81
|
"""Return a formatted string summarizing class metrics of YOLO model."""
|
|
77
|
-
return (
|
|
82
|
+
return ("%22s" + "%11s" * 6) % ("Class", "Images", "Instances", "Box(P", "R", "mAP50", "mAP50-95)")
|
|
78
83
|
|
|
79
84
|
def postprocess(self, preds):
|
|
80
85
|
"""Apply Non-maximum suppression to prediction outputs."""
|
|
81
|
-
return ops.non_max_suppression(
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
86
|
+
return ops.non_max_suppression(
|
|
87
|
+
preds,
|
|
88
|
+
self.args.conf,
|
|
89
|
+
self.args.iou,
|
|
90
|
+
labels=self.lb,
|
|
91
|
+
multi_label=True,
|
|
92
|
+
agnostic=self.args.single_cls,
|
|
93
|
+
max_det=self.args.max_det,
|
|
94
|
+
)
|
|
88
95
|
|
|
89
96
|
def _prepare_batch(self, si, batch):
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
"""Prepares a batch of images and annotations for validation."""
|
|
98
|
+
idx = batch["batch_idx"] == si
|
|
99
|
+
cls = batch["cls"][idx].squeeze(-1)
|
|
100
|
+
bbox = batch["bboxes"][idx]
|
|
101
|
+
ori_shape = batch["ori_shape"][si]
|
|
102
|
+
imgsz = batch["img"].shape[2:]
|
|
103
|
+
ratio_pad = batch["ratio_pad"][si]
|
|
96
104
|
if len(cls):
|
|
97
105
|
bbox = ops.xywh2xyxy(bbox) * torch.tensor(imgsz, device=self.device)[[1, 0, 1, 0]] # target boxes
|
|
98
106
|
ops.scale_boxes(imgsz, bbox, ori_shape, ratio_pad=ratio_pad) # native-space labels
|
|
@@ -100,9 +108,11 @@ class DetectionValidator(BaseValidator):
|
|
|
100
108
|
return prepared_batch
|
|
101
109
|
|
|
102
110
|
def _prepare_pred(self, pred, pbatch):
|
|
111
|
+
"""Prepares a batch of images and annotations for validation."""
|
|
103
112
|
predn = pred.clone()
|
|
104
|
-
ops.scale_boxes(
|
|
105
|
-
|
|
113
|
+
ops.scale_boxes(
|
|
114
|
+
pbatch["imgsz"], predn[:, :4], pbatch["ori_shape"], ratio_pad=pbatch["ratio_pad"]
|
|
115
|
+
) # native-space pred
|
|
106
116
|
return predn
|
|
107
117
|
|
|
108
118
|
def update_metrics(self, preds, batch):
|
|
@@ -110,19 +120,21 @@ class DetectionValidator(BaseValidator):
|
|
|
110
120
|
for si, pred in enumerate(preds):
|
|
111
121
|
self.seen += 1
|
|
112
122
|
npr = len(pred)
|
|
113
|
-
stat = dict(
|
|
114
|
-
|
|
115
|
-
|
|
123
|
+
stat = dict(
|
|
124
|
+
conf=torch.zeros(0, device=self.device),
|
|
125
|
+
pred_cls=torch.zeros(0, device=self.device),
|
|
126
|
+
tp=torch.zeros(npr, self.niou, dtype=torch.bool, device=self.device),
|
|
127
|
+
)
|
|
116
128
|
pbatch = self._prepare_batch(si, batch)
|
|
117
|
-
cls, bbox = pbatch.pop(
|
|
129
|
+
cls, bbox = pbatch.pop("cls"), pbatch.pop("bbox")
|
|
118
130
|
nl = len(cls)
|
|
119
|
-
stat[
|
|
131
|
+
stat["target_cls"] = cls
|
|
120
132
|
if npr == 0:
|
|
121
133
|
if nl:
|
|
122
134
|
for k in self.stats.keys():
|
|
123
135
|
self.stats[k].append(stat[k])
|
|
124
136
|
# TODO: obb has not supported confusion_matrix yet.
|
|
125
|
-
if self.args.plots and self.args.task !=
|
|
137
|
+
if self.args.plots and self.args.task != "obb":
|
|
126
138
|
self.confusion_matrix.process_batch(detections=None, gt_bboxes=bbox, gt_cls=cls)
|
|
127
139
|
continue
|
|
128
140
|
|
|
@@ -130,24 +142,24 @@ class DetectionValidator(BaseValidator):
|
|
|
130
142
|
if self.args.single_cls:
|
|
131
143
|
pred[:, 5] = 0
|
|
132
144
|
predn = self._prepare_pred(pred, pbatch)
|
|
133
|
-
stat[
|
|
134
|
-
stat[
|
|
145
|
+
stat["conf"] = predn[:, 4]
|
|
146
|
+
stat["pred_cls"] = predn[:, 5]
|
|
135
147
|
|
|
136
148
|
# Evaluate
|
|
137
149
|
if nl:
|
|
138
|
-
stat[
|
|
150
|
+
stat["tp"] = self._process_batch(predn, bbox, cls)
|
|
139
151
|
# TODO: obb has not supported confusion_matrix yet.
|
|
140
|
-
if self.args.plots and self.args.task !=
|
|
152
|
+
if self.args.plots and self.args.task != "obb":
|
|
141
153
|
self.confusion_matrix.process_batch(predn, bbox, cls)
|
|
142
154
|
for k in self.stats.keys():
|
|
143
155
|
self.stats[k].append(stat[k])
|
|
144
156
|
|
|
145
157
|
# Save
|
|
146
158
|
if self.args.save_json:
|
|
147
|
-
self.pred_to_json(predn, batch[
|
|
159
|
+
self.pred_to_json(predn, batch["im_file"][si])
|
|
148
160
|
if self.args.save_txt:
|
|
149
|
-
file = self.save_dir /
|
|
150
|
-
self.save_one_txt(predn, self.args.save_conf, pbatch[
|
|
161
|
+
file = self.save_dir / "labels" / f'{Path(batch["im_file"][si]).stem}.txt'
|
|
162
|
+
self.save_one_txt(predn, self.args.save_conf, pbatch["ori_shape"], file)
|
|
151
163
|
|
|
152
164
|
def finalize_metrics(self, *args, **kwargs):
|
|
153
165
|
"""Set final values for metrics speed and confusion matrix."""
|
|
@@ -157,19 +169,19 @@ class DetectionValidator(BaseValidator):
|
|
|
157
169
|
def get_stats(self):
|
|
158
170
|
"""Returns metrics statistics and results dictionary."""
|
|
159
171
|
stats = {k: torch.cat(v, 0).cpu().numpy() for k, v in self.stats.items()} # to numpy
|
|
160
|
-
if len(stats) and stats[
|
|
172
|
+
if len(stats) and stats["tp"].any():
|
|
161
173
|
self.metrics.process(**stats)
|
|
162
|
-
self.nt_per_class = np.bincount(
|
|
163
|
-
|
|
174
|
+
self.nt_per_class = np.bincount(
|
|
175
|
+
stats["target_cls"].astype(int), minlength=self.nc
|
|
176
|
+
) # number of targets per class
|
|
164
177
|
return self.metrics.results_dict
|
|
165
178
|
|
|
166
179
|
def print_results(self):
|
|
167
180
|
"""Prints training/validation set metrics per class."""
|
|
168
|
-
pf =
|
|
169
|
-
LOGGER.info(pf % (
|
|
181
|
+
pf = "%22s" + "%11i" * 2 + "%11.3g" * len(self.metrics.keys) # print format
|
|
182
|
+
LOGGER.info(pf % ("all", self.seen, self.nt_per_class.sum(), *self.metrics.mean_results()))
|
|
170
183
|
if self.nt_per_class.sum() == 0:
|
|
171
|
-
LOGGER.warning(
|
|
172
|
-
f'WARNING ⚠️ no labels found in {self.args.task} set, can not compute metrics without labels')
|
|
184
|
+
LOGGER.warning(f"WARNING ⚠️ no labels found in {self.args.task} set, can not compute metrics without labels")
|
|
173
185
|
|
|
174
186
|
# Print results per class
|
|
175
187
|
if self.args.verbose and not self.training and self.nc > 1 and len(self.stats):
|
|
@@ -178,10 +190,9 @@ class DetectionValidator(BaseValidator):
|
|
|
178
190
|
|
|
179
191
|
if self.args.plots:
|
|
180
192
|
for normalize in True, False:
|
|
181
|
-
self.confusion_matrix.plot(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
on_plot=self.on_plot)
|
|
193
|
+
self.confusion_matrix.plot(
|
|
194
|
+
save_dir=self.save_dir, names=self.names.values(), normalize=normalize, on_plot=self.on_plot
|
|
195
|
+
)
|
|
185
196
|
|
|
186
197
|
def _process_batch(self, detections, gt_bboxes, gt_cls):
|
|
187
198
|
"""
|
|
@@ -199,7 +210,7 @@ class DetectionValidator(BaseValidator):
|
|
|
199
210
|
iou = box_iou(gt_bboxes, detections[:, :4])
|
|
200
211
|
return self.match_predictions(detections[:, 5], gt_cls, iou)
|
|
201
212
|
|
|
202
|
-
def build_dataset(self, img_path, mode=
|
|
213
|
+
def build_dataset(self, img_path, mode="val", batch=None):
|
|
203
214
|
"""
|
|
204
215
|
Build YOLO Dataset.
|
|
205
216
|
|
|
@@ -212,28 +223,32 @@ class DetectionValidator(BaseValidator):
|
|
|
212
223
|
|
|
213
224
|
def get_dataloader(self, dataset_path, batch_size):
|
|
214
225
|
"""Construct and return dataloader."""
|
|
215
|
-
dataset = self.build_dataset(dataset_path, batch=batch_size, mode=
|
|
226
|
+
dataset = self.build_dataset(dataset_path, batch=batch_size, mode="val")
|
|
216
227
|
return build_dataloader(dataset, batch_size, self.args.workers, shuffle=False, rank=-1) # return dataloader
|
|
217
228
|
|
|
218
229
|
def plot_val_samples(self, batch, ni):
|
|
219
230
|
"""Plot validation image samples."""
|
|
220
|
-
plot_images(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
231
|
+
plot_images(
|
|
232
|
+
batch["img"],
|
|
233
|
+
batch["batch_idx"],
|
|
234
|
+
batch["cls"].squeeze(-1),
|
|
235
|
+
batch["bboxes"],
|
|
236
|
+
paths=batch["im_file"],
|
|
237
|
+
fname=self.save_dir / f"val_batch{ni}_labels.jpg",
|
|
238
|
+
names=self.names,
|
|
239
|
+
on_plot=self.on_plot,
|
|
240
|
+
)
|
|
228
241
|
|
|
229
242
|
def plot_predictions(self, batch, preds, ni):
|
|
230
243
|
"""Plots predicted bounding boxes on input images and saves the result."""
|
|
231
|
-
plot_images(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
244
|
+
plot_images(
|
|
245
|
+
batch["img"],
|
|
246
|
+
*output_to_target(preds, max_det=self.args.max_det),
|
|
247
|
+
paths=batch["im_file"],
|
|
248
|
+
fname=self.save_dir / f"val_batch{ni}_pred.jpg",
|
|
249
|
+
names=self.names,
|
|
250
|
+
on_plot=self.on_plot,
|
|
251
|
+
) # pred
|
|
237
252
|
|
|
238
253
|
def save_one_txt(self, predn, save_conf, shape, file):
|
|
239
254
|
"""Save YOLO detections to a txt file in normalized coordinates in a specific format."""
|
|
@@ -241,8 +256,8 @@ class DetectionValidator(BaseValidator):
|
|
|
241
256
|
for *xyxy, conf, cls in predn.tolist():
|
|
242
257
|
xywh = (ops.xyxy2xywh(torch.tensor(xyxy).view(1, 4)) / gn).view(-1).tolist() # normalized xywh
|
|
243
258
|
line = (cls, *xywh, conf) if save_conf else (cls, *xywh) # label format
|
|
244
|
-
with open(file,
|
|
245
|
-
f.write((
|
|
259
|
+
with open(file, "a") as f:
|
|
260
|
+
f.write(("%g " * len(line)).rstrip() % line + "\n")
|
|
246
261
|
|
|
247
262
|
def pred_to_json(self, predn, filename):
|
|
248
263
|
"""Serialize YOLO predictions to COCO json format."""
|
|
@@ -251,28 +266,31 @@ class DetectionValidator(BaseValidator):
|
|
|
251
266
|
box = ops.xyxy2xywh(predn[:, :4]) # xywh
|
|
252
267
|
box[:, :2] -= box[:, 2:] / 2 # xy center to top-left corner
|
|
253
268
|
for p, b in zip(predn.tolist(), box.tolist()):
|
|
254
|
-
self.jdict.append(
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
269
|
+
self.jdict.append(
|
|
270
|
+
{
|
|
271
|
+
"image_id": image_id,
|
|
272
|
+
"category_id": self.class_map[int(p[5])],
|
|
273
|
+
"bbox": [round(x, 3) for x in b],
|
|
274
|
+
"score": round(p[4], 5),
|
|
275
|
+
}
|
|
276
|
+
)
|
|
259
277
|
|
|
260
278
|
def eval_json(self, stats):
|
|
261
279
|
"""Evaluates YOLO output in JSON format and returns performance statistics."""
|
|
262
280
|
if self.args.save_json and self.is_coco and len(self.jdict):
|
|
263
|
-
anno_json = self.data[
|
|
264
|
-
pred_json = self.save_dir /
|
|
265
|
-
LOGGER.info(f
|
|
281
|
+
anno_json = self.data["path"] / "annotations/instances_val2017.json" # annotations
|
|
282
|
+
pred_json = self.save_dir / "predictions.json" # predictions
|
|
283
|
+
LOGGER.info(f"\nEvaluating pycocotools mAP using {pred_json} and {anno_json}...")
|
|
266
284
|
try: # https://github.com/cocodataset/cocoapi/blob/master/PythonAPI/pycocoEvalDemo.ipynb
|
|
267
|
-
check_requirements(
|
|
285
|
+
check_requirements("pycocotools>=2.0.6")
|
|
268
286
|
from pycocotools.coco import COCO # noqa
|
|
269
287
|
from pycocotools.cocoeval import COCOeval # noqa
|
|
270
288
|
|
|
271
289
|
for x in anno_json, pred_json:
|
|
272
|
-
assert x.is_file(), f
|
|
290
|
+
assert x.is_file(), f"{x} file not found"
|
|
273
291
|
anno = COCO(str(anno_json)) # init annotations api
|
|
274
292
|
pred = anno.loadRes(str(pred_json)) # init predictions api (must pass string, not Path)
|
|
275
|
-
eval = COCOeval(anno, pred,
|
|
293
|
+
eval = COCOeval(anno, pred, "bbox")
|
|
276
294
|
if self.is_coco:
|
|
277
295
|
eval.params.imgIds = [int(Path(x).stem) for x in self.dataloader.dataset.im_files] # images to eval
|
|
278
296
|
eval.evaluate()
|
|
@@ -280,5 +298,5 @@ class DetectionValidator(BaseValidator):
|
|
|
280
298
|
eval.summarize()
|
|
281
299
|
stats[self.metrics.keys[-1]], stats[self.metrics.keys[-2]] = eval.stats[:2] # update mAP50-95 and mAP50
|
|
282
300
|
except Exception as e:
|
|
283
|
-
LOGGER.warning(f
|
|
301
|
+
LOGGER.warning(f"pycocotools unable to run: {e}")
|
|
284
302
|
return stats
|