ultralytics 8.0.65__py3-none-any.whl → 8.0.66__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.

Files changed (41) hide show
  1. ultralytics/__init__.py +1 -1
  2. ultralytics/datasets/coco-pose.yaml +38 -0
  3. ultralytics/datasets/coco8-pose.yaml +25 -0
  4. ultralytics/models/v8/yolov8-pose-p6.yaml +57 -0
  5. ultralytics/models/v8/yolov8-pose.yaml +47 -0
  6. ultralytics/nn/autobackend.py +7 -2
  7. ultralytics/nn/modules.py +33 -2
  8. ultralytics/nn/tasks.py +24 -7
  9. ultralytics/tracker/track.py +2 -3
  10. ultralytics/yolo/cfg/__init__.py +4 -4
  11. ultralytics/yolo/cfg/default.yaml +2 -0
  12. ultralytics/yolo/data/augment.py +24 -19
  13. ultralytics/yolo/data/build.py +4 -4
  14. ultralytics/yolo/data/dataset.py +9 -3
  15. ultralytics/yolo/data/utils.py +108 -33
  16. ultralytics/yolo/engine/exporter.py +9 -7
  17. ultralytics/yolo/engine/model.py +5 -4
  18. ultralytics/yolo/engine/predictor.py +1 -0
  19. ultralytics/yolo/engine/results.py +70 -56
  20. ultralytics/yolo/utils/benchmarks.py +4 -2
  21. ultralytics/yolo/utils/downloads.py +3 -3
  22. ultralytics/yolo/utils/instance.py +1 -1
  23. ultralytics/yolo/utils/loss.py +14 -0
  24. ultralytics/yolo/utils/metrics.py +111 -13
  25. ultralytics/yolo/utils/ops.py +30 -50
  26. ultralytics/yolo/utils/plotting.py +79 -4
  27. ultralytics/yolo/utils/torch_utils.py +11 -9
  28. ultralytics/yolo/v8/__init__.py +2 -2
  29. ultralytics/yolo/v8/detect/train.py +1 -1
  30. ultralytics/yolo/v8/detect/val.py +2 -2
  31. ultralytics/yolo/v8/pose/__init__.py +7 -0
  32. ultralytics/yolo/v8/pose/predict.py +103 -0
  33. ultralytics/yolo/v8/pose/train.py +170 -0
  34. ultralytics/yolo/v8/pose/val.py +213 -0
  35. ultralytics/yolo/v8/segment/val.py +3 -4
  36. {ultralytics-8.0.65.dist-info → ultralytics-8.0.66.dist-info}/METADATA +27 -2
  37. {ultralytics-8.0.65.dist-info → ultralytics-8.0.66.dist-info}/RECORD +41 -33
  38. {ultralytics-8.0.65.dist-info → ultralytics-8.0.66.dist-info}/LICENSE +0 -0
  39. {ultralytics-8.0.65.dist-info → ultralytics-8.0.66.dist-info}/WHEEL +0 -0
  40. {ultralytics-8.0.65.dist-info → ultralytics-8.0.66.dist-info}/entry_points.txt +0 -0
  41. {ultralytics-8.0.65.dist-info → ultralytics-8.0.66.dist-info}/top_level.txt +0 -0
@@ -6,10 +6,10 @@ import json
6
6
  import os
7
7
  import subprocess
8
8
  import time
9
+ import zipfile
9
10
  from multiprocessing.pool import ThreadPool
10
11
  from pathlib import Path
11
12
  from tarfile import is_tarfile
12
- from zipfile import is_zipfile
13
13
 
14
14
  import cv2
15
15
  import numpy as np
@@ -61,7 +61,7 @@ def exif_size(img):
61
61
 
62
62
  def verify_image_label(args):
63
63
  # Verify one image-label pair
64
- im_file, lb_file, prefix, keypoint, num_cls = args
64
+ im_file, lb_file, prefix, keypoint, num_cls, nkpt, ndim = args
65
65
  # number (missing, found, empty, corrupt), message, segments, keypoints
66
66
  nm, nf, ne, nc, msg, segments, keypoints = 0, 0, 0, 0, '', [], None
67
67
  try:
@@ -92,25 +92,19 @@ def verify_image_label(args):
92
92
  nl = len(lb)
93
93
  if nl:
94
94
  if keypoint:
