Files
ModelTrainEval/datasets/target_encoder.py
T
Shaun ZhouandClaude Opus 5 af22dc639c feat(datasets): YOLO 风格标注数据集与几何/像素增强
- yolo: images/ 与同层 labels/ 的路径约定、`class_id cx cy w h` 归一化行解析
  (兼容分割多边形取外接矩形)、data.yaml 读写;缺失或空 txt 即无缺陷样本
- film_defect: 抛出图像、框、图像级标签;label_mode 决定多热向量还是互斥下标
- transforms: 图框同步的 Resize / Letterbox / CenterCrop / RandomResizedCrop /
  翻转 / ColorJitter / GaussianBlur / PerImageStandardize
- target_encoder: 高斯半径与 heatmap / wh / offset / mask 监督信号编码

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:55:20 +08:00

112 lines
3.8 KiB
Python

"""把框标注编码成 CenterHead 需要的监督信号。"""
from __future__ import annotations
import numpy as np
import torch
def gaussian_radius(height: float, width: float, min_overlap: float = 0.7) -> float:
"""CenterNet 的半径公式:保证半径内的框与 GT 的 IoU >= min_overlap。"""
a1 = 1.0
b1 = height + width
c1 = width * height * (1 - min_overlap) / (1 + min_overlap)
r1 = (b1 - np.sqrt(max(b1**2 - 4 * a1 * c1, 0.0))) / (2 * a1)
a2 = 4.0
b2 = 2 * (height + width)
c2 = (1 - min_overlap) * width * height
r2 = (b2 - np.sqrt(max(b2**2 - 4 * a2 * c2, 0.0))) / (2 * a2)
a3 = 4 * min_overlap
b3 = -2 * min_overlap * (height + width)
c3 = (min_overlap - 1) * width * height
r3 = (-b3 + np.sqrt(max(b3**2 - 4 * a3 * c3, 0.0))) / (2 * a3)
return max(0.0, min(r1, r2, r3))
def draw_gaussian(heatmap: np.ndarray, center: tuple[int, int], radius: int) -> None:
"""在 heatmap 上叠加二维高斯(取 max,避免相邻目标互相削弱)。"""
diameter = 2 * radius + 1
sigma = diameter / 6.0
grid = np.arange(-radius, radius + 1, dtype=np.float32)
gaussian = np.exp(-(grid[None, :] ** 2 + grid[:, None] ** 2) / (2 * sigma**2))
cx, cy = center
height, width = heatmap.shape
left, right = min(cx, radius), min(width - cx, radius + 1)
top, bottom = min(cy, radius), min(height - cy, radius + 1)
if right <= -left or bottom <= -top:
return
masked_heatmap = heatmap[cy - top : cy + bottom, cx - left : cx + right]
masked_gaussian = gaussian[radius - top : radius + bottom, radius - left : radius + right]
np.maximum(masked_heatmap, masked_gaussian, out=masked_heatmap)
class CenterTargetEncoder:
"""生成 heatmap / wh / offset / mask 四个监督张量。
Args:
num_classes: 类别数。
stride: 模型输出相对输入的下采样倍数。
max_objects: 单图最多编码的目标数。
min_overlap: 高斯半径的 IoU 阈值。
"""
def __init__(
self,
num_classes: int,
stride: int,
max_objects: int = 64,
min_overlap: float = 0.7,
) -> None:
self.num_classes = num_classes
self.stride = stride
self.max_objects = max_objects
self.min_overlap = min_overlap
def __call__(
self,
boxes: np.ndarray,
labels: np.ndarray,
image_size: tuple[int, int],
) -> dict[str, torch.Tensor]:
img_h, img_w = image_size
out_h, out_w = img_h // self.stride, img_w // self.stride
heatmap = np.zeros((self.num_classes, out_h, out_w), dtype=np.float32)
wh = np.zeros((2, out_h, out_w), dtype=np.float32)
offset = np.zeros((2, out_h, out_w), dtype=np.float32)
mask = np.zeros((out_h, out_w), dtype=np.float32)
count = 0
for box, label in zip(np.asarray(boxes, dtype=np.float32), np.asarray(labels), strict=True):
if count >= self.max_objects:
break
x1, y1, x2, y2 = box / self.stride
box_w, box_h = x2 - x1, y2 - y1
if box_w <= 0 or box_h <= 0:
continue
cx, cy = (x1 + x2) / 2, (y1 + y2) / 2
cx_int, cy_int = int(cx), int(cy)
if not (0 <= cx_int < out_w and 0 <= cy_int < out_h):
continue
radius = max(1, int(gaussian_radius(box_h, box_w, self.min_overlap)))
draw_gaussian(heatmap[int(label)], (cx_int, cy_int), radius)
wh[:, cy_int, cx_int] = (box_w, box_h)
offset[:, cy_int, cx_int] = (cx - cx_int, cy - cy_int)
mask[cy_int, cx_int] = 1.0
count += 1
return {
"heatmap": torch.from_numpy(heatmap),
"wh": torch.from_numpy(wh),
"offset": torch.from_numpy(offset),
"mask": torch.from_numpy(mask),
}