计算机视觉:YOLOv11遥感图像目标检测(附代码)

第一章:计算机视觉中图像的基础认知
第二章:计算机视觉:卷积神经网络(CNN)基本概念(一)
第三章:计算机视觉:卷积神经网络(CNN)基本概念(二)
第四章:搭建一个经典的LeNet5神经网络(附代码)
第五章:计算机视觉:神经网络实战之手势识别(附代码)
第六章:计算机视觉:目标检测从简单到容易(附代码)
第七章:MTCNN 人脸检测技术揭秘:原理、实现与实战(附代码)
第八章:探索YOLO技术:目标检测的高效解决方案
第九章:计算机视觉:主流数据集整理
第十章:生成对抗网络(GAN):从概念到代码实践(附代码)
第十一章:计算机视觉:经典数据格式(VOC、YOLO、COCO)解析与转换(附代码)
第十二章:计算机视觉:YOLOv11遥感图像目标检测(附代码)

一、任务简介

随着卫星技术和无人机技术的发展,获取高质量的遥感图像变得更加容易。然而,如何从这些海量的数据中准确提取有用的信息,特别是识别出特定的目标(如飞机)。YOLO系列作为实时对象检测算法中的佼佼者,因其高速度与高精度的特点,在众多应用场景中得到了广泛应用。

计算机视觉领域,遥感图像中的目标检测,识别和定位不同类型的飞机是一个典型的应用场景。本文利用最新的YOLOv11模型来解决这一挑战,通过构建一个高效的飞机检测模型,实现对遥感图像中各类飞机的精准识别。

比如 下面这个图片,如果使用人眼观察是很难看出来,遥感图像里哪些地方是飞机,分别是什么型号的飞机。
在这里插入图片描述
使用模型经过训练之后,模型可以预测出飞机所在位置,同时还标注飞机的型号:
在这里插入图片描述

二、数据准备

2.1 数据集介绍

数据集可以从这里下载:《SAR-AIRcraft-1.0:高分辨率SAR飞机检测识别数据集》,
本文使用的数据集包含遥感飞机的图像和对应的标注信息。数据集的原始目录结构如下:

- JPEGImages
    - 1.jpg
    - 2.jpg
    - ...
- Annotations
    - 1.xml
    - 2.xml
    - ...
<?xml version="1.0" encoding="utf-8"?>
<annotation>
	<size>
		<width>1500</width>
		<height>1500</height>
	</size>
	<object>
		<name>Boeing787</name>
		<pose>Unspecified</pose>
		<truncated>0</truncated>
		<difficult>0</difficult>
		<bndbox>
			<xmin>459</xmin>
			<ymin>1433</ymin>
			<xmax>536</xmax>
			<ymax>1499</ymax>
		</bndbox>
	</object>
	<object>
		<name>Boeing737</name>
		<pose>Unspecified</pose>
		<truncated>0</truncated>
		<difficult>0</difficult>
		<bndbox>
			<xmin>988</xmin>
			<ymin>1426</ymin>
			<xmax>1064</xmax>
			<ymax>1499</ymax>
		</bndbox>
	</object>
</annotation>

数据集的处理包括一下两个步骤:

  • 数据集构建:收集包含多种类型飞机的遥感图像,并对其进行标注,形成训练所需的VOC格式数据集。
  • 数据预处理:编写脚本将原始数据按照一定比例划分为训练集和测试集,并转换为YOLO模型可接受的格式(即从VOC到YOLO格式的转换)。

2.2 数据拷贝

检测ultralytics 版本

import os
import shutil
import random
from xml.etree import ElementTree
from ultralytics import YOLO
import ultralytics

print(ultralytics.__version__) # 输出 8.3.36

将原始数据拷贝到yolo的 datasets 目录下,数据目录结构:

datasets
└── sar-aircraft-1.0
    ├── images
    │   ├── train  # 训练集图片(示例8000张)
    │   └── val    # 验证集图片(示例2000张)
    └── labels
        ├── train  # 训练集标签
        └── val    # 验证集标签
#原始图片所在目录
source_images_dir = './JPEGImages'
#原始标注所在目录
source_labels_dir = './Annotations'
#目标目录
dest_dir = './datasets/sar-aircraft-1.0'
#目标标注目录
dest_labels_dir = os.path.join(dest_dir, "labels")
#目标图片目录
dest_images_dir = os.path.join(dest_dir, "images")
#测试集占比20%
test_ratio = 20

