Towards Data Science

Synthetic-to-Real Transfer with Frozen DINOv3 Features

6.9内容质量

TL;DR · AI 摘要

Synthetic-to-Real Transfer with Frozen DINOv3 Features Towards Data Science Computer Vision Synthetic-to-Real Transfer w...

核心要点

  • 主题聚焦:Synthetic-to-Real Transfer with Frozen DINOv3 Fe
  • 来源:Towards Data Science,建议结合原文判断细节。
  • AI 分析暂不可用,本条为保底评分与摘要。
#AI#编程#安全#开源
打开原文

使用冻结 DINOv3 特征的合成到现实迁移 | Towards Data Science

计算机视觉

使用冻结 DINOv3 特征的合成到现实迁移

在程序生成的数据集上训练模型进行卡片检测和分割

Florian Trautweiler

2026年10月20日

11分钟阅读时间

在 Google 上推荐我们

分享

数据是机器学习中最宝贵的组成部分。你拥有的数据越多样化、越符合任务需求,就越能将模型拟合到目标上。但收集这些数据并按照你的需求进行标注并不总是容易的。你可能需要手动采集数据,同时确保不向数据集引入任何偏见,或者验证第三方的标注,如今更常见的是验证用于预标注的 VLM 的标注。

手动标注流程

获取大量带有完美标签的数据的一个好方法是合成生成数据。这样,你可以通过设计直接获得真实标注。但对计算机视觉任务进行此类操作的一个巨大挑战是,这些合成数据在多大程度上可以用于训练能泛化到真实世界数据的模型。在这个项目中,我想探索是否可以使用最先进的预训练特征提取器来生成丰富的特征表示,并仅在合成数据上训练不同的头部,从而泛化到真实数据。

任务

我需要一个领域,生成感兴趣对象的复杂度不高,但我也想保持一定的趣味性。因此,我找到了一个不错的中间点,这些来自名为 SET 的游戏的扑克牌。游戏的目标是在 4x3 的游戏区域中检测三张卡片(称为集合),其中每张卡片的每个特征(形状、颜色、阴影和数量)必须在所有三张卡片上完全相同,或在每张卡片上完全不同。

棋盘上集合的示例:所有形状/阴影相同,所有数量和颜色不同

另一个集合三元组示例,所有属性都不同

在游戏中,第一个发现三元组集合的玩家会大喊“SET”,然后收集这三张牌,同时它们会被从牌堆中替换为三张新牌,直到牌堆中没有牌为止。拥有最多牌的玩家获胜。

模型架构

我希望为这个项目使用一个小型模型,这样我就可以在自己的 PC GPU 上训练它,但仍然希望获得丰富的特征表示。最终我选择了 Meta 最新的 DINOv3 模型,因为它具有最佳的局部特征提取性能,并有几个蒸馏后的更小版本。通过他们的蒸馏方法,他们甚至能够将大变压器模型学到的知识蒸馏到卷积架构中:

在网页数据集(LVD-1689M)上预训练的 ConvNeXt 模型:

模型

参数

预训练数据集

下载

ConvNeXt Tiny

29M

LVD-1689M

[链接]

ConvNeXt Small

50M

ConvNeXt Base

89M

ConvNeXt Large

198M

由于硬件限制以及即使是最小的蒸馏模型仍然表现出色,我选择了最小的卷积模型 ConvNeXt Tiny。

为了预测边界框、关键点或分割掩码等任务特定输出,我添加了不同的任务特定头部。这个项目的主要目标之一是尝试不同的目标并了解它们的具体挑战。作为第一步,我们暂时不会检测卡片的类型,而是仅在图像中定位卡片。

卡片生成

在合成数据生成过程中,我使用 OpenCV 绘制基本形状,从白色圆角矩形到各个独立形状。以卡片形状为例,我们使用两个矩形和四个圆的组合:

python

