Buckets:
| """Train<->inference-gap augmentations: pose noise, mask dilate/erode, channel dropout. | |
| Every function here is pure numpy/OpenCV (no torch), takes an explicit | |
| ``numpy.random.Generator``, and is independently toggleable -- see | |
| :class:`fpgm.training.types.AugmentConfig`. :func:`apply_augmentations` is the | |
| single entry point :mod:`fpgm.training.dataset` calls; the three primitives | |
| below are exported separately so each can be unit-tested in isolation against | |
| hand-built synthetic arrays (no filesystem, no GPU). | |
| """ | |
| from __future__ import annotations | |
| import cv2 | |
| import numpy as np | |
| from fpgm.training.types import AugmentConfig | |
| def pose_noise( | |
| control: np.ndarray, | |
| fg_mask: np.ndarray, | |
| rng: np.random.Generator, | |
| translate_px_std: float, | |
| rotate_deg_std: float, | |
| ) -> np.ndarray: | |
| """Rigid-jitter the foreground region of the composited control video only. | |
| **Why a 2D affine warp of the raster, not a 3D pose perturbation re-rendered | |
| through meshes.** True pose noise (perturb ``T_world_obj`` by +-2mm/+-1deg, | |
| re-render depth/seg/normal) needs the mesh + camera-intrinsics + pyrender | |
| machinery that ``fpgm.datagen.export_vace`` (a sibling agent's owned | |
| module, not duplicated here) already owns, and pulling that dependency | |
| into a lightweight Dataset augmentation would mean re-instantiating an | |
| OSMesa-backed GPU/CPU renderer per training sample -- disproportionate to | |
| what this augmentation needs to achieve. A small rigid affine warp of the | |
| already-rendered foreground pixels is a raster-level approximation of the | |
| same effect (an object rendered from a slightly-off pose looks, to first | |
| order, like the correctly-rendered object translated/rotated a few | |
| pixels) -- it will not reproduce genuine parallax/occlusion changes a | |
| real re-render would, but its purpose is only to keep the model from | |
| overfitting to pixel-exact conditioning, which this achieves. | |
| One random affine draw per call, applied identically to every frame in | |
| the window (not redrawn per frame) -- a per-frame-random jitter would | |
| look like flicker no real pose-estimation error produces; a real pose | |
| bias is temporally coherent within a short window. | |
| Args: | |
| control: ``(T, H, W, 3)`` uint8 composited control video. | |
| fg_mask: ``(T, H, W)`` bool, the region allowed to move (background | |
| pixels are left untouched regardless of the warp). | |
| rng: source of randomness. | |
| translate_px_std: stddev of the (independent x/y) pixel translation. | |
| rotate_deg_std: stddev of the in-plane rotation, degrees. | |
| Returns: | |
| A new ``(T, H, W, 3)`` uint8 array; ``control`` is not modified in place. | |
| """ | |
| if control.shape[:3] != fg_mask.shape: | |
| raise ValueError( | |
| f"control {control.shape[:3]} and fg_mask {fg_mask.shape} disagree on (T,H,W)" | |
| ) | |
| t_frames, h, w = fg_mask.shape | |
| dx = float(rng.normal(0.0, translate_px_std)) | |
| dy = float(rng.normal(0.0, translate_px_std)) | |
| angle_deg = float(rng.normal(0.0, rotate_deg_std)) | |
| center = (w / 2.0, h / 2.0) | |
| matrix = cv2.getRotationMatrix2D(center, angle_deg, 1.0) | |
| matrix[0, 2] += dx | |
| matrix[1, 2] += dy | |
| out = control.copy() | |
| for t in range(t_frames): | |
| if not fg_mask[t].any(): | |
| continue | |
| warped_frame = cv2.warpAffine( | |
| control[t], matrix, (w, h), flags=cv2.INTER_NEAREST, borderMode=cv2.BORDER_REPLICATE | |
| ) | |
| warped_mask = cv2.warpAffine( | |
| fg_mask[t].astype(np.uint8) * 255, matrix, (w, h), | |
| flags=cv2.INTER_NEAREST, borderMode=cv2.BORDER_CONSTANT, borderValue=0, | |
| ) > 127 | |
| combined = fg_mask[t] | warped_mask # cover both the vacated and the arrived-at footprint | |
| out[t] = np.where(combined[..., None], warped_frame, control[t]) | |
| return out | |
| def mask_dilate_erode( | |
| fg_mask: np.ndarray, rng: np.random.Generator, max_kernel_px: int | |
| ) -> np.ndarray: | |
| """Randomly dilate, erode, or leave ``fg_mask`` unchanged. | |
| One draw per call (op choice + kernel size), applied identically to every | |
| frame -- same temporal-coherence argument as :func:`pose_noise`: a mask | |
| generator's systematic bias (consistently a few px too tight/loose) looks | |
| like a fixed dilate/erode, not per-frame flicker. | |
| Args: | |
| fg_mask: ``(T, H, W)`` bool. | |
| rng: source of randomness. | |
| max_kernel_px: inclusive upper bound on the structuring element's | |
| half-width; ``0`` degenerates to always-unchanged. | |
| Returns: | |
| A new ``(T, H, W)`` bool array. | |
| """ | |
| if max_kernel_px <= 0: | |
| return fg_mask.copy() | |
| op = rng.choice(("dilate", "erode", "none")) | |
| if op == "none": | |
| return fg_mask.copy() | |
| kernel_px = int(rng.integers(1, max_kernel_px + 1)) | |
| kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (2 * kernel_px + 1, 2 * kernel_px + 1)) | |
| fn = cv2.dilate if op == "dilate" else cv2.erode | |
| out = np.empty_like(fg_mask) | |
| for t in range(fg_mask.shape[0]): | |
| out[t] = fn(fg_mask[t].astype(np.uint8), kernel) > 0 | |
| return out | |
| def channel_dropout(control: np.ndarray, rng: np.random.Generator, prob: float) -> np.ndarray: | |
| """Independently zero each of the 3 composited channels with probability ``prob``. | |
| Whole-window, not per-frame (same temporal-coherence reasoning as the | |
| other two augmentations): this simulates "this render channel was | |
| unavailable/degenerate for this clip", not a per-frame glitch. | |
| Args: | |
| control: ``(T, H, W, 3)`` uint8. | |
| rng: source of randomness. | |
| prob: independent per-channel dropout probability. | |
| Returns: | |
| A new ``(T, H, W, 3)`` uint8 array. | |
| """ | |
| out = control.copy() | |
| for c in range(control.shape[-1]): | |
| if rng.random() < prob: | |
| out[..., c] = 0 | |
| return out | |
| def apply_augmentations( | |
| control: np.ndarray, fg_mask: np.ndarray, cfg: AugmentConfig, sample_index: int | |
| ) -> tuple[np.ndarray, np.ndarray]: | |
| """Apply the enabled augmentations in :class:`~fpgm.training.types.AugmentConfig` order. | |
| Order: mask dilate/erode first (its output is both returned and used as | |
| the foreground region for pose noise, so a systematically-off mask and a | |
| pose jitter compose the way they would in reality), then pose noise, then | |
| channel dropout last (it should be able to zero a channel pose noise just | |
| perturbed). | |
| ``target``/``ref_plate``/``caption`` are never touched here: the real | |
| video is ground truth and the reference plate is a real photograph -- | |
| both must reach the model unmodified for the loss/context to mean | |
| anything (only the *rendered control conditioning* is uncertain enough | |
| at inference time to warrant jitter). | |
| Args: | |
| control: ``(T, H, W, 3)`` uint8 composited control video. | |
| fg_mask: ``(T, H, W)`` bool. | |
| cfg: which augmentations are enabled and their magnitudes. | |
| sample_index: mixed with ``cfg.seed`` to derive this call's RNG, so a | |
| given (seed, index) is reproducible across runs/workers while | |
| different indices/epochs draw independently. | |
| Returns: | |
| ``(control_aug, fg_mask_aug)``. | |
| """ | |
| rng = np.random.default_rng((cfg.seed, sample_index)) | |
| mask_aug = fg_mask | |
| if cfg.enable_mask_dilate_erode: | |
| mask_aug = mask_dilate_erode(mask_aug, rng, cfg.mask_dilate_erode_max_px) | |
| control_aug = control | |
| if cfg.enable_pose_noise: | |
| translate_px_std = cfg.pose_noise_translate_mm_std * cfg.pose_noise_px_per_mm | |
| control_aug = pose_noise( | |
| control_aug, mask_aug, rng, translate_px_std, cfg.pose_noise_rotate_deg_std | |
| ) | |
| if cfg.enable_channel_dropout: | |
| control_aug = channel_dropout(control_aug, rng, cfg.channel_dropout_prob) | |
| return control_aug, mask_aug | |
Xet Storage Details
- Size:
- 7.85 kB
- Xet hash:
- dd4d6e7335123db9cbd8bb15ab695d55208c07619aa3f156985ba9065f8b6cdb
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.