95
- assert lb.shape[1] == 56, 'labels require 56 columns each'
96
- assert (lb[:, 5::3] <= 1).all(), 'non-normalized or out of bounds coordinate labels'
97
- assert (lb[:, 6::3] <= 1).all(), 'non-normalized or out of bounds coordinate labels'
98
- kpts = np.zeros((lb.shape[0], 39))
99
- for i in range(len(lb)):
100
- kpt = np.delete(lb[i, 5:], np.arange(2, lb.shape[1] - 5, 3)) # remove occlusion param from GT
101
- kpts[i] = np.hstack((lb[i, :5], kpt))
102
- lb = kpts
103
- assert lb.shape[1] == 39, 'labels require 39 columns each after removing occlusion parameter'
95
+ assert lb.shape[1] == (5 + nkpt * ndim), f'labels require {(5 + nkpt * ndim)} columns each'
96
+ assert (lb[:, 5::ndim] <= 1).all(), 'non-normalized or out of bounds coordinate labels'
97
+ assert (lb[:, 6::ndim] <= 1).all(), 'non-normalized or out of bounds coordinate labels'
104
98
  else:
105
99
  assert lb.shape[1] == 5, f'labels require 5 columns, {lb.shape[1]} columns detected'
106
100
  assert (lb[:, 1:] <= 1).all(), \
107
101
  f'non-normalized or out of bounds coordinates {lb[:, 1:][lb[:, 1:] > 1]}'
102
+ assert (lb >= 0).all(), f'negative label values {lb[lb < 0]}'
108
103
  # All labels
109
104
  max_cls = int(lb[:, 0].max()) # max label count
110
105
  assert max_cls <= num_cls, \
111
106
  f'Label class {max_cls} exceeds dataset class count {num_cls}. ' \
112
107
  f'Possible class labels are 0-{num_cls - 1}'
113
- assert (lb >= 0).all(), f'negative label values {lb[lb < 0]}'
114
108
  _, i = np.unique(lb, axis=0, return_index=True)
115
109
  if len(i) < nl: # duplicate row check
116
110
  lb = lb[i] # remove duplicates
@@ -119,12 +113,18 @@ def verify_image_label(args):
119
113
  msg = f'{prefix}WARNING ⚠️ {im_file}: {nl - len(i)} duplicate labels removed'
120
114
  else:
121
115
  ne = 1 # label empty
122
- lb = np.zeros((0, 39), dtype=np.float32) if keypoint else np.zeros((0, 5), dtype=np.float32)
116
+ lb = np.zeros((0, (5 + nkpt * ndim)), dtype=np.float32) if keypoint else np.zeros(
117
+ (0, 5), dtype=np.float32)
123
118
  else:
124
119
  nm = 1 # label missing
125
- lb = np.zeros((0, 39), dtype=np.float32) if keypoint else np.zeros((0, 5), dtype=np.float32)
120
+ lb = np.zeros((0, (5 + nkpt * ndim)), dtype=np.float32) if keypoint else np.zeros((0, 5), dtype=np.float32)
126
121
  if keypoint:
127
- keypoints = lb[:, 5:].reshape(-1, 17, 2)
122
+ keypoints = lb[:, 5:].reshape(-1, nkpt, ndim)
123
+ if ndim == 2:
124
+ kpt_mask = np.ones(keypoints.shape[:2], dtype=np.float32)
125
+ kpt_mask = np.where(keypoints[..., 0] < 0, 0.0, kpt_mask)
126
+ kpt_mask = np.where(keypoints[..., 1] < 0, 0.0, kpt_mask)
127
+ keypoints = np.concatenate([keypoints, kpt_mask[..., None]], axis=-1) # (nl, nkpt, 3)
128
128
  lb = lb[:, :5]
129
129
  return im_file, lb, shape, segments, keypoints, nm, nf, ne, nc, msg
130
130
  except Exception as e:
@@ -195,7 +195,7 @@ def check_det_dataset(dataset, autodownload=True):
195
195
 
196
196
  # Download (optional)
197
197
  extract_dir = ''
198
- if isinstance(data, (str, Path)) and (is_zipfile(data) or is_tarfile(data)):
198
+ if isinstance(data, (str, Path)) and (zipfile.is_zipfile(data) or is_tarfile(data)):
199
199
  new_dir = safe_download(data, dir=DATASETS_DIR, unzip=True, delete=False, curl=False)
200
200
  data = next((DATASETS_DIR / new_dir).rglob('*.yaml'))