def split_array(array, percentage=20):
    """
        将一个数组随机拆分成2个子数组,其中一个子组数占比20%的数据量,另外一个子数组占比80%的数据量
    """
    # 计算要拆分的数量
    total_length = len(array)
    split_index = int(total_length * (percentage / 100.0))
    
    # 随机打乱数组
    shuffled_array = random.sample(array, total_length)
    
    # 拆分数组
    small_array = shuffled_array[:split_index]
    large_array = shuffled_array[split_index:]
    
    return small_array, large_array

def get_file_name_without_extension(file_path):
    """
        获取文件名
    """
    file_name_with_extension = os.path.basename(file_path)
    file_name, file_extension = os.path.splitext(file_name_with_extension)
    
    return file_name

def copy_random_files(source_labels_dir,source_images_dir, dest_labels_dir,dest_images_dir, test_sets_percentage=20,test_sets_total_num=5):
    """
        随机从一个目录,复制%20的文件到另外一个目录
        source_labels_dir:源标注文件所在目录
        source_images_dir:源图片文件所在目录
        dest_labels_dir:目标标注目录
        dest_images_dir:目标图片目录
        test_sets_percentage: 测试数据集占比,默认20%
        test_sets_total_num: 测试数据集最大数量,默认5个文件,如果小于0,代表全量数据集
    """
    if test_sets_total_num == 0:
        print("测试数据集最大数量等于0")
        return 
    # 确保目标目录存在,如果不存在则创建
    if not os.path.exists(dest_labels_dir):
        os.makedirs(dest_labels_dir)
        
    # 确保目标目录存在,如果不存在则创建
    if not os.path.exists(dest_images_dir):
        os.makedirs(dest_images_dir)

    # 目标目录需要是空的 
    dest_label_files = [f for f in os.listdir(dest_labels_dir) if not f.startswith('.')]
    if len(dest_label_files) > 0:
        print(f"目标目录:{dest_labels_dir}下存在文件,终止拷贝程序!!!")
        return
    dest_images_files = [f for f in os.listdir(dest_images_dir) if not f.startswith('.')]
    if len(dest_images_files) > 0:
        print(f"目标目录:{dest_images_dir}下存在文件,终止拷贝程序!!!")
        return
    
    # 获取源目录中的所有标签文件
    source_label_files = [f for f in os.listdir(source_labels_dir) if os.path.isfile(os.path.join(source_labels_dir, f))]
    
    # 将文件随机拆分成,测试集和训练集
    label_files_test,label_files_train = split_array(source_label_files, test_sets_percentage)
    # 控制复制多少张
    if test_sets_total_num > 0 and len(label_files_test) > test_sets_total_num:
        label_files_test = label_files_test[:test_sets_total_num]
        label_files_train = label_files_train[:int(((100-test_sets_percentage)/test_sets_percentage)*test_sets_total_num)]

    # 复制 测试集文件
    dest_label_test_dir = os.path.join(dest_labels_dir, "val")
    if not os.path.exists(dest_label_test_dir):
        os.makedirs(dest_label_test_dir)
    dest_image_test_dir = os.path.join(dest_images_dir, "val")
    if not os.path.exists(dest_image_test_dir):
        os.makedirs(dest_image_test_dir)
        
    for file_name in label_files_test:
        if file_name.startswith('.'):
            continue
        # 首先复制标签文件
        source_file = os.path.join(source_labels_dir, file_name)
        destination_file = os.path.join(dest_label_test_dir, file_name)
        shutil.copy2(source_file, destination_file)
        # 再复制图片文件
        file_name = get_file_name_without_extension(source_file) + ".jpg"
        source_file = os.path.join(source_images_dir, file_name)
        destination_file = os.path.join(dest_image_test_dir, file_name)
        shutil.copy2(source_file, destination_file)
    
    # 复制 训练集文件
    dest_label_train_dir = os.path.join(dest_labels_dir, "train")
    if not os.path.exists(dest_label_train_dir):
        os.makedirs(dest_label_train_dir)
    dest_image_train_dir = os.path.join(dest_images_dir, "train")
    if not os.path.exists(dest_image_train_dir):
        os.makedirs(dest_image_train_dir)
        
    for file_name in label_files_train:
        if file_name.startswith('.'):
            continue
        # 首先复制标签文件
        source_file = os.path.join(source_labels_dir, file_name)
        destination_file = os.path.join(dest_label_train_dir, file_name)
        shutil.copy2(source_file, destination_file)
        # 再复制图片文件
        file_name = get_file_name_without_extension(source_file) + ".jpg"
        source_file = os.path.join(source_images_dir, file_name)
        destination_file = os.path.join(dest_image_train_dir, file_name)
        shutil.copy2(source_file, destination_file)

