YOLOv8 Crop Detected Objects: How to Save Individual Detections

YOLOv8 crop detected objects workflows let you extract each detected object from an image or video and save it as a separate image file. Instead of saving only a full annotated image, you can use each detection’s bounding box to isolate the corresponding region. Current Ultralytics Predict mode supports this directly with save_crop=True, and the Results.save_crop() method can also save crops into class-specific subdirectories.

Object cropping is useful for dataset creation, OCR pipelines, product extraction, face or vehicle analysis, visual search, secondary classification, and other workflows where the detected object must be processed independently from the full scene.

Table of Contents

Introduction to Cropping Detected Objects in YOLOv8

YOLOv8 normally predicts where objects are located by returning bounding boxes. Those boxes can also be used as crop coordinates.

For example:

Input Image
    ↓
YOLOv8 Detection
    ↓
Bounding Boxes
    ↓
Crop Each Box Region
    ↓
person_1.jpg
car_1.jpg
car_2.jpg

Suppose YOLOv8 detects a car with:

x1 = 120
y1 = 180
x2 = 620
y2 = 520

The corresponding image region can be extracted using those pixel coordinates.

Ultralytics provides two main approaches. You can enable automatic crop saving with save_crop=True, or manually access result.boxes.xyxy and slice the original image yourself. The manual method gives more control over filenames, filtering, padding, crop quality, and duplicate handling.

What Does Cropping Detected Objects Mean in YOLOv8?

Cropping means extracting only the rectangular image region covered by a detected object’s bounding box.

If the original image contains:

person
car
dog
background

and YOLOv8 detects all three objects, cropping can produce:

person crop
car crop
dog crop

as separate images.

The crop normally includes the original pixels inside the detection box. It does not automatically remove the background inside that rectangle. For true object isolation, instance segmentation masks are more appropriate.

How Bounding Boxes Define Crop Areas

YOLOv8 detection results expose bounding boxes through:

result.boxes.xyxy

The format is:

[x1, y1, x2, y2]

where:

x1 = left edge
y1 = top edge
x2 = right edge
y2 = bottom edge

A NumPy or OpenCV image can then be sliced as:

crop = image[y1:y2, x1:x2]

Ultralytics’ own segmentation-object guide demonstrates the same basic technique by reading boxes.xyxy, converting coordinates to integers, and slicing the image region.

Why Object Cropping Is Useful

Cropping can simplify downstream processing because the next system sees only the relevant detected region.

Common uses include:

vehicle crop → license plate OCR
person crop → clothing analysis
product crop → product classification
animal crop → species classifier
document crop → text recognition

It can also help create focused datasets from larger scenes.

For example, if a detector processes 5,000 warehouse images and finds 30,000 boxes, those detections can be converted into individual object images for review or secondary model training.

How to Crop Detected Objects with YOLOv8

The manual cropping workflow consists of three main stages:

Run Detection
    ↓
Read Bounding Boxes
    ↓
Crop Original Image

This approach gives you full control over which objects are saved.

Run Object Detection on an Image

Load a model:

from ultralytics import YOLO

model = YOLO("yolov8n.pt")

Run prediction:

results = model("image.jpg")

Select the result:

result = results[0]

You can inspect:

print(result.boxes)

Each box contains coordinates, confidence, and class information.

Extract Bounding Box Coordinates

Loop through detections:

for box in result.boxes:
    x1, y1, x2, y2 = (
        box.xyxy[0]
        .cpu()
        .tolist()
    )

    print(x1, y1, x2, y2)

For image slicing, convert them to integers:

x1, y1, x2, y2 = map(
    int,
    box.xyxy[0].cpu().tolist()
)

Integer conversion is important because NumPy image indexes must be integer positions.

Crop Each Detected Object

The original input image is available from:

result.orig_img

A manual crop is:

image = result.orig_img