201
201
  extract_dir, autodownload = data.parent, False
@@ -356,23 +356,8 @@ class HUBDatasetStats():
356
356
  assert dir.is_dir(), f'Error unzipping {path}, {dir} not found. path/to/abc.zip MUST unzip to path/to/abc/'
357
357
  return True, str(dir), self._find_yaml(dir) # zipped, data_dir, yaml_path
358
358
 
359
- def _hub_ops(self, f, max_dim=1920):
360
- # HUB ops for 1 image 'f': resize and save at reduced quality in /dataset-hub for web/app viewing
361
- f_new = self.im_dir / Path(f).name # dataset-hub image filename
362
- try: # use PIL
363
- im = Image.open(f)
364
- r = max_dim / max(im.height, im.width) # ratio
365
- if r < 1.0: # image too large
366
- im = im.resize((int(im.width * r), int(im.height * r)))
367
- im.save(f_new, 'JPEG', quality=50, optimize=True) # save
368
- except Exception as e: # use OpenCV
369
- LOGGER.info(f'WARNING ⚠️ HUB ops PIL failure {f}: {e}')
370
- im = cv2.imread(f)
371
- im_height, im_width = im.shape[:2]
372
- r = max_dim / max(im_height, im_width) # ratio
373
- if r < 1.0: # image too large
374
- im = cv2.resize(im, (int(im_width * r), int(im_height * r)), interpolation=cv2.INTER_AREA)
375
- cv2.imwrite(str(f_new), im)
359
+ def _hub_ops(self, f):
360
+ compress_one_image(f, self.im_dir / Path(f).name) # save to dataset-hub
376
361
 
377
362
  def get_json(self, save=False, verbose=False):
378
363
  # Return dataset JSON for Ultralytics HUB
@@ -426,3 +411,93 @@ class HUBDatasetStats():
426
411
  pass
427
412
  LOGGER.info(f'Done. All images saved to {self.im_dir}')
428
413
  return self.im_dir
414
+
415
+
416
+ def compress_one_image(f, f_new=None, max_dim=1920, quality=50):
417
+ """
418
+ Compresses a single image file to reduced size while preserving its aspect ratio and quality using either the
419
+ Python Imaging Library (PIL) or OpenCV library. If the input image is smaller than the maximum dimension, it will
420
+ not be resized.
421
+
422
+ Args:
423
+ f (str): The path to the input image file.
424
+ f_new (str, optional): The path to the output image file. If not specified, the input file will be overwritten.
425
+ max_dim (int, optional): The maximum dimension (width or height) of the output image. Default is 1920 pixels.
426
+ quality (int, optional): The image compression quality as a percentage. Default is 50%.
427
+
428
+ Returns:
429
+ None
430
+
431
+ Usage:
432
+ from pathlib import Path
433
+ from ultralytics.yolo.data.utils import compress_one_image
434
+ for f in Path('/Users/glennjocher/Downloads/dataset').rglob('*.jpg'):
435
+ compress_one_image(f)
436
+ """
437
+ try: # use PIL
438
+ im = Image.open(f)
439
+ r = max_dim / max(im.height, im.width) # ratio
440
+ if r < 1.0: # image too large
441
+ im = im.resize((int(im.width * r), int(im.height * r)))
442
+ im.save(f_new or f, 'JPEG', quality=quality, optimize=True) # save
443
+ except Exception as e: # use OpenCV
444
+ LOGGER.info(f'WARNING ⚠️ HUB ops PIL failure {f}: {e}')
445
+ im = cv2.imread(f)
446
+ im_height, im_width = im.shape[:2]
447
+ r = max_dim / max(im_height, im_width) # ratio
448
+ if r < 1.0: # image too large
449
+ im = cv2.resize(im, (int(im_width * r), int(im_height * r)), interpolation=cv2.INTER_AREA)
450
+ cv2.imwrite(str(f_new or f), im)
451
+
452
+
453
+ def delete_dsstore(path):
454
+ """
455
+ Deletes all ".DS_store" files under a specified directory.
456
+
457
+ Args:
458
+ path (str, optional): The directory path where the ".DS_store" files should be deleted.
459
+
460
+ Returns:
461
+ None
462
+
463
+ Usage:
464
+ from ultralytics.yolo.data.utils import delete_dsstore
465
+ delete_dsstore('/Users/glennjocher/Downloads/dataset')
466
+
467
+ Note:
468
+ ".DS_store" files are created by the Apple operating system and contain metadata about folders and files. They
469
+ are hidden system files and can cause issues when transferring files between different operating systems.
470
+ """
471
+ # Delete Apple .DS_store files
472
+ files = list(Path(path).rglob('.DS_store'))
473
+ LOGGER.info(f'Deleting *.DS_store files: {files}')
474
+ for f in files:
475
+ f.unlink()
476
+
477
+
478
+ def zip_directory(dir, use_zipfile_library=True):
479
+ """Zips a directory and saves the archive to the specified output path.
480
+
481
+ Args:
482
+ dir (str): The path to the directory to be zipped.
483
+ use_zipfile_library (bool): Whether to use zipfile library or shutil for zipping.
484
+
485
+ Returns:
486
+ None
487
+
488
+ Usage:
489
+ from ultralytics.yolo.data.utils import zip_directory
490
+ zip_directory('/Users/glennjocher/Downloads/playground')
491
+
492
+ zip -r coco8-pose.zip coco8-pose
493
+ """
494
+ delete_dsstore(dir)
495
+ if use_zipfile_library:
496
+ dir = Path(dir)
497
+ with zipfile.ZipFile(dir.with_suffix('.zip'), 'w', zipfile.ZIP_DEFLATED) as zip_file:
498
+ for file_path in dir.glob('**/*'):
499
+ if file_path.is_file():
500
+ zip_file.write(file_path, file_path.relative_to(dir))
501
+ else:
502
+ import shutil
503
+ shutil.make_archive(dir, 'zip', dir)
@@ -209,8 +209,8 @@ class Exporter:
209
209
  self.file = file
