核心贡献与创新点

  1. 数据集特性

    • 规模:240段视频、15万帧(MOT17的15倍)、160万边界框(MOT17的3倍),覆盖篮球、排球、足球三类运动。
    • 关键挑战
      • 快速变速运动:运动员高速移动且频繁变速(图2-3显示相邻帧IoU显著低于MOT17/DanceTrack)。
      • 相似但可区分的外观:球员身着相似队服但号码/姿态不同(图4特征可视化证明其外观区分度介于MOT17与DanceTrack之间)。
    • 标注质量:仅标注场上球员(排除观众/裁判),标注工具结合KCF单目标跟踪辅助人工修正,确保ID一致性与边界框精度。
  2. 现有方法瓶颈分析

    • 主流跟踪器(如ByteTrack、OC-SORT)依赖卡尔曼滤波的运动关联,但体育场景中运动员的非线性运动导致传统运动模型失效(表6显示直接IoU匹配优于卡尔曼滤波)。

在这里插入图片描述

  • 外观模型需兼顾相似性与可区分性:DanceTrack外观高度一致,而SportsMOT要求模型学习更细粒度特征(如球衣号码)。
  1. 新框架MixSort
    • 设计思路:在检测跟踪框架(如ByteTrack)中引入类MixFormer的外观关联模块(图6),融合运动与外观相似度矩阵:
      M = α ⋅ IoU + ( 1 − α ) ⋅ V MixFormer M = \alpha \cdot \text{IoU} + (1-\alpha) \cdot V_{\text{MixFormer}} M=αIoU+(1α)VMixFormer
    • 优势
      • 在SportsMOT上提升显著:ByteTrack + MixSort使HOTA↑1.6,IDF1↑2.7(表3)。
        在这里插入图片描述

      • 在MOT17上刷新SOTA:HOTA达64.0(表5),证明泛化能力。

在这里插入图片描述

  • 关键实现:将MixFormer的角点预测改为热力图中心预测,加速推理并适配MOT任务(图5)。
    在这里插入图片描述

数据集对比与实验洞察

数据集视频数帧数边界框数核心挑战
MOT171411k292k行人低速线性运动
DanceTrack100105k-外观一致+运动多样性
SportsMOT240150k1.6M快速变速+外观弱区分
  • 运动特性(图2-3):
    • 足球场景运动最快(相邻帧IoU最低),篮球因频繁身体对抗遮挡最严重(表7中HOTA最低:60.8)。

在这里插入图片描述

  • 关联策略验证(表4):
    • 纯外观关联(α=0)在足球场景效果最佳(HOTA 65.7),但融合策略(α=0.5~0.8)整体最优。

在这里插入图片描述

  • 对比实验
    • MixSort在SportsMOT提升显著,但在DanceTrack上效果下降(表8),印证SportsMOT外观更具可区分性。
    • 超越SoccerNet:DetA↑7.3(表7),凸显专注球员跟踪的优势。
      在这里插入图片描述

应用价值与资源开放

  • 目标:推动体育自动分析(如跑动距离统计、战术生成)。
  • 数据拆分:训练集(45段)、验证集(45段)、测试集(150段)。
  • 评估指标:推荐HOTA(平衡检测与关联)、IDF1、AssA。
  • 开源:数据集与代码已公开(项目链接)。

总结

SportsMOT填补了体育场景MOT数据集的空白,其快速变速运动弱外观区分特性挑战现有跟踪范式。所提MixSort框架通过融合运动预测与Transformer外观建模,在SportsMOT与MOT17均达SOTA。该数据集有望推动体育分析与多目标跟踪算法的协同发展。

下载链接

https://hf-mirror.com/datasets/MCG-NJU/SportsMOT/tree/main/dataset

转为Labelme格式的数据集

删除缓存文件

清楚很多不需要的文件(一些.开头的文件),代码如下:

import os
import shutil


