split_dota.py
ultralytics\data\split_dota.py
目录
2.def bbox_iof(polygon1, bbox2, eps=1e-6):
3.def load_yolo_dota(data_root, split="train"):
4.def get_windows(im_size, crop_sizes=(1024,), gaps=(200,), im_rate_thr=0.6, eps=0.01):
5.def get_window_obj(anno, windows, iof_thr=0.7):
6.def crop_and_save(anno, windows, window_objs, im_dir, lb_dir, allow_background_images=True):
7.def split_images_and_labels(data_root, save_dir, split="train", crop_sizes=(1024,), gaps=(200,)):
8.def split_trainval(data_root, save_dir, crop_size=1024, gap=200, rates=(1.0,)):
9.def split_test(data_root, save_dir, crop_size=1024, gap=200, rates=(1.0,)):
1.所需的库和模块
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
import itertools
from glob import glob
from math import ceil
from pathlib import Path
import cv2
import numpy as np
from PIL import Image
from ultralytics.data.utils import exif_size, img2label_paths
from ultralytics.utils import TQDM
from ultralytics.utils.checks import check_requirements
2.def bbox_iof(polygon1, bbox2, eps=1e-6):
# 这段代码定义了一个函数 bbox_iof ,用于计算多边形( polygon1 )与边界框( bbox2 )之间的交并比(Intersection over Foreground,IOF)。它通过结合多边形的几何操作和边界框的计算,实现了一种混合的交并比计算方法。
# 这行代码定义了一个函数 bbox_iof ,它接受三个参数。
# 1.polygon1 :一个多边形的顶点坐标数组。
# 2.bbox2 :一个边界框的坐标数组。
# 3.eps :一个可选参数,用于数值稳定性的极小值,默认为 1e-6 。
def bbox_iof(polygon1, bbox2, eps=1e-6):
# 计算多边形和边界框之间的前景交点 (IoF)。
# 参数:
# polygon1 (np.ndarray):多边形坐标,形状 (n, 8)。
# bbox2 (np.ndarray):边界框,形状 (n, 4)。
# eps (float,可选):较小的值,以防止除以零。默认为 1e-6。
# 返回:
# (np.ndarray):IoF 分数,形状 (n, 1) 或 (n, m)(如果 bbox2 为 (m, 4)。
# 注意:
# 多边形格式:[x1, y1, x2, y2, x3, y3, x4, y4]。
# 边界框格式:[x_min, y_min, x_max, y_max]。
"""
Calculate Intersection over Foreground (IoF) between polygons and bounding boxes.
Args:
polygon1 (np.ndarray): Polygon coordinates, shape (n, 8).
bbox2 (np.ndarray): Bounding boxes, shape (n, 4).
eps (float, optional): Small value to prevent division by zero. Defaults to 1e-6.
Returns:
(np.ndarray): IoF scores, shape (n, 1) or (n, m) if bbox2 is (m, 4).
Note:
Polygon format: [x1, y1, x2, y2, x3, y3, x4, y4].
Bounding box format: [x_min, y_min, x_max, y_max].
"""
# 调用 check_requirements 函数,检查是否安装了 shapely 库。 shapely 是一个用于 几何操作 的 Python 库,后续代码中会用到它来 处理多边形的几何运算 。
check_requirements("shapely")
# 从 shapely.geometry 模块导入 Polygon 类,用于 创建和操作多边形对象 。
from shapely.geometry import Polygon
# 将输入的 polygon1 重新整形为一个形状为 (N, 4, 2) 的数组,其中 N 是多边形的数量, 4 表示每个多边形有 4 个顶点, 2 表示每个顶点的坐标(x, y)。
polygon1 = polygon1.reshape(-1, 4, 2)
# 计算 polygon1 的 边界框 ( bbox1 )。
# 通过取每个多边形的顶点的 最小值 和 最大值 ,分别得到 左上角点 ( lt_point )和 右下角点 ( rb_point ),然后将它们拼接成 边界框的坐标格式 (x1, y1, x2, y2) 。
lt_point = np.min(polygon1, axis=-2) # left-top
rb_point = np.max(polygon1, axis=-2) # right-bottom
bbox1 = np.concatenate([lt_point, rb_point], axis=-1)
# 计算 bbox1 和 bbox2 的重叠区域。
# lt 和 rb 分别计算两个边界框的 重叠区域 的 左上角 和 右下角坐标 。
lt = np.maximum(bbox1[:, None, :2], bbox2[..., :2])
rb = np.minimum(bbox1[:, None, 2:], bbox2[..., 2:])
# wh 是 重叠区域的宽度和高度 ,通过 np.clip 确保不会出现负值。
wh = np.clip(rb - lt, 0, np.inf)
# h_overlaps 是 重叠区域的面积 ,通过宽度和高度的乘积计算得到。
h_overlaps = wh[..., 0] * wh[..., 1]
# 将边界框的坐标拆分为四个独立的数组, left 、 top 、 right 和 bottom 。
# bbox2[..., i] :从 bbox2 中提取第 i 列的值。假设 bbox2 的形状是 (M, 4) ,其中 M 是边界框的数量, 4 表示每个边界框的四个坐标值 (x1, y1, x2, y2) 。
# for i in range(4) :循环遍历 bbox2 的每一列,分别提取出 x1 (左边界)、 y1 (上边界)、 x2 (右边界)和 y2 (下边界)。
# left, top, right, bottom :将提取的四列分别赋值给这四个变量。 left 是所有边界框的左边界坐标( x1 )。 top 是所有边界框的上边界坐标( y1 )。 right 是所有边界框的右边界坐标( x2 )。 bottom 是所有边界框的下边界坐标( y2 )。
left, top, right, bottom = (bbox2[..., i] for i in range(4))
# 将边界框的四个角点坐标重新排列,形成多边形的顶点坐标。
# np.stack(..., axis=-1) :将 left, top, right, top, right, bottom, left, bottom 这些数组沿着最后一个轴( axis=-1 )堆叠起来。这些数组分别表示多边形的顶点坐标。
# left, top :左上角点 (x1, y1) 。
# right, top :右上角点 (x2, y1) 。
# right, bottom :右下角点 (x2, y2) 。
# left, bottom :左下角点 (x1, y2) 。
# .reshape(-1, 4, 2) :将堆叠后的数组重新整形为 (M, 4, 2) 的形状,其中 : M 是边界框的数量。 4 表示每个多边形有 4 个顶点。 2 表示每个顶点的 (x, y) 坐标。
polygon2 = np.stack([left, top, right, top, right, bottom, left, bottom], axis=-1).reshape(-1, 4, 2)
# 这段代码的核心目的是计算两个多边形集合之间的重叠面积( overlaps )以及每个多边形的面积( unions ),并为后续的交并比(IOF)计算做准备。
# 将 polygon1 中的每个顶点数组转换为 shapely.geometry.Polygon 对象。
# polygon1 是一个多边形集合,形状为 (N, 4, 2) ,其中 N 是多边形的数量, 4 是每个四边形的顶点数, 2 是每个顶点的 (x, y) 坐标。
# Polygon(p) 是 shapely 库中的一个类,用于创建多边形对象。
# 列表推导式 [Polygon(p) for p in polygon1] 遍历 polygon1 中的每个顶点数组,并将其转换为 Polygon 对象,最终得到一个包含 N 个多边形对象的列表 sg_polys1 。
sg_polys1 = [Polygon(p) for p in polygon1]
# 作用与第一行类似,将 polygon2 中的每个顶点数组转换为 shapely.geometry.Polygon 对象。 polygon2 是另一个多边形集合,形状为 (M, 4, 2) ,其中 M 是多边形的数量。 同样使用列表推导式,将每个顶点数组转换为 Polygon 对象,最终得到一个包含 M 个多边形对象的列表 sg_polys2 。
sg_polys2 = [Polygon(p) for p in polygon2]
# 初始化一个全零数组 overlaps ,其形状与 h_overlaps 相同。 h_overlaps 是之前计算的边界框重叠区域的面积( wh[..., 0] * wh[..., 1] ),形状为 (N, M) ,表示 polygon1 中的每个多边形与 polygon2 中的每个多边形之间的重叠面积。 初始化为全零数组是为了在后续计算中存储实际的多边形重叠面积。
overlaps = np.zeros(h_overlaps.shape)
# 计算 polygon1 和 polygon2 中每对多边形的实际重叠面积,并将其存储到 overlaps 中。
# np.nonzero(h_overlaps) 返回 h_overlaps 中非零元素的索引,表示存在重叠的多边形对。
# zip(*np.nonzero(h_overlaps)) 将这些索引解包为 (i, j) 对,其中 i 是 polygon1 中多边形的索引, j 是 polygon2 中多边形的索引。
for p in zip(*np.nonzero(h_overlaps)):
# sg_polys1[p[0]].intersection(sg_polys2[p[-1]]) 使用 shapely 的 intersection 方法计算两个多边形的交集,并通过 .area 获取交集的面积。
# overlaps[p] = ... 将计算得到的 重叠面积 存储到 overlaps 数组中对应的位置。
overlaps[p] = sg_polys1[p[0]].intersection(sg_polys2[p[-1]]).area
# 计算 polygon1 中每个多边形的面积,并将结果存储为一个浮点数组。 列表推导式 [p.area for p in sg_polys1] 遍历 sg_polys1 中的每个 Polygon 对象,并通过 .area 获取其面积。 np.array(..., dtype=np.float32) 将结果转换为一个形状为 (N,) 的浮点数组,其中 N 是 polygon1 中多边形的数量。
unions = np.array([p.area for p in sg_polys1], dtype=np.float32)
# 将 unions 的形状从 (N,) 转换为 (N, 1) ,使其成为一个二维数组。 这是为了后续计算的方便,例如在后续代码中可能会将 overlaps 和 unions 相除,而 overlaps 的形状是 (N, M) ,因此需要将 unions 扩展为二维数组以便进行广播操作。
unions = unions[..., None]
# 这段代码的核心功能是计算两个多边形集合之间的重叠面积( overlaps )和每个多边形的面积( unions )。主要步骤如下。将输入的多边形顶点数组转换为 shapely.geometry.Polygon 对象。初始化一个全零数组 overlaps ,用于存储重叠面积。遍历所有可能的多边形对,使用 shapely 的 intersection 方法计算实际的重叠面积,并存储到 overlaps 中。计算 polygon1 中每个多边形的面积,并将其存储为一个二维数组 unions 。这些结果将用于后续的交并比(IOF)计算,即 overlaps / unions 。
# 这段代码的目的是对计算得到的多边形重叠面积( overlaps )和多边形面积( unions )进行数值稳定性和格式处理,最终输出交并比(IOF)的结果。
# 码对 unions 数组进行数值稳定性处理。
# unions 是之前计算的每个多边形的面积,形状为 (N, 1) 。
# np.clip 函数的作用是将数组中的值限制在指定的范围内。这里将 unions 中的值限制在 [eps, +∞) 。
# eps 是一个极小值(如 1e-6 ),用于防止除零错误。
# np.inf 表示正无穷,确保不会对较大的值进行限制。
# 这一步是为了确保后续的除法操作( overlaps / unions )不会因为分母为零而导致数值不稳定或错误。
unions = np.clip(unions, eps, np.inf)
# 计算交并比(IOF) 。 overlaps 是一个形状为 (N, M) 的数组,表示 polygon1 中的每个多边形与 polygon2 中的每个多边形之间的重叠面积。 unions 是一个形状为 (N, 1) 的数组,表示 polygon1 中每个多边形的面积。 通过广播机制, overlaps / unions 会逐元素计算交并比(IOF),结果是一个形状为 (N, M) 的数组,表示 每对多边形的重叠面积与多边形面积的比值 。
outputs = overlaps / unions
# 确保输出的数组至少是二维的。
# outputs.ndim 检查 outputs 的维度。如果 outputs 是一维数组(即形状为 (N,) ),则需要将其扩展为二维数组。
if outputs.ndim == 1:
# outputs[..., None] 在数组的最后一个维度上添加一个新的轴,将一维数组 (N,) 转换为二维数组 (N, 1) 。 这一步是为了确保输出的格式一致性,方便后续处理。
outputs = outputs[..., None]
# 返回最终的交并比(IOF)结果。 outputs 的形状为 (N, M) 或 (N, 1) ,表示每对多边形的交并比。
return outputs
# 这段代码的核心功能是完成交并比(IOF)的计算,并对结果进行格式化处理。主要步骤如下。数值稳定性处理:通过 np.clip 确保分母 unions 不为零,避免除零错误。计算交并比:使用 overlaps / unions 计算每对多边形的交并比。格式化输出:确保输出的数组至少是二维的,以保持格式一致性。返回结果:返回最终的交并比(IOF)结果。这种处理方式在计算机视觉任务中非常常见,尤其是在目标检测和分割任务中,用于评估预测结果与真实标注之间的匹配程度。
# 这段代码实现了一个复杂的交并比计算方法,结合了边界框的快速计算和多边形的精确几何运算。它主要用于计算多边形与边界框之间的交并比(IOF),适用于目标检测、图像分割等计算机视觉任务。代码的核心逻辑包括。将多边形和边界框转换为统一的几何对象。利用边界框快速计算重叠区域。使用 shapely 库精确计算多边形的重叠面积。计算交并比(IOF)并返回结果。这种混合方法既利用了边界框的高效性,又通过多边形的几何运算保证了计算的准确性。
3.def load_yolo_dota(data_root, split="train"):
# 这段代码定义了一个函数 load_yolo_dota ,用于加载用于目标检测任务(特别是 DOTA 数据集)的 YOLO 格式数据。它从指定的数据根目录中读取图像和标签文件,并将它们组织成一个包含图像尺寸、标签和文件路径的字典列表。
# 这行代码定义了一个函数 load_yolo_dota ,接受两个参数。
# 1.data_root :数据集的根目录路径。
# 2.split :数据集的划分,可以是 "train" 或 "val" ,默认为 "train" 。
def load_yolo_dota(data_root, split="train"):
# 加载 DOTA 数据集。
# 注释:
# DOTA 数据集的目录结构如下:
# - data_root
# - images
# - train
# - val
# - labels
# - train
# - val
"""
Load DOTA dataset.
Args:
data_root (str): Data root.
split (str): The split data set, could be `train` or `val`.
Notes:
The directory structure assumed for the DOTA dataset:
- data_root
- images
- train
- val
- labels
- train
- val
"""
# 检查 split 参数是否为 "train" 或 "val" 。如果不是,会抛出一个断言错误,提示用户输入正确的划分类型。
assert split in {"train", "val"}, f"Split must be 'train' or 'val', not {split}." # 拆分必须是“train”或“val”,而不是{split}。
# 构建了 图像文件夹的路径 。 使用 Path 类(来自 pathlib 模块)来处理路径。 图像文件夹的路径由 data_root 、 images 和 split 组成,例如 data_root/images/train 。
im_dir = Path(data_root) / "images" / split
# 检查图像文件夹路径是否存在。 如果路径不存在,会抛出一个断言错误,提示用户检查数据根目录是否正确。
assert im_dir.exists(), f"Can't find {im_dir}, please check your data root." # 找不到 {im_dir},请检查您的数据根目录。
# 获取图像文件夹中的 所有 图像文件路径 。使用 glob 函数(来自 glob 模块)来匹配路径下的所有文件。 str(Path(data_root) / "images" / split / "*") 将路径转换为字符串,表示匹配 data_root/images/split/ 下的所有文件。
im_files = glob(str(Path(data_root) / "images" / split / "*"))
# 将 图像文件路径 转换为对应的 标签文件路径 。
# img2label_paths 是一个函数,它将图像路径映射为对应的标签路径。例如,如果图像路径是 data_root/images/train/image1.jpg ,则对应的标签路径可能是 data_root/labels/train/image1.txt 。 这里假设标签文件与图像文件具有相同的文件名,但扩展名不同。
# def img2label_paths(img_paths): -> 它接收一个包含图像路径的列表 img_paths ,并返回一个对应的标签路径列表。返回一个列表,其中包含 与输入图像路径对应的标签路径 。 -> return [sb.join(x.rsplit(sa, 1)).rsplit(".", 1)[0] + ".txt" for x in img_paths]
lb_files = img2label_paths(im_files)
# 初始化一个空列表 annos ,用于存储每个图像的 注释信息 。
annos = []
# 开始一个循环,遍历每个 图像文件路径 和 对应的标签文件路径 。
for im_file, lb_file in zip(im_files, lb_files):
# 获取图像的 宽度 和 高度 。 使用 Image.open(im_file) 打开图像文件。 调用之前定义的 exif_size 函数,获取经过 EXIF 信息校正后的图像宽度和高度。
# def exif_size(img: Image.Image): -> 它接收一个 PIL 图像对象 img ,并返回经过 EXIF 信息校正后的图像尺寸。返回经过 EXIF 信息 校正后的图像尺寸 。如果图像没有 EXIF 信息或不需要调整,返回的将是原始尺寸。 -> return s
w, h = exif_size(Image.open(im_file))
# 打开对应的标签文件。
with open(lb_file) as f:
# 读取标签文件的内容,并将其解析为一个二维列表。
# f.read().strip().splitlines() :读取文件内容,去除首尾空白字符,并按行分割。
# [x.split() for x in ... if len(x)] :对每一行进行分割,忽略空行。
lb = [x.split() for x in f.read().strip().splitlines() if len(x)]
# 将标签数据转换为一个 NumPy 数组,数据类型为 float32 。 标签文件通常包含 目标的类别 和 边界框信息 ,格式为 class_id x_center y_center width height (归一化坐标)。
lb = np.array(lb, dtype=np.float32)
# 将 当前图像的注释信息存 储到 annos 列表中。 创建一个字典,包含以下键值对 :
# "ori_size" :图像的原始尺寸 (h, w) 。
# "label" :标签数组 lb 。
# "filepath" :图像文件路径 im_file 。
annos.append(dict(ori_size=(h, w), label=lb, filepath=im_file))
# 返回 最终的注释信息列表 annos 。
return annos
# 这段代码的主要功能是从指定的数据集根目录加载 YOLO 格式的图像和标签数据,并将它们组织成一个包含图像尺寸、标签和文件路径的字典列表。具体步骤如下。验证数据集划分( split )是否正确。构建图像文件夹路径,并检查路径是否存在。获取图像文件路径及其对应的标签文件路径。遍历每个图像文件:获取图像的宽度和高度。读取并解析标签文件。将图像尺寸、标签和文件路径存储为字典。返回包含所有图像注释信息的列表。这种数据加载方式适用于目标检测任务,尤其是处理像 DOTA 这样包含大量图像和标签的大型数据集。
# def load_yolo_dota(data_root, split="train"):
# -> 用于加载用于目标检测任务(特别是 DOTA 数据集)的 YOLO 格式数据。它从指定的数据根目录中读取图像和标签文件,并将它们组织成一个包含图像尺寸、标签和文件路径的字典列表。返回 最终的注释信息列表 annos 。
# -> return annos
4.def get_windows(im_size, crop_sizes=(1024,), gaps=(200,), im_rate_thr=0.6, eps=0.01):
# 这段代码定义了一个函数 get_windows ,用于生成图像的滑动窗口(crop windows),并根据一定的条件筛选出满足图像覆盖率要求的窗口。它主要用于图像分割、目标检测等任务中,对大图像进行分块处理。
# 这行代码定义了函数 get_windows ,接受以下参数 :
# 1.im_size :图像的尺寸 (h, w) 。
# 2.crop_sizes :一个元组,表示每个滑动窗口的大小,默认为 (1024,) 。
# 3.gaps :一个元组,表示每个滑动窗口之间的重叠大小,默认为 (200,) 。
# 4.im_rate_thr :图像覆盖率阈值,默认为 0.6 ,用于筛选窗口。
# 5.eps :一个小的数值,默认为 0.01 ,用于处理数值精度问题。
def get_windows(im_size, crop_sizes=(1024,), gaps=(200,), im_rate_thr=0.6, eps=0.01):
# 获取窗口的坐标。
"""
Get the coordinates of windows.
Args:
im_size (tuple): Original image size, (h, w).
crop_sizes (List(int)): Crop size of windows.
gaps (List(int)): Gap between crops.
im_rate_thr (float): Threshold of windows areas divided by image ares.
eps (float): Epsilon value for math operations.
"""
# 将图像的 高 和 宽 分别赋值给 h 和 w 。
h, w = im_size
# 初始化一个空列表 windows ,用于 存储所有生成的滑动窗口 。
windows = []
# 开始一个循环,遍历每个 窗口大小 crop_size 和 对应的重叠大小 gap 。
for crop_size, gap in zip(crop_sizes, gaps):
# 检查 crop_size 是否大于 gap 。如果不是,会抛出一个断言错误,因为窗口大小必须大于重叠大小。
assert crop_size > gap, f"invalid crop_size gap pair [{crop_size} {gap}]" # 无效的 crop_size 间隙对 [{crop_size} {gap}]。
# 计算 每个窗口的步长 step ,即 窗口之间的实际移动距离 。
step = crop_size - gap
# 计算 水平方向上的窗口数量 xn 。 如果图像宽度 w 小于或等于窗口大小 crop_size ,则只需要一个窗口。 否则,根据步长 step 计算所需的窗口数量,并向上取整。
xn = 1 if w <= crop_size else ceil((w - crop_size) / step + 1)
# 生成 水平方向 上 每个窗口的起始坐标 xs 。
xs = [step * i for i in range(xn)]
# 调整最后一个窗口的起始坐标,确保窗口不会超出图像边界。
if len(xs) > 1 and xs[-1] + crop_size > w:
xs[-1] = w - crop_size
# 计算 垂直方向上的窗口数量 yn ,逻辑与水平方向相同。
yn = 1 if h <= crop_size else ceil((h - crop_size) / step + 1)
# 生成 垂直方向 上 每个窗口的起始坐标 ys 。
ys = [step * i for i in range(yn)]
# 调整最后一个窗口的起始坐标,确保窗口不会超出图像边界。
if len(ys) > 1 and ys[-1] + crop_size > h:
ys[-1] = h - crop_size
# itertools.product(*iterables, repeat=1)
# itertools.product() 函数是Python标准库 itertools 模块中的一个函数,用于生成多个可迭代对象的笛卡尔积(即所有可能的组合)。这个函数在处理多维数据时非常有用,特别是在需要生成所有可能的组合时。
# 参数 :
# \*iterables :一个或多个可迭代对象(如列表、元组、字符串等)。
# repeat :一个整数,表示每个可迭代对象的重复次数,默认值为1。如果 repeat 大于1,则每个可迭代对象会被重复指定次数。
# 返回值 :
# 返回一个迭代器,生成所有可能的组合。每个组合是一个元组,元组的长度等于输入的可迭代对象数量乘以 repeat 值。
# 应用场景 :
# 生成所有可能的参数组合 :在进行参数调优或实验设计时,可以使用 itertools.product 生成所有可能的参数组合。
# 生成网格搜索的参数组合 :在机器学习中,可以使用 itertools.product 生成所有可能的超参数组合,进行网格搜索。
# 生成多维数据的索引 :在处理多维数组时,可以使用 itertools.product 生成所有可能的索引组合。
# itertools.product 函数是一个非常有用的工具,可以方便地生成多个可迭代对象的笛卡尔积。它在处理多维数据、参数调优、实验设计等场景中非常有用。通过生成所有可能的组合,可以简化代码并提高效率。
# 生成 所有窗口的起始坐标 (x, y) ,通过 itertools.product 计算水平和垂直坐标的笛卡尔积。
start = np.array(list(itertools.product(xs, ys)), dtype=np.int64)
# 计算 每个窗口的结束坐标 ,通过在起始坐标上加上窗口大小 crop_size 。
stop = start + crop_size
# 将每个窗口的起始坐标和结束坐标拼接在一起,形成一个形状为 (N, 4) 的数组,并将其添加到 windows 列表中。
windows.append(np.concatenate([start, stop], axis=1))
# 将所有窗口的坐标数组合并为一个完整的数组。
windows = np.concatenate(windows, axis=0)
# 复制 窗口坐标数组 ,用于后续计算。
im_in_wins = windows.copy()
# 将窗口的起始和结束坐标限制在图像宽度范围内。 im_in_wins[:, 0::2] 表示所有窗口的 x 坐标(起始和结束)。 使用 np.clip 确保这些坐标不会超出 [0, w] 的范围。
im_in_wins[:, 0::2] = np.clip(im_in_wins[:, 0::2], 0, w)
# 将窗口的起始和结束坐标限制在图像高度范围内。 im_in_wins[:, 1::2] 表示所有窗口的 y 坐标(起始和结束)。 使用 np.clip 确保这些坐标不会超出 [0, h] 的范围。
im_in_wins[:, 1::2] = np.clip(im_in_wins[:, 1::2], 0, h)
# 计算每个窗口内 实际包含的图像区域面积 。
im_areas = (im_in_wins[:, 2] - im_in_wins[:, 0]) * (im_in_wins[:, 3] - im_in_wins[:, 1])
# 计算 每个窗口的理论面积 。
win_areas = (windows[:, 2] - windows[:, 0]) * (windows[:, 3] - windows[:, 1])
# 计算 每个窗口的图像覆盖率 ,即实际图像区域面积与窗口面积的比值。
im_rates = im_areas / win_areas
# 检查是否没有任何一个窗口的图像覆盖率( im_rates )超过阈值( im_rate_thr )。
# im_rates > im_rate_thr :生成一个布尔数组,表示每个窗口的覆盖率是否超过阈值。
# .any() :检查布尔数组中是否有任何 True 值。如果没有(即所有窗口的覆盖率都小于阈值),则执行后续代码。
if not (im_rates > im_rate_thr).any():
# 找到所有窗口的图像覆盖率中的最大值( max_rate )。 im_rates.max() :返回 im_rates 数组中的最大值。 这个最大值表示在当前情况下,哪个窗口的图像覆盖率是最高的,尽管它仍然低于阈值。
max_rate = im_rates.max()
# 将与最大覆盖率( max_rate )非常接近的窗口的覆盖率设置为 1 。
# abs(im_rates - max_rate) :计算每个窗口的覆盖率与最大覆盖率的差的绝对值。
# < eps :判断这些差值是否小于一个极小值 eps (如 0.01 )。
# im_rates[...] = 1 :将满足条件的窗口的覆盖率设置为 1。
# 这一步的目的是为了在没有任何窗口满足阈值要求的情况下,选择一个“足够接近”的窗口作为“最佳”窗口。通过这种方式,确保至少有一个窗口被选中。
im_rates[abs(im_rates - max_rate) < eps] = 1
# 返回满足图像覆盖率阈值的窗口坐标数组。
return windows[im_rates > im_rate_thr]
# 这段代码的主要功能是生成图像的滑动窗口,并筛选出满足图像覆盖率要求的窗口。主要步骤如下。根据图像尺寸和窗口参数(大小、重叠)计算窗口的起始和结束坐标。确保窗口不会超出图像边界。计算每个窗口的图像覆盖率(实际图像区域面积与窗口面积的比值)。筛选出覆盖率超过阈值的窗口。返回满足条件的窗口坐标。这种窗口划分方法在处理大图像时非常有用,例如在目标检测、语义分割等任务中,可以将大图像划分为多个小块进行处理,同时确保每个窗口包含足够的图像内容。
# def get_windows(im_size, crop_sizes=(1024,), gaps=(200,), im_rate_thr=0.6, eps=0.01):
# -> 用于生成图像的滑动窗口(crop windows),并根据一定的条件筛选出满足图像覆盖率要求的窗口。它主要用于图像分割、目标检测等任务中,对大图像进行分块处理。返回满足图像覆盖率阈值的窗口坐标数组。
# -> return windows[im_rates > im_rate_thr]
5.def get_window_obj(anno, windows, iof_thr=0.7):
# 这段代码定义了一个函数 get_window_obj ,用于将一个图像的注释信息( anno )分配到多个滑动窗口( windows )中。它通过计算每个目标( label )与每个窗口的交并比(IOF),并根据设定的阈值( iof_thr )筛选出属于每个窗口的目标。
# 这行代码定义了函数 get_window_obj ,接受以下参数 :
# 1.anno :一个字典,包含图像的注释信息,例如原始尺寸和标签。
# 2.windows :一个数组,包含多个窗口的坐标(每个窗口是一个 (x1, y1, x2, y2) 的数组)。
# 3.iof_thr :交并比(IOF)的阈值,默认为 0.7 ,用于判断目标是否属于某个窗口。
def get_window_obj(anno, windows, iof_thr=0.7):
# 获取每个窗口的对象。
"""Get objects for each window."""
# 从注释信息 anno 中提取图像的 原始高度 h 和 宽度 w 。
h, w = anno["ori_size"]
# 从注释信息 anno 中提取 标签数据 label 。 label 是一个二维数组,其中每一行表示一个目标的 注释信息 。通常格式为 [class_id, x_center, y_center, width, height] (归一化坐标)。
label = anno["label"]
# 检查是否有目标( label )存在。如果 label 的长度为 0,则表示图像中没有目标。
if len(label):
# 将 目标 的 宽度 和 高度 坐标从归一化坐标转换为绝对坐标。 label[:, 1::2] 表示 label 中的宽度和高度列(归一化值)。 通过乘以图像宽度 w ,将其转换为 绝对像素值 。
label[:, 1::2] *= w
# 将 目标的中心点 的 y 坐标从归一化坐标转换为绝对坐标。 label[:, 2::2] 表示 label 中的中心点 y 坐标列(归一化值)。 通过乘以图像高度 h ,将其转换为 绝对像素值 。
label[:, 2::2] *= h
# 计算 每个目标 与 每个窗口 的交并比(IOF)。
# label[:, 1:] :提取目标的边界框坐标( x_center, y_center, width, height )。
# windows :窗口的坐标( x1, y1, x2, y2 )。
# bbox_iof :一个函数,用于计算目标与窗口的交并比(IOF)。
# iofs 是一个二维数组,形状为 (num_labels, num_windows) ,表示每个目标与每个窗口的 IOF 值。
iofs = bbox_iof(label[:, 1:], windows)
# Unnormalized and misaligned coordinates
# 根据 IOF 阈值筛选出 属于每个窗口的目标 。
# iofs[:, i] >= iof_thr :对于每个窗口 i ,提取与该窗口的 IOF 大于阈值 iof_thr 的目标索引。
# label[...] :根据索引提取 属于该窗口的目标 。
# 使用列表推导式,为每个窗口生成一个目标数组。
# 返回值是一个列表,其中每个元素是一个窗口的目标数组。
return [(label[iofs[:, i] >= iof_thr]) for i in range(len(windows))] # window_anns
# 处理图像中没有目标的情况。
else:
# 目标数组的格式 :
# 在目标检测任务中,每个目标通常由一个包含 9 个元素的数组表示,格式为 [class_id, x_center, y_center, width, height, ...] 。
# 其中, class_id 是目标的类别, x_center 和 y_center 是目标中心点的坐标, width 和 height 是目标的宽度和高度。剩余的元素可能包含其他信息,如目标的旋转角度、置信度等。
# 如果 label 的长度为 0,则返回一个空的注释列表。
# 每个窗口的目标数组是一个形状为 (0, 9) 的零数组,表示没有目标。
# 返回值是一个长度为 len(windows) 的列表,每个元素是一个空数组。
return [np.zeros((0, 9), dtype=np.float32) for _ in range(len(windows))] # window_anns
# 这段代码的主要功能是将图像的注释信息分配到多个滑动窗口中。具体步骤如下。提取图像的原始尺寸和标签信息。如果有目标:将目标的归一化坐标转换为绝对坐标。计算每个目标与每个窗口的交并比(IOF)。根据 IOF 阈值筛选出属于每个窗口的目标。如果没有目标:返回一个空的注释列表,每个窗口对应一个空数组。这种分配方式在目标检测任务中非常常见,特别是在处理大图像时,通过滑动窗口将大图像划分为多个小块,并将目标分配到对应的窗口中,以便进行分块处理。
# def get_window_obj(anno, windows, iof_thr=0.7):
# -> 用于将一个图像的注释信息( anno )分配到多个滑动窗口( windows )中。返回值是一个列表,其中每个元素是一个窗口的目标数组。返回值是一个长度为 len(windows) 的列表,每个元素是一个空数组。
# -> return [(label[iofs[:, i] >= iof_thr]) for i in range(len(windows))] / return [np.zeros((0, 9), dtype=np.float32) for _ in range(len(windows))]
6.def crop_and_save(anno, windows, window_objs, im_dir, lb_dir, allow_background_images=True):
# 这段代码定义了一个函数 crop_and_save ,用于根据滑动窗口对图像进行裁剪,并将裁剪后的图像及其对应的标签保存到指定的目录中。
# 这行代码定义了函数 crop_and_save ,接受以下参数 :
# 1.anno :一个包含图像注释信息的字典,例如文件路径、原始尺寸等。
# 2.windows :一个数组,包含每个滑动窗口的坐标 [x_start, y_start, x_stop, y_stop] 。
# 3.window_objs :一个列表,包含每个窗口对应的目标标签。
# 4.im_dir :裁剪后的图像保存目录。
# 5.lb_dir :裁剪后的标签文件保存目录。
# 6.allow_background_images :一个布尔值,默认为 True ,表示是否允许保存没有目标的背景图像。
def crop_and_save(anno, windows, window_objs, im_dir, lb_dir, allow_background_images=True):
# 裁剪图像并保存新标签。
# 注释:
# DOTA 数据集的假定目录结构:
# - data_root
# - images
# - train
# - val
# - labels
# - train
# - val
"""
Crop images and save new labels.
Args:
anno (dict): Annotation dict, including `filepath`, `label`, `ori_size` as its keys.
windows (list): A list of windows coordinates.
window_objs (list): A list of labels inside each window.
im_dir (str): The output directory path of images.
lb_dir (str): The output directory path of labels.
allow_background_images (bool): Whether to include background images without labels.
Notes:
The directory structure assumed for the DOTA dataset:
- data_root
- images
- train
- val
- labels
- train
- val
"""
# 使用 OpenCV 的 cv2.imread 函数读取图像文件。图像路径从注释字典 anno 中的 "filepath" 键获取。
im = cv2.imread(anno["filepath"])
# 使用 pathlib.Path 获取图像文件的文件名(不包含扩展名)。 stem 属性会去掉文件名中的扩展名,例如 "image.jpg" 会变成 "image" 。
name = Path(anno["filepath"]).stem
# 开始一个循环,遍历 每个滑动窗口 及 其索引 i 。
for i, window in enumerate(windows):
# 将滑动窗口的坐标 [x_start, y_start, x_stop, y_stop] 分别赋值给对应的变量。
x_start, y_start, x_stop, y_stop = window.tolist()
# 生成裁剪后图像的新文件名。文件名格式为 "原文件名__窗口宽度__x_start___y_start" ,用于 区分不同的裁剪窗口 。
new_name = f"{name}__{x_stop - x_start}__{x_start}___{y_start}"
# 从原始图像 im 中裁剪出当前窗口对应的区域,生成裁剪后的图像 patch_im 。
patch_im = im[y_start:y_stop, x_start:x_stop]
# 获取裁剪后图像的高度 ph 和宽度 pw 。
ph, pw = patch_im.shape[:2]
# 获取 当前窗口 对应的 目标标签 。
label = window_objs[i]
# 检查当前窗口是否有目标( len(label) > 0 ),或者是否允许保存背景图像( allow_background_images=True )。 如果满足任一条件,则保存裁剪后的图像。
if len(label) or allow_background_images:
# 将 裁剪后的图像 保存到指定的目录 im_dir 中,文件名为 new_name.jpg 。
cv2.imwrite(str(Path(im_dir) / f"{new_name}.jpg"), patch_im)
# 检查当前窗口是否有目标。如果有目标,则需要保存对应的标签文件。
if len(label):
# 将目标的 x 坐标从全局坐标系转换为裁剪窗口的局部坐标系。 通过减去 x_start ,将 目标的中心点 x 坐标 和 宽度坐标 调整到裁剪窗口的范围内。
label[:, 1::2] -= x_start
# 将目标的 y 坐标从全局坐标系转换为裁剪窗口的局部坐标系。 通过减去 y_start ,将 目标的中心点 y 坐标 和 高度坐标 调整到裁剪窗口的范围内。
label[:, 2::2] -= y_start
# 将 目标的 x 坐标 和 宽度坐标 归一化到裁剪窗口的宽度范围内。 通过除以裁剪窗口的宽度 pw ,将绝对坐标转换为归一化坐标。
label[:, 1::2] /= pw
# 将 目标的 y 坐标 和 高度坐标 归一化到裁剪窗口的高度范围内。 通过除以裁剪窗口的高度 ph ,将绝对坐标转换为归一化坐标。
label[:, 2::2] /= ph
# 打开(或创建)裁剪窗口对应的标签文件,文件名格式为 new_name.txt 。
with open(Path(lb_dir) / f"{new_name}.txt", "w") as f:
# 开始一个循环,遍历 当前窗口的所有目标标签 。
for lb in label:
# 将目标的坐标格式化为字符串,保留 6 位有效数字。
formatted_coords = [f"{coord:.6g}" for coord in lb[1:]]
# 将 目标的类别 和 坐标 写入标签文件。
# int(lb[0]) :目标的类别 ID。
# ' '.join(formatted_coords) :目标的坐标字符串。
# 每个目标占一行,格式为 class_id x_center y_center width height 。
f.write(f"{int(lb[0])} {' '.join(formatted_coords)}\n")
# 这段代码的主要功能是根据滑动窗口对图像进行裁剪,并将裁剪后的图像及其对应的标签保存到指定的目录中。具体步骤如下。读取原始图像。遍历每个滑动窗口:裁剪出窗口对应的图像区域。如果窗口包含目标或允许保存背景图像,则保存裁剪后的图像。如果窗口包含目标,则将目标坐标从全局坐标系转换为局部坐标系,并归一化到裁剪窗口的范围内。将目标标签写入对应的文本文件。通过这种方式,可以将大图像划分为多个小块,并为每个小块生成对应的图像和标签文件,适用于目标检测任务中的分块处理。
# def crop_and_save(anno, windows, window_objs, im_dir, lb_dir, allow_background_images=True): -> 用于根据滑动窗口对图像进行裁剪,并将裁剪后的图像及其对应的标签保存到指定的目录中。
7.def split_images_and_labels(data_root, save_dir, split="train", crop_sizes=(1024,), gaps=(200,)):
# 这段代码定义了一个函数 split_images_and_labels ,用于将大图像分割成多个小块(滑动窗口),并将对应的标签分配到每个小块中,最后将分割后的图像和标签保存到指定的目录中。
# 这行代码定义了函数 split_images_and_labels ,接受以下参数 :
# 1.data_root :原始数据集的根目录路径。
# 2.save_dir :保存分割后图像和标签的根目录路径。
# 3.split :数据集的划分,如 "train" 或 "val" ,默认为 "train" 。
# 4.crop_sizes :一个元组,表示每个滑动窗口的大小,默认为 (1024,) 。
# 5.gaps :一个元组,表示每个滑动窗口之间的重叠大小,默认为 (200,) 。
def split_images_and_labels(data_root, save_dir, split="train", crop_sizes=(1024,), gaps=(200,)):
# 分割图像和标签。
# 注意:
# DOTA 数据集的目录结构假设为:
# - data_root
# - images
# - split
# - labels
# - split
# 输出目录结构为:
# - save_dir
# - images
# - split
# - labels
# - split
"""
Split both images and labels.
Notes:
The directory structure assumed for the DOTA dataset:
- data_root
- images
- split
- labels
- split
and the output directory structure is:
- save_dir
- images
- split
- labels
- split
"""
# 构建 保存分割后图像 的目录路径。 使用 pathlib.Path 拼接路径,格式为 save_dir/images/split 。
im_dir = Path(save_dir) / "images" / split
# 创建保存图像的目录。
# parents=True :如果父目录不存在,则一并创建。
# exist_ok=True :如果目录已存在,不会报错。
im_dir.mkdir(parents=True, exist_ok=True)
# 构建 保存分割后标签 的目录路径。 格式为 save_dir/labels/split 。
lb_dir = Path(save_dir) / "labels" / split
# 创建保存标签的目录,逻辑与创建图像目录相同。
lb_dir.mkdir(parents=True, exist_ok=True)
# 调用 load_yolo_dota 函数加 载数据集的注释信息 。 load_yolo_dota 会从 data_root 中读取图像和标签文件,并返回一个包含注释信息的列表 annos 。
# def load_yolo_dota(data_root, split="train"):
# -> 用于加载用于目标检测任务(特别是 DOTA 数据集)的 YOLO 格式数据。它从指定的数据根目录中读取图像和标签文件,并将它们组织成一个包含图像尺寸、标签和文件路径的字典列表。返回 最终的注释信息列表 annos 。
# -> return annos
annos = load_yolo_dota(data_root, split=split)
# 开始一个循环,遍历每个注释信息 anno 。 使用 TQDM (是一个进度条工具,如 tqdm )显示进度。
# total=len(annos) :设置进度条的总长度。
# desc=split :设置进度条的描述信息,如 "train" 或 "val" 。
for anno in TQDM(annos, total=len(annos), desc=split):
# 调用 get_windows 函数,根据图像的原始尺寸和滑动窗口参数( crop_sizes 和 gaps ),生成 滑动窗口的坐标 。
# def get_windows(im_size, crop_sizes=(1024,), gaps=(200,), im_rate_thr=0.6, eps=0.01):
# -> 用于生成图像的滑动窗口(crop windows),并根据一定的条件筛选出满足图像覆盖率要求的窗口。它主要用于图像分割、目标检测等任务中,对大图像进行分块处理。返回满足图像覆盖率阈值的窗口坐标数组。
# -> return windows[im_rates > im_rate_thr]
windows = get_windows(anno["ori_size"], crop_sizes, gaps)
# 调用 get_window_obj 函数,将 注释信息中的目标 分配到 每个滑动窗口中 。 返回值 window_objs 是一个列表, 每个元素对应一个窗口的目标标签 。
# def get_window_obj(anno, windows, iof_thr=0.7):
# -> 用于将一个图像的注释信息( anno )分配到多个滑动窗口( windows )中。返回值是一个列表,其中每个元素是一个窗口的目标数组。返回值是一个长度为 len(windows) 的列表,每个元素是一个空数组。
# -> return [(label[iofs[:, i] >= iof_thr]) for i in range(len(windows))] / return [np.zeros((0, 9), dtype=np.float32) for _ in range(len(windows))]
window_objs = get_window_obj(anno, windows)
# 调用 crop_and_save 函数,对每个滑动窗口进行裁剪,并将裁剪后的图像和标签保存到指定的目录中。 str(im_dir) 和 str(lb_dir) 是保存图像和标签的目录路径。
# def crop_and_save(anno, windows, window_objs, im_dir, lb_dir, allow_background_images=True): -> 用于根据滑动窗口对图像进行裁剪,并将裁剪后的图像及其对应的标签保存到指定的目录中。
crop_and_save(anno, windows, window_objs, str(im_dir), str(lb_dir))
# 这段代码的主要功能是将大图像分割成多个小块,并将对应的标签分配到每个小块中,最后将分割后的图像和标签保存到指定的目录中。具体步骤如下。创建保存分割后图像和标签的目录。加载数据集的注释信息。遍历每个图像的注释信息:根据图像尺寸和滑动窗口参数生成窗口坐标。将目标分配到每个窗口中。对每个窗口进行裁剪,并保存裁剪后的图像和标签。使用进度条显示处理进度。这种处理方式在处理大图像数据集时非常常见,特别是在目标检测任务中。通过将大图像分割成小块,可以降低内存占用,同时提高模型训练和推理的效率。
# def split_images_and_labels(data_root, save_dir, split="train", crop_sizes=(1024,), gaps=(200,)): -> 用于将大图像分割成多个小块(滑动窗口),并将对应的标签分配到每个小块中,最后将分割后的图像和标签保存到指定的目录中。
8.def split_trainval(data_root, save_dir, crop_size=1024, gap=200, rates=(1.0,)):
# 这段代码定义了一个函数 split_trainval ,用于对数据集进行训练集和验证集的分割,并对每个分割后的数据集执行图像分割和标签分配操作。
# 这行代码定义了函数 split_trainval ,接受以下参数 :
# 1.data_root :原始数据集的根目录路径。
# 2.save_dir :保存分割后图像和标签的根目录路径。
# 3.crop_size :滑动窗口的默认大小,默认为 1024 。
# 4.gap :滑动窗口之间的默认重叠大小,默认为 200 。
# 5.rates :一个元组,表示窗口大小和重叠大小的缩放比例,默认为 (1.0,) 。
def split_trainval(data_root, save_dir, crop_size=1024, gap=200, rates=(1.0,)):
# 拆分 DOTA 的训练和验证集。
# 注意:
# DOTA 数据集的目录结构假设为:
# - data_root
# - images
# - train
# - val
# - labels
# - train
# - val
# 输出目录结构为:
# - save_dir
# - images
# - train
# - val
# - labels
# - train
# - val
"""
Split train and val set of DOTA.
Notes:
The directory structure assumed for the DOTA dataset:
- data_root
- images
- train
- val
- labels
- train
- val
and the output directory structure is:
- save_dir
- images
- train
- val
- labels
- train
- val
"""
# 初始化两个空列表 crop_sizes 和 gaps ,用于存储根据缩放比例调整后的 窗口大小 和 重叠大小 。
crop_sizes, gaps = [], []
# 开始一个循环,遍历每个 缩放比例 r 。
for r in rates:
# 根据缩放比例 r 计算 调整后的窗口大小 ,并将其添加到 crop_sizes 列表中。
# crop_size / r :根据比例调整窗口大小。
# int(...) :将结果转换为整数。
crop_sizes.append(int(crop_size / r))
# 根据缩放比例 r 计算 调整后的重叠大小 ,并将其添加到 gaps 列表中。
# gap / r :根据比例调整重叠大小。
# int(...) :将结果转换为整数。
gaps.append(int(gap / r))
# 开始一个循环,分别处理训练集( "train" )和验证集( "val" )。
for split in ["train", "val"]:
# 调用 split_images_and_labels 函数,对当前数据集划分( split )执行 图像分割 和 标签分配 操作。
# data_root :原始数据集的根目录。
# save_dir :保存分割后图像和标签的目录。
# split :当前处理的数据集划分( "train" 或 "val" )。
# crop_sizes 和 gaps :根据缩放比例调整后的 窗口大小 和 重叠大小 。
# def split_images_and_labels(data_root, save_dir, split="train", crop_sizes=(1024,), gaps=(200,)): -> 用于将大图像分割成多个小块(滑动窗口),并将对应的标签分配到每个小块中,最后将分割后的图像和标签保存到指定的目录中。
split_images_and_labels(data_root, save_dir, split, crop_sizes, gaps)
# 这段代码的主要功能是根据指定的缩放比例对数据集进行训练集和验证集的分割,并对每个分割后的数据集执行图像分割和标签分配操作。具体步骤如下。根据缩放比例 rates 调整窗口大小和重叠大小。遍历训练集和验证集:对每个数据集划分调用 split_images_and_labels 函数。执行图像分割和标签分配操作。通过这种方式,可以灵活地处理不同大小的图像块,并为训练和验证集生成对应的图像和标签文件。这种处理方式在目标检测和分割任务中非常常见,尤其是在处理大规模数据集时,通过调整窗口大小和重叠大小,可以更好地适应不同尺度的目标。
# 通过一个具体的例子来说明 split_trainval 函数的功能。假设有一个目标检测数据集(例如 DOTA 数据集),其中包含大尺寸的图像和对应的标签文件。目标是将这些图像分割成多个小块(滑动窗口),并为每个小块生成对应的标签文件,同时将这些小块分别保存到训练集和验证集的目录中。
# 示例场景 :
# 假设有以下数据集结构 :
# data_root/
# ├── images/
# │ ├── train/
# │ │ ├── image1.jpg
# │ │ ├── image2.jpg
# │ │ └── ...
# │ └── val/
# │ ├── image1.jpg
# │ ├── image2.jpg
# │ └── ...
# └── labels/
# ├── train/
# │ ├── image1.txt
# │ ├── image2.txt
# │ └── ...
# └── val/
# ├── image1.txt
# ├── image2.txt
# └── ...
# 目标是将这些大图像分割成多个小块,并将分割后的图像和标签保存到新的目录中 :
# save_dir/
# ├── images/
# │ ├── train/
# │ │ ├── image1__1024__0___0.jpg
# │ │ ├── image1__1024__1024___0.jpg
# │ │ └── ...
# │ └── val/
# │ ├── image1__1024__0___0.jpg
# │ ├── image1__1024__1024___0.jpg
# │ └── ...
# └── labels/
# ├── train/
# │ ├── image1__1024__0___0.txt
# │ ├── image1__1024__1024___0.txt
# │ └── ...
# └── val/
# ├── image1__1024__0___0.txt
# ├── image1__1024__1024___0.txt
# └── ...
# 调用 split_trainval 函数 :
# 假设调用函数如下 :
# split_trainval(data_root="/path/to/data_root",
# save_dir="/path/to/save_dir",
# crop_size=1024,
# gap=200,
# rates=(1.0, 0.5))
# 函数执行过程 :
# 计算窗口大小和重叠大小 :
# crop_size = 1024 , gap = 200 。
# rates = (1.0, 0.5) 表示两种窗口大小 :
# 第一种 : crop_size = 1024 , gap = 200 。
# 第二种 : crop_size = 1024 / 0.5 = 2048 , gap = 200 / 0.5 = 400 。
# 因此, crop_sizes = [1024, 2048] , gaps = [200, 400] 。
# 处理训练集和验证集 :
# 遍历 "train" 和 "val" 两个划分。
# 对每个划分 :调用 split_images_and_labels 函数,将图像分割成多个小块。 为每个小块生成对应的标签文件。 将分割后的图像和标签保存到 save_dir 中对应的目录。
# 具体处理过程 :
# 假设处理的是 "train" 划分中的 image1.jpg ,其尺寸为 (4096, 4096) 。
# 第一步 :生成滑动窗口 :
# 第一种窗口大小 : crop_size = 1024 , gap = 200 。
# 窗口步长 step = 1024 - 200 = 824 。
# 水平方向窗口数量 : ceil((4096 - 1024) / 824 + 1) = 5 。
# 垂直方向窗口数量: ceil((4096 - 1024) / 824 + 1) = 5 。
# 生成的窗口坐标如下(以左上角为起点) :
# (0, 0, 1024, 1024)
# (824, 0, 1848, 1024)
# (1648, 0, 2672, 1024)
# (2472, 0, 3496, 1024)
# (3296, 0, 4320, 1024) (超出部分会被裁剪)
# 以此类推,垂直方向也有类似的窗口。
# 第二种窗口大小 : crop_size = 2048 , gap = 400 。
# 窗口步长 step = 2048 - 400 = 1648 。
# 水平方向窗口数量 : ceil((4096 - 2048) / 1648 + 1) = 2 。
# 垂直方向窗口数量 : ceil((4096 - 2048) / 1648 + 1) = 2 。
# 生成的窗口坐标如下 :
# (0, 0, 2048, 2048)
# (1648, 0, 3696, 2048) (超出部分会被裁剪)
# 以此类推。
# 第二步 :裁剪图像和分配标签 :
# 对每个窗口 :
# 裁剪出图像块。
# 将目标标签分配到对应的窗口中(通过计算目标与窗口的交并比 IOF)。
# 如果窗口中有目标或允许保存背景图像,则保存裁剪后的图像和标签文件。
# 第三步 :保存结果 :
# 裁剪后的图像保存到 save_dir/images/train 中,文件名格式为 image1__1024__0___0.jpg 。
# 对应的标签文件保存到 save_dir/labels/train 中,文件名格式为 image1__1024__0___0.txt 。
# 最终结果 :
# 通过 split_trainval 函数,我们完成了以下任务 :
# 将大图像分割成多个小块(滑动窗口)。
# 为每个小块生成对应的标签文件。
# 将分割后的图像和标签分别保存到训练集和验证集的目录中。
# 支持多种窗口大小和重叠大小(通过缩放比例 rates )。
# 这种处理方式特别适合处理大图像数据集,例如遥感图像或全景图像,能够有效地将大图像划分为适合模型处理的小块,同时保留目标信息。
9.def split_test(data_root, save_dir, crop_size=1024, gap=200, rates=(1.0,)):
# 这段代码定义了一个函数 split_test ,用于处理测试集图像,将大图像分割成多个小块(滑动窗口),并将这些小块保存到指定的目录中。与 split_trainval 函数类似,但它只处理测试集图像,而不涉及标签的分配和保存。
# 这行代码定义了函数 split_test ,接受以下参数 :
# 1.data_root :原始数据集的根目录路径。
# 2.save_dir :保存分割后图像的根目录路径。
# 3.crop_size :滑动窗口的默认大小,默认为 1024 。
# 4.gap :滑动窗口之间的默认重叠大小,默认为 200 。
# 5.rates :一个元组,表示窗口大小和重叠大小的缩放比例,默认为 (1.0,) 。
def split_test(data_root, save_dir, crop_size=1024, gap=200, rates=(1.0,)):
# DOTA 的拆分测试集,标签不包含在该集合中。
# 注释:
# DOTA 数据集的假定目录结构为:
# - data_root
# - images
# - test
# 输出目录结构为:
# - save_dir
# - images
# - test
"""
Split test set of DOTA, labels are not included within this set.
Notes:
The directory structure assumed for the DOTA dataset:
- data_root
- images
- test
and the output directory structure is:
- save_dir
- images
- test
"""
# 初始化两个空列表 crop_sizes 和 gaps ,用于 存储根据缩放比例调整后的 窗口大小 和 重叠大小 。
crop_sizes, gaps = [], []
# 开始一个循环,遍历每个 缩放比例 r 。
for r in rates:
# 根据缩放比例 r 计算调整后的窗口大小 ,并将其添加到 crop_sizes 列表中。
# crop_size / r :根据比例调整窗口大小。
# int(...) :将结果转换为整数。
crop_sizes.append(int(crop_size / r))
# 根据缩放比例 r 计算调整后的重叠大小 ,并将其添加到 gaps 列表中。
# gap / r :根据比例调整重叠大小。
# int(...) :将结果转换为整数。
gaps.append(int(gap / r))
# 构建保存分割后图像的目录路径。 格式为 save_dir/images/test 。
save_dir = Path(save_dir) / "images" / "test"
# 创建保存图像的目录。
# parents=True :如果父目录不存在,则一并创建。
# exist_ok=True :如果目录已存在,不会报错。
save_dir.mkdir(parents=True, exist_ok=True)
# 构建 测试集图像的源目录路径 。格式为 data_root/images/test 。
im_dir = Path(data_root) / "images" / "test"
# 检查测试集图像目录是否存在。 如果目录不存在,会抛出断言错误,提示用户检查数据根目录。
assert im_dir.exists(), f"Can't find {im_dir}, please check your data root." # 找不到 {im_dir},请检查您的数据根目录。
# 获取测试集目录中的所有图像文件路径。 使用 glob 函数匹配路径下的所有文件。
im_files = glob(str(im_dir / "*"))
# 开始一个循环,遍历每个图像文件。 使用 TQDM (是一个进度条工具,如 tqdm )显示进度。
# total=len(im_files) :设置进度条的总长度。
# desc="test" :设置进度条的描述信息为 "test" 。
for im_file in TQDM(im_files, total=len(im_files), desc="test"):
# 获取图像的 宽度 和 高度 。 使用 Image.open(im_file) 打开图像文件。 调用 exif_size 函数获取经过 EXIF 信息校正后的图像宽度和高度。
# def exif_size(img: Image.Image): -> 它接收一个 PIL 图像对象 img ,并返回经过 EXIF 信息校正后的图像尺寸。返回经过 EXIF 信息 校正后的图像尺寸 。如果图像没有 EXIF 信息或不需要调整,返回的将是原始尺寸。 -> return s
w, h = exif_size(Image.open(im_file))
# 调用 get_windows 函数,根据图像尺寸和滑动窗口参数生成 窗口坐标 。 get_windows 函数会根据 crop_sizes 和 gaps 计算每个窗口的起始和结束坐标。
# def get_windows(im_size, crop_sizes=(1024,), gaps=(200,), im_rate_thr=0.6, eps=0.01):
# -> 用于生成图像的滑动窗口(crop windows),并根据一定的条件筛选出满足图像覆盖率要求的窗口。它主要用于图像分割、目标检测等任务中,对大图像进行分块处理。返回满足图像覆盖率阈值的窗口坐标数组。
# -> return windows[im_rates > im_rate_thr]
windows = get_windows((h, w), crop_sizes=crop_sizes, gaps=gaps)
# 使用 OpenCV 的 cv2.imread 函数读取图像文件。
im = cv2.imread(im_file)
# 获取 图像文件的文件名 (不包含扩展名)。 使用 Path(im_file).stem 获取文件名。
name = Path(im_file).stem
# 开始一个循环,遍历每个 滑动窗口 。
for window in windows:
# 将窗口的坐标 [x_start, y_start, x_stop, y_stop] 分别赋值给对应的变量。
x_start, y_start, x_stop, y_stop = window.tolist()
# 生成裁剪后图像的新文件名。 格式为 "原文件名__窗口宽度__x_start___y_start" 。
new_name = f"{name}__{x_stop - x_start}__{x_start}___{y_start}"
# 从原始图像中裁剪出当前窗口对应的区域,生成裁剪后的图像 patch_im 。
patch_im = im[y_start:y_stop, x_start:x_stop]
# 将裁剪后的图像保存到指定的目录中。 文件名格式为 new_name.jpg 。
cv2.imwrite(str(save_dir / f"{new_name}.jpg"), patch_im)
# 这段代码的主要功能是将测试集中的大图像分割成多个小块(滑动窗口),并将这些小块保存到指定的目录中。具体步骤如下。根据缩放比例计算窗口大小和重叠大小。创建保存分割后图像的目录。遍历测试集中的每个图像:获取图像的宽度和高度。根据图像尺寸和窗口参数生成滑动窗口的坐标。对每个窗口进行裁剪,并保存裁剪后的图像。使用进度条显示处理进度。这种处理方式特别适合处理测试集中的大图像,例如在目标检测或分割任务中,将大图像分割成小块以便于模型处理。
10.if __name__ == "__main__":
# 这段代码是 Python 脚本的入口部分,用于在脚本直接运行时(而不是作为模块导入时)调用 split_trainval 和 split_test 函数,对 DOTA 数据集进行处理。
# 检查脚本是否作为主程序运行。 如果脚本被直接运行(而不是作为模块被其他脚本导入), __name__ 的值会是 "__main__" 。 这是 Python 中常见的入口点检查方式,用于确保某些代码只在脚本直接运行时执行。
if __name__ == "__main__":
# 调用 split_trainval 函数,对 DOTA 数据集的训练集和验证集进行处理。
# data_root="DOTAv2" :指定原始数据集的根目录为 "DOTAv2" 。
# save_dir="DOTAv2-split" :指定保存分割后图像和标签的目录为 "DOTAv2-split" 。
# 这个函数会 :对训练集和验证集的图像进行分割。为每个分割后的图像块生成对应的标签文件。将结果保存到 save_dir 中的 images/train 和 labels/train ,以及 images/val 和 labels/val 。
split_trainval(data_root="DOTAv2", save_dir="DOTAv2-split")
# 调用 split_test 函数,对 DOTA 数据集的测试集进行处理。
# data_root="DOTAv2" :指定原始数据集的根目录为 "DOTAv2" 。
# save_dir="DOTAv2-split" :指定保存分割后图像的目录为 "DOTAv2-split" 。
# 这个函数会 :对测试集的图像进行分割。将每个分割后的图像块保存到 save_dir/images/test 中。
split_test(data_root="DOTAv2", save_dir="DOTAv2-split")
# 功能说明 :
# 假设有一个 DOTA 数据集,其目录结构如下 :
# DOTAv2/
# ├── images/
# │ ├── train/
# │ ├── val/
# │ └── test/
# └── labels/
# ├── train/
# └── val/
# 运行这段代码后,会生成一个新的目录结构 :
# DOTAv2-split/
# ├── images/
# │ ├── train/
# │ ├── val/
# │ └── test/
# └── labels/
# ├── train/
# └── val/
# 这段代码的作用是将 DOTA 数据集的训练集、验证集和测试集中的大图像分割成多个小块,并将分割后的图像和标签保存到新的目录中。具体步骤如下。对训练集和验证集:分割图像。分配标签。保存图像和标签。对测试集:分割图像。保存图像。这种处理方式非常适合处理大规模图像数据集,能够将大图像划分为适合模型处理的小块,同时保留目标信息。