对象检测是迄今为止计算机视觉中最重要的应用领域。然而,小物体的检测和大图像的推理仍然是实际使用中的主要问题,这是因为小目标物体有效特征少,覆盖范围少。小目标物体的定义通常有两种方式。一种是绝对尺度定义,即以物体的像素尺寸来判断是否为小目标,如在COCO数据集中,尺寸小于32×32像素的目标被判定为小目标。另外一种是相对尺度定义,即以物体在图像中的占比面积比例来判断是否为小目标,例如国际光学工程学会SPIE定义,若目标尺寸小于原图的0.12%则可以判定成小目标。
SAHI: Slicing Aided Hyper Inference(切片辅助超推理)通过图像切片的方式来检测小目标。SAHI检测过程可以描述为:通过滑动窗口将图像切分成若干区域,各个区域分别进行预测,同时也对整张图片进行推理。然后将各个区域的预测结果和整张图片的预测结果合并,最后用NMS(非极大值抑制)进行过滤。用动图表示该识别过程如下:
SAHI的官方仓库地址为:sahi。关于SAHI的使用可以阅读官方demo和官方文档:sahi-demo和sahi-docs。如果想进一步了解SAHI具体工作性能和原理,可以阅读官方发表的论文:Slicing Aided Hyper Inference and Fine-Tuning for Small Object Detection。
SAHI安装指令如下:
pip install sahi
本文所有算法展示效果和代码见:
github: Python-Study-Notes
文章目录
- 1 SAHI使用
- 1.1 图像切片
- 1.1.1 单张图像切片
- 1.1.2 COCO数据集切片
- 1.2 图像预测
- 1.2.1 接口介绍
- 1.2.2 应用实例
- 1.3 SAHI工具函数
- 1.3.1 coco数据集制作与精度分析
- 1.3.2 coco数据集处理
- 1.3.3 coco数据集转换
- 1.4 总结
- 2 参考
1 SAHI使用
import sahi # 打印sahi版本 print(sahi.__version__)
0.11.6
1.1 图像切片
SAHI提供了封装好的函数接口,以切分输入图像和其标注数据。切分后的子图及其标注数据可以用于识别,或者保存为本地数据以供模型训练。
1.1.1 单张图像切片
SAHI提供slice_image函数以切分单张图片及其标注文件(仅支持coco标注文件),slice_image函数接口介绍如下:
# 返回SAHI的图像分片结果类SliceImageResult def slice_image( image: Union[str, Image.Image], # 单张图像地址或单个pillow image对象,必填参数 coco_annotation_list: Optional[CocoAnnotation] = None, # coco标注文件 output_file_name: Optional[str] = None, # 输出文件名前缀 output_dir: Optional[str] = None, # 输出文件地址 slice_height: int = None, # 子图切分高度 slice_width: int = None, # 子图切分宽度 overlap_height_ratio: float = None, # 子图高度间的重叠率 overlap_width_ratio: float = None, # 子图宽度间的重叠率 auto_slice_resolution: bool = True, # 如果没有设置slice_height和slice_width,则自动确定slice_height、slice_width、overlap_height_ratio、overlap_width_ratio min_area_ratio: float = 0.1, # 子图中标注框小于原始标注框占比,则放弃该标注框 out_ext: Optional[str] = None, # 图像后缀格式 verbose: bool = False, # 是否打印详细信息 )
slice_image函数源代码位于sahi/slicing.py中,这段代码可以单步调试看看怎么运行的,主要逻辑如下:
-
获得pillow image图像对象
-
调用get_slice_bboxes函数切分图像
- 获得切分参数
if slice_height and slice_width: # 计算重叠像素 y_overlap = int(overlap_height_ratio * slice_height) x_overlap = int(overlap_width_ratio * slice_width) elif auto_slice_resolution: x_overlap, y_overlap, slice_width, slice_height = get_auto_slice_params(height=image_height, width=image_width)
- 循环切分图像
# 行循环 while y_max image_height or x_max > image_width: xmax = min(image_width, x_max) ymax = min(image_height, y_max) xmin = max(0, xmax - slice_width) ymin = max(0, ymax - slice_height) slice_bboxes.append([xmin, ymin, xmax, ymax]) else: slice_bboxes.append([x_min, y_min, x_max, y_max]) # 下一次切分从本次切分图像x_max-x_overlap开始 x_min = x_max - x_overlap y_min = y_max - y_overlap
-
保存图片结果和标注结果,并包装返回SliceImageResult对象
- 循环切分图像
- 获得切分参数
以下代码演示了对单张图片进行切片,并将切分后的子图保存到本地。
展示原图
# 展示输入图片 from PIL import Image # 图像地址:https://github.com/obss/sahi/tree/main/demo/demo_data image_path = "image/small-vehicles1.jpeg" img = Image.open(image_path).convert('RGB') img
切分图片
from sahi.slicing import slice_image # 输出文件名前缀 output_file_name = "slice" # 输出文件夹 output_dir = "result" # 切分图像 slice_image_result = slice_image( image=image_path, output_file_name=output_file_name, output_dir=output_dir, slice_height=256, slice_width=256, overlap_height_ratio=0.2, overlap_width_ratio=0.2, verbose=False, ) print("原图宽{},高{}".format(slice_image_result.original_image_width, slice_image_result.original_image_height)) # 切分后的子图以形式:图像前缀_所在原图顶点坐标来保存文件 print("切分子图{}张".format(len(slice_image_result.filenames)))
原图宽1068,高580 切分子图15张
展示切分后的子图
import matplotlib.pyplot as plt from PIL import Image import math import os axarr_row = 3 axarr_col = math.ceil(len(slice_image_result.filenames)/axarr_row) f, axarr = plt.subplots(axarr_row, axarr_col, figsize=(14,7)) for index, file in enumerate(slice_image_result.filenames): img = Image.open(os.path.join(slice_image_result.image_dir,file)) axarr[int(index/axarr_col), int(index%axarr_col)].imshow(img)
1.1.2 COCO数据集切片
SAHI提供slice_coco函数以切分coco数据集(仅支持coco数据集)。slice_coco函数接口介绍如下:
# 返回切片后的coco标注字典文件,coco文件保存地址 def slice_coco( coco_annotation_file_path: str, # coco标注文件 image_dir: str, # coco图像集地址 output_coco_annotation_file_name: str, # 输出coco标注集文件名,不需要加文件类型后缀 output_dir: Optional[str] = None, # 输出文件地址 ignore_negative_samples: bool = False, # 是否忽略没有标注框的子图 slice_height: int = 512, # 切分子图高度 slice_width: int = 512, # 切分子图宽度 overlap_height_ratio: float = 0.2, # 子图高度之间的重叠率 overlap_width_ratio: float = 0.2, # 子图宽度之间的重叠率 min_area_ratio: float = 0.1, # 如果没有设置slice_height和slice_width,则自动确定slice_height、slice_width、overlap_height_ratio、overlap_width_ratio out_ext: Optional[str] = None, # 保存图像的扩展 verbose: bool = False, # 是否打印详细信息 )
slice_coco函数源代码位于sahi/slicing.py中,这段代码可以单步调试看看怎么做的,主要逻辑如下:
- 读取coco文件和图片信息
- 循环读取coco数据集的图片,每张图片调用get_slice_bboxes函数切分图像
- 创建coco dict结果并保存文件
以下代码演示了对coco数据集进行切片,并将切分后的子图和标注文件保存到本地。coco数据集可以包含若干张图片,但是以下代码示例中只包含一张图片,方便演示。
展示数据集
# 展示图像 from PIL import Image, ImageDraw from sahi.utils.file import load_json import matplotlib.pyplot as plt import os # coco图像集地址 image_path = "image" # coco标注文件 coco_annotation_file_path="image/terrain2_coco.json" # 加载数据集 coco_dict = load_json(coco_annotation_file_path) f, axarr = plt.subplots(1, 1, figsize=(8, 8)) # 读取图像 img_ind = 0 img = Image.open(os.path.join(image_path,coco_dict["images"][img_ind]["file_name"])).convert('RGBA') # 绘制标注框 for ann_ind in range(len(coco_dict["annotations"])): xywh = coco_dict["annotations"][ann_ind]["bbox"] xyxy = [xywh[0], xywh[1], xywh[0] + xywh[2], xywh[1] + xywh[3]] ImageDraw.Draw(img, 'RGBA').rectangle(xyxy, width=5) axarr.imshow(img)
切分数据集
from sahi.slicing import slice_coco # 保存的coco数据集标注文件名 output_coco_annotation_file_name="sliced" # 输出文件夹 output_dir = "result" # 切分数据集 coco_dict, coco_path = slice_coco( coco_annotation_file_path=coco_annotation_file_path, image_dir=image_path, output_coco_annotation_file_name=output_coco_annotation_file_name, ignore_negative_samples=False, output_dir=output_dir, slice_height=320, slice_width=320, overlap_height_ratio=0.2, overlap_width_ratio=0.2, min_area_ratio=0.2, verbose=False ) print("切分子图{}张".format(len(coco_dict['images']))) print("获得标注框{}个".format(len(coco_dict['annotations'])))
indexing coco dataset annotations... Loading coco annotations: 100%|█████████████████████████████████████████████████████████| 1/1 [00:00 "big_vehicle": 1, "car": 2, "human": 3 } # 更新标注类别 coco.update_categories(desired_name2id) print("修改后标注类别:{}".format(coco.category_mapping)) # 保存结果 save_json(coco.json, "updated_coco.json") "human": {"min": 20, "max": 30000}, "vehicle": {"min": 50, "max": 90000}, } area_filtered_coco = coco.get_area_filtered_coco(intervals_per_category=intervals_per_category) # 导出数据 save_json(area_filtered_coco.json, "area_filtered_coco.json")
-