Diff of /yolov5/utils/plots.py [000000] .. [f26a44]

Switch to unified view

a b/yolov5/utils/plots.py
1
# YOLOv5 🚀 by Ultralytics, GPL-3.0 license
2
"""
3
Plotting utils
4
"""
5
6
import math
7
import os
8
from copy import copy
9
from pathlib import Path
10
11
import cv2
12
import matplotlib
13
import matplotlib.pyplot as plt
14
import numpy as np
15
import pandas as pd
16
import seaborn as sn
17
import torch
18
from PIL import Image, ImageDraw, ImageFont
19
20
from utils.general import (LOGGER, Timeout, check_requirements, clip_coords, increment_path, is_ascii, is_chinese,
21
                           try_except, user_config_dir, xywh2xyxy, xyxy2xywh)
22
from utils.metrics import fitness
23
24
# Settings
25
CONFIG_DIR = user_config_dir()  # Ultralytics settings dir
26
RANK = int(os.getenv('RANK', -1))
27
matplotlib.rc('font', **{'size': 11})
28
matplotlib.use('Agg')  # for writing to files only
29
30
31
class Colors:
32
    # Ultralytics color palette https://ultralytics.com/
33
    def __init__(self):
34
        # hex = matplotlib.colors.TABLEAU_COLORS.values()
35
        hex = ('FF3838', 'FF9D97', 'FF701F', 'FFB21D', 'CFD231', '48F90A', '92CC17', '3DDB86', '1A9334', '00D4BB',
36
               '2C99A8', '00C2FF', '344593', '6473FF', '0018EC', '8438FF', '520085', 'CB38FF', 'FF95C8', 'FF37C7')
37
        self.palette = [self.hex2rgb('#' + c) for c in hex]
38
        self.n = len(self.palette)
39
40
    def __call__(self, i, bgr=False):
41
        c = self.palette[int(i) % self.n]
42
        return (c[2], c[1], c[0]) if bgr else c
43
44
    @staticmethod
45
    def hex2rgb(h):  # rgb order (PIL)
46
        return tuple(int(h[1 + i:1 + i + 2], 16) for i in (0, 2, 4))
47
48
49
colors = Colors()  # create instance for 'from utils.plots import colors'
50
51
52
def check_font(font='Arial.ttf', size=10):
53
    # Return a PIL TrueType Font, downloading to CONFIG_DIR if necessary
54
    font = Path(font)
55
    font = font if font.exists() else (CONFIG_DIR / font.name)
56
    try:
57
        return ImageFont.truetype(str(font) if font.exists() else font.name, size)
58
    except Exception as e:  # download if missing
59
        url = "https://ultralytics.com/assets/" + font.name
60
        print(f'Downloading {url} to {font}...')
61
        torch.hub.download_url_to_file(url, str(font), progress=False)
62
        try:
63
            return ImageFont.truetype(str(font), size)
64
        except TypeError:
65
            check_requirements('Pillow>=8.4.0')  # known issue https://github.com/ultralytics/yolov5/issues/5374
66
67
68
class Annotator:
69
    if RANK in (-1, 0):
70
        check_font()  # download TTF if necessary
71
72
    # YOLOv5 Annotator for train/val mosaics and jpgs and detect/hub inference annotations
73
    def __init__(self, im, line_width=None, font_size=None, font='Arial.ttf', pil=False, example='abc'):
74
        assert im.data.contiguous, 'Image not contiguous. Apply np.ascontiguousarray(im) to Annotator() input images.'
75
        self.pil = pil or not is_ascii(example) or is_chinese(example)
76
        if self.pil:  # use PIL
77
            self.im = im if isinstance(im, Image.Image) else Image.fromarray(im)
78
            self.draw = ImageDraw.Draw(self.im)
79
            self.font = check_font(font='Arial.Unicode.ttf' if is_chinese(example) else font,
80
                                   size=font_size or max(round(sum(self.im.size) / 2 * 0.035), 12))
81
        else:  # use cv2
82
            self.im = im
83
        self.lw = line_width or max(round(sum(im.shape) / 2 * 0.003), 2)  # line width
84
85
    def box_label(self, box, label='', color=(128, 128, 128), txt_color=(255, 255, 255)):
86
        # Add one xyxy box to image with label
87
        if self.pil or not is_ascii(label):