# 测试集总样本数量,对应的训练集样本数量 = test_sets_total_num*(100/test_ratio - 1)
test_sets_total_num = 800
# 拷贝标签文件
copy_random_files(source_labels_dir,source_images_dir, dest_labels_dir,dest_images_dir, 
                    test_sets_percentage=test_ratio,test_sets_total_num=test_sets_total_num)

2.3 VOC格式转换成为yolo格式

将xml格式转换成为txt格式,为什么要转?以及转换文件的格式是什么样的?可以看看这篇介绍《计算机视觉:经典数据格式(VOC、YOLO、COCO)解析与转换(附代码)

#飞机的分类-索引创建
label2idx = {"A330": 0, 
             "A320/321": 1, 
             "A220": 2, 
             "ARJ21":3, 
             "Boeing737": 4, 
             "Boeing787":5, 
             "other":6}

def voc_to_yolo(file_name):
    """
        把VOC格式转为yolo格式
    """
    tree = ElementTree.parse(source=file_name)
    root = tree.getroot()
    # 图像宽度和高度
    img_width = int(root.find(path="size").find(path="width").text)
    img_height = int(root.find(path="size").find(path="height").text)
    with open(file=file_name.replace(".xml", ".txt"), mode="w", encoding="utf8") as f:
        # 遍历每个目标
        for obj in root.findall(path="object"):
            name = obj.find("name").text
            cls_id = label2idx.get(name)
            xmin = int(obj.find("bndbox").find("xmin").text)
            ymin = int(obj.find("bndbox").find("ymin").text)
            xmax = int(obj.find("bndbox").find("xmax").text)
            ymax = int(obj.find("bndbox").find("ymax").text)
            # 中心点x
            x_center = round(number=(xmin + xmax) / 2 / img_width, ndigits=6)
            y_center = round(number=(ymin + ymax) / 2 / img_height, ndigits=6)
            box_width = round(number=(xmax - xmin) / img_width, ndigits=6)
            box_height = round(number=(ymax - ymin) / img_height, ndigits=6)
            print(cls_id, x_center, y_center, box_width, box_height, sep=" ", end="\n", file=f)

#遍历标注的测试集目录
labels_test_sets_dir = os.path.join(dest_labels_dir, "val")
for f in os.listdir(labels_test_sets_dir):
    if f.startswith('.'):
        continue
    voc_to_yolo(os.path.join(labels_test_sets_dir, f))

#遍历标注的训练集目录
labels_train_sets_dir = os.path.join(dest_labels_dir, "train")
for f in os.listdir(labels_train_sets_dir):
    if f.startswith('.'):
        continue
    voc_to_yolo(os.path.join(labels_train_sets_dir, f))

三、训练模型

# 加载模型
model = YOLO("yolo11n.pt") 
# 训练模型
results = model.train(data="sar_aircraft.yaml", epochs=10, imgsz=1024,batch=16)

sar_aircraft.yaml文件内容如下:

# Ultralytics YOLO 🚀, AGPL-3.0 license
# COCO8 dataset (first 8 images from COCO train2017) by Ultralytics
# Documentation: https://docs.ultralytics.com/datasets/detect/coco8/
# Example usage: yolo train data=coco8.yaml
# parent
# ├── ultralytics
# └── datasets
#     └── coco8  ← downloads here (1 MB)

# Train/val/test sets as 1) dir: path/to/imgs, 2) file: path/to/imgs.txt, or 3) list: [path/to/imgs1, path/to/imgs2, ..]
path: /Users/xxx/ultralytics/datasets/sar-aircraft-1.0 # dataset root dir
train: images/train # train images (relative to 'path') 4 images
val: images/val # val images (relative to 'path') 4 images
test: # test images (optional)

# Classes
names:
  0: A330
  1: A320/321
  2: A220
  3: ARJ21
  4: Boeing737
  5: Boeing787
  6: other
  • path:指定训练模型的数据集的根目录
  • train:指定训练集目录,相对于path指定的根目录
  • val:指定验证集目录,相对于path指定的根目录
  • names:指定索引以及索引对应的飞机类型

模型训练过程日志:

Ultralytics 8.3.36 🚀 Python-3.10.14 torch-2.3.1+cu121 CUDA:0 (NVIDIA A10, 22732MiB)
engine/trainer: task=detect, mode=train, model=yolo11n.pt, data=sar_aircraft.yaml, epochs=10, time=None, patience=100, batch=16, imgsz=1024, save=True, save_period=-1, cache=False, device=None, workers=8, project=None, name=train4, exist_ok=False, pretrained=True, optimizer=auto, verbose=True, seed=0, deterministic=True, single_cls=False, rect=False, cos_lr=False, close_mosaic=10, resume=False, amp=True, fraction=1.0, profile=False, freeze=None, multi_scale=False, overlap_mask=True, mask_ratio=4, dropout=0.0, val=True, split=val, save_json=False, save_hybrid=False, conf=None, iou=0.7, max_det=300, half=False, dnn=False, plots=True, source=None, vid_stride=1, stream_buffer=False, visualize=False, augment=False, agnostic_nms=False, classes=None, retina_masks=False, embed=None, show=False, save_frames=False, save_txt=False, save_conf=False, save_crop=False, show_labels=True, show_conf=True, show_boxes=True, line_width=None, format=torchscript, keras=False, optimize=False, int8=False, dynamic=False, simplify=True, opset=None, workspace=4, nms=False, lr0=0.01, lrf=0.01, momentum=0.937, weight_decay=0.0005, warmup_epochs=3.0, warmup_momentum=0.8, warmup_bias_lr=0.1, box=7.5, cls=0.5, dfl=1.5, pose=12.0, kobj=1.0, label_smoothing=0.0, nbs=64, hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, degrees=0.0, translate=0.1, scale=0.5, shear=0.0, perspective=0.0, flipud=0.0, fliplr=0.5, bgr=0.0, mosaic=1.0, mixup=0.0, copy_paste=0.0, copy_paste_mode=flip, auto_augment=randaugment, erasing=0.4, crop_fraction=1.0, cfg=None, tracker=botsort.yaml, save_dir=runs/detect/train4
Downloading https://ultralytics.com/assets/Arial.ttf to '/root/.config/Ultralytics/Arial.ttf'...
100%|██████████| 755k/755k [03:20<00:00, 3.86kB/s]
Overriding model.yaml nc=80 with nc=7

                   from  n    params  module                                       arguments                     
  0                  -1  1       464  ultralytics.nn.modules.conv.Conv             [3, 16, 3, 2]                 
  1                  -1  1      4672  ultralytics.nn.modules.conv.Conv             [16, 32, 3, 2]                
  2                  -1  1      6640  ultralytics.nn.modules.block.C3k2            [32, 64, 1, False, 0.25]      
  3                  -1  1     36992  ultralytics.nn.modules.conv.Conv             [64, 64, 3, 2]                
  4                  -1  1     26080  ultralytics.nn.modules.block.C3k2            [64, 128, 1, False, 0.25]     
  5                  -1  1    147712  ultralytics.nn.modules.conv.Conv             [128, 128, 3, 2]              
  6                  -1  1     87040  ultralytics.nn.modules.block.C3k2            [128, 128, 1, True]           
  7                  -1  1    295424  ultralytics.nn.modules.conv.Conv             [128, 256, 3, 2]              
  8                  -1  1    346112  ultralytics.nn.modules.block.C3k2            [256, 256, 1, True]           
  9                  -1  1    164608  ultralytics.nn.modules.block.SPPF            [256, 256, 5]                 
 10                  -1  1    249728  ultralytics.nn.modules.block.C2PSA           [256, 256, 1]                 
 11                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          
 12             [-1, 6]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           
 13                  -1  1    111296  ultralytics.nn.modules.block.C3k2            [384, 128, 1, False]          
 14                  -1  1         0  torch.nn.modules.upsampling.Upsample         [None, 2, 'nearest']          
 15             [-1, 4]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           
 16                  -1  1     32096  ultralytics.nn.modules.block.C3k2            [256, 64, 1, False]           
 17                  -1  1     36992  ultralytics.nn.modules.conv.Conv             [64, 64, 3, 2]                
 18            [-1, 13]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           
 19                  -1  1     86720  ultralytics.nn.modules.block.C3k2            [192, 128, 1, False]          
 20                  -1  1    147712  ultralytics.nn.modules.conv.Conv             [128, 128, 3, 2]              
 21            [-1, 10]  1         0  ultralytics.nn.modules.conv.Concat           [1]                           
 22                  -1  1    378880  ultralytics.nn.modules.block.C3k2            [384, 256, 1, True]           
 23        [16, 19, 22]  1    432037  ultralytics.nn.modules.head.Detect           [7, [64, 128, 256]]           
YOLO11n summary: 319 layers, 2,591,205 parameters, 2,591,189 gradients, 6.4 GFLOPs

