import os import sys import cv2 import torch import argparse from pathlib import Path # ========================= # 需要确保本脚本能找到 YOLOv5 源码 # 如果脚本不在 yolov5 根目录,可手动添加: # sys.path.append("/path/to/yolov5") # ========================= from models.common import DetectMultiBackend from utils.general import ( check_img_size, non_max_suppression, scale_boxes ) from utils.torch_utils import select_device from utils.augmentations import letterbox def is_image_file(filename): image_exts = {".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff", ".webp"} return Path(filename).suffix.lower() in image_exts def load_image(img_path, img_size=640, stride=32): """ 读取图像,并进行与YOLOv5推理一致的预处理 返回: im0: 原图(BGR) im: 预处理后的输入张量所对应的numpy图像(CHW, RGB) """ im0 = cv2.imread(str(img_path)) if im0 is None: raise ValueError(f"无法读取图片: {img_path}") # letterbox缩放 im = letterbox(im0, img_size, stride=stride, auto=True)[0] # BGR -> RGB, HWC -> CHW im = im[:, :, ::-1].transpose(2, 0, 1) im = im.copy() return im0, im def save_crop(img, xyxy, save_path): """ 根据检测框裁剪并保存 xyxy: [x1, y1, x2, y2] """ x1, y1, x2, y2 = map(int, xyxy) h, w = img.shape[:2] x1 = max(0, min(x1, w - 1)) x2 = max(0, min(x2, w - 1)) y1 = max(0, min(y1, h - 1)) y2 = max(0, min(y2, h - 1)) if x2 <= x1 or y2 <= y1: return False crop = img[y1:y2, x1:x2] if crop.size == 0: return False cv2.imwrite(str(save_path), crop) return True def run( weights, source, output_dir, imgsz=640, conf_thres=0.25, iou_thres=0.45, device='', max_det=1000, classes=None, class_num=5 ): # 设备 device = select_device(device) # 加载模型 model = DetectMultiBackend(weights, device=device, dnn=False, fp16=False) stride, names, pt = model.stride, model.names, model.pt imgsz = check_img_size(imgsz, s=stride) # 创建输出目录和类别子目录 output_dir = Path(output_dir) output_dir.mkdir(parents=True, exist_ok=True) for i in range(class_num): (output_dir / str(i)).mkdir(parents=True, exist_ok=True) # 收集图片 source = Path(source) if not source.exists(): raise FileNotFoundError(f"输入文件夹不存在: {source}") image_files = [p for p in source.iterdir() if p.is_file() and is_image_file(p.name)] image_files.sort() if len(image_files) == 0: print(f"在文件夹中未找到图片: {source}") return print(f"共找到 {len(image_files)} 张图片") print(f"类别名称: {names}") total_crops = 0 for img_idx, img_path in enumerate(image_files, 1): try: im0, im = load_image(img_path, img_size=imgsz, stride=stride) # 转tensor im = torch.from_numpy(im).to(device) im = im.float() / 255.0 if im.ndimension() == 3: im = im.unsqueeze(0) # 推理 pred = model(im) # NMS pred = non_max_suppression( pred, conf_thres, iou_thres, classes=classes, agnostic=False, max_det=max_det ) det = pred[0] crop_count_this_image = 0 if len(det): # 将框映射回原图尺寸 det[:, :4] = scale_boxes(im.shape[2:], det[:, :4], im0.shape).round() # 遍历每个检测目标 for det_idx, (*xyxy, conf, cls) in enumerate(det): cls_id = int(cls.item()) # 只保存 0~4 这5类 if cls_id < 0 or cls_id >= class_num: continue # 保存文件名:原图名_序号_类别_置信度.jpg stem = img_path.stem conf_val = float(conf.item()) save_name = f"{stem}_{det_idx:03d}_cls{cls_id}_{conf_val:.4f}.jpg" save_path = output_dir / str(cls_id) / save_name ok = save_crop(im0, xyxy, save_path) if ok: crop_count_this_image += 1 total_crops += 1 print(f"[{img_idx}/{len(image_files)}] {img_path.name} -> 裁剪保存 {crop_count_this_image} 个目标") except Exception as e: print(f"处理失败: {img_path},原因: {e}") print("\n处理完成") print(f"总共保存裁剪目标数: {total_crops}") print(f"输出目录: {output_dir.resolve()}") def parse_opt(): parser = argparse.ArgumentParser() parser.add_argument('--weights', type=str, required=True, help='训练好的YOLOv5 pt权重路径') parser.add_argument('--source', type=str, required=True, help='待检测图片文件夹路径') parser.add_argument('--output-dir', type=str, required=True, help='裁剪结果输出文件夹') parser.add_argument('--imgsz', '--img', '--img-size', nargs='+', type=int, default=[640], help='推理尺寸') parser.add_argument('--conf-thres', type=float, default=0.25, help='置信度阈值') parser.add_argument('--iou-thres', type=float, default=0.45, help='NMS IoU阈值') parser.add_argument('--device', default='', help='cuda device,例如 0 或 0,1,2,3 或 cpu') parser.add_argument('--max-det', type=int, default=1000, help='每张图最多检测目标数') parser.add_argument('--class-num', type=int, default=5, help='类别总数,这里默认是5') parser.add_argument('--classes', nargs='+', type=int, default=None, help='只保留某些类别,例如 --classes 0 1 2') opt = parser.parse_args() # imgsz兼容处理 if len(opt.imgsz) == 1: opt.imgsz = opt.imgsz[0] elif len(opt.imgsz) == 2: # yolov5这里一般接受单值,若传两个值则取最大,避免报错 opt.imgsz = max(opt.imgsz) else: raise ValueError("imgsz 参数格式错误") return opt def main(opt): run( weights=opt.weights, source=opt.source, output_dir=opt.output_dir, imgsz=opt.imgsz, conf_thres=opt.conf_thres, iou_thres=opt.iou_thres, device=opt.device, max_det=opt.max_det, classes=opt.classes, class_num=opt.class_num ) if __name__ == "__main__": opt = parse_opt() main(opt)