123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- import cv2
- import numpy as np
- class FieldTransformer:
- """用于将视频中的像素坐标转换为场地坐标的工具类"""
-
- def __init__(self):
- # 标准橄榄球场尺寸(单位:米)
- self.field_length = 100 # 大约100码(不包括端区)
- self.field_width = 50 # 大约53.3码
-
- # 默认变换矩阵(假设俯视图)
- self.perspective_matrix = None
-
- # 是否已自动检测场地
- self.field_detected = False
-
- # 场地边界点(图像坐标系)
- self.field_corners = None
-
- def auto_detect_field(self, frame):
- """尝试自动检测球场区域"""
- try:
- # 转换为灰度图
- gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
-
- # 应用高斯模糊
- blur = cv2.GaussianBlur(gray, (5, 5), 0)
-
- # 边缘检测
- edges = cv2.Canny(blur, 50, 150)
-
- # 膨胀边缘以连接间隙
- dilated = cv2.dilate(edges, np.ones((3, 3), np.uint8), iterations=2)
-
- # 查找轮廓
- contours, _ = cv2.findContours(dilated, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
-
- # 按面积排序
- contours = sorted(contours, key=cv2.contourArea, reverse=True)
-
- # 尝试找到矩形区域(假设场地为矩形)
- for contour in contours[:5]: # 只检查最大的5个轮廓
- # 近似轮廓
- epsilon = 0.02 * cv2.arcLength(contour, True)
- approx = cv2.approxPolyDP(contour, epsilon, True)
-
- # 如果近似为四边形,可能是场地
- if len(approx) == 4:
- # 进一步处理,确认是否为场地
- field_area = cv2.contourArea(approx)
- frame_area = frame.shape[0] * frame.shape[1]
-
- # 如果面积占总面积的比例合适,则认为找到了场地
- if 0.2 < field_area / frame_area < 0.95:
- # 存储场地角点
- self.field_corners = approx.reshape(4, 2)
-
- # 根据场地角点计算透视变换矩阵
- self._calculate_perspective_matrix()
-
- self.field_detected = True
- return True
-
- # 未找到合适的场地轮廓,使用默认变换
- self._setup_default_transform(frame)
- return False
-
- except Exception as e:
- print(f"场地自动检测失败: {str(e)}")
- self._setup_default_transform(frame)
- return False
-
- def _setup_default_transform(self, frame):
- """设置默认变换矩阵(假设俯视图)"""
- height, width = frame.shape[:2]
-
- # 默认场地框(图像四角)
- self.field_corners = np.array([
- [0.1 * width, 0.1 * height], # 左上
- [0.9 * width, 0.1 * height], # 右上
- [0.9 * width, 0.9 * height], # 右下
- [0.1 * width, 0.9 * height] # 左下
- ], dtype=np.float32)
-
- self._calculate_perspective_matrix()
-
- def _calculate_perspective_matrix(self):
- """计算透视变换矩阵"""
- # 标准化角点顺序:左上,右上,右下,左下
- rect = self._order_points(self.field_corners)
-
- # 目标场地坐标(标准场地,单位:米)
- dst = np.array([
- [0, 0], # 左上
- [self.field_length, 0], # 右上
- [self.field_length, self.field_width], # 右下
- [0, self.field_width] # 左下
- ], dtype=np.float32)
-
- # 计算透视变换矩阵
- self.perspective_matrix = cv2.getPerspectiveTransform(rect, dst)
-
- def _order_points(self, pts):
- """对四边形角点进行排序: 左上,右上,右下,左下"""
- # 初始化结果数组
- rect = np.zeros((4, 2), dtype=np.float32)
-
- # 计算点的和,左上角的和最小,右下角的和最大
- s = pts.sum(axis=1)
- rect[0] = pts[np.argmin(s)] # 左上
- rect[2] = pts[np.argmax(s)] # 右下
-
- # 计算差,右上角的差最小,左下角的差最大
- diff = np.diff(pts, axis=1)
- rect[1] = pts[np.argmin(diff)] # 右上
- rect[3] = pts[np.argmax(diff)] # 左下
-
- return rect
-
- def transform_to_field(self, tracks, frame=None):
- """将球员位置从像素坐标转换到场地坐标
-
- Args:
- tracks: {id: (x, y, w, h)} 形式的字典,x,y是中心点
- frame: 可选的原始视频帧,用于自动检测场地
-
- Returns:
- {id: (field_x, field_y)} 形式的字典
- """
- # 如果没有初始化透视矩阵且提供了frame
- if self.perspective_matrix is None and frame is not None:
- self.auto_detect_field(frame)
-
- # 如果仍未初始化,使用默认值
- if self.perspective_matrix is None:
- # 创建一个假想的frame
- mock_frame = np.zeros((720, 1280, 3), dtype=np.uint8)
- self._setup_default_transform(mock_frame)
-
- # 转换每个轨迹到场地坐标
- field_positions = {}
-
- for track_id, (x, y, w, h) in tracks.items():
- # 转换中心点
- pt = np.array([[x, y]], dtype=np.float32)
- pt = cv2.perspectiveTransform(pt.reshape(-1, 1, 2), self.perspective_matrix)
-
- # 存储场地坐标
- field_positions[track_id] = (float(pt[0][0][0]), float(pt[0][0][1]))
-
- return field_positions
-
- def convert_field_to_image(self, field_positions, frame_shape):
- """将场地坐标转换回图像坐标
-
- Args:
- field_positions: {id: (field_x, field_y)} 形式的字典
- frame_shape: 原始帧的形状(高度, 宽度)
-
- Returns:
- {id: (image_x, image_y)} 形式的字典
- """
- if self.perspective_matrix is None:
- # 创建一个假想的frame
- mock_frame = np.zeros((frame_shape[0], frame_shape[1], 3), dtype=np.uint8)
- self._setup_default_transform(mock_frame)
-
- # 计算逆变换矩阵
- inv_matrix = np.linalg.inv(self.perspective_matrix)
-
- # 转换每个场地坐标到图像坐标
- image_positions = {}
-
- for track_id, (field_x, field_y) in field_positions.items():
- # 转换点
- pt = np.array([[[field_x, field_y]]], dtype=np.float32)
- pt = cv2.perspectiveTransform(pt, inv_matrix)
-
- # 存储图像坐标
- image_positions[track_id] = (float(pt[0][0][0]), float(pt[0][0][1]))
-
- return image_positions
-
- def visualize_field_transform(self, frame):
- """在帧上可视化场地变换"""
- if self.field_corners is None:
- self.auto_detect_field(frame)
-
- vis_frame = frame.copy()
-
- # 绘制检测到的场地边界
- if self.field_corners is not None:
- # 将角点转换为整数坐标
- corners = self.field_corners.astype(np.int32)
-
- # 绘制场地边界
- cv2.polylines(vis_frame, [corners], True, (0, 255, 0), 2)
-
- # 标记角点
- for i, (x, y) in enumerate(corners):
- cv2.circle(vis_frame, (x, y), 5, (0, 0, 255), -1)
- cv2.putText(vis_frame, str(i), (x-10, y-10),
- cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 0, 0), 2)
-
- # 添加说明文本
- cv2.putText(vis_frame, "检测到场地", (10, 30),
- cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
- else:
- cv2.putText(vis_frame, "未检测到场地,使用默认变换", (10, 30),
- cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 255), 2)
-
- return vis_frame
|