88
            self.draw.rectangle(box, width=self.lw, outline=color)  # box
89
            if label:
90
                w, h = self.font.getsize(label)  # text width, height
91
                outside = box[1] - h >= 0  # label fits outside box
92
                self.draw.rectangle([box[0],
93
                                     box[1] - h if outside else box[1],
94
                                     box[0] + w + 1,
95
                                     box[1] + 1 if outside else box[1] + h + 1], fill=color)
96
                # self.draw.text((box[0], box[1]), label, fill=txt_color, font=self.font, anchor='ls')  # for PIL>8.0
97
                self.draw.text((box[0], box[1] - h if outside else box[1]), label, fill=txt_color, font=self.font)
98
        else:  # cv2
99
            p1, p2 = (int(box[0]), int(box[1])), (int(box[2]), int(box[3]))
100
            cv2.rectangle(self.im, p1, p2, color, thickness=self.lw, lineType=cv2.LINE_AA)
101
            if label:
102
                tf = max(self.lw - 1, 1)  # font thickness
103
                w, h = cv2.getTextSize(label, 0, fontScale=self.lw / 3, thickness=tf)[0]  # text width, height
104
                outside = p1[1] - h - 3 >= 0  # label fits outside box
105
                p2 = p1[0] + w, p1[1] - h - 3 if outside else p1[1] + h + 3
106
                cv2.rectangle(self.im, p1, p2, color, -1, cv2.LINE_AA)  # filled
107
                cv2.putText(self.im, label, (p1[0], p1[1] - 2 if outside else p1[1] + h + 2), 0, self.lw / 3, txt_color,
108
                            thickness=tf, lineType=cv2.LINE_AA)
109
110
    def rectangle(self, xy, fill=None, outline=None, width=1):
111
        # Add rectangle to image (PIL-only)
112
        self.draw.rectangle(xy, fill, outline, width)
113
114
    def text(self, xy, text, txt_color=(255, 255, 255)):
115
        # Add text to image (PIL-only)
116
        w, h = self.font.getsize(text)  # text width, height
117
        self.draw.text((xy[0], xy[1] - h + 1), text, fill=txt_color, font=self.font)
118
119
    def result(self):
120
        # Return annotated image as array
121
        return np.asarray(self.im)
122
123
124
def feature_visualization(x, module_type, stage, n=32, save_dir=Path('runs/detect/exp')):
125
    """
126
    x:              Features to be visualized
127
    module_type:    Module type
128
    stage:          Module stage within model
129
    n:              Maximum number of feature maps to plot
130
    save_dir:       Directory to save results
131
    """
132
    if 'Detect' not in module_type:
133
        batch, channels, height, width = x.shape  # batch, channels, height, width
134
        if height > 1 and width > 1:
135
            f = save_dir / f"stage{stage}_{module_type.split('.')[-1]}_features.png"  # filename
136
137
            blocks = torch.chunk(x[0].cpu(), channels, dim=0)  # select batch index 0, block by channels
138
            n = min(n, channels)  # number of plots
139
            fig, ax = plt.subplots(math.ceil(n / 8), 8, tight_layout=True)  # 8 rows x n/8 cols
140
            ax = ax.ravel()
141
            plt.subplots_adjust(wspace=0.05, hspace=0.05)
142
            for i in range(n):
143
                ax[i].imshow(blocks[i].squeeze())  # cmap='gray'
144
                ax[i].axis('off')
145
146
            print(f'Saving {f}... ({n}/{channels})')
147
            plt.savefig(f, dpi=300, bbox_inches='tight')
148
            plt.close()
149
            np.save(str(f.with_suffix('.npy')), x[0].cpu().numpy())  # npy save
150
151
152
def hist2d(x, y, n=100):
153
    # 2d histogram used in labels.png and evolve.png
154
    xedges, yedges = np.linspace(x.min(), x.max(), n), np.linspace(y.min(), y.max(), n)
155
    hist, xedges, yedges = np.histogram2d(x, y, (xedges, yedges))
156
    xidx = np.clip(np.digitize(x, xedges) - 1, 0, hist.shape[0] - 1)
157
    yidx = np.clip(np.digitize(y, yedges) - 1, 0, hist.shape[1] - 1)
158
    return np.log(hist[xidx, yidx])
159
160
161
def butter_lowpass_filtfilt(data, cutoff=1500, fs=50000, order=5):
162
    from scipy.signal import butter, filtfilt