210
210
  self.output_shape = tuple(y.shape) if isinstance(y, torch.Tensor) else tuple(tuple(x.shape) for x in y)
211
211
  self.pretty_name = Path(self.model.yaml.get('yaml_file', self.file)).stem.replace('yolo', 'YOLO')
212
- description = f'Ultralytics {self.pretty_name} model ' + f'trained on {Path(self.args.data).name}' \
213
- if self.args.data else '(untrained)'
212
+ trained_on = f'trained on {Path(self.args.data).name}' if self.args.data else '(untrained)'
213
+ description = f'Ultralytics {self.pretty_name} model {trained_on}'
214
214
  self.metadata = {
215
215
  'description': description,
216
216
  'author': 'Ultralytics',
@@ -221,6 +221,8 @@ class Exporter:
221
221
  'batch': self.args.batch,
222
222
  'imgsz': self.imgsz,
223
223
  'names': model.names} # model metadata
224
+ if model.task == 'pose':
225
+ self.metadata['kpt_shape'] = model.kpt_shape
224
226
 
225
227
  LOGGER.info(f"\n{colorstr('PyTorch:')} starting from {file} with input shape {tuple(im.shape)} BCHW and "
226
228
  f'output shape(s) {self.output_shape} ({file_size(file):.1f} MB)')
@@ -295,7 +297,8 @@ class Exporter:
295
297
  check_requirements(requirements)
296
298
  import onnx # noqa
297
299
 
298
- LOGGER.info(f'\n{prefix} starting export with onnx {onnx.__version__}...')
300
+ opset_version = self.args.opset or get_latest_opset()
301
+ LOGGER.info(f'\n{prefix} starting export with onnx {onnx.__version__} opset {opset_version}...')
299
302
  f = str(self.file.with_suffix('.onnx'))
300
303
 
301
304
  output_names = ['output0', 'output1'] if isinstance(self.model, SegmentationModel) else ['output0']
@@ -313,7 +316,7 @@ class Exporter:
313
316
  self.im.cpu() if dynamic else self.im,
314
317
  f,
315
318
  verbose=False,
316
- opset_version=self.args.opset or get_latest_opset(),
319
+ opset_version=opset_version,
317
320
  do_constant_folding=True, # WARNING: DNN inference with torch>=1.12 may require do_constant_folding=False
318
321
  input_names=['images'],
319
322
  output_names=output_names,
@@ -377,7 +380,6 @@ class Exporter:
377
380
  yaml_save(Path(f) / 'metadata.yaml', self.metadata) # add metadata.yaml
378
381
  return f, None
379
382
 
380
- @try_export
381
383
  def _export_coreml(self, prefix=colorstr('CoreML:')):
382
384
  # YOLOv8 CoreML export