code
# src/set_master/generation/card.py
def draw_card_surface(
img: np.ndarray,
position: tuple[int, int],
size: tuple[int, int],
corner_radius: int = 8,
) -> None:
"""在指定位置绘制白色圆角卡片表面,以 position 为中心。
Args:
img: 需要进行原地修改的 OpenCV 图像。
position: 卡片中心坐标,以图像像素的 (x, y) 表示。
size: 卡片尺寸,以像素的 (宽度, 高度) 表示。
corner_radius: 圆角半径,以像素为单位。
"""
...
# 第一个重叠矩形
cv2.rectangle(
img,
pt1=(position[0] - size[0] // 2 + corner_radius, position[1] - size[1] // 2),
pt2=(position[0] + size[0] // 2 - corner_radius, position[1] + size[1] // 2),
color=(255, 255, 255),
thickness=-1,
)
...
# 绘制四个角的圆
for x in [-1, 1]:
for y in [-1, 1]:
cv2.ellipse( ... )

程序化数据的优势在于我们可以对卡片外观进行完全控制。通过调整这些参数值,很容易沉浸在创造过程之中,非常迷人。

程序化卡片生成可调整的参数示例

我生成了一些基础卡片;通常在游戏中只有三种形状,但根据游戏版本的不同,形状会略有变化,因此我选择生成五种不同的形状以覆盖最常见的类型。

卡片布局

单独生成俯视视角的卡片很有用,但最终我们需要一个与现实世界示例相似的数据集,其中卡片应躺在桌面上。我最初尝试在黑色背景上随机绘制多张卡片,但效果不够真实。

早期阶段的合成卡片(纯黑色背景)

为了解决这个问题,我从 Poly Haven 获取了一些免费高质量纹理(采用 CC0 许可)。利用这些资源,我创建了一个纹理背景库,显著提升了生成图像的逼真度。

来自 Poly Haven 的示例背景纹理

添加轻微的透视倾斜、柔和的阴影和微妙的高光,可以让卡片看起来像是静止在桌面上。通过调整卡片的位置、旋转角度和间距,可以打破完美的网格布局,而纹理背景和柔和的光照变化则能让场景呈现出更自然的外观。

生成卡片的合成图像示例

当然,由于我们自己生成图像,也可以同时获得所需的精确标注信息。我们确切知道每张卡片的位置,并可以对卡片的角坐标应用相同的透视变换。基于这些信息,我们可以生成边界框、定向框或关键点标注。我们还可以为每张卡片渲染掩膜,从而同时获得语义分割掩膜(所有卡片属于同一前景类别)和实例分割掩膜(每张卡片单独标记)。这使我们能够在相同数据集上尝试不同任务,而无需重新进行人工标注。

自动标注的边界框、角点和分割掩膜

真实世界数据

训练

现在我们已经可以生成图像及其标注,可以开始训练模型了。在此次实验中,我保持了 DINOv3 的主干网络冻结状态,仅训练了特征金字塔和任务特定的头部。这使我们能够复用预训练的特征,同时学习如何将它们转换为卡片检测或分割掩码。我假设由于 DINO 特征的大量预训练和泛化能力,我们可以从训练数据集零样本迁移至真实数据。因此,我们仅在训练期间使用合成数据集,然后在真实图像上进行评估。

对于训练,我使用了 PyTorch 框架 Lightning。我非常喜欢这个框架,因为它抽象了一些底层 PyTorch 代码,使我需要更少的样板代码,但你仍然可以像平常一样使用 torch。

对于主干网络,我们可以定义 DINOv3 模型,从自定义的预训练检查点文件加载权重,并添加一个将在训练中使用的特征金字塔网络层。

code
...
from dinov3.hub.backbones import dinov3_convnext_tiny
from torchvision.ops import FeaturePyramidNetwork
...
class FrozenDinoFPN(nn.Module):
out_channels = 256
def __init__(self, weights_path: str, initialize_pretrained: bool = True) -> None:
super().__init__()
self.encoder = dinov3_convnext_tiny(pretrained=False, patch_size=None)
if initialize_pretrained:
self.encoder.load_state_dict(torch.load(weights_path, map_location="cpu", weights_only=True), strict=True)
self.encoder.requires_grad_(False)
self.encoder.eval()
self.fpn = FeaturePyramidNetwork([96, 192, 384, 768], self.out_channels)

然后我们可以基于此构建任务特定的模型,例如检测器模型:

code
class CardDetector(pl.LightningModule):
"""冻结的 DINOv3 特征与可训练任务头及验证指标。"""
def __init__(
self,
weights_path: str = str(DEFAULT_WEIGHTS),
learning_rate: float = 1e-3,
weight_decay: float = 1e-4,
min_size: int = 640,
max_size: int = 1280,
initialize_pretrained: bool = True,
variant: str | None = None,
) -> None:
super().__init__()
self.save_hyperparameters()
backbone = FrozenDinoFPN(weights_path, initialize_pretrained)
self.detector = FasterRCNN(
backbone,
num_classes=2,
min_size=min_size,
max_size=max_size,
image_mean=[0.485, 0.456, 0.406],
image_std=[0.229, 0.224, 0.225],
rpn_anchor_generator=AnchorGenerator(
sizes=((32,), (64,), (128,), (256,)),
aspect_ratios=((0.5, 1.0, 2.0),) * 4,
),
box_roi_pool=MultiScaleRoIAlign(featmap_names=["0", "1", "2", "3"], output_size=7, sampling_ratio=2),
box_score_thresh=0.05,
)
self.box_map = (
MeanAveragePrecision(box_format="xyxy", iou_type="bbox", compute_on_cpu=True)
)

这些超参数大多并未进行精细调整,因为此处的目标不是训练最佳模型,而是实现从合成数据集到真实图像的零样本迁移。

为了帮助模型迁移并增加训练数据的多样性,我们添加了多种图像增强技术,例如翻转、透视偏移、对比度调整、亮度变化、模糊等。

结果

我在合成数据集上训练了这些模型,使用2000张图像进行训练,从该合成数据集中选取200张图像用于验证。这些图像的尺寸为1280 × 720,每张场景包含5-15张卡片。每个训练周期会从增强后的训练图像中采样500张图像,我进行了10个周期的训练,并选择验证得分最高的运行结果。

| 分类 | 卡片数量 | |------|---------| | 训练集 | 2,000 | 20,011 | | 验证集 | 200 | 1,948 | | 总计 | 2,200 | 21,959 |

为了评估模型,我使用手机拍摄的真实图像作为测试集。运行评估需要设置一些阈值参数。由于希望保持简单,我没有在真实图像测试集上花费太多时间优化这些阈值。理想情况下,我们应该像调整其他超参数一样,在验证集上微调这些值,然后在另一个独立的测试数据集上测量最终性能。但在这个项目中,我使用以下参数选择的定性基准结果已经感到满意:

我使用0.75的置信度阈值过滤掉置信度较低的检测结果。更高的阈值通常会减少误检,但可能导致漏检更多卡片;而更低的阈值虽然能找回更多卡片,但会增加误检数量。对于语义分割,我也使用了0.75的阈值,仅保留覆盖图像面积至少0.1%的连通区域,过滤掉小的错误预测。下表中的精确率、召回率和F1分数使用轴对齐的边界框匹配(IoU ≥ 0.50)计算,适用于所有四个任务。使用这些参数,我们得到以下结果:

| 任务 | 精确率 | 召回率 | F1 | |------------------|--------|--------|------| | 检测器 | 0.950 | 0.998 | 0.974| | 旋转边界框 | 0.973 | 0.683 | 0.803| | 角点检测 | 0.978 | 0.925 | 0.951| | 语义分割 | 0.881 | 0.944 | 0.911|

总体而言,我对这些结果非常满意。模型从未见过任何真实图像,但迁移到这些图像上表现良好。根据F1分数,目标检测任务似乎表现最佳。让我们深入探讨每个任务的具体挑战。

  • 目标检测

这些实验中反复出现的主题是模型会检测到背景或牌组中的物体。由于我们的图像使用了纯色背景,模型并未学习到背景中可能出现的更复杂物体(例如,它会将背景中带有白色加热器的椅子误认为卡片)。对于矩形边界框预测任务,旋转/倾斜卡片的问题尤为突出,因为它们难以适应正交矩形。这也是为什么需要为边界框添加旋转功能以实现更好对齐的主要原因。

  • 旋转边界框检测

对于旋转边界框,我们观察到一些不同的失败模式。一些倾斜的卡片仍然无法被旋转矩形很好地捕捉。感觉边界框旋转的学习信号并不理想,这个任务仍有改进空间。旋转角度±180度之间的模糊性可能加剧了这个问题。此外,模型还漏检了一些卡片,因此添加旋转目标使其对某些卡片的检测信心降低。

  • 角点检测

角点检测在处理点的顺序时遇到了不少困难。在上面的例子中,一些预测结果接近实际的角点,但按照预测顺序连接这些点会导致交叉的边。在另一种情况下,两个点最终靠近同一个角点,导致多边形塌陷。预测四个点可以为我们提供更多灵活性以适应透视变换,但我们也需要对这些点进行一致的分配。即使卡片看起来几乎完全相同,微小的旋转也可能改变排序中第一个角点的位置。

  • 语义分割

通过语义分割,我们通过预测哪些像素属于卡片来避免角点排序问题。这种方法在真实图像上的效果出乎意料地好。在这里我们看到了一些熟悉的问题:椅子的部分被分类为前景,模型对卡片组也有响应,尽管掩码中存在一些缺口。预测的边界也不总是完全遵循卡片边缘。另一个限制是所有卡片共享相同的前景类别。如果两张卡片接触或重叠,它们可能形成一个连通区域,因此我们需要额外的步骤将它们分离为单独的卡片,这就是实例分割要解决的问题。

总结

在本项目中,我们生成了一个包含多种计算机视觉任务标注的合成集卡数据集,并在冻结的DINOv3主干网络上训练了不同的头部模型。我们发现预测目标的选择带来了各自的挑战,从将旋转的卡片拟合到矩形中,到一致地排序角点或在前景掩码中分离卡片。我很高兴这些模型在没有使用任何特定任务的真实世界数据进行训练的情况下,就能很好地迁移到真实图像中。在模型训练方面仍有大量改进空间,以缩小不同任务之间的差距。一个有趣的后续挑战是处理卡片的部分遮挡,目前我们明确不生成重叠的卡片。另一个自然的下一步是识别卡片特征,这样我们就可以从找到单张卡片过渡到找到实际的三元组,真正让计算机"玩"这款游戏。

···

如需了解更多细节以及完整的实现代码(包括数据集生成、增强、模型训练和评估的代码),请查看该项目在GitHub上的源代码。

本文中的所有可视化均由作者创建。