"""学習・評価用の Dataset と入力の正規化(SPEC_02「データ拡張」「前処理」).
Dataset はレターボックス済みの uint8 画像を返し,正規化(ImageNet の mean / std)はデバイス上で
`normalize_batch` が行う(CPU のワーカーで float 化するより転送量が小さいため).
データ拡張は学習時のみ・幾何変換のみ(左右反転・回転・拡大縮小・平行移動.はみ出しは黒).
舌の色そのものがラベルであるため,**色を変える拡張は行わない**.
"""
from __future__ import annotations
import numpy as np
import torch
from torch.utils.data import Dataset
from torchvision.transforms import v2
IMAGENET_MEAN: tuple[float, float, float] = (0.485, 0.456, 0.406)
IMAGENET_STD: tuple[float, float, float] = (0.229, 0.224, 0.225)
# 幾何拡張の強さ(SPEC_02「学習の設定」の箇条書き).
_FLIP_PROB = 0.5
_ROTATION_DEGREES = 10.0
_SCALE_RANGE = (0.9, 1.1)
_TRANSLATE_RATIO = (0.05, 0.05)
def make_train_augmentation() -> v2.Compose:
"""学習時の幾何拡張(左右反転・回転 ±10°・拡大縮小 0.9〜1.1・平行移動 ±5%)を作る.
はみ出した部分は黒 (0) で埋める.色を変える変換は含めない.
Returns:
`(3, H, W)` の uint8 テンソルを受け取り同じ形状で返す変換.
"""
return v2.Compose(
[
v2.RandomHorizontalFlip(p=_FLIP_PROB),
v2.RandomAffine(
degrees=_ROTATION_DEGREES,
translate=_TRANSLATE_RATIO,
scale=_SCALE_RANGE,
interpolation=v2.InterpolationMode.BILINEAR,
fill=0,
),
]
)
class TongueImageDataset(Dataset):
"""レターボックス済み画像とクラス ID の Dataset.
Args:
images: `(N, H, W, 3)` の uint8 配列(RGB).
labels: `(N,)` の整数配列(クラス ID.`classes.py` の固定 ID).
augment: True なら学習時の幾何拡張をかける.
"""
def __init__(self, images: np.ndarray, labels: np.ndarray, *, augment: bool) -> None:
if len(images) != len(labels):
raise RuntimeError("画像とラベルの件数が一致しない")
self._images = images
self._labels = labels.astype(np.int64)
self._augment = make_train_augmentation() if augment else None
def __len__(self) -> int:
return len(self._labels)
def __getitem__(self, index: int) -> tuple[torch.Tensor, int]:
"""1 件を返す.
Returns:
(`(3, H, W)` の uint8 テンソル, クラス ID).
"""
image = torch.from_numpy(np.ascontiguousarray(self._images[index])).permute(2, 0, 1)
if self._augment is not None:
image = self._augment(image)
return image, int(self._labels[index])
def normalize_batch(images: torch.Tensor) -> torch.Tensor:
"""uint8 画像のバッチを float にして ImageNet の mean / std で正規化する.
Args:
images: `(B, 3, H, W)` の uint8 テンソル(任意のデバイス).
Returns:
`(B, 3, H, W)` の float32 テンソル(同じデバイス).
"""
mean = torch.tensor(IMAGENET_MEAN, device=images.device).view(1, 3, 1, 1)
std = torch.tensor(IMAGENET_STD, device=images.device).view(1, 3, 1, 1)
return (images.float() / 255.0 - mean) / std