for box in result.boxes:

    x1, y1, x2, y2 = map(
        int,
        box.xyxy[0]
        .cpu()
        .tolist()
    )

    crop = image[
        y1:y2,
        x1:x2
    ]

You can then save the crop using OpenCV.

Crop YOLOv8 Detections Using Python

Python gives the most flexibility when you need custom naming, filtering, metadata, or processing.

A complete basic workflow is:

from ultralytics import YOLO
import cv2

model = YOLO("yolov8n.pt")

results = model("image.jpg")

result = results[0]
image = result.orig_img

for i, box in enumerate(result.boxes):

    x1, y1, x2, y2 = map(
        int,
        box.xyxy[0]
        .cpu()
        .tolist()
    )

    crop = image[y1:y2, x1:x2]

    cv2.imwrite(
        f"crop_{i}.jpg",
        crop
    )

This saves each detection separately.

Access results.boxes

The bounding boxes are stored in:

result.boxes

Useful properties include:

result.boxes.xyxy
result.boxes.conf
result.boxes.cls

For example:

for box in result.boxes:

    class_id = int(box.cls[0])

    confidence = float(box.conf[0])

    coordinates = (
        box.xyxy[0]
        .cpu()
        .tolist()
    )

    print(
        class_id,
        confidence,
        coordinates
    )

This information can be used to decide whether a particular detection should be cropped.

Convert Box Coordinates to Image Regions

Bounding boxes must stay within valid image boundaries.

A safer implementation is:

image = result.orig_img

height, width = image.shape[:2]

for box in result.boxes:

    x1, y1, x2, y2 = map(
        int,
        box.xyxy[0]
        .cpu()
        .tolist()
    )

    x1 = max(0, x1)
    y1 = max(0, y1)
    x2 = min(width, x2)
    y2 = min(height, y2)

    crop = image[y1:y2, x1:x2]

This prevents invalid coordinates from extending outside the image.

Save Cropped Objects to Files

Use the class name when saving:

import os
import cv2

os.makedirs(
    "crops",
    exist_ok=True
)

for i, box in enumerate(result.boxes):

    class_id = int(
        box.cls[0]
    )

    class_name = result.names[
        class_id
    ]

    x1, y1, x2, y2 = map(
        int,
        box.xyxy[0]
        .cpu()
        .tolist()
    )

    crop = result.orig_img[
        y1:y2,
        x1:x2
    ]

    filename = (
        f"crops/"
        f"{class_name}_{i}.jpg"
    )

    cv2.imwrite(
        filename,
        crop
    )

This creates files such as:

person_0.jpg
car_1.jpg
car_2.jpg
dog_3.jpg

Save Cropped Objects Automatically

Manual cropping is not always necessary.

Ultralytics Predict mode currently provides:

save_crop=True

which automatically saves detection crops. The Predict documentation lists save_crop as a built-in visualization/output option specifically for saving cropped detection images.

Use the save_crop Option

Python:

from ultralytics import YOLO

model = YOLO("yolov8n.pt")

model.predict(
    source="image.jpg",
    save_crop=True
)

CLI:

yolo detect predict \
model=yolov8n.pt \
source=image.jpg \
save_crop=True

Ultralytics also exposes the result-level method:

result.save_crop(
    save_dir="crops",
    file_name="detection"
)

The method saves one crop per detected object.

Organize Crops by Class Name

Current Results.save_crop() automatically creates a subdirectory using the detected class name.

The documented pattern is:

save_dir/
└── class_name/
    └── file_name.jpg

For example:

crops/
├── person/
│   ├── detection.jpg
│   └── detection2.jpg
├── car/
│   ├── detection.jpg
│   └── detection2.jpg
└── dog/
    └── detection.jpg

The current Ultralytics implementation builds the path using the class name associated with each detection.

Choose the Output Directory

For direct result saving:

for result in results:

    result.save_crop(
        save_dir="my_crops",
        file_name="object"
    )

The method creates required directories automatically.

With normal Predict mode and save_crop=True, crops are placed under the prediction run’s output structure.

