Files
yolov5_birdproject/detect_and_crop_by_class.py
T
2026-05-26 12:45:11 +08:00

220 lines
6.5 KiB
Python

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)