383
385
  check_requirements('coremltools>=6.0')
@@ -410,8 +412,8 @@ class Exporter:
410
412
  model = self.model
411
413
  elif self.model.task == 'detect':
412
414
  model = iOSDetectModel(self.model, self.im) if self.args.nms else self.model
413
- elif self.model.task == 'segment':
414
- # TODO CoreML Segmentation model pipelining
415
+ else:
416
+ # TODO CoreML Segment and Pose model pipelining
415
417
  model = self.model
416
418
 
417
419
  ts = torch.jit.trace(model.eval(), self.im, strict=False) # TorchScript model
@@ -5,8 +5,8 @@ from pathlib import Path
5
5
  from typing import Union
6
6
 
7
7
  from ultralytics import yolo # noqa
8
- from ultralytics.nn.tasks import (ClassificationModel, DetectionModel, SegmentationModel, attempt_load_one_weight,
9
- guess_model_task, nn, yaml_model_load)
8
+ from ultralytics.nn.tasks import (ClassificationModel, DetectionModel, PoseModel, SegmentationModel,
9
+ attempt_load_one_weight, guess_model_task, nn, yaml_model_load)
10
10
  from ultralytics.yolo.cfg import get_cfg
11
11
  from ultralytics.yolo.engine.exporter import Exporter
12
12
  from ultralytics.yolo.utils import (DEFAULT_CFG, DEFAULT_CFG_DICT, DEFAULT_CFG_KEYS, LOGGER, RANK, ROOT, callbacks,
@@ -25,7 +25,8 @@ TASK_MAP = {
25
25
  yolo.v8.detect.DetectionPredictor],
26
26
  'segment': [
27
27
  SegmentationModel, yolo.v8.segment.SegmentationTrainer, yolo.v8.segment.SegmentationValidator,
28
- yolo.v8.segment.SegmentationPredictor]}
28
+ yolo.v8.segment.SegmentationPredictor],
29
+ 'pose': [PoseModel, yolo.v8.pose.PoseTrainer, yolo.v8.pose.PoseValidator, yolo.v8.pose.PosePredictor]}
29
30
 
30
31
 
31
32
  class YOLO:
@@ -195,7 +196,7 @@ class YOLO:
195
196
  self.model.load(weights)
196
197
  return self
197
198
 
198
- def info(self, verbose=False):
199
+ def info(self, verbose=True):
199
200
  """
200
201
  Logs model info.
201
202
 
@@ -246,6 +246,7 @@ class BasePredictor:
246
246
  dnn=self.args.dnn,
247
247
  data=self.args.data,
248
248
  fp16=self.args.half,
249
+ fuse=True,
249
250
  verbose=verbose)
250
251
  self.device = device
251
252
  self.model.eval()
@@ -17,6 +17,53 @@ from ultralytics.yolo.utils.plotting import Annotator, colors
17
17
  from ultralytics.yolo.utils.torch_utils import TORCHVISION_0_10
18
18
 
19
19
 
20
+ class BaseTensor(SimpleClass):
21
+ """
22
+
23
+ Attributes:
24
+ tensor (torch.Tensor): A tensor.
25
+ orig_shape (tuple): Original image size, in the format (height, width).
26
+
27
+ Methods:
28
+ cpu(): Returns a copy of the tensor on CPU memory.
29
+ numpy(): Returns a copy of the tensor as a numpy array.
30
+ cuda(): Returns a copy of the tensor on GPU memory.
31
+ to(): Returns a copy of the tensor with the specified device and dtype.
32
+ """
33
+
34
+ def __init__(self, tensor, orig_shape) -> None:
35
+ super().__init__()
36
+ assert isinstance(tensor, torch.Tensor)
37
+ self.tensor = tensor
38
+ self.orig_shape = orig_shape
39
+
40
+ @property
41
+ def shape(self):
42
+ return self.data.shape
43
+
44
+ @property
45
+ def data(self):
46
+ return self.tensor
47
+
48
+ def cpu(self):
49
+ return self.__class__(self.data.cpu(), self.orig_shape)
50
+
51
+ def numpy(self):
52
+ return self.__class__(self.data.numpy(), self.orig_shape)
53
+
54
+ def cuda(self):
55
+ return self.__class__(self.data.cuda(), self.orig_shape)
56
+
57
+ def to(self, *args, **kwargs):
58
+ return self.__class__(self.data.to(*args, **kwargs), self.orig_shape)
59
+
60
+ def __len__(self): # override len(results)
61
+ return len(self.data)
62
+
63
+ def __getitem__(self, idx):
64
+ return self.__class__(self.data[idx], self.orig_shape)
65
+
66
+
20
67
  class Results(SimpleClass):
21
68
  """