Transferred 448/499 items from pretrained weights
TensorBoard: Start with 'tensorboard --logdir runs/detect/train4', view at http://localhost:6006/
Freezing layer 'model.23.dfl.conv.weight'
AMP: running Automatic Mixed Precision (AMP) checks...
AMP: checks passed ✅
train: Scanning /mnt/workspace/datasets/sar-aircraft-1.0/labels/train.cache... 3200 images, 0 backgrounds, 0 corrupt: 100%|██████████| 3200/3200 [00:00<?, ?it/s]
albumentations: Blur(p=0.01, blur_limit=(3, 7)), MedianBlur(p=0.01, blur_limit=(3, 7)), ToGray(p=0.01, num_output_channels=3, method='weighted_average'), CLAHE(p=0.01, clip_limit=(1.0, 4.0), tile_grid_size=(8, 8))
/usr/local/lib/python3.10/site-packages/albumentations/check_version.py:51: UserWarning: Error fetching version info <urlopen error _ssl.c:990: The handshake operation timed out>
  data = fetch_version_info()
val: Scanning /mnt/workspace/datasets/sar-aircraft-1.0/labels/val.cache... 800 images, 0 backgrounds, 0 corrupt: 100%|██████████| 800/800 [00:00<?, ?it/s]
Plotting labels to runs/detect/train4/labels.jpg... 
optimizer: 'optimizer=auto' found, ignoring 'lr0=0.01' and 'momentum=0.937' and determining best 'optimizer', 'lr0' and 'momentum' automatically... 
optimizer: AdamW(lr=0.000909, momentum=0.9) with parameter groups 81 weight(decay=0.0), 88 weight(decay=0.0005), 87 bias(decay=0.0)
TensorBoard: model graph visualization added ✅
Image sizes 1024 train, 1024 val
Using 8 dataloader workers
Logging results to runs/detect/train4
Starting training for 10 epochs...
Closing dataloader mosaic
albumentations: Blur(p=0.01, blur_limit=(3, 7)), MedianBlur(p=0.01, blur_limit=(3, 7)), ToGray(p=0.01, num_output_channels=3, method='weighted_average'), CLAHE(p=0.01, clip_limit=(1.0, 4.0), tile_grid_size=(8, 8))

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       1/10      7.08G      1.995      4.726      1.576         48       1024: 100%|██████████| 200/200 [00:39<00:00,  5.09it/s]
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:07<00:00,  3.36it/s]
                   all        800       3049      0.281       0.23      0.105     0.0472


      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       2/10      6.57G      1.727      3.382      1.417         38       1024: 100%|██████████| 200/200 [00:35<00:00,  5.70it/s]
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:04<00:00,  5.39it/s]
                   all        800       3049      0.191      0.382      0.203      0.102

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       3/10      6.58G      1.657      2.688      1.396         47       1024: 100%|██████████| 200/200 [00:34<00:00,  5.74it/s]
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:04<00:00,  5.47it/s]
                   all        800       3049      0.327      0.426      0.329      0.176

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       4/10      6.56G      1.581      2.148      1.362         50       1024: 100%|██████████| 200/200 [00:34<00:00,  5.77it/s]
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:04<00:00,  5.52it/s]
                   all        800       3049      0.473      0.505      0.476      0.258

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       5/10       6.6G      1.536      1.816      1.332         43       1024: 100%|██████████| 200/200 [00:34<00:00,  5.77it/s]
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:04<00:00,  5.67it/s]
                   all        800       3049      0.686      0.603      0.646      0.372

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       6/10      6.62G      1.463      1.535       1.29         56       1024: 100%|██████████| 200/200 [00:34<00:00,  5.77it/s]
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:04<00:00,  5.73it/s]
                   all        800       3049      0.722      0.687      0.767      0.448

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       7/10      6.67G      1.403      1.334      1.253         61       1024: 100%|██████████| 200/200 [00:34<00:00,  5.76it/s]
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:04<00:00,  5.71it/s]
                   all        800       3049      0.783      0.748      0.825      0.505

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       8/10      6.64G      1.337      1.166      1.223         82       1024: 100%|██████████| 200/200 [00:34<00:00,  5.76it/s]
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:04<00:00,  5.72it/s]
                   all        800       3049      0.844      0.834      0.904       0.57

      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
       9/10      6.56G      1.287      1.076      1.191         33       1024: 100%|██████████| 200/200 [00:34<00:00,  5.77it/s]
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:04<00:00,  5.73it/s]
                   all        800       3049      0.864      0.876      0.925       0.59


      Epoch    GPU_mem   box_loss   cls_loss   dfl_loss  Instances       Size
      10/10      6.56G      1.224     0.9877      1.158         53       1024: 100%|██████████| 200/200 [00:34<00:00,  5.77it/s]
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:04<00:00,  5.67it/s]
                   all        800       3049      0.874      0.913      0.948      0.623


