(负样本+参数调整+预处理模型-缩放+引入新的模块机制)

(目前我只会这几种方法,后续学到了新的方法会更新博客)

1.数据集的好坏程度决定模型性能的上限,即使参数调整的再好,数据集质量太低,也会导致训练结果出现偏差。所以我们在选择数据集或者自己标注数据集时一定要注意:

标注准确性:确保边界框紧贴目标边缘

标签一致性:同一目标在不同图像中标注方式一致

剔除错误标注:删除明显的错误标注样本
数据增强要适当

2.可以适当添加负样本,降低误检率

这里解释一下什么是负样本:负样本 = 没有目标的图片,负样本就是告诉模型:"这些地方没有横幅,别瞎猜!"

我们在没添加负样本时:
模型学习:横幅 → 横幅特征

结果:容易把相似的纹理/形状误判为横幅

添加负样本后:
模型学习:

1. 横幅 → 横幅特征

2. 非横幅 → 这不是横幅!

结果:减少误判,提高精确率

知道什么是负样本之后,该怎么收集负样本呢?

从这些地方找:

  1. 你的数据集环境:同一场景但没有横幅的图片
  2. 容易误检的区域:模型之前误检过的地方截图
  3. 相似纹理:与横幅颜色/纹理相似的背景(墙壁、海报栏、窗户)
  4. 负样本来源
    • 从视频中截取无横幅的帧
    • 从网上找类似场景图片

负样本数量不宜过多,一千张图片有个最多100张的负样本就差不多了,太多了的话会让模型"畏手畏脚",连真正的目标也不敢检测。

找到图片之后,对负样本进行标注!!!,负样本也是需要标注的!我们在训练模型时每个图片都需要有对应的标签,只是负样本的标签是空白,但是也要有一个.txt的文件和图片对应。

然后分别将负样本图片和标注加入images-trian和labels-train中

(为了避免后续不想要负样本了,可以提前将没加入负样本的数据集备份一个)

添加负样本后,训练脚本不需要额外设置


下面是我在生成横幅检测模型时遇到的问题:
这是模型正在检测视频时的截图,可以看到目前还算正常

下面是无人机飞离横幅之后拍摄到跑道后,模型把红色的跑道误检成横幅

因为,数据集中的横幅大部分符合红色的长方形,所以导致模型误检,现在我们加入负样本,看看模型性能能不能得到提升。

在images中的train中加入自己找的不包含目标的照片。
 

因为数据集中的横幅多是红色且长条的形状,所以这里的负样本我找了很多类似横幅特征的图片。

再让AI写一个基于图片名字创建空白.txt文档的脚本。

全是0字节的。

下面是没添加负样本和添加了负样本的训练结果:

可以看到添加了负样本的模型的map50,精确率,召回率都有所降低,但是都不多(可能是我添加的负样本不是很多的原因),其实按理来说应该只有召回率降低一些,map50和精确率应该上升一点,不过没关系,零点零几的改变并没有什么太大差距,我们更应该关注模型在预测时的实际变化,将模型导入预测脚本,

可以看到,在观察真正的横幅时,模型还是可以正常预测的:

我们再继续播放,无人机已经飞到之前的红色跑道的地方:

此时模型并没有和之前一样误检,说明添加负样本还是很有用的。

3.一些训练参数的设置

损失函数的优化:可以根据自己的目的进行侧重

 权重调整:

增加 box_loss 权重 → 提升定位精度

调整 cls_loss 权重 → 改善分类能力

优化 dfl_loss → 改善边界框回归

 Focal Loss:关注难分类样本,减少易分类样本影响

还有就是我们在训练时的评估参数:

其中注意我们的conf和iou参数的设置,

我在进行横幅检测时,由于训练脚本是让AI帮忙配置的,而且没有细看,他给我的conf设置为0.001,这导致我的模型的精确率和召回率虚高,下面的图片是我用视频对模型进行预测时出现的情况,可以看到除了真正的横幅,模型还把路沿认成了横幅。

除了训练脚本中的conf我们需要设置,我们在对视频进行预测时还需要设置conf,

适当提高我们对视频进行预测的conf也可以提高检测结果,但是瑕不掩瑜,认真调整训练脚本的参数才是关键,在这个AI盛行的年代,确实方便了我们的学习,不过最重要的是要有甄别的能力,AI肯定可以用,但是不能完全依赖它,这么简单的错误,训练了一上午最后预测结果这么差,真是血的教训。

4.预处理数据集-缩放