22
69
  A class for storing and manipulating inference results.
@@ -40,22 +87,23 @@ class Results(SimpleClass):
40
87
  _keys (tuple): A tuple of attribute names for non-empty attributes.
41
88
  """
42
89
 
43
- def __init__(self, orig_img, path, names, boxes=None, masks=None, probs=None) -> None:
90
+ def __init__(self, orig_img, path, names, boxes=None, masks=None, probs=None, keypoints=None) -> None:
44
91
  self.orig_img = orig_img
45
92
  self.orig_shape = orig_img.shape[:2]
46
93
  self.boxes = Boxes(boxes, self.orig_shape) if boxes is not None else None # native size boxes
47
94
  self.masks = Masks(masks, self.orig_shape) if masks is not None else None # native size or imgsz masks
48
95
  self.probs = probs if probs is not None else None
96
+ self.keypoints = keypoints if keypoints is not None else None
49
97
  self.names = names
50
98
  self.path = path
51
- self._keys = ('boxes', 'masks', 'probs')
99
+ self._keys = ('boxes', 'masks', 'probs', 'keypoints')
52
100
 
53
101
  def pandas(self):
54
102
  pass
55
103
  # TODO masks.pandas + boxes.pandas + cls.pandas
56
104
 
57
105
  def __getitem__(self, idx):
58
- r = Results(orig_img=self.orig_img, path=self.path, names=self.names)
106
+ r = self.new()
59
107
  for k in self.keys:
60
108
  setattr(r, k, getattr(self, k)[idx])
61
109
  return r
@@ -69,25 +117,25 @@ class Results(SimpleClass):
69
117
  self.probs = probs
70
118
 
71
119
  def cpu(self):
72
- r = Results(orig_img=self.orig_img, path=self.path, names=self.names)
120
+ r = self.new()
73
121
  for k in self.keys:
74
122
  setattr(r, k, getattr(self, k).cpu())
75
123
  return r
76
124
 
77
125
  def numpy(self):
78
- r = Results(orig_img=self.orig_img, path=self.path, names=self.names)
126
+ r = self.new()
79
127
  for k in self.keys:
80
128
  setattr(r, k, getattr(self, k).numpy())
81
129
  return r
82
130
 
83
131
  def cuda(self):
84
- r = Results(orig_img=self.orig_img, path=self.path, names=self.names)
132
+ r = self.new()
85
133
  for k in self.keys:
86
134
  setattr(r, k, getattr(self, k).cuda())
87
135
  return r
88
136
 
89
137
  def to(self, *args, **kwargs):
90
- r = Results(orig_img=self.orig_img, path=self.path, names=self.names)
138
+ r = self.new()
91
139
  for k in self.keys:
92
140
  setattr(r, k, getattr(self, k).to(*args, **kwargs))
93
141
  return r
@@ -96,6 +144,9 @@ class Results(SimpleClass):
96
144
  for k in self.keys:
97
145
  return len(getattr(self, k))
98
146
 
147
+ def new(self):
148
+ return Results(orig_img=self.orig_img, path=self.path, names=self.names)
149
+
99
150
  @property
100
151
  def keys(self):
101
152
  return [k for k in self._keys if getattr(self, k) is not None]
@@ -109,6 +160,7 @@ class Results(SimpleClass):
109
160
  pil=False,
110
161
  example='abc',
111
162
  img=None,
163
+ kpt_line=True,
112
164
  labels=True,
113
165
  boxes=True,
114
166
  masks=True,
@@ -126,6 +178,7 @@ class Results(SimpleClass):
126
178
  pil (bool): Whether to return the image as a PIL Image.
127
179
  example (str): An example string to display. Useful for indicating the expected format of the output.
128
180
  img (numpy.ndarray): Plot to another image. if not, plot to original image.
181
+ kpt_line (bool): Whether to draw lines connecting keypoints.
129
182
  labels (bool): Whether to plot the label of bounding boxes.
130
183
  boxes (bool): Whether to plot the bounding boxes.
131
184
  masks (bool): Whether to plot the masks.
@@ -146,11 +199,12 @@ class Results(SimpleClass):
146
199
  pred_masks, show_masks = self.masks, masks
147
200
  pred_probs, show_probs = self.probs, probs
148
201
  names = self.names
202
+ keypoints = self.keypoints
149
203
  if pred_boxes and show_boxes:
150
204
  for d in reversed(pred_boxes):
151
205
  c, conf, id = int(d.cls), float(d.conf) if conf else None, None if d.id is None else int(d.id.item())
152
206
  name = ('' if id is None else f'id:{id} ') + names[c]
153
- label = (name if not conf else f'{name} {conf:.2f}') if labels else None
207
+ label = (f'{name} {conf:.2f}' if conf else name) if labels else None
154
208
  annotator.box_label(d.xyxy.squeeze(), label, color=colors(c, True))
155
209
 
156
210
  if pred_masks and show_masks:
@@ -168,10 +222,14 @@ class Results(SimpleClass):
168
222
  text = f"{', '.join(f'{names[j] if names else j} {pred_probs[j]:.2f}' for j in top5i)}, "
169
223
  annotator.text((32, 32), text, txt_color=(255, 255, 255)) # TODO: allow setting colors
170
224
 
225
+ if keypoints is not None:
226
+ for k in reversed(keypoints):
227
+ annotator.kpts(k, self.orig_shape, kpt_line=kpt_line)
228
+
171
229
  return np.asarray(annotator.im) if annotator.pil else annotator.im
172
230
 
173
231
 
174
- class Boxes(SimpleClass):
232
+ class Boxes(BaseTensor):
175
233
  """
