|
|
import os
|
|
|
import random
|
|
|
import math
|
|
|
import numpy as np
|
|
|
from PIL import Image, ImageDraw, ImageFilter, ImageEnhance
|
|
|
from qrcode import QRCode
|
|
|
import qrcode.constants
|
|
|
import cv2
|
|
|
|
|
|
|
|
|
def generate_random_gradient(size=(2048, 2048)):
|
|
|
"""生成随机渐变背景"""
|
|
|
width, height = size
|
|
|
|
|
|
# 随机选择两个颜色
|
|
|
color1 = [random.randint(0, 255) for _ in range(3)]
|
|
|
color2 = [random.randint(0, 255) for _ in range(3)]
|
|
|
|
|
|
# 使用 NumPy 生成渐变数组(更高效)
|
|
|
gradient = np.linspace(color1, color2, height, dtype=np.uint8)
|
|
|
gradient = np.tile(gradient, (width, 1, 1))
|
|
|
gradient = np.transpose(gradient, (1, 0, 2))
|
|
|
|
|
|
# 转换为 PIL Image
|
|
|
img = Image.fromarray(gradient)
|
|
|
|
|
|
return img
|
|
|
|
|
|
|
|
|
def generate_qr_code(size=(200, 200)):
|
|
|
"""生成二维码(透明背景)"""
|
|
|
qr = QRCode(
|
|
|
version=1,
|
|
|
error_correction=qrcode.constants.ERROR_CORRECT_L,
|
|
|
box_size=10,
|
|
|
border=4,
|
|
|
)
|
|
|
|
|
|
# 随机生成数据
|
|
|
data = ''.join([str(random.randint(0, 9)) for _ in range(50)])
|
|
|
qr.add_data(data)
|
|
|
qr.make(fit=True)
|
|
|
|
|
|
# 转换为 PIL Image(先生成白色背景)
|
|
|
img = qr.make_image(fill_color="black", back_color="white")
|
|
|
|
|
|
# 转换为 RGBA
|
|
|
img = img.convert('RGBA')
|
|
|
|
|
|
# 将白色背景转为透明
|
|
|
datas = img.getdata()
|
|
|
new_data = []
|
|
|
for item in datas:
|
|
|
# 如果是白色(接近白色),则设为透明
|
|
|
if item[0] > 230 and item[1] > 230 and item[2] > 230:
|
|
|
new_data.append((255, 255, 255, 0))
|
|
|
else:
|
|
|
new_data.append(item)
|
|
|
|
|
|
img.putdata(new_data)
|
|
|
|
|
|
# 调整大小
|
|
|
img = img.resize(size, Image.Resampling.LANCZOS)
|
|
|
|
|
|
return img
|
|
|
|
|
|
|
|
|
def transform_qr_code(qr_img):
|
|
|
"""对二维码进行随机变换(亮度、清晰度)"""
|
|
|
# 确保图片是 RGBA 模式
|
|
|
if qr_img.mode != 'RGBA':
|
|
|
qr_img = qr_img.convert('RGBA')
|
|
|
|
|
|
# 随机亮度(只影响 RGB 通道,不影响 alpha 通道)
|
|
|
r, g, b, a = qr_img.split()
|
|
|
rgb_img = Image.merge('RGB', (r, g, b))
|
|
|
|
|
|
brightness = ImageEnhance.Brightness(rgb_img)
|
|
|
rgb_img = brightness.enhance(random.uniform(0.7, 1.3))
|
|
|
|
|
|
# 随机对比度
|
|
|
contrast = ImageEnhance.Contrast(rgb_img)
|
|
|
rgb_img = contrast.enhance(random.uniform(0.8, 1.2))
|
|
|
|
|
|
# 随机模糊(清晰度)
|
|
|
blur_radius = random.uniform(0, 1.5)
|
|
|
if blur_radius > 0:
|
|
|
rgb_img = rgb_img.filter(ImageFilter.GaussianBlur(radius=blur_radius))
|
|
|
|
|
|
# 重新合并 RGB 和 alpha 通道
|
|
|
qr_img = Image.merge('RGBA', rgb_img.split() + (a,))
|
|
|
|
|
|
return qr_img
|
|
|
|
|
|
|
|
|
def is_overlapping(x1, y1, w1, h1, x2, y2, w2, h2, margin=20):
|
|
|
"""检查两个矩形是否重叠(添加边距)"""
|
|
|
return not (x1 + w1 + margin < x2 or
|
|
|
x2 + w2 + margin < x1 or
|
|
|
y1 + h1 + margin < y2 or
|
|
|
y2 + h2 + margin < y1)
|
|
|
|
|
|
|
|
|
def calculate_bbox(img_size, x, y, width, height):
|
|
|
"""
|
|
|
计算边界框(YOLO 格式)
|
|
|
|
|
|
Args:
|
|
|
img_size: 原始图片大小 (width, height)
|
|
|
x, y: 二维码左上角坐标
|
|
|
width, height: 二维码宽高
|
|
|
|
|
|
Returns:
|
|
|
(x_center, y_center, bbox_width, bbox_height) - YOLO 格式的归一化坐标
|
|
|
"""
|
|
|
img_w, img_h = img_size
|
|
|
|
|
|
# 计算二维码中心
|
|
|
center_x = x + width / 2
|
|
|
center_y = y + height / 2
|
|
|
|
|
|
# 边界框就是二维码本身
|
|
|
bbox_width = width
|
|
|
bbox_height = height
|
|
|
|
|
|
# 归一化到 [0, 1]
|
|
|
x_norm = center_x / img_w
|
|
|
y_norm = center_y / img_h
|
|
|
w_norm = bbox_width / img_w
|
|
|
h_norm = bbox_height / img_h
|
|
|
|
|
|
# 确保在 [0, 1] 范围内
|
|
|
x_norm = max(0, min(1, x_norm))
|
|
|
y_norm = max(0, min(1, y_norm))
|
|
|
w_norm = max(0, min(1, w_norm))
|
|
|
h_norm = max(0, min(1, h_norm))
|
|
|
|
|
|
return x_norm, y_norm, w_norm, h_norm
|
|
|
|
|
|
|
|
|
def place_qr_codes(img, num_qrcodes=20):
|
|
|
"""
|
|
|
在图片上放置多个不重叠的二维码
|
|
|
|
|
|
Returns:
|
|
|
img: 生成的图片
|
|
|
annotations: 标注信息列表 [(x, y, w, h), ...]
|
|
|
"""
|
|
|
img_w, img_h = img.size
|
|
|
annotations = []
|
|
|
placed_boxes = []
|
|
|
|
|
|
for i in range(num_qrcodes):
|
|
|
# 随机二维码大小(50-200像素)
|
|
|
# 重点关注 50-100 范围,增加小目标样本
|
|
|
if random.random() < 0.7:
|
|
|
# 70% 概率生成 50-100 的小目标
|
|
|
qr_size_value = random.randint(50, 100)
|
|
|
else:
|
|
|
# 30% 概率生成 100-200 的中等目标
|
|
|
qr_size_value = random.randint(100, 200)
|
|
|
qr_size = (qr_size_value, qr_size_value)
|
|
|
|
|
|
# 生成二维码
|
|
|
qr_img = generate_qr_code(qr_size)
|
|
|
|
|
|
# 变换二维码(不旋转)
|
|
|
qr_img_transformed = transform_qr_code(qr_img)
|
|
|
|
|
|
qr_w, qr_h = qr_img_transformed.size
|
|
|
|
|
|
# 随机位置(确保二维码完全在图片内)
|
|
|
max_x = img_w - qr_w
|
|
|
max_y = img_h - qr_h
|
|
|
|
|
|
# 尝试找到不重叠的位置
|
|
|
max_attempts = 100
|
|
|
for attempt in range(max_attempts):
|
|
|
x = random.randint(0, max_x)
|
|
|
y = random.randint(0, max_y)
|
|
|
|
|
|
# 检查是否与已放置的二维码重叠
|
|
|
overlapping = False
|
|
|
for px, py, pw, ph in placed_boxes:
|
|
|
if is_overlapping(x, y, qr_w, qr_h, px, py, pw, ph):
|
|
|
overlapping = True
|
|
|
break
|
|
|
|
|
|
if not overlapping:
|
|
|
# 粘贴二维码(使用 alpha 通道作为 mask)
|
|
|
# img 需要先转换为 RGBA 模式
|
|
|
if img.mode != 'RGBA':
|
|
|
img = img.convert('RGBA')
|
|
|
|
|
|
# 提取 alpha 通道作为 mask
|
|
|
alpha = qr_img_transformed.split()[-1]
|
|
|
img.paste(qr_img_transformed, (x, y), alpha)
|
|
|
|
|
|
# 记录放置的盒子
|
|
|
placed_boxes.append((x, y, qr_w, qr_h))
|
|
|
|
|
|
# 计算边界框(直接使用二维码的位置和大小)
|
|
|
bbox_x, bbox_y, bbox_w, bbox_h = calculate_bbox(
|
|
|
(img_w, img_h), x, y, qr_w, qr_h
|
|
|
)
|
|
|
|
|
|
annotations.append((bbox_x, bbox_y, bbox_w, bbox_h))
|
|
|
break
|
|
|
else:
|
|
|
print(f"Warning: Could not place QR code {i+1} without overlap")
|
|
|
|
|
|
return img, annotations
|
|
|
|
|
|
|
|
|
def save_annotation(label_path, annotations):
|
|
|
"""保存标注文件(YOLO 格式)"""
|
|
|
with open(label_path, 'w') as f:
|
|
|
for x, y, w, h in annotations:
|
|
|
# 类别为 0(二维码)
|
|
|
f.write(f"0 {x:.6f} {y:.6f} {w:.6f} {h:.6f}\n")
|
|
|
|
|
|
|
|
|
def generate_dataset(output_root, num_train_images=30):
|
|
|
"""生成 YOLO8 二维码检测数据集"""
|
|
|
image_size = (2048, 2048)
|
|
|
# 增加每张图片的二维码数量,提高小目标检测效果
|
|
|
num_qrcodes_per_image = 50 # 从 20 增加到 50
|
|
|
num_val_images = max(1, num_train_images // 10)
|
|
|
|
|
|
# 创建文件夹
|
|
|
train_images_dir = os.path.join(output_root, "train", "images")
|
|
|
train_labels_dir = os.path.join(output_root, "train", "labels")
|
|
|
val_images_dir = os.path.join(output_root, "val", "images")
|
|
|
val_labels_dir = os.path.join(output_root, "val", "labels")
|
|
|
|
|
|
os.makedirs(train_images_dir, exist_ok=True)
|
|
|
os.makedirs(train_labels_dir, exist_ok=True)
|
|
|
os.makedirs(val_images_dir, exist_ok=True)
|
|
|
os.makedirs(val_labels_dir, exist_ok=True)
|
|
|
|
|
|
print(f"Generating {num_train_images} training images...")
|
|
|
for i in range(num_train_images):
|
|
|
print(f" Generating training image {i+1}/{num_train_images}")
|
|
|
|
|
|
# 生成随机渐变背景
|
|
|
img = generate_random_gradient(image_size)
|
|
|
|
|
|
# 放置二维码
|
|
|
img, annotations = place_qr_codes(img, num_qrcodes_per_image)
|
|
|
|
|
|
# 保存图片(转换为 RGB,JPEG 不支持透明通道)
|
|
|
if img.mode == 'RGBA':
|
|
|
img = img.convert('RGB')
|
|
|
img_name = f"train_{i:04d}.jpg"
|
|
|
img_path = os.path.join(train_images_dir, img_name)
|
|
|
img.save(img_path, quality=95)
|
|
|
|
|
|
# 保存标注
|
|
|
label_name = f"train_{i:04d}.txt"
|
|
|
label_path = os.path.join(train_labels_dir, label_name)
|
|
|
save_annotation(label_path, annotations)
|
|
|
|
|
|
print(f"Generating {num_val_images} validation images...")
|
|
|
for i in range(num_val_images):
|
|
|
print(f" Generating validation image {i+1}/{num_val_images}")
|
|
|
|
|
|
# 生成随机渐变背景
|
|
|
img = generate_random_gradient(image_size)
|
|
|
|
|
|
# 放置二维码
|
|
|
img, annotations = place_qr_codes(img, num_qrcodes_per_image)
|
|
|
|
|
|
# 保存图片(转换为 RGB,JPEG 不支持透明通道)
|
|
|
if img.mode == 'RGBA':
|
|
|
img = img.convert('RGB')
|
|
|
img_name = f"val_{i:04d}.jpg"
|
|
|
img_path = os.path.join(val_images_dir, img_name)
|
|
|
img.save(img_path, quality=95)
|
|
|
|
|
|
# 保存标注
|
|
|
label_name = f"val_{i:04d}.txt"
|
|
|
label_path = os.path.join(val_labels_dir, label_name)
|
|
|
save_annotation(label_path, annotations)
|
|
|
|
|
|
# 生成 data.yaml
|
|
|
yaml_content = f"""path: {os.path.abspath(output_root)}
|
|
|
train: train/images
|
|
|
val: val/images
|
|
|
|
|
|
nc: 1
|
|
|
names: ['qrcode']
|
|
|
"""
|
|
|
|
|
|
yaml_path = os.path.join(output_root, "data.yaml")
|
|
|
with open(yaml_path, 'w', encoding='utf-8') as f:
|
|
|
f.write(yaml_content)
|
|
|
|
|
|
print("\n" + "="*50)
|
|
|
print("Dataset generation completed!")
|
|
|
print("="*50)
|
|
|
print(f"Output directory: {output_root}")
|
|
|
print(f"Training images: {num_train_images}")
|
|
|
print(f"Validation images: {num_val_images}")
|
|
|
print(f"QR codes per image: {num_qrcodes_per_image}")
|
|
|
print(f"Image size: {image_size}")
|
|
|
print(f"Number of classes: 1")
|
|
|
print(f"Class name: qrcode")
|
|
|
print("="*50)
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
# 输出目录
|
|
|
output_root = r"D:\PycharmProjects\yolo\qrcode_dataset"
|
|
|
|
|
|
# 生成训练集图片数量(建议至少 100 张)
|
|
|
num_train_images = 100
|
|
|
|
|
|
# 生成数据集
|
|
|
generate_dataset(output_root, num_train_images)
|
|
|
|
|
|
print("\n" + "="*50)
|
|
|
print("训练建议:")
|
|
|
print("="*50)
|
|
|
print("使用 YOLOv8-p2 进行小目标检测:")
|
|
|
print(" yolo train data=data.yaml model=yolov8n-p2.pt epochs=200 imgsz=4096")
|
|
|
print("")
|
|
|
print("如果显存足够,可以使用 8192 输入尺寸:")
|
|
|
print(" yolo train data=data.yaml model=yolov8n-p2.pt epochs=200 imgsz=8192")
|
|
|
print("")
|
|
|
print("如果需要更高精度,使用 s 模型:")
|
|
|
print(" yolo train data=data.yaml model=yolov8s-p2.pt epochs=200 imgsz=4096")
|
|
|
print("="*50)
|