因为我现在需要做的是通过无人机在空中检测横幅,但是在网上搜索的很多资料都是正面且怼脸拍摄的横幅,所以必须要无人机在空中放大之后模型才能检测到横幅,但是有因为条件有限,主要是因为没钱,没有无人机俯拍横幅的数据集,有也很少,肯定不够支持训练模型,用来验证都够呛,只能通过缩放图片大小来尽量模拟无人机俯拍的情况。

第一步:我们让AI写一个图片缩放可视化的脚本,可以自己设置缩放scale参数,观察生成的图片的大小,选择符合自己项目要求的scale。
可视化缩放脚本:

######################################################

#该脚本用来可视化数据集的增强效果,模拟无人机俯拍时的视角

######################################################
import cv2
import numpy as np
import random
import os
from pathlib import Path
import matplotlib.pyplot as plt

# ====== 配置参数(修改这里) ======
IMAGE_PATH = r"C:\Users\JAMES HARDEN\Desktop\横幅数据集\横幅数据集\images\train"
OUTPUT_DIR = r"D:\yoloV12\yolov12-main\yolov12-main\OutPut\overhead_simulation"
IMGSZ = 640  # 输出图像大小
SAMPLE_COUNT = 5  # 随机抽取的图片数量

# 固定三个缩放值
FIXED_SCALES = [0.08, 0.09, 0.10]  # 修改这三个值即可
# ===============================

def resize_image(img, scale):
    """按指定比例缩放图片"""
    h, w = img.shape[:2]
    new_w = int(w * scale)
    new_h = int(h * scale)
    
    # 确保最小尺寸
    new_w = max(10, new_w)
    new_h = max(10, new_h)
    
    # 缩放图片
    img_resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
    
    return img_resized, scale

def add_text_to_image(img, text, position=(10, 30), font_scale=0.6, thickness=2):
    """在图像上添加文字"""
    img_with_text = img.copy()
    font = cv2.FONT_HERSHEY_SIMPLEX
    color = (0, 255, 0)  # 绿色文字
    
    # 添加文字
    cv2.putText(img_with_text, text, position, font, font_scale, color, thickness)
    
    return img_with_text