def remove_dot_files_and_dirs(root_dir):
    """
    递归删除指定目录下所有以点开头的文件和文件夹
    :param root_dir: 要清理的根目录路径
    """
    for root, dirs, files in os.walk(root_dir, topdown=False):
        # 删除所有以点开头的文件
        for file in files:
            if file.startswith('.'):
                file_path = os.path.join(root, file)
                try:
                    os.remove(file_path)
                    print(f"已删除文件: {file_path}")
                except Exception as e:
                    print(f"删除文件失败 [{file_path}]: {str(e)}")

        # 删除所有以点开头的文件夹(需要后序遍历确保先处理子内容)
        for dir in dirs:
            if dir.startswith('.'):
                dir_path = os.path.join(root, dir)
                try:
                    shutil.rmtree(dir_path)  # 递归删除整个目录
                    print(f"已删除目录: {dir_path}")
                except Exception as e:
                    print(f"删除目录失败 [{dir_path}]: {str(e)}")


if __name__ == "__main__":
    target_dir ="../sportsmot"

    if not os.path.isdir(target_dir):
        print(f"错误: 目录不存在 [{target_dir}]")
    else:
        print(f"开始清理目录: {target_dir}")
        remove_dot_files_and_dirs(target_dir)
        print("清理完成!")

转为Labelme格式

代码:

'''
创建以下四个目录,用于存放图片和标签
images/train
images/val
labels/train
labels/val
'''
import os
import shutil
import numpy as np
import configparser
import cv2
import base64
import json
version = '5.0.2'
flags = {}
lineColor = [0, 255, 0, 128]
fillColor = [255, 0, 0, 128]

list_mot17=['../sportsmot/train/train','../sportsmot/val/val']
labelme_out='../Labelme_sportmot'
os.makedirs(labelme_out,exist_ok=True)
for dir in list_mot17:
    for mot_dir in os.listdir(dir):  # mot_dir是例如MOT17-02-FRCNN这种
        det_path = os.path.join(dir, mot_dir, 'gt/gt.txt')  # det.txt路径
        dets = np.loadtxt(det_path, delimiter=',')  # 读取det.txt文件
        ini_path = os.path.join(dir, mot_dir, 'seqinfo.ini')  # seqinfo.ini路径
        conf = configparser.ConfigParser()
        conf.read(ini_path)  # 读取seqinfo.ini文件
        seqLength = int(conf['Sequence']['seqLength'])  # MOT17-02-FRCNN序列的长度
        imgWidth = int(conf['Sequence']['imWidth'])  # 图片宽度
        imgHeight = int(conf['Sequence']['imHeight'])  # 图片长度
        dic_frame_id={}
        for det in dets:
            frame_id, _, left, top, width, height = int(det[0]), det[1], det[2], det[3], det[4], det[5]
            if frame_id in dic_frame_id:
                dic_frame_id[frame_id].append([left, top, width, height])
            else:
                ann_list = [[left, top, width, height]]
                dic_frame_id[frame_id]=ann_list

        for key_frame in dic_frame_id.keys():
            dic = {}
            dic['version'] = version
            dic['flags'] = flags
            dic['shapes'] = []
            oldimgpath = os.path.join(dir, mot_dir, 'img1','%06d' % key_frame + '.jpg')  # train/MOT17-02-FRCNN/img1/000001.jpg
            image_name = mot_dir + '-' + '%06d' % key_frame + '.jpg'
            img = cv2.imread(oldimgpath)
            imageHeight, imageWidth, _ = img.shape
            for data in dic_frame_id[key_frame]:
                shape = {}
                shape['label'] = 'person'
                shape['line_color'] = None
                shape['fill_color'] = None
                x1 = float(data[0])
                y1 = float(data[1])
                w = float(data[2])
                h = float(data[3])
                x2 = x1 + w
                y2 = y1 + h
                shape['points'] = [[x1, y1], [x2, y2]]
                shape['shape_type'] = 'rectangle'
                shape['flags'] = {}
                dic['shapes'].append(shape)
            dic['lineColor'] = lineColor
            dic['fillColor'] = fillColor
            dic['imagePath'] =image_name
            dic['imageData'] = base64.b64encode(
                open(oldimgpath, "rb").read()).decode('utf-8')
            dic['imageHeight'] = imageHeight
            dic['imageWidth'] = imageWidth
            fw = open(os.path.join(labelme_out, image_name.split('.')[0]+'.json'), 'w')
            json.dump(dic, fw)
            fw.close()
            shutil.copy(oldimgpath, os.path.join(labelme_out,image_name))

转为Labelme后,你会发现,这个数据集只标注了运动员,场外的很多人没有标注,如果想用来做目标检测数据集,场外的人会形成干扰,所以就要把背景去除掉,代码如下:

import json
import os
import cv2
import numpy as np
from pathlib import Path
import shutil