163
164
    # https://stackoverflow.com/questions/28536191/how-to-filter-smooth-with-scipy-numpy
165
    def butter_lowpass(cutoff, fs, order):
166
        nyq = 0.5 * fs
167
        normal_cutoff = cutoff / nyq
168
        return butter(order, normal_cutoff, btype='low', analog=False)
169
170
    b, a = butter_lowpass(cutoff, fs, order=order)
171
    return filtfilt(b, a, data)  # forward-backward filter
172
173
174
def output_to_target(output):
175
    # Convert model output to target format [batch_id, class_id, x, y, w, h, conf]
176
    targets = []
177
    for i, o in enumerate(output):
178
        for *box, conf, cls in o.cpu().numpy():
179
            targets.append([i, cls, *list(*xyxy2xywh(np.array(box)[None])), conf])
180
    return np.array(targets)
181
182
183
def plot_images(images, targets, paths=None, fname='images.jpg', names=None, max_size=1920, max_subplots=16):
184
    # Plot image grid with labels
185
    if isinstance(images, torch.Tensor):
186
        images = images.cpu().float().numpy()
187
    if isinstance(targets, torch.Tensor):
188
        targets = targets.cpu().numpy()
189
    if np.max(images[0]) <= 1:
190
        images *= 255  # de-normalise (optional)
191
    bs, _, h, w = images.shape  # batch size, _, height, width
192
    bs = min(bs, max_subplots)  # limit plot images
193
    ns = np.ceil(bs ** 0.5)  # number of subplots (square)
194
195
    # Build Image
196
    mosaic = np.full((int(ns * h), int(ns * w), 3), 255, dtype=np.uint8)  # init
197
    for i, im in enumerate(images):
198
        if i == max_subplots:  # if last batch has fewer images than we expect
199
            break