If you require a completely custom path layout, manual cropping or result.save_crop() gives more control than relying only on the automatic Predict run directory.

Crop Multiple Detected Objects

One image can contain dozens or hundreds of detections.

You should therefore generate unique filenames for each crop.

Loop Through All Predictions

Example:

for i, box in enumerate(
    result.boxes
):

    print(
        "Detection:",
        i
    )

Then extract each region independently:

for i, box in enumerate(
    result.boxes
):

    x1, y1, x2, y2 = map(
        int,
        box.xyxy[0]
        .cpu()
        .tolist()
    )

    crop = result.orig_img[
        y1:y2,
        x1:x2
    ]

Save Each Detection Separately

Use the index:

filename = (
    f"object_{i}.jpg"
)

Or combine it with the class:

filename = (
    f"{class_name}_{i}.jpg"
)

A better production naming scheme might include:

source image
class
confidence
detection index
timestamp

For example:

street01_car_003_0.91.jpg

Handle Multiple Objects of the Same Class

Suppose the image contains five persons.

Saving every one as:

person.jpg

would overwrite previous files.

Instead:

person_index = 0

for box in result.boxes:

    class_id = int(
        box.cls[0]
    )

    class_name = result.names[
        class_id
    ]

    if class_name == "person":

        filename = (
            f"person_"
            f"{person_index}.jpg"
        )

        person_index += 1

Unique naming is particularly important in video processing because the same class may appear thousands of times.

Crop Objects from Video with YOLOv8

A video contains a sequence of frames, so crops can be extracted from every processed frame.

Using stream=True is appropriate for long videos because results are yielded incrementally rather than retained in one large list.

Process Video Frames

Example:

from ultralytics import YOLO

model = YOLO("yolov8n.pt")

results = model.predict(
    source="video.mp4",
    stream=True
)

for frame_id, result in enumerate(
    results
):
    print(
        frame_id,
        len(result.boxes)
    )

Each result.orig_img contains the current frame.

Crop Detections Frame by Frame

Example:

import cv2
import os
from ultralytics import YOLO

model = YOLO("yolov8n.pt")

os.makedirs(
    "video_crops",
    exist_ok=True
)

for frame_id, result in enumerate(
    model.predict(
        source="video.mp4",
        stream=True
    )
):

    for detection_id, box in enumerate(
        result.boxes
    ):

        x1, y1, x2, y2 = map(
            int,
            box.xyxy[0]
            .cpu()
            .tolist()
        )

        crop = result.orig_img[
            y1:y2,
            x1:x2
        ]

        filename = (
            f"video_crops/"
            f"frame_{frame_id}_"
            f"object_{detection_id}.jpg"
        )

        cv2.imwrite(
            filename,
            crop
        )

This gives every detection a frame-specific filename.

Avoid Saving Duplicate Crops

Video creates a major problem: the same object may be detected in many consecutive frames.

For example:

Frame 100 → Car A
Frame 101 → Car A
Frame 102 → Car A
Frame 103 → Car A

Saving every detection creates four nearly identical crops.

A better approach is to use object tracking:

results = model.track(
    source="video.mp4",
    stream=True,
    persist=True
)

Then use:

box.id

to identify a tracked object.

You can save only:

first appearance of each track ID

or:

one crop every N frames per track

or:

best-confidence crop per track

This substantially reduces duplicate images.

Filter Objects Before Cropping

Not every detection should necessarily be saved.

Filtering improves crop quality and reduces unnecessary files.

Crop Only Selected Classes

Suppose only vehicles are needed.

Example:

allowed_classes = {
    2,
    5,
    7
}

for box in result.boxes:

    class_id = int(
        box.cls[0]
    )

    if class_id not in allowed_classes:
        continue

Alternatively, filter by class name:

allowed_names = {
    "car",
    "bus",
    "truck"
}

Then:

class_name = result.names[
    class_id
]

if class_name not in allowed_names:
    continue