def visualize_augmentations():
    """可视化无人机俯拍效果"""
    # 创建输出目录
    os.makedirs(OUTPUT_DIR, exist_ok=True)
    
    # 获取所有图片文件
    image_extensions = ['.jpg', '.jpeg', '.png', '.bmp']
    image_files = []
    
    if os.path.isfile(IMAGE_PATH):
        image_files = [IMAGE_PATH]
    elif os.path.isdir(IMAGE_PATH):
        for ext in image_extensions:
            image_files.extend([str(p) for p in Path(IMAGE_PATH).glob(f'*{ext}')])
    
    if not image_files:
        print(f"未找到图片文件: {IMAGE_PATH}")
        return
    
    print(f"找到 {len(image_files)} 张图片")
    print(f"固定缩放值: {FIXED_SCALES}")
    
    # 随机抽取指定数量的图片
    if len(image_files) > SAMPLE_COUNT:
        selected_images = random.sample(image_files, SAMPLE_COUNT)
    else:
        selected_images = image_files
    
    # 处理随机抽取的图片
    for i, img_path in enumerate(selected_images):
        print(f"处理图片 {i+1}: {os.path.basename(img_path)}")
        
        try:
            # 读取图片
            with open(img_path, 'rb') as f:
                img_array = np.frombuffer(f.read(), np.uint8)
                img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
            
            if img is None:
                print(f"  无法读取图片: {img_path}")
                continue
            
            original_h, original_w = img.shape[:2]
            
            # 创建空白画布
            result_height = IMGSZ
            result_width = IMGSZ * 4  # 原始图片 + 3种缩放效果
            
            result_canvas = np.zeros((result_height, result_width, 3), dtype=np.uint8)
            
            # 1. 原始图片
            original_aspect = original_w / original_h
            original_height = int(result_height * 0.9)
            original_width = int(original_height * original_aspect)
            
            if original_width > IMGSZ:
                original_width = IMGSZ
                original_height = int(original_width / original_aspect)
            
            original_resized = cv2.resize(img, (original_width, original_height), interpolation=cv2.INTER_LINEAR)
            
            x_offset = (IMGSZ - original_width) // 2
            y_offset = (result_height - original_height) // 2
            
            result_canvas[y_offset:y_offset+original_height, x_offset:x_offset+original_width] = original_resized
            result_canvas = add_text_to_image(result_canvas, f"Original: {original_w}x{original_h}", (10, 30))
            
            # 2. 三种固定缩放效果
            for j, scale in enumerate(FIXED_SCALES):
                # 应用缩放
                resized_img, actual_scale = resize_image(img.copy(), scale)
                
                # 在画布上定位
                canvas_x = IMGSZ * (j + 1)
                
                # 获取缩放后尺寸
                h_resized, w_resized = resized_img.shape[:2]
                
                # 居中放置
                x_offset_canvas = canvas_x + (IMGSZ - w_resized) // 2
                y_offset_canvas = (result_height - h_resized) // 2
                
                x_end = min(x_offset_canvas + w_resized, canvas_x + IMGSZ)
                y_end = min(y_offset_canvas + h_resized, result_height)
                
                if x_end > x_offset_canvas and y_end > y_offset_canvas:
                    result_canvas[y_offset_canvas:y_end, x_offset_canvas:x_end] = resized_img[:y_end-y_offset_canvas, :x_end-x_offset_canvas]
                
                # 添加文字
                text_y = result_height - 60
                result_canvas = add_text_to_image(result_canvas, f"Scale: {actual_scale:.4f}", (canvas_x + 10, text_y))
                result_canvas = add_text_to_image(result_canvas, f"Size: {w_resized}x{h_resized}", (canvas_x + 10, text_y + 30))
            
            # 添加分隔线
            for j in range(1, 4):
                line_x = IMGSZ * j
                cv2.line(result_canvas, (line_x, 0), (line_x, result_height), (255, 255, 255), 2)
            
            # 保存结果
            output_path = os.path.join(OUTPUT_DIR, f'drone_view_{i+1}.jpg')
            cv2.imwrite(output_path, result_canvas)
            print(f"  已保存: {output_path}")
            
            # 显示图片
            plt.figure(figsize=(16, 6))
            plt.imshow(cv2.cvtColor(result_canvas, cv2.COLOR_BGR2RGB))
            plt.title(f'无人机俯拍模拟 - 图片 {i+1}')
            plt.axis('off')
            plt.tight_layout()
            plt.show()
            
            # 单独保存每种缩放效果
            for j, scale in enumerate(FIXED_SCALES):
                resized_img, actual_scale = resize_image(img.copy(), scale)
                h_resized, w_resized = resized_img.shape[:2]
                
                canvas = np.zeros((IMGSZ, IMGSZ, 3), dtype=np.uint8)
                
                # 随机位置放置
                x_offset = random.randint(0, max(0, IMGSZ - w_resized))
                y_offset = random.randint(0, max(0, IMGSZ - h_resized))
                
                x_end = min(x_offset + w_resized, IMGSZ)
                y_end = min(y_offset + h_resized, IMGSZ)
                
                if x_end > x_offset and y_end > y_offset:
                    canvas[y_offset:y_end, x_offset:x_end] = resized_img[:y_end-y_offset, :x_end-x_offset]
                
                # 添加文字
                canvas = add_text_to_image(canvas, f"Scale: {actual_scale:.4f}", (10, 30))
                canvas = add_text_to_image(canvas, f"Size: {w_resized}x{h_resized}", (10, 60))
                
                cv2.imwrite(os.path.join(OUTPUT_DIR, f'scale_{i+1}_{j+1}.jpg'), canvas)
            
        except Exception as e:
            print(f"  处理图片时出错: {e}")
            continue
    
    print(f"\n完成! 结果已保存到: {OUTPUT_DIR}")
    print(f"随机抽取了 {len(selected_images)} 张图片进行可视化")

if __name__ == "__main__":
    print("无人机俯拍模拟可视化")
    print(f"图片路径: {IMAGE_PATH}")
    print(f"输出目录: {OUTPUT_DIR}")
    print(f"随机抽取 {SAMPLE_COUNT} 张图片\n")
    
    visualize_augmentations()

例:下面就是我生成的可视化的图片结果

经过观察,我发现scale在0.15和0.25时比较符合我的项目需求,所以我的数据增强-缩放范围就选择0.15-0.25,但是我们在训练脚本中的数据增强的scale并不支持0.15-0.25这样的缩放范围,我们可以在

Ultralytics-data-augment.py-randomperspective-affine_transform中看到,YOLOv12-main框架中的缩放规则如下:

如果我们只是在训练脚本中将scale设置为0.15,

他执行的是[1-0.15,1+0.15],并不是我们要求的范围,所以我们只有通过对数据集进行预处理,此时只有再劳驾我们的AI帮忙写一个将图片进行缩放的脚本,此时缩放范围就可以自定义了,注意!!图片缩放之后,标签也要跟着一起进行相应变换,这不能忘。