200
        x, y = int(w * (i // ns)), int(h * (i % ns))  # block origin
201
        im = im.transpose(1, 2, 0)
202
        mosaic[y:y + h, x:x + w, :] = im
203
204
    # Resize (optional)
205
    scale = max_size / ns / max(h, w)
206
    if scale < 1:
207
        h = math.ceil(scale * h)
208
        w = math.ceil(scale * w)
209
        mosaic = cv2.resize(mosaic, tuple(int(x * ns) for x in (w, h)))
210
211
    # Annotate
212
    fs = int((h + w) * ns * 0.01)  # font size
213
    annotator = Annotator(mosaic, line_width=round(fs / 10), font_size=fs, pil=True)
214
    for i in range(i + 1):
215
        x, y = int(w * (i // ns)), int(h * (i % ns))  # block origin
216
        annotator.rectangle([x, y, x + w, y + h], None, (255, 255, 255), width=2)  # borders
217
        if paths:
218
            annotator.text((x + 5, y + 5 + h), text=Path(paths[i]).name[:40], txt_color=(220, 220, 220))  # filenames
219
        if len(targets) > 0:
220
            ti = targets[targets[:, 0] == i]  # image targets
221
            boxes = xywh2xyxy(ti[:, 2:6]).T
222
            classes = ti[:, 1].astype('int')
223
            labels = ti.shape[1] == 6  # labels if no conf column
224
            conf = None if labels else ti[:, 6]  # check for confidence presence (label vs pred)
225
226
            if boxes.shape[1]:
227
                if boxes.max() <= 1.01:  # if normalized with tolerance 0.01
228
                    boxes[[0, 2]] *= w  # scale to pixels
229
                    boxes[[1, 3]] *= h
230
                elif scale < 1:  # absolute coords need scale if image scales
231
                    boxes *= scale
232
            boxes[[0, 2]] += x
233
            boxes[[1, 3]] += y
234
            for j, box in enumerate(boxes.T.tolist()):
235
                cls = classes[j]
236
                color = colors(cls)
237
                cls = names[cls] if names else cls
238
                if labels or conf[j] > 0.25:  # 0.25 conf thresh
239
                    label = f'{cls}' if labels else f'{cls} {conf[j]:.1f}'
240
                    annotator.box_label(box, label, color=color)
241
    annotator.im.save(fname)  # save
242
243
244
def plot_lr_scheduler(optimizer, scheduler, epochs=300, save_dir=''):
245
    # Plot LR simulating training for full epochs
246
    optimizer, scheduler = copy(optimizer), copy(scheduler)  # do not modify originals
247
    y = []
248
    for _ in range(epochs):
249
        scheduler.step()
250
        y.append(optimizer.param_groups[0]['lr'])
251
    plt.plot(y, '.-', label='LR')
252
    plt.xlabel('epoch')
253
    plt.ylabel('LR')
254
    plt.grid()
255
    plt.xlim(0, epochs)
256
    plt.ylim(0)
257
    plt.savefig(Path(save_dir) / 'LR.png', dpi=200)
258
    plt.close()
259
260
261
def plot_val_txt():  # from utils.plots import *; plot_val()
262
    # Plot val.txt histograms
263
    x = np.loadtxt('val.txt', dtype=np.float32)
264
    box = xyxy2xywh(x[:, :4])
265
    cx, cy = box[:, 0], box[:, 1]
266
267
    fig, ax = plt.subplots(1, 1, figsize=(6, 6), tight_layout=True)
268
    ax.hist2d(cx, cy, bins=600, cmax=10, cmin=0)
269
    ax.set_aspect('equal')
270
    plt.savefig('hist2d.png', dpi=300)
271
272
    fig, ax = plt.subplots(1, 2, figsize=(12, 6), tight_layout=True)
273
    ax[0].hist(cx, bins=600)
274
    ax[1].hist(cy, bins=600)
275
    plt.savefig('hist1d.png', dpi=200)
276
277
278
def plot_targets_txt():  # from utils.plots import *; plot_targets_txt()
279
    # Plot targets.txt histograms
280
    x = np.loadtxt('targets.txt', dtype=np.float32).T
281
    s = ['x targets', 'y targets', 'width targets', 'height targets']
282
    fig, ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)
283
    ax = ax.ravel()
284
    for i in range(4):
285
        ax[i].hist(x[i], bins=100, label=f'{x[i].mean():.3g} +/- {x[i].std():.3g}')
286
        ax[i].legend()
287
        ax[i].set_title(s[i])
288
    plt.savefig('targets.jpg', dpi=200)
289
290
291
def plot_val_study(file='', dir='', x=None):  # from utils.plots import *; plot_val_study()
292
    # Plot file=study.txt generated by val.py (or plot all study*.txt in dir)
293
    save_dir = Path(file).parent if file else Path(dir)
294
    plot2 = False  # plot additional results
295
    if plot2:
296
        ax = plt.subplots(2, 4, figsize=(10, 6), tight_layout=True)[1].ravel()
297
298
    fig2, ax2 = plt.subplots(1, 1, figsize=(8, 4), tight_layout=True)
299
    # for f in [save_dir / f'study_coco_{x}.txt' for x in ['yolov5n6', 'yolov5s6', 'yolov5m6', 'yolov5l6', 'yolov5x6']]:
300
    for f in sorted(save_dir.glob('study*.txt')):
301
        y = np.loadtxt(f, dtype=np.float32, usecols=[0, 1, 2, 3, 7, 8, 9], ndmin=2).T
302
        x = np.arange(y.shape[1]) if x is None else np.array(x)
303
        if plot2:
304
            s = ['P', 'R', 'mAP@.5', 'mAP@.5:.95', 't_preprocess (ms/img)', 't_inference (ms/img)', 't_NMS (ms/img)']
305
            for i in range(7):
306
                ax[i].plot(x, y[i], '.-', linewidth=2, markersize=8)
307
                ax[i].set_title(s[i])
308
309
        j = y[3].argmax() + 1
310
        ax2.plot(y[5, 1:j], y[3, 1:j] * 1E2, '.-', linewidth=2, markersize=8,
311
                 label=f.stem.replace('study_coco_', '').replace('yolo', 'YOLO'))
312
313
    ax2.plot(1E3 / np.array([209, 140, 97, 58, 35, 18]), [34.6, 40.5, 43.0, 47.5, 49.7, 51.5],
314
             'k.-', linewidth=2, markersize=8, alpha=.25, label='EfficientDet')
315
316
    ax2.grid(alpha=0.2)
317
    ax2.set_yticks(np.arange(20, 60, 5))
318
    ax2.set_xlim(0, 57)
319
    ax2.set_ylim(25, 55)
320
    ax2.set_xlabel('GPU Speed (ms/img)')
321
    ax2.set_ylabel('COCO AP val')
322
    ax2.legend(loc='lower right')
323
    f = save_dir / 'study.png'
324
    print(f'Saving {f}...')
325
    plt.savefig(f, dpi=300)
326
327
328
@try_except  # known issue https://github.com/ultralytics/yolov5/issues/5395
329
@Timeout(30)  # known issue https://github.com/ultralytics/yolov5/issues/5611
330
def plot_labels(labels, names=(), save_dir=Path('')):
331
    # plot dataset labels
332
    LOGGER.info(f"Plotting labels to {save_dir / 'labels.jpg'}... ")
333
    c, b = labels[:, 0], labels[:, 1:].transpose()  # classes, boxes
334
    nc = int(c.max() + 1)  # number of classes
335
    x = pd.DataFrame(b.transpose(), columns=['x', 'y', 'width', 'height'])
336
337
    # seaborn correlogram
338
    sn.pairplot(x, corner=True, diag_kind='auto', kind='hist', diag_kws=dict(bins=50), plot_kws=dict(pmax=0.9))
339
    plt.savefig(save_dir / 'labels_correlogram.jpg', dpi=200)
340
    plt.close()
341
342
    # matplotlib labels
343
    matplotlib.use('svg')  # faster
344
    ax = plt.subplots(2, 2, figsize=(8, 8), tight_layout=True)[1].ravel()
345
    y = ax[0].hist(c, bins=np.linspace(0, nc, nc + 1) - 0.5, rwidth=0.8)
346
    # [y[2].patches[i].set_color([x / 255 for x in colors(i)]) for i in range(nc)]  # update colors bug #3195
347
    ax[0].set_ylabel('instances')
348
    if 0 < len(names) < 30:
349
        ax[0].set_xticks(range(len(names)))
350
        ax[0].set_xticklabels(names, rotation=90, fontsize=10)
351
    else:
352
        ax[0].set_xlabel('classes')
353
    sn.histplot(x, x='x', y='y', ax=ax[2], bins=50, pmax=0.9)
354
    sn.histplot(x, x='width', y='height', ax=ax[3], bins=50, pmax=0.9)
355
356
    # rectangles
357
    labels[:, 1:3] = 0.5  # center
358
    labels[:, 1:] = xywh2xyxy(labels[:, 1:]) * 2000
359
    img = Image.fromarray(np.ones((2000, 2000, 3), dtype=np.uint8) * 255)
360
    for cls, *box in labels[:1000]:
361
        ImageDraw.Draw(img).rectangle(box, width=1, outline=colors(cls))  # plot
362
    ax[1].imshow(img)
363
    ax[1].axis('off')
364
365
    for a in [0, 1, 2, 3]:
366
        for s in ['top', 'right', 'left', 'bottom']:
367
            ax[a].spines[s].set_visible(False)
368
369
    plt.savefig(save_dir / 'labels.jpg', dpi=200)
370
    matplotlib.use('Agg')
371
    plt.close()
372
373
374
def plot_evolve(evolve_csv='path/to/evolve.csv'):  # from utils.plots import *; plot_evolve()
375
    # Plot evolve.csv hyp evolution results
376
    evolve_csv = Path(evolve_csv)
377
    data = pd.read_csv(evolve_csv)
378
    keys = [x.strip() for x in data.columns]
379
    x = data.values
380
    f = fitness(x)
381
    j = np.argmax(f)  # max fitness index
382
    plt.figure(figsize=(10, 12), tight_layout=True)
383
    matplotlib.rc('font', **{'size': 8})
384
    for i, k in enumerate(keys[7:]):
385
        v = x[:, 7 + i]
386
        mu = v[j]  # best single result
387
        plt.subplot(6, 5, i + 1)
388
        plt.scatter(v, f, c=hist2d(v, f, 20), cmap='viridis', alpha=.8, edgecolors='none')
389
        plt.plot(mu, f.max(), 'k+', markersize=15)
390
        plt.title(f'{k} = {mu:.3g}', fontdict={'size': 9})  # limit to 40 characters
391
        if i % 5 != 0:
392
            plt.yticks([])
393
        print(f'{k:>15}: {mu:.3g}')
394
    f = evolve_csv.with_suffix('.png')  # filename
395
    plt.savefig(f, dpi=200)
396
    plt.close()
397
    print(f'Saved {f}')
398
399
400
def plot_results(file='path/to/results.csv', dir=''):
401
    # Plot training results.csv. Usage: from utils.plots import *; plot_results('path/to/results.csv')
402
    save_dir = Path(file).parent if file else Path(dir)
403
    fig, ax = plt.subplots(2, 5, figsize=(12, 6), tight_layout=True)
404
    ax = ax.ravel()
405
    files = list(save_dir.glob('results*.csv'))
406
    assert len(files), f'No results.csv files found in {save_dir.resolve()}, nothing to plot.'
407
    for fi, f in enumerate(files):
408
        try:
409
            data = pd.read_csv(f)
410
            s = [x.strip() for x in data.columns]
411
            x = data.values[:, 0]
412
            for i, j in enumerate([1, 2, 3, 4, 5, 8, 9, 10, 6, 7]):
413
                y = data.values[:, j]
414
                # y[y == 0] = np.nan  # don't show zero values
415
                ax[i].plot(x, y, marker='.', label=f.stem, linewidth=2, markersize=8)
416
                ax[i].set_title(s[j], fontsize=12)
417
                # if j in [8, 9, 10]:  # share train and val loss y axes
418
                #     ax[i].get_shared_y_axes().join(ax[i], ax[i - 5])
419
        except Exception as e:
420
            print(f'Warning: Plotting error for {f}: {e}')
421
    ax[1].legend()
422
    fig.savefig(save_dir / 'results.png', dpi=200)
423
    plt.close()
424
425
426
def profile_idetection(start=0, stop=0, labels=(), save_dir=''):
427
    # Plot iDetection '*.txt' per-image logs. from utils.plots import *; profile_idetection()
428
    ax = plt.subplots(2, 4, figsize=(12, 6), tight_layout=True)[1].ravel()
429
    s = ['Images', 'Free Storage (GB)', 'RAM Usage (GB)', 'Battery', 'dt_raw (ms)', 'dt_smooth (ms)', 'real-world FPS']
430
    files = list(Path(save_dir).glob('frames*.txt'))
431
    for fi, f in enumerate(files):
432
        try:
433
            results = np.loadtxt(f, ndmin=2).T[:, 90:-30]  # clip first and last rows
434
            n = results.shape[1]  # number of rows
435
            x = np.arange(start, min(stop, n) if stop else n)
436
            results = results[:, x]
437
            t = (results[0] - results[0].min())  # set t0=0s
438
            results[0] = x
439
            for i, a in enumerate(ax):
440
                if i < len(results):
441
                    label = labels[fi] if len(labels) else f.stem.replace('frames_', '')
442
                    a.plot(t, results[i], marker='.', label=label, linewidth=1, markersize=5)
443
                    a.set_title(s[i])
444
                    a.set_xlabel('time (s)')
445
                    # if fi == len(files) - 1:
446
                    #     a.set_ylim(bottom=0)
447
                    for side in ['top', 'right']:
448
                        a.spines[side].set_visible(False)
449
                else:
450
                    a.remove()
451
        except Exception as e:
452
            print(f'Warning: Plotting error for {f}; {e}')
453
    ax[1].legend()
454
    plt.savefig(Path(save_dir) / 'idetection_profile.png', dpi=200)
455
456
457
def save_one_box(xyxy, im, file='image.jpg', gain=1.02, pad=10, square=False, BGR=False, save=True):
458
    # Save image crop as {file} with crop size multiple {gain} and {pad} pixels. Save and/or return crop
459
    xyxy = torch.tensor(xyxy).view(-1, 4)
460
    b = xyxy2xywh(xyxy)  # boxes
461
    if square:
462
        b[:, 2:] = b[:, 2:].max(1)[0].unsqueeze(1)  # attempt rectangle to square
463
    b[:, 2:] = b[:, 2:] * gain + pad  # box wh * gain + pad
464
    xyxy = xywh2xyxy(b).long()
465
    clip_coords(xyxy, im.shape)
466
    crop = im[int(xyxy[0, 1]):int(xyxy[0, 3]), int(xyxy[0, 0]):int(xyxy[0, 2]), ::(1 if BGR else -1)]
467
    if save:
468
        file.parent.mkdir(parents=True, exist_ok=True)  # make directory
469
        cv2.imwrite(str(increment_path(file).with_suffix('.jpg')), crop)
470
    return crop