176
234
  A class for storing and manipulating detection boxes.
177
235
 
@@ -246,37 +304,15 @@ class Boxes(SimpleClass):
246
304
  def xywhn(self):
247
305
  return self.xywh / self.orig_shape[[1, 0, 1, 0]]
248
306
 
249
- def cpu(self):
250
- return Boxes(self.boxes.cpu(), self.orig_shape)
251
-
252
- def numpy(self):
253
- return Boxes(self.boxes.numpy(), self.orig_shape)
254
-
255
- def cuda(self):
256
- return Boxes(self.boxes.cuda(), self.orig_shape)
257
-
258
- def to(self, *args, **kwargs):
259
- return Boxes(self.boxes.to(*args, **kwargs), self.orig_shape)
260
-
261
307
  def pandas(self):
262
308
  LOGGER.info('results.pandas() method not yet implemented')
263
309
 
264
- @property
265
- def shape(self):
266
- return self.boxes.shape
267
-
268
310
  @property
269
311
  def data(self):
270
312
  return self.boxes
271
313
 
272
- def __len__(self): # override len(results)
273
- return len(self.boxes)
274
-
275
- def __getitem__(self, idx):
276
- return Boxes(self.boxes[idx], self.orig_shape)
277
-
278
314
 
279
- class Masks(SimpleClass):
315
+ class Masks(BaseTensor):
280
316
  """
281
317
  A class for storing and manipulating detection masks.
282
318
 
@@ -316,7 +352,7 @@ class Masks(SimpleClass):
316
352
  def xyn(self):
317
353
  # Segments (normalized)
318
354
  return [
319
- ops.scale_segments(self.masks.shape[1:], x, self.orig_shape, normalize=True)
355
+ ops.scale_coords(self.masks.shape[1:], x, self.orig_shape, normalize=True)
320
356
  for x in ops.masks2segments(self.masks)]
321
357
 
322
358
  @property
@@ -324,31 +360,9 @@ class Masks(SimpleClass):
324
360
  def xy(self):
325
361
  # Segments (pixels)
326
362
  return [
327
- ops.scale_segments(self.masks.shape[1:], x, self.orig_shape, normalize=False)
363
+ ops.scale_coords(self.masks.shape[1:], x, self.orig_shape, normalize=False)
328
364
  for x in ops.masks2segments(self.masks)]
329
365
 
330
- @property
331
- def shape(self):
332
- return self.masks.shape
333
-
334
366
  @property
335
367
  def data(self):
336
368
  return self.masks
337
-
338
- def cpu(self):
339
- return Masks(self.masks.cpu(), self.orig_shape)
340
-
341
- def numpy(self):
342
- return Masks(self.masks.numpy(), self.orig_shape)
343
-
344
- def cuda(self):
345
- return Masks(self.masks.cuda(), self.orig_shape)
346
-
347
- def to(self, *args, **kwargs):
348
- return Masks(self.masks.to(*args, **kwargs), self.orig_shape)
349
-
350
- def __len__(self): # override len(results)
351
- return len(self.masks)
352
-
353
- def __getitem__(self, idx):
354
- return Masks(self.masks[idx], self.orig_shape)
@@ -75,11 +75,13 @@ def benchmark(model=Path(SETTINGS['weights_dir']) / 'yolov8n.pt', imgsz=160, hal
75
75
 
76
76
  # Validate
77
77
  if model.task == 'detect':
78
- data, key = 'coco128.yaml', 'metrics/mAP50-95(B)'
78
+ data, key = 'coco8.yaml', 'metrics/mAP50-95(B)'
79
79
  elif model.task == 'segment':
80
- data, key = 'coco128-seg.yaml', 'metrics/mAP50-95(M)'
80
+ data, key = 'coco8-seg.yaml', 'metrics/mAP50-95(M)'
81
81
  elif model.task == 'classify':
82
82
  data, key = 'imagenet100', 'metrics/accuracy_top5'
83
+ elif model.task == 'pose':
84
+ data, key = 'coco8-pose.yaml', 'metrics/mAP50-95(P)'
83
85
 
84
86
  results = export.val(data=data, batch=1, imgsz=imgsz, plots=False, device=device, half=half, verbose=False)
85
87
  metric, speed = results.results_dict[key], results.speed['inference']
@@ -14,9 +14,9 @@ from tqdm import tqdm
14
14
 
15
15
  from ultralytics.yolo.utils import LOGGER, checks, emojis, is_online
16
16
 
17
- GITHUB_ASSET_NAMES = [f'yolov8{size}{suffix}.pt' for size in 'nsmlx' for suffix in ('', '6', '-cls', '-seg')] + \
18
- [f'yolov5{size}u.pt' for size in 'nsmlx'] + \
19
- [f'yolov3{size}u.pt' for size in ('', '-spp', '-tiny')]
17
+ GITHUB_ASSET_NAMES = [f'yolov8{k}{suffix}.pt' for k in 'nsmlx' for suffix in ('', '6', '-cls', '-seg', '-pose')] + \
18
+ [f'yolov5{k}u.pt' for k in 'nsmlx'] + \
19
+ [f'yolov3{k}u.pt' for k in ('', '-spp', '-tiny')]
20
20
  GITHUB_ASSET_STEMS = [Path(k).stem for k in GITHUB_ASSET_NAMES]
21
21
 
22
22
 
@@ -168,7 +168,7 @@ class Instances:
168
168
  Args:
169
169
  bboxes (ndarray): bboxes with shape [N, 4].
170
170
  segments (list | ndarray): segments.
171
- keypoints (ndarray): keypoints with shape [N, 17, 2].
171
+ keypoints (ndarray): keypoints(x, y, visible) with shape [N, 17, 3].
172
172
  """