Use a Confidence Threshold

You can filter before inference output:

results = model.predict(
    source="image.jpg",
    conf=0.60
)

Or after prediction:

confidence = float(
    box.conf[0]
)

if confidence < 0.80:
    continue

For crop datasets, a higher threshold may reduce low-quality false-positive crops.

However, setting the threshold too high can remove difficult but valid examples.

Ignore Small or Low-Quality Detections

Small boxes often produce low-resolution crops.

You can calculate:

width = x2 - x1
height = y2 - y1

and reject tiny boxes:

if width < 80 or height < 80:
    continue

You can also use area:

area = width * height

if area < 5000:
    continue

The correct threshold depends on the source resolution and downstream application.

For OCR or fine-grained classification, a very small crop may contain too little detail to be useful.

Crop Objects with a Custom YOLOv8 Model

Custom models work with the same cropping workflow as pretrained YOLOv8 models.

The main difference is that class names come from your own dataset.

Load Custom-Trained Weights

Example:

from ultralytics import YOLO

model = YOLO(
    "runs/detect/train/"
    "weights/best.pt"
)

Run:

results = model(
    "custom_image.jpg"
)

Then use:

results[0].boxes

normally.

Crop Custom Object Classes

Suppose your custom model contains:

0 = defect
1 = crack
2 = corrosion

You can crop only cracks:

for box in result.boxes:

    class_id = int(
        box.cls[0]
    )

    if class_id != 1:
        continue

    x1, y1, x2, y2 = map(
        int,
        box.xyxy[0]
        .cpu()
        .tolist()
    )

    crop = result.orig_img[
        y1:y2,
        x1:x2
    ]

Nothing else about the crop logic changes.

Save Results with Custom Class Names

Custom names are available through:

result.names

Example:

class_name = result.names[
    class_id
]

Then:

filename = (
    f"{class_name}_{i}.jpg"
)

Using:

result.save_crop(
    save_dir="custom_crops",
    file_name="sample"
)

also organizes saved detections into class-name subdirectories automatically.

Common Problems When Cropping YOLOv8 Detections

Most crop problems come from coordinate handling, file naming, video duplication, or insufficient source resolution.

Incorrect Crop Coordinates

Make sure you use:

box.xyxy

when performing:

image[y1:y2, x1:x2]

Do not accidentally use:

box.xywh

as if it were XYXY.

xywh means:

x_center
y_center
width
height

not left, top, right, bottom.

Also convert coordinates to integers before slicing.

Empty or Partial Cropped Images

An empty crop can occur if:

x2 <= x1
y2 <= y1
coordinates exceed image bounds
coordinates were interpreted incorrectly

Check:

if (
    x2 <= x1
    or y2 <= y1
):
    continue

and clamp values:

x1 = max(0, x1)
y1 = max(0, y1)
x2 = min(width, x2)
y2 = min(height, y2)

This protects against invalid slicing.

Duplicate Crops from Video

Video naturally repeats the same object across many frames.

Use tracking IDs if you want one image per physical track.

For example:

saved_ids = set()

for result in results:

    for box in result.boxes:

        if box.id is None:
            continue

        track_id = int(
            box.id[0]
        )

        if track_id in saved_ids:
            continue

        saved_ids.add(
            track_id
        )

        # save crop here

For better quality, you may instead keep the highest-confidence crop for each tracking ID.

Low-Resolution Cropped Objects

The crop cannot contain more native detail than the source image.

For example, if an object occupies only:

25 × 30 pixels

inside the source frame, cropping it produces a tiny image.

Upscaling:

25×30 → 250×300

makes the file larger but does not restore missing visual detail.

Possible improvements include:

use higher-resolution source
increase camera zoom
use higher-resolution stream
use larger inference size when appropriate
move camera closer

FAQs About Cropping Detected Objects in YOLOv8

How do I crop detected objects in YOLOv8?

The easiest automatic method is:

model.predict(
    source="image.jpg",
    save_crop=True
)

Current Ultralytics Predict mode supports save_crop specifically for saving detection crops.

You can also manually crop:

x1, y1, x2, y2 = map(
    int,
    box.xyxy[0].cpu().tolist()
)

crop = result.orig_img[
    y1:y2,
    x1:x2
]

Does YOLOv8 have a built-in save_crop option?

Yes.

Use:

save_crop=True

during prediction.

You can also call:

result.save_crop(
    save_dir="crops",
    file_name="detection"
)

The current save_crop() method automatically creates class-specific directories.

How do I crop only one class in YOLOv8?

Filter the class before saving:

for box in result.boxes:

    class_id = int(
        box.cls[0]
    )

    if class_id != 0:
        continue

    # crop detection

You can also identify the desired class using:

result.names[class_id]

instead of relying only on a numeric ID.

Can YOLOv8 crop multiple detected objects automatically?

Yes.

save_crop=True saves detected objects individually.

The result-level save_crop() method loops through the current Boxes collection and saves one bounding-box crop for each detection.

Can I crop objects from a video stream?

Yes.

Use streaming prediction:

for result in model.predict(
    source="video.mp4",
    stream=True
):
    for box in result.boxes:
        # crop current frame
        pass

For live streams, the same approach applies because each result gives access to the current orig_img and detection boxes.

Use tracking IDs if you need to avoid repeatedly saving the same object.

Where does YOLOv8 save cropped detections?

With Results.save_crop(), the documented structure is:

save_dir/
└── class_name/
    └── file_name.jpg

and the required class directories are created automatically.

With Predict mode save_crop=True, the crops are saved under the active prediction run’s output directory structure.

Can I use custom-trained weights for object cropping?

Yes.

Load custom weights:

model = YOLO(
    "runs/detect/train/"
    "weights/best.pt"
)

Then use:

model.predict(
    source="image.jpg",
    save_crop=True
)

or manually access:

result.boxes

Custom class names are preserved through:

result.names

so crops can be organized according to your own object categories.

Conclusion

YOLOv8 crop detected objects functionality makes it easy to turn ordinary object detections into individual image files.

The fastest built-in method is:

from ultralytics import YOLO

model = YOLO("yolov8n.pt")

model.predict(
    source="image.jpg",
    save_crop=True
)

Current Ultralytics documents save_crop=True as a Predict option for saving cropped images of detected objects.

For more control, use the Results API:

from ultralytics import YOLO
import cv2

model = YOLO("yolov8n.pt")

result = model(
    "image.jpg"
)[0]

for i, box in enumerate(
    result.boxes
):

    class_id = int(
        box.cls[0]
    )

    class_name = result.names[
        class_id
    ]

    x1, y1, x2, y2 = map(
        int,
        box.xyxy[0]
        .cpu()
        .tolist()
    )

    crop = result.orig_img[
        y1:y2,
        x1:x2
    ]

    cv2.imwrite(
        f"{class_name}_{i}.jpg",
        crop
    )

Ultralytics also provides:

result.save_crop(
    save_dir="crops",
    file_name="detection"
)

which saves crops into class-specific subdirectories and creates those directories automatically. The current implementation uses each detection’s xyxy bounding box and the original image when creating the saved crop.

A practical cropping workflow is:

Run Detection
      ↓
Read Bounding Boxes
      ↓
Filter Classes
      ↓
Filter Confidence
      ↓
Check Box Size
      ↓
Crop Image Regions
      ↓
Assign Unique Filenames
      ↓
Save Crops

For video, add tracking IDs so the same object is not saved repeatedly on every frame. For low-quality crops, improve the source resolution rather than relying only on upscaling after the crop has already been created.

Automatic save_crop=True is best when you simply need all detected objects saved. Manual cropping is better when you need custom filenames, selected classes, confidence filtering, minimum-size rules, padding, duplicate prevention, or additional processing before each crop is written.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top