1.背景
在目标检测中,需要进行图像增强。这里的代码模拟了旋转、扭曲图像的功能,并且在扭曲的时候,能够同时把标注的结果也进行扭曲。
这里忽略了读取xml的过程,假设图像IMG存在对应的标注框,且坐标为左上、右下两个点,这立刻哟把IMG进行扭曲,并且尽可能保证标签的坐标点也随之扭曲。
2.效果
我有一张铁塔的照片(原图略,原图是一张角度非常正确的图片),处理后效果如下图所示,可以看到图像出现了扭曲(出现了黑边),并且图像右侧中间部分的红色绝缘子旁边出现了两个小白点(为了便于观察而添加的),说明起到了扭曲的作用,并且大概率能保证坐标也被正确变化。
3.动机
设计这种数据增强策略,主要出于以下两种考虑:
- 在实际使用中,我发现即便是yolov8这样的模型,对于扭曲的图像(例如扭曲的笔记本),识别效果会有非常严重的下降,而真实情况中非常有可能出现扭曲(例如有人把笔记本半合上),模型需要识别。在电力场景中,也可能出现拍摄角度异常等情况。
- 在训练中,将不同角度的目标展现给模型,也可能提高模型的效果。例如上图中的绝缘子,比原图要“矮胖”一些,因此可以算作“新样本”,为模型提供更丰富的数据来源。
4.具体实现
第一步是图像扭曲。在这一步中,代码里的trapezoid_vertices表示你希望把图像的哪一部分进行扭曲,target_trapezoid_vertices表示你希望把trapezoid_vertices扭曲到什么位置。你可以把代码中相关位置的generate_random_cut和generate_random_perspective都去掉,然后就能明白了。
第二步是转化原始标注的框。在使用gpt生成代码的时候,提示词将任务划分为了5个步骤,提示词如下
假设原始图像PIC宽度为W,高度为H。在图像标注场景中,我会告诉你两个点special_points,分别代表原始的标注框的左上角和右下角。你需要执行以下步骤:
【步骤1】根据special_points,恢复原始的标注框的四个坐标。注意保留这四个点的顺序关系。
【步骤2】:利用自定义函数fun1计算出变化之后的四个点的新坐标。
【步骤3】依次判断四个点所连成的线段是否与原始图像PIC的边缘有交点,如果有,就将这些交点的坐标存到point_list中。
【步骤4】判断变换后的四个点有哪些位于原始图像PIC的范围内,如果有,则将这些点的坐标存到point_list中。
【步骤5】分析point_list中的所有点,找出一个能完整包含这些点的矩形R,返回这个矩形R的左上角和右下角。
辅助代码包含了前面提到的5个步骤,具体如下:
import random
import cv2
import numpy as np
# 原始图像剪裁的范围
max_cut = 0.1
min_cut = 0.05
# 扭曲的最大和最小程度
max_perspective = 0.1
min_perspective = 0.05
# 制定初始剪裁范围,你可以选择从某些地方开始剪裁你的图像
def generate_random_cut(x):
# 计算 x 的 10% 和 20%
min_value = min_perspective * x
max_value = max_perspective * x
# 生成一个在 10% 到 20% 范围内的随机数
random_value = random.uniform(min_value, max_value)
# 随机决定这个数是正数还是负数
sign = random.choice([-1, 1])
return sign * random_value
# 制定目标梯形的范围
def generate_random_perspective(x):
# 计算 x 的 10% 和 20%
min_value = min_perspective * x
max_value = max_perspective * x
random_value = random.uniform(min_value, max_value)
# 随机决定这个数是正数还是负数
sign = random.choice([-1, 1])
return sign * random_value
# 根据输入的两个点的坐标,复原出连续的矩形框的四个点坐标
def get_bbox_corners(special_points):
top_left = special_points[0][0]
bottom_right = special_points[0][1]
top_right = [bottom_right[0], top_left[1]]
bottom_left = [top_left[0], bottom_right[1]]
return np.array([top_left, top_right, bottom_right, bottom_left])
# line_intersection所需要的辅助函数
def on_segment(p, q, r):
if (q[0] <= max(p[0], r[0]) and q[0] >= min(p[0], r[0]) and
q[1] <= max(p[1], r[1]) and q[1] >= min(p[1], r[1])):
return True
return False
# line_intersection所需要的辅助函数
def orientation(p, q, r):
val = (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])
if val == 0:
return 0 # collinear
elif val > 0:
return 1 # clockwise
else:
return 2 # counterclockwise
# line_intersection所需要的辅助函数
def segments_intersect(p1, q1, p2, q2):
o1 = orientation(p1, q1, p2)
o2 = orientation(p1, q1, q2)
o3 = orientation(p2, q2, p1)
o4 = orientation(p2, q2, q1)
if o1 != o2 and o3 != o4:
return True
if o1 == 0 and on_segment(p1, p2, q1):
return True
if o2 == 0 and on_segment(p1, q2, q1):
return True
if o3 == 0 and on_segment(p2, p1, q2):
return True
if o4 == 0 and on_segment(p2, q1, q2):
return True
return False
# 判断四个点所连成的线段是否与原始图像的边缘有交点
def line_intersection(p1, p2, edge_start, edge_end):
if not segments_intersect(p1, p2, edge_start, edge_end):
return None
xdiff = (p1[0] - p2[0] + 0.01, edge_start[0] - edge_end[0] + 0.01)
ydiff = (p1[1] - p2[1] + 0.01, edge_start[1] - edge_end[1] + 0.01)
def det(a, b):
return a[0] * b[1] - a[1] * b[0]
div = det(xdiff, ydiff)
if div == 0:
return None
d = (det(p1, p2), det(edge_start, edge_end))
x = det(d, xdiff) / div
y = det(d, ydiff) / div
return x, y
# 判断四个点所连成的线段是否与原始图像的边缘有交点
def check_intersections(bbox_corners, w, h):
edges = [
((0.01, 0), (w, 0.01)),
((w, 0.01), (w, h)),
((w, h), (0.01, h)),
((0.01, h), (0.01, 0.01))
]
point_list = []
for i in range(len(bbox_corners)):
p1 = bbox_corners[i]
p2 = bbox_corners[(i + 1) % len(bbox_corners)]
for edge in edges:
intersection = line_intersection(p1, p2, edge[0], edge[1])
if intersection:
point_list.append(intersection)
return point_list
# 判断变换后的四个点是否位于原始图像范围内
def check_points_within_image(bbox_corners, w, h):
point_list = []
for point in bbox_corners:
if 0 <= point[0] <= w and 0 <= point[1] <= h:
point_list.append(point)
return point_list
# 分析所有点,找出一个能完整包含这些点的矩形
def get_bounding_box(point_list):
min_x = min(point[0] for point in point_list)
max_x = max(point[0] for point in point_list)
min_y = min(point[1] for point in point_list)
max_y = max(point[1] for point in point_list)
return (min_x, min_y), (max_x, max_y)
使用方法如下:
# 数据增强 - 拉伸
if __name__ == "__main__":
# 定义两个特殊点,即原始标注框的左上和右下
special_points = [
[3400, 1655], # Example points, replace with your own
[4550, 2350]
]
# 读取图片
image_path = r'D:\data\拉伸原始图像.jpg'
image = cv2.imread(image_path)
# 获取图像宽度和高度
height, width = image.shape[:2]
# 定义顶点坐标,你会保留这四个点之内的图像,以便进行后续步骤
trapezoid_vertices = np.array([[0 + generate_random_cut(width), 0 + generate_random_cut(height)],
[width + generate_random_cut((width)), 0 + generate_random_cut((height))],
[width + generate_random_cut(width), height + generate_random_cut(height)],
[0 + generate_random_cut(width), height + generate_random_cut(height)]],
dtype=np.float32)
# 定义目标图像的顶点坐标,会将trapezoid_vertices所保留的图像拉伸,拉伸到target_trapezoid_vertices所对应的范围内
target_trapezoid_vertices = np.array(
[[0 + generate_random_perspective(width), 0 + generate_random_perspective(height)],
[width + generate_random_perspective(width), 0 + generate_random_perspective(height)],
[width + generate_random_perspective(width),
height + generate_random_perspective(height)],
[0 + generate_random_perspective(width), height + generate_random_perspective(height)]],
dtype=np.float32)
# 计算透视变换矩阵,用于将image进行拉伸
perspective_matrix = cv2.getPerspectiveTransform(trapezoid_vertices, target_trapezoid_vertices)
# 进行透视变换
trapezoid_image = cv2.warpPerspective(image, perspective_matrix, (width, height))
# 将坐标框进行处理,利用perspective_matrix得到新的坐标框的左上角和右下角
special_points = np.array(special_points, dtype='float32')
special_points = np.array([special_points])
transformed_special_points = cv2.perspectiveTransform(special_points, perspective_matrix)
# 得到新的标注的框的四个顶点的坐标
bbox_corners = get_bbox_corners(transformed_special_points)
# 如果有超出图像边界的点,就计算与图像边界的交点,保存到point_list中。
point_list = check_intersections(bbox_corners, width, height)
# 把留在图像范围内的点也加到point_list中
point_list.extend(check_points_within_image(bbox_corners, width, height))
# 得出新的理想中的标注框的坐标
if point_list:
rect = get_bounding_box(point_list)
else:
rect = None
print(len(rect))
# 就爱那个新的标注框的左上角和右下角绘制出来,以便判断是否正确
for point in rect:
x = tuple([int(i) for i in point])
cv2.circle(trapezoid_image, x, 15, (256, 256, 256), 10)
# 保存变换后的图像
save_path = r'D:\data\拉伸结果.jpg'
cv2.imwrite(save_path, trapezoid_image)