173
173
  if segments is None:
174
174
  segments = []
@@ -54,3 +54,17 @@ class BboxLoss(nn.Module):
54
54
  wr = 1 - wl # weight right
55
55
  return (F.cross_entropy(pred_dist, tl.view(-1), reduction='none').view(tl.shape) * wl +
56
56
  F.cross_entropy(pred_dist, tr.view(-1), reduction='none').view(tl.shape) * wr).mean(-1, keepdim=True)
57
+
58
+
59
+ class KeypointLoss(nn.Module):
60
+
61
+ def __init__(self, sigmas) -> None:
62
+ super().__init__()
63
+ self.sigmas = sigmas
64
+
65
+ def forward(self, pred_kpts, gt_kpts, kpt_mask, area):
66
+ d = (pred_kpts[..., 0] - gt_kpts[..., 0]) ** 2 + (pred_kpts[..., 1] - gt_kpts[..., 1]) ** 2
67
+ kpt_loss_factor = (torch.sum(kpt_mask != 0) + torch.sum(kpt_mask == 0)) / (torch.sum(kpt_mask != 0) + 1e-9)
68
+ # e = d / (2 * (area * self.sigmas) ** 2 + 1e-9) # from formula
69
+ e = d / (2 * self.sigmas) ** 2 / (area + 1e-9) / 2 # from cocoeval
70
+ return kpt_loss_factor * ((1 - torch.exp(-e)) * kpt_mask).mean()