10 epochs completed in 0.114 hours.
Optimizer stripped from runs/detect/train4/weights/last.pt, 5.6MB
Optimizer stripped from runs/detect/train4/weights/best.pt, 5.6MB

Validating runs/detect/train4/weights/best.pt...
Ultralytics 8.3.36 🚀 Python-3.10.14 torch-2.3.1+cu121 CUDA:0 (NVIDIA A10, 22732MiB)
YOLO11n summary (fused): 238 layers, 2,583,517 parameters, 0 gradients, 6.3 GFLOPs
                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:05<00:00,  4.67it/s]
                   all        800       3049      0.873      0.914      0.948      0.623
                  A330         53         55      0.893      0.891       0.96      0.603
              A320/321        161        332      0.824      0.898       0.92      0.621
                  A220        385        705      0.846      0.863       0.92      0.621
                 ARJ21        128        195      0.884      0.979       0.97      0.603
             Boeing737        255        458      0.899      0.953      0.969      0.658
             Boeing787        322        535      0.916      0.942      0.968      0.657
                 other        381        769      0.852      0.869      0.928      0.601
Speed: 0.2ms preprocess, 2.2ms inference, 0.0ms loss, 0.9ms postprocess per image
Results saved to runs/detect/train4

3.1 训练结果说明

在训练YOLO模型后,您会看到类似上述的输出结果。这些指标提供了关于模型性能的重要信息,包括精确度、召回率和平均精度等关键评估标准

                 Class     Images  Instances      Box(P          R      mAP50  mAP50-95): 100%|██████████| 25/25 [00:05<00:00,  4.67it/s]
                   all        800       3049      0.873      0.914      0.948      0.623
                  A330         53         55      0.893      0.891       0.96      0.603
              A320/321        161        332      0.824      0.898       0.92      0.621
                  A220        385        705      0.846      0.863       0.92      0.621
                 ARJ21        128        195      0.884      0.979       0.97      0.603
             Boeing737        255        458      0.899      0.953      0.969      0.658
             Boeing787        322        535      0.916      0.942      0.968      0.657
                 other        381        769      0.852      0.869      0.928      0.601
Speed: 0.2ms preprocess, 2.2ms inference, 0.0ms loss, 0.9ms postprocess per image
Results saved to runs/detect/train4

总体统计(all

  • Class: 类别名称。all表示所有类别的综合表现。
  • Images: 参与评估的图像总数(800张)。
  • Instances: 图像中目标实例的总数(3049个)。
  • Box(P): 边界框的精确度(Precision),即正确预测的目标占所有预测目标的比例。对于所有类别,该值为0.873。
  • R: 召回率(Recall),即正确预测的目标占所有实际目标的比例。对于所有类别,该值为0.914。
  • mAP50: 平均精度(Average Precision),当IoU阈值设置为0.5时的平均精度。对于所有类别,该值为0.948。
  • mAP50-95: 当IoU阈值从0.5变化到0.95时的平均精度。对于所有类别,该值为0.623。

每个类别的具体表现

下面是对不同飞机类型的具体表现分析:

  • A330:

    • 精确度(P):0.893
    • 召回率(R):0.891
    • mAP50:0.96
    • mAP50-95:0.603
  • A320/321:

    • P:0.824
    • R:0.898
    • mAP50:0.92
    • mAP50-95:0.621
  • ......

3.2 性能指标说明

  • 精确度(Precision, P):衡量模型在所有被识别为目标的对象中有多少是真正正确的。高精确度意味着较少的误报。
    • Precision 精准率 = TP / (TP + FP),
    • 例如:精确率80%表示一堆样本中,模型预测出100个正例,这100个正例中有80个是真实的正例,
    • 换句话说误报率是20%,也就是说模型预测出的100个正例里,有20个是预测错了,
    • 准确率越高,误报率就会越低,准确率越低,误报率就会越高
  • 召回率(Recall, R):衡量模型成功找到的所有真实目标的比例。高召回率意味着较少的漏报。
    • 例如:召回率80%表示一堆样本中,有100个正例,模型预测出90个正例,这90个正例中有80个是真实的正例,
    • 换句话说误漏报率是20%,也就是说模型预测出的正例中漏掉了20个正例,召回率越高,漏掉的数量就越少,召回率越低,漏掉的数量就会多
  • mAP: Mean Average Precision 平均准确率,mAP50:50表示真实框的面积和预测框的面积两者的交集占并集的比例,简称交并比超过50%,怎么样才算目标分类预测正确?
    • 预测正确:分类正确并且交并比超过50%才算是预测正确
    • 预测错误:分类预测错误算预测错误,分类预测正确但是交并比小于50%也算预测错误
  • mAP50mAP50-95:这两个指标分别代表了不同交并比(IoU)阈值下的平均精度。mAP50使用的是IoU=0.5作为阈值,而mAP50-95则考虑了从0.5到0.95的不同阈值,以提供更全面的模型性能视图。较高的mAP值通常表明模型具有更好的泛化能力。

3.3 处理速度

  • Speed: 表明每张图片处理所需的时间分布:
    • 预处理时间:0.2毫秒
    • 推理时间:2.2毫秒
    • 损失计算时间:0毫秒
    • 后处理时间:0.9毫秒

这意味着整个过程平均每张图片大约需要3.3毫秒完成,这显示了YOLOv11模型在保持高性能的同时也具备良好的实时性。

根据上述结果,可以看出模型在遥感图像中的飞机检测任务上表现出色,特别是在Boeing737和Boeing787这两种类型的飞机上达到了非常高的mAP50值(分别为0.969和0.968)。尽管在某些类别上的mAP50-95略低,但整体来看,这个模型已经能够有效地执行其设计的任务。此外,快速的处理速度也使得它非常适合实时应用。

3.4 对比不同参数量的模型

使用不同参数量的模型进行训练

模型名称训练轮次Box(P)RmAP50mAP50-95训练时长(小时)模型概要
yolo11n.pt100.8740.9130.9480.6230.114238层, 2,583,517参数, 0梯度, 6.3 GFLOPs
yolo11n.pt200.8880.9210.960.6680.213238层, 2,583,517参数, 0梯度, 6.3 GFLOPs
yolo11s.pt100.8790.9040.9520.6550.177238层, 9,415,509参数, 0梯度, 21.3 GFLOPs
yolo11m.pt100.8710.9150.9520.6620.367303层, 20,035,429参数, 0梯度, 67.7 GFLOPs

选用不同的参数量的模型,以及训练不同的轮次,从统计结果来看,yolo11n.pt的参数量是2.6M,训练 20 次比 10 次Box§、R、mAP50、mAP50-95指标均有所提高,但提高不多,同时训练时长也增加了一倍,

使用参数量更大的模型yolo11s.pt 和yolo11m.pt,训练结果并没有比yolo11n.pt的结果更好,训练时长大幅增加。

所以参数量更大的模型训练效果不一定就比参数量小的模型效果更好,可以多尝试,从准确率各项指标和训练时长平衡决策。

四、模型预测

将上面训练好的模型部署到服务端,使用框架streamlit做一个简单的页面,在页面上可以选择一张图片,提交图片给模型,让模型预测并标注出飞机的类型以及飞机所在位置。

Streamlit 是一个用于快速创建和部署机器学习与数据科学应用的开源框架。它使得开发者可以通过编写Python脚本的方式轻松构建出交互性强的Web应用程序,而无需深入了解前端开发技术(如HTML、CSS或JavaScript)。本地安装命令 pip install streamlit
在这里插入图片描述

import json
import os

import streamlit as st
from xml.etree import ElementTree
from PIL import Image, ImageDraw, ImageFont
from ultralytics import YOLO
# 从本地加载训练好的模型
model = YOLO("/Users/xxx/Documents/SAR-AIRcraft-1.0/detect/train4/weights/best.pt")
# st.set_page_config(layout="wide")
st.title('遥感飞机目标检测')

def voc_to_yolo(file_name):
    """
        把VOC格式转为yolo格式
    """
    tree = ElementTree.parse(source=file_name)
    root = tree.getroot()
    # 图像宽度和高度
    img_width = int(root.find(path="size").find(path="width").text)
    img_height = int(root.find(path="size").find(path="height").text)
    airplane_data = []
    with open(file=file_name, mode="r", encoding="utf8") as f:
        # 遍历每个目标
        for obj in root.findall(path="object"):
            name = obj.find("name").text
            xmin = int(obj.find("bndbox").find("xmin").text)
            ymin = int(obj.find("bndbox").find("ymin").text)
            xmax = int(obj.find("bndbox").find("xmax").text)
            ymax = int(obj.find("bndbox").find("ymax").text)
            airplane_data.append("{"+f'"name": "{name}", "xmin": {xmin}, "ymin": {ymin}, "xmax": {xmax}, "ymax": {ymax}'+"}")
    return airplane_data

# 创建一个file uploader,并命名为'file'
uploaded_file = st.file_uploader("选择遥感图片",type=["jpg","xml"])
local_file_path = ''
if uploaded_file is not None:
    # 获取上传文件的文件名和文件类型
    filename = uploaded_file.name
    filetype = filename.split('.')[-1]
    # 构建本地文件路径
    if filetype == 'xml':
        local_file_path = f"./upload/0000001.xml"
    elif filetype == 'jpg':
        local_file_path = f"./upload/0000001.jpg"
    else:
        st.write("不支持的文件格式!")
    # 使用with语句确保文件正确关闭
    with open(local_file_path, 'wb') as file:
        # 将上传的文件内容写入本地文件
        file.write(uploaded_file.getbuffer())

    st.success(f"文件 '{filename}' 上传完成!")
else:
    st.info("上传一张JPG图片;上传标注XML文件,坐标为VOC格式")

def orig():
    """
        原始标注
    """
    # 在图片上标注飞机
    # 假设你有一个图片路径和飞机的坐标及名称
    image_path = './upload/0000001.jpg'
    orig_annot = "./upload/0000001.xml"
    if not os.path.exists(image_path):
        st.write("请先上传jpg格式的原始图片!")
        return False
    if not os.path.exists(orig_annot):
        st.write("请先上传VOC格式的标注XML文件!")
        return False
    airplane_data = voc_to_yolo(orig_annot)
    # 加载图片
    image = Image.open(image_path)
    # 转换为RGB模式
    if image.mode != 'RGB':
        image = image.convert('RGB')
    # 创建绘图对象
    draw = ImageDraw.Draw(image)
    # 设置字体和字体大小
    try:
    	# 尝试加载特定字体,这个字体库从网上可以下载
        font = ImageFont.truetype("/Users/xxx/Documents/Arial.ttf", 60)  
    except IOError:
        font = ImageFont.load_default()  # 如果找不到特定字体,使用默认字体
    # 绘制方框和标签
    for plane in airplane_data:
        plane = json.loads(plane)
        xmin, ymin, xmax, ymax = plane['xmin'], plane['ymin'], plane['xmax'], plane['ymax']
        name = plane['name']
        # 文字颜色和背景颜色
        text_color = (255, 255, 255)  # 红色
        background_color = (255, 0, 0)  # 绿色
        # 绘制边界框
        draw.rectangle([xmin, ymin, xmax, ymax], outline=(255, 0, 0), width=3)  # 红色边框
        # 计算文字位置
        text_position = (xmin, ymin - 25)  # 假设文字放在边界框上方
        # 使用 textbbox 获取文本尺寸
        bbox = draw.textbbox((0, 0), name, font=font)
        text_width = bbox[2] - bbox[0]
        text_height = bbox[3] - bbox[1] + 5
        # 绘制文字背景
        draw.rectangle([text_position, (text_position[0] + text_width, text_position[1] + text_height)],
                       fill=background_color)
        # 绘制文字
        draw.text(text_position, name, fill=text_color, font=font)
    # 显示图片
    # st.image(image, caption='原始标注', use_column_width=True)
    # 保存图片到文件
    image.save('./upload/orig_annot.jpg', 'PNG')
    image.close()

def modelPredict():
    image_path = './upload/0000001.jpg'
    if not os.path.exists(image_path):
        print("预测的图片不存在!")
        return False
    results = model(image_path)
    # print(f"模型预测结果:{results}")
    # Process results list
    for result in results:
        boxes = result.boxes  # Boxes object for bounding box outputs
        masks = result.masks  # Masks object for segmentation masks outputs
        keypoints = result.keypoints  # Keypoints object for pose outputs
        probs = result.probs  # Probs object for classification outputs
        obb = result.obb  # Oriented boxes object for OBB outputs
        # result.show()  # display to screen
        result.save(filename="./upload/pred_annot.jpg")  # save to disk

def show_image():
    orig_annot = './upload/orig_annot.jpg'
    if not os.path.exists(orig_annot):
        print("原始标注图片不存在!")
        return False
    pred_annot = './upload/pred_annot.jpg'
    if not os.path.exists(pred_annot):
        print("预测结果图片不存在!")
        return False
    # 加载图片
    image1 = Image.open(orig_annot)  # 替换为您的第一张图片路径
    image2 = Image.open(pred_annot)  # 替换为您的第二张图片路径
    # 使用st.columns创建两列布局
    col1, col2 = st.columns(2)
    # 在第一列中显示第一张图片
    with col1:
        st.image(image1, caption='原始标注', use_column_width=True)
    # 在第二列中显示第二张图片
    with col2:
        st.image(image2, caption='模型预测', use_column_width=True)

dect_button = st.button("目标检测", type="secondary")
if dect_button:
    #原始标注
    orig()
    #模型标注
    modelPredict()
    # 显示对比
    show_image()

通过如下命令,运行上面的 Python 代码,假设代码所在文件为st_web.py

streamlit run st_web.py
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值