def process_labelme_images(json_dir, image_dir, output_dir):
    """
    处理LabelMe数据集,将标注区域之外的图像变为黑色
    只保留所有标注框组成的最小外接矩形区域
    同时复制并更新对应的JSON文件

    参数:
    json_dir: 包含LabelMe JSON文件的目录
    image_dir: 原始图片目录
    output_dir: 输出图片和JSON文件的目录
    """
    # 创建输出目录
    Path(output_dir).mkdir(parents=True, exist_ok=True)

    # 遍历所有JSON文件
    for json_file in Path(json_dir).glob('*.json'):
        with open(json_file, 'r') as f:
            data = json.load(f)

        # 获取图片路径
        image_path = Path(image_dir) / Path(data['imagePath']).name
        if not image_path.exists():
            print(f"图片不存在: {image_path}")
            continue

        # 读取图片
        img = cv2.imread(str(image_path))
        if img is None:
            print(f"无法读取图片: {image_path}")
            continue

        h, w = img.shape[:2]

        # 初始化边界值
        min_x, min_y = w, h
        max_x, max_y = 0, 0
        has_annotations = False

        # 计算所有标注框的最小外接矩形
        for shape in data['shapes']:
            points = np.array(shape['points'], dtype=np.int32)

            # 获取当前标注的边界
            if shape['shape_type'] == 'rectangle':
                x1, y1 = points[0]
                x2, y2 = points[1]
                cur_min_x, cur_min_y = min(x1, x2), min(y1, y2)
                cur_max_x, cur_max_y = max(x1, x2), max(y1, y2)
            else:
                # 对于多边形/其他形状,计算其最小外接矩形
                cur_min_x, cur_min_y = np.min(points, axis=0)
                cur_max_x, cur_max_y = np.max(points, axis=0)

            # 更新全局边界
            min_x = min(min_x, cur_min_x)
            min_y = min(min_y, cur_min_y)
            max_x = max(max_x, cur_max_x)
            max_y = max(max_y, cur_max_y)
            has_annotations = True

        # 如果没有标注,跳过处理
        if not has_annotations:
            print(f"图片无标注: {image_path.name}")
            continue

        # 确保边界在图像范围内
        min_x = max(0, min_x)
        min_y = max(0, min_y)
        max_x = min(w - 1, max_x)
        max_y = min(h - 1, max_y)

        # 创建全黑蒙版
        mask = np.zeros((h, w), dtype=np.uint8)

        # 在蒙版上绘制最小外接矩形(白色)
        cv2.rectangle(mask, (min_x, min_y), (max_x, max_y), 255, -1)

        # 应用蒙版:标注区域外变为黑色
        if len(img.shape) == 3:  # 彩色图像
            result = cv2.bitwise_and(img, img, mask=mask)
        else:  # 灰度图像
            result = cv2.bitwise_and(img, img, mask=mask)

        # 保存结果图片
        output_image_path = Path(output_dir) / image_path.name
        cv2.imwrite(str(output_image_path), result)
        print(f"处理完成: {output_image_path} - 保留区域: [{min_x}, {min_y}] 到 [{max_x}, {max_y}]")

        # 复制并更新JSON文件
        output_json_path = Path(output_dir) / json_file.name
        shutil.copy(json_file, output_json_path)

        # 更新JSON中的图像路径和大小
        with open(output_json_path, 'r+') as f:
            json_data = json.load(f)
            # 更新图像路径为当前文件名(相对路径)
            json_data['imagePath'] = image_path.name
            # 更新图像尺寸(虽然尺寸没变,但以防万一)
            json_data['imageWidth'] = w
            json_data['imageHeight'] = h
            # 设置imageData为None而不是删除
            json_data['imageData'] = None

            # 写回更新后的JSON
            f.seek(0)
            json.dump(json_data, f, indent=2)
            f.truncate()

        print(f"已更新JSON文件: {output_json_path}")


# 使用示例
if __name__ == "__main__":
    json_dir = "../Labelme_sportmot"  # 替换为JSON文件目录
    image_dir = "../Labelme_sportmot"  # 替换为原始图片目录
    output_dir = "../sportsmot_out"  # 替换为输出目录

    process_labelme_images(json_dir, image_dir, output_dir)

运行上面的代码就能去除大部分的背景,结果如下:
在这里插入图片描述

更多推荐