预处理-缩放脚本:

import cv2
import numpy as np
import os
import random
from pathlib import Path
import sys
#################################################

#功能:对数据集进行预处理,进行缩放

##################################################

# ====== 配置参数(修改这里) ======
IMAGE_INPUT_DIR = r"C:\Users\JAMES HARDEN\Desktop\横幅数据集\横幅数据集\images\val"
LABEL_INPUT_DIR = r"C:\Users\JAMES HARDEN\Desktop\横幅数据集\横幅数据集\labels\val"
IMAGE_OUTPUT_DIR = r"D:\yoloV12\yolov12-main\yolov12-main\datasets\dataset_banner_scale\images\val"
LABEL_OUTPUT_DIR = r"D:\yoloV12\yolov12-main\yolov12-main\datasets\dataset_banner_scale\labels\val"
IMGSZ = 640  # 输出画布大小
SCALE_MIN = 0.07  # 最小缩放比例
SCALE_MAX = 0.09  # 最大缩放比例
# ===============================

def main():
    print(f"输入图片目录: {IMAGE_INPUT_DIR}")
    print(f"输入标签目录: {LABEL_INPUT_DIR}")
    print(f"输出图片目录: {IMAGE_OUTPUT_DIR}")
    print(f"输出标签目录: {LABEL_OUTPUT_DIR}")
    
    # 检查输入目录是否存在
    if not os.path.exists(IMAGE_INPUT_DIR):
        print(f"错误: 输入图片目录不存在: {IMAGE_INPUT_DIR}")
        sys.exit(1)
    
    if not os.path.exists(LABEL_INPUT_DIR):
        print(f"警告: 输入标签目录不存在: {LABEL_INPUT_DIR}")
    
    # 创建输出目录
    try:
        os.makedirs(IMAGE_OUTPUT_DIR, exist_ok=True)
        os.makedirs(LABEL_OUTPUT_DIR, exist_ok=True)
        print(f"已创建输出目录")
    except Exception as e:
        print(f"错误: 无法创建输出目录: {e}")
        sys.exit(1)
    
    # 获取图片文件
    image_extensions = ['.jpg', '.jpeg', '.png', '.bmp']
    image_files = []
    
    for ext in image_extensions:
        # 只找小写扩展名
        files = list(Path(IMAGE_INPUT_DIR).glob(f'*{ext}'))
        image_files.extend([str(p) for p in files])
    
    # 去重
    image_files = list(set(image_files))
    
    if not image_files:
        print(f"错误: 在 {IMAGE_INPUT_DIR} 中没有找到图片文件")
        print(f"支持的扩展名: {image_extensions}")
        
        # 列出目录内容
        print(f"\n目录内容:")
        try:
            for item in os.listdir(IMAGE_INPUT_DIR)[:10]:
                print(f"  {item}")
        except:
            pass
        sys.exit(1)
    
    print(f"找到 {len(image_files)} 张图片")
    
    # 处理每张图片
    success_count = 0
    for i, img_path in enumerate(image_files):
        img_name = Path(img_path).stem
        
        try:
            # 1. 读取图片
            img = cv2.imread(img_path)
            if img is None:
                # 尝试用另一种方式读取
                with open(img_path, 'rb') as f:
                    img_data = np.frombuffer(f.read(), np.uint8)
                    img = cv2.imdecode(img_data, cv2.IMREAD_COLOR)
            
            if img is None:
                print(f"  图片 {i+1}: 无法读取 {os.path.basename(img_path)}")
                continue
            
            original_h, original_w = img.shape[:2]
            
            # 2. 随机缩放
            scale = random.uniform(SCALE_MIN, SCALE_MAX)
            new_w = int(original_w * scale)
            new_h = int(original_h * scale)
            
            # 确保最小尺寸
            new_w = max(10, new_w)
            new_h = max(10, new_h)
            
            # 3. 缩放图片
            img_resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
            
            # 4. 创建画布并放置图片
            canvas = np.zeros((IMGSZ, IMGSZ, 3), dtype=np.uint8)
            
            # 计算放置位置
            max_x_offset = max(0, IMGSZ - new_w)
            max_y_offset = max(0, IMGSZ - new_h)
            
            if max_x_offset > 0:
                x_offset = random.randint(0, max_x_offset)
            else:
                x_offset = 0
                
            if max_y_offset > 0:
                y_offset = random.randint(0, max_y_offset)
            else:
                y_offset = 0
            
            # 放置图片
            end_x = min(x_offset + new_w, IMGSZ)
            end_y = min(y_offset + new_h, IMGSZ)
            
            if end_y - y_offset > 0 and end_x - x_offset > 0:
                canvas[y_offset:end_y, x_offset:end_x] = img_resized[:end_y-y_offset, :end_x-x_offset]
            
            # 5. 保存图片
            output_img_path = os.path.join(IMAGE_OUTPUT_DIR, f"{img_name}_drone.jpg")
            success = cv2.imwrite(output_img_path, canvas)
            
            if not success:
                print(f"  图片 {i+1}: 保存失败 {os.path.basename(output_img_path)}")
                continue
            
            # 6. 处理标签
            output_label_path = os.path.join(LABEL_OUTPUT_DIR, f"{img_name}_drone.txt")
            label_path = os.path.join(LABEL_INPUT_DIR, f"{img_name}.txt")
            
            if os.path.exists(label_path):
                with open(label_path, 'r', encoding='utf-8') as f:
                    lines = f.readlines()
                
                new_lines = []
                for line in lines:
                    parts = line.strip().split()
                    if len(parts) >= 5:
                        cls_id = parts[0]
                        x_center = float(parts[1])
                        y_center = float(parts[2])
                        bbox_w = float(parts[3])
                        bbox_h = float(parts[4])
                        
                        # 转换坐标
                        x_center_new = (x_center * new_w + x_offset) / IMGSZ
                        y_center_new = (y_center * new_h + y_offset) / IMGSZ
                        bbox_w_new = bbox_w * new_w / IMGSZ
                        bbox_h_new = bbox_h * new_h / IMGSZ
                        
                        # 检查边界框是否在画布内
                        if (0 <= x_center_new - bbox_w_new/2 and 
                            x_center_new + bbox_w_new/2 <= 1 and
                            0 <= y_center_new - bbox_h_new/2 and 
                            y_center_new + bbox_h_new/2 <= 1):
                            
                            new_lines.append(f"{cls_id} {x_center_new:.6f} {y_center_new:.6f} {bbox_w_new:.6f} {bbox_h_new:.6f}")
                
                # 保存标签
                if new_lines:
                    with open(output_label_path, 'w', encoding='utf-8') as f:
                        f.write('\n'.join(new_lines))
                else:
                    # 保存空文件
                    with open(output_label_path, 'w', encoding='utf-8') as f:
                        pass
            else:
                # 如果原始标签不存在,创建空标签文件
                with open(output_label_path, 'w', encoding='utf-8') as f:
                    pass
            
            success_count += 1
            
            # 显示进度
            if (i + 1) % 100 == 0:
                print(f"已处理 {i + 1}/{len(image_files)} 张图片")
                
        except Exception as e:
            print(f"  图片 {i+1}: 处理 {os.path.basename(img_path)} 时出错: {e}")
            continue
    
    print(f"\n处理完成!")
    print(f"总图片数: {len(image_files)}")
    print(f"成功处理: {success_count}")
    print(f"失败: {len(image_files) - success_count}")
    
    # 验证输出
    print(f"\n验证输出:")
    
    # 检查输出目录
    output_images = list(Path(IMAGE_OUTPUT_DIR).glob('*.jpg'))
    output_labels = list(Path(LABEL_OUTPUT_DIR).glob('*.txt'))
    
    print(f"输出图片数量: {len(output_images)}")
    print(f"输出标签数量: {len(output_labels)}")
    
    if output_images:
        print(f"输出目录中的图片示例:")
        for img in output_images[:3]:
            print(f"  - {img.name} ({os.path.getsize(img)} 字节)")
    else:
        print(f"错误: 输出目录中没有图片!")
        print(f"请检查: {IMAGE_OUTPUT_DIR}")

if __name__ == "__main__":
    main()

生成的图片如下:

简直是low到爆,不过我使用的是已经用正常大小的横幅训练了的模型,对这个模型再次用我们进行了预处理-缩放的数据集进行训练。

再次训练我是关闭了数据增强模块,就专注于对缩放大小的检测,训练轮次不宜太高,因为比较是同一数据集,只是进行了缩放变换,轮次过高的话容易有过拟合风险且容易导致泛化能力下降。

微调的目标是让模型适应新数据,而不是从头学习。轮次过高会导致过拟合,轮次过低则效果不明显。

可以看到相较之前,召回率明显降低了,不过没关系,什么都无法舍弃的